# Source: repository/Branch.js

    (function(window)
    {
        var Gitana = window.Gitana;

        Gitana.Branch = Gitana.AbstractRepositoryObject.extend(
        /** @lends Gitana.Branch.prototype */
        {
            /**
             * @constructs
             * @augments Gitana.AbstractRepositoryObject
             *
             * @class Branch
             *
             * @param {Gitana.Repository} repository
             * @param [Object] object json object (if no callback required for populating)
             */
            constructor: function(repository, object)
            {
                this.base(repository, object);

                this.objectType = function() { return "Gitana.Branch"; };
            },

            /**
             * @OVERRIDE
             */
            getType: function()
            {
                return Gitana.TypedIDConstants.TYPE_BRANCH;
            },

            /**
             * @OVERRIDE
             */
            getUri: function()
            {
                return "/repositories/" + this.getRepositoryId() + "/branches/" + this.getId();
            },

            /**
             * @override
             */
            clone: function()
            {
                return this.getFactory().branch(this.getRepository(), this);
            },

            /**
             * @returns {Boolean} whether this is the master branch
             */
            isMaster: function()
            {
                return (this.getBranchType().toLowerCase() == "master");
            },

            /**
             * @return {String} the type of branch ("master" or "custom")
             */
            getBranchType: function()
            {
                return this.get("type");
            },

            /**
             * @return {String} the tip changeset of the branch
             */
            getTip: function()
            {
                return this.get("tip");
            },

            /**
             * Acquires a list of mount nodes under the root of the repository.
             *
             * @chained node map
             *
             * @public
             *
             * @param [Object] pagination
             */
            listMounts: function(pagination)
            {
                var self = this;

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes";
                };

                var chainable = this.getFactory().nodeMap(this);
                return this.chainGet(chainable, uriFunction, params);
            },

            /**
             * Reads a node.
             *
             * @chained node
             *
             * @public
             *
             * @param {String} nodeId the node id
             * @param [String] offset path
             */
            readNode: function(nodeId, path)
            {
                var self = this;

                var uriFunction = function()
                {
                    var uri = self.getUri() + "/nodes/" + nodeId;
                    if (path) {
                        uri += "?path=" + path;
                    }

                    return uri;
                };

                var chainable = this.getFactory().node(this);
                return this.chainGet(chainable, uriFunction);
            },

            /**
             * Reads the root node.
             *
             * @chained node
             *
             * @public
             */
            rootNode: function()
            {
                return this.readNode("root");
            },

            /**
             * Create a node
             *
             * @chained node
             *
             * @public
             *
             * @param [Object] object JSON object
             * @param [Object|String] options a JSON object providing the configuration for the create operation.
             *                                If a string, must follow format (<rootNode>/<filePath>)
             */
            createNode: function(object, options)
            {
                var self = this;

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes";
                };

                var params = {};

                if (options)
                {
                    var rootNodeId = "root"; // default
                    var associationType = "a:child"; // default
                    var filePath = null;
                    var parentFolderPath = null;
                    var fileName = null;

                    // if they pass in a string instead of an options object, then the string can follow the format
                    // (/root/pages/file.txt) where root is the root node to start from
                    if (typeof(options) === "string")
                    {
                        var rootPrefixedFilePath = options;

                        // filePath should not start with "/"
                        if (Gitana.startsWith(rootPrefixedFilePath, "/")) {
                            rootPrefixedFilePath = rootPrefixedFilePath.substring(1);
                        }

                        if (rootPrefixedFilePath == "") {
                            filePath = "/";
                        } else {
                            var i = rootPrefixedFilePath.indexOf("/");
                            rootNodeId = rootPrefixedFilePath.substring(0, i);
                            filePath = rootPrefixedFilePath.substring(i + 1);
                        }
                    }
                    else if (typeof(options) === "object")
                    {
                        if (options.rootNodeId) {
                            rootNodeId = options.rootNodeId;
                        }
                        if (options.associationType) {
                            associationType = options.associationType;
                        }
                        if (options.fileName) {
                            fileName = options.fileName;
                        }
                        else if (options.filename) {
                            fileName = options.filename;
                        }
                        if (options.parentFolderPath) {
                            parentFolderPath = options.parentFolderPath;
                        }
                        else if (options.folderPath) {
                            parentFolderPath = options.folderPath;
                        }
                        else if (options.folderpath) {
                            parentFolderPath = options.folderpath;
                        }
                        if (options.filePath) {
                            filePath = options.filePath;
                        }
                        else if (options.filepath) {
                            filePath = options.filepath;
                        }
                    }

                    // plug in the resolved params
                    if (rootNodeId) {
                        params.rootNodeId = rootNodeId;
                    }
                    if (associationType) {
                        params.associationType = associationType;
                    }
                    if (fileName) {
                        params.fileName = fileName;
                    }
                    if (filePath) {
                        params.filePath = filePath;
                    }
                    if (parentFolderPath) {
                        params.parentFolderPath = parentFolderPath;
                    }

                    // allow custom params to be passed through
                    if (options.params) {
                        for (var param in options.params) {
                            params[param] = options.params[param];
                        }
                    }
                }

                var chainable = this.getFactory().node(this);
                return this.chainCreate(chainable, object, uriFunction, params);
            },

            /**
             * Searches the branch.
             *
             * @chained node map
             *
             * Config should be:
             *
             *    {
             *       "search": {
             *           ... Elastic Search Config Block
             *       }
             *    }
             *
             * For a full text term search, you can simply provide text in place of a config json object.
             *
             * See the Elastic Search documentation for more advanced examples
             *
             * @public
             *
             * @param search
             * @param [Object] pagination
             */
            searchNodes: function(search, pagination)
            {
                var self = this;

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                if (Gitana.isString(search))
                {
                    search = {
                        "search": search
                    };
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes/search";
                };

                var chainable = this.getFactory().nodeMap(this);
                return this.chainPost(chainable, uriFunction, params, search);
            },

            /**
             * Queries for nodes on the branch.
             *
             * Config should be:
             *
             *    {
             *       Gitana query configs
             *    }
             *
             * @chained node map
             *
             * @public
             *
             * @param {Object} query
             * @param [Object] pagination
             */
            queryNodes: function(query, pagination)
            {
                var self = this;

                if (!query) {
                    query = {};
                }

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes/query";
                };

                var chainable = this.getFactory().nodeMap(this);

                if (!Gitana.PREFER_GET_OVER_POST)
                {
                    return this.chainPost(chainable, uriFunction, params, query);
                }
                else
                {
                    Gitana.copyInto(params, {
                        "query": JSON.stringify(query)
                    });

                    return this.chainGet(chainable, uriFunction, params);
                }

            },

            /**
             * Queries for a single matching node to a query on the branch.
             *
             * @chained node
             *
             * @param query
             * @param errHandler
             *
             * @returns Gitana.Node
             */
            queryOne: function(query, errHandler)
            {
                return this.queryNodes(query).keepOne(function(err) {
                    if (errHandler)
                    {
                        errHandler(err);
                        return false;
                    }
                });
            },

            /**
             * Deletes the nodes described the given array of node ids.
             *
             * @hcained branch
             *
             * @param nodeIds
             *
             * @returns Gitana.Branch
             */
            deleteNodes: function(nodeIds)
            {
                var self = this;

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes/delete";
                };

                return this.chainPost(this, uriFunction, {}, {
                    "_docs": nodeIds
                });
            },

            /**
             * Performs a bulk check of permissions against permissioned objects of type node.
             *
             * Example of checks array:
             *
             * [{
             *    "permissionedId": "<permissionedId>",
             *    "principalId": "<principalId>",
             *    "permissionId": "<permissionId>"
             * }]
             *
             * The callback receives an array of results, example:
             *
             * [{
             *    "permissionedId": "<permissionedId>",
             *    "principalId": "<principalId>",
             *    "permissionId": "<permissionId>",
             *    "result": true
             * }]
             *
             * The order of elements in the array will be the same for checks and results.
             *
             * @param checks
             * @param callback
             */
            checkNodePermissions: function(checks, callback)
            {
                var self = this;

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes/permissions/check";
                };

                var object = {
                    "checks": checks
                };

                return this.chainPostResponse(this, uriFunction, {}, object).then(function(response) {
                    callback.call(this, response["results"]);
                });
            },

            /**
             * Reads the person object for a security user.
             *
             * @chained node
             *
             * @param {Object} user either the user id, user name or the user object
             * @param [Boolean] createIfNotFound whether to create the person object if it isn't found
             */
            readPersonNode: function(user, createIfNotFound)
            {
                var self = this;

                var principalDomainQualifiedId = this.extractPrincipalDomainQualifiedId(user);

                var uriFunction = function()
                {
                    var uri = self.getUri() + "/person/acquire?id=" + principalDomainQualifiedId;
                    if (createIfNotFound)
                    {
                        uri += "&createIfNotFound=" + createIfNotFound;
                    }

                    return uri;
                };

                var chainable = this.getFactory().node(this, "n:person");
                return this.chainGet(chainable, uriFunction);
            },

            /**
             * Reads the group object for a security group.
             *
             * @chained node
             *
             * @param {Object} group eitehr the group id, group name or the group object
             * @param [Boolean] createIfNotFound whether to create the group object if it isn't found
             */
            readGroupNode: function(group, createIfNotFound)
            {
                var self = this;

                var principalDomainQualifiedId = this.extractPrincipalDomainQualifiedId(group);

                var uriFunction = function()
                {
                    var uri = self.getUri() + "/group/acquire?id=" + principalDomainQualifiedId;
                    if (createIfNotFound)
                    {
                        uri += "&createIfNotFound=" + createIfNotFound;
                    }

                    return uri;
                };

                var chainable = this.getFactory().node(this, "n:group");
                return this.chainGet(chainable, uriFunction);
            },

            /**
             * Acquire a list of definitions.
             *
             * @chained node map
             *
             * @public
             *
             * @param [String] filter Optional filter of the kind of definition to fetch - "association", "type" or "feature"
             * @param [Object] pagination Optional pagination
             */
            listDefinitions: function(filter, pagination)
            {
                if (filter && typeof(filter) === "object")
                {
                    pagination = filter;
                    filter = null;
                }

                var self = this;

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                var uriFunction = function()
                {
                    // uri
                    var uri = self.getUri() + "/definitions";
                    if (filter)
                    {
                        uri = uri + "?filter=" + filter;
                    }

                    return uri;
                };

                var chainable = this.getFactory().nodeMap(this);
                return this.chainGet(chainable, uriFunction, params);
            },

            /**
             * Reads a definition by qname.
             *
             * @chained definition
             *
             * @public
             *
             * @param {String} qname the qname
             */
            readDefinition: function(qname)
            {
                var self = this;

                var uriFunction = function()
                {
                    return self.getUri() + "/definitions/" + qname;
                };

                var chainable = this.getFactory().definition(this);
                return this.chainGet(chainable, uriFunction);
            },

            /**
             * Loads a list of schemas for an optional given type.
             *
             * @chained this
             *
             * @public
             *
             * @param [String] filter Optional filter of the kind of definition to fetch - "association", "type" or "feature"
             * @param {Function} callback
             */
            loadSchemas: function(filter, callback)
            {
                if (typeof(filter) == "function")
                {
                    callback = filter;
                    filter = null;
                }

                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/schemas";
                    if (filter) {
                        uri += "?filter=" + filter;
                    }
                    self.getDriver().gitanaGet(uri, null, {}, function(response) {

                        callback.call(chain, response);

                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            /**
             * Reads a schema by qname.
             *
             * @chained this
             *
             * @public
             *
             * @param {String} qname the qname
             */
            loadSchema: function(qname, callback)
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/schemas/" + qname;
                    self.getDriver().gitanaGet(uri, null, {}, function(response) {
                        callback.call(chain, response);
                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            /**
             * Determines an available QName on this branch given some input.
             * This makes a call to the repository and asks it to provide a valid QName.
             *
             * The valid QName is passed as an argument to the next method in the chain.
             *
             * Note: This QName is a recommended QName that is valid at the time of the call.
             *
             * If another thread writes a node with the same QName after this call but ahead of this thread
             * attempting to commit, an invalid qname exception may still be thrown back.
             *
             * @chained this
             *
             * @public
             *
             * @param {Object} object an object with "title" or "description" fields to base generation upon
             */
            generateQName: function(object, callback)
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/qnames/generate";
                    self.getDriver().gitanaPost(uri, null, object, function(response) {

                        var qname = response["_qname"];

                        callback.call(chain, qname);

                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            /**
             * Creates an association between the source node and the target node of the given type.
             *
             * @chained branch (this)
             *
             * @param sourceNode
             * @param targetNode
             * @param object (or string identifying type)
             */
            associate: function(sourceNode, targetNode, object)
            {
                // source
                var sourceNodeId = null;
                if (Gitana.isString(sourceNode))
                {
                    sourceNodeId = sourceNode;
                }
                else
                {
                    sourceNodeId = sourceNode.getId();
                }

                // target
                var targetNodeId = null;
                if (Gitana.isString(targetNode))
                {
                    targetNodeId = targetNode;
                }
                else
                {
                    targetNodeId = targetNode.getId();
                }

                // make sure we hand back the branch
                var result = this.subchain(this);

                // run a subchain to do the association
                result.subchain(this).then(function() {
                    this.readNode(sourceNodeId).associate(targetNodeId, object);
                });

                return result;
            },

            /**
             * Traverses around the given node.
             *
             * Note: This is a helper function provided for convenience that delegates off to the node to do the work.
             *
             * @chained traversal results
             *
             * @param node or node id
             * @param config
             */
            traverse: function(node, config)
            {
                var nodeId = null;
                if (Gitana.isString(node))
                {
                    nodeId = node;
                }
                else
                {
                    nodeId = node.getId();
                }

                return this.readNode(nodeId).traverse(config);
            },

            //////////////////////////////////////////////////////////////////////////////////////////
            //
            // CONTAINER (a:child) CONVENIENCE FUNCTIONS
            //
            //////////////////////////////////////////////////////////////////////////////////////////

            /**
             * Creates a container node.
             *
             * This is a convenience function that simply applies the container feature to the object
             * ahead of calling createNode.
             *
             * @chained node
             *
             * @public
             *
             * @param [Object] object JSON object
             */
            createContainer: function(object)
            {
                if (!object)
                {
                    object = {};
                }

                if (!object["_features"])
                {
                    object["_features"] = {};
                }

                object["_features"]["f:container"] = {
                    "active": "true"
                };

                return this.createNode(object);
            },

            //////////////////////////////////////////////////////////////////////////////////////////
            //
            // FIND
            //
            //////////////////////////////////////////////////////////////////////////////////////////

            /**
             * Finds nodes within a branch
             *
             * @chained node map
             *
             * Config should be:
             *
             *    {
             *       "query": {
             *           ... Query Block
             *       },
             *       "search": {
             *           ... Elastic Search Config Block
             *       }
             *    }
             *
             * Alternatively, the value for "search" in the JSON block above can simply be text.
             *
             * @public
             *
             * @param {Object} config search configuration
             */
            find: function(config, pagination)
            {
                var self = this;

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/nodes/find";
                };

                var chainable = this.getFactory().nodeMap(this);
                return this.chainPost(chainable, uriFunction, params, config);
            },

            /**
             * Another way to access the find() method that is more consistent with the API
             * that would be expected.
             *
             * @param config
             * @param pagination
             * @return {*}
             */
            findNodes: function(config, pagination)
            {
                return this.find(config, pagination);
            },

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            //
            // NODE LIST
            //
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            /**
             * List the items in a node list.
             *
             * @chained node map
             *
             * @public
             *
             * @param {String} listKey
             * @param [Object] pagination
             */
            listItems: function(listKey, pagination)
            {
                var self = this;

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/lists/" + listKey + "/items";
                };

                var chainable = this.getFactory().nodeMap(this);
                return this.chainGet(chainable, uriFunction, pagination);
            },

            /**
             * Queries for items in a node list.
             *
             * @chained node map
             *
             * @public
             *
             * @param {String} listKey
             * @param {Object} query
             * @param [Object] pagination
             */
            queryItems: function(listKey, query, pagination)
            {
                var self = this;

                var params = {};
                if (pagination)
                {
                    Gitana.copyInto(params, pagination);
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/lists/" + listKey + "/items/query";
                };

                var chainable = this.getFactory().nodeMap(this);
                return this.chainPost(chainable, uriFunction, params, query);
            },

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            //
            // UTILITIES
            //
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            /**
             * Loads all of the definitions, forms and key mappings on this branch.
             *
             * @param filter
             * @param callback
             */
            loadForms: function(filter, callback)
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/forms";
                    if (filter) {
                        uri += "?filter=" + filter;
                    }
                    self.getDriver().gitanaGet(uri, null, {}, function(response) {

                        callback.call(chain, response);

                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            //
            // ADMIN
            //
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            adminRebuildPathIndexes: function()
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/admin/paths/index";
                    self.getDriver().gitanaPost(uri, null, {}, function(response) {
                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            adminRebuildSearchIndexes: function()
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/admin/search/index";
                    self.getDriver().gitanaPost(uri, null, {}, function(response) {
                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            adminContentMaintenance: function()
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/admin/content";
                    self.getDriver().gitanaPost(uri, null, {}, function(response) {
                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            adminUpgradeSchema: function()
            {
                var self = this;

                return this.then(function() {

                    var chain = this;

                    // call
                    var uri = self.getUri() + "/admin/upgradeschema";
                    self.getDriver().gitanaPost(uri, null, {}, function(response) {
                        chain.next();
                    });

                    // NOTE: we return false to tell the chain that we'll manually call next()
                    return false;
                });
            },

            createForExport: function(exportId, config, callback)
            {
                var self = this;

                if (!config)
                {
                    config = {};
                }

                if (!config.repositoryId)
                {
                    config.repositoryId = self.getRepositoryId();
                }
                if (!config.branchId)
                {
                    config.branchId = self.getId();
                }
                if (!config.properties)
                {
                    config.properties = {};
                }
                if (!config.parentFolderPath)
                {
                    config.parentFolderPath = {};
                }

                var uriFunction = function()
                {
                    return "/ref/exports/" + exportId + "/generate";
                };

                var params = {};

                return this.chainPostResponse(this, uriFunction, params, config).then(function(response) {
                    callback(response);
                });
            },

            //////////////////////////////////////////////////////////////////////////////////////////
            //
            // INFO
            //
            //////////////////////////////////////////////////////////////////////////////////////////

            /**
             * Loads information about the branch.
             *
             * @param callback
             */
            loadInfo: function(callback)
            {
                var uriFunction = function()
                {
                    return this.getUri() +  "/info";
                };

                return this.chainGetResponse(this, uriFunction, {}).then(function(response) {
                    callback(response);
                });
            },

            //////////////////////////////////////////////////////////////////////////////////////////
            //
            // INDEXES
            //
            //////////////////////////////////////////////////////////////////////////////////////////

            createCustomIndex: function(name, index)
            {
                var self = this;

                var payload = null;
                if (typeof(index) === "undefined")
                {
                    payload = name;
                }
                else
                {
                    payload = {
                        "name": name,
                        "index": index
                    };
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/indexes";
                };

                return this.chainPost(this, uriFunction, {}, payload);
            },

            dropCustomIndex: function(name)
            {
                var self = this;

                var uriFunction = function()
                {
                    return self.getUri() + "/indexes/" + name;
                };

                return this.chainDelete(this, uriFunction);
            },

            loadCustomIndexes: function(callback)
            {
                var self = this;

                var uriFunction = function()
                {
                    return self.getUri() + "/indexes";
                };

                return this.chainGetResponse(this, uriFunction, {}).then(function(response) {
                    callback(response);
                });
            },

            //////////////////////////////////////////////////////////////////////////////////////////
            //
            // HISTORY
            //
            //////////////////////////////////////////////////////////////////////////////////////////

            /**
             * Loads the historic changesets for a branch.
             *
             * The config is optional and can specify "root" and "tip" changeset ids.
             *
             * @param config
             * @param callback
             * @returns {*}
             */
            loadHistoryChangesets: function(config, callback)
            {
                var self = this;

                if (typeof(config) === "function") {
                    callback = config;
                    config = {};
                }

                if (!config) {
                    config = {};
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/history/changesets";
                };

                var params = {};
                if (config.root) {
                    params.root = config.root;
                }
                if (config.tip) {
                    params.tip = config.tip;
                }

                return this.chainGetResponse(this, uriFunction, params).then(function(response) {
                    callback(response);
                });
            },

            /**
             * Loads the history node differences for a branch.
             *
             * The config is optional and can specify "root" and "tip" changeset ids.
             *
             * @param config
             * @param callback
             * @returns {*}
             */
            loadHistoryNodeDiffs: function(config, callback)
            {
                var self = this;

                if (typeof(config) === "function") {
                    callback = config;
                    config = {};
                }

                if (!config) {
                    config = {};
                }

                var uriFunction = function()
                {
                    return self.getUri() + "/history/nodediffs";
                };

                var params = {};
                if (config.root) {
                    params.root = config.root;
                }
                if (config.tip) {
                    params.tip = config.tip;
                }

                return this.chainGetResponse(this, uriFunction, params).then(function(response) {
                    callback(response);
                });
            }

        });

    })(window);