
package org.jgroups.blocks;


import org.jgroups.*;
import org.jgroups.annotations.Unsupported;
import org.jgroups.jmx.JmxConfigurator;
import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;
import org.jgroups.util.Util;

import javax.management.MBeanServer;
import java.io.*;
import java.util.*;




/**
 * A tree-like structure that is replicated across several members. Updates will be multicast to all group
 * members reliably and in the same order.
 * @author Bela Ban Jan 17 2002
 * @author <a href="mailto:aolias@yahoo.com">Alfonso Olias-Sanz</a>
 */
@Unsupported
public class ReplicatedTree extends ReceiverAdapter {
    public static final String SEPARATOR="/";
    final static int INDENT=4;
    Node root=new Node(SEPARATOR, SEPARATOR, null, null);
    final Vector<ReplicatedTreeListener> listeners=new Vector<ReplicatedTreeListener>();
    JChannel channel=null;
    String groupname="ReplicatedTree-Group";
    final List<Address> members=new ArrayList<Address>();
    long state_fetch_timeout=10000;
    boolean jmx=false;

    protected final Log log=LogFactory.getLog(this.getClass());


    /** Whether or not to use remote calls. If false, all methods will be invoked directly on this
     instance rather than sending a message to all replicas and only then invoking the method.
     Useful for testing */
    boolean remote_calls=true;
    String props="udp.xml";

	/** Determines when the updates have to be sent across the network, avoids sending unnecessary
     * messages when there are no member in the group */
	private boolean send_message = false;



    public interface ReplicatedTreeListener {
        void nodeAdded(String fqn);

        void nodeRemoved(String fqn);

        void nodeModified(String fqn);

        void viewChange(View new_view);  // might be MergeView after merging
    }


    /**
     * Creates a channel with the given properties. Connects to the channel, then creates a PullPushAdapter
     * and starts it
     */
    public ReplicatedTree(String groupname, String props, long state_fetch_timeout) throws Exception {
        if(groupname != null)
            this.groupname=groupname;
        if(props != null)
            this.props=props;
        this.state_fetch_timeout=state_fetch_timeout;
        channel=new JChannel(this.props);
        channel.setReceiver(this);
        channel.connect(this.groupname);
        start();
    }

    public ReplicatedTree(String groupname, String props, long state_fetch_timeout, boolean jmx) throws Exception {
        if(groupname != null)
            this.groupname=groupname;
        if(props != null)
            this.props=props;
        this.jmx=jmx;
        this.state_fetch_timeout=state_fetch_timeout;
        channel=new JChannel(this.props);
        channel.setReceiver(this);
        channel.connect(this.groupname);

        if(jmx) {
            MBeanServer server=Util.getMBeanServer();
            if(server == null)
                throw new Exception("No MBeanServers found; need to run with an MBeanServer present, or inside JDK 5");
            JmxConfigurator.registerChannel(channel, server, "jgroups", channel.getClusterName() , true);
        }
        start();
    }

    public ReplicatedTree() {
    }


    /**
     * Expects an already connected channel. Creates a PullPushAdapter and starts it
     */
    public ReplicatedTree(JChannel channel) throws Exception {
        this.channel=channel;
        channel.setReceiver(this);
        viewAccepted(channel.getView());
        start();
    }


    public void setRemoteCalls(boolean flag) {
        remote_calls=flag;
    }

    public void setRootNode(Node n) {
        root=n;
    }

    public Address getLocalAddress() {
        return channel != null? channel.getAddress() : null;
    }

    public List<Address> getMembers() {
        return members;
    }


    /**
     * Fetch the group state from the current coordinator. If successful, this will trigger setState().
     */
    public void fetchState(long timeout) throws Exception {
        channel.getState(null, timeout);
    }


    public void addReplicatedTreeListener(ReplicatedTreeListener listener) {
        if(!listeners.contains(listener))
            listeners.addElement(listener);
    }


    public void removeReplicatedTreeListener(ReplicatedTreeListener listener) {
        listeners.removeElement(listener);
    }


    public final void start() throws Exception {
        channel.getState(null, state_fetch_timeout);
    }


    public void stop() {
        Util.close(channel);
    }


