Source: io/cml/kekule.io.cml.js

/**
 * @fileoverview
 * File for supporting CML (Chemical Markup Language), especially on read/write molecules and reactions.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /lan/xmlJsons.js
 * requires /core/kekule.common.js
 * requires /core/kekule.elements.js
 * requires /core/kekule.electrons.js
 * requires /core/kekule.structures.js
 * requires /core/kekule.structureBuilder.js
 * requires /core/kekule.reactions.js
 * requires /utils/kekule.domHelper.js
 * requires /io/kekule.io.js
 * requires /localization
 */

/*
 * Default options to read/write CML format data.
 * @object
 */
Kekule.globalOptions.add('IO.cml', {
	prettyPrint: true
});

/**
 * Defines some constants about CML.
 * @class
 * @private
 */
Kekule.IO.CML = {
	CML2CORE_NAMESPACE_URI: 'http://www.xml-cml.org/schema/cml2/core',  //'http://www.xml-cml.org/schema',
	CML3_SCHEMA_NAMESPACE_URI: 'http://www.xml-cml.org/schema',
	ARRAY_VALUE_DELIMITER: ' ',
	TYPED_ELEM_NAMES: ['string', 'integer', 'float'],
	TYPED_ARRAY_ELEM_NAMES: ['stringArray', 'integerArray', 'floatArray'],
	ATOMS_REF_ATTRIBS: ['atomRef', 'atomRefs2', 'atomRefs3', 'atomRefs4', 'atomRefs', 'atomRefArray'],
	BONDS_REF_ATTRIBS: ['bondRef', 'bondRefs', 'bondRefArray']
};
Kekule.IO.CML.LEGAL_CORE_NAMESPACE_URIS = [
	Kekule.IO.CML.CML2CORE_NAMESPACE_URI,
	Kekule.IO.CML.CML3_SCHEMA_NAMESPACE_URI
];

/**
 * A help class to handle CML
 * @class
 */
Kekule.IO.CmlUtils = {
	/*
	 * Convert a JS type name to XSD type name to be used in <scalar>
	 * @param {String} dataTypeName
	 * returns {String}
	 */
	/*
	dataTypeToXsdType: function(dataTypeName)
	{
		var result =
				(dataTypeName == DataType.INT)? 'xsd:integer':
				(dataTypeName == DataType.FLOAT)? 'xsd:float':
				(dataTypeName == DataType.BOOL)? 'xsd:boolean':
				(dataTypeName == DataType.DATE)? 'xsd:dateTime':
				null;
	}
	*/
	/**
	 * Turn a CML bond order value to a Kekule one.
	 * @param {String} cmlOrder
	 * @returns {Int} Value from {Kekule.BondOrder}
	 */
	cmlBondOrderToKekule: function(cmlOrder)
	{
		var result = parseInt(cmlOrder);
		if (result !== result)  // result is NaN
		{
			switch (cmlOrder.toUpperCase())
			{
				case 'A': result = Kekule.BondOrder.EXPLICIT_AROMATIC; break;
				case 'D': result = Kekule.BondOrder.DOUBLE; break;
				case 'T': result = Kekule.BondOrder.TRIPLE; break;
				case 'S':
				default:
					result = Kekule.BondOrder.SINGLE; break;
			}
		}
		return result;
	},
	/**
	 * Turn a Kekule bond order value to a CML string.
	 * @param {Int} kekuleOrder
	 * @returns {String}
	 */
	kekuleBondOrderToCml: function(kekuleOrder)
	{
		switch (kekuleOrder)
		{
			case Kekule.BondOrder.EXPLICIT_AROMATIC: return 'A';
			case Kekule.BondOrder.QUAD: return '4';
			case Kekule.BondOrder.DOUBLE: return 'D';
			case Kekule.BondOrder.TRIPLE: return 'T';
			//case Kekule.BondOrder.SINGLE: return 'S';
			//case Kekule.BondOrder.OTHER: return 'S';
			default:
				return 'S';
		}
	},
	/**
	 * Turn a CML bond stereo value to a Kekule one.
	 * @param {String} cmlStereo
	 * @returns {Int} Value from {Kekule.BondStereo}
	 */
	cmlBondStereoToKekule: function(cmlStereo)
	{
		var result;
		switch (cmlStereo.toUpperCase())
		{
			case 'E': result = Kekule.BondStereo.E; break;
			case 'Z': result = Kekule.BondStereo.Z; break;
			case 'C': result = Kekule.BondStereo.CIS; break;
			case 'T': result = Kekule.BondStereo.TRANS; break;
			case 'W': result = Kekule.BondStereo.UP; break;
			case 'H': result = Kekule.BondStereo.DOWN; break;
			default: result = Kekule.BondStereo.NONE; break;
		}
		return result;
	},
	/**
	 * Turn a Kekule bond stereo value to a CML string.
	 * Note Kekule has invert stereo enumerations, when translate to CML,
	 * you have to manually invert the bond atom refs to get a correct stereo result.
	 * @param {Int} kekuleStereo Value from {Kekule.BondStereo}
	 * @returns {Hash} {value, invert}
	 */
	kekuleBondStereoToCml: function(kekuleStereo)
	{
		switch (kekuleStereo)
		{
			case Kekule.BondStereo.UP: return {'value': 'W'};
			case Kekule.BondStereo.UP_INVERTED: return {'value': 'W', 'invert': true};
			case Kekule.BondStereo.DOWN: return {'value': 'H'};
			case Kekule.BondStereo.DOWN_INVERTED: return {'value': 'H', 'invert': true};
			case Kekule.BondStereo.E: return {'value': 'E'};
			case Kekule.BondStereo.Z: return {'value': 'Z'};
			case Kekule.BondStereo.CIS: return {'value': 'C'};
			case Kekule.BondStereo.TRANS: return {'value': 'T'};
			//case Kekule.BondStereo.NONE: return null;
			default:
				return {'value': null};
		}
	},

	/**
	 * Turn a CML role value of reactant, product or substance to a Kekule one in {@link Kekule.ReactionRole}.
	 * If it is not a standard Kekule role, returns the origin value.
	 * @param {String} cmlRole
	 * @returns {String}
	 */
	reagentRoleToKekule: function(cmlRole)
	{
		var R = Kekule.ReactionRole;
		switch (cmlRole)
		{
			case 'reagent': return R.REAGENT;
			case 'catalyst': return R.CATALYST;
			case 'solvent': return R.CATALYST;
			default: return cmlRole;
		}
	},
	/**
	 * Turn a Kekule role value of reactant, product or substance to a CML one.
	 * If it is not a standard CML role, returns the origin value.
	 * @param {String} kekuleRole
	 * @returns {String}
	 */
	kekuleReagentRoleToCml: function(kekuleRole)
	{
		var R = Kekule.ReactionRole;
		switch (kekuleRole)
		{
			case R.REAGENT: return 'reagent';
			case R.CATALYST: return 'catalyst';
			case R.CATALYST: return 'solvent';
			default: return kekuleRole;
		}
	},

	/**
	 * Check if an element type is standing for a dummy atom.
	 * @param {String} value
	 * @returns {Bool}
	 */
	isDummyElementType: function(value)
	{
		return ((value == 'Du') || (value == 'Dummy'));
	},
	/**
	 * Check if an element type is standing for a RGroup.
	 * @param {String} value
	 * @returns {Bool}
	 */
	isRGroupElementType: function(value)
	{
		return (value == 'R');
	},

	/**
	 * Check if an element type is standing for an atom list.
	 * NOTE: atom list is not native CML concept, just borrowed from MDL.
	 * @param {String} value
	 * @returns {Bool}
	 */
	isAtomListElementType: function(value)
	{
		return (value == 'L');
	},
	/**
	 * Get element symbol (used as elementType in CML) of a {@link Kekule.ChemStructureNode}.
	 * @param {Kekule.ChemStructureNode} node
	 * @return {String}
	 */
	getNodeElementType: function(node)
	{
		if (node instanceof Kekule.Atom)
			return node.getSymbol();
		else if (node instanceof Kekule.Pseudoatom)
		{
			//console.log(node);
			switch (node.getAtomType())
			{
				case Kekule.PseudoatomType.DUMMY: return 'Du';
				case Kekule.PseudoatomType.ANY: return 'A';
				case Kekule.PseudoatomType.HETERO: return 'Q';
				case Kekule.PseudoatomType.CUSTOM:
				default:
					{
						return node.getSymbol();
					}
			}
		}
		else if (node instanceof Kekule.VariableAtom)
			return 'R';  // 'L'; CML standard has no L
		else if (node instanceof Kekule.StructureFragment)
			return 'R';
		else if (node.getSymbol)
		{
			return node.getSymbol();
		}
		else
			return '*';  // do not know what's the proper symbol
	},

	/**
	 * Create a proper structure node by elementType of CML.
	 * @param {String} id
	 * @param {String} elemType
	 * @param {Number} massNumber Can be null.
	 * @returns {Kekule.ChemStructureNode}
	 */
	createNodeByCmdElementType: function(id, elemType, massNumber)
	{
		if (Kekule.IO.CmlUtils.isDummyElementType(elemType))
				result = new Kekule.Pseudoatom(id, Kekule.PseudoatomType.DUMMY);
		else if (Kekule.IO.CmlUtils.isRGroupElementType(elemType))
			result = new Kekule.RGroup(id);
		else if (Kekule.IO.CmlUtils.isAtomListElementType(elemType))
			result = new Kekule.VariableAtom(id);
		else if (Kekule.Element.isElementSymbolAvailable(elemType)
			|| (elemType == Kekule.Element.UNSET_ELEMENT)) // a normal atom or unset element
		{
			// in CML, an isotope attribute can be a mass number or a accurate mass, so round it
			// here to get a integer mass number
			//var massNumber = attribs.isotope? Math.round(attribs.isotope): null;
			result = new Kekule.Atom(id, elemType, Math.round(massNumber));
		}
		else  // elemType not a real symbol symbol, create a pseudo atom
		{
			result = new Kekule.Pseudoatom(id, Kekule.PseudoatomType.CUSTOM, elemType);
		}
		return result;
	}
}

/**
 * A help class to do some DOM work on CML
 * @class
 */
