/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2006 Helma Software. All Rights Reserved.
 *
 * $RCSfile: Group.js,v $
 * $Author: zumbrunn $
 * $Revision: 8714 $
 * $Date: 2007-12-13 13:21:48 +0100 (Don, 13. Dez 2007) $
 */

/**
 * @fileoverview A JavaScript library wrapping 
 * Packages.helma.extensions.helmagroups
 * <br /><br />
 * To use this optional module, its repository needs to be added to the 
 * application, for example by calling app.addRepository('modules/helma/Group.js')
 */

// Define the global namespace if not existing
if (!global.helma) {
    global.helma = {};
}

/**
 * Constructs a new helma.Group Object.
 * @class This is what is retrieved through groups.get(groupName),
 * wrapping the root object of each group tree.
 * @param {FIXME} javaGroup FIXME
 * @constructor
 */
helma.Group = function(javaGroup) {
    // private variable containing the wrapper object
    var groupRoot = new helma.Group.GroupObject(javaGroup.getRoot());

    /**
     * @returns the wrapped java object Group
     */
    this.getJavaObject = function() {
        return javaGroup;
    };

    /**
     * sets a key/value pair on the group's root, 
     * wraps the function of the wrapper object
     */
    this.set = function(key, val, sendMode) {
        groupRoot.set(key, val, sendMode);
        return;
    };

    /**
     * removes a key from the group's root, 
     * wraps the function of the root GroupObject
     */
    this.remove = function(key, sendMode) {
        return groupRoot.remove(key, sendMode);
    };

    /**
     * retrieves a key from the group's root, 
     * wraps the function of the root GroupObject
     */
    this.get = function(key) {
        return groupRoot.get(key);
    };

    /**
     * @see helma.Group.GroupObject.listChildren
     */   
    this.listChildren = function() {
        return groupRoot.listChildren();
    };

    /**
     * @see helma.Group.GroupObject.listProperties
     */   
    this.listProperties = function() {
        return groupRoot.listProperties();
    };
   
    /**
     * @see helma.Group.GroupObject.countChildren
     */   
    this.countChildren = function() {
        return groupRoot.countChildren();
    };

    /**
     * @see helma.Group.GroupObject.countProperties
     */   
    this.countProperties = function() {
        return groupRoot.countProperties();
    };

    /**
     * calls a function in all connected applications 
     * (to be specific: in all registered localClients).
     * @param method name of the method in xmlrpc-style: test 
     * is called as root.test(), stories.137.render
     * is called as root.stories.get("137").render() etc etc.
     * @param argArr array of arguments to the remote method
     * @param sendMode as defined for helma.Group.GroupObject
     * @returns array of result objects
     */
    this.callFunction = function(method, argArr, sendMode) {
        groups.checkWriteAccess(javaGroup);
        if (sendMode == null) {
            sendMode = helma.Group.GroupObject.DEFAULT_GET;
        }
        var argVec = new java.util.Vector();
        for (var i=0; i<argArr.length; i++) {
            argVec.add(argArr[i]);
        }
        var resVec = javaGroup.execute(method, argVec, sendMode, 
                                       javaGroup.DEFAULT_EXECUTE_TIMEOUT);
        var resArr = [];
        for (var i=0; i<resVec.size(); i++) {
            resArr[i] = resVec.get(i);
        }
        return resArr;
     };
   
     this.toString = function() {
         return javaGroup.toString();
     };
   
     return this;
};

/**
 * Constructs a new helma.Group.GroupObject. 
 * @class This class wraps the java GroupObject
 * and provides several methods for retrieving and manipulating properties.
 * @param {Object} Instance of helma.extensions.helmagroups.GroupObject
 * @constructor
 */