    /**
     * Adds a new node to the tree and sets its data. If the node doesn not yet exist, it will be created.
     * Also, parent nodes will be created if not existent. If the node already has data, then the new data
     * will override the old one. If the node already existed, a nodeModified() notification will be generated.
     * Otherwise a nodeCreated() motification will be emitted.
     * @param fqn The fully qualified name of the new node
     * @param data The new data. May be null if no data should be set in the node.
     */
    public void put(String fqn, HashMap data) {
        if(!remote_calls) {
            _put(fqn, data);
            return;
        }

		//Changes done by <aos>
		//if true, propagate action to the group
        if(send_message == true) {
            if(channel == null) {
                if(log.isErrorEnabled()) log.error("channel is null, cannot broadcast PUT request");
                return;
            }
            try {
                channel.send(
                        new Message(
                                null,
                                null,
                                new Request(Request.PUT, fqn, data)));
            }
            catch(Exception ex) {
                if(log.isErrorEnabled()) log.error("failure bcasting PUT request: " + ex);
            }
        }
        else {
            _put(fqn, data);
        }
    }


    /**
     * Adds a key and value to a given node. If the node doesn't exist, it will be created. If the node
     * already existed, a nodeModified() notification will be generated. Otherwise a
     * nodeCreated() motification will be emitted.
     * @param fqn The fully qualified name of the node
     * @param key The key
     * @param value The value
     */
    public void put(String fqn, String key, Object value) {
        if(!remote_calls) {
            _put(fqn, key, value);
            return;
        }

        //Changes done by <aos>
        //if true, propagate action to the group
        if(send_message == true) {

            if(channel == null) {
                if(log.isErrorEnabled()) log.error("channel is null, cannot broadcast PUT request");
                return;
            }
            try {
                channel.send(
                        new Message(
                                null,
                                null,
                                new Request(Request.PUT, fqn, key, value)));
            }
            catch(Exception ex) {
                if(log.isErrorEnabled()) log.error("failure bcasting PUT request: " + ex);
            }
        }
        else {
            _put(fqn, key, value);
        }
    }


    /**
     * Removes the node from the tree.
     * @param fqn The fully qualified name of the node.
     */
    public void remove(String fqn) {
        if(!remote_calls) {
            _remove(fqn);
            return;
        }
		//Changes done by <aos>
		//if true, propagate action to the group
        if(send_message == true) {
            if(channel == null) {
                if(log.isErrorEnabled()) log.error("channel is null, cannot broadcast REMOVE request");
                return;
            }
            try {
                channel.send(
                        new Message(null, null, new Request(Request.REMOVE, fqn)));
            }
            catch(Exception ex) {
                if(log.isErrorEnabled()) log.error("failure bcasting REMOVE request: " + ex);
            }
        }
        else {
            _remove(fqn);
        }
    }


    /**
     * Removes <code>key</code> from the node's hashmap
     * @param fqn The fullly qualified name of the node
     * @param key The key to be removed
     */
    public void remove(String fqn, String key) {
        if(!remote_calls) {
            _remove(fqn, key);
            return;
        }
		//Changes done by <aos>
		//if true, propagate action to the group
        if(send_message == true) {
            if(channel == null) {
                if(log.isErrorEnabled()) log.error("channel is null, cannot broadcast REMOVE request");
                return;
            }
            try {
                channel.send(
                        new Message(
                                null,
                                null,
                                new Request(Request.REMOVE, fqn, key)));
            }
            catch(Exception ex) {
                if(log.isErrorEnabled()) log.error("failure bcasting REMOVE request: " + ex);
            }
        }
        else {
            _remove(fqn, key);
        }
    }


    /**
     * Checks whether a given node exists in the tree
     * @param fqn The fully qualified name of the node
     * @return boolean Whether or not the node exists
     */
    public boolean exists(String fqn) {
        if(fqn == null) return false;
        return findNode(fqn) != null;
    }


    /**
     * Gets the keys of the <code>data</code> map. Returns all keys as Strings. Returns null if node
     * does not exist.
     * @param fqn The fully qualified name of the node
     * @return Set A set of keys (as Strings)
     */
    public Set getKeys(String fqn) {
        Node n=findNode(fqn);
        Map data;

        if(n == null) return null;
        data=n.getData();
        if(data == null) return null;
        return data.keySet();
    }


