/*******************************************************************************
 * Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is made available under the terms of the
 * Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Exadel, Inc. and Red Hat, Inc. - initial API and implementation
 ******************************************************************************/ 
package org.jboss.tools.common.model.util;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.jboss.tools.common.meta.XAttribute;
import org.jboss.tools.common.meta.XChild;
import org.jboss.tools.common.meta.XModelEntity;
import org.jboss.tools.common.model.ServiceDialog;
import org.jboss.tools.common.model.XModel;
import org.jboss.tools.common.model.XModelConstants;
import org.jboss.tools.common.model.XModelException;
import org.jboss.tools.common.model.XModelObject;
import org.jboss.tools.common.model.XModelObjectConstants;
import org.jboss.tools.common.model.engines.impl.EnginesLoader;
import org.jboss.tools.common.model.filesystems.XFileObject;
import org.jboss.tools.common.model.filesystems.impl.FileAnyImpl;
import org.jboss.tools.common.model.impl.XModelImpl;
import org.jboss.tools.common.model.impl.XModelObjectImpl;
import org.jboss.tools.common.model.loaders.XObjectLoader;
import org.jboss.tools.common.model.plugin.ModelMessages;
import org.jboss.tools.common.model.plugin.ModelPlugin;
import org.jboss.tools.common.util.FileUtil;
import org.jboss.tools.common.xml.XMLUtilities;

public class XModelObjectLoaderUtil {
	public static String ENT_ANY_ELEMENT = "AnyElement"; //$NON-NLS-1$
	public static String ATTR_ID_NAME = "_id_"; //$NON-NLS-1$
    private Hashtable<String,String> singular = null;
    private boolean saveentity = true;
    private String namespace = null;
    private NamespaceMapping namespaceMapping = null;
    
    protected String error = null;

    public XModelObjectLoaderUtil() {}

    public void setup(Hashtable<String,String> singular, boolean saveentity) {
        this.singular = singular;
        this.saveentity = saveentity;
        error = null;
    }
    
    public String getError() {
    	return error;
    }
    
    public void setNamespace(String namespace) {
    	this.namespace = namespace;
    }
    
    public void setNamespaceMapping(NamespaceMapping nm) {
    	namespaceMapping = nm;
    }
    
    public void setSaveEntity(boolean b) {
    	saveentity = b;
    }

    public void load(Element element, XModelObject o) {
    	if(ENT_ANY_ELEMENT.equals(o.getModelEntity().getName())) {
    		loadAnyElement(element, o);
    	} else {
			loadAttributes(element, o);
			loadChildren(element, o);
			
			if(error == null) {
				Set<String> allowed = getAllowedChildren(o.getModelEntity());
				if(allowed != null && hasUnallowedChildren(element, allowed));
			}
			if(error == null) {
				Set<String> allowed = getAllowedAttributes(o.getModelEntity());
				if(allowed != null && hasUnallowedAttributes(element, allowed));
			}
			
    	}
    }
    
    static Map<XModelEntity, Set<String>> allowedChildren = new HashMap<XModelEntity, Set<String>>();
    
    protected Set<String> getAllowedChildren(XModelEntity entity) {
    	if(entity.getChild(ENT_ANY_ELEMENT) != null) return null;
    	Set<String> x = allowedChildren.get(entity);
    	if(x != null) return x;
    	Set<String> children = new HashSet<String>();
    	XAttribute[] as = entity.getAttributes();
    	for (int i = 0; i < as.length; i++) {
    		String xml = as[i].getXMLName();
    		if(xml == null) continue;
    		int s = xml.indexOf('|');
    		if(s > 0) {
    			StringTokenizer st = new StringTokenizer(xml, "|"); //$NON-NLS-1$
    			while(st.hasMoreTokens()) {
    				String dxml = st.nextToken();
        			int k = dxml.indexOf('.');
        			if(k >= 0) children.add(dxml.substring(0, k));
    			}
    		} else {    		
    			int k = xml.indexOf('.');
    			if(k < 0) continue;
    			children.add(xml.substring(0, k));
    		}
    	}
    	XChild[] cs = entity.getChildren();
    	for (int i = 0; i < cs.length; i++) {
    		XModelEntity c = entity.getMetaModel().getEntity(cs[i].getName());
    		if(c == null) continue;
    		String xml = c.getXMLSubPath();
    		if(xml == null || xml.length() == 0) {
    			if(cs[i].isRequired()) {
    				Set<String> a1 = getAllowedChildren(c);
    				if(a1 == null) return null;
    				children.addAll(a1);
    			}
    		}
    		int k = xml.indexOf('.');
    		if(k >= 0) xml = xml.substring(0, k);
    		children.add(xml);
    	}
    	if(children != null) allowedChildren.put(entity, children);
    	return children;
    }

    protected Set<String> getAllowedAttributes(XModelEntity entity) {
    	if(entity.getChild(ENT_ANY_ELEMENT) != null) return null;
    	Set<String> attributes = new HashSet<String>();
    	if(saveentity) {
    		attributes.add(XModelConstants.XMODEL_ENTITY_ATTR);
    		attributes.add(XModelConstants.XMODEL_ENTITY_ATTR_OLD);
    	}
    	XAttribute[] as = entity.getAttributes();
    	for (int i = 0; i < as.length; i++) {
    		String xml = as[i].getXMLName();
    		if(xml == null) continue;
    		int s = xml.indexOf('|');
    		if(s > 0) {
    			StringTokenizer st = new StringTokenizer(xml, "|"); //$NON-NLS-1$
    			while(st.hasMoreTokens()) {
    				String dxml = st.nextToken();
        			int k = dxml.indexOf('.');
        			if(k < 0) attributes.add(dxml);
    			}
    		} else {
    			int k = xml.indexOf('.');
    			if(k >= 0) continue;
    			attributes.add(xml);
    		}
    	}
    	return attributes;
    }

    private boolean hasUnallowedChildren(Element element, Set<String> allowed) {
    	NodeList nl = element.getChildNodes();
    	for (int i = 0; i < nl.getLength(); i++) {
    		Node n = nl.item(i);
    		if(n.getNodeType() == Node.ELEMENT_NODE) {
    			String name = n.getNodeName();
       	    	if(namespaceMapping != null) { 
       	    		name = namespaceMapping.convertToDefault(name);
       	    	}
    			if(allowed.contains(name)) continue;
    			error = MessageFormat
						.format(
								"Editor model does not support child element {0} of {1}:0:0",
								name, element.getNodeName());
    			return true;
    		}
    	}
    	return false;
    }

    private boolean hasUnallowedAttributes(Element element, Set<String> allowed) {
    	NamedNodeMap nl = element.getAttributes();
    	for (int i = 0; i < nl.getLength(); i++) {
    		Node n = nl.item(i);
    		if(n.getNodeType() == Node.ATTRIBUTE_NODE) {
    			String name = n.getNodeName();
    			if(allowed.contains(name)) continue;
    			if(name.startsWith("xmlns")) continue; //$NON-NLS-1$
    			error = MessageFormat
						.format(
								"Editor model does not support attribute {0} of {1}:0:0",
								name, element.getNodeName());;
    			return true;
    		}
    	}
    	return false;
    }

    public void loadAttributes(Element element, XModelObject o) {
        XModelEntity entity = o.getModelEntity();
        XAttribute[] as = entity.getAttributes();
        for (int i = 0; i < as.length; i++) {
            String n = as[i].getName();
            if("attributes".equals(n) && "true".equals(as[i].getProperty("any"))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            	loadAnyAtributes(element, o);
            	continue;
            }
            String xmlname = as[i].getXMLName();
            if (xmlname == null || xmlname.length() == 0) continue;
            String value = getAttribute(element, xmlname, as[i]);
            if (value != null) o.setAttributeValue(n, value);
            String commentName = getAttributeCommentName(xmlname);
            if(commentName != null) {
            	String fn = xmlname.substring(0, xmlname.indexOf('.'));
            	Element child = XMLUtilities.getFirstChild(element, fn);
            	if(child != null) {
            		String comment = getComment(child);
            		if(comment != null && comment.length() > 0) {
            			o.set(commentName, comment);
            		}
            	}
            }
        }
        String s = getFinalComment(element);
        if(s != null && s.length() > 0) o.set("#final-comment", s); //$NON-NLS-1$
    }
    
    private String applyNamespaceToAttribute(String xmlname) {
    	if(namespaceMapping != null) { 
    		xmlname = namespaceMapping.convertToActual(xmlname);
    	}
    	if(namespace == null || namespace.length() == 0) return xmlname;
    	if(xmlname.indexOf(':') > 0 || xmlname.indexOf('.') >= 0 || xmlname.indexOf('#') >= 0) return xmlname;
    	if(xmlname.equals("xmlns")) { //$NON-NLS-1$
    		return xmlname + ":" + namespace; //$NON-NLS-1$
    	}
    	return xmlname;
    }
    
    public final String applyNamespaceToTag(String xmlname) {
    	if(namespaceMapping != null) { 
    		xmlname = namespaceMapping.convertToActual(xmlname);
    	}
    	if(namespace != null && namespace.length() > 0) {
    		return namespace + ":" + xmlname; //$NON-NLS-1$
    	}
    	return xmlname;
    }

    //TODO final. getAttribute(Element, String, XAttribute) should be overridden
    public String getAttribute(Element element, String xmlname) {
    	return getAttribute(element, xmlname, null);
    }

    public String getAttribute(Element element, String xmlname, XAttribute attr) {
        if (xmlname.equals("CDATA") || xmlname.equals("#text")) { //$NON-NLS-1$ //$NON-NLS-2$
            return getCDATA(element, attr == null || attr.isTrimmable());
        } else if (xmlname.equals("#comment")) { //$NON-NLS-1$
            return getComment(element);
        } else {
       		int ind = xmlname.indexOf('.');
       		if (ind > 0) {
       			String childName = xmlname.substring(0, ind);
       	    	if(namespaceMapping != null) { 
       	    		childName = namespaceMapping.convertToActual(childName);
       	    	}
       			if(namespace != null && namespace.length() > 0 && childName.indexOf(':') < 0) {
       				childName = namespace + ":" + childName; //$NON-NLS-1$
       			}
       			Element child = XMLUtil.getFirstChild(element, childName);
       			if (child != null) return getAttribute(child, xmlname.substring(ind+1), attr);
       		} else {
               	xmlname = applyNamespaceToAttribute(xmlname);
               	if (XMLUtil.hasAttribute(element, xmlname)) {
               		return element.getAttribute(xmlname);
               	}
        	}
        }
        return null;
    }

    public void loadChildren(Element element, XModelObject o) {
        XModelEntity entity = o.getModelEntity();
        XModel model = o.getModel();
        Set<String> childset = entity.getRequiredChildren();
        NodeList nl = element.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node n = nl.item(i);
            if(n.getNodeType() != Node.ELEMENT_NODE) continue;
            Element e = (Element)n;
            String en = getChildEntity(entity, e);
            if(en == null && entity.getChild(ENT_ANY_ELEMENT) != null) en = ENT_ANY_ELEMENT;
            if(en == null) continue;
            XModelObject co = model.createModelObject(en, null);
            if(co == null) continue;
            load(e, co);
            if(!o.addChild(co) && co.getFileType() == XModelObject.NONE) {
        		if(co.getModelEntity().getAttribute(ATTR_ID_NAME) != null) {
        			int k = 1;
        			String pp = co.getPathPart();
        			while(o.getChildByPath(pp) != null) {
        				co.setAttributeValue(ATTR_ID_NAME, "" + k); //$NON-NLS-1$
        				String ppn = co.getPathPart();
        				if(ppn.equals(pp)) break;
        				pp = ppn;
        				++k;
        			}
        			o.addChild(co);
        		} else if(o.isActive()
        				|| (entity.getChild(en) != null
        						&& entity.getChild(en).isRequired())) try {
            		XModelObject q = o.getChildByPath(co.getPathPart());
            		if(q != null) EnginesLoader.merge(q, co, false);
            	} catch (XModelException exc) {
            		ModelPlugin.getPluginLog().logError("XModelObjectLoaderUtil:loadChildren:" + exc.getMessage(), exc); //$NON-NLS-1$
        		} else {
        			int k = 1;
        			String pp = co.getPathPart();
        			while(o.getChildByPath(pp) != null) {
        				co.set(XModelObjectImpl.DUPLICATE, "" + k); //$NON-NLS-1$
        				String ppn = co.getPathPart();
        				if(ppn.equals(pp)) break;
        				pp = ppn;
        				++k;
        			}
        			o.addChild(co);
            	}
            	continue;
            } 
            if(childset.contains(en)) {
            	childset.remove(en);
            }
        }
        if(!childset.isEmpty()) { //in most cases the set is empty and this check is faster than creating an iterator.
            for (String en: childset) {
                o.addChild(createValidObject(model, en));
            }
        }
    }
    
    public static String getModelEntityAttribute(Element element) {
        if(XMLUtil.hasAttribute(element, XModelConstants.XMODEL_ENTITY_ATTR)) {
        	return element.getAttribute(XModelConstants.XMODEL_ENTITY_ATTR);
        }
        if(XMLUtil.hasAttribute(element, XModelConstants.XMODEL_ENTITY_ATTR_OLD)) {
        	return element.getAttribute(XModelConstants.XMODEL_ENTITY_ATTR_OLD);
        }
        return null;
    }

    protected String getChildEntity(XModelEntity entity, Element e) {
		String en = getModelEntityAttribute(e);
		if(en == null || en.length() == 0) {
			String n = e.getNodeName();
   	    	if(namespaceMapping != null) { 
   	    		n = namespaceMapping.convertToDefault(n);
   	    	}
			if(namespace != null && namespace.length() > 0 && n.startsWith(namespace + ":")) { //$NON-NLS-1$
				n = n.substring(namespace.length() + 1);
			}
			en = entity.getChildByXML(n);
		}
		return en;
    }

    protected boolean acceptElement(Element e, String entity) {
        return !saveentity || 
        	(entity.equals(e.getAttribute(XModelConstants.XMODEL_ENTITY_ATTR)) 
        	|| entity.equals(e.getAttribute(XModelConstants.XMODEL_ENTITY_ATTR_OLD)));
    }

    public static XModelObject createValidObject(XModel model, String entityname) {
        return createValidObject(model, entityname, null);
    }    

    public static XModelObject createValidObject(XModel model, String entityname, Properties p) {
        XModelObject o = model.createModelObject(entityname, p);
        if(o != null) addRequiredChildren(o);
        return o;
    }

	public static void addRequiredChildren(XModelObject object) {
		XChild[] cs = object.getModelEntity().getChildren();
		for (int i = 0; i < cs.length; i++) {
			if(cs[i].getMaxCount() != 1 || !cs[i].isRequired()) continue;
			XModelObject c = createValidObject(object.getModel(), cs[i].getName(), null);
			if(c != null) object.addChild(c);
		}
	}

	public static void addRequiredChildren(XModelObject object, boolean onlyHavingNoXMLPath) {
		XChild[] cs = object.getModelEntity().getChildren();
		for (int i = 0; i < cs.length; i++) {
			if(cs[i].getMaxCount() != 1 || !cs[i].isRequired()) continue;
			if(onlyHavingNoXMLPath) {
				XModelEntity e = cs[i].getMetaModel().getEntity(cs[i].getName());
				if(e == null) continue;
				String xml = e.getXMLSubPath();
				if(xml != null && xml.length() > 0) continue;
			}
			XModelObject c = createValidObject(object.getModel(), cs[i].getName(), null);
			if(c != null) object.addChild(c);
		}
	}

    public boolean save(Element parent, XModelObject o) {
		if(ENT_ANY_ELEMENT.equals(o.getModelEntity().getName())) {
			saveAnyElement(parent, o);
			return true;
		} else {
			String xmlname = getXMLSubPath(o);
			if(xmlname == null || xmlname.trim().length() == 0) return true;
   	    	if(namespaceMapping != null) { 
   	    		xmlname = namespaceMapping.convertToActual(xmlname);
   	    	}
			if(namespace != null && namespace.length() > 0) {
				xmlname = namespace + ":" + xmlname; //$NON-NLS-1$
			}
			Element element = XMLUtil.createElement(parent, xmlname);
			saveAttributes(element, o);
			boolean b = saveChildren(element, o);
			saveFinalComment(element, o);
			if(b) o.setModified(false);
			return b;
		}
    }
    
    protected String getXMLSubPath(XModelObject o) {
    	return o.getModelEntity().getXMLSubPath();
    }

    public void saveAttributes(Element element, XModelObject o) {
        XModelEntity entity = o.getModelEntity();
        XAttribute[] as = entity.getAttributes();
        if (saveentity) {
            element.setAttribute(XModelConstants.XMODEL_ENTITY_ATTR, entity.getName());
        }
        for (int i = 0; i < as.length; i++) {
            if (as[i].isFake()) continue;
            String n = as[i].getName();
            if("attributes".equals(n) && "true".equals(as[i].getProperty("any"))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            	saveAnyAtributes(element, o);
            	continue;
            }
            String xmlname = as[i].getXMLName();
            if (xmlname == null || xmlname.length() == 0) continue;
            String v = o.getAttributeValue(n);
            if (isSaveable(entity, n, v, as[i].getDefaultValue())) {
                saveAttribute(element, xmlname, v);
                String commentName = getAttributeCommentName(xmlname);
                if(commentName != null) {
                	String comment = o.get(commentName);
                	if(comment != null && comment.length() > 0) {
                		String cn = xmlname.substring(0, xmlname.indexOf('.'));
                		Element child = XMLUtilities.getLastChild(element, cn);
                		if(child != null) {
                			setComment(child, comment);
                		}
                	}                		
                }
            }
        }
    }

    public void saveAttribute(Element element, String xmlname, String value) {
        if (xmlname.equals("CDATA")) { //$NON-NLS-1$
            setCDATA(element, value);
        } else if (xmlname.equals("#text")) { //$NON-NLS-1$
            setText(element, value);
        } else if (xmlname.equals("#comment")) { //$NON-NLS-1$
            setComment(element, value);
        } else {
            int ind = xmlname.indexOf('.');
            if (ind > 0) {
                String childName = xmlname.substring(0, ind);
       	    	if(namespaceMapping != null) { 
       	    		childName = namespaceMapping.convertToActual(childName);
       	    	}
                if(namespace != null && namespace.length() > 0 && childName.indexOf(':') < 0) {
                	childName = namespace + ":" + childName; //$NON-NLS-1$
                }
                Element child = XMLUtil.getFirstChild(element, childName);
                if (child == null) child = XMLUtil.createElement(element, childName);
                saveAttribute(child, xmlname.substring(ind+1), value);
            } else {
            	xmlname = applyNamespaceToAttribute(xmlname);
                element.setAttribute(xmlname, value);
            }
        }
    }
    
    protected boolean isSaveable(XModelEntity entity, String n, String v, String dv) {
        if (v == null || v.length() == 0) return false;
        if (!v.equals(dv)) return true;
        return (singular != null && singular.get(n) != null);
    }
    
    public void saveFinalComment(Element element, XModelObject o) {
    	String data = o.get("#final-comment"); //$NON-NLS-1$
    	if(data == null || data.length() == 0) return;
        Comment comm = element.getOwnerDocument().createComment(data);
        element.appendChild(comm);    	
    }

    public boolean saveChildren(Element element, XModelObject o) {
        XModelObject[] os = ((XModelObjectImpl)o).getChildrenForSave();
        boolean b = true;
        for (int i = 0; i < os.length; i++) if(!save(element, os[i])) b = false;
        return b;
    }

    public XModelObject parse(XModel model, Reader reader) {
        Element element = XMLUtil.getElement(reader);
        if(element == null) return null;
        String entity = getModelEntityAttribute(element);
        XModelObject o = model.createModelObject(entity, null);
        load(element, o);
        return o;
    }

    public Element asElement(XModelObject o) {
        Element root = XMLUtil.createDocumentElement(o.getModelEntity().getXMLSubPath());
        saveAttributes(root, o);
        boolean b = saveChildren(root, o);
////        if(o instanceof XSavable && !((XSavable)o).save()) b = false;
        return (b) ? root : null;
    }

    public static final void serialize(Element element, String filename) throws IOException {
        File f = new File(filename);
        if(f.exists() && !f.canWrite()) return;
        if(!f.exists()) f.createNewFile();
        FileWriter fw = new FileWriter(f);
        serialize(element, new BufferedWriter(fw));
        fw.close();
    }

	public static String getEncoding(String body) {
		if(body == null || body.length() == 0) return null; //"UTF-8"
		String encoding = FileUtil.getEncoding(body);
		if(encoding == null) return null; //"UTF-8"
		return FileUtil.validateEncoding(encoding, "UTF-8"); //$NON-NLS-1$
	}

    public static OutputFormat createOutputFormat(String encoding) {
    	return XMLUtilities.createOutputFormat(encoding);
    }

    public static final boolean serialize(Element element, Writer w) throws IOException {
        if(element == null) return false;
        serialize(element, new XMLSerializer(w, createOutputFormat("UTF-8"))); //$NON-NLS-1$
        w.close();
        return true;
    }

    public boolean serialize(XModelObject object, Writer w) throws XModelException, IOException {
        return serialize(asElement(object), w);
    }

    public static final boolean serialize(Element element, OutputStream w) throws IOException {
        if(element == null) return false;
        serialize(element, new XMLSerializer(w, createOutputFormat("UTF-8"))); //$NON-NLS-1$
        w.close();
        return true;
    }

    public boolean serialize(XModelObject object, OutputStream w) throws XModelException, IOException {
        return serialize(asElement(object), w);
    }

    public static void serialize(Element element, XMLSerializer serial) throws IOException {
        serial.asDOMSerializer();
        serial.serialize(element);
    }

    public static void serialize(Document document, XMLSerializer serial) throws IOException {
        serial.asDOMSerializer();
        serial.serialize(document);
    }

    public static final boolean serialize(Document document, Writer w) throws IOException {
    	return serialize(document, w, null);
    }

	public static final boolean serialize(Document document, Writer w, String encoding) throws IOException, IOException {
		if(document == null) return false;
		serialize(document, new XMLSerializer(w, createOutputFormat(encoding)));
		w.close();
		return true;
	}

    public String asString(XModelObject object) {
        StringWriter w = new StringWriter();
        Exception e = null;
        try {
            serialize(object, w);
            return w.toString();
        } catch (XModelException e1) {
        	e = e1;
        } catch (IOException e2) {
        	e = e2;
        }
    	ModelPlugin.getPluginLog().logError("XModelObjectLoaderUtil:asString:" + e.getMessage(), e); //$NON-NLS-1$
    	return ""; //$NON-NLS-1$
    }

    public static final String getCDATA(Element elem) {
    	return XMLUtilities.getCDATA(elem, true);
    }

    public static final String getCDATA(Element elem, boolean trim) {
    	return XMLUtilities.getCDATA(elem, trim);
    }

    public static final String getComment(Element elem) {
        StringBuffer sb = new StringBuffer();
        Node node = elem.getPreviousSibling();
        loadComment(sb, node);
        return sb.toString();
    }

    public static final String getFinalComment(Element elem) {
        StringBuffer sb = new StringBuffer();
        NodeList nl = elem.getChildNodes();
        if(nl == null || nl.getLength() == 0) return ""; //$NON-NLS-1$
        Node node = nl.item(nl.getLength() - 1);
        loadComment(sb, node);
        return sb.toString();
    }

    static final void loadComment(StringBuffer sb, Node node) {
        while (node != null) {
            if (node.getNodeType() == Node.ELEMENT_NODE) break;
            if (node.getNodeType() == Node.COMMENT_NODE) {
                if (sb.length() > 0) {
                    sb.insert(0, '\n');
                    sb.insert(0, ((Comment) node).getData());
                } else {
                    sb.append(((Comment) node).getData());
                }
            }
            node = node.getPreviousSibling();
        }
    }

    public static final void setCDATA(Element element, String data) {
        if (data == null) data = ""; //$NON-NLS-1$
        element.appendChild(element.getOwnerDocument().createCDATASection(data));
    }

    public static final void setText(Element element, String data) {
        if (data == null) data = ""; //$NON-NLS-1$
        element.appendChild(element.getOwnerDocument().createTextNode(data));
    }

    public static final void setComment(Element element, String data) {
        if (data == null) data = ""; //$NON-NLS-1$
        Comment comm = element.getOwnerDocument().createComment(data);
        element.getParentNode().insertBefore(comm, element);
    }

    public void load(File f, XModelObject o) {
    	if(f == null || !f.isFile()) return;
        Element element = XMLUtil.getElement(f.getAbsolutePath());
        if(element != null) {
        	load(element, o);
        }
    }

    public boolean save(File f, XModelObject o) {
        if(f.exists() && !o.isModified()) return true;
        StringWriter w = new StringWriter();
        Exception e = null;
        try {
            if(!serialize(o, w)) return false;
        } catch (XModelException e1) {
        	e = e1;
        } catch (IOException e2) {
        	e = e2;
        }
        if(e != null) {
        	ModelPlugin.getPluginLog().logError("XModelObjectLoaderUtil:save(f,o):" + e.getMessage(), e); //$NON-NLS-1$
            return false;
        }
        String r = w.toString();
        boolean c = coincides(f, r);
        int i = (c) ? 0 : handleReadOnly(f, o);
        if(i != 0) return (i == 1);
        if(!c) writeFile(f, r);
        o.setModified(false);
        return true;
    }

    public static boolean saveBody(File f, XModelObject o, String defaultEncoding) {
        boolean same = FileUtil.isSameFile(f);
        if(same && !o.isModified()) return true;
        String r = getTempBody(o);
        boolean c = same && coincides(f, r);
        int i = (c) ? 0 : handleReadOnly(f, o);
        if(i != 0) return (i == 1);
        if(!c) {
       		if(!FileUtil.writeFileWithEncodingCheck(f, r, defaultEncoding)) return false;
        }
        o.setModified(false);
        return true;
    }

    public static int handleReadOnly(File f, XModelObject o) {
        int i = 0;
        while(i == 0 && f.exists() && !f.canWrite())
          i = o.getModel().getService().showDialog("Question",
                getReadOnlyMessage(f), new String[]{"Retry", "Discard", ModelMessages.Cancel}, null,
                ServiceDialog.QUESTION);
        return i;
    }

    public static String getReadOnlyMessage(File f) {
        return MessageFormat.format("Cannot save to read-only file {0}.", f.getAbsolutePath());
    }

    public static String getTempBody(XModelObject o) {
        String b = o.get(XModelObjectConstants.ATTR_NAME__BODY_);
        o.set(XModelObjectConstants.ATTR_NAME__BODY_, ""); //$NON-NLS-1$
        return b;
    }

    public static void setTempBody(XModelObject o, String body) {
        o.set(XModelObjectConstants.ATTR_NAME__BODY_, body);
    }

    //////pass correct properties!

    private static String expandString(String s) {
        return Paths.expand(s, System.getProperties());
    }

    public static String readFile(String filename) {
    	if(filename == null) return ""; //$NON-NLS-1$
        return readFile(new File(expandString(filename)));
    }

    public static String readFile(File f) {
        return FileUtil.readFile(f);
    }

    public static boolean isTextFile(File f, int length) {
        return FileUtil.isTextFile(f, length);
    }

    public static boolean writeFile(String filename, String value) {
    	if(filename == null) return false;
        return writeFile(new File(expandString(filename)), value);
    }

    public static boolean writeFile(File f, String value) {
        return FileUtil.writeFile(f, value);
    }

    public static boolean coincides(File f, String value) {
        return f.isFile() && value != null && readFile(f).equals(value);
    }

    public static XObjectLoader getObjectLoader(XModelObject object) {
    	if(object == null) return null;
    	return object.getModelEntity().getObjectLoader();
    }

    public static void remove(File f) {
        FileUtil.remove(f);
    }

    public static final String getResourcePath(XModelObject object) {
        int t = object.getFileType();
        if(t == XFileObject.SYSTEM) return ""; //$NON-NLS-1$
        XModelObject p = object.getParent();
        if(p == null) return null;
        String n = (t == XFileObject.FOLDER) ? object.get(XModelObjectConstants.XML_ATTR_NAME) :
                   (t == XFileObject.FILE) ? FileAnyImpl.toFileName(object) :
                   object.getPathPart();
        String pp = getResourcePath(p);
        return (pp == null) ? n : pp + XModelObjectConstants.SEPARATOR + n;
    }

    /*
     * Multilined text may be saved as xml attribute value
     * by coding chars '\\', '\n' and '\t'
     * with strings "\\\\", "\\n" and "\\t"
     * Editor for this attribute uses load-method to present
     * true value of attribute and save-method to write
     * modified value. 
     */

    public static String loadFromXMLAttribute(String text) {
        StringBuffer sb = new StringBuffer();
        char[] c = text.toCharArray();
        boolean slash = false;
        for (int i = 0; i < c.length; i++) {
            if(!slash) {
                if(c[i] != '\\') {
                    sb.append(c[i]);
                } else if(c[i] == '\\') {
                    slash = true;
                }
            } else {
                slash = false;
                if(c[i] == '\\') sb.append('\\');
                else if(c[i] == 'n') sb.append('\n');
                else if(c[i] == 't') sb.append('\t');
                else sb.append('\\').append(c[i]);
            }
        }
        return sb.toString();
    }

    public static String saveToXMLAttribute(String text) {
        StringBuffer sb = new StringBuffer();
        char[] c = text.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if(c[i] == '\r') continue;
            else if(c[i] == '\n') sb.append('\\').append('n');
            else if(c[i] == '\\') sb.append('\\').append('\\');
            else if(c[i] == '\t') sb.append('\\').append('t');
            else sb.append(c[i]);
        }
        return sb.toString();
    }
    
    //

	public static void updateModifiedOnSave(XModelObject o) {
		if(!o.isModified()) updateModifiedFlag(o.getParent());
	}
    
	public static void updateModifiedFlag(XModelObject o) {
		if(o == null) return;
		XModelObject[] cs = o.getChildren();
		for (int i = 0; i < cs.length; i++) if(cs[i].isModified()) return;
		o.setModified(false);
		updateModifiedFlag(o.getParent());
	}
	
	protected void loadAnyElement(Element element, XModelObject o) {
		o.setAttributeValue("tag", element.getTagName()); //$NON-NLS-1$
		loadAnyAtributes(element, o);
		String text = getAttribute(element, "#text").trim(); //$NON-NLS-1$
		if(text.length() > 0) {
			while(text.startsWith("\n") || text.startsWith("\r")) text = text.substring(1); //$NON-NLS-1$ //$NON-NLS-2$
			while(text.endsWith("\n") || text.endsWith("\r")) text = text.substring(0, text.length() - 1); //$NON-NLS-1$ //$NON-NLS-2$
			o.setAttributeValue("text", text); //$NON-NLS-1$
		}
		loadChildren(element, o);
	}

	public static void loadAnyAtributes(Element element, XModelObject o) {
		HashSet<String> attrs = new HashSet<String>();
		XAttribute[] oas = o.getModelEntity().getAttributes();
		for (XAttribute a: oas) {
			String xml = a.getXMLName();
			if(xml != null && xml.length() > 0) attrs.add(xml);
		}
		StringBuffer sb = new StringBuffer();
		NamedNodeMap as = element.getAttributes();
		for (int i = 0; i < as.getLength(); i++) {
			Node n = as.item(i);
			String nm = n.getNodeName();
			if(attrs.contains(nm)) continue;
			String v = n.getNodeValue();
			if(v == null) continue;
			if(sb.length() > 0) sb.append(";"); //$NON-NLS-1$
			sb.append(nm).append("=").append(v); //$NON-NLS-1$
		}
		o.setAttributeValue("attributes", sb.toString()); //$NON-NLS-1$
	}
    
	protected void saveAnyElement(Element parent, XModelObject o) {
		String xmlname = o.getAttributeValue("tag"); //$NON-NLS-1$
		if(xmlname == null || xmlname.trim().length() == 0) return;
		if(namespace != null && namespace.length() > 0) {
			xmlname = namespace + ":" + xmlname; //$NON-NLS-1$
		}
		Element element = XMLUtil.createElement(parent, xmlname);
		saveAnyAtributes(element, o);
		String text = o.getAttributeValue("text"); //$NON-NLS-1$
		if(text != null && text.length() > 0) {
			saveAttribute(element, "#text", text); //$NON-NLS-1$
		}
		XModelObject[] cs = o.getChildren();
		for (int i = 0; i < cs.length; i++) {
			saveAnyElement(element, cs[i]);
		}
	}

	public static void saveAnyAtributes(Element element, XModelObject o) {
		String attrs = o.getAttributeValue("attributes"); //$NON-NLS-1$
		StringTokenizer st = new StringTokenizer(attrs, ";"); //$NON-NLS-1$
		while(st.hasMoreTokens()) {
			String t = st.nextToken();
			int i = t.indexOf('=');
			if(i < 0) continue;
			String n = t.substring(0, i);
			String v = t.substring(i + 1);
			element.setAttribute(n, v);
		}
	}

	protected void eitherOr(Element element, String attr1, String attr2) {
		Element e1 = XMLUtil.getUniqueChild(element, attr1);
		Element e2 = XMLUtil.getUniqueChild(element, attr2);
		if(e1 != null && e2 != null) element.removeChild(e2);
	}

	protected void saveArray(Element element, String xmlname, String value) {
		String[] ns = XModelObjectUtil.asStringArray(value);
    	if(namespaceMapping != null) { 
    		xmlname = namespaceMapping.convertToActual(xmlname);
    	}
		if(namespace != null && namespace.length() > 0 && xmlname.indexOf(':') < 0) {
			xmlname = namespace + ":" + xmlname; //$NON-NLS-1$
		}
		for (int i = 0; i < ns.length; i++) {
			Element c = XMLUtil.createElement(element, xmlname);
			saveAttribute(c, "#text", ns[i]); //$NON-NLS-1$
		}
	}

	protected String loadArray(Element element, String xmlname) {
		Element[] es = XMLUtil.getChildren(element, xmlname);
		if(es == null || es.length == 0) return ""; //$NON-NLS-1$
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < es.length; i++) {
			if(i > 0) sb.append(","); //$NON-NLS-1$
			sb.append(getAttribute(es[i], "#text")); //$NON-NLS-1$
		}
		return sb.toString();
	}

	public static void mergeAttributes(XModelObject destination, XModelObject source) throws XModelException {
		mergeAttributes(destination, source, destination.isActive());
	}
	
	public static void mergeAttributes(XModelObject destination, XModelObject source, boolean fire) throws XModelException {
		XAttribute[] as = destination.getModelEntity().getAttributes();
		for (int i = 0; i < as.length; i++) {
			String n = as[i].getName();
			if(!as[i].isCopyable()) continue;
			mergeAttributeComment(destination, source, as[i], fire);
			String ov = destination.getAttributeValue(n);
			String nv = source.getAttributeValue(n);
			if(nv == null || (ov != null && ov.equals(nv))) continue;
			if(fire) {
				destination.getModel().changeObjectAttribute(destination, n, nv);
			} else {
				destination.setAttributeValue(n, nv);
			}
		}
		mergeFinalComment(destination, source, fire);
		
		String d = destination.get(XModelObjectImpl.DUPLICATE);
		if(d != null && d.length() > 0) destination.set(XModelObjectImpl.DUPLICATE, "");
	}
	
	public static void mergeAttributeComment(XModelObject destination, XModelObject source, XAttribute attr, boolean fire) {
		String commentName = getAttributeCommentName(attr.getXMLName());
		mergeComment(destination, source, commentName, fire);
	}
	
	private static String getAttributeCommentName(String xmlname) {
        if(xmlname != null && xmlname.indexOf('.') > 0 && !xmlname.endsWith("#comment")) { //$NON-NLS-1$
			return xmlname + ".#comment"; //$NON-NLS-1$
		}
        return null;		
	}

	public static void mergeFinalComment(XModelObject destination, XModelObject source, boolean fire) {
		String commentName = "#final-comment"; //$NON-NLS-1$
		mergeComment(destination, source, commentName, fire);
	}
	
	static void mergeComment(XModelObject destination, XModelObject source, String commentName, boolean fire) {
		if(commentName == null) return;
		String newComment = source.get(commentName);
		String oldComment = destination.get(commentName);
		String set = null;
		if(newComment == null || newComment.length() == 0) {
			if(oldComment != null && oldComment.length() > 0) {
				set = ""; //$NON-NLS-1$
			} 
		} else {
			if(oldComment == null || !oldComment.equals(newComment)) {
				set = newComment;
			}
		}
		if(set != null) {
			destination.set(commentName, set);
			if(fire) {
				XModelImpl impl = (XModelImpl)destination.getModel();
				impl.fireNodeChanged(destination, destination.getPath());
			}
		}
	}
	
}