helma.Group.GroupObject = function(javaGroupObject) {
    var helmagroups = Packages.helma.extensions.helmagroups;

    if (!javaGroupObject) {
        var javaGroupObject = new helmagroups.GroupObject();
    }

    /**
     * private method that returns true if the group
     * is writable
     * @returns Boolean
     */
    var checkWriteAccess = function() {
        if (javaGroupObject.getState() == helmagroups.GroupObject.REPLICATED) {
            groups.checkWriteAccess(javaGroupObject.getGroup());
        }
        return true;
    };

    /**
     * Checks if the key passed as argument is a path
     * (either an Array or a String that contains separator characters)
     * @returns Boolean
     */
    var keyIsPath = function(key) {
        var separator = helmagroups.GroupObject.SEPARATOR;
        if ((key instanceof Array) || key.indexOf(separator) != -1) {
            return true;
        }
        return false;
    };
   
    /**
     * Returns the last element if the key passed as argument is a path.
     * @returns Boolean
     */
    var getLastKeyElement = function(key) {
        var separator = helmagroups.GroupObject.SEPARATOR;
        if (!(key instanceof Array) && key.indexOf(separator) != -1) {
            if (key.charAt(key.length-1)==separator) {
                key = key.substring(0, key.length-1);
            }
            key = key.split(separator);
        }
        if (key instanceof Array) {
            return key[key.length-1];
        }
        return null;
    };
 
 
    /**
     * if key is a path, walks through the path and returns the lowest GroupObject.
     * if tree ends somewhere in the path, function returns null.
     * @returns null or GroupObject
     */
    var walkPath = function(obj, key) {
        var separator = helmagroups.GroupObject.SEPARATOR;
        if (!(key instanceof Array) && key.indexOf(separator) != -1) {
            if (key.charAt(key.length-1)==separator) {
                key = key.substring(0, key.length-1);
            }
            key = key.split(separator);
        }
        if (key instanceof Array) {
            // loop down until end of array
            for (var i=0; i<key.length-1; i++) {
                var nextObj = obj.get(key[i]);
                if (nextObj == null || !(nextObj instanceof helma.Group.GroupObject)) {
                    return null;
                }
                obj = nextObj;
            }
            return obj;
        }
    };
 
 
    /**
     * if key is a path, walks through the path and returns the lowest GroupObject.
     * if tree ends somewhere in the path, function creates the missing GroupObjects.
     * @returns helma.Group.GroupObject
     */
    var createPath = function(obj, key) {
        var separator = helmagroups.GroupObject.SEPARATOR;
        if (!(key instanceof Array) && key.indexOf(separator) != -1) {
            if (key.charAt(key.length-1)==separator) {
                key = key.substring(0, key.length-1);
            }
            key = key.split(separator);
        }
        if (key instanceof Array) {
            // loop down until end of array
            for (var i=0; i<key.length-1; i++) {
                var nextObj = obj.get(key[i]);
                if (nextObj == null || !(nextObj instanceof helma.Group.GroupObject)) {
                    nextObj = new helma.Group.GroupObject();
                    obj.set(key[i], nextObj);
                }
                obj = nextObj;
            }
            return obj;
        }
    };
 
 
    /**
     * Returns the wrapped java GroupObject.
     * @return Instance of helma.extensions.helmagroups.GroupObject;
     * @type helma.extensions.helmagroups.GroupObject
     */
    this.getJavaObject = function() {
        return javaGroupObject;
    };
 
 
    /**
     * Sets a property or a child GroupObject in this instance.
     * The Key may be a String, an Array or a String with separator characters ("/").
     * In the latter two cases the argument is considered a path and
     * all GroupObjects along this path are created if necessary.
     * @param {Object} key Either
     * <ul>
     * <li>a String</li>
     * <li>a String containing slashes</li>
     * <li>an Array containing String keys</li>
     * </ul>
     * @param {Number} The value to set the property to.
     * @param {Object} The mode to use when committing the change to
     * the helma.Group
     */
    this.set = function(key, val, sendMode) {
        if (!key) {
            throw "helma.Group.GroupObject.set(): key can't be null";
        }
        checkWriteAccess();
        // check content type of value:
        var ok = false;
        if (val == null)
            ok = true;
        else if (typeof(val) == "string")
            ok = true;
        else if (typeof(val) == "number")
            ok = true;
        else if (typeof(val) == "boolean")
            ok = true;
        else if (val instanceof Date)
            ok = true;
        else if (val instanceof helma.Group.GroupObject)
            ok = true;
        if (ok == false) {   
            throw "only primitive values, Date and helma.Group.GroupObject allowed in helma.Group.GroupObject.set()";
        }
        if (sendMode == null) {
            sendMode = helma.Group.GroupObject.DEFAULT_GET;
        }
        
        if (keyIsPath(key)) {
            var obj = createPath(this, key);
            if (obj != null) {
                obj.set(getLastKeyElement(key), val, sendMode);
            }
        } else {
            // set a property/child of this object
            if (val == null) {
                // null values aren't permitted in the group,
                // setting a property to null is the same as deleting it
                this.remove(key, sendMode);
            } else if (val instanceof helma.Group.GroupObject) {
                // replicate helma.Group.GroupObject
                javaGroupObject.put(key, val.getJavaObject(), sendMode);
            } else {
                // put the primitive property (or maybe replicate,
                // decision's up to helma.Group.GroupObject)
                if (val instanceof Date) {
                    // convert javascript dates to java dates
                    val = new java.util.Date(val.getTime());
                }
                javaGroupObject.put(key, val, sendMode);
            }
        }
        return;
    };
 
 
 
    /**
     * Removes a property or a child GroupObject from this instance.
     * The Key may be a String, an Array or a String with separator characters ("/").
     * In the latter two cases the argument is considered a path and
     * the function walks down that path to find the GroupObject and
     * deletes it.
     * @param {Object} key Either
     * <ul>
     * <li>a String</li>
     * <li>a String containing slashes</li>
     * <li>an Array containing String keys</li>
     * </ul>
     * @param {Number} The mode to use when committing the change to
     * the helma.Group
     */
    this.remove = function(key, sendMode) {
        checkWriteAccess();
        if (sendMode == null) {
            sendMode = helma.Group.GroupObject.DEFAULT_GET;
        }
        if (keyIsPath(key)) {
            var obj = walkPath(this, key);
            if (obj != null) {
                obj.remove(getLastKeyElement(key));
            }
        } else {
            javaGroupObject.remove(key, sendMode);
        }
        return;
    };
 
 
    /**
     * Returns either a property or a child GroupObject from
     * this GroupObject instance. The key passed as argument
     * may be a String, an Array containing Strings or a
     * String containing separator characters ("/"). In the latter
     * two cases the argument is considered a path and
     * the function walks down that path to find the requested
     * GroupObject.
     * @param {Object} key Either
     * <ul>
     * <li>a String</li>
     * <li>a String containing slashes</li>
     * <li>an Array containing String keys</li>
     * </ul>
     * @return Depending on the argument either the appropriate property
     * value or a helma.Group.GroupObject
     * @type Object
     */
    this.get = function(key) {
        if (key == null) {
            return null;
        }
        if (keyIsPath(key)) {
            var obj = walkPath(this, key);
            if (obj != null) {
                return obj.get(getLastKeyElement(key));
            } else {
                return null;
            }
        } else if (javaGroupObject.hasProperty(key)) {
           // we got a primitive property
           var val = javaGroupObject.getProperty(key);
           if (val instanceof java.util.Date) {
               // convert java dates to javascript dates
               val = new Date(val);
           }
           return val;
        } else if (javaGroupObject.hasChild(key)) {
            // we got a child object
            return new helma.Group.GroupObject(javaGroupObject.getChild(key));
        }
        return null;
    };
 
 
    /**
     * Gets a property from this GroupObject. The key passed as argument
     * is always considered a property even if it contains a slash.
     * This is actually a workaround for the fact that other
     * instances of the group not using the javascript extension aren't forbidden
     * to add properties containing a slash in the property's name.
     * So, using this extension we can at least read the property.
     * @param {String} key The name of the property to return
     * @returns The value of the property
     * @type Object
     */
    this.getProperty = function(key) {
        if (key == null) {
            return null;
        } else if (javaGroupObject.hasProperty(key)) {
            // we got a primitive property
            var val = javaGroupObject.getProperty(key);
            if (val instanceof java.util.Date) {
                // convert java dates to javascript dates
                val = new Date(val);
            }
            return val;
        }
        return null;
    }
    
 
    /**
     * Exchanges this GroupObject with the one passed
     * as argument. This is done by exchanging the wrapped
     * instance of helma.extensions.helmagroups.GroupObject
     * @param {GroupObject} The GroupObject to use
     * @returns The GroupObject with the exchanged wrapped java object
     * @type GroupObject
     */
    this.wrap = function(newGroupObject) {
        checkWriteAccess();
        if (javaGroupObject.getState() != helmagroups.GroupObject.REPLICATED) {
            throw "helma.Group.GroupObject.wrap() may only be called on replicated GroupObjects";
        }
        if (newGroupObject == null || !(newGroupObject instanceof helma.Group.GroupObject)) {
            throw "helma.Group.GroupObject.wrap() requires a helma.Group.GroupObject as an argument";
        }
        javaGroupObject.wrap(newGroupObject.getJavaObject());
        return this;
    };
 
    /**
     * Clones this GroupObject and returns it.
     * This method should be considered if many properties
     * of a GroupObject must be set or modified since every
     * change to an already replicated GroupObject will
     * result in immediate network traffic. Using unwrap
     * one can modify several properties and then commit
     * the GroupObject at once using {@link #wrap).
     * @returns A clone of this GroupObject
     * @type GroupObject
     */
    this.unwrap = function() {
        var javaGroupObjectClone = javaGroupObject.clone();
        javaGroupObjectClone.setChildren(new java.util.Hashtable());
        javaGroupObjectClone.setState(helmagroups.GroupObject.LOCAL);
        javaGroupObjectClone.setPath(null);
        return new helma.Group.GroupObject(javaGroupObjectClone);
    };
 
    /**
     * Converts this GroupObject into a vanilla Object
     * @returns An Object containing all properties of this GroupObject
     * @type Object
     */
    this.toJSObject = function() {
        var key;
        var obj = {};
        var e = javaGroupObject.properties();
        while(e.hasMoreElements()) {
            obj[key = e.nextElement()] = javaGroupObject.getProperty(key);
        }
        return obj;
    };
   
    /**
     * Returns an Array containing all child GroupObjects
     * @returns An Array containing GroupObjects
     * @type Array
     */
    this.listChildren = function() {
        var arr = [];
        var e = javaGroupObject.children();
        while(e.hasMoreElements()) {
            arr.push(e.nextElement());
        }
        return arr;
    };
 
    /**
     * Returns an Array containing all property
     * names of this GroupObject instance
     * @returns An Array containing property names
     * @type Array
     */
    this.listProperties = function() {
        var arr = [];
        var e = javaGroupObject.properties();
        while(e.hasMoreElements()) {
            arr.push(e.nextElement());
        }
        return arr;
    };
  
    /**
     * Returns the number of child GroupObjects
     * @returns The number of child GroupObjects of this
     * helma.Group.GroupObject instance
     * @type Number
     */
    this.countChildren = function() {
        var ht = javaGroupObject.getChildren();
        if (ht == null) {
            return 0;
        } else {
            return ht.size();
        }
    };
 
    /**
     * Returns the number of properties of this GroupObject
     * @return The number of properties
     * @type Number
     */
    this.countProperties = function() {
        var ht = javaGroupObject.getProperties();
        return (ht == null) ? 0 : ht.size();
    };
 
    /**
     * Returns true if the GroupObject is <em>not</em> replicated
     * @returns True if this GroupObject is still local
     * @type Boolean
     */
    this.isLocal = function() {
        return (javaGroupObject.getState()
                == helmagroups.GroupObject.LOCAL);
    };
 
    /** @ignore */
    this.toString = function() {
        return javaGroupObject.toString();
    };
 
    return this;
};

/**
 * Static properties of GroupObject constructor function.
 * These values determine if and for how many confirmation of the
 * group members this instance waits after a modification.
 * These values are passed through to org.jgroups.blocks.GroupRequest,
 * for further comments see the sourcecode of that class
 */
// wait just for the first response
helma.Group.GroupObject.GET_FIRST = 1;
// wait until all members have responded
helma.Group.GroupObject.GET_ALL = 2;
// wait for majority (50% + 1) to respond
helma.Group.GroupObject.GET_MAJORITY = 3;
// wait for majority of all members (may block!)
helma.Group.GroupObject.GET_ABS_MAJORITY = 4;
// don't wait for any response (fire & forget)
helma.Group.GroupObject.GET_NONE = 6;
// default: wait for all responses
helma.Group.GroupObject.DEFAULT_GET = helma.Group.GroupObject.GET_ALL;

/**
 * This is mounted as "groups". 
 * @class The root for all groups started in this application
 * @constructor
 */
helma.Group.Manager = function() {
    var helmagroups = Packages.helma.extensions.helmagroups;
    var extension = helmagroups.GroupExtension.self;

    if (extension == null) {
        throw("helma.Group.Manager requires the HelmaGroups Extension \
               located in lib/ext or the application's top-level directory \
               [http://adele.helma.org/download/helma/contrib/helmagroups/]");
    }
    
    
    /**
     * get a java object Group for a groupname.
     * object is fetched regardless of connection status
     * @returns null if group is not defined
     */
    var getJavaGroup = function(name) {
        return extension.checkAppLink(app.name).get(name);
    };
    
    
    /**
     * visible to scripting env: get a group, wrapped as a javascript helma.Group object.
     * the group must be defined in app.properties: group.nameXX = <configfile>
     * and can then be accessed like this group.get("nameXX")
     * @returns null if group is not defined or not connected
     */
    this.get = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null || javaGroup.isConnected() == false) {
            return null;
        } else {
            return new helma.Group(javaGroup);
        }
    };
    
    
    /**
     * checks for write access to a group according to app.properties
     * group.nameXX.writable must be true so that this function returns
     * @param nameOrJGroup can be the name of a group or a java Group itself
     * @throws an error if group is not writable
     */
    this.checkWriteAccess = function(nameOrJGroup) {
        var extension = helmagroups.GroupExtension.self;
        var jAppLink = extension.checkAppLink(app.name);
        if (nameOrJGroup instanceof helmagroups.Group) {
            // arg was a Group
            var javaGroup = nameOrJGroup;
        } else {
            // arg was the name of the group
            var javaGroup = jAppLink.get(nameOrJGroup);
        }
        if (javaGroup != null && jAppLink.isWritable(javaGroup) == false) {
            throw("tried to access write-protected group");
        }
        return true;
    };
    
    
    /**
     * try to connect a group
     * @returns false if group is not found
     */
    this.connect = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        javaGroup.connect();
        return true;
    };
    
    
    /**
     * try to disconnect from a group
     * @returns false if group is not found
     */
    this.disconnect = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        javaGroup.disconnect();
        return true;
    };
    
    
    /**
     * try to disconnect and connect again to a group
     * @returns false if group is not found
     */
    this.reconnect = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        javaGroup.reconnect();
        return true;
    };
    
    
    /**
     * try to reset a group (if application may write in group).
     * all instances of the group empty their cache.
     * @returns false if group is not found
     */
    this.reset = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        groups.checkWriteAccess(javaGroup);
        javaGroup.reset();
        return true;
    };
    
    
    /**
     * try to destroy a group (if application may write in group).
     * all other instances of the group disconnect
     * @returns false if group is not found
     */
    this.destroy = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        groups.checkWriteAccess(javaGroup);
        javaGroup.destroy();
        return true;
    };
    
    
    /**
     * try to restart a group (if application may write in group).
     * all other instances of the group disconnect and reconnect - each app after a different pause
     * so that they don't all come up at the same moment
     * @returns false if group is not found
     */
    this.restart = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        groups.checkWriteAccess(javaGroup);
        javaGroup.restart();
        return true;
    };
    
    
    /**
     * list the members of this group (ie instances of Group, one helma server is one instance)
     * @returns array of strings, false if group is not found
     */
    this.listMembers = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        var addrArr = javaGroup.info.listMembers();
        var arr = [];
        for (var i=0; i<addrArr.length; i++) {
            arr[arr.length] = helmagroups.Config.addressToString(addrArr[i]);
        }
        return arr;
    };
    
    
    /**
     * lists the members applications of this group (may be more than one per instance but also none)
     * @returns array of strings, false if group is not found
     */
    this.listMemberApps = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        var appsArr = javaGroup.info.listMemberApps();
        var arr = [];
        for (var i=0; i<appsArr.length; i++) {
            arr[arr.length] = appsArr[i];
        }
        return arr;
    };
    
    
    /**
     * dumps the keys of the group to a string
     * @returns string, notice if group is not found
     */
    this.getContent = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return "[not connected]";
        }
        return javaGroup.info.print();
    };
    
    
    /**
     * dumps the keys and the content of the group to a string
     * @returns string, notice if group is not found
     */
    this.getFullContent = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return "[not connected]";
        }
        return javaGroup.info.printFull();
    };
    
    
    /**
     * dumps the config of the jgroups stack to a string
     * @returns string, false if group is not found
     */
    this.getConfig = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        return javaGroup.info.printStack(false);
    };
    
    
    /**
     * dumps the config of the jgroups stack including all properties to a string
     * @returns string, false if group is not found
     */
    this.getFullConfig = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        return javaGroup.info.printStack(true);
    };
    
    
    /**
     * returns the connection identifier of the Group instance (localname + multicast-target)
     * @returns string, false if group is not found
     */
    this.getConnection = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        return javaGroup.info.getConnection();
    };
    
    
    /**
     * returns true/false if the group is connected
     */
    this.isConnected = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        return javaGroup.isConnected();
    };
    
    
    /**
     * returns the total number of groupobjects in this group
     */
    this.size = function(name) {
        var javaGroup = getJavaGroup(name);
        if (javaGroup == null) {
            return false;
        }
        return javaGroup.size();
    };
    
    
    /**
     * returns the total number of groupobjects in this group
     */
    this.count = function(name) {
        return this.size(name);
    };
    
    this.toString = function() {
        return "[helma.Group.Manager]";
    };
    
    return this;
}

// Instantiate helma.Group.Manager as "groups" variable
var groups = new helma.Group.Manager();