    /**
     * Finds a node given its name and returns the value associated with a given key in its <code>data</code>
     * map. Returns null if the node was not found in the tree or the key was not found in the hashmap.
     * @param fqn The fully qualified name of the node.
     * @param key The key.
     */
    public Object get(String fqn, String key) {
        Node n=findNode(fqn);

        if(n == null) return null;
        return n.getData(key);
    }


    /**
     * Returns the data hashmap for a given node. This method can only be used by callers that are inside
     * the same package. The reason is that callers must not modify the return value, as these modifications
     * would not be replicated, thus rendering the replicas inconsistent.
     * @param fqn The fully qualified name of the node
     * @return HashMap The data hashmap for the given node
     */
    Map<String,Object> get(String fqn) {
        Node n=findNode(fqn);

        if(n == null) return null;
        return n.getData();
    }


    /**
     * Prints a representation of the node defined by <code>fqn</code>. Output includes name, fqn and
     * data.
     */
    public String print(String fqn) {
        Node n=findNode(fqn);
        if(n == null) return null;
        return n.toString();
    }


    /**
     * Returns all children of a given node
     * @param fqn The fully qualified name of the node
     * @return Set A list of child names (as Strings)
     */
    public Set getChildrenNames(String fqn) {
        Node n=findNode(fqn);
        Map m;

        if(n == null) return null;
        m=n.getChildren();
        if(m != null)
            return m.keySet();
        else
            return null;
    }


    public String toString() {
        StringBuilder sb=new StringBuilder();
        int indent=0;
        Map children;

        children=root.getChildren();
        if(children != null && children.size() > 0) {
            Collection nodes=children.values();
            for(Iterator it=nodes.iterator(); it.hasNext();) {
                ((Node)it.next()).print(sb, indent);
                sb.append('\n');
            }
        }
        else
            sb.append(SEPARATOR);
        return sb.toString();
    }

   /**
    * Returns the name of the group that the DistributedTree is connected to
    * @return String
    */
    public String  getGroupName()           {return groupname;}
	 	
   /**
    * Returns the Channel the DistributedTree is connected to 
    * @return Channel
    */
    public Channel getChannel()             {return channel;}

   /**
    * Returns the number of current members joined to the group
    * @return int
    */
    public int getGroupMembersNumber()			{return members.size();}




    /* --------------------- Callbacks -------------------------- */


    public void _put(String fqn, HashMap data) {
        Node n;
        StringHolder child_name=new StringHolder();
        boolean child_exists=false;

        if(fqn == null) return;
        n=findParentNode(fqn, child_name, true); // create all nodes if they don't exist
        if(child_name.getValue() != null) {
            child_exists=n.childExists(child_name.getValue());
            n.createChild(child_name.getValue(), fqn, n, data);
        }
        else {
            child_exists=true;
            n.setData(data);
        }
        if(child_exists)
            notifyNodeModified(fqn);
        else
            notifyNodeAdded(fqn);
    }


    public void _put(String fqn, String key, Object value) {
        Node n;
        StringHolder child_name=new StringHolder();
        boolean child_exists=false;

        if(fqn == null || key == null || value == null) return;
        n=findParentNode(fqn, child_name, true);
        if(child_name.getValue() != null) {
            child_exists=n.childExists(child_name.getValue());
            n.createChild(child_name.getValue(), fqn, n, key, value);
        }
        else {
            child_exists=true;
            n.setData(key, value);
        }
        if(child_exists)
            notifyNodeModified(fqn);
        else
            notifyNodeAdded(fqn);
    }


    public void _remove(String fqn) {
        Node n;
        StringHolder child_name=new StringHolder();

        if(fqn == null) return;
        if(fqn.equals(SEPARATOR)) {
            root.removeAll();
            notifyNodeRemoved(fqn);
            return;
        }
        n=findParentNode(fqn, child_name, false);
        if(n == null) return;
        n.removeChild(child_name.getValue(), fqn);
        notifyNodeRemoved(fqn);
    }


    public void _remove(String fqn, String key) {
        Node n;

        if(fqn == null || key == null) return;
        n=findNode(fqn);
        if(n != null)
            n.removeData(key);
    }


    public void _removeData(String fqn) {
        Node n;

        if(fqn == null) return;
        n=findNode(fqn);
        if(n != null)
            n.removeData();
    }


    /* ----------------- End of  Callbacks ---------------------- */






    /*-------------------- MessageListener ----------------------*/