Kekule.IO.CmlDomUtils = {
	/**
	 * Split a CML array string to a JavaScript array
	 * @param {Object} value
	 * @returns {Array}
	 */
	splitCmlArrayValue: function(value)
	{
		if ((typeof(value) == 'object') && (value.length))  // already an array
			return value;
		var s = Kekule.StrUtils.normalizeSpace(Kekule.StrUtils.trim(value));
		return s.split(Kekule.IO.CML.ARRAY_VALUE_DELIMITER);
	},
	/**
	 * Merge an array to a CML array string.
	 * @param {Array} arrayObj
	 * @returns {String}
	 */
	mergeToCmlArrayValue: function(arrayObj)
	{
		return arrayObj.join(' ');
	},
	/**
	 * Split a CML formula concise value to an array.
	 *   For example, 'S 1 O 4 -2' will be transformed to:
	 *   {'isotopes': [{'elementType': 'S', 'count': 1}, {'elementType': 'O', 'count': 4}], 'formalCharge': -2}
	 * @param {String} value
	 * @returns {Hash}
	 */
	analysisCmlFormulaConciseValue: function(value)
	{
		var tokens = Kekule.IO.CmlDomUtils.splitCmlArrayValue(value);
		var i = 0;
		var l = tokens.length;
		var isotopes = [];
		var currIsotope = null;
		var formalCharge;
		while (i < l)
		{
			var token = tokens[i];
			var count = parseFloat(token);
			if (count != count)  // count is NaN, so token is really not a number, first one of pair, should be a symbol
			{
				if (currIsotope)
					isotopes.push(currIsotope);
				currIsotope = {'elementType': token};
			}
			else if (currIsotope)  // second of pair, should be the count
			{
				currIsotope.count = count;
				isotopes.push(currIsotope);
				currIsotope = null;
			}
			else if (i == l - 1) // should be the last formal charge
				formalCharge = count;
			++i;
		}
		return {'isotopes': isotopes, 'formalCharge': formalCharge};
	},
	/**
	 * Get information of CML element with builtin attribute.
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @param {Kekule.DomHelper} domHelper
	 * @returns {Hash} {name, value} while name is the builtin attribute and value is the content of element.
	 * @private
	 */
	getCmlBuiltinElemInfo: function(elem, namespaceURI, domHelper)
	{
		var propName;
		propName = elem.getAttribute('builtin');
		if ((!propName) && namespaceURI)
		{
			if (elem.getAttributeNS)
				propName = elem.getAttributeNS(namespaceURI, 'builtin');
			else if (domHelper)
				propName = domHelper.getAttributeNS(namespaceURI, 'builtin', elem);
		}
		if (propName)
		{
			var propValue = Kekule.DomUtils.getElementText(node);
			return {'name': propName, 'value': propValue};
		}
		else
			return null;
	},
	/**
	 * Read element of <string>, <integer> and <float>
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @param {Kekule.DomHelper} domHelper
	 * @returns {Hash} {name, value}
	 * @private
	 */
	readCmlTypedPropertyElem: function(elem, namespaceURI, domHelper)
	{
		var propName;
		propName = elem.getAttribute('builtin');
		if ((!propName) && namespaceURI)
		{
			if (elem.getAttributeNS)
				propName = elem.getAttributeNS(namespaceURI, 'builtin');
			else if (domHelper)
				propName = domHelper.getAttributeNS(namespaceURI, 'builtin', elem);
		}

		if (propName)
		{
			var propValue = Kekule.DomUtils.getElementText(elem);
			switch (Kekule.DomUtils.getLocalName(elem))
			{
				case 'float':
					propValue = parseFloat(propValue);
					break;
				case 'integer':
					propValue = parseInt(propValue);
					break;
				default:
					; // string
			}
			return {'name': propName, 'value': propValue};
		}
		else
			return null;
	},
	/**
	 * Read element of <stringArray>, <integerArray> and <floatArray>
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @param {Kekule.DomHelper} domHelper
	 * @returns {Hash} {name, values: [values]}
	 * @private
	 */
	readCmlTypedPropertyArrayElem: function(elem, namespaceURI, domHelper)
	{
		var propName;
		propName = elem.getAttribute('builtin');
		if ((!propName) && namespaceURI)
		{
			if (elem.getAttributeNS)
				propName = elem.getAttributeNS(namespaceURI, 'builtin');
			else if (domHelper)
				propName = domHelper.getAttributeNS(namespaceURI, 'builtin', elem);
		}

		if (propName)
		{
			var propValue = Kekule.DomUtils.getElementText(node);
			var values = Kekule.IO.CmlDomUtils.splitCmlArrayValue(propValue);
			switch (Kekule.DomUtils.getLocalName(elem))
			{
				case 'float':
					for (var i = 0, l = values.length; i < l; ++i)
						values[i] = parseFloat(values[i]);
					break;
				case 'integer':
					for (var i = 0, l = values.length; i < l; ++i)
						values[i] = parseInt(values[i]);
					break;
				default:
					; // string
			}
			return {'name': propName, 'values': values};
		}
		else
			return null;
	},

	/**
	 * Check if an element has direct <string><integer> or <float> children
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @returns {Bool}
	 */
	hasDirectCmlTypedElemChildren: function(elem, namespaceURI)
	{
		for (var i = 0, l = Kekule.IO.CML.TYPED_ELEM_NAMES.length; i < l; ++i)
		{
			var localName = Kekule.IO.CML.TYPED_ELEM_NAMES[i];
			var children = Kekule.DomUtils.getDirectChildElems(elem, null, localName, namespaceURI);
			if (children.length > 0)
				return true;
		}
		return false;
	},

	/**
	 * Check if an element has direct <stringArray>, <integerArray> or <floatArray> children
	 * @param {Object} elem
	 * @param {String} namespaceURI
	 * @returns {Bool}
	 */
	hasDirectCmlTypedArrayElemChildren: function(elem, namespaceURI)
	{
		for (var i = 0, l = Kekule.IO.CML.TYPED_ARRAY_ELEM_NAMES.length; i < l; ++i)
		{
			var localName = Kekule.IO.CML.TYPED_ARRAY_ELEM_NAMES[i];
			var children = Kekule.DomUtils.getDirectChildElems(elem, null, localName, namespaceURI);
			if (children.length > 0)
				return true;
		}
		return false;
	},

	/**
	 * Check if an element is <string><integer> or <float>
	 * @param {Object} elem
	 * @param {String} namespaceURI Can be null.
	 * @returns {Bool}
	 */
	isCmlTypedElem: function(elem, namespaceURI)
	{
		var result = (Kekule.IO.CML.TYPED_ELEM_NAMES.indexOf(Kekule.DomUtils.getLocalName(elem)) >= 0);
		if (namespaceURI)
			result = result && (elem.namespaceURI == namespaceURI);
		return result;
	},
	/**
	 * Check if an element is <stringArray>, <integerArray> or <floatArray>
	 * @param {Object} elem
	 * @param {String} namespaceURI Can be null.
	 * @returns {Bool}
	 */
	isCmlTypedArrayElem: function(elem, namespaceURI)
	{
		var result = (Kekule.IO.CML.TYPED_ARRAY_ELEM_NAMES.indexOf(Kekule.DomUtils.getLocalName(elem)) >= 0);
		if (namespaceURI)
			result = result && (elem.namespaceURI == namespaceURI);
		return result;
	},
	/**
	 * Check if an element has builtin attribute.
	 * @param {Object} elem
	 * @param {String} namespaceURI Can be null.
	 * @returns {Bool}
	 */
	isCmlBuiltInMarkedElem: function(elem, namespaceURI)
	{
		var result = Kekule.DomUtils.hasAttribute(elem, 'builtin');
		if (namespaceURI && result)
			result = elem.namespaceURI === namespaceURI;
		return result;
	},

	FILTER_TYPED_ELEM: 1,
	FILTER_TYPEDARRAY_ELEM: 2,
	FILTER_ALL: 3,
	/**
	 * Get child typed element with specified builtinName.
	 * @param {Object} parent
	 * @param {String} builtinName
	 * @param {Int} filter
	 * @returns {Object} Element found or null.
	 */
	getCmlTypedElem: function(parent, builtinName, filter)
	{
		var nsURI = parent.namespaceURI;
		var elems = Kekule.DomUtils.getDirectChildElemsOfAttribValues(parent, [{'builtin': builtinName}], null, null, nsURI);
		if (elems && (elems.length > 0))
		{
			for (var i = elems.length - 1; i >= 0; --i)
			{
				if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedElem(elems[i], nsURI))
					return elems[i];
				else if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(elems[i], nsURI))
					return elems[i];
			}
		}
		return null;
	},
	/**
	 * Get all child typed elements with specified builtinName.
	 * @param {Object} parent
	 * @param {String} builtinName
	 * @param {Int} filter
	 * @returns {Array} Elements found or null.
	 */
	getCmlTypedElems: function(parent, builtinName, filter)
	{
		var result = [];
		var nsURI = parent.namespaceURI;
		var elems = Kekule.DomUtils.getDirectChildElemsOfAttribValues(parent, [{'builtin': builtinName}], null, null, nsURI);
		if (elems && (elems.length > 0))
		{
			for (var i = elems.length - 1; i >= 0; --i)
			{
				if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedElem(elems[i], nsURI))
					result.push(elems[i]);
				else if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(elems[i], nsURI))
					result.push(elems[i]);
			}
		}
		return result.length? result: null;
	},
	/**
	 * Get value of child typed element with specified builtinName.
	 * @param {Object} parent
	 * @param {String} builtinName
	 * @param {Int} filter
	 * @returns {Variant} Value found or null.
	 */
	getCmlTypedElemValue: function(parent, builtinName, filter, domHelper)
	{
		if (!filter)
			filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
		var elem = Kekule.IO.CmlDomUtils.getCmlTypedElem(parent, builtinName, filter);
		if (elem)
			//return Kekule.IO.CmlDomUtils.readCmlTypedPropertyElem(elem, parent.namespaceURI, domHelper);
			return Kekule.DomUtils.getElementText(elem);
	},
	/**
	 * Get value of child typed elements (maybe multiple) with specified builtinName.
	 * @param {Object} parent
	 * @param {String} builtinName
	 * @param {Int} filter
	 * @returns {Variant} Value found (array) or null.
	 */
	getMultipleCmlTypedElemValues: function(parent, builtinName, filter, domHelper)
	{
		if (!filter)
			filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
		var elems = Kekule.IO.CmlDomUtils.getCmlTypedElems(parent, builtinName, filter);
		if (elems)
		{
			var result = [];
			//return Kekule.IO.CmlDomUtils.readCmlTypedPropertyElem(elem, parent.namespaceURI, domHelper);
			for (var i = 0, l = elems.length; i < l; ++i)
			{
				result.push(Kekule.DomUtils.getElementText(elems[i]));
			}
			return result;
		}
		else
			return null;
	},
	/**
	 * Get attribute value of elem. If attribName not found in elem's attributes,
	 *   this method will automatically check child <string>, <float> or <integer> elements.
	 * @param {Object} elem
	 * @param {String} attribName
	 * @param {Int} filter
	 * @returns {Variant} Value of attribute.
	 */
	getCmlElemAttribute: function(elem, attribName, filter, domHelper)
	{
		if (!filter)
			filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
		var result = Kekule.DomUtils.getSameNSAttributeValue(elem, attribName, domHelper);
		if ((result === null) || (result === undefined))  // attrib not found, check child typed elements
		{
			result = Kekule.IO.CmlDomUtils.getCmlTypedElemValue(elem, attribName, filter, domHelper);
		}
		return result;
	},
	/**
	 * Get attribute values of (may) multiple elem. If attribName not found in elem's attributes,
	 *   this method will automatically check child <string>, <float> or <integer> elements.
	 * @param {Object} elem
	 * @param {String} attribName
	 * @param {Int} filter
	 * @returns {Variant} Value of attribute.
	 */
	getMultipleCmlElemAttribute: function(elem, attribName, filter, domHelper)
	{
		if (!filter)
			filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
		var result = Kekule.DomUtils.getSameNSAttributeValue(elem, attribName, domHelper);
		if ((result === null) || (result === undefined))  // attrib not found, check child typed elements
		{
			result = Kekule.IO.CmlDomUtils.getMultipleCmlTypedElemValues(elem, attribName, filter, domHelper);
		}
		return result;
	},
	setCmlElemAttribute: function(elem, attribName, value, domHelper)
	{
		return Kekule.DomUtils.setSameNSAttributeValue(elem, attribName, value, domHelper);
	},

	/**
	 * Fetch all attribute values of CML element into an JSON object, including typed or typed array child elements.
	 * @param {Object} elem
	 * @param {Int} filter
	 * @param {Bool} mergeSameNameTypedElem If multiple child typed elements has same builtin, whether merge their values
	 * @param {Kekule.DomHelper} domHelper
	 * @returns {Hash} Each item is a hash: {name: value}.
	 */
	fetchCmlElemAttributeValuesToJson: function(elem, filter, mergeSameNameTypedElem, domHelper)
	{
		if (!filter)
			filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
		var nsURI = elem.namespaceURI;
		// get all attributes first
		var result = Kekule.DomUtils.fetchAttributeValuesToJson(elem, nsURI, true);
		// then check child elements
		var childElems = Kekule.DomUtils.getDirectChildElems(elem, null, null, nsURI);
		for (var i = 0, l = childElems.length; i < l; ++i)
		{
			var childElem = childElems[i];
			if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM)
					&& (Kekule.IO.CmlDomUtils.isCmlTypedElem(childElem, nsURI) || Kekule.IO.CmlDomUtils.isCmlBuiltInMarkedElem(childElem, nsURI)))
			{
				var obj = Kekule.IO.CmlDomUtils.readCmlTypedPropertyElem(childElem, nsURI, domHelper);
				if (obj)
				{
					if (result[obj.name] && mergeSameNameTypedElem) // multiple elements with same builtin name, merge values
						result[obj.name] += Kekule.IO.CML.ARRAY_VALUE_DELIMITER + obj.value;
					else
						result[obj.name] = obj.value;
				}
			}
			else if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(childElem, nsURI))
			{
				var obj = Kekule.IO.CmlDomUtils.getCmlBuiltinElemInfo(childElem, nsURI, domHelper);
				if (obj)
				{
					if (result[obj.name] && mergeSameNameTypedElem) // multiple elements with same builtin name, merge values
						result[obj.name] += Kekule.IO.CML.ARRAY_VALUE_DELIMITER + obj.value;
					else
						result[obj.name] = obj.value;
				}
			}
		}
		return result;
	},

	/**
	 * Check is element is <scalar>
	 * @param {Object} elem
	 * @param {String} namespaceURI Can be null.
	 * @returns {Bool}
	 */
	isScalarElem: function(elem, namespaceURI)
	{
		var result = (Kekule.DomUtils.getLocalName(elem) == 'scalar');
		if (namespaceURI)
			result = result && (elem.namespaceURI == namespaceURI);
		return result;
		//return ((elem.localName == 'scalar') && this.matchCoreNamespace(elem));
	},

	/**
	 * Get Id of CML element
	 * @param {Object} elem
	 * @returns {String}
	 */
	getCmlId: function(elem, domHelper)
	{
		return Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'id', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
	},
	/**
	 * Get Id attribute of CML element
	 * @param {String} id
	 * @param {Object} elem
	 */
	setCmlId: function(elem, id, domHelper)
	{
		return Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, 'id', id, domHelper);
	},
	/**
	 * Get title of CML element
	 * @param {Object} elem
	 * @returns {String}
	 */
	getCmlTitle: function(elem, domHelper)
	{
		return Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'title', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
	},
	/**
	 * Set title attribute of CML element
	 * @param {Object} elem
	 * @returns {String}
	 */
	setCmlTitle: function(elem, title, domHelper)
	{
		return Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, 'title', title, domHelper);
	}
};

/**
 * A manager to create suitable element reader for CML document.
 * @class
 */
Kekule.IO.CmlElementReaderFactory = {
	/** @private */
	_readers: {},
	/**
	 * Register new reader class.
	 * @param {Variant} elemLocalName Local name or array of names.
	 * @param {Object} readerClass Class of reader.
	 */
	register: function(elemLocalName, readerClass)
	{
		if (Kekule.ArrayUtils.isArray(elemLocalName))
		{
			for (var i = 0, l = elemLocalName.length; i < l; ++i)
				Kekule.IO.CmlElementReaderFactory.register(elemLocalName[i], readerClass);
		}
		else
			Kekule.IO.CmlElementReaderFactory._readers[elemLocalName] = readerClass;
	},
	/**
	 * Returns suitable reader for an CML element.
	 * @param {Variant} elemOrLocalName Element (Object) or element's local name (String).
	 * @returns {Kekule.IO.CmlElementReader}
	 */
	getReader: function(elemOrLocalName)
	{
		var name;
		if (typeof(elemOrLocalName) != 'string')
			name = Kekule.DomUtils.getLocalName(elemOrLocalName);
		else
			name = elemOrLocalName;
		var readerClass = Kekule.IO.CmlElementReaderFactory._readers[name];
		if (readerClass)
			return new readerClass();
		else
			return null;
	}
};

/**
 * A manager to create suitable element writer for CML document.
 * @class
 */
Kekule.IO.CmlElementWriterFactory = {
	/** @private */
	_writers: {},
	/**
	 * Register new writer class.
	 * @param {Variant} objTypeName Class name or type name or name array of source object.
	 * @param {Object} writerClass Class of writer.
	 */
	register: function(objTypeName, writerClass)
	{
		if (Kekule.ArrayUtils.isArray(objTypeName))
		{
			for (var i = 0, l = objTypeName.length; i < l; ++i)
				Kekule.IO.CmlElementWriterFactory.register(objTypeName[i], writerClass);
		}
		else
			Kekule.IO.CmlElementWriterFactory._writers[objTypeName] = writerClass;
	},
	/**
	 * Returns suitable writer.
	 * @param {Variant} objOrTypeName Object or object's type name.
	 * @returns {Kekule.IO.CmlElementWriter}
	 */
	getWriter: function(objOrTypeName)
	{
		var name;
		if (typeof(objOrTypeName) != 'string')
			name = DataType.getType(objOrTypeName);
		else
			name = objOrTypeName;
		var writerClass = Kekule.IO.CmlElementWriterFactory._writers[name];
		if ((!writerClass) && (objOrTypeName.getClassName))  // not found, check if obj is instance of type
		{
			var obj = objOrTypeName;
			var typeNames = Kekule.ObjUtils.getOwnedFieldNames(Kekule.IO.CmlElementWriterFactory._writers);
			for (var i = typeNames.length - 1; i >= 0; --i)  // the later the superior
			{
				if (obj instanceof eval(typeNames[i]))
				{
					writerClass = Kekule.IO.CmlElementWriterFactory._writers[typeNames[i]];
					break;
				}
			}
		}
		if (writerClass)
			return new writerClass();
		else
			return null;
	}
};

/**
 * Base class for classes to read or write CML element.
 * @class
 * @augments ObjectEx
 *
 * @property {String} coreNamespaceURI Namespace URI of CML core.
 */
Kekule.IO.CmlElementHandler = Class.create(ObjectEx,
/** @lends Kekule.IO.CmlElementReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlElementHandler',
	/** @private */
	initProperties: function()
	{
		// a private property
		this.defineProp('domHelper', {
			'dataType': 'Kekule.DomHelper',
			'serializable': false,
			'getter': function()
				{
					if (!this.getPropStoreFieldValue('domHelper'))
						this.setPropStoreFieldValue('domHelper', new Kekule.DomHelper());
					return this.getPropStoreFieldValue('domHelper');
				}
		});

		this.defineProp('coreNamespaceURI', {
			'dataType': DataType.STRING, 'serializable': false
			// temp
			//'getter': function() { return Kekule.IO.CML.CML2CORE_NAMESPACE_URI; },
			//'setter': function() {}
		});
		/*
		this.defineProp('reactionNamespaceURI', {
			'dataType': DataType.STRING, 'serializable': false,
			// temp
			'getter': function() { return Kekule.IO.CML.CML2CORE_NAMESPACE_URI; },
			'setter': function() {}
		});
		*/
	},

	/** @private */
	matchCoreNamespace: function(nodeOrNsURI)
	{
		var nsURI;
		if (!nodeOrNsURI)
			nsURI = '';
		else if (typeof(nodeOrNsURI) != 'string')
			nsURI = nodeOrNsURI.namespaceURI;
		else
			nsURI = nodeOrNsURI;
		return ((!this.getCoreNamespaceURI()) && (!nsURI)
			|| (this.getCoreNamespaceURI() && (this.getCoreNamespaceURI() == nsURI)));
	},
	/**
	 * Copy domHelper and coreNamespaceURI to child reader or writer.
	 * @param {Object} childHandler
	 * @private
	 */
	copySettingsToChildHandler: function(childHandler)
	{
		childHandler.setDomHelper(this.getDomHelper());
		childHandler.setCoreNamespaceURI(this.getCoreNamespaceURI());
	}
});

/**
 * Base class of readers to read different CML elements.
 * @class
 * @augments Kekule.IO.CmlElementHandler
 */
Kekule.IO.CmlElementReader = Class.create(Kekule.IO.CmlElementHandler,
/** @lends Kekule.IO.CmlElementReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlElementReader',

	/**
	 * Read an element in CML document and returns a proper Kekule object or insert something into parentObj.
	 * @param {Object} elem
	 * @param {Kekule.ChemObject} parentObj
	 * @returns {Variant}
	 */
	readElement: function(elem, parentObj)
	{
		var result = this.doReadElement(elem, parentObj);
		if (result && result.getId && (!result.getId()))
		{
			if (result.setId)
			{
				var id = Kekule.IO.CmlDomUtils.getCmlId(elem, this.getDomHelper);
				if (id)
					result.setId(id);
			}
		}
		return result;
	},
	/**
	 * Method to do the actual readElement job. Descendants should override this method.
	 * @param {Object} elem
	 * @param {Object} parentObj
	 * @returns {Variant}
	 * @private
	 */
	doReadElement: function(elem, parentObj)
	{
		// do nothing here
	},

	/**
	 * Read and handle child elements,
	 * @param {Object} elem
	 * @param {Object} parentObj
	 * @returns {Variant}
	 */
	readChildElement: function(elem, parentObj)
	{
		return this.doReadChildElement(elem, parentObj);
	},
	/**
	 * The real job of readChildElem is done here. Descendants may override this.
	 * @param {Object} elem
	 * @param {Object} parentObj
	 * @returns {Variant}
	 * @private
	 */
	doReadChildElement: function(elem, parentObj)
	{
		return this.doReadChildElementDef(elem, parentObj);
	},

	/**
	 * A default method to read child elements,
	 *   just ask CmlElementReaderFactory to create a suitable reader and use the reader to read.
	 * @param {Object} elem
	 * @param {Object} parentObj
	 * @returns {Variant}
	 */
	readChildElementDef: function(elem, parentObj)
	{
		return this.doReadChildElementDef(elem, parentObj);
	},
	/**
	 * The real job of readChildElementDef is done here. Descendants may override this.
	 * @param {Object} elem
	 * @param {Object} parentObj
	 * @returns {Variant}
	 * @private
	 */
	doReadChildElementDef: function(elem, parentObj)
	{
		var reader = Kekule.IO.CmlElementReaderFactory.getReader(elem);
		if (reader)
		{
			//reader.setCoreNamespaceURI(this.getCoreNamespaceURI());
			this.copySettingsToChildHandler(reader);
			return reader.readElement(elem, parentObj);
		}
		else
			return null;
	},
	/**
	 * Iterate through and read all direct children of elem.
	 * @param {Object} elem
	 * @param {Object} parentObj
	 */
	iterateChildElements: function(elem, parentObj)
	{
		var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
		for (var i = 0, l = children.length; i < l; ++i)
		{
			this.readChildElement(children[i], parentObj);
		}
	}
});

/**
 * Base class of readers to read different CML elements.
 * @class
 * @augments Kekule.IO.CmlElementHandler
 */
Kekule.IO.CmlElementWriter = Class.create(Kekule.IO.CmlElementHandler,
/** @lends Kekule.IO.CmlElementWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlElementWriter',
	/**
	 * Write Kekule obj to a new CML element in doc or insert new attributes to parentElem.
	 * @param {Kekule.ChemObject} obj
	 * @param {Object} parentElem
	 * @param {Object} doc
	 * @returns {Object} Element created.
	 */
	writeObject: function(obj, parentElem, doc)
	{
		if (!(parentElem) && (!doc))
		{
			Kekule.error(/*Kekule.ErrorMsg.CML_CAN_NOT_OUTPUT_TO_EMPTY_ELEMENT*/Kekule.$L('ErrorMsg.CML_CAN_NOT_OUTPUT_TO_EMPTY_ELEMENT'));
			return null;
		}
		if ((!parentElem) && doc)
			parentElem = doc.documentElement;
		else if (!doc)
			doc = parentElem.ownerDocument;
		if (this.getDomHelper().getDocument != doc)
			this.getDomHelper().setDocument(doc);
		var targetElem = this.doCreateElem(obj, parentElem, doc);
		if (targetElem)
			parentElem.appendChild(targetElem);
		else // if no need to create child element, write directly on parentElem
 			targetElem = parentElem;
		var result = this.doWriteObject(obj, targetElem) || targetElem;
		if (result && obj)
		{
			// id
			this.writeObjId(obj, result);
			// scalar & info
			this.writeObjAdditionalInfo(obj, result);
		}

		return result;
	},
	writeObjId: function(obj, elem)
	{
		if (obj.getId && obj.getId())
		{
			Kekule.IO.CmlDomUtils.setCmlId(elem, obj.getId(), this.getDomHelper());
		}
	},
	/**
	 * If obj has scalar property, write them.
	 * @param {Object} obj
	 * @param {Object} elem
	 * @private
	 */
	writeScalarAttribs: function(obj, elem)
	{
		if (obj.getScalarAttribs)
		{
			var writer = Kekule.IO.CmlElementWriterFactory.getWriter('Kekule.Scalar');
			if (writer)
			{
				this.copySettingsToChildHandler(writer);
				//var scalars = obj.getScalarAttribs();
				for (var i = 0, l = obj.getScalarAttribCount(); i < l; ++i)
					writer.writeObject(obj.getScalarAttribAt(i), elem);
			}
		}
	},
	/**
	 * If obj has info property, write it.
	 * @param {Object} obj
	 * @param {Object} elem
	 * @private
	 */
	writeObjInfoValues: function(obj, elem)
	{
		if (obj.getInfo)
		{
			var keys = obj.getInfoKeys();
			var metaListElem;
			for (var i = 0, l = keys.length; i < l; ++i)
			{
				/*
				var childElem = this.createChildElem('scalar', elem);
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(childElem, 'title', keys[i], this.getDomHelper());
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(childElem, 'value', keys[i], this.getDomHelper());
				*/
				var key = keys[i];
				var value = obj.getInfoValue(keys[i]);
				if (key && value && (!Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, key, this.getDomHelper()))) // attrib not set
				{
					//Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, keys[i], value, this.getDomHelper());
					// add meta
					if (!metaListElem)
						metaListElem = this.createChildElem('metaDataList', elem);
					var metaElem = this.createChildElem('metaData', metaListElem);
					Kekule.IO.CmlDomUtils.setCmlElemAttribute(metaElem, 'name', key, this.getDomHelper());
					Kekule.IO.CmlDomUtils.setCmlElemAttribute(metaElem, 'content', DataType.StringUtils.serializeValue(value), this.getDomHelper());
				}
			}
		}
	},
	/**
	 * Write scalarAttribs and info property of obj.
	 * @param {Object} obj
	 * @param {Object} elem
	 * @private
	 */
	writeObjAdditionalInfo: function(obj, elem)
	{
		//console.log('called', this.getClassName());
		this.writeScalarAttribs(obj, elem);
		this.writeObjInfoValues(obj, elem);
	},
	/**
	 * Create suitable new child element to write obj.
	 * Descendants may override this method.
	 * @param {Kekule.ChemObject} obj
	 * @param {Object} parentElem
	 * @returns {Object} Element created.
	 */
	doCreateElem: function(obj, parentElem)
	{
		// do nothing here
	},
	/**
	 * Create a new child element with qualifiedName with namespace.
	 * @param {String} qualifiedName
	 * @param {Object} parentElem
	 * @param {String} namespaceURI Can be null.
	 * @private
	 */
	createChildElem: function(qualifiedName, parentElem, namespaceURI)
	{
		var ns = namespaceURI || this.getCoreNamespaceURI();
		var result = this.getDomHelper().createElementNS(ns, qualifiedName);
		if (parentElem)
			parentElem.appendChild(result);
		return result;
	},
	/**
	 * Method to do the actual writeObject job. Descendants should override this method.
	 * @param {Kekule.ChemObject} obj
	 * @param {Object} targetElem
	 * @returns {Object}
	 */
	doWriteObject: function(obj, targetElem)
	{
		// do nothing here
	},
	/**
	 * Report this writer is not proper for writing obj.
	 * @param {Object} obj
	 */
	reportTypeMismatchError: function(obj)
	{
		Kekule.error(/*Kekule.ErrorMsg.CML_ELEM_WRITER_TYPE_INPROPER*/
			Kekule.$L('ErrorMsg.CML_ELEM_WRITER_TYPE_INPROPER').format(this.getClassName(), DataType.getType(obj)));
	}
});

/**
 * CML <name> element reader.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlNameReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlNameReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlNameReader',
	/** @constructs */
	initialize: function($super)
	{
		$super();
	},

	/**
	 * Read an <name> element in CML document.
	 * @param {Object} elem
	 * @returns {Hash} A hash of {name, convention}
	 * @private
	 */
	doReadElement: function(elem, parentObj)
	{
		var name = Kekule.DomUtils.getElementText(elem);
		var convention = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'convention');
		if (name)
		{
			var result = {};
			result.name = name;
			if (convention)
				result.convention = convention;
			else  // no convertion, regard it as a default name as insert to parentObj
			{
				if (parentObj.setName)
					parentObj.setName(name);
			}
			return result;
		}
		return null;
	}
});

/**
 * CML <scalar> element reader.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlScalarReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlScalarReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlScalarReader',
	/**
	 * Read an <scalar> element in CML document.
	 * @param {Object} elem
	 * @private
	 */
	doReadElement: function(elem, parentObj)
	{
		/*
		var scalarInfo = this.readScalar(elem);
		// insert scalar into parentObj's info property
		var keys = Kekule.ObjUtils.getOwnedFieldNames(scalarInfo);
		var valueObj = {};
		var svalueKey = scalarInfo.dictRef?
			'dictRef'
			: scalarInfo.title? 'title': null;
		if (svalueKey)  // has key, insert directly into parentObj
		{
			for (var i = 0, l = keys.length; i < l; ++i)
			{
				if (keys[i] != svalueKey)
					valueObj[keys[i]] = scalarInfo[keys[i]];
			}
			parentObj.setInfoValue(scalarInfo[svalueKey], valueObj);
		}
		else  // may be used in other situation, do nothing and just return a JSON object
			;
		return scalarInfo;
		*/
		var scalar = this.readScalar(elem);
		if (parentObj && parentObj.appendScalarAttrib)  // parent is a ChemObject and can insert scalar attrib
			parentObj.appendScalarAttrib(scalar);
		else  // may be used in other situation, do nothing and just return a object
			;
		return scalar;
	},

	/** @private */
	readScalar: function(elem)
	{
		var jsonObj = Kekule.DomUtils.fetchAttributeValuesToJson(elem, this.getCoreNamespaceURI(), true);
		var value = Kekule.DomUtils.getElementText(elem);
		if (value)
			jsonObj.value = value;
		if (jsonObj.value) // check dataType and do the proper conversion
		{
			if (jsonObj.dataType)
			{
				switch (jsonObj.dataType)
				{
					case 'xsd:boolean':
					{
						jsonObj.value = Kekule.StrUtils.strToBool(jsonObj.value);
						break;
					}
					case 'xsd:float':
					case 'xsd:double':
					case 'xsd:duration':
					/*
					{
						jsonObj.value = parseFloat(jsonObj.value);
						if (jsonObj.errorValue)
							jsonObj.errorValue = parseFloat(jsonObj.errorValue);
						break;
					}
					*/
					case 'xsd:decimal':
					case 'xsd:integer':
					case 'xsd:nonPositiveInteger':
					case 'xsd:negativeInteger':
					case 'xsd:long':
					case 'xsd:int':
					case 'xsd:short':
					case 'xsd:byte':
					case 'xsd:nonNegativeInteger':
					case 'xsd:unsignedLong':
					case 'xsd:unsignedInt':
					case 'xsd:unsignedShort':
					case 'xsd:unsignedByte':
					case 'xsd:positiveInteger':
					{
						jsonObj.value = parseFloat(jsonObj.value);
						if (jsonObj.errorValue)
							jsonObj.errorValue = parseFloat(jsonObj.errorValue);
						break;
					}
				}
			}
		}
		// turn jsonObj to Kekule.Scalar instance
		var result;
		if (jsonObj)
		{
			result = new Kekule.Scalar();
			var keys = Kekule.ObjUtils.getOwnedFieldNames(jsonObj);
			for (var i = 0, l = keys.length; i < l; ++i)
			{
				var key = keys[i];
				var value = jsonObj[key];
				switch (key)
				{
					case 'value': result.setValue(value); break;
					case 'errorValue': result.setErrorValue(value); break;
					case 'units': result.setUnit(value); break;
					case 'title': result.setTitle(value); break;
					case 'dictRef': result.setName(value); break;
					default: result.setInfoValue(key, value);
				}
			}
		}
		return result;
	}
});

/**
 * CML <scalar> element writer, save {@link Kekule.Scalar}.
 * @class
 * @augments Kekule.IO.CmlElementWriter
 */
Kekule.IO.CmlScalarWriter = Class.create(Kekule.IO.CmlElementWriter,
/** @lends Kekule.IO.CmlScalarWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlScalarWriter',
	/** @private */
	doCreateElem: function(obj, parentElem)
	{
		return this.createChildElem('scalar', parentElem);
	},
	/** @private */
	doWriteObject: function(obj, targetElem)
	{
		if (!(obj instanceof Kekule.Scalar))
		{
			this.reportTypeMismatchError(obj);
			return null;
		}
		var sValueType;
		if (!Kekule.ObjUtils.isUnset(obj.getValue()))
		{
			Kekule.DomUtils.setElementText(targetElem, obj.getValue());
			sValueType = DataType.getType(obj.getValue());
		}
		if (obj.getName())
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, 'dictRef', obj.getName(), this.getDomHelper());
		if (obj.getErrorValue())
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, 'errorValue', obj.getErrorValue(), this.getDomHelper());
		if (obj.getUnit())
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, 'units', obj.getUnit(), this.getDomHelper());
		if (obj.getTitle())
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, 'title', obj.getTitle(), this.getDomHelper());
		// dataType
		if (sValueType)
		{
			var sDataType =
				(sValueType == DataType.INT)? 'xsd:integer':
				(sValueType == DataType.FLOAT)? 'xsd:float':
				(sValueType == DataType.BOOL)? 'xsd:boolean':
				null;
			if (sDataType)
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, 'datatype', sDataType, this.getDomHelper());
		}
	}
});

/**
 * CML <meta> element reader.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlMetaDataReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlMetaReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlMetaReader',
	/** @private */
	doReadElement: function(elem, parentObj)
	{
		var meta = this.readMeta(elem);
		if (parentObj && parentObj.setInfoValue)  // parent is a ChemObject and can insert info
		{
			if (meta.key && meta.value)
				parentObj.setInfoValue(meta.key, meta.value);
		}
		else  // may be used in other situation, do nothing and just return a object
			;
		return meta;
	},
	/** @private */
	readMeta: function(elem)
	{
		var jsonObj = Kekule.DomUtils.fetchAttributeValuesToJson(elem, this.getCoreNamespaceURI(), true);
		var result = {'key': jsonObj.name, 'value': DataType.StringUtils.deserializeValue(jsonObj.content)};
		return result;
	}
});

/**
 * CML <metaList> element reader.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlMetaDataListReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlMetaListReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlMetaListReader',
	/** @private */
	doReadElement: function(elem, parentObj)
	{
		var metaElems = Kekule.DomUtils.getDirectChildElems(elem, null, 'metaData', this.getCoreNamespaceURI());
		for (var i = 0, l = metaElems.length; i < l; ++i)
		{
			var metaElem = metaElems[i];
			var reader = Kekule.IO.CmlElementReaderFactory.getReader(metaElem);
			if (reader)
			{
				this.copySettingsToChildHandler(reader);
				reader.readElement(metaElem, parentObj);
			}
		}
	}
});

/**
 * Reader to read a molecule structure in CML.
 * Base class of {@link Kekule.IO.CmlFormulaReader} and {@link Kekule.IO.CmlMoleculeReader}.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlChemStructureReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlChemStructureReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlChemStructureReader',
	/** @constructs */
	initialize: function($super)
	{
		$super();
	},
	/**
	 * Check if a <atomArray> element has child <atom> elements.
	 * @param {Object} elem
	 * @param {Object} domHelper
	 */
	hasAtomChildren: function(elem, domHelper)
	{
		var atomElems = domHelper.getElementsByTagNameNS(this.getCoreNamespaceURI(), 'atom', elem);
		return atomElems.length;
	},
	/**
	 * Returns child <atom> elements of <atomArray>.
	 * @param {Object} elem
	 * @param {Object} domHelper
	 */
	getAtomChildren: function(elem, domHelper)
	{
		var atomElems = domHelper.getElementsByTagNameNS(this.getCoreNamespaceURI(), 'atom', elem);
		return atomElems;
	},

	/**
	 * Read and analysis an atom element of CML and fetch informations to a JSON object.
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @returns {Hash}
	 * @private
	 */
	atomInfoToJSON: function(elem, domHelper)
	{
		var result;
		var attribs = {}; // store properties of this atom
		//var hasChildInfoElems = false;  // in mode I, atom may has scalar, electron, atomParity... to store additional information
		// check if elem has string/integer/float children, if so, the old CML1 mode
		//hasChildInfoElems = !Kekule.IO.CmlDomUtils.hasDirectCmlTypedElemChildren(elem, this.getCoreNamespaceURI());
		attribs = Kekule.IO.CmlDomUtils.fetchCmlElemAttributeValuesToJson(elem, Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, true, domHelper);
		return attribs;
	},

	/**
	 * Read <atomArray> array information and create an array of JSON to store atom informations.
	 * @returns {Array} Array of JSON of atom attribs.
	 * @private
	 */
	arrayedAtomInfosToJSON: function(elem, domHelper)
	{
		var arrayedAttribNames = ['id', 'atomID', 'elementType', 'isotope', 'hydrogenCount', 'formalCharge', 'count',
			'xFract', 'yFract', 'zFract',
			'x2', 'y2', 'x3', 'y3', 'z3'];
		var attribNames = ['id', 'id', 'elementType', 'isotope', 'hydrogenCount', 'formalCharge', 'count',
			'xFract', 'yFract', 'zFract',
			'x2', 'y2', 'x3', 'y3', 'z3'];
		var atomAttribs = [];

		//var childElems = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());

		for (var i = 0, l = arrayedAttribNames.length; i < l; ++i)
		{
			var svalue;
			var values = [];

			svalue = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, arrayedAttribNames[i], Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM, domHelper);
			if (svalue)
				values = Kekule.IO.CmlDomUtils.splitCmlArrayValue(svalue);

			for (var j = 0, k = values.length; j < k; ++j)
			{
				if (!atomAttribs[j])
					atomAttribs[j] = {};
				if (values[j])
					atomAttribs[j][attribNames[i]] = values[j];
			}
		}

		return atomAttribs;
	}
});

/**
 * CML <formula> element reader.
 * @class
 * @augments Kekule.IO.CmlChemStructureReader
 */
Kekule.IO.CmlFormulaReader = Class.create(Kekule.IO.CmlChemStructureReader,
/** @lends Kekule.IO.CmlFormulaReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlFormulaReader',
	/**
	 * Read a <formula> element and returns new Kekule.MolecularFormula.
	 * @param {Object} elem
	 * @returns {Kekule.MolecularFormula}
	 * @private
	 */
	doReadElement: function(elem, parentObj)
	{
		return this.readFormula(elem, this.getDomHelper());
	},

	/**
	 * Read an formula a CML formula element.
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @returns {Kekule.MolecularFormula}
	 */
	readFormula: function(elem, domHelper)
	{
		var result = new Kekule.MolecularFormula();
		var charge = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'formalCharge', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
		if (charge)
			result.setCharge(parseFloat(charge));
		var concise = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'concise', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
		if (concise)
		{
			this.setConcise(result, concise);
		}
		else
		{
			// iterate through direct children of molecule element
			var childElems = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
			for (var i = 0, l = childElems.length; i < l; ++i)
			{
				// Check node name for different function
				switch (Kekule.DomUtils.getLocalName(childElems[i]))
				{
					case 'formula':
						this.readSubFormula(result, childElems[i], domHelper);
						break;
					case 'atomArray':
						this.readAtomArray(result, childElems[i], domHelper);
						break;
					default:
						{
							// bypass CML1 style typed elements
							if (Kekule.IO.CmlDomUtils.isCmlTypedElem(node) || Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(node))
								;
							else
								this.readChildElementDef(node, result);
						}
				}
			}
		}
		return result;
	},
	/**
	 * Read concise directly and return a formula object.
	 * Sometimes formula attrib are in molecule element directly, such as <molecule formula="H 2 O 1" />,
	 * use this method to analysis and return proper object instead of {@link Kekule.IO.CmlFormulaReader.readElement}.
	 * @param {String} concise
	 * @returns {Kekule.MolecularFormula}
	 */
	readConsice: function(concise)
	{
		var result = null;
		if (concise)
		{
			result = new Kekule.MolecularFormula();
			this.setConcise(result, concise);
		}
		return result;
	},

	/** @private */
	setConcise: function(formula, concise)
	{
		var info = Kekule.IO.CmlDomUtils.analysisCmlFormulaConciseValue(concise);
		if (info.isotopes && info.isotopes.length)
		{
			this.createSectionsFromAtomAttribs(formula, info.isotopes);
		}
		if (info.formalCharge)
			formula.setCharge(info.formalCharge);
		return formula;
	},

	/**
	 * Read <atomArray> element inside <formula>
	 * @param {Object} parentFormula
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @private
	 */
	readAtomArray: function(parentFormula, elem, domHelper)
	{
		var atomAttribs = [];
		var atomElems = this.getAtomChildren(elem, domHelper);
		if (atomElems.length > 0)  // has child atom elements
		{
			for (var i = 0, l = atomElems.length; i < l; ++i)
			{
				var atomElem = atomElems[i];
				var atomAttrib = this.atomInfoToJSON(atomElem, domHelper);
				atomAttribs.push(atomAttrib);
			}
		}
		else  // no child atom element, in array mode
		{
			atomAttribs = this.arrayedAtomInfosToJSON(elem, domHelper);
		}
		// add information in atomAttribs to parentFormula
		this.createSectionsFromAtomAttribs(parentFormula, atomAttribs);
		return parentFormula;
	},
	/** @private */
	createSectionsFromAtomAttribs: function(formula, atomAttribs)
	{
		// add information in atomAttribs to parentFormula
		for (var i = 0, l = atomAttribs.length; i < l; ++i)
		{
			var symbol = atomAttribs[i].elementType;
			if (symbol)
			{
				//var isotope = Kekule.IsotopeFactory.getIsotope(symbol, atomAttribs[i].isotope || null);
				var atom = Kekule.IO.CmlUtils.createNodeByCmdElementType(null, symbol);
				var charge = (atomAttribs[i].formalCharge) || 0;
				var count = atomAttribs[i].count;
				//var charge = atomAttribs[i].formalCharge;
				formula.appendSection(atom, count, charge);
			}
		}
	},
	/**
	 * Read nested formula element.
	 * @param {Object} parentFormula
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @private
	 */
	readSubFormula: function(parentFormula, elem, domHelper)
	{
		var formula = this.readElement(elem, domHelper);
		// check sub formula count
		var count = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'count', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
		parentFormula.appendSection(formula, parseFloat(count) || 1, formula.getCharge() || 0);
		//formula.setCharge(0);
	}
});

/**
 * CML <formula> element writer for outputting {@link Kekule.MolecularFormula}.
 * @class
 * @augments Kekule.IO.CmlElementWriter
 */
Kekule.IO.CmlFormulaWriter = Class.create(Kekule.IO.CmlElementWriter,
/** @lends Kekule.IO.CmlFormulaWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlFormulaWriter',
	/** @private */
	doCreateElem: function(obj, parentElem)
	{
		return this.createChildElem('formula', parentElem);
	},
	/** @private */
	doWriteObject: function(obj, targetElem)
	{
		if (!(obj instanceof Kekule.MolecularFormula))
		{
			this.reportTypeMismatchError(obj);
			return null;
		}
		var atomSymbols = [];
		var counts = [];
		var charges = [];
		for (var i = 0, l = obj.getSectionCount(); i < l; ++i)
		{
			var section = obj.getSectionAt(i);
			if (section.obj instanceof Kekule.MolecularFormula)  // sub formula
			{
				// write atoms before this sub formula
				this.createAtomArrayElem(atomSymbols, counts, charges, targetElem);
				// empty them
				atomSymbols = [];
				counts = [];
				charges = [];
				// create sub formula element
				var subElem = this.writeSubFormula(section.obj, section.count || 1, targetElem);
			}
			else
			{
				atomSymbols.push(Kekule.IO.CmlUtils.getNodeElementType(section.obj));
				counts.push(section.count || 1);
				//charges.push(section.obj.getCharge? (section.obj.getCharge() || 0): 0);
				charges.push(obj.getSectionCharge(section) || 0);
				if (i == l - 1)  // last item, write to element
				{
					this.createAtomArrayElem(atomSymbols, counts, charges, targetElem);
				}
			}
		}
		if (obj.getCharge())
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, 'formalCharge', obj.getCharge(), this.getDomHelper());
	},
	/** @private */
	writeSubFormula: function(formula, count, parentElem)
	{
		var result = this.createChildElem('formula', parentElem);
		this.doWriteObject(formula, result);  // as formula has no id, doWrite is the same as write
		if (count > 1)
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'count', count, this.getDomHelper());
		return result;
	},
	/** @private */
	createAtomArrayElem: function(atomSymbols, counts, charges, parentElem)
	{
		if (atomSymbols.length <= 0)
			return;
		var result = this.createChildElem('atomArray', parentElem);
		var sSymbols = Kekule.IO.CmlDomUtils.mergeToCmlArrayValue(atomSymbols);
		var scounts = Kekule.IO.CmlDomUtils.mergeToCmlArrayValue(counts);
		Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'elementType', sSymbols, this.getDomHelper());
		Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'count', scounts, this.getDomHelper());
		// check if charges need to be set explicitly
		var needCharge = false;
		for (var i = 0, l = charges.length; i < l; ++i)
		{
			if (charges[i])
			{
				needCharge = true;
				break;
			}
		}
		if (needCharge)
		{
			var scharges = Kekule.IO.CmlDomUtils.mergeToCmlArrayValue(counts);
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'formalCharge', scharges, this.getDomHelper());
		}
		return result;
	}
});

/**
 * CML <molecule> element reader.
 * @class
 * @augments Kekule.IO.CmlChemStructureReader
 */
Kekule.IO.CmlMoleculeReader = Class.create(Kekule.IO.CmlChemStructureReader,
/** @lends Kekule.IO.CmlMoleculeReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlMoleculeReader',
	/** @constructs */
	initialize: function($super)
	{
		$super();
		//this._coreNamespaceURI = '';  // used internally
	},
	/** @private */
	initProperties: function()
	{
		// a private property
		this.defineProp('structureBuilder', {
			'dataType': 'Kekule.ChemStructureBuilder',
			'serializable': false,
			// clone result object so that user can not modify x/y directly from getter
			'getter': function()
				{
					if (!this.getPropStoreFieldValue('structureBuilder'))
						this.setPropStoreFieldValue('structureBuilder', new Kekule.ChemStructureBuilder());
					return this.getPropStoreFieldValue('structureBuilder');
				},
			'setter': null
		});
	},

	/**
	 * Read a <molecule> element and returns new Kekule.Molecule.
	 * @param {Object} elem
	 * @returns {Variant}
	 * @private
	 */
	doReadElement: function(elem, parentObj)
	{
		return this.readMolecule(elem, this.getDomHelper());
	},

	/**
	 * Override to handle child elements of molecule.
	 * @private
	 */
	doReadChildElement: function($super, elem, parentObj)
	{
		if ((Kekule.DomUtils.getLocalName(elem) == 'molecule') && (this.matchCoreNamespace(elem))) // has sub molecule
		{
			if (parentObj.getSubMolecules)
			{
				var result = this.readElement(elem, parentObj);
				if (result)
				{
					// sub molecule element should has amount attribute
					var amount =
						Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'amount', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM)
							|| Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'count', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM);
					var attributes = amount? {'amount': parseFloat(amount)}: null;
					parentObj.getSubMolecules().appendObj(result, attributes);
				}
				return result;
			}
			else
				return null;
		}
		else
			return $super(elem, parentObj);
	},

	/**
	 * check if a <molecule> elemen has sub molecules
	 * @param {Object} elem
	 */
	hasSubMolecule: function(elem)
	{
		var subElems = Kekule.DomUtils.getDirectChildElems(elem, null, 'molecule', this.getCoreNamespaceURI());
		return (subElems.length > 0);
	},

	/**
	 * Read an molecule structure from a CML molecule element. This function can handle sub molecules.
	 * @param {Object} elem
	 * @param {Object} domHelper
	 */
	readMolecule: function(elem, domHelper)
	{
		var result;
		if (this.hasSubMolecule(elem))
		{
			result = new Kekule.CompositeMolecule();
			this.iterateChildElements(elem, result);
		}
		else  // a pure molecule
		{
			result = this.readMoleculeCore(elem, domHelper);
		}
		this.readMoleculeAttribs(result, elem, domHelper);
		return result;
	},

	/**
	 * Read and analysis attributes of molecule element.
	 * @param {Object} molecule
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @private
	 */
	readMoleculeAttribs: function(molecule, elem, domHelper)
	{
		// read molecule element attributes
		var attribs = Kekule.IO.CmlDomUtils.fetchCmlElemAttributeValuesToJson(elem, null, true, domHelper);
		var attribKeys = Kekule.ObjUtils.getOwnedFieldNames(attribs);
		for (var i = 0, l = attribKeys.length; i < l; ++i)
		{
			var key = attribKeys[i];
			var value = attribs[key];
			switch (key)
			{
				case 'id':
					molecule.setId(value);
					break;
				case 'title':
					molecule.setInfoValue('title', value);
					break;
				case 'formalCharge':
					if (value)
						molecule.setCharge(parseFloat(value));
					break;
				case 'formula':  // formula embedded in attribute, rather than an element
					this.readFormulaAttrib(molecule, value);
					break;
				case 'amount':
				case 'count':  // used for sub-molecule, handled by parent molecule, not here
					break;
				default:
					// TODO: bypassed attributes: [dictRef, chirality[], ref,
					//   spinMultiplicity, symmetryOriented, role]
					molecule.setInfoValue(key, value);
			}
		}
	},

	/**
	 * Read an molecule structure from a CML molecule element. This function only handles
	 *   a "pure" molecule tag (with no sub-molecule elements).
	 * In CML, there is four forms to define a molecule's connection table:
	 *
	 *   I. Use detailed elements:
	 *
<cml title="schematic molecule example">
  <molecule id="dummyId">
    <atomArray>
      <atom id="a1" elementType="C"
        hydrogenCount="0" x2="6.1964" y2="8.988"/>
      <atom id="a2" elementType="C"
        hydrogenCount="0" x2="6.1964" y2="7.587"/>
      <atom id="a3" elementType="C"
        hydrogenCount="2" x2="4.983" y2="6.887"/>
<!-- omitted -->
      <atom id="a28" elementType="C"
        hydrogenCount="3" x2="15.777" y2="6.554"/>
      <atom id="a29" elementType="O"
        hydrogenCount="0" x2="13.388" y2="6.188"/>
    </atomArray>
    <bondArray>
      <bond atomRefs2="a1 a2" order="1"/>
      <bond atomRefs2="a2 a3" order="1"/>
      <bond atomRefs2="a3 a4" order="1"/>
<!-- omitted -->
      <bond atomRefs2="a11 a15" order="1"/>
      <bond atomRefs2="a12 a18" order="1">
        <bondStereo>W</bondStereo>
      </bond>
      <bond atomRefs2="a2 a19" order="1">
        <bondStereo>W</bondStereo>
      </bond>
      <bond atomRefs2="a5 a20" order="2"/>
      <bond atomRefs2="a17 a21" order="1"/>
      <bond atomRefs2="a21 a22" order="1"/>
<!-- omitted -->
      <bond atomRefs2="a10 a9" order="1"/>
      <bond atomRefs2="a16 a29" order="2"/>
    </bondArray>
  </molecule>
</cml>
	 *
	 * II. Use array property:
	 *
<cml title="electron example">
  <molecule id="m1">
    <atomArray atomID="a1 a2 a3 a4 a5 a6"/>
    <bondArray
      order="A A A A A A"
      bondID="b1 b2 b3 b4 b5 b6"
      atomRef1="a1 a2 a3 a4 a5 a6"
      atomRef2="a6 a1 a2 a3 a4 a5"/>
    <electron count="6"
      bondRefs="b1 b2 b3 b4 b5 b6"
      atomRefs="a1 a2 a3 a4 a5 a6"/>
  </molecule>
</cml>
	 *
	 *  III. Deprecated form in CML1
	 *
<cml title="curan molecule">
	<molecule convention="MDLMol" id="curan">
	  <metadataList>
	    <metadata name="dc:date">2000-08-27</metadata>
	  </metadataList>
	  <atomArray>
	    <atom id="a1">
	      <string builtin="elementType">C</string>
	      <float builtin="x2">10.62</float>
	      <float builtin="y2">-9.0918</float>
	    </atom>
	    <atom id="a2">
	      <string builtin="elementType">C</string>
	      <float builtin="x2">10.62</float>
	      <float builtin="y2">-10.0442</float>
	    </atom>
	    <!-- ... -->
	  </atomArray>
	  <bondArray>
	    <bond id="b1">
	      <string builtin="atomRef">a1</string>
	      <string builtin="atomRef">a3</string>
	      <string builtin="order">2</string>
	    </bond>
	    <bond id="b2">
	      <string builtin="atomRef">a2</string>
	      <string builtin="atomRef">a4</string>
	      <string builtin="order">2</string>
	    </bond>
	    <!-- ... -->
	  </bondArray>
	</molecule>
</cml>
	 *
	 *  IV. Deprecated form 2 in CML1
	 *
<cml title="CML-1 JCICS examples">
	<molecule id="formamide">
		<atomArray>
			<stringArray builtin="atomId">H1 C1 O1 N1 Me1 Me2</stringArray>
			<stringArray builtin="elementType">H C O N C C</stringArray>
			<integerArray builtin="hydrogenCount">0 1 0 1 3	3</integerArray>
		</atomArray>
		<bondArray>
			<stringArray builtin="atomRef">C1 C1 C1 N1 N1</stringArray>
			<stringArray builtin="atomRef">H1 O1 N1 Me1 Me2</stringArray>
			<stringArray builtin="order">1 2 1 1 1</stringArray>
		</bondArray>
	</molecule>
</cml>
	 *
	 * @private
	 * @param {Object} elem
	 * @param {Kekule.DomHelper} domHelper
	 * @returns {Kekule.Molecule}
	 */
	readMoleculeCore: function(elem, domHelper)
	{
		var result = new Kekule.Molecule();
		this.getStructureBuilder().setTarget(result);
		// iterate through direct children of molecule element
		var node = elem.firstChild;
		while (node)
		{
			if (node.nodeType != Node.ELEMENT_NODE) // bypass non-element nodes
				;
			else if (!this.matchCoreNamespace(node)) // ignore element of other namespaces
				;
			else
			{
				// Check node name for different function
				switch (Kekule.DomUtils.getLocalName(node))
				{
					case 'atomArray':
						this.readMoleculeAtomArray(result, node, this.getStructureBuilder(), domHelper);
						break;
					case 'bondArray':
						this.readMoleculeBondArray(result, node, this.getStructureBuilder(), domHelper);
						break;
					case 'formula':
						this.readFormula(result, node, domHelper);
						break;
					case 'atom':   // sometimes atom or bond is directly add to molecule element
						this.getStructureBuilder().setTarget(result);
						var structureNode = this.readAtom(node, domHelper);
						this.getStructureBuilder().appendNode(structureNode);
						break;
					case 'bond':
						this.getStructureBuilder().setTarget(result);
						var connector = this.readBond(result, node, domHelper);
						this.getStructureBuilder().appendConnector(connector);
						break;
					case 'molecule':
						break;  // do nothing, as pure molecule element should not contain sub-molecule
					default:
						{
							// bypass CML1 style typed elements
							if (Kekule.IO.CmlDomUtils.isCmlTypedElem(node) || Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(node))
								;
							else
								this.readChildElementDef(node, result);
						}
				}
			}
			node = node.nextSibling;
		}

		if (result.hasCtab())
			this.connectUpBondAndAtom(result);

		return result;
	},

	/**
	 * As we do not know whether atomArray or bondArray is read first, all connection info
	 * are stored in private fields temporarily. After all atoms and bonds object are created,
	 * we need to connect them togather.
	 * @private
	 */
	connectUpBondAndAtom: function(molecule)
	{
		var bonds = molecule.getConnectors();
		for (var i = 0, l = bonds.length; i < l; ++i)
		{
			var bond = bonds[i];
			// add bond-atom relation
			var atomIds = bond[this._BOND_ATOM_REF_TEMP_FIELD];
			if (atomIds && atomIds.length)
			{
				for (var j = 0, k = atomIds.length; j < k; ++j)
				{
					var refedAtom = molecule.getNodeById(atomIds[j]);
					if (refedAtom)
						bond.appendConnectedObj(refedAtom);
					else
					{
						Kekule.raise(
							(Kekule.hasLocalRes()?	/*Kekule.ErrorMsg.ATOMID_NOT_EXISTS*/Kekule.$L('ErrorMsg.ATOMID_NOT_EXISTS'): 'Atom id not exists: ')
								+ atomIds[j]
						);
					}
				}
			}
			delete bond[this._BOND_ATOM_REF_TEMP_FIELD];
			// as bond element may have refs to another not created bond or atom, handle this after all bonds are created
			var bondIds = bond[this._BOND_BOND_REF_TEMP_FIELD];
			if (bondIds && bondIds.length)
			{
				for (var j = 0, k = bondIds.length; j < k; ++j)
				{
					var refedBond = molecule.getConnectorById(bondIds[j]);
					if (refedBond)
						bond.appendConnectedObj(refedBond);
					else
					{
						Kekule.raise(
							(Kekule.hasLocalRes()?	/*Kekule.ErrorMsg.BONDID_NOT_EXISTS*/Kekule.$L('ErrorMsg.BONDID_NOT_EXISTS'): 'Bond id not exists: ')
								+ bondIds[j]
						);
					}
				}
			}
			delete bond[this._BOND_BOND_REF_TEMP_FIELD];
		}
	},

	/**
	 * Read atomArray element in CML.
	 * @param {Kekule.Molecule} molecule
	 * @param {Object} elem
	 * @param {Kekule.ChemStructureBuilder} structureBuilder
	 * @param {Object} domHelper
	 * @private
	 */
	readMoleculeAtomArray: function(molecule, elem, structureBuilder, domHelper)
	{
		structureBuilder.setTarget(molecule);
		// check if has atom child, if true, in mode I or III
		//var atomElems = domHelper.getElementsByTagNameNS(this.getCoreNamespaceURI(), 'atom', elem);
		var atomElems = this.getAtomChildren(elem, domHelper);
		if (atomElems.length > 0)  // in mode I or III
		{
			for (var i = 0, l = atomElems.length; i < l; ++i)
			{
				var atomElem = atomElems[i];
				var structureNode = this.readAtom(atomElem, domHelper);
				structureBuilder.appendNode(structureNode);
			}
		}
		else  // may in mode II or IV
		{
			var atoms = this.readArrayedAtoms(elem, domHelper);
			for (var i = 0, l = atoms.length; i < l; ++i)
			{
				structureBuilder.appendNode(atoms[i]);
			}
		}
	},

	/**
	 * Read and analysis an atom element of CML
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @returns {Variant} Return values may be Kekule.Atom, Kekule.UnrealAtom or Kekule.SubGroup or null
	 * @private
	 */
	readAtom: function(elem, domHelper)
	{
		var result;
		//var attribs = []; // store properties of this atom
		//var hasChildInfoElems = false;  // in mode I, atom may has scalar, electron, atomParity... to store additional information
		// check if elem has string/integer/float children, if so, the old CML1 mode
		hasChildInfoElems = !Kekule.IO.CmlDomUtils.hasDirectCmlTypedElemChildren(elem, this.getCoreNamespaceURI());
		//attribs = Kekule.IO.CmlDomUtils.fetchCmlElemAttributeValuesToJson(elem, Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, true, domHelper);
		var attribs = this.atomInfoToJSON(elem, domHelper);

		result = this.createStructureNodeOfAttribs(attribs);

		if (hasChildInfoElems)  // additional info, check child elements
		{
			// TODO: currently ignore electron and atomParity element, only consider <scalar>
			var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
			for (var i = 0, l = children.length; i < l; ++i)
			{
				var elem = children[i];
				/*
				if (Kekule.IO.CmlDomUtils.isScalarElem(elem, this.getCoreNamespaceURI()))
				{
					var scalarInfo = this.readScalar(elem);
					// add scalar info to result
					var keys = Kekule.ObjUtils.getOwnedFieldNames(scalarInfo);
					for (var i = 0, l = keys.length; i < l; ++i)
					{
						result.setInfoValue(keys[i], scalarInfo[keys[i]]);
					}
				}
				*/
				/*
				var reader = Kekule.IO.CmlElementReaderFactory.getReader(elem);
				reader.setCoreNamespaceURI(this.getCoreNamespaceURI());
				if (reader)
					reader.readElement(elem, result);
				*/
				this.readChildElementDef(elem, result);
			}
		}

		return result;
	},

	/**
	 * Read array information and create all atoms
	 * @returns {Array} Array of Kekule.Atom
	 * @private
	 */
	readArrayedAtoms: function(elem, domHelper/*, useTypedArrayElem*/)
	{
		/*
		var arrayedAttribNames = ['atomID', 'elementType', 'isotope', 'hydrogenCount', 'formalCharge',
			'xFract', 'yFract', 'zFract',
			'x2', 'y2', 'x3', 'y3', 'z3'];
		var attribNames = ['id', 'elementType', 'isotope', 'hydrogenCount', 'formalCharge',
			'xFract', 'yFract', 'zFract',
			'x2', 'y2', 'x3', 'y3', 'z3'];
		var atomAttribs = [];

		//var childElems = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());

		for (var i = 0, l = arrayedAttribNames.length; i < l; ++i)
		{
			var svalue;
			var values = [];

			svalue = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, arrayedAttribNames[i], Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM, domHelper);
			if (svalue)
				values = Kekule.IO.CmlDomUtils.splitCmlArrayValue(svalue);

			for (var j = 0, k = values.length; j < k; ++j)
			{
				if (!atomAttribs[j])
					atomAttribs[j] = {};
				if (values[j])
					atomAttribs[j][attribNames[i]] = values[j];
			}
		}
		*/
		var atomAttribs = this.arrayedAtomInfosToJSON(elem, domHelper);

		var result = [];
		for (var i = 0, l = atomAttribs.length; i < l; ++i)
		{
			var atom = this.createStructureNodeOfAttribs(atomAttribs[i]);
			if (atom)
				result.push(atom);
		}

		return result;
	},

	/**
	 * Create atom by JSON attribs object.
	 * @param {Hash} attribs
	 * @returns {Kekule.ChemStructureNode}
	 * @private
	 */
	createStructureNodeOfAttribs: function(attribs)
	{
		var result;
		// analysis attribs to create atom
		if (!attribs.elementType)
			attribs.elementType = Kekule.Element.UNSET_ELEMENT;
		//if (attribs.elementType)
		{
			// id
			var id = null;
			if (attribs.id !== undefined)
			{
				id = attribs.id;
			}
			delete attribs['id'];

			/*
			if (Kekule.IO.CmlUtils.isDummyElementType(attribs.elementType))
				result = new Kekule.Pseudoatom(id, Kekule.PseudoatomType.DUMMY);
			else if (Kekule.IO.CmlUtils.isRGroupElementType(attribs.elementType))
			{
				result = new Kekule.RGroup(id);
			}
			else  // a normal atom
			{
				// in CML, an isotope attribute can be a mass number or a accurate mass, so round it
				// here to get a integer mass number
				var massNumber = attribs.isotope? Math.round(attribs.isotope): null;
				result = new Kekule.Atom(id, attribs.elementType, massNumber);

				// hydrogen count
				if (attribs.hydrogenCount !== undefined)
				{
					result.setHydrogenCount(parseInt(attribs.hydrogenCount));
					delete attribs['hydrogenCount'];
				}
			}
			*/
			// in CML, an isotope attribute can be a mass number or a accurate mass, so round it
			// here to get a integer mass number
			var massNumber = attribs.isotope? Math.round(attribs.isotope): null;
			result = Kekule.IO.CmlUtils.createNodeByCmdElementType(id, attribs.elementType, massNumber);
			delete attribs['elementType'];
			// hydrogen count
			if (attribs.hydrogenCount !== undefined)
			{
				if (result.setExplicitHydrogenCount)
					result.setExplicitHydrogenCount(parseInt(attribs.hydrogenCount, 10));
			}
			delete attribs['hydrogenCount'];

			// charge
			if (attribs.formalCharge)
			{
				result.setCharge(parseInt(attribs.formalCharge)); // in CML formalCharge has no partial ones, all int
			}
			delete attribs['formalCharge'];
			// title
			if (attribs.title)
			{
				result.setInfoValue('title', attribs.title);
			}
			delete attribs['title'];
			// role
			if (attribs.role)
			{
				result.setInfoValue('role', attribs.role);
			}
			delete attribs['role'];

			// coordinates
			var coord2D, coord3D;
			if ((attribs.x2 !== undefined) && (attribs.y2 !== undefined))  // has x2/y2
			{
				coord2D = {'x': parseFloat(attribs.x2), 'y': parseFloat(attribs.y2)};
			}
			else if (attribs.xy2)  // has xy2
			{
				var xy = Kekule.IO.CmlDomUtils.splitCmlArrayValue(attribs.xy2);
				if (xy.length == 2)
				{
					coord2D = {'x': parseFloat(xy[0]), 'y': parseFloat(xy[1])};
				}
			}
			delete attribs['x2'];
			delete attribs['y2'];
			delete attribs['xy2'];
			if ((attribs.x3 !== undefined) && (attribs.y3 !== undefined) && (attribs.z3 !== undefined))  // has x3/y3/z3
			{
				coord3D = {'x': parseFloat(attribs.x3), 'y': parseFloat(attribs.y3), 'z': parseFloat(attribs.z3)};
			}
			else if (attribs.xyz3)  // has xyz2
			{
				var xyz = Kekule.IO.CmlDomUtils.splitCmlArrayValue(attribs.xyz3);
				if (xyz.length == 3)
				{
					coord2D = {'x': parseFloat(xyz[0]), 'y': parseFloat(xyz[1]), 'z': parseFloat(xyz[2])};
				}
			}
			delete attribs['x3'];
			delete attribs['y3'];
			delete attribs['z3'];
			delete attribs['xyz3'];
			if (coord2D)
				result.setCoord2D(coord2D);
			if (coord3D)
				result.setCoord3D(coord3D);

			// TODO: ignore attribs: [occupancy, xFract, xyzFract, yFract, convention, dictRef, ref]
			// rest of attribs will be set to info property of result
			for (var keyName in attribs)
			{
				if (attribs.hasOwnProperty(keyName) && (typeof(attribs[keyName]) == 'string'))
					result.setInfoValue(keyName, attribs[keyName]);
			}
		}
		return result;
	},

	/** @private */
	_BOND_ATOM_REF_TEMP_FIELD: '__cml_atomRefs',
	/** @private */
	_BOND_BOND_REF_TEMP_FIELD: '__cml_bondRefs',

	/**
	 * Read <bondArray> element from molecule.
	 * @param {Object} molecule
	 * @param {Object} elem
	 * @param {Kekule.ChemStructureBuilder} structureBuilder
	 * @param {Kekule.DomHelper} domHelper
	 * @private
	 */
	readMoleculeBondArray: function(molecule, elem, structureBuilder, domHelper)
	{
		structureBuilder.setTarget(molecule);
		// check if has bond child, if true, in mode I or III
		var bondElems = domHelper.getElementsByTagNameNS(this.getCoreNamespaceURI(), 'bond', elem);
		if (bondElems.length > 0)  // in mode I or III
		{
			for (var i = 0, l = bondElems.length; i < l; ++i)
			{
				var bondElem = bondElems[i];
				var connector = this.readBond(molecule, bondElem, domHelper);
				structureBuilder.appendConnector(connector);
			}
		}
		else  // may in mode II or IV
		{
			var bonds = this.readArrayedBonds(molecule, elem, domHelper);
			for (var i = 0, l = bonds.length; i < l; ++i)
			{
				structureBuilder.appendConnector(bonds[i]);
			}
		}

		/*
		var bonds = molecule.getConnectors();
		for (var i = 0, l = bonds.length; i < l; ++i)
		{
			var bond = bonds[i];
			// add bond-atom relation
			var atomIds = bond[this._BOND_ATOM_REF_TEMP_FIELD];
			if (atomIds && atomIds.length)
			{
				for (j = 0, k = atomIds.length; j < k; ++j)
				{
					var refedAtom = molecule.getNodeById(atomIds[j]);
					if (refedAtom)
						bond.appendConnectedObj(refedAtom);
					else
						Kekule.raise(Kekule.ErrorMsg.ATOMID_NOT_EXISTS + atomIds[j]);
				}
			}
			delete bond[this._BOND_ATOM_REF_TEMP_FIELD];
			// as bond element may have refs to another not created bond or atom, handle this after all bonds are created
			var bondIds = bond[this._BOND_BOND_REF_TEMP_FIELD];
			if (bondIds && bondIds.length)
			{
				for (j = 0, k = bondIds.length; j < k; ++j)
				{
					var refedBond = molecule.getConnectorById(bondIds[j]);
					if (refedBond)
						bond.appendConnectedObj(refedBond);
					else
						Kekule.raise(Kekule.ErrorMsg.BONDID_NOT_EXISTS + bondIds[j]);
				}
			}
			delete bond[this._BOND_BOND_REF_TEMP_FIELD];
		}
		*/
	},

	/**
	 * Read and analysis an <bond> element of CML
	 * @param {Kekule.StructureFragment} molecule
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @returns {Kekule.Bond}
	 * @private
	 */
	readBond: function(molecule, elem, domHelper)
	{
		var result;
		var attribs = []; // store properties of this bond
		var hasChildInfoElems = false;  // in mode I, atom may has scalar, electron, atomParity... to store additional information
		// check if elem has string/integer/float children, if so, the old CML1 mode
		hasChildInfoElems = !Kekule.IO.CmlDomUtils.hasDirectCmlTypedElemChildren(elem, this.getCoreNamespaceURI());
		attribs = Kekule.IO.CmlDomUtils.fetchCmlElemAttributeValuesToJson(elem, Kekule.IO.CmlUtils.FILTER_TYPED_ELEM, true, domHelper);
		result = this.createStructureConnectorOfAttribs(molecule, attribs);

		if (hasChildInfoElems)  // additional info, check child elements
		{
			// TODO: currently only consider <scalar> and <bondStereo>
			var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
			for (var i = 0, l = children.length; i < l; ++i)
			{
				var elem = children[i];
				/*
				if (Kekule.IO.CmlDomUtils.isScalarElem(elem, this.getCoreNamespaceURI()))
				{
					var scalarInfo = this.readScalar(elem);
					// add scalar info to result
					var keys = Kekule.ObjUtils.getOwnedFieldNames(scalarInfo);
					for (var i = 0, l = keys.length; i < l; ++i)
					{
						result.setInfoValue(keys[i], scalarInfo[keys[i]]);
					}
				}
				else*/
				if (Kekule.DomUtils.getLocalName(elem) == 'bondStereo') // check bondStereo element
				{
					// TODO: haven't handle convertion and convertionValue attribs
					var cmlStereo = Kekule.DomUtils.getElementText(elem);
					if (!cmlStereo)
						cmlStereo = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'conversionValue', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
					var kStereo = Kekule.IO.CmlUtils.cmlBondStereoToKekule(cmlStereo);
					if ((kStereo != Kekule.BondStereo.NONE) && result.setStereo)
						result.setStereo(kStereo);
				}
				else
				{
					/*
					var reader = Kekule.IO.CmlElementReaderFactory.getReader(elem);
					reader.setCoreNamespaceURI(this.getCoreNamespaceURI());
					if (reader)
						reader.readElement(elem, result);
					*/
					this.readChildElementDef(elem, result);
				}
			}
		}

		return result;
	},

	/**
	 * Read array information and create all bonds
	 * @returns {Array} Array of Kekule.Bond
	 * @private
	 */
	readArrayedBonds: function(molecule, elem, domHelper)
	{
		var arrayedAttribNames = ['id', 'bondID', /*'atomRefs',*/ 'atomRef1', 'atomRef2', 'order', 'bondStereo'];
		var attribNames = ['id', 'id', /*'atomRefs',*/ 'atomRefs2', 'atomRefs2', 'order', 'bondStereo'];
		var bondAttribs = [];

		// handle atomRefs attrib first, as it may concern to multiple child elems
		var svalues = Kekule.IO.CmlDomUtils.getMultipleCmlElemAttribute(elem, 'atomRefs', Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM, domHelper);
		if (svalues && svalues.length)
		{
			for (var i = 0, l = svalues.length; i < l; ++i)
			{
				var svalue = svalues[i];
				var values = Kekule.IO.CmlDomUtils.splitCmlArrayValue(svalue);;
				for (var j = 0, k = values.length; j < k; ++j)
				{
					if (!bondAttribs[j])
						bondAttribs[j] = {};
					if (bondAttribs[j]['atomRefs'] == undefined)
						bondAttribs[j]['atomRefs'] = [];
					(bondAttribs[j]['atomRefs']).push(values[j]);
				}
			}
		}

		for (var i = 0, l = arrayedAttribNames.length; i < l; ++i)
		{
			var svalue;
			var values = [];

			svalue = Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, arrayedAttribNames[i], Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM, domHelper);
			if (svalue)
				values = Kekule.IO.CmlDomUtils.splitCmlArrayValue(svalue);

			//console.log(arrayedAttribNames[i], svalue, values);

			for (var j = 0, k = values.length; j < k; ++j)
			{
				if (!bondAttribs[j])
					bondAttribs[j] = {};
				if (values[j])
				{
					/*
					if (arrayedAttribNames[i] == 'atomRefs')
					{
						if (bondAttribs[j]['atomRefs'] == undefined)
							bondAttribs[j]['atomRefs'] = [];
						(bondAttribs[j]['atomRefs']).push(values[j]);
					}
					else
					*/
					if ((arrayedAttribNames[i] == 'atomRef1') || (arrayedAttribNames[i] == 'atomRef2'))
					{
						if (bondAttribs[j]['atomRefs2'] == undefined)
							bondAttribs[j]['atomRefs2'] = [];
						if (arrayedAttribNames[i] == 'atomRef1')
							bondAttribs[j]['atomRefs2'][0] = values[j];
						else
							bondAttribs[j]['atomRefs2'][1] = values[j];
					}
					else
						bondAttribs[j][attribNames[i]] = values[j];
				}
			}
		}

		var result = [];
		for (var i = 0, l = bondAttribs.length; i < l; ++i)
		{
			var bond = this.createStructureConnectorOfAttribs(molecule, bondAttribs[i]);
			if (bond)
				result.push(bond);
		}

		return result;
	},

	/**
	 * Create connector (mainly bond) by JSON attribs object.
	 * @param {Kekule.StructureFragment} molecule
	 * @param {Hash} attribs
	 * @returns {Kekule.ChemStructureConnector}
	 * @private
	 */
	createStructureConnectorOfAttribs: function(molecule, attribs)
	{
		var result;
		var id, bondOrder;
		if (attribs.id)
		{
			id = attribs.id;
		}
		delete attribs['id'];
		if (attribs.order !== undefined)
		{
			bondOrder = Kekule.IO.CmlUtils.cmlBondOrderToKekule(attribs.order);
		}
		delete attribs['order'];
		result = new Kekule.Bond(id, null, bondOrder);

		if (attribs.bondStereo)
		{
			// TODO: haven't handle convertion and convertionValue attribs
			var kStereo = Kekule.IO.CmlUtils.cmlBondStereoToKekule(attribs.bondStereo);
			if ((kStereo != Kekule.BondStereo.NONE) && result.setStereo)
				result.setStereo(kStereo);
		}
		delete attribs['bondStereo'];

		// check referenced atoms
		for (var i = 0 , l = Kekule.IO.CML.ATOMS_REF_ATTRIBS.length; i < l; ++i)
		{
			var arName = Kekule.IO.CML.ATOMS_REF_ATTRIBS[i];
			if (attribs[arName] != undefined)
			{
				var atomIds = Kekule.IO.CmlDomUtils.splitCmlArrayValue(attribs[arName]);
				// as atoms may not added to molecule yet, we store the ids here to handle it later
				if (atomIds.length > 0)
					result[this._BOND_ATOM_REF_TEMP_FIELD] = atomIds;
				delete attribs[arName];
				break;
			}
		}
		// check referenced bonds
		for (var i = 0, l = Kekule.IO.CML.BONDS_REF_ATTRIBS.length; i < l; ++i)
		{
			var arName = Kekule.IO.CML.BONDS_REF_ATTRIBS[i];
			if (attribs[arName] != undefined)
			{
				var bondIds = Kekule.IO.CmlDomUtils.splitCmlArrayValue(attribs[arName]);
				// as bond are not added to molecule yet, we store the ids here to handle it later
				if (bondIds.length > 0)
					result[this._BOND_BOND_REF_TEMP_FIELD] = bondIds;
				delete attribs[arName];
				break;
			}
		}

		// TODO: ignore attribs: [convention, dictRef, ref]
		// rest of attribs will be set to info property of result
		for (var keyName in attribs)
		{
			if (attribs.hasOwnProperty(keyName) && (typeof(attribs[keyName]) == 'string'))
				result.setInfoValue(keyName, attribs[keyName]);
		}

		return result;
	},

	/**
	 * Read child formula element.
	 * @param {Object} molecule
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @private
	 */
	readFormula: function(molecule, elem, domHelper)
	{
		var reader = Kekule.IO.CmlElementReaderFactory.getReader(elem);
		if (reader)
		{
			//reader.setCoreNamespaceURI(this.getCoreNamespaceURI());
			this.copySettingsToChildHandler(reader);
			//reader.setDomHelper(domHelper);
			var formula = reader.readElement(elem, molecule);
			if (formula)
				molecule.setFormula(formula);
		}
	},
	/**
	 * Read formula info directly from formula attrib of <molecule>.
	 * For example <molecule formula="H 2 O 1" />.
	 * @param {Object} molecule
	 * @param {String} concise
	 */
	readFormulaAttrib: function(molecule, concise)
	{
		var reader = Kekule.IO.CmlElementReaderFactory.getReader('formula');
		if (reader)
		{
			var formula = reader.readConsice ? reader.readConsice(concise) : null;
			if (formula)
				molecule.setFormula(formula);
		}
	}
});

/**
 * CML <molecule> element writer for outputting {@link Kekule.Molecule} or {@link Kekule.CompositeMolecule}.
 * @class
 * @augments Kekule.IO.CmlElementWriter
 *
 * @property {Bool} allowCascadeGroup If true,sub group in molecule will be output in nested element,
 *   otherwise, all atoms in molecule will be put togather regardless of sub groups.
 */
Kekule.IO.CmlMoleculeWriter = Class.create(Kekule.IO.CmlElementWriter,
/** @lends Kekule.IO.CmlMoleculeWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlMoleculeWriter',
	/** @private */
	initProperties: function()
	{
		this.defineProp('allowCascadeGroup', {'dataType': DataType.BOOL});
	},
	/** @private */
	doCreateElem: function(obj, parentElem)
	{
		return this.createChildElem('molecule', parentElem);
	},
	/** @private */
	doWriteObject: function(obj, targetElem)
	{
		return this.writeMolecule(obj, targetElem);
	},
	writeMolecule: function(molecule, targetElem)
	{
		var result;
		this.writeMoleculeAttribs(molecule, targetElem);
		if (molecule instanceof Kekule.CompositeMolecule)
			result = this.writeCompositeMolecule(molecule, targetElem);
		else  // Kekule.Molecule
			result = this.writeMoleculeCore(molecule, targetElem);
		return result;
	},
	writeMoleculeAttribs: function(molecule, targetElem)
	{
		var attribs = [];
		if (molecule.getCharge())
			attribs.push({'key': 'formalCharge', 'value': molecule.getCharge()});

		var cmlInfoAttribs = ['title', 'dictRef', 'chirality', 'ref', 'spinMultiplicity', 'symmetryOriented', 'role'];
		for (var i = 0, l = cmlInfoAttribs.length; i < l; ++i)
		{
			var key = cmlInfoAttribs[i];
			var value = molecule.getInfoValue(key);
			if (!Kekule.ObjUtils.isUnset(value))
				attribs.push({'key': key, 'value': molecule.getInfoValue(key)});
		}
		// write to CML element
		for (var i = 0, l = attribs.length; i < l; ++i)
		{
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, attribs[i].key, attribs[i].value, this.getDomHelper());
		}
	},
	/**
	 * Write composite molecule (with sub molecules)
	 */
	writeCompositeMolecule: function(compositeMolecule, targetElem)
	{
		var subMolGroup = compositeMolecule.getSubMolecules();
		for (var i = 0, l = subMolGroup.getItemCount(); i < l; ++i)
		{
			var subItem = subMolGroup.getItemAt(i);
			var subMol = subItem.obj;
			//var subElem = this.createChildElem('molecule', targetElem);
			//this.writeObject(subMol, subElem);
			var subElem = this.writeObject(subMol, targetElem);
			if (subItem.amount)
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(subElem, 'amount', subItem.amount, this.getDomHelper());
		}
		return targetElem;
	},
	/**
	 * Write a pure molecule.
	 * @private
	 */
	writeMoleculeCore: function(molecule, targetElem)
	{
		//console.log('has F:', molecule.hasFormula, molecule instanceof ObjectEx);
		if (molecule.hasFormula())
		{
			var formula = molecule.getFormula();
			var formulaWriter = Kekule.IO.CmlElementWriterFactory.getWriter(formula);
			this.copySettingsToChildHandler(formulaWriter);
			formulaWriter.writeObject(formula, targetElem);
		}
		if (molecule.hasCtab())
		{
			this.writeCtab(molecule.getCtab(), molecule, targetElem);
		}
	},
	/**
	 * Write Ctab nodes and connectors to element.
	 * @param {Object} ctab
	 * @param {Object} elem
	 */
	writeCtab: function(ctab, molecule, elem)
	{
		// firstly we should make every nodes and connectors in ctab has a unique Id to be refed
		this.identifyObjsInCtab(ctab, molecule);
		// nodes
		var nodes = this.getAllowCascadeGroup()? ctab.getNodes(): ctab.getLeafNodes();
		if (nodes.length > 0)
		{
			var atomsElem = this.createChildElem('atomArray', elem);
			for (var i = 0, l = nodes.length; i < l; ++i)
				this.writeCtabNode(nodes[i], atomsElem);
		}
		// connectors
		var connectors = this.getAllowCascadeGroup()? ctab.getConnectors(): ctab.getAllChildConnectors();
		if (connectors.length > 0)
		{
			var bondsElem = this.createChildElem('bondArray', elem);
			for (var i = 0, l = connectors.length; i < l; ++i)
				this.writeCtabConnector(connectors[i], bondsElem);
		}
	},
	/** @private */
	writeCtabNode: function(node, elem)
	{
		var result = this.createChildElem('atom', elem);
		this.writeObjId(node, result);
		// symbol
		var elementType = Kekule.IO.CmlUtils.getNodeElementType(node);
		Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'elementType', elementType, this.getDomHelper());
		// mass number
		var massNumber = node.getMassNumber? node.getMassNumber(): null;
		if (massNumber)
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'isotope', massNumber, this.getDomHelper());
		// hydrongen count
		var hcount = node.getExplicitHydrogenCount? node.getExplicitHydrogenCount(): null;
		if (!Kekule.ObjUtils.isUnset(hcount))
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'hydrogenCount', hcount, this.getDomHelper());
		// charge
		var charge = node.getCharge? node.getCharge(): null;
		if (charge)
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'formalCharge', charge, this.getDomHelper());
		// TODO: radical was ignored, as CML do not have corresponding attribute
		// parity
		var parity = node.getParity? node.getParity(): null;
		if (parity)  // stereo center, and if stereo center is perceived, the whole molecule should be standardized
		{
			//var neighbors = node.linkedChemNodes();
			// TODO: how to handle atomRef4 attribute when there is a implicit H?
		}
		// title
		var title = node.getInfoValue? node.getInfoValue('title'): null;
		if (title)
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'title', title, this.getDomHelper());
		// role
		var role = node.getInfoValue? node.getInfoValue('role'): null;
		if (role)
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'role', role, this.getDomHelper());
		// coordinates
		if (node.hasCoord2D && node.hasCoord2D())
		{
			var coord2D = node.getAbsCoord2D();
			Kekule.ObjUtils.isUnset(coord2D.x)? null: Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'x2', coord2D.x, this.getDomHelper());
			Kekule.ObjUtils.isUnset(coord2D.y)? null: Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'y2', coord2D.y, this.getDomHelper());
		}
		if (node.hasCoord3D && node.hasCoord3D())
		{
			var coord3D = node.getAbsCoord3D();
			Kekule.ObjUtils.isUnset(coord3D.x)? null: Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'x3', coord3D.x, this.getDomHelper());
			Kekule.ObjUtils.isUnset(coord3D.y)? null: Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'y3', coord3D.y, this.getDomHelper());
			Kekule.ObjUtils.isUnset(coord3D.z)? null: Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'z3', coord3D.z, this.getDomHelper());
		}
		// some infoValues that need to be put to attribute
		var cmlAdditionalAttribs = ['occupancy', 'xFract', 'xyzFract', 'yFract', 'convention', 'dictRef', 'ref'];
		for (var i = 0, l = cmlAdditionalAttribs.length; i < l; ++i)
		{
			var key = cmlAdditionalAttribs[i];
			var value = node.getInfoValue(key);
			if (value)
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, key, value, this.getDomHelper());
		}
		// write addiitioanl meta
		this.writeObjAdditionalInfo(node, result);
		return result;
	},
	/** @private */
	writeCtabConnector: function(connector, elem)
	{
		var invertAtoms = false;
		var result = this.createChildElem('bond', elem);
		this.writeObjId(connector, result);
		// order
		var order = connector.getBondOrder? connector.getBondOrder(): null;
		if (order)
		{
			var cmlOrder = Kekule.IO.CmlUtils.kekuleBondOrderToCml(order);
			if (cmlOrder)
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'order', cmlOrder, this.getDomHelper());
		}
		// stereo
		var stereo = connector.getStereo? connector.getStereo(): null;
		if (stereo)
		{
			var cmlStereo = Kekule.IO.CmlUtils.kekuleBondStereoToCml(stereo);
			if (cmlStereo.value)
			{
				//Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, 'bondStereo', cmlStereo.value, this.getDomHelper());
				// stereo should be put in child element according to CML standard
				var stereoElem = this.createChildElem('bondStereo', result);
				Kekule.DomUtils.setElementText(stereoElem, cmlStereo.value);
				if (cmlStereo.invert)
					invertAtoms = true;
			}
			// NOTE later we will check cmlStereo.invert
		}
		// ref atom / bond ids
		var refAtomIds = [];
		var refBondIds = [];
		for (var i = 0, l = connector.getConnectedObjCount(); i < l; ++i)
		{
			var obj = connector.getConnectedObjAt(i);
			if (obj.getId && obj.getId())
			{
				var id = obj.getId();
				if (obj instanceof Kekule.ChemStructureNode)
				{
					if (invertAtoms)
						refAtomIds.unshift(id);
					else
						refAtomIds.push(id);
				}
				else if (obj instanceof Kekule.ChemStructureConnector)
					refBondIds.push(id);
			}
		}
		var length = refAtomIds.length;
		if (length)
		{
			var attribName =
				(length == 1)? 'atomRefs':
				((length >= 2) && (length <= 4))? 'atomRefs' + length: 'atomRefs';
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, attribName,
				Kekule.IO.CmlDomUtils.mergeToCmlArrayValue(refAtomIds), this.getDomHelper());
		}
		var length = refBondIds.length;
		if (length)
		{
			//var attribName = (length == 1)? 'bondRefs': 'bondRefs';
			var attribName = 'bondRefs';
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, attribName,
				Kekule.IO.CmlDomUtils.mergeToCmlArrayValue(refBondIds), this.getDomHelper());
		}

		// some infoValues that need to be put to attribute
		var cmlAdditionalAttribs = ['convention', 'dictRef', 'ref'];
		for (var i = 0, l = cmlAdditionalAttribs.length; i < l; ++i)
		{
			var key = cmlAdditionalAttribs[i];
			var value = connector.getInfoValue(key);
			if (value)
				Kekule.IO.CmlDomUtils.setCmlElemAttribute(result, key, value, this.getDomHelper());
		}

		this.writeObjAdditionalInfo(connector, result);
		return result;
	},
	/**
	 * Set id of every objects in ctab.
	 * @private
	 */
	identifyObjsInCtab: function(ctab, molecule)
	{
		for (var i = 0, l = ctab.getNodeCount(); i < l; ++i)
		{
			var node = ctab.getNodeAt(i);
			if (!node.getId())
				this.autoIdentifyForObj(node, molecule);
			if (ctab.isSubFragment(node) && node.hasCtab && node.hasCtab())
				this.identifyObjsInCtab(node.getCtab(), molecule);
		}
		for (var i = 0, l = ctab.getConnectorCount(); i < l; ++i)
		{
			var connector = ctab.getConnectorAt(i);
			if (!connector.getId())
				this.autoIdentifyForObj(connector, molecule);
		}
	},
	/**
	 * Gives obj an auto id if its id is not explicit setted.
	 * @private
	 */
	autoIdentifyForObj: function(obj, molecule)
	{
		if (!obj.getId())
		{
			if (obj.getOwner && obj.getOwner() && obj.getOwner().getAutoId)  // use owner's auto id function
				obj.setId(obj.getOwner().getAutoId(obj));
			else
				obj.setId(this.getAutoIdForObj(obj, molecule));
		}
	},
	/**
	 * A very simple way to generate UID for atoms and bonds in molecule.
	 * @returns {String}
	 * @private
	 */
	getAutoIdForObj: function(obj, molecule)
	{
		var prefix = obj.getAutoIdPrefix? obj.getAutoIdPrefix(): 'obj';
		if (!Kekule.IO.CmlMoleculeWriter._UID_SEED)
			Kekule.IO.CmlMoleculeWriter._UID_SEED = (new Date()).getTime();
		++Kekule.IO.CmlMoleculeWriter._UID_SEED;
		var s = Kekule.IO.CmlMoleculeWriter._UID_SEED.toString();  //.substr(7);
		var r = '' + prefix + s;
		return r;
	}
});
/**
 * A Class var to store seed to generate UIDs.
 * @private
 */
Kekule.IO.CmlMoleculeWriter._UID_SEED = null;

/**
 * Reader to read <reactant>, <product> and <substance> element inside <reaction>.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlReactionReagentReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlReactionReagentReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlReactionReagentReader',
	/** @private */
	doReadElement: function(elem, parentObj)
	{
		return this.readReagent(elem, this.getDomHelper());
	},
	/** @private */
	readReagent: function(elem, domHelper)
	{
		var map = null;
		// read sub <molecule> element first
		var molElems = Kekule.DomUtils.getDirectChildElems(elem, null, 'molecule', this.getCoreNamespaceURI());
		if (molElems && molElems.length)  // has molecule
		{
			var molElem = molElems[molElems.length - 1];
			var reader = Kekule.IO.CmlElementReaderFactory.getReader(molElem);
			this.copySettingsToChildHandler(reader);
			var molecule = reader.readElement(molElem, null);
			if (molecule)
			{
				map = {};
				Kekule.RoleMapUtils.setItem(map, molecule);
				// get attributes
				this.readReagentAttribs(map, elem, domHelper);
			}
		}
		else // no explicit molecule, bypass elem
			;
		return map;
	},
	/**
	 * Read attributes of elem and fetch them to current map.
	 * @private
	 */
	readReagentAttribs: function(map, elem, domHelper)
	{
		var attribs = Kekule.IO.CmlDomUtils.fetchCmlElemAttributeValuesToJson(elem,
			Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, true, domHelper);
		var keys = Kekule.ObjUtils.getOwnedFieldNames(attribs);
		for (var i = 0, l = keys.length; i < l; ++i)
		{
			var key = keys[i];
			var value = attribs[key];
			switch (key)
			{
				case 'role': map.role = Kekule.IO.CmlUtils.reagentRoleToKekule(value); break;
				case 'amount':
				case 'count':  map.amount = parseFloat(value); break;
				default: map[key] = value;
			}
		}
		return map;
	}
});

/**
 * Reader to read <reaction> element.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlReactionReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlReactionReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlReactionReader',
	/** @private */
	doReadElement: function(elem, parentObj)
	{
		return this.readReaction(elem, this.getDomHelper());
	},
	/**
	 * Read <reaction> element and create a {@link Kekule.Reaction} object.
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @returns {Kekule.Reaction}
	 * @private
	 */
	readReaction: function(elem, domHelper)
	{
		var result = new Kekule.Reaction();
		var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
		for (var i = 0, l = children.length; i < l; ++i)
		{
			if (this.isComponentListElem(children[i]))
				this.readComponentList(result, children[i], domHelper);
			else  // other normal children
				this.readChildElement(children[i], result);
		}
		// reaction element attribs
		var attribs = Kekule.IO.CmlDomUtils.fetchCmlElemAttributeValuesToJson(elem,
			Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, null, this.getDomHelper());
		var attribKeys = Kekule.ObjUtils.getOwnedFieldNames(attribs);
		for (var i = 0, l = attribKeys.length; i < l; ++i)
		{
			var key = attribKeys[i];
			var value = attribs[key];
			switch (key)
			{
				case 'name': result.setName(value); break;
				case 'title': result.setTitle(value); break;
				case 'type': result.setReactionType(value); break;
				case 'yield': result.setYield(parseFloat(value)); break;
				default: // ignored: [dictRef, convention, format, ref, role, type, state, yield]
					result.setInfoValue(key, value);
			}
		}
		return result;
	},
	/**
	 * Read <reactantList>/<productList>/<substanceList>/<conditionList>
	 * @param {Kekule.Reaction} reaction
	 * @param {Object} elem
	 * @param {Object} domHelper
	 * @private
	 */
	readComponentList: function(reaction, elem, domHelper)
	{
		var compName = this.getListElemComponentName(elem);
		var compArray = reaction.getComponentArray(compName, true);
		if (compArray)
		{
			// get direct children of elem and analysis
			var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
			for (var i = 0, l = children.length; i < l; ++i)
			{
				// childObj is generally objects returned by CmlReactionReagentReader, but condition list be contains objects directly
				var childObj = this.readChildElement(children[i], /*reaction*/compArray);
				if (childObj)
				{
					if ((compName == Kekule.ReactionComponent.CONDITION) && (childObj instanceof ObjectEx)) // not returned by CmlReactionReagentReader
						reaction.appendItem(compName, childObj);
					else
						reaction.appendMap(compName, childObj);
				}
			}
		}
	},
	/**
	 * Check if element is <reactantList>/<productList>/<substanceList>/<conditionList>
	 * @param {Object} elem
	 * @returns {Bool}
	 * @private
	 */
	isComponentListElem: function(elem)
	{
		return ['reactantList', 'productList', 'substanceList', 'conditionList'].indexOf(Kekule.DomUtils.getLocalName(elem)) >= 0;
	},
	/**
	 * Find out the list is corresponding to which component in reaction.
	 * @param {Object} elem
	 * @returns {String}
	 * @private
	 */
	getListElemComponentName: function(elem)
	{
		switch (Kekule.DomUtils.getLocalName(elem))
		{
			case 'reactantList': return Kekule.ReactionComponent.REACTANT;
			case 'productList': return Kekule.ReactionComponent.PRODUCT;
			case 'conditionList': return Kekule.ReactionComponent.CONDITION;
			//case 'substanceList':
			default:
				return Kekule.ReactionComponent.SUBSTANCE;
		}
	}
});

/**
 * Reader to write {@link Kekule.Reaction} to CML <reaction> element.
 * @class
 * @augments Kekule.IO.CmlElementWriter
 */
