Added support to Composer to support a model leaf of type empty.
- complete rewrite the the serialization code
- the new architecture should allow for more easily adding in validation and default value support
- properties are now written out in the order defined by the meta data (it could matter is some definitions)
- changed from using utils.resolvePath to use the lodash version
- did much manual testing and did test comparison of ping-vnfd sample with new generation

Change-Id: Ibd2bbdbdaa436b95ed19016f94425bfab4f85d8e
Signed-off-by: Bob Gallagher <bob.gallagher@riftio.com>
diff --git a/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js b/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
index 41e87b3..c1d65be 100644
--- a/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
+++ b/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
@@ -48,6 +48,8 @@
 
 import '../styles/EditDescriptorModelProperties.scss'
 
+const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
+
 function getDescriptorMetaBasicForType(type) {
 	const basicPropertiesFilter = d => _includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
 	return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
@@ -292,7 +294,37 @@
 					key={fieldKey} 
 					id={fieldKey} 
 					className={ClassNames({'-value-not-set': !isValueSet})} 
-					defaultValue={val && val.toUpperCase()} title={pathToProperty} 
+					defaultValue={val && val.toUpperCase()} 
+					title={pathToProperty} 
+					onChange={onSelectChange} 
+					onFocus={onFocus} 
+					onBlur={endEditing} 
+					onMouseDown={startEditing} 
+					onMouseOver={startEditing} 
+					readOnly={!isEditable}>
+						{options}
+				</select>
+			);
+		}
+		
+		if (Property.isLeafEmpty(property)) {
+			// A null value indicates the leaf exists (as opposed to undefined).
+			// We stick in a string when the user actually sets it to simplify things
+			// but the correct thing happens when we serialize to user data
+			let isEmptyLeafPresent = (value === EMPTY_LEAF_PRESENT || value === null); 
+			let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
+			const options = [
+				<option key={'true'} value={EMPTY_LEAF_PRESENT}>Enabled</option>,
+				<option key={'false'} value="">Not Enabled</option>
+			]
+
+			return (
+				<select 
+					key={fieldKey} 
+					id={fieldKey} 
+					className={ClassNames({'-value-not-set': !isEmptyLeafPresent})} 
+					defaultValue={present} 
+					title={pathToProperty} 
 					onChange={onSelectChange} 
 					onFocus={onFocus} 
 					onBlur={endEditing} 
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
index 5488e77..69098ec 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
@@ -4,10 +4,11 @@
  * This class provides methods to get the metadata about descriptor models.
  */
 
-'use strict';
-
 import _cloneDeep from 'lodash/cloneDeep'
-import utils from './../utils'
+import _isEmpty from 'lodash/isEmpty'
+import _pick from 'lodash/pick'
+import _get from 'lodash/get'
+import _set from 'lodash/set'
 import DescriptorModelMetaProperty from './DescriptorModelMetaProperty'
 import CommonUtils from 'utils/utils';
 const assign = Object.assign;
@@ -24,6 +25,156 @@
 	return type;
 }
 
+const uiStateToSave = ['containerPositionMap'];
+
+//////
+// Data serialization will be done on a meta model basis. That is,
+// given a schema and data, retrieve from the data only that which is 
+// defined by the schema.
+//
+
+// serialize data for a list of properties
+function serializeAll(properties, data) {
+	if (data) {
+		return properties.reduce((obj, p) => {
+			return Object.assign(obj, p.serialize(data));
+		}, {});
+	}
+	return null;
+}
+
+function serialize_container(data) {
+	data = data[this.name];
+	if (_isEmpty(data)) {
+		return null;
+	}
+	let obj = {};
+	obj[this.name] = serializeAll(this.properties, data);
+	return obj;
+}
+
+function serialize_list(data) {
+	data = data[this.name];
+	if (data) {
+		if (!Array.isArray(data)) {
+			return serializeAll(this.properties, data);
+		} else if (data.length) {
+			let list = data.reduce((c, d) => {
+				let obj = serializeAll(this.properties, d);
+				if (!_isEmpty(obj)) {
+					c.push(obj);
+				}
+				return c;
+			}, []);
+			if (!_isEmpty(list)){
+				let obj = {};
+				obj[this.name] = list;
+				return obj;
+			}
+		}
+	}
+	return null;
+}
+
+function serialize_leaf(data) {
+	let value = data[this.name];
+	if (value === null || typeof value === 'undefined' || value === '') {
+		return null;
+	}
+	let obj = {};
+	if (this['data-type'] === 'empty') {
+		value = ''; // empty string does get sent as value
+	}
+	obj[this.name] = value;
+	return obj;
+}
+
+function serialize_leaf_empty(data) {
+	let value = data[this.name];
+	if (value) {
+		let obj = {};
+		obj[this.name] = "";
+		return obj;
+	}
+	return null;
+}
+
+function serialize_leaf_list(data) {
+	data = data[this.name];
+	if (data) {
+		commaSeparatedValues = data.reduce((d, v) => {
+			let leaf = Serializer.leaf.call(this, d);
+			let value = leaf & leaf[this.name];
+			if (value && value.length) {
+				if (v.length) {
+					v += ', ';
+				}
+				v += value;
+			}
+		}, "");
+		if (commaSeparatedValues.length) {
+			let obj = {};
+			obj[this.name] = commaSeparatedValues;
+			return obj;
+		}
+	}
+	return null;
+}
+
+function serialize_choice(data) {
+	let keys = Object.keys(data);
+	if (keys) {
+		const chosen = this.properties.find(
+			c => c.type === 'case' && c.properties && c.properties.some(p => keys.indexOf(p.name) > -1));
+		return chosen && serializeAll(chosen.properties, data);
+	}
+	return null;
+}
+
+function serialize_case(data) {
+	return Serializer.container.call(this, data);
+}
+
+// special ui data handler for leaf of type string named 'meta' 
+function serialize_meta(data) {
+	let uiState = data['uiState'];
+	let meta = uiState && _pick(uiState, uiStateToSave);
+	// if there is no uiState to save perhaps this was not a ui state property
+	return _isEmpty(meta) ? null : {meta: JSON.stringify(meta)};
+}
+
+function serialize_unsupported(data) {
+	console.error('unsupported property', property);
+	return null;
+}
+
+function getSerializer(property) {
+	switch (property.name) {
+		case 'rw-nsd:meta':
+		case 'rw-vnfd:meta':
+			return serialize_meta.bind(property);
+	}
+	switch (property.type) {
+		case 'list':
+		return serialize_list.bind(property);
+		case 'container':
+		return serialize_container.bind(property);
+		case 'choice':
+		return serialize_choice.bind(property);
+		case 'case':
+		return serialize_case.bind(property);
+		case 'leaf_list':
+		return serialize_leaf_list.bind(property);
+		case 'leaf':
+		switch (property['data-type']){
+			case 'empty':
+			return serialize_leaf_empty.bind(property);
+		}
+		return serialize_leaf.bind(property);
+	}
+	return serialize_unsupported.bind(property);
+}
+
 let modelMetaByPropertyNameMap = [];
 
 let cachedDescriptorModelMetaRequest = null;
@@ -31,32 +182,38 @@
 export default {
 	init() {
 		if (!cachedDescriptorModelMetaRequest) {
-			cachedDescriptorModelMetaRequest = new Promise(function(resolve, reject) {
-				CommonUtils.getDescriptorModelMeta().then(function(data) {
+			cachedDescriptorModelMetaRequest = new Promise(function (resolve, reject) {
+				CommonUtils.getDescriptorModelMeta().then(function (data) {
 					let DescriptorModelMetaJSON = data;
 					modelMetaByPropertyNameMap = Object.keys(DescriptorModelMetaJSON).reduce((map, key) => {
 						function mapProperties(parentMap, parentObj) {
+							// let's beef up the meta info with a helper (more to come?)
+							parentObj.serialize = getSerializer(parentObj);
 							parentMap[':meta'] = parentObj;
 							const properties = parentObj && parentObj.properties ? parentObj.properties : [];
 							properties.forEach(p => {
-								parentMap[p.name] = mapProperties({}, assign(p, {':qualified-type': parentObj[':qualified-type'] + '.' + p.name}));
+								parentMap[p.name] = mapProperties({}, assign(p, {
+									':qualified-type': parentObj[':qualified-type'] + '.' + p.name
+								}));
 								return map;
 							}, parentMap);
 							return parentMap;
 						}
-						map[key] = mapProperties({}, assign(DescriptorModelMetaJSON[key], {':qualified-type': key}));
+						map[key] = mapProperties({}, assign(DescriptorModelMetaJSON[key], {
+							':qualified-type': key
+						}));
 						return map;
 					}, {});
 
 					(() => {
 						// initialize the UI centric properties that CONFD could care less about
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'nsd.meta.:meta.preserve-line-breaks', true);
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'vnfd.meta.:meta.preserve-line-breaks', true);
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'vnfd.vdu.cloud-init.:meta.preserve-line-breaks', true);
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'nsd.constituent-vnfd.vnf-configuration.config-template.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'nsd.meta.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'vnfd.meta.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'vnfd.vdu.cloud-init.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'nsd.constituent-vnfd.vnf-configuration.config-template.:meta.preserve-line-breaks', true);
 					})();
 					resolve();
-				}, function(error) {
+				}, function (error) {
 					cachedDescriptorModelMetaRequest = null;
 				})
 			})
@@ -78,7 +235,7 @@
 	},
 	getModelMetaForType(typeOrPath, filterProperties = () => true) {
 		// resolve paths like 'nsd' or 'vnfd.vdu' or 'nsd.constituent-vnfd'
-		const found = utils.resolvePath(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
+		const found = _get(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
 		if (found) {
 			const uiState = _cloneDeep(found[':meta']);
 			uiState.properties = uiState.properties.filter(filterProperties);
@@ -88,18 +245,18 @@
 	},
 	getModelFieldNamesForType(typeOrPath) {
 		// resolve paths like 'nsd' or 'vnfd.vdu' or 'nsd.constituent-vnfd'
-		const found = utils.resolvePath(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
+		const found = _get(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
 		if (found) {
 			let result = [];
 			found[':meta'].properties.map((p) => {
 				// if(false) {
-				if(p.type == 'choice') {
+				if (p.type == 'choice') {
 					result.push(p.name)
-					return p.properties.map(function(q){
+					return p.properties.map(function (q) {
 						result.push(q.properties[0].name);
 					})
 
-				} else  {
+				} else {
 					return result.push(p.name);
 				}
 			})
@@ -120,10 +277,10 @@
 	 *  will be used. 
 	 * @returns {string}
 	 */
-	generateItemUniqueName (list, property, prefix) {
-		if (   property.type !== 'list' 
-			|| property.key.length !== 1
-			|| property.properties.find(prop => prop.name === property.key[0])['data-type'] !== 'string') {
+	generateItemUniqueName(list, property, prefix) {
+		if (property.type !== 'list' ||
+			property.key.length !== 1 ||
+			property.properties.find(prop => prop.name === property.key[0])['data-type'] !== 'string') {
 			// only support list with a single key of type string
 			return null;
 		}
@@ -133,11 +290,12 @@
 		let key = property.key[0];
 		let suffix = list ? list.length + 1 : 1
 		let keyValue = prefix + '-' + suffix;
+
 		function makeUniqueName() {
 			if (list) {
 				for (let i = 0; i < list.length; i = ++i) {
 					if (list[i][key] === keyValue) {
-						keyValue = keyValue + '-' + (i+1);
+						keyValue = keyValue + '-' + (i + 1);
 						makeUniqueName(); // not worried about recursing too deep (chances ??)
 						break;
 					}
@@ -148,4 +306,4 @@
 		return keyValue;
 	}
 
-}
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
index 2955e55..e064457 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
@@ -21,8 +21,6 @@
  * This class provides utility methods for interrogating an instance of model uiState object.
  */
 
-'use strict';
-
 import _includes from 'lodash/includes'
 import _isArray from 'lodash/isArray'
 import guid from './../guid'
@@ -36,6 +34,9 @@
 	isBoolean(property = {}) {
 		return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'boolean')
 	},
+	isLeafEmpty(property = {}) {
+		return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'empty')
+	},
 	isLeaf(property = {}) {
 		return /leaf|choice/.test(property.type);
 	},
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
index 737078f..b496041 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
@@ -1,4 +1,3 @@
-
 /*
  *
  *   Copyright 2016 RIFT.IO Inc
@@ -20,227 +19,28 @@
  * Created by onvelocity on 10/20/15.
  */
 
-import _isNumber from 'lodash/isNumber'
-import _cloneDeep from 'lodash/cloneDeep'
-import _isEmpty from 'lodash/isEmpty'
-import _omit from 'lodash/omit'
-import _pick from 'lodash/pick'
-import utils from './../utils'
-import DescriptorModelFields from './DescriptorModelFields'
 import DescriptorModelMetaFactory from './DescriptorModelMetaFactory'
 
-let nsdFields = null;
-let vldFields = null;
-let vnfdFields = null;
-let cvnfdFields = null;
-
-
-
-
-/**
- * Serialize DescriptorModel JSON into CONFD JSON. Also, cleans up the data as needed.
- *
- * @type {{serialize: (function(*=)), ':clean': (function(*=)), nsd: {serialize: (function(*=))}, vld: {serialize: (function(*=))}, vnfd-connection-point-ref: {serialize: (function(*=))}, constituent-vnfd: {serialize: (function(*=))}, vnfd: {serialize: (function(*=))}, vdu: {serialize: (function(*=))}}}
- */
 const DescriptorModelSerializer = {
+	/**
+	 * Create a json object that can be sent to the backend. I.e. CONFD JSON compliant to the schema definition.
+	 * 
+	 * @param {any} model - the data blob from the editor. This is not modified.
+	 * @returns cleansed data model
+	 */
 	serialize(model) {
-		const type = model.uiState && model.uiState.type;
-		const serializer = this[type];
-		if (serializer) {
-			model = serializer.serialize(model);
-			this[':clean'](model);
-			return model;
+		if (!model.uiState) {
+			console.error('model uiState null', model);
+			return {};
 		}
-		return false;
-	},
-	':clean'(model) {
-		// remove uiState from all elements accept nsd and vnfd
-		// remove empty / blank value fields
-		function clean(m) {
-			Object.keys(m).forEach(k => {
-				const isEmptyObject = typeof m[k] === 'object' && _isEmpty(m[k]);
-				if (typeof m[k] === 'undefined' || isEmptyObject || m[k] === '') {
-					delete m[k];
-				}
-				const isMetaAllowed = /^nsd|vnfd$/.test(m.uiState && m.uiState.type);
-				if (k === 'uiState') {
-					if (isMetaAllowed) {
-						// remove any transient ui state properties
-						const uiState = _pick(m.uiState, DescriptorModelFields.meta);
-						if (!_isEmpty(uiState)) {
-							// uiState field must be a string
-							m['meta'] = JSON.stringify(uiState);
-						}
-					}
-					delete m[k];
-				}
-				if (typeof m[k] === 'object') {
-					clean(m[k]);
-				}
-			});
-		}
-		clean(model);
-		return model;
-	},
-	nsd: {
-		serialize(nsdModel) {
-			if(!nsdFields) nsdFields = DescriptorModelMetaFactory.getModelFieldNamesForType('nsd').concat('uiState');
-			const confd = _pick(nsdModel, nsdFields);
-
-			// vnfd is defined in the ETSI etsi_gs reference manual but RIFT does not use it
-			delete confd.vnfd;
-
-			// map the vnfd instances into the CONFD constituent-vnfd ref instances
-			confd['constituent-vnfd'] = confd['constituent-vnfd'].map((d, index) => {
-
-				const constituentVNFD = {
-					'member-vnf-index': d['member-vnf-index'],
-					'vnfd-id-ref': d['vnfd-id-ref']
-				};
-
-				if (d['vnf-configuration']) {
-					const vnfConfig = _cloneDeep(d['vnf-configuration']);
-					const configType = vnfConfig['config-type'] || 'none';
-					// make sure we send the correct values based on config type
-					if (configType === 'none') {
-						constituentVNFD['vnf-configuration'] = {'config-type': 'none'};
-						const configPriority = utils.resolvePath(vnfConfig, 'input-params.config-priority');
-						const configPriorityValue = _isNumber(configPriority) ? configPriority : d.uiState['member-vnf-index'];
-						utils.assignPathValue(constituentVNFD['vnf-configuration'], 'input-params.config-priority', configPriorityValue);
-					} else {
-						// remove any unused configuration options
-						['netconf', 'rest', 'script', 'juju'].forEach(type => {
-							if (configType !== type) {
-								delete vnfConfig[type];
-							}
-						});
-						constituentVNFD['vnf-configuration'] = vnfConfig;
-					}
-				}
-
-				if (d.hasOwnProperty('start-by-default')) {
-					constituentVNFD['start-by-default'] = d['start-by-default'];
-				}
-
-				return constituentVNFD;
-
-			});
-			for (var key in confd) {
-				checkForChoiceAndRemove(key, confd, nsdModel);
-			}
-			// serialize the VLD instances
-			confd.vld = confd.vld.map(d => {
-				return DescriptorModelSerializer.serialize(d);
-			});
-
-			return cleanEmptyTopKeys(confd);
-
-		}
-	},
-	vld: {
-		serialize(vldModel) {
-			if(!vldFields) vldFields = DescriptorModelMetaFactory.getModelFieldNamesForType('nsd.vld');
-			const confd = _pick(vldModel, vldFields);
-			const property = 'vnfd-connection-point-ref';
-
-			// TODO: There is a bug in RIFT-REST that is not accepting empty
-			// strings for string properties.
-			// once that is fixed, remove this piece of code.
-			// fix-start
-			for (var key in confd) {
-			  	if (confd.hasOwnProperty(key) && confd[key] === '') {
-                	delete confd[key];
-                } else {
-                	//removes choice properties from top level object and copies immediate children onto it.
-					checkForChoiceAndRemove(key, confd, vldModel);
-                }
-			}
-
-
-			const deepProperty = 'provider-network';
-			for (var key in confd[deepProperty]) {
-				if (confd[deepProperty].hasOwnProperty(key) && confd[deepProperty][key] === '') {
-					delete confd[deepProperty][key];
-				}
-			}
-			// fix-end
-			confd[property] = confd[property].map(d => DescriptorModelSerializer[property].serialize(d));
-			return cleanEmptyTopKeys(confd);
-		}
-	},
-	'vnfd-connection-point-ref': {
-		serialize(ref) {
-			return _pick(ref, ['member-vnf-index-ref', 'vnfd-id-ref', 'vnfd-connection-point-ref']);
-		}
-	},
-	'internal-connection-point': {
-		serialize(ref) {
-			return _pick(ref, ['id-ref']);
-		}
-	},
-	'constituent-vnfd': {
-		serialize(cvnfdModel) {
-			if(!cvnfdFields) cvnfdFields = DescriptorModelMetaFactory.getModelFieldNamesForType('nsd.constituent-vnfd');
-			return _pick(cvnfdModel, cvnfdFields);
-		}
-	},
-	vnfd: {
-		serialize(vnfdModel) {
-			if(!vnfdFields) vnfdFields = DescriptorModelMetaFactory.getModelFieldNamesForType('vnfd').concat('uiState');
-			const confd = _pick(vnfdModel, vnfdFields);
-			confd.vdu = confd.vdu.map(d => DescriptorModelSerializer.serialize(d));
-			return cleanEmptyTopKeys(confd);
-		}
-	},
-	vdu: {
-		serialize(vduModel) {
-			const copy = _cloneDeep(vduModel);
-			for (let k in copy) {
-				checkForChoiceAndRemove(k, copy, vduModel)
-			}
-			const confd = _omit(copy, ['uiState']);
-			return cleanEmptyTopKeys(confd);
-		}
+		const path = model.uiState['qualified-type'] || model.uiState['type'];
+		const metaModel = DescriptorModelMetaFactory.getModelMetaForType(path);
+		const data = {};
+		const name = model.uiState['type'];
+		data[name] = model; // lets get the meta hierachy from the top
+		const result = metaModel.serialize(data);
+		console.debug(result);
+		return result;
 	}
-};
-
-
-function checkForChoiceAndRemove(k, confd, model) {
-    let state = model.uiState;
-    if (state.choice) {
-        let choice = state.choice[k]
-        if(choice) {
-            if (choice.constructor.name == "Array") {
-                for(let i = 0; i < choice.length; i++) {
-                    for (let key in confd[k][i]) {
-                        if(choice[i] && (choice[i].selected.indexOf(key) > -1)) {
-                            confd[k][i][key] = confd[k][i][key]
-                        }
-                        confd[key];
-                    };
-                }
-            } else {
-                for (let key in confd[k]) {
-                    if(choice && (choice.selected.indexOf(key) > -1)) {
-                        confd[key] = confd[k][key]
-                    }
-                };
-                delete confd[k];
-            }
-
-        }
-    }
-    return confd;
 }
-
-function cleanEmptyTopKeys(m){
-    Object.keys(m).forEach(k => {
-        const isEmptyObject = typeof m[k] === 'object' && _isEmpty(m[k]);
-        if (typeof m[k] === 'undefined' || isEmptyObject || m[k] === '') {
-            delete m[k];
-        }
-    });
-    return m;
-}
-
-export default DescriptorModelSerializer;
+export default DescriptorModelSerializer;
\ No newline at end of file