    /** Callback. Process the contents of the message; typically an _add() or _set() request */
    public void receive(Message msg) {
        Request req=null;

        if(msg == null || msg.getLength() == 0)
            return;
        try {
            req=(Request)msg.getObject();

            String fqn=req.fqn;
            switch(req.type) {
                case Request.PUT:
                    if(req.key != null && req.value != null)
                        _put(fqn, req.key, req.value);
                    else
                        _put(fqn, req.data);
                    break;
                case Request.REMOVE:
                    if(req.key != null)
                        _remove(fqn, req.key);
                    else
                        _remove(fqn);
                    break;
                default:
                    if(log.isErrorEnabled()) log.error("type " + req.type + " unknown");
                    break;
            }
        }
        catch(Exception ex) {
            if(log.isErrorEnabled()) log.error("failed unmarshalling request: " + ex);
        }
    }


    public void getState(OutputStream ostream) throws Exception {
        Util.objectToStream(root.clone(), new DataOutputStream(ostream));
    }


    public void setState(InputStream istream) throws Exception {
        Object obj=Util.objectFromStream(new DataInputStream(istream));
        root=(Node)((Node)obj).clone();
        notifyAllNodesCreated(root);
    }

    /*-------------------- End of MessageListener ----------------------*/





    /*----------------------- MembershipListener ------------------------*/

    public void viewAccepted(View new_view) {
        List<Address> new_mbrs=new_view.getMembers();

        // todo: if MergeView, fetch and reconcile state from coordinator
        // actually maybe this is best left up to the application ? we just notify them and let the appl handle it ?

        if(new_mbrs != null) {
            notifyViewChange(new_view);
            members.clear();
            members.addAll(new_mbrs);
        }
		//if size is bigger than one, there are more peers in the group
		//otherwise there is only one server.
        send_message=members.size() > 1;
    }

    /*------------------- End of MembershipListener ----------------------*/




    /**
     * Find the node just <em>above</em> the one indicated by <code>fqn</code>. This is needed in many cases,
     * e.g. to add a new node or remove an existing node.
     * @param fqn The fully qualified name of the node.
     * @param child_name Will be filled with the name of the child when this method returns. The child name
     *                   is the last relative name of the <code>fqn</code>, e.g. in "/a/b/c" it would be "c".
     * @param create_if_not_exists Create parent nodes along the way if they don't exist. Otherwise, this method
     *                             will return when a node cannot be found.
     */
    Node findParentNode(String fqn, StringHolder child_name, boolean create_if_not_exists) {
        Node curr=root, node;
        StringTokenizer tok;
        String name;
        StringBuilder sb=null;

        if(fqn == null || fqn.equals(SEPARATOR) || "".equals(fqn))
            return curr;

        sb=new StringBuilder();
        tok=new StringTokenizer(fqn, SEPARATOR);
        while(tok.countTokens() > 1) {
            name=tok.nextToken();
            sb.append(SEPARATOR).append(name);
            node=curr.getChild(name);
            if(node == null && create_if_not_exists)
                node=curr.createChild(name, sb.toString(), null, null);
            if(node == null)
                return null;
            else
                curr=node;
        }

        if(tok.countTokens() > 0 && child_name != null)
            child_name.setValue(tok.nextToken());
        return curr;
    }


    /**
     * Returns the node at fqn. This method should not be used by clients (therefore it is package-private):
     * it is only used internally (for navigation). C++ 'friend' would come in handy here...
     * @param fqn The fully qualified name of the node
     * @return Node The node at fqn
     */
    Node findNode(String fqn) {
        StringHolder sh=new StringHolder();
        Node n=findParentNode(fqn, sh, false);
        String child_name=sh.getValue();

        if(fqn == null || fqn.equals(SEPARATOR) || "".equals(fqn))
            return root;

        if(n == null || child_name == null)
            return null;
        else
            return n.getChild(child_name);
    }


    void notifyNodeAdded(String fqn) {
        for(int i=0; i < listeners.size(); i++)
            listeners.elementAt(i).nodeAdded(fqn);
    }

    void notifyNodeRemoved(String fqn) {
        for(int i=0; i < listeners.size(); i++)
            listeners.elementAt(i).nodeRemoved(fqn);
    }

    void notifyNodeModified(String fqn) {
        for(int i=0; i < listeners.size(); i++)
            listeners.elementAt(i).nodeModified(fqn);
    }

    void notifyViewChange(View v) {
        for(int i=0; i < listeners.size(); i++)
            listeners.elementAt(i).viewChange(v);
    }

    /** Generates NodeAdded notifications for all nodes of the tree. This is called whenever the tree is
     initially retrieved (state transfer) */
    void notifyAllNodesCreated(Node curr) {
        Node n;
        Map children;

        if(curr == null) return;
        notifyNodeAdded(curr.fqn);
        if((children=curr.getChildren()) != null) {
            for(Iterator it=children.values().iterator(); it.hasNext();) {
                n=(Node)it.next();
                notifyAllNodesCreated(n);
            }
        }
    }


    public static class Node implements Serializable {
        String name=null;     // relative name (e.g. "Security")
        String fqn=null;      // fully qualified name (e.g. "/federations/fed1/servers/Security")
        Node parent=null;   // parent node
        TreeMap<String,Node> children=null; // keys: child name, value: Node
        Map<String,Object> data=null;     // data for current node
        private static final long serialVersionUID = -3077676554440038890L;


        private Node(String child_name, String fqn, Node parent, Map<String,Object> data) {
            name=child_name;
            this.fqn=fqn;
            this.parent=parent;
            if(data != null) this.data=(HashMap<String,Object>)((HashMap)data).clone();
        }

        private Node(String child_name, String fqn, Node parent, String key, Object value) {
            name=child_name;
            this.fqn=fqn;
            this.parent=parent;
            if(data == null) data=new HashMap<String,Object>();
            data.put(key, value);
        }

        void setData(Map data) {
            if(data == null) return;
            if(this.data == null)
                this.data=new HashMap<String,Object>();
            this.data.putAll(data);
        }

        void setData(String key, Object value) {
            if(this.data == null)
                this.data=new HashMap<String,Object>();
            this.data.put(key, value);
        }

        Map<String,Object> getData() {
            return data;
        }

        Object getData(String key) {
            return data != null? data.get(key) : null;
        }


        boolean childExists(String child_name) {
            return child_name != null && children != null && children.containsKey(child_name);
        }


        Node createChild(String child_name, String fqn, Node parent, HashMap<String,Object> data) {
            Node child=null;

            if(child_name == null) return null;
            if(children == null) children=new TreeMap<String,Node>();
            child=children.get(child_name);
            if(child != null)
                child.setData(data);
            else {
                child=new Node(child_name, fqn, parent, data);
                children.put(child_name, child);
            }
            return child;
        }

        Node createChild(String child_name, String fqn, Node parent, String key, Object value) {
            Node child=null;

            if(child_name == null) return null;
            if(children == null) children=new TreeMap<String,Node>();
            child=(Node)children.get(child_name);
            if(child != null)
                child.setData(key, value);
            else {
                child=new Node(child_name, fqn, parent, key, value);
                children.put(child_name, child);
            }
            return child;
        }


        Node getChild(String child_name) {
            return child_name == null? null : children == null? null : (Node)children.get(child_name);
        }

        Map<String,Node> getChildren() {
            return children;
        }

        void removeData(String key) {
            if(data != null)
                data.remove(key);
        }

        void removeData() {
            if(data != null)
                data.clear();
        }

        void removeChild(String child_name, String fqn) {
            if(child_name != null && children != null && children.containsKey(child_name)) {
                children.remove(child_name);
            }
        }

        void removeAll() {
            if(children != null)
                children.clear();
        }

        void print(StringBuilder sb, int indent) {
            printIndent(sb, indent);
            sb.append(SEPARATOR).append(name);
            if(children != null && !children.isEmpty()) {
                Collection values=children.values();
                for(Iterator it=values.iterator(); it.hasNext();) {
                    sb.append('\n');
                    ((Node)it.next()).print(sb, indent + INDENT);
                }
            }
        }

        static void printIndent(StringBuilder sb, int indent) {
            if(sb != null) {
                for(int i=0; i < indent; i++)
                    sb.append(' ');
            }
        }


        public String toString() {
            StringBuilder sb=new StringBuilder();
            if(name != null) sb.append("\nname=" + name);
            if(fqn != null) sb.append("\nfqn=" + fqn);
            if(data != null) sb.append("\ndata=" + data);
            return sb.toString();
        }


        public Object clone() throws CloneNotSupportedException {
            Node n=new Node(name, fqn, parent != null? (Node)parent.clone() : null, data);
            if(children != null) n.children=(TreeMap)children.clone();
            return n;
        }

    }


    private static class StringHolder {
        String s=null;

        private StringHolder() {
        }

        void setValue(String s) {
            this.s=s;
        }

        String getValue() {
            return s;
        }
    }


    /**
     * Class used to multicast add(), remove() and set() methods to all members.
     */
    private static class Request implements Serializable {
        static final int PUT=1;
        static final int REMOVE=2;

        int type=0;
        String fqn=null;
        String key=null;
        Object value=null;
        HashMap data=null;
        private static final long serialVersionUID = 7772753222127676782L;

        private Request(int type, String fqn) {
            this.type=type;
            this.fqn=fqn;
        }

        private Request(int type, String fqn, HashMap data) {
            this(type, fqn);
            this.data=data;
        }

        private Request(int type, String fqn, String key) {
            this(type, fqn);
            this.key=key;
        }

        private Request(int type, String fqn, String key, Object value) {
            this(type, fqn);
            this.key=key;
            this.value=value;
        }

        public String toString() {
            StringBuilder sb=new StringBuilder();
            sb.append(type2String(type)).append(" (");
            if(fqn != null) sb.append(" fqn=" + fqn);
            switch(type) {
                case PUT:
                    if(data != null) sb.append(", data=" + data);
                    if(key != null) sb.append(", key=" + key);
                    if(value != null) sb.append(", value=" + value);
                    break;
                case REMOVE:
                    if(key != null) sb.append(", key=" + key);
                    break;
                default:
                    break;
            }
            sb.append(')');
            return sb.toString();
        }

        static String type2String(int t) {
            switch(t) {
                case PUT:
                    return "PUT";
                case REMOVE:
                    return "REMOVE";
                default:
                    return "UNKNOWN";
            }
        }

    }


    public static void main(String[] args) {


        ReplicatedTree tree=null;
        HashMap m=new HashMap();
        String props;

        props="UDP(mcast_addr=224.0.0.36;mcast_port=55566;ip_ttl=32;" +
                "mcast_send_buf_size=150000;mcast_recv_buf_size=80000):" +
                "PING(timeout=2000;num_initial_members=3):" +
                "MERGE2(min_interval=5000;max_interval=10000):" +
                "FD_SOCK:" +
                "VERIFY_SUSPECT(timeout=1500):" +
                "pbcast.STABLE(desired_avg_gossip=20000):" +
                "pbcast.NAKACK(retransmit_timeout=600,1200,2400,4800):" +
                "UNICAST(timeout=5000):" +
                "FRAG(frag_size=16000;down_thread=false;up_thread=false):" +
                "pbcast.GMS(join_timeout=5000;" +
                "print_local_addr=true):" +
                "pbcast.STATE_TRANSFER";
        // "PERF(details=true)";

        try {

            tree=new ReplicatedTree(null, props, 10000);
            // tree.setRemoteCalls(false);
            tree.addReplicatedTreeListener(new MyListener());
            tree.put("/a/b/c", null);
            tree.put("/a/b/c1", null);
            tree.put("/a/b/c2", null);
            tree.put("/a/b1/chat", null);
            tree.put("/a/b1/chat2", null);
            tree.put("/a/b1/chat5", null);
            System.out.println(tree);
            m.put("name", "Bela Ban");
            m.put("age", new Integer(36));
            m.put("cube", "240-17");
            tree.put("/a/b/c", m);
            System.out.println("info for for \"/a/b/c\" is " + tree.print("/a/b/c"));
            tree.put("/a/b/c", "age", new Integer(37));
            System.out.println("info for for \"/a/b/c\" is " + tree.print("/a/b/c"));
            tree.remove("/a/b");
            System.out.println(tree);
        }
        catch(Exception ex) {
            System.err.println(ex);
        }
    }


    static class MyListener implements ReplicatedTreeListener {

        public void nodeAdded(String fqn) {
            System.out.println("** node added: " + fqn);
        }

        public void nodeRemoved(String fqn) {
            System.out.println("** node removed: " + fqn);
        }

        public void nodeModified(String fqn) {
            System.out.println("** node modified: " + fqn);
        }

        public void viewChange(View new_view) {
            System.out.println("** view change: " + new_view);
        }

    }


}