Kekule.IO.CmlReactionWriter = Class.create(Kekule.IO.CmlElementWriter,
/** @lends Kekule.IO.CmlReactionWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlReactionWriter',
	/** @private */
	doCreateElem: function(obj, parentElem)
	{
		return this.createChildElem('reaction', parentElem);
	},
	/** @private */
	doWriteObject: function(obj, targetElem)
	{
		return this.writeReaction(obj, targetElem);
	},
	/** @private */
	writeReaction: function(reaction, targetElem)
	{
		var reactionCompNames = [
			Kekule.ReactionComponent.REACTANT,
			Kekule.ReactionComponent.PRODUCT,
			Kekule.ReactionComponent.SUBSTANCE,
			Kekule.ReactionComponent.CONDITION
		];
		for (var i = 0, l = reactionCompNames.length; i < l; ++i)
			this.writeCompList(reaction, reactionCompNames[i], targetElem);
		this.writeReactionAttribs(reaction, targetElem);
		return targetElem;
	},
	/**
	 * Write attribs of reaction directly to targetElem's attributes
	 * @private
	 */
	writeReactionAttribs: function(reaction, targetElem)
	{
		var attribs = [];
		reaction.getName()? attribs.push({'key': 'name', 'value': reaction.getName()}): null;
		reaction.getTitle()? attribs.push({'key': 'title', 'value': reaction.getTitle()}): null;
		reaction.getReactionType()? attribs.push({'key': 'type', 'value': reaction.getReactionType()}): null;
		reaction.getYield()? attribs.push({'key': 'yield', 'value': reaction.getYield()}): null;

		var infoAttribs = ['dictRef', 'convention', 'format', 'ref', 'role', 'type',
			'state', 'yield', 'atomMap', 'electronMap'];
		for (var i = 0, l = infoAttribs.length; i < l; ++i)
		{
			if (!Kekule.ObjUtils.isUnset(reaction.getInfoValue(infoAttribs[i])))
				attribs.push({'key': infoAttribs[i], 'value': reaction.getInfoValue(infoAttribs[i])});
		}

		for (var i = 0, l = attribs.length; i < l; ++i)
		{
			Kekule.IO.CmlDomUtils.setCmlElemAttribute(targetElem, attribs[i].key, attribs[i].value, this.getDomHelper());
		}
	},
	/**
	 * Write reactant, product, substance or condition list to targetElem.
	 * @private
	 */
	writeCompList: function(reaction, compName, targetElem)
	{
		if (reaction.getComponentItemCount(compName) <= 0)  // no component inside, skip
			return null;
		else // really has component inside
		{
			var cmlTagName = this.getReactionComponentCmlName(compName);
			// create list element
			var listElem = this.createChildElem(cmlTagName + 'List', targetElem);
			// write each component
			for (var i = 0, l = reaction.getComponentItemCount(compName); i < l; ++i)
			{
				var compMap = reaction.getMapAt(compName, i);
				var obj = reaction.getMapItemAt(compName, i);
				if (compMap && obj)
				{
					var elem;
					// conditionList do not need child <condition> element,
					// while reactant, product and substance do need them
					if (compName != Kekule.ReactionComponent.CONDITION)
					{
						elem = this.createChildElem(cmlTagName, listElem);
						// and need to set amount / role attribs as well
						if (!Kekule.ObjUtils.isUnset(compMap.amount))
							Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, 'amount', compMap.amount, this.getDomHelper());
						if (compMap.role)
							Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, 'role', Kekule.IO.CmlUtils.kekuleReagentRoleToCml(compMap.role), this.getDomHelper());
					}
					else  // condition
						elem = listElem;
					var writer = Kekule.IO.CmlElementWriterFactory.getWriter(obj);
					if (writer)
					{
						this.copySettingsToChildHandler(writer);
						writer.writeObject(obj, elem);
					}
				}
			}
		}
	},

	/**
	 * Find out the CML tagName corresponding to certain component in reaction.
	 * @param {String} compName
	 * @returns {String}
	 * @private
	 */
	getReactionComponentCmlName: function(compName)
	{
		switch (compName)
		{
			case Kekule.ReactionComponent.REACTANT: return 'reactant';
			case Kekule.ReactionComponent.PRODUCT: return 'product';
			case Kekule.ReactionComponent.CONDITION: return 'condition';
			//case Kekule.ReactionComponent.SUBSTANCE:
			default:
				return 'substance';
		}
	}
});

/**
 * Reader to read list (<list>/<moleculeList>/<reactionList>...) element of CML.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlListReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlListReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlListReader',
	/** @private */
	doReadElement: function(elem, parentObj)
	{
		var result = this.createList(elem);
		result = this.readList(result, elem, this.getDomHelper());
		return result;
	},
	/** Create a suitable list for elem */
	createList: function(elem)
	{
		switch (Kekule.DomUtils.getLocalName(elem))
		{
			case 'moleculeList': return new Kekule.MoleculeList(); break;
			case 'reactionList': return new Kekule.ReactionList(); break;
			default:
				return new Kekule.ChemObjList();
		}
	},
	/** @private */
	readList: function(list, elem, domHelper)
	{
		var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
		if (children)
		{
			for (var i = 0, l = children.length; i < l; ++i)
			{
				var child = children[i];
				var reader = Kekule.IO.CmlElementReaderFactory.getReader(child);
				if (reader)
				{
					this.copySettingsToChildHandler(reader);
					var obj = reader.readElement(child, null);
					if (obj)
						list.append(obj);
				}
			}
		}
		return list;
	}
});

/**
 * Reader to write {@link Kekule.MoleculeList}, {@link Kekule.ReactionList} and so on to CML <list> series elements.
 * @class
 * @augments Kekule.IO.CmlElementWriter
 */
Kekule.IO.CmlListWriter = Class.create(Kekule.IO.CmlElementWriter,
/** @lends Kekule.IO.CmlListWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlListWriter',
	/** @private */
	doCreateElem: function(obj, parentElem)
	{
		var tagName;
		switch (DataType.getType(obj))
		{
			case 'Kekule.MoleculeList': tagName = 'moleculeList'; break;
			case 'Kekule.ReactionList': tagName = 'reactionList'; break;
			default: tagName = 'list';  // ChemObjList
		}
		return this.createChildElem(tagName, parentElem);
	},
	/** @private */
	doWriteObject: function(obj, targetElem)
	{
		var list = Kekule.ChemStructureUtils.getChildStructureObjs(obj, false);
		return this.writeList(list, targetElem);
	},
	/**
	 * Write list items to listElem.
	 * @private
	 */
	writeList: function(list, listElem)
	{
		for (var i = 0, l = list.length; i < l; ++i)
		{
			var item = list[i];
			if (item)
			{
				var writer = Kekule.IO.CmlElementWriterFactory.getWriter(item);
				if (writer)
				{
					this.copySettingsToChildHandler(writer);
					writer.writeObject(item, listElem);
				}
			}
		}
		return listElem;
	}
});

/**
 * Reader to read <cml> element.
 * @class
 * @augments Kekule.IO.CmlElementReader
 */
Kekule.IO.CmlRootReader = Class.create(Kekule.IO.CmlElementReader,
/** @lends Kekule.IO.CmlRootReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlRootReader',
	/** @private */
	doReadElement: function(elem, parentObj)
	{
		this.analysisElem(elem);
		// iterate through elem and direct children and find first readable one
		var reader;
		if (Kekule.DomUtils.getLocalName(elem) != 'cml')
			reader = this.getReader(elem);
		if (reader)
			return reader.readElement(elem, null);
		else
		{
			var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
			if (children)
			{
				for (var i = 0, l = children.length; i < l; ++i)
				{
					reader = this.getReader(children[i]);
					if (reader)
					{
						return reader.readElement(children[i], null);
						break;
					}
				}
			}
		}
		return null;
	},
	/**
	 * Get a suitable reader for elem.
	 * @private
	 */
	getReader: function(elem)
	{
		var reader = Kekule.IO.CmlElementReaderFactory.getReader(elem);
		if (reader)
		{
			this.copySettingsToChildHandler(reader);
			return reader;
		}
		return null;
	},
	/**
	 * Init domHelper, analysis root element and find the suitable namespace.
	 * @private
	 */
	analysisElem: function(rootElem)
	{
		var domHelper = this.getDomHelper();
		domHelper.setForceAnalysisDoc(true);  // force analysis doc even in Gecko and Webkit to get namespaces info
		this.getDomHelper().setRootElement(rootElem);
		// check namespaces info, find out which is for CML CORE
		var namespaces = domHelper.getNamespaces();
		var legalCoreUri = null;
		for (var i = 0, l = namespaces.length; i < l; ++i)
		{
			var uri = namespaces[i].namespaceURI;
			if (Kekule.IO.CML.LEGAL_CORE_NAMESPACE_URIS.indexOf(uri) >= 0)  // find
			{
				legalCoreUri = uri;
				break;
			}
		}
		this.setCoreNamespaceURI(legalCoreUri || null);
	}
});

/**
 * Reader to write {@link Kekule.ChemDocument} to CML <cml> element.
 * @class
 * @augments Kekule.IO.CmlElementWriter
 */
Kekule.IO.CmlRootWriter = Class.create(Kekule.IO.CmlElementWriter,
/** @lends Kekule.IO.CmlRootWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlRootWriter',
	/** @private */
	/*
	doGetCoreNamespaceURI: function()
	{
		var result = this.getPropStoreFieldValue('coreNamespaceURI');
		if (!result)  // return a default one
			result = Kekule.IO.CML.CML3_SCHEMA_NAMESPACE_URI;
		return result;
	},
	*/
	/**
	 * Get a suitable writer for obj.
	 * @private
	 */
	getWriter: function(obj)
	{
		var writer = Kekule.IO.CmlElementWriterFactory.getWriter(obj);
		if (writer)
		{
			this.copySettingsToChildHandler(writer);
			return writer;
		}
		return null;
	},
	/** @private */
	/*
	doCreateElem: function(obj, parentElem)
	{
		if (parentElem && (Kekule.DomUtils.getLocalName(parentElem) == 'cml'))  // already a cml root
			return null;
		else
			return this.createChildElem('cml', parentElem);
	},
	*/
	/** @private */
	/*
	doWriteObject: function(obj, targetElem)
	{
		var writer = this.getWriter(obj);
		if (writer)
			return writer.writeObject(obj, targetElem);
		else
			return null;
	},
	*/
	/** @private **/
	writeObject: function(/*$super,*/ obj, parentElem, doc)
	{
		// initialize domHelper
		var domHelper = this.getDomHelper();
		domHelper.setForceAnalysisDoc(true);
		this.getDomHelper().setRootElement(parentElem);

		var o;
		/*
		if (obj.docObj)  // obj is a chem space
			o = obj.docObj;
		*/
		if (obj instanceof Kekule.ChemSpace)
			o = obj.getRoot();
		else
			o = obj;

		var writer = this.getWriter(o);
		if (writer)
			return writer.writeObject(o, parentElem);
		else
			return null;
		//return $super(obj, parentElem, doc);
	}
});

/**
 * Reader for CML document.
 * Use CmlReader.readData() can retrieve a suitable Kekule.ChemObject.
 * Data fetch in can be a string, reader will parse it to XML automatically;
 *   otherwise it should be a XML document or XML element.
 * @class
 * @augments Kekule.IO.ChemDataReader
 */
Kekule.IO.CmlReader = Class.create(Kekule.IO.ChemDataReader,
/** @lends Kekule.IO.CmlReader# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlReader',
	/** @private */
	doReadData: function(data, dataType, format)
	{
		var rootElem;
		if (dataType != Kekule.IO.ChemDataType.DOM) // not a dom doc, parse it first
		{
			var doc = XmlUtility.parse(data);
			rootElem = doc.documentElement;
		}
		else
		{
			if (data.documentElement)  // is document
				rootElem = data.documentElement;
			else
				rootElem = data;
		}
		var reader = Kekule.IO.CmlElementReaderFactory.getReader('cml');
		return reader.readElement(rootElem, null);
	}
});

/**
 * Writer for CML document.
 * Use CmlWriter.writeData() to write a Kekule.ChemObject to suitable CML element.
 * @class
 * @augments Kekule.IO.ChemDataReader
 */
Kekule.IO.CmlWriter = Class.create(Kekule.IO.ChemDataWriter,
/** @lends Kekule.IO.CmlWriter# */
{
	/** @private */
	CLASS_NAME: 'Kekule.IO.CmlWriter',
	/** @private */
	initialize: function($super, options)
	{
		$super(options);
		var op = options || {};
		this.setPrettyPrint(Kekule.ObjUtils.isUnset(op.prettyPrint)? Kekule.globalOptions.IO.cml.prettyPrint: op.prettyPrint);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('prettyPrint', {'dataType': DataType.BOOL, 'defaultValue': true});
	},
	/** @private */
	doWriteData: function(obj, dataType, format)
	{
		var nsUri = Kekule.IO.CML.CML3_SCHEMA_NAMESPACE_URI;
		// create a new XML document
		var xmlDoc = XmlUtility.newDocument('cml', nsUri);
		var writer = Kekule.IO.CmlElementWriterFactory.getWriter('Kekule.ChemDocument');
		if (writer)
		{
			writer.setCoreNamespaceURI(nsUri);
			//writer.setCoreNamespaceURI('');
			var result = writer.writeObject(obj, xmlDoc.documentElement);
			if (dataType == Kekule.IO.ChemDataType.TEXT) // convert DOM to text
			{
				result = XmlUtility.serializeNode(xmlDoc.documentElement, {'prettyPrint': this.getPrettyPrint()});
			}
			return result;
		}
		else
		{
			Kekule.error(/*Kekule.ErrorMsg.UNABLE_TO_OUTPUT_AS_CML*/Kekule.$L('ErrorMsg.UNABLE_TO_OUTPUT_AS_CML').format(DataType.getType(obj)));
			return null;
		}
	}
});


(function ()
{
	// extends mime type consts
	Kekule.IO.MimeType.CML = 'chemical/x-cml';

	// register default CML element readers and writers
	var RF = Kekule.IO.CmlElementReaderFactory;
	RF.register('name', Kekule.IO.CmlNameReader);
	RF.register('scalar', Kekule.IO.CmlScalarReader);
	RF.register('metaData', Kekule.IO.CmlMetaDataReader);
	RF.register('metaDataList', Kekule.IO.CmlMetaDataListReader);
	RF.register('formula', Kekule.IO.CmlFormulaReader);
	RF.register('molecule', Kekule.IO.CmlMoleculeReader);
	RF.register(['reactant', 'product', 'substance'], Kekule.IO.CmlReactionReagentReader);
	RF.register('reaction', Kekule.IO.CmlReactionReader);
	RF.register(['list', 'moleculeList', 'reactionList'], Kekule.IO.CmlListReader);
	RF.register('cml', Kekule.IO.CmlRootReader);

	var WF = Kekule.IO.CmlElementWriterFactory;
	WF.register('Kekule.Scalar', Kekule.IO.CmlScalarWriter);
	WF.register('Kekule.MolecularFormula', Kekule.IO.CmlFormulaWriter);
	WF.register(['Kekule.ChemStructureFragment', 'Kekule.Molecule', 'Kekule.CompositeMolecule'], Kekule.IO.CmlMoleculeWriter);
	WF.register('Kekule.Reaction', Kekule.IO.CmlReactionWriter);
	WF.register(['Kekule.ChemObjList', 'Kekule.ChemStructureObjectGroup', 'Kekule.ChemSpaceElement', 'Kekule.ChemSpace'], Kekule.IO.CmlListWriter);
	WF.register('Kekule.ChemDocument', Kekule.IO.CmlRootWriter);

	// register chem data formats
	Kekule.IO.DataFormatsManager.register('cml', Kekule.IO.MimeType.CML, 'cml', Kekule.IO.ChemDataType.TEXT, 'Chemical Markup Language');

	// register ChemData reader and writer
	/*
	Kekule.IO.ChemDataReaderManager.register('cml', Kekule.IO.CmlReader, {
		'title': 'Chemical Markup Language',
		'mimeType': 'chemical/x-cml',
		'fileExt': 'cml'
	});
	Kekule.IO.ChemDataWriterManager.register('cml', Kekule.IO.CmlWriter,
		['Kekule.Scalar', 'Kekule.ChemStructureFragment', 'Kekule.Reaction',
		 'Kekule.ChemObjList', 'Kekule.ChemDocument'],
		{
			'title': 'Chemical Markup Language',
			'mimeType': 'chemical/x-cml',
			'fileExt': 'cml'
		});
	*/
	var cmdFmtId = Kekule.IO.DataFormatsManager.findFormatId(Kekule.IO.MimeType.CML);

	Kekule.IO.ChemDataReaderManager.register('cml', Kekule.IO.CmlReader, [cmdFmtId]);
	Kekule.IO.ChemDataWriterManager.register('cml', Kekule.IO.CmlWriter,
		[Kekule.Scalar, Kekule.StructureFragment, Kekule.Reaction,
			Kekule.ChemObjList, Kekule.ChemSpace],
		[cmdFmtId]);
})();