You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@falcon.apache.org by sr...@apache.org on 2015/04/01 13:10:32 UTC

[05/21] falcon git commit: FALCON-790 Falcon UI to enable entity/process/feed edits and management. Contributed by Armando Reyna/Kenneth Ho

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/lib/xml2json.min.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/lib/xml2json.min.js b/falcon-ui/app/js/lib/xml2json.min.js
new file mode 100644
index 0000000..12c4a45
--- /dev/null
+++ b/falcon-ui/app/js/lib/xml2json.min.js
@@ -0,0 +1,578 @@
+/*
+ Copyright 2011-2013 Abdulla Abdurakhmanov
+ Original sources are available at https://code.google.com/p/x2js/
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+function X2JS(config) {
+    'use strict';
+        
+    var VERSION = "1.1.7";
+    
+    config = config || {};
+    initConfigDefaults();
+    initRequiredPolyfills();
+    
+    function initConfigDefaults() {
+        if(config.escapeMode === undefined) {
+            config.escapeMode = true;
+        }
+        config.attributePrefix = config.attributePrefix || "_";
+        config.arrayAccessForm = config.arrayAccessForm || "none";
+        config.emptyNodeForm = config.emptyNodeForm || "text";
+        if(config.enableToStringFunc === undefined) {
+            config.enableToStringFunc = true; 
+        }
+        config.arrayAccessFormPaths = config.arrayAccessFormPaths || []; 
+        if(config.skipEmptyTextNodesForObj === undefined) {
+            config.skipEmptyTextNodesForObj = true;
+        }
+        if(config.stripWhitespaces === undefined) {
+            config.stripWhitespaces = true;
+        }
+        config.datetimeAccessFormPaths = config.datetimeAccessFormPaths || [];
+
+        if(config.useDoubleQuotes === undefined) {
+            config.useDoubleQuotes = false;
+        }
+    }
+
+    var DOMNodeTypes = {
+        ELEMENT_NODE       : 1,
+        TEXT_NODE          : 3,
+        CDATA_SECTION_NODE : 4,
+        COMMENT_NODE       : 8,
+        DOCUMENT_NODE      : 9
+    };
+    
+    function initRequiredPolyfills() {
+        function pad(number) {
+          var r = String(number);
+          if ( r.length === 1 ) {
+            r = '0' + r;
+          }
+          return r;
+        }
+        // Hello IE8-
+        if(typeof String.prototype.trim !== 'function') {           
+            String.prototype.trim = function() {
+                return this.replace(/^\s+|^\n+|(\s|\n)+$/g, '');
+            }
+        }
+        if(typeof Date.prototype.toISOString !== 'function') {
+            // Implementation from http://stackoverflow.com/questions/2573521/how-do-i-output-an-iso-8601-formatted-string-in-javascript
+            Date.prototype.toISOString = function() {
+              return this.getUTCFullYear()
+                + '-' + pad( this.getUTCMonth() + 1 )
+                + '-' + pad( this.getUTCDate() )
+                + 'T' + pad( this.getUTCHours() )
+                + ':' + pad( this.getUTCMinutes() )
+                + ':' + pad( this.getUTCSeconds() )
+                + '.' + String( (this.getUTCMilliseconds()/1000).toFixed(3) ).slice( 2, 5 )
+                + 'Z';
+            };
+        }
+    }
+    
+    function getNodeLocalName( node ) {
+        var nodeLocalName = node.localName;         
+        if(nodeLocalName == null) // Yeah, this is IE!! 
+            nodeLocalName = node.baseName;
+        if(nodeLocalName == null || nodeLocalName=="") // =="" is IE too
+            nodeLocalName = node.nodeName;
+        return nodeLocalName;
+    }
+    
+    function getNodePrefix(node) {
+        return node.prefix;
+    }
+        
+    function escapeXmlChars(str) {
+        if(typeof(str) == "string")
+            return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
+        else
+            return str;
+    }
+
+    function unescapeXmlChars(str) {
+        return str.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&amp;/g, '&');
+    }
+    
+    function toArrayAccessForm(obj, childName, path) {
+        switch(config.arrayAccessForm) {
+        case "property":
+            if(!(obj[childName] instanceof Array))
+                obj[childName+"_asArray"] = [obj[childName]];
+            else
+                obj[childName+"_asArray"] = obj[childName];
+            break;      
+        /*case "none":
+            break;*/
+        }
+        
+        if(!(obj[childName] instanceof Array) && config.arrayAccessFormPaths.length > 0) {
+            var idx = 0;
+            for(; idx < config.arrayAccessFormPaths.length; idx++) {
+                var arrayPath = config.arrayAccessFormPaths[idx];
+                if( typeof arrayPath === "string" ) {
+                    if(arrayPath == path)
+                        break;
+                }
+                else
+                if( arrayPath instanceof RegExp) {
+                    if(arrayPath.test(path))
+                        break;
+                }               
+                else
+                if( typeof arrayPath === "function") {
+                    if(arrayPath(obj, childName, path))
+                        break;
+                }
+            }
+            if(idx!=config.arrayAccessFormPaths.length) {
+                obj[childName] = [obj[childName]];
+            }
+        }
+    }
+    
+    function fromXmlDateTime(prop) {
+        // Implementation based up on http://stackoverflow.com/questions/8178598/xml-datetime-to-javascript-date-object
+        // Improved to support full spec and optional parts
+        var bits = prop.split(/[-T:+Z]/g);
+        
+        var d = new Date(bits[0], bits[1]-1, bits[2]);          
+        var secondBits = bits[5].split("\.");
+        d.setHours(bits[3], bits[4], secondBits[0]);
+        if(secondBits.length>1)
+            d.setMilliseconds(secondBits[1]);
+
+        // Get supplied time zone offset in minutes
+        if(bits[6] && bits[7]) {
+            var offsetMinutes = bits[6] * 60 + Number(bits[7]);
+            var sign = /\d\d-\d\d:\d\d$/.test(prop)? '-' : '+';
+
+            // Apply the sign
+            offsetMinutes = 0 + (sign == '-'? -1 * offsetMinutes : offsetMinutes);
+
+            // Apply offset and local timezone
+            d.setMinutes(d.getMinutes() - offsetMinutes - d.getTimezoneOffset())
+        }
+        else
+            if(prop.indexOf("Z", prop.length - 1) !== -1) {
+                d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()));                  
+            }
+
+        // d is now a local time equivalent to the supplied time
+        return d;
+    }
+    
+    function checkFromXmlDateTimePaths(value, childName, fullPath) {
+        if(config.datetimeAccessFormPaths.length > 0) {
+            var path = fullPath.split("\.#")[0];
+            var idx = 0;
+            for(; idx < config.datetimeAccessFormPaths.length; idx++) {
+                var dtPath = config.datetimeAccessFormPaths[idx];
+                if( typeof dtPath === "string" ) {
+                    if(dtPath == path)
+                        break;
+                }
+                else
+                if( dtPath instanceof RegExp) {
+                    if(dtPath.test(path))
+                        break;
+                }               
+                else
+                if( typeof dtPath === "function") {
+                    if(dtPath(obj, childName, path))
+                        break;
+                }
+            }
+            if(idx!=config.datetimeAccessFormPaths.length) {
+                return fromXmlDateTime(value);
+            }
+            else
+                return value;
+        }
+        else
+            return value;
+    }
+
+    function parseDOMChildren( node, path ) {
+        if(node.nodeType == DOMNodeTypes.DOCUMENT_NODE) {
+            var result = new Object;
+            var nodeChildren = node.childNodes;
+            // Alternative for firstElementChild which is not supported in some environments
+            for(var cidx=0; cidx <nodeChildren.length; cidx++) {
+                var child = nodeChildren.item(cidx);
+                if(child.nodeType == DOMNodeTypes.ELEMENT_NODE) {
+                    var childName = getNodeLocalName(child);
+                    result[childName] = parseDOMChildren(child, childName);
+                }
+            }
+            return result;
+        }
+        else
+        if(node.nodeType == DOMNodeTypes.ELEMENT_NODE) {
+            var result = new Object;
+            result.__cnt=0;
+            
+            var nodeChildren = node.childNodes;
+            
+            // Children nodes
+            for(var cidx=0; cidx <nodeChildren.length; cidx++) {
+                var child = nodeChildren.item(cidx); // nodeChildren[cidx];
+                var childName = getNodeLocalName(child);
+                
+                if(child.nodeType!= DOMNodeTypes.COMMENT_NODE) {
+                    result.__cnt++;
+                    if(result[childName] == null) {
+                        result[childName] = parseDOMChildren(child, path+"."+childName);
+                        toArrayAccessForm(result, childName, path+"."+childName);                   
+                    }
+                    else {
+                        if(result[childName] != null) {
+                            if( !(result[childName] instanceof Array)) {
+                                result[childName] = [result[childName]];
+                                toArrayAccessForm(result, childName, path+"."+childName);
+                            }
+                        }
+                        (result[childName])[result[childName].length] = parseDOMChildren(child, path+"."+childName);
+                    }
+                }                               
+            }
+            
+            // Attributes
+            for(var aidx=0; aidx <node.attributes.length; aidx++) {
+                var attr = node.attributes.item(aidx); // [aidx];
+                result.__cnt++;
+                result[config.attributePrefix+attr.name]=attr.value;
+            }
+            
+            // Node namespace prefix
+            var nodePrefix = getNodePrefix(node);
+            if(nodePrefix!=null && nodePrefix!="") {
+                result.__cnt++;
+                result.__prefix=nodePrefix;
+            }
+            
+            if(result["#text"]!=null) {             
+                result.__text = result["#text"];
+                if(result.__text instanceof Array) {
+                    result.__text = result.__text.join("\n");
+                }
+                //if(config.escapeMode)
+                //  result.__text = unescapeXmlChars(result.__text);
+                if(config.stripWhitespaces)
+                    result.__text = result.__text.trim();
+                delete result["#text"];
+                if(config.arrayAccessForm=="property")
+                    delete result["#text_asArray"];
+                result.__text = checkFromXmlDateTimePaths(result.__text, childName, path+"."+childName);
+            }
+            if(result["#cdata-section"]!=null) {
+                result.__cdata = result["#cdata-section"];
+                delete result["#cdata-section"];
+                if(config.arrayAccessForm=="property")
+                    delete result["#cdata-section_asArray"];
+            }
+            
+            if( result.__cnt == 1 && result.__text!=null  ) {
+                result = result.__text;
+            }
+            else
+            if( result.__cnt == 0 && config.emptyNodeForm=="text" ) {
+                result = '';
+            }
+            else
+            if ( result.__cnt > 1 && result.__text!=null && config.skipEmptyTextNodesForObj) {
+                if( (config.stripWhitespaces && result.__text=="") || (result.__text.trim()=="")) {
+                    delete result.__text;
+                }
+            }
+            delete result.__cnt;            
+            
+            if( config.enableToStringFunc && (result.__text!=null || result.__cdata!=null )) {
+                result.toString = function() {
+                    return (this.__text!=null? this.__text:'')+( this.__cdata!=null ? this.__cdata:'');
+                };
+            }
+            
+            return result;
+        }
+        else
+        if(node.nodeType == DOMNodeTypes.TEXT_NODE || node.nodeType == DOMNodeTypes.CDATA_SECTION_NODE) {
+            return node.nodeValue;
+        }   
+    }
+    
+    function startTag(jsonObj, element, attrList, closed) {
+        var resultStr = "<"+ ( (jsonObj!=null && jsonObj.__prefix!=null)? (jsonObj.__prefix+":"):"") + element;
+        if(attrList!=null) {
+            for(var aidx = 0; aidx < attrList.length; aidx++) {
+                var attrName = attrList[aidx];
+                var attrVal = jsonObj[attrName];
+                if(config.escapeMode)
+                    attrVal=escapeXmlChars(attrVal);
+                resultStr+=" "+attrName.substr(config.attributePrefix.length)+"=";
+                if(config.useDoubleQuotes)
+                    resultStr+='"'+attrVal+'"';
+                else
+                    resultStr+="'"+attrVal+"'";
+            }
+        }
+        if(!closed)
+            resultStr+=">";
+        else
+            resultStr+="/>";
+        return resultStr;
+    }
+    
+    function endTag(jsonObj,elementName) {
+        return "</"+ (jsonObj.__prefix!=null? (jsonObj.__prefix+":"):"")+elementName+">";
+    }
+    
+    function endsWith(str, suffix) {
+        return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    }
+    
+    function jsonXmlSpecialElem ( jsonObj, jsonObjField ) {
+        if((config.arrayAccessForm=="property" && endsWith(jsonObjField.toString(),("_asArray"))) 
+                || jsonObjField.toString().indexOf(config.attributePrefix)==0 
+                || jsonObjField.toString().indexOf("__")==0
+                || (jsonObj[jsonObjField] instanceof Function) )
+            return true;
+        else
+            return false;
+    }
+    
+    function jsonXmlElemCount ( jsonObj ) {
+        var elementsCnt = 0;
+        if(jsonObj instanceof Object ) {
+            for( var it in jsonObj  ) {
+                if(jsonXmlSpecialElem ( jsonObj, it) )
+                    continue;           
+                elementsCnt++;
+            }
+        }
+        return elementsCnt;
+    }
+    
+    function parseJSONAttributes ( jsonObj ) {
+        var attrList = [];
+        if(jsonObj instanceof Object ) {
+            for( var ait in jsonObj  ) {
+                if(ait.toString().indexOf("__")== -1 && ait.toString().indexOf(config.attributePrefix)==0) {
+                    attrList.push(ait);
+                }
+            }
+        }
+        return attrList;
+    }
+    
+    function parseJSONTextAttrs ( jsonTxtObj ) {
+        var result ="";
+        
+        if(jsonTxtObj.__cdata!=null) {                                      
+            result+="<![CDATA["+jsonTxtObj.__cdata+"]]>";                   
+        }
+        
+        if(jsonTxtObj.__text!=null) {           
+            if(config.escapeMode)
+                result+=escapeXmlChars(jsonTxtObj.__text);
+            else
+                result+=jsonTxtObj.__text;
+        }
+        return result;
+    }
+    
+    function parseJSONTextObject ( jsonTxtObj ) {
+        var result ="";
+
+        if( jsonTxtObj instanceof Object ) {
+            result+=parseJSONTextAttrs ( jsonTxtObj );
+        }
+        else
+            if(jsonTxtObj!=null) {
+                if(config.escapeMode)
+                    result+=escapeXmlChars(jsonTxtObj);
+                else
+                    result+=jsonTxtObj;
+            }
+        
+        return result;
+    }
+    
+    function parseJSONArray ( jsonArrRoot, jsonArrObj, attrList ) {
+        var result = ""; 
+        if(jsonArrRoot.length == 0) {
+            result+=startTag(jsonArrRoot, jsonArrObj, attrList, true);
+        }
+        else {
+            for(var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) {
+                result+=startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false);
+                result+=parseJSONObject(jsonArrRoot[arIdx]);
+                result+=endTag(jsonArrRoot[arIdx],jsonArrObj);                      
+            }
+        }
+        return result;
+    }
+    
+    function parseJSONObject ( jsonObj ) {
+        var result = "";    
+
+        var elementsCnt = jsonXmlElemCount ( jsonObj );
+        
+        if(elementsCnt > 0) {
+            for( var it in jsonObj ) {
+                
+                if(jsonXmlSpecialElem ( jsonObj, it) )
+                    continue;           
+                
+                var subObj = jsonObj[it];                       
+                
+                var attrList = parseJSONAttributes( subObj )
+                
+                if(subObj == null || subObj == undefined) {
+                    result+=startTag(subObj, it, attrList, true);
+                }
+                else
+                if(subObj instanceof Object) {
+                    
+                    if(subObj instanceof Array) {                   
+                        result+=parseJSONArray( subObj, it, attrList );                 
+                    }
+                    else if(subObj instanceof Date) {
+                        result+=startTag(subObj, it, attrList, false);
+                        result+=subObj.toISOString();
+                        result+=endTag(subObj,it);
+                    }
+                    else {
+                        var subObjElementsCnt = jsonXmlElemCount ( subObj );
+                        if(subObjElementsCnt > 0 || subObj.__text!=null || subObj.__cdata!=null) {
+                            result+=startTag(subObj, it, attrList, false);
+                            result+=parseJSONObject(subObj);
+                            result+=endTag(subObj,it);
+                        }
+                        else {
+                            result+=startTag(subObj, it, attrList, true);
+                        }
+                    }
+                }
+                else {
+                    result+=startTag(subObj, it, attrList, false);
+                    result+=parseJSONTextObject(subObj);
+                    result+=endTag(subObj,it);
+                }
+            }
+        }
+        result+=parseJSONTextObject(jsonObj);
+        
+        return result;
+    }
+    
+    this.parseXmlString = function(xmlDocStr) {
+        var isIEParser = window.ActiveXObject || "ActiveXObject" in window;
+        if (xmlDocStr === undefined) {
+            return null;
+        }
+        var xmlDoc;
+        if (window.DOMParser) {
+            var parser=new window.DOMParser();          
+            var parsererrorNS = null;
+            // IE9+ now is here
+            if(!isIEParser) {
+                try {
+                    parsererrorNS = parser.parseFromString("INVALID", "text/xml").childNodes[0].namespaceURI;
+                }
+                catch(err) {                    
+                    parsererrorNS = null;
+                }
+            }
+            try {
+                xmlDoc = parser.parseFromString( xmlDocStr, "text/xml" );
+                if( parsererrorNS!= null && xmlDoc.getElementsByTagNameNS(parsererrorNS, "parsererror").length > 0) {
+                    //throw new Error('Error parsing XML: '+xmlDocStr);
+                    xmlDoc = null;
+                }
+            }
+            catch(err) {
+                xmlDoc = null;
+            }
+        }
+        else {
+            // IE :(
+            if(xmlDocStr.indexOf("<?")==0) {
+                xmlDocStr = xmlDocStr.substr( xmlDocStr.indexOf("?>") + 2 );
+            }
+            xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
+            xmlDoc.async="false";
+            xmlDoc.loadXML(xmlDocStr);
+        }
+        return xmlDoc;
+    };
+    
+    this.asArray = function(prop) {
+        if (prop === undefined || prop == null)
+            return [];
+        else
+        if(prop instanceof Array)
+            return prop;
+        else
+            return [prop];
+    };
+    
+    this.toXmlDateTime = function(dt) {
+        if(dt instanceof Date)
+            return dt.toISOString();
+        else
+        if(typeof(dt) === 'number' )
+            return new Date(dt).toISOString();
+        else    
+            return null;
+    };
+    
+    this.asDateTime = function(prop) {
+        if(typeof(prop) == "string") {
+            return fromXmlDateTime(prop);
+        }
+        else
+            return prop;
+    };
+
+    this.xml2json = function (xmlDoc) {
+        return parseDOMChildren ( xmlDoc );
+    };
+    
+    this.xml_str2json = function (xmlDocStr) {
+        var xmlDoc = this.parseXmlString(xmlDocStr);
+        if(xmlDoc!=null)
+            return this.xml2json(xmlDoc);
+        else
+            return null;
+    };
+
+    this.json2xml_str = function (jsonObj) {
+        return parseJSONObject ( jsonObj );
+    };
+
+    this.json2xml = function (jsonObj) {
+        var xmlDocStr = this.json2xml_str (jsonObj);
+        return this.parseXmlString(xmlDocStr);
+    };
+    
+    this.getVersion = function () {
+        return VERSION;
+    };
+    
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/common/falcon-api.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/common/falcon-api.js b/falcon-ui/app/js/services/common/falcon-api.js
new file mode 100644
index 0000000..cfb1ddc
--- /dev/null
+++ b/falcon-ui/app/js/services/common/falcon-api.js
@@ -0,0 +1,128 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  
+  var falconModule = angular.module('app.services.falcon', ['app.services.x2js', 'ngCookies']);
+
+  falconModule.factory('Falcon', ["$http", "X2jsService", function ($http, X2jsService) {
+
+    var Falcon = {},
+        NUMBER_OF_RESULTS = 50; 
+    
+    function buildURI(uri) {
+      var paramSeparator = (uri.indexOf('?') !== -1) ? '&' : '?';
+      uri = uri + paramSeparator + 'user.name=ambari-qa';
+      return uri;
+    }
+    
+    //-------------Server RESPONSE----------------------//
+    Falcon.responses = {
+      display:true,
+      queue:[], 
+      count: {pending: 0, success:0, error:0},
+      multiRequest: {cluster:0, feed:0, process:0},
+      listLoaded: {cluster:false, feed:false, process:false}  
+    };
+    
+    Falcon.logRequest = function () {
+      Falcon.responses.count.pending = Falcon.responses.count.pending + 1;
+      
+    };
+    Falcon.logResponse = function (type, messageObject, entityType, hide) {
+      if(type === 'success') {  
+        if(!hide) {
+          var message = { success: true, status: messageObject.status, message: messageObject.message, requestId: messageObject.requestId};
+          Falcon.responses.queue.push(message);
+          Falcon.responses.count.success = Falcon.responses.count.success +1;  
+        }       
+        Falcon.responses.count.pending = Falcon.responses.count.pending -1; 
+      }
+      if(type === 'error') {
+        
+        if(messageObject.slice(0,6) !== "Cannot") {
+          var errorMessage = X2jsService.xml_str2json(messageObject),
+             message = { success: false, status: errorMessage.result.status, message: errorMessage.result.message, requestId: errorMessage.result.requestId};       
+        }     
+        else {
+          var message = { success: false, status: "No connection", message: messageObject, requestId: "no ID"};      
+        }    
+        Falcon.responses.queue.push(message);
+        Falcon.responses.count.error = Falcon.responses.count.error +1;
+        Falcon.responses.count.pending = Falcon.responses.count.pending -1;    
+      }
+      if(entityType !== false) {
+        entityType = entityType.toLowerCase();
+        Falcon.responses.multiRequest[entityType] = Falcon.responses.multiRequest[entityType] - 1;   
+      }
+       
+    };
+    Falcon.removeMessage = function (index) {
+      if(Falcon.responses.queue[index].success) { Falcon.responses.count.success = Falcon.responses.count.success -1; }
+      else { Falcon.responses.count.error = Falcon.responses.count.error -1; }    
+      Falcon.responses.queue.splice(index, 1); 
+    };
+   // serverResponse: null,
+    //    success: null
+    
+    //-------------METHODS-----------------------------//
+    Falcon.getServerVersion = function () {
+      return $http.get(buildURI('../api/admin/version'));
+    };
+    Falcon.getServerStack = function () {
+      return $http.get(buildURI('../api/admin/stack'));
+    };
+    Falcon.postValidateEntity = function (xml, type) {
+      return $http.post(buildURI('../api/entities/validate/' + type), xml, { headers: {'Content-Type': 'text/plain'} });
+    };
+    Falcon.postSubmitEntity = function (xml, type) {
+      return $http.post(buildURI('../api/entities/submit/' + type), xml, { headers: {'Content-Type': 'text/plain'} });
+    };
+    Falcon.postUpdateEntity = function (xml, type, name) {
+      return $http.post(buildURI('../api/entities/update/' + type + '/' + name), xml, { headers: {'Content-Type': 'text/plain'} });
+    };
+
+    Falcon.postScheduleEntity = function (type, name) {
+      return $http.post(buildURI('../api/entities/schedule/' + type + '/' + name));
+    };
+    Falcon.postSuspendEntity = function (type, name) {
+      return $http.post(buildURI('../api/entities/suspend/' + type + '/' + name));
+    };
+    Falcon.postResumeEntity = function (type, name) {
+      return $http.post(buildURI('../api/entities/resume/' + type + '/' + name));
+    };
+
+    Falcon.deleteEntity = function (type, name) {
+      return $http.delete(buildURI('../api/entities/delete/' + type + '/' + name));
+    };
+    
+    Falcon.getEntities = function (type) {
+    return $http.get(buildURI('../api/entities/list/' + type + '?fields=status,tags&numResults=' + NUMBER_OF_RESULTS));
+    };
+
+    Falcon.getEntityDefinition = function (type, name) {
+      return $http.get(buildURI('../api/entities/definition/' + type + '/' + name), { headers: {'Accept': 'text/plain'} });
+    };
+
+
+    //----------------------------------------------//
+    return Falcon;
+
+  }]);
+
+})();

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/common/file-api.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/common/file-api.js b/falcon-ui/app/js/services/common/file-api.js
new file mode 100644
index 0000000..e3f6745
--- /dev/null
+++ b/falcon-ui/app/js/services/common/file-api.js
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  
+  var fileModule = angular.module('app.services.fileapi', ['app.services.entity.model']);
+
+  fileModule.factory('FileApi', ["$http", "$q", "EntityModel", function ($http, $q, EntityModel) {
+
+    var FileApi = {};
+
+    FileApi.supported = (window.File && window.FileReader && window.FileList && window.Blob);
+    FileApi.errorMessage = 'The File APIs are not fully supported in this browser.';
+
+    FileApi.fileDetails = "No file loaded";
+    FileApi.fileRaw = "No file loaded";
+
+    FileApi.loadFile = function (evt) {
+
+      if (FileApi.supported) {
+        var deferred = $q.defer(),
+          reader = new FileReader(),
+          file = evt.target.files[0];
+
+        reader.onload = (function (theFile) {
+
+          reader.readAsText(theFile, "UTF-8");
+
+          return function (e) {
+            FileApi.fileRaw = e.target.result;
+            FileApi.fileDetails = theFile;
+            EntityModel.getJson(FileApi.fileRaw);
+            deferred.resolve();
+          };
+
+        })(file);
+
+        return deferred.promise;
+      }
+      else {
+        alert(FileApi.errorMessage);
+      }
+    };
+    return FileApi;
+  }]);
+
+})();

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/common/json-transformer.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/common/json-transformer.js b/falcon-ui/app/js/services/common/json-transformer.js
new file mode 100644
index 0000000..e08a477
--- /dev/null
+++ b/falcon-ui/app/js/services/common/json-transformer.js
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  var module = angular.module('app.services.json.transformer', []);
+
+  module.factory('JsonTransformerFactory', function() {
+    return {
+      transform: newFieldTransformation
+    };
+  });
+
+  function newFieldTransformation(sourceField, targetField, mappingCallback) {
+    return new FieldTransformation(sourceField, targetField || sourceField, mappingCallback);
+  }
+
+  function FieldTransformation(sourceField, targetField, mappingCallback) {
+    var self = this;
+
+    self.sourceFieldPath = sourceField.split(".");
+    self.targetFieldPath = targetField.split(".");
+    self.mappingCallback = mappingCallback;
+
+    self.transform = function(sourceField, targetField, mappingCallback) {
+      return new ComposedFieldTransformation(self, newFieldTransformation(sourceField, targetField, mappingCallback));
+    };
+
+    self.apply = function(source, target) {
+
+      var sourceHolder = find(source, self.sourceFieldPath);
+
+      var sourceValue = sourceHolder.get();
+      sourceValue = sourceValue && self.mappingCallback ? self.mappingCallback(sourceValue) : sourceValue;
+
+      if (sourceValue) {
+        var targetHolder = find(target, self.targetFieldPath);
+        targetHolder.set(sourceValue);
+      }
+
+      return target;
+    };
+
+    function find(target, path) {
+      var current = target;
+      var child;
+      for(var i = 0, n = path.length - 1; i < n; i++) {
+        child = path[i];
+        createIfNotExists(current, child);
+        current = current[child];
+      }
+      return new Holder(current, path[path.length - 1]);
+    }
+
+    function createIfNotExists(current, child) {
+      if (!current[child]) {
+        current[child] = {};
+      }
+    }
+
+    function Holder(target, child) {
+      this.target = target;
+      this.child = child;
+
+      this.get = function() {
+        return target[child];
+      };
+
+      this.set = function(value) {
+        target[child] = value;
+      };
+
+    }
+
+    function ComposedFieldTransformation (firstTransformation, secondTransformation) {
+      var self = this;
+
+      self.firstTransformation = firstTransformation;
+      self.secondTransformation = secondTransformation;
+
+      self.apply = function(source, target) {
+        self.firstTransformation.apply(source, target);
+        self.secondTransformation.apply(source, target);
+        return target;
+      };
+
+      self.transform = function(sourceField, targetField, mappingCallback) {
+        return new ComposedFieldTransformation(self, newFieldTransformation(sourceField, targetField, mappingCallback));
+      };
+
+    }
+
+  }
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/common/validation-service.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/common/validation-service.js b/falcon-ui/app/js/services/common/validation-service.js
new file mode 100644
index 0000000..fe45d7a
--- /dev/null
+++ b/falcon-ui/app/js/services/common/validation-service.js
@@ -0,0 +1,128 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+
+  var module = angular.module('app.services.validation', []);
+
+  module.factory('ValidationService', ["$window", function ($window) {
+    var checkMessages = {
+        name: {
+          patternInvalid: "The name has an invalid format.",
+          unavailable: "The name you choosed is not available",
+          empty: "You need to specify a name"
+        },
+        colo: {
+          empty: "You need to provide a colo",
+          patternInvalid: "The Colo has an invalid format. "
+        },
+        description: {
+          empty: "You need to provide a description",
+          patternInvalid: "The Description has an invalid format. "
+        },
+        path: {
+          empty: "You need to provide a path",
+          patternInvalid: "The Path has an invalid format. "
+        },
+        key: {
+          empty: "You need to provide a key",
+          patternInvalid: "The Key has an invalid format. "
+        },
+        value: {
+          empty: "You need to provide a value",
+          patternInvalid: "The Value has an invalid format. "
+        },
+        location: {
+          empty: "You need to provide a  location",
+          patternInvalid: "The Location has an invalid format. "
+        },
+        provider: {
+          empty: "You need to provide a provider",
+          patternInvalid: "The provider has an invalid format. "
+        },
+        engine: { empty: "You need to select an engine" },
+        cluster: { empty: "You need to select a cluster" },
+        feed: { empty: "You need to select a feed" },
+        date: { empty: "You need to select a date" },
+        number: { empty: "You need to provide a number" },
+        option: { empty: "You need to select an option" },
+        user: { empty: "Please enter your user name." },
+        password: { empty: "Please enter your password." }
+      },
+      checkPatterns = {
+        name: new RegExp("^[a-zA-Z0-9]{1,39}$"),
+        id: new RegExp("^(([a-zA-Z]([\\-a-zA-Z0-9])*){1,39})$"),
+        password: new RegExp("^(([a-zA-Z]([\\-a-zA-Z0-9])*){1,39})$"),
+        freeText: new RegExp("^([\\sa-zA-Z0-9]){1,40}$"),
+        alpha: new RegExp("^([a-zA-Z0-9]){1,100}$"),
+        commaSeparated: new RegExp("/^[a-zA-Z0-9,]{1,80}$"),
+        unixId: new RegExp("^([a-z_][a-z0-9-_\\.\\-]{0,30})$"),
+        unixPermissions: new RegExp("^((([0-7]){1,4})|(\\*))$"),
+        osPath: new RegExp("^[^\\0 ]+$"),
+        twoDigits: new RegExp("^([0-9]){1,2}$"),
+        tableUri: new RegExp("^[^\\0]+$"),
+        versionNumbers: new RegExp("^[0-9]{1,2}\\.[0-9]{1,2}\\.[0-9]{1,2}$")
+      };
+
+    function acceptOnlyNumber(evt) {
+      var theEvent = evt || $window.event,
+        key = theEvent.keyCode || theEvent.which,
+        BACKSPACE = 8,
+        DEL = 46,
+        ARROW_KEYS = {left: 37, right: 39},
+        regex = /[0-9]|\./;
+
+      if (key === BACKSPACE || key === DEL || key === ARROW_KEYS.left || key === ARROW_KEYS.right) {
+        return true;
+      }
+
+      key = String.fromCharCode(key);
+
+      if (!regex.test(key)) {
+        theEvent.returnValue = false;
+        if (theEvent.preventDefault) { theEvent.preventDefault(); }
+      }
+    }
+    function acceptNoSpaces(evt) {
+      var theEvent = evt || $window.event,
+        key = theEvent.keyCode || theEvent.which,
+        SPACE = 32;
+
+      if (key === SPACE) {
+        theEvent.returnValue = false;
+        theEvent.preventDefault();
+        return false;
+      }
+    }
+
+    return {
+      messages: checkMessages,
+      patterns: checkPatterns,
+      nameAvailable: true,
+      displayValidations: {show: false, nameShow: false},
+      acceptOnlyNumber: acceptOnlyNumber,
+      acceptNoSpaces: acceptNoSpaces
+    };
+
+  }]);
+}());
+
+
+
+
+

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/common/xml-to-json-service.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/common/xml-to-json-service.js b/falcon-ui/app/js/services/common/xml-to-json-service.js
new file mode 100644
index 0000000..d054e89
--- /dev/null
+++ b/falcon-ui/app/js/services/common/xml-to-json-service.js
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  
+  var servicesModule = angular.module('app.services.x2js', []);
+
+  servicesModule.factory('X2jsService', function() {
+    var x2js = new X2JS(
+      {arrayAccessFormPaths: [
+        'feed.properties.property',
+        'feed.locations.location',
+        'feed.clusters.cluster',
+        'feed.clusters.cluster.locations.location',
+        'cluster.properties.property',
+        'cluster.locations.location',
+        'process.clusters.cluster',
+        'process.inputs.input',
+        'process.outputs.output'
+      ]});
+
+    return {
+      xml_str2json: function(string) {
+        return x2js.xml_str2json(string);
+      },
+
+      json2xml_str: function(jsonObj) {
+        return x2js.json2xml_str(jsonObj);
+      },
+
+      prettifyXml: function (xml) {
+        var formatted = '';
+        var reg = /(>)(<)(\/*)/g;
+        xml = xml.replace(reg, '$1\r\n$2$3');
+        var pad = 0;
+        jQuery.each(xml.split('\r\n'), function(index, node) {
+          var indent = 0;
+          if (node.match( /.+<\/\w[^>]*>$/ )) {
+            indent = 0;
+          } else if (node.match( /^<\/\w/ )) {
+            if (pad !== 0) {
+              pad -= 1;
+            }
+          } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
+            indent = 1;
+          } else {
+            indent = 0;
+          }
+
+          var padding = '';
+          for (var i = 0; i < pad; i++) {
+            padding += '  ';
+          }
+
+          formatted += padding + node + '\r\n';
+          pad += indent;
+        });
+
+        return formatted;
+      }
+    };
+
+  });
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/entity/entity-factory.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/entity/entity-factory.js b/falcon-ui/app/js/services/entity/entity-factory.js
new file mode 100644
index 0000000..c64ad98
--- /dev/null
+++ b/falcon-ui/app/js/services/entity/entity-factory.js
@@ -0,0 +1,248 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  var module = angular.module('app.services.entity.factory', []);
+
+  module.factory('EntityFactory', [function () {
+    return {
+      newFeed: function () {
+        return new Feed();
+      },
+
+      newFeedProperties: function () {
+        return feedProperties();
+      },
+
+      newFeedCustomProperties: function () {
+        return feedCustomProperties();
+      },
+
+      newFrequency: function (quantity, unit) {
+        return new Frequency(quantity, unit);
+      },
+
+      newLocation: function (type, path) {
+        return new Location(type, path);
+      },
+
+      newCluster: function (type, selected) {
+        return new Cluster(type, selected);
+      },
+
+      newEntry: function (key, value) {
+        return new Entry(key, value);
+      },
+
+      newProcess: function () {
+        return new Process();
+      },
+
+      newInput: function () {
+        return new Input();
+      },
+
+      newOutput: function () {
+        return new Output();
+      },
+
+      newEntity: function (type) {
+        if (type === 'feed') {
+          return this.newFeed();
+        }
+        if (type === 'process') {
+          return this.newProcess();
+        }
+      }
+
+    };
+  }]);
+
+  function Feed() {
+//    this.name = null;
+    this.name = "";
+    this.description = null;
+    this.groups = null;
+    this.tags = [new Entry(null, null)];
+    this.ACL = new ACL();
+    this.schema = new Schema();
+    this.frequency = new Frequency(null, 'hours');
+    this.lateArrival = new LateArrival();
+    this.availabilityFlag = null;
+    this.properties = feedProperties();
+    this.customProperties = [new Entry(null, null)];
+    this.storage = new Storage();
+    this.clusters = [new Cluster('source', true)];
+    this.timezone = null;
+  }
+
+
+  function ACL() {
+    this.owner = null;
+    this.group = null;
+    this.permission = '*';
+  }
+
+  function Schema() {
+    this.location = null;
+    this.provider = null;
+  }
+
+  function feedProperties() {
+    return [
+      new Entry('queueName', 'default'),
+      new Entry('jobPriority', ''),
+      new Entry('timeout', new Frequency(1, 'hours')),
+      new Entry('parallel', 3),
+      new Entry('maxMaps', 8),
+      new Entry('mapBandwidthKB', 1024)
+    ];
+  }
+
+  function feedCustomProperties() {
+    return [
+      new Entry(null, null)
+    ];
+  }
+
+  function LateArrival() {
+    this.active = false;
+    this.cutOff = new Frequency(null, 'hours');
+  }
+
+  function Frequency(quantity, unit) {
+    this.quantity = quantity;
+    this.unit = unit;
+  }
+
+  function Entry(key, value) {
+    this.key = key;
+    this.value = value;
+  }
+
+  function Storage() {
+    this.fileSystem = new FileSystem();
+    this.catalog = new Catalog();
+  }
+
+  function Catalog() {
+    this.active = false;
+    this.catalogTable = new CatalogTable();
+  }
+
+  function CatalogTable() {
+    this.uri = null;
+    this.focused = false;
+  }
+
+  function FileSystem() {
+    this.active = true;
+    this.locations = [new Location('data','/'), new Location('stats','/'), new Location('meta','/')];
+  }
+
+  function Location(type, path) {
+    this.type = type;
+    this.path= path;
+    this.focused = false;
+  }
+
+  function Cluster(type, selected) {
+//    this.name = null;
+	this.name = "";
+    this.type = type;
+    this.selected = selected;
+    this.retention = new Frequency(null, 'hours');
+    this.retention.action = 'delete';
+    this.validity = new Validity();
+    this.storage = new Storage();
+  }
+
+  function Validity() {
+    this.start = new DateAndTime();
+    this.end = new DateAndTime();
+    this.timezone = "";
+  }
+
+  function DateAndTime() {
+    this.date = "";
+    this.time = currentTime();
+    this.opened = false;
+  }
+
+  /*function currentDate() {
+    var now = new Date();
+    return now;
+  }*/
+
+  function currentTime() {
+    return new Date(1900, 1, 1, 0, 0, 0);
+  }
+
+  function Process() {
+    this.name = null;
+    this.tags = [new Entry(null, null)];
+    this.workflow = new Workflow();
+    this.timezone = null;
+    this.frequency = new Frequency(null, 'hours');
+    this.parallel = 1;
+    this.order = "";
+    this.retry = new Retry();
+    this.clusters = [new Cluster('source', true)];
+    this.inputs = [];
+    this.outputs = [];
+
+    /*
+    this.name = 'P';
+    this.workflow.name = 'W';
+    this.workflow.engine = 'oozie';
+    this.workflow.version = '3.3.1';
+    this.frequency.quantity = '2';
+    this.retry.attempts = '4';
+    this.retry.delay.quantity = '4';
+    this.clusters[0].name = 'backupCluster';
+    this.tags = [{key: 'tag1', value: 'value1'},{key: 'tag2', value: 'value2'}];
+    */
+  }
+
+  function Workflow() {
+    this.name = null;
+    this.engine = null;
+    this.version = '';
+    this.path = '/';
+  }
+
+  function Retry() {
+    this.policy = '';
+    this.attempts = null;
+    this.delay = new Frequency(null, '');
+  }
+
+  function Input() {
+    this.name = null;
+    this.feed = "";
+    this.start = null;
+    this.end = null;
+  }
+
+  function Output() {
+    this.name = null;
+    this.feed = null;
+    this.outputInstance = null;
+  }
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/entity/entity-messages.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/entity/entity-messages.js b/falcon-ui/app/js/services/entity/entity-messages.js
new file mode 100644
index 0000000..76f4cd2
--- /dev/null
+++ b/falcon-ui/app/js/services/entity/entity-messages.js
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  var module = angular.module('app.services.messages', []);
+
+  var service = new MessagesService();
+
+  module.factory('MessagesService', [function() {
+    return service;
+  }]);
+
+  function MessagesService() {
+    this.messages = {
+      error: [],
+      info: []
+    };
+  }
+
+  MessagesService.prototype.validateCategory = function(category) {
+    if(!this.messages[category]) {
+      throw new Error('Category not registered');
+    }
+  };
+
+  MessagesService.prototype.push = function(category, title, detail) {
+    this.validateCategory(category);
+    this.messages[category].push(new Message(title, detail));
+  };
+
+  MessagesService.prototype.pop = function(category) {
+    this.validateCategory(category);
+    return this.messages[category].pop();
+  };
+
+
+  function Message(title, detail) {
+    this.title = title;
+    this.detail = detail;
+  }
+
+})();

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/entity/entity-model.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/entity/entity-model.js b/falcon-ui/app/js/services/entity/entity-model.js
new file mode 100644
index 0000000..3398af2
--- /dev/null
+++ b/falcon-ui/app/js/services/entity/entity-model.js
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  var module = angular.module('app.services.entity.model', []);
+
+  module.factory('EntityModel', ["X2jsService", function(X2jsService) {
+
+    var EntityModel = {};
+
+    EntityModel.json = null;
+    EntityModel.detailsPageModel = null;  
+
+    EntityModel.identifyType = function(json) {
+      if(json.feed) { EntityModel.type = "feed"; }
+      else if(json.cluster) { EntityModel.type = "cluster"; }
+      else if(json.process) { EntityModel.type = "process"; }
+      else { EntityModel.type = 'Type not recognized'; }
+    };
+
+    EntityModel.getJson = function(xmlString) {
+      EntityModel.json = X2jsService.xml_str2json( xmlString );
+      return EntityModel.identifyType(EntityModel.json);
+    };
+
+    EntityModel.clusterModel = {
+      cluster:{
+        tags: "",
+        interfaces:{
+          interface:[
+            {
+              _type:"readonly",
+              _endpoint:"hftp://sandbox.hortonworks.com:50070",
+              _version:"2.2.0"       
+            },
+            {
+              _type:"write",
+              _endpoint:"hdfs://sandbox.hortonworks.com:8020",
+              _version:"2.2.0"
+         
+            },
+            {
+              _type:"execute",
+              _endpoint:"sandbox.hortonworks.com:8050",
+              _version:"2.2.0"
+           
+            },
+            {
+              _type:"workflow",
+              _endpoint:"http://sandbox.hortonworks.com:11000/oozie/",
+              _version:"4.0.0"
+           
+            },
+            {
+              _type:"messaging",
+              _endpoint:"tcp://sandbox.hortonworks.com:61616?daemon=true",
+              _version:"5.1.6"
+            
+            }
+          ]
+        },
+        locations:{
+          location:[
+            {_name: "staging", _path: ""},
+            {_name: "temp", _path: ""},
+            {_name: "working", _path: ""}
+          ]
+        },
+        ACL: {
+          _owner: "",
+          _group: "",
+          _permission: ""
+        },
+        properties: {
+          property: [
+            { _name: "", _value: ""}
+          ]
+        },
+        _xmlns:"uri:falcon:cluster:0.1",       
+        _name:"",
+        _description:"",
+        _colo:""
+      }
+    };
+
+    EntityModel.feedModel = {
+      feed: {
+        tags: "",
+        groups: "",
+        frequency: "",
+        timezone: "",
+        "late-arrival": {
+          "_cut-off": ""
+        },
+        clusters: [{
+          "cluster": {
+            validity: {
+              _start: "",
+              _end: ""
+            },
+            retention: {
+              _limit: "",
+              _action: ""
+            },
+            _name: "",
+            _type: "source"
+          }
+        }],
+        locations: {
+          location: [{
+            _type: "data",
+            _path: "/none"
+          }, {
+            _type: "stats",
+            _path: "/none"
+          }, {
+            _type: "meta",
+            _path: "/none"
+          }]
+        },
+        ACL: {
+          _owner: "",
+          _group: "",
+          _permission: ""
+        },
+        schema: {
+          _location: "/none",
+          _provider: "none"
+        },
+        _xmlns: "uri:falcon:feed:0.1",
+        _name: "",
+        _description: ""
+      }
+    };
+
+    return EntityModel;
+
+  }]);
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/entity/entity-serializer.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/entity/entity-serializer.js b/falcon-ui/app/js/services/entity/entity-serializer.js
new file mode 100644
index 0000000..b7d723e
--- /dev/null
+++ b/falcon-ui/app/js/services/entity/entity-serializer.js
@@ -0,0 +1,510 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  var module = angular.module('app.services.entity.serializer',
+    ['app.services.json.transformer',
+      'app.services',
+      'app.services.entity.factory']);
+
+  module.factory('EntitySerializer',
+    ['EntityFactory', 'JsonTransformerFactory', 'X2jsService',
+    function(EntityFactory, JsonTransformerFactory, X2jsService) {
+
+    return {
+      preSerialize: function(feed, type) {
+        if(type === 'feed') {
+          if(feed.properties) {
+            feed.allproperties = feed.properties.concat(feed.customProperties);
+          }
+          return preSerializeFeed(feed, JsonTransformerFactory);
+        } else if(type === 'process') {
+          return preSerializeProcess(feed, JsonTransformerFactory);
+        }
+      },
+
+      serialize: function(feed, type) {
+        return X2jsService.json2xml_str(this.preSerialize(feed, type));
+      },
+
+      preDeserialize: function(entityModel, type) {
+        if(type === 'feed') {
+          return preDeserializeFeed(entityModel, JsonTransformerFactory);
+        } else if(type === 'process') {
+          return preDeserializeProcess(entityModel, JsonTransformerFactory);
+        }
+      },
+
+      deserialize: function(xml, type) {
+        var entityModel = X2jsService.xml_str2json(xml);
+        return this.preDeserialize(entityModel, type);
+      }
+
+    };
+
+      function keyValuePairs(input) {
+        return input.filter(emptyKey).map(entryToString).join(',');
+      }
+
+      function emptyKey (input) {
+        return input.key;
+      }
+
+      function emptyValue (input) {
+        return input && input.value;
+      }
+
+      function emptyFrequency (input) {
+        return input.value.unit ? input.value.quantity : input.value;
+      }
+
+      function entryToString(input) {
+        return input.key + '=' + input.value;
+      }
+
+      function frequencyToString(input) {
+        return input.quantity ? input.unit + '(' + input.quantity + ')' : null;
+      }
+
+      function pad(n) {
+        return String("00" + n).slice(-2);
+      }
+
+      function timeAndDateToString(input) {
+        if(input.date !== "") {
+          var dateComponent =
+          input.date.getFullYear() + '-' +
+          pad(input.date.getMonth()+1) + '-' +
+          pad(input.date.getDate());
+        }
+        else {
+          var dateComponent = "";
+        }
+
+        if(input.time !== "") {
+          var timeComponent =
+          pad(input.time.getHours()) + ':' +
+          pad(input.time.getMinutes());
+        }
+        else {
+          var timeComponent = "00:00";
+        }
+
+        return dateComponent + 'T' + timeComponent + 'Z';
+      }
+
+      function emptyElement() {return {};}
+
+      function EntityModel(type) {
+        this[type] = {_xmlns: 'uri:falcon:' + type + ':0.1'};
+      }
+
+      function preSerializeFeed (feed, transformerFactory) {
+        var propertyTransform = transformerFactory
+          .transform('key', '_name')
+          .transform('value', '_value', function(value) {
+            return value.quantity ? frequencyToString(value) : value;
+          });
+
+        var locationTransform = transformerFactory
+          .transform('type', '_type')
+          .transform('path', '_path');
+
+        var clusterTransform = transformerFactory
+          .transform('name', '_name')
+          .transform('type', '_type')
+          .transform('validity.start', 'validity._start', timeAndDateToString)
+          .transform('validity.end', 'validity._end', timeAndDateToString)
+          .transform('retention', 'retention._limit', frequencyToString)
+          .transform('retention.action', 'retention._action')
+          .transform('storage.fileSystem', 'locations.location', function(fileSystem) {
+            return feed.storage.fileSystem.active ? transformfileSystem(fileSystem) : null;
+          })
+          .transform('storage.catalog', 'table', function(catalog) {
+            return feed.storage.catalog.active ? transformCatalog(catalog) : null;
+          });
+
+        var transform = transformerFactory
+          .transform('name', 'feed._name')
+          .transform('description', 'feed._description')
+          .transform('tags', 'feed.tags', keyValuePairs)
+          .transform('groups', 'feed.groups')
+          .transform('availabilityFlag', 'feed.availabilityFlag')
+          .transform('frequency', 'feed.frequency', frequencyToString)
+          .transform('timezone', 'feed.timezone')
+          .transform('lateArrival.cutOff', 'feed.late-arrival._cut-off', frequencyToString)
+          .transform('clusters', 'feed.clusters.cluster', function(clusters) {
+            return clusters.map(function(cluster) {
+              return clusterTransform.apply(cluster, {});
+            });
+          })
+          .transform('storage.fileSystem', 'feed.locations.location', function(fileSystem) {
+            return fileSystem.active ? transformfileSystem(fileSystem) : null;
+          })
+          .transform('storage.catalog', 'feed.table', function(catalog) {
+            return catalog.active ? transformCatalog(catalog) : null;
+          })
+          .transform('ACL', 'feed.ACL', emptyElement)
+          .transform('ACL.owner', 'feed.ACL._owner')
+          .transform('ACL.group', 'feed.ACL._group')
+          .transform('ACL.permission', 'feed.ACL._permission')
+          .transform('schema', 'feed.schema', emptyElement)
+          .transform('schema.location', 'feed.schema._location')
+          .transform('schema.provider', 'feed.schema._provider')
+          .transform('allproperties', 'feed.properties.property', function(properties) {
+            return properties.filter(emptyValue).filter(emptyFrequency).map(function(property) {
+              return propertyTransform.apply(property, {});
+            });
+          });
+
+        function transformfileSystem (fileSystem) {
+          return fileSystem.locations.map(function(location) {
+            return locationTransform.apply(location, {});
+          });
+        }
+
+        function transformCatalog(catalog) {
+          return {_uri : catalog.catalogTable.uri};
+        }
+
+        return transform.apply(feed, new EntityModel('feed'));
+
+      }
+
+      function preSerializeProcess (process, transformerFactory) {
+
+        var clusterTransform = transformerFactory
+          .transform('name', '_name')
+          .transform('validity.start', 'validity._start', timeAndDateToString)
+          .transform('validity.end', 'validity._end', timeAndDateToString);
+
+        var inputTransform = transformerFactory
+          .transform('name', '_name')
+          .transform('feed', '_feed')
+          .transform('start', '_start')
+          .transform('end', '_end');
+
+        var outputTransform = transformerFactory
+          .transform('name', '_name')
+          .transform('feed', '_feed')
+          .transform('outputInstance', '_instance');
+
+        var transform = transformerFactory
+          .transform('name', 'process._name')
+          .transform('tags', 'process.tags', keyValuePairs)
+          .transform('clusters', 'process.clusters.cluster', function(clusters) {
+            return clusters.map(function(cluster) {
+              return clusterTransform.apply(cluster, {});
+            });
+          })
+          .transform('parallel', 'process.parallel')
+          .transform('order', 'process.order')
+          .transform('frequency', 'process.frequency', frequencyToString)
+          .transform('timezone', 'process.timezone')
+          .transform('inputs', 'process.inputs.input', function(inputs) {
+            if(inputs.length === 0) {
+              return null;
+            }
+            return inputs.map(function(input) {
+              return inputTransform.apply(input, {});
+            });
+          })
+          .transform('outputs', 'process.outputs.output', function(outputs) {
+            if(outputs.length === 0) {
+              return null;
+            }
+            return outputs.map(function(output) {
+              return outputTransform.apply(output, {});
+            });
+          })
+          .transform('workflow.name', 'process.workflow._name')
+          .transform('workflow.version', 'process.workflow._version')
+          .transform('workflow.engine', 'process.workflow._engine')
+          .transform('workflow.path', 'process.workflow._path')
+          .transform('retry.policy', 'process.retry._policy')
+          .transform('retry.delay', 'process.retry._delay', frequencyToString)
+          .transform('retry.attempts', 'process.retry._attempts');
+
+
+        return transform.apply(process, new EntityModel('process'));
+
+      }
+
+      function preDeserializeFeed(feedModel, transformerFactory) {
+        var feed = EntityFactory.newFeed();
+        feed.storage.fileSystem.active = false;
+
+        var clusterTransform = transformerFactory
+            .transform('_name', 'name')
+            .transform('_type', 'type')
+            .transform('validity._start', 'validity.start.date', parseDate)
+            .transform('validity._start', 'validity.start.time', parseTime)
+            .transform('validity._end', 'validity.end.date', parseDate)
+            .transform('validity._end', 'validity.end.time', parseTime)
+            .transform('retention._limit', 'retention', parseFrequency)
+            .transform('retention._action', 'retention.action')
+            .transform('locations', 'storage.fileSystem.active', parseBoolean)
+            .transform('locations.location', 'storage.fileSystem.locations', parseLocations)
+            .transform('table', 'storage.catalog.active', parseBoolean)
+            .transform('table._uri', 'storage.catalog.catalogTable.uri')
+          ;
+
+        var transform = transformerFactory
+            .transform('_name', 'name')
+            .transform('_description', 'description')
+            .transform('tags', 'tags', parseKeyValuePairs)
+            .transform('groups','groups')
+            .transform('ACL._owner','ACL.owner')
+            .transform('ACL._group','ACL.group')
+            .transform('ACL._permission','ACL.permission')
+            .transform('schema._location','schema.location')
+            .transform('schema._provider','schema.provider')
+            .transform('frequency','frequency', parseFrequency)
+            .transform('late-arrival','lateArrival.active', parseBoolean)
+            .transform('late-arrival._cut-off','lateArrival.cutOff', parseFrequency)
+            .transform('availabilityFlag', 'availabilityFlag')
+            .transform('properties.property', 'customProperties', parseProperties(isCustomProperty, EntityFactory.newFeedCustomProperties()))
+            .transform('properties.property', 'properties', parseProperties(isFalconProperty, EntityFactory.newFeedProperties()))
+            .transform('locations', 'storage.fileSystem.active', parseBoolean)
+            .transform('locations.location', 'storage.fileSystem.locations', parseLocations)
+            .transform('table', 'storage.catalog.active', parseBoolean)
+            .transform('table._uri', 'storage.catalog.catalogTable.uri')
+            .transform('clusters.cluster', 'clusters', parseClusters(clusterTransform))
+            .transform('timezone', 'timezone')
+          ;
+
+        return transform.apply(angular.copy(feedModel.feed), feed);
+      }
+
+      function preDeserializeProcess(processModel, transformerFactory) {
+        var process = EntityFactory.newProcess();
+
+        var clusterTransform = transformerFactory
+            .transform('_name', 'name')
+            .transform('validity._start', 'validity.start.date', parseDate)
+            .transform('validity._start', 'validity.start.time', parseTime)
+            .transform('validity._end', 'validity.end.date', parseDate)
+            .transform('validity._end', 'validity.end.time', parseTime);
+
+        var inputTransform = transformerFactory
+          .transform('_name', 'name')
+          .transform('_feed', 'feed')
+          .transform('_start', 'start')
+          .transform('_end', 'end');
+
+        var outputTransform = transformerFactory
+          .transform('_name', 'name')
+          .transform('_feed', 'feed')
+          .transform('_instance', 'outputInstance');
+
+        var transform = transformerFactory
+          .transform('_name', 'name')
+          .transform('tags', 'tags', parseKeyValuePairs)
+          .transform('workflow._name', 'workflow.name')
+          .transform('workflow._version', 'workflow.version')
+          .transform('workflow._engine', 'workflow.engine')
+          .transform('workflow._path', 'workflow.path')
+          .transform('timezone', 'timezone')
+          .transform('frequency','frequency', parseFrequency)
+          .transform('parallel','parallel')
+          .transform('order','order')
+          .transform('retry._policy','retry.policy')
+          .transform('retry._attempts','retry.attempts')
+          .transform('retry._delay','retry.delay', parseFrequency)
+          .transform('clusters.cluster', 'clusters', parseClusters(clusterTransform))
+          .transform('inputs.input', 'inputs', parseInputs(inputTransform))
+          .transform('outputs.output', 'outputs', parseOutputs(outputTransform));
+
+
+        function parseClusters(transform) {
+          return function(clusters) {
+            var result = clusters.map(parseCluster(transform));
+            return  result;
+          };
+        }
+
+        return transform.apply(angular.copy(processModel.process), process);
+      }
+
+      function parseDate(input) {
+        var dateComponent = (input.split('T')[0]).split('-');
+        return newUtcDate(dateComponent[0], dateComponent[1], dateComponent[2]);
+      }
+
+      function parseTime(input) {
+        var timeComponent = (input.split('T')[1].split('Z')[0]).split(':');
+        return newUtcTime(timeComponent[0], timeComponent[1]);
+      }
+
+      function parseClusters(transform) {
+        return function(clusters) {
+          var result = clusters.map(parseCluster(transform));
+          selectFirstSourceCluster(result);
+          return  result;
+        };
+      }
+
+      function parseInputs(transform) {
+        return function(inputs) {
+          return inputs.map(parseInput(transform));
+        };
+      }
+
+      function parseInput(transform) {
+        return function(input) {
+          return transform.apply(input, EntityFactory.newInput());
+        };
+      }
+
+      function parseOutputs(transform) {
+        return function(outputs) {
+          return outputs.map(parseOutput(transform));
+        };
+      }
+
+      function parseOutput(transform) {
+        return function(output) {
+          return transform.apply(output, EntityFactory.newOutput());
+        };
+      }
+
+      function parseCluster(transform) {
+        return function(input) {
+          var cluster = EntityFactory.newCluster('target', false);
+          cluster.storage.fileSystem.active = false;
+          return  transform.apply(input, cluster);
+        };
+      }
+
+      function selectFirstSourceCluster(clusters) {
+        for(var i = 0, n = clusters.length; i < n; i++) {
+          if(clusters[i].type === 'source') {
+            clusters[i].selected = true;
+            return;
+          }
+        }
+      }
+
+      function parseKeyValue(keyValue) {
+        var parsedPair = keyValue.split('=');
+        return EntityFactory.newEntry(parsedPair[0], parsedPair[1]);
+      }
+
+      function parseKeyValuePairs(tagsString) {
+        return tagsString.split(',').map(parseKeyValue);
+      }
+
+      function parseFrequency(frequencyString) {
+        var parsedFrequency = frequencyString.split('(');
+        return EntityFactory.newFrequency(parsedFrequency[1].split(')')[0], parsedFrequency[0]);
+      }
+
+      function parseBoolean(input) {
+        return !!input;
+      }
+
+      function parseProperties(filterCallback, defaults) {
+        return function(properties) {
+          var result = filter(properties, filterCallback).map(parseProperty);
+          return merge(defaults, result);
+        };
+      }
+
+
+      function merge(defaults, newValues) {
+        var result = angular.copy(defaults);
+        var defaultMap = indexBy(result, 'key');
+
+        newValues.forEach(function(newValue) {
+          if(defaultMap[newValue.key]) {
+            defaultMap[newValue.key].value = newValue.value;
+          } else {
+            result.push(newValue);
+          }
+        });
+
+
+        return result;
+      }
+
+
+      function filter(array, callback) {
+        var out = [];
+        for(var i = 0, n = array.length; i < n; i++) {
+          if(callback(array[i])) {
+            out.push(array[i]);
+          }
+        }
+        return out;
+      }
+
+
+      function parseProperty(property) {
+        var value = property._name !== 'timeout' ? property._value : parseFrequency(property._value);
+        return EntityFactory.newEntry(property._name, value);
+      }
+
+      function parseLocations(locations) {
+        return locations.map(parseLocation);
+      }
+
+      function parseLocation(location) {
+        return EntityFactory.newLocation(location._type, location._path);
+      }
+
+      function indexBy(array, property) {
+        var map = {};
+
+        array.forEach(function(element) {
+          map[element[property]] = element;
+        });
+
+        return map;
+      }
+
+      function newUtcDate(year, month, day) {
+        return new Date(year, (month-1), day);
+      }
+
+      function newUtcTime(hours, minutes) {
+        return new Date(1900, 1, 1, hours, minutes, 0);
+      }
+
+  }]);
+
+
+  var falconProperties = {
+    queueName: true,
+    jobPriority: true,
+    timeout: true,
+    parallel: true,
+    maxMaps: true,
+    mapBandwidthKB: true
+  };
+
+
+  function isCustomProperty(property) {
+    return !falconProperties[property._name];
+  }
+
+  function isFalconProperty(property) {
+    return falconProperties[property._name];
+  }
+
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/js/services/services.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/js/services/services.js b/falcon-ui/app/js/services/services.js
new file mode 100644
index 0000000..98235d3
--- /dev/null
+++ b/falcon-ui/app/js/services/services.js
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict';
+  
+  angular.module('app.services', [
+    'app.services.falcon', 
+    'app.services.fileapi', 
+    'app.services.json.transformer',
+    'app.services.x2js',
+    'app.services.validation',   
+    'app.services.entity.serializer',
+    'app.services.entity.factory',
+    'app.services.entity.model'
+  ]);
+
+})();

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/test/controllers/HeaderControllerSpec.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/test/controllers/HeaderControllerSpec.js b/falcon-ui/app/test/controllers/HeaderControllerSpec.js
new file mode 100644
index 0000000..13a73be
--- /dev/null
+++ b/falcon-ui/app/test/controllers/HeaderControllerSpec.js
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () { 
+  'use strict';
+
+  describe('HeaderController', function () {
+    var controller,
+        entityModel = {},
+        scope;
+
+    beforeEach(module('app.controllers.navHeader'));
+    
+    beforeEach(inject(function($rootScope, $controller) {
+
+      scope = $rootScope.$new();
+   
+      controller = $controller('HeaderController', { 
+        $scope: scope,
+        EntityModel: entityModel,
+        $state: {
+          $current: {
+            name: 'forms.feed.general'
+          },
+          go: angular.noop
+        }
+      });
+      
+    }));
+
+    it('should reset EntityModel.clusterModel', function() {
+      expect(entityModel).toEqual({});
+      expect(entityModel.clusterModel).toBeUndefined();
+      scope.resetCluster();
+      expect(entityModel.clusterModel).not.toBeUndefined();
+      expect(entityModel.clusterModel).toEqual(
+        {cluster:{tags: "",interfaces:{interface:[
+            {_type:"readonly",_endpoint:"hftp://sandbox.hortonworks.com:50070",_version:"2.2.0"},
+            {_type:"write",_endpoint:"hdfs://sandbox.hortonworks.com:8020",_version:"2.2.0"},
+            {_type:"execute",_endpoint:"sandbox.hortonworks.com:8050",_version:"2.2.0"},
+            {_type:"workflow",_endpoint:"http://sandbox.hortonworks.com:11000/oozie/",_version:"4.0.0"},
+            {_type:"messaging",_endpoint:"tcp://sandbox.hortonworks.com:61616?daemon=true",_version:"5.1.6"}
+          ]},locations:{location:[{_name: "staging", _path: ""},{_name: "temp", _path: ""},{_name: "working", _path: ""}]},
+          ACL: {_owner: "",_group: "",_permission: ""},properties: {property: [{ _name: "", _value: ""}]},
+          _xmlns:"uri:falcon:cluster:0.1",_name:"",_description:"",_colo:""}
+        }
+      );
+    });
+    
+  });
+  
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/falcon/blob/c4df0a5e/falcon-ui/app/test/controllers/MainControllerSpec.js
----------------------------------------------------------------------
diff --git a/falcon-ui/app/test/controllers/MainControllerSpec.js b/falcon-ui/app/test/controllers/MainControllerSpec.js
new file mode 100644
index 0000000..26e92fc
--- /dev/null
+++ b/falcon-ui/app/test/controllers/MainControllerSpec.js
@@ -0,0 +1,203 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function () {
+  'use strict'; 
+  var q;
+  var scope;
+  var controller;
+  var falconServiceMock = jasmine.createSpyObj('Falcon', ['getEntities', 'getEntityDefinition', 'logResponse', 'logRequest']);
+  var x2jsServiceMock = jasmine.createSpyObj('X2jsService', ['xml_str2json']);
+  var stateMock = jasmine.createSpyObj('state', ['go']);
+  var entityModel = {};
+
+
+  describe('MainController', function () {
+
+    beforeEach(module('app.controllers'));
+
+    beforeEach(inject(function($q, $rootScope, $controller) {
+      q = $q;
+
+      var promise = {};
+      promise.success = function() {return {error: function() {}}};
+
+
+      scope = $rootScope.$new();
+      scope.$parent.refreshLists = angular.noop;
+      scope.models = {};
+      falconServiceMock.getEntities.andReturn(successResponse({}));
+
+      controller = $controller('DashboardCtrl', {
+        $scope: scope,
+        $timeout: {},
+        Falcon: falconServiceMock,
+        FileApi: {},
+        EntityModel: entityModel,
+        $state: stateMock,
+        X2jsService: x2jsServiceMock
+      });
+    }));
+
+
+
+    describe('editEntity', function() {
+
+      it('Should invoke the Falcon.getEntityDefinition', function() {
+        falconServiceMock.getEntityDefinition.andReturn(successResponse({}));
+
+        scope.editEntity('feed', 'myFeed');
+
+        expect(falconServiceMock.getEntityDefinition).toHaveBeenCalled();
+      });
+
+      describe('call to the api was successful', function() {
+        it('Should set the retrieved entity from the server into EntityModel', function () {
+          var myFeed = {};
+          falconServiceMock.getEntityDefinition.andReturn(successResponse({}));
+          x2jsServiceMock.xml_str2json.andReturn(myFeed);
+
+          scope.editEntity('feed', 'myFeed');
+
+          expect(entityModel.feedModel).toBe(myFeed);
+        });
+
+        it('Should set editing mode to true', function () {
+          falconServiceMock.getEntityDefinition.andReturn(successResponse({}));
+          scope.editingMode = false;
+
+          scope.editEntity('feed', 'myFeed');
+
+          expect(scope.editingMode).toBe(true);
+        });
+
+        it('Should navigate to the appropriate landing page for the entity type', function () {
+          falconServiceMock.getEntityDefinition.andReturn(successResponse());
+          scope.editingMode = false;
+
+          scope.editEntity('feed', 'myFeed');
+
+          expect(stateMock.go).toHaveBeenCalledWith('forms.feed.general');
+        });
+
+        it('Should set a copy of the model into the scope', function () {
+          var feedModel = {name: 'MyFeed'};
+          falconServiceMock.getEntityDefinition.andReturn(successResponse());
+          x2jsServiceMock.xml_str2json.andReturn(feedModel);
+
+          scope.editEntity('feed', 'myFeed');
+
+          expect(scope.models.feedModel).toNotBe(feedModel);
+          expect(scope.models.feedModel).toEqual(feedModel);
+        });
+      });
+
+      xdescribe('call to the api errored out', function() {
+        it('Should set the retrieved entity from the server into EntityModel', function () {
+          var error = {result: 'error message'};
+          falconServiceMock.success = true;
+          falconServiceMock.getEntityDefinition.andReturn(errorResponse());
+          x2jsServiceMock.xml_str2json.andReturn(error);
+
+          scope.editEntity('feed', 'myFeed');
+
+          expect(falconServiceMock.success).toBe(false);
+          expect(falconServiceMock.serverResponse).toBe('error message');
+        });
+
+      });
+    });
+
+    describe('clone entity', function() {
+      it('Should invoke the Falcon.getEntityDefinition', function() {
+        var myFeed = {feed: {}};
+        falconServiceMock.getEntityDefinition.andReturn(successResponse({}));
+        x2jsServiceMock.xml_str2json.andReturn(myFeed);
+
+        scope.cloneEntity('feed', 'myFeed');
+
+        expect(falconServiceMock.getEntityDefinition).toHaveBeenCalled();
+      });
+
+      describe('call to the api was successful', function() {
+        it('Should set the retrieved entity from the server into EntityModel', function () {
+          var myFeed = {feed: {}};
+          falconServiceMock.getEntityDefinition.andReturn(successResponse({}));
+          x2jsServiceMock.xml_str2json.andReturn(myFeed);
+
+          scope.cloneEntity('feed', 'myFeed');
+
+          expect(entityModel.feedModel).toBe(myFeed);
+        });
+
+        it('Should set clone mode to true', function () {
+          falconServiceMock.getEntityDefinition.andReturn(successResponse({}));
+          scope.cloningMode = false;
+
+          scope.cloneEntity('feed', 'myFeed');
+
+          expect(scope.cloningMode).toBe(true);
+        });
+
+        it('Should navigate to the appropriate landing page for the entity type', function () {
+          falconServiceMock.getEntityDefinition.andReturn(successResponse());
+          scope.cloningMode = false;
+
+          scope.cloneEntity('feed', 'myFeed');
+
+          expect(stateMock.go).toHaveBeenCalledWith('forms.feed.general');
+        });
+
+        it('Should set a copy of the model into the scope', function () {
+          var feedModel = {feed: {name: 'MyFeed'}};
+          falconServiceMock.getEntityDefinition.andReturn(successResponse());
+          x2jsServiceMock.xml_str2json.andReturn(feedModel);
+
+          scope.cloneEntity('feed', 'myFeed');
+
+          expect(scope.models.feedModel).toNotBe(feedModel);
+          expect(scope.models.feedModel).toEqual(feedModel);
+        });
+      });
+    });
+
+
+  });
+
+  function successResponse(value) {
+    var fakePromise = {};
+    fakePromise.success = function(callback) {
+      callback(value);
+      return fakePromise;
+    };
+    fakePromise.error = angular.noop;
+    return fakePromise;
+  }
+
+  function errorResponse(value) {
+    var fakePromise = {};
+    fakePromise.success = function() {
+      return fakePromise;
+    };
+    fakePromise.error = function(callback) {
+      callback(value);
+      return fakePromise;
+    };
+    return fakePromise;
+  }
+
+})();
\ No newline at end of file