Package org.fao.geonet.kernel

Source Code of org.fao.geonet.kernel.EditLib$SelectResult

//==============================================================================
//===
//=== EditLib
//===
//=============================================================================
//===  Copyright (C) 2001-2007 Food and Agriculture Organization of the
//===  United Nations (FAO-UN), United Nations World Food Programme (WFP)
//===  and United Nations Environment Programme (UNEP)
//===
//===  This program is free software; you can redistribute it and/or modify
//===  it under the terms of the GNU General Public License as published by
//===  the Free Software Foundation; either version 2 of the License, or (at
//===  your option) any later version.
//===
//===  This program is distributed in the hope that it will be useful, but
//===  WITHOUT ANY WARRANTY; without even the implied warranty of
//===  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//===  General Public License for more details.
//===
//===  You should have received a copy of the GNU General Public License
//===  along with this program; if not, write to the Free Software
//===  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
//===
//===  Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
//===  Rome - Italy. email: geonetwork@osgeo.org
//==============================================================================

package org.fao.geonet.kernel;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.jxpath.ri.parser.Token;
import org.apache.commons.jxpath.ri.parser.XPathParser;
import org.apache.commons.jxpath.ri.parser.XPathParserConstants;
import org.fao.geonet.constants.Edit;
import org.fao.geonet.constants.Geonet;
import org.fao.geonet.constants.Geonet.Namespaces;
import org.fao.geonet.domain.Pair;
import org.fao.geonet.kernel.schema.MetadataAttribute;
import org.fao.geonet.kernel.schema.MetadataSchema;
import org.fao.geonet.kernel.schema.MetadataType;
import org.fao.geonet.utils.Log;
import org.fao.geonet.utils.Xml;
import org.jaxen.JaxenException;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.jdom.JDOMXPath;
import org.jdom.Attribute;
import org.jdom.Content;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.filter.ElementFilter;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Vector;

/**
* TODO javadoc.
*
*/
public class EditLib {
    private Hashtable<String, Integer> htVersions   = new Hashtable<String, Integer>(1000);
  private SchemaManager scm;

    public static final String XML_FRAGMENT_SEPARATOR = "&&&";
    public static final String COLON_SEPARATOR = "COLON";
    public static final String MSG_ELEMENT_NOT_FOUND_AT_REF = "Element not found at ref = ";
   
  //--------------------------------------------------------------------------
  //---
  //--- Constructor
  //---
  //--------------------------------------------------------------------------

    /**
     * Init structures.
     *
     * @param scm
     */
  public EditLib(SchemaManager scm) {
    this.scm = scm;
        htVersions.clear();
  }

  //--------------------------------------------------------------------------
  //---
  //--- API methods
  //---
  //--------------------------------------------------------------------------

    /**
     * Expands a metadata adding all information needed for editing.
     *
     * @param schema
     * @param id
     * @param md
     * @return
     * @throws Exception
     */
  public String getVersionForEditing(String schema, String id, Element md) throws Exception {
    String version = getVersion(id, true) +"";
    addEditingInfo(schema,md,1,0);
    return version;
  }

    /**
     * TODO javadoc.
     *
     * @param schema
     * @param md
     * @param id
     * @param parent
     * @throws Exception
     */
  public void addEditingInfo(String schema, Element md, int id, int parent) throws Exception {
        if(Log.isDebugEnabled(Geonet.EDITOR))
            Log.debug(Geonet.EDITOR,"MD before editing infomation:\n" + Xml.getString(md));
    enumerateTree(md,id,parent);
    expandTree(scm.getSchema(schema), md);
        if(Log.isDebugEnabled(Geonet.EDITOR))
            Log.debug(Geonet.EDITOR,"MD after editing infomation:\n" + Xml.getString(md));
  }

    /**
     * TODO javadoc.
     *
     * @param md
     * @throws Exception
     */
  public void enumerateTree(Element md) throws Exception {
    enumerateTree(md,1,0);
  }

    /**
     * TODO javadoc.
     *
     * @param md
     * @param id
     * @param parent
     * @throws Exception
     */
  public void enumerateTreeStartingAt(Element md, int id, int parent) throws Exception {
    enumerateTree(md,id,parent);
  }

    /**
     * TODO javadoc.
     *
     * @param id
     * @return
     */
  public String getVersion(String id) {
    return Integer.toString(getVersion(id, false));
  }

    /**
     * TODO javadoc.
     *
     * @param id
     * @return
     */
  public String getNewVersion(String id) {
    return Integer.toString(getVersion(id, true));
  }

    /**
     * Given an element, creates all mandatory sub-elements. The given element should be empty.
     * @param schema
     * @param parent
     * @param md
     * @throws Exception
     */
  public void fillElement(String schema, Element parent, Element md) throws Exception {
    fillElement(scm.getSchema(schema), scm.getSchemaSuggestions(schema), parent, md);
  }

    /**
     * Given an expanded tree, removes all info added for editing and replaces choice_elements with their children.
     *
     * @param md
     */
  public void removeEditingInfo(Element md) {
    //--- purge geonet: attributes

    @SuppressWarnings("unchecked")
        List<Attribute> listAtts = md.getAttributes();
    for (int i=0; i<listAtts.size(); i++) {
      Attribute attr = listAtts.get(i);
      if (Edit.NAMESPACE.getPrefix().equals(attr.getNamespacePrefix())) {
        attr.detach();
        i--;
      }
    }

    //--- purge geonet: children
    @SuppressWarnings("unchecked")
        List<Element> list = md.getChildren();
    for (int i=0; i<list.size(); i++) {
      Element child = list.get(i);
      if (!Edit.NAMESPACE.getPrefix().equals(child.getNamespacePrefix()))
        removeEditingInfo(child);
      else {
        child.detach();
        i--;
      }
    }
  }

    /**
     * Returns the element at a given reference.
     *
     * @param md the metadata element expanded with editing info
     * @param ref the element position in a pre-order visit
     * @return
     */
  public Element findElement(Element md, String ref) {
    Element elem = md.getChild(Edit.RootChild.ELEMENT, Edit.NAMESPACE);

    if (elem != null && ref.equals(elem.getAttributeValue(Edit.Element.Attr.REF)))
       return md;

    //--- search on children

    @SuppressWarnings("unchecked")
        List<Element> list = md.getChildren();

        for (Element child : list) {
            if (!Edit.NAMESPACE.getPrefix().equals(child.getNamespacePrefix())) {
                child = findElement(child, ref);

                if (child != null) {
                    return child;
                }
            }
        }
    return null;
  }

    /**
     * TODO javadoc.
     *
     * @param mdSchema
     * @param el
     * @param qname
     * @return
     * @throws Exception
     */
  public Element addElement(MetadataSchema mdSchema, Element el, String qname) throws Exception {
        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)){
            Log.debug(Geonet.EDITORADDELEMENT,"#### in addElement()");
            Log.debug(Geonet.EDITORADDELEMENT,"#### - parent = " + el.getName());
            Log.debug(Geonet.EDITORADDELEMENT,"#### - child qname = " + qname);
        }

    String name   = getUnqualifiedName(qname);
    String ns     = getNamespace(qname, el, mdSchema);
    String prefix = getPrefix(qname);
    String parentName = getParentNameFromChild(el);

        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
            Log.debug(Geonet.EDITORADDELEMENT,"#### - parent name for type retrieval = " + parentName);
            Log.debug(Geonet.EDITORADDELEMENT,"#### - child name = " + name);
            Log.debug(Geonet.EDITORADDELEMENT,"#### - child namespace = " + ns);
            Log.debug(Geonet.EDITORADDELEMENT,"#### - child prefix = " + prefix);
        }
    @SuppressWarnings("unchecked")
        List<Element> childS = el.getChildren();
    if (childS.size() > 0) {
      Element elChildS = childS.get(0);
      Log.debug(Geonet.EDITORADDELEMENT,"####   - parents first child = " + elChildS.getName());
    }

    Element child = new Element(name, prefix, ns);

    SchemaSuggestions mdSugg   = scm.getSchemaSuggestions(mdSchema.getName());

    String typeName = mdSchema.getElementType(el.getQualifiedName(),parentName);

        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT))
            Log.debug(Geonet.EDITORADDELEMENT,"#### - type name = " + typeName);

     MetadataType type = mdSchema.getTypeInfo(typeName);

        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT))
            Log.debug(Geonet.EDITORADDELEMENT,"#### - metadata tpe = " + type);

    //--- collect all children, adding the new one at the end of the others

    Vector<Element> children = new Vector<Element>();

    for(int i=0; i<type.getElementCount(); i++) {
      List<Element> list = getChildren(el, type.getElementAt(i));

            if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT))
                Log.debug(Geonet.EDITORADDELEMENT,"####   - child of type " + type.getElementAt(i) + " list size = " + list.size());
            for (Element aChild : list) {
                children.add(aChild);
                if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT))
                    Log.debug(Geonet.EDITORADDELEMENT, "####    - add child " + aChild.toString());
            }

      if (qname.equals(type.getElementAt(i)))
        children.add(child);
    }
    //--- remove everything and then add all collected children to the element to assure a correct position for the
    // new one

    el.removeContent();
        for (Element aChildren : children) {
            el.addContent(aChildren);
        }

    //--- add mandatory sub-tags
    fillElement(mdSchema, mdSugg, el, child);

    return child;
  }
 
    /**
     * Adds XML fragment to the metadata record in the last element
     * of the type of the element in its parent.
     *
     * @param schema The metadata schema
     * @param el The element
     * @param qname The qualified name of the element
     * @param fragment XML fragment
     * @param removeExisting Remove element of the same type before insertion
     * @throws Exception
     * @throws IllegalStateException Fail to parse the fragment.
     */
    public void addFragment(String schema, Element el, String qname, String fragment, boolean removeExisting) throws Exception {
       
        MetadataSchema mdSchema = scm.getSchema(schema);
        String parentName = getParentNameFromChild(el);
        Element fragElt;

        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT))
            Log.debug(Geonet.EDITORADDELEMENT, "Add XML fragment for element name:" + qname + ", parent: " + parentName);
       
        try {
            fragElt = Xml.loadString(fragment, false);
        }
        catch (JDOMException e) {
            Log.error(Geonet.EDITORADDELEMENT, "EditLib : Error parsing XML fragment " + fragment);
            throw new IllegalStateException("EditLib : Error when loading XML fragment, " + e.getMessage());
        }
       
        String typeName = mdSchema.getElementType(el.getQualifiedName(), parentName);
        MetadataType type = mdSchema.getTypeInfo(typeName);
       
        // --- collect all children, adding the new one at the end of the others
        Vector<Element> children = new Vector<Element>();
       
        for (int i = 0; i < type.getElementCount(); i++) {
            // Add existing children of all types
            List<Element> list = getChildren(el, type.getElementAt(i));
            if (qname.equals(type.getElementAt(i)) && removeExisting) {
                // Remove all existing children of the type of element to add
            } else {
                for (Element aList : list) {
                    children.add(aList);
                }
            }
            if (qname.equals(type.getElementAt(i)))
                children.add(fragElt);
        }
        // --- remove everything and then add all collected children to the element
        // --- to assure a correct position for the new one
        el.removeContent();
        for (Element aChildren : children) {
            el.addContent(aChildren);
        }
    }

    public void addXMLFragments(String schema, Element md, Map<String, String> xmlInputs) throws Exception, IOException,
        JDOMException {
      // Loop over each XML fragments to insert or replace
      for (Map.Entry<String, String> entry : xmlInputs.entrySet()) {
          String nodeRef = entry.getKey();
          String xmlSnippetAsString = entry.getValue();
          String nodeName = null;
          boolean replaceExisting = false;
         
          String[] nodeConfig = nodeRef.split("_");
          // Possibilities:
          // * X125
          // * X125_replace
          // * X125_gmdCOLONkeywords
          // * X125_gmdCOLONkeywords_replace
          nodeRef = nodeConfig[0];
         
          if (nodeConfig.length > 1 && nodeConfig[1] != null) {
              if (nodeConfig[1].equals("replace")) {
                  replaceExisting = true;
              } else {
                  nodeName = nodeConfig[1].replace(COLON_SEPARATOR, ":");
              }
          }
         
          if (nodeConfig.length > 2 && nodeConfig[2] != null) {
              if (nodeConfig[2].equals("replace")) {
                  replaceExisting = true;
              }
          }
         
         
          // Get element to fill
          Element el = findElement(md, nodeRef);
          if (el == null) {
              Log.error(Geonet.EDITOR, MSG_ELEMENT_NOT_FOUND_AT_REF + nodeRef);
              continue;
          }
         
         
          if (xmlSnippetAsString != null && !xmlSnippetAsString.equals("")) {
              String[] fragments = xmlSnippetAsString.split(XML_FRAGMENT_SEPARATOR);
              for (String fragment : fragments) {
                  if (nodeName != null) {
                      if(Log.isDebugEnabled(Geonet.EDITOR))
                          Log.debug(Geonet.EDITOR, "Add XML fragment; " + fragment + " to element with ref: " + nodeRef);
                     
                      addFragment(schema, el, nodeName, fragment, replaceExisting);
                  } else {
                      if(Log.isDebugEnabled(Geonet.EDITOR))
                          Log.debug(Geonet.EDITOR, "Add XML fragment; " + fragment
                              + " to element with ref: " + nodeRef + " replacing content.");
                     
                      // clean before update
                      el.removeContent();
                      fragment = addNamespaceToFragment(fragment);
                     
                      // Add content
                      Element node = Xml.loadString(fragment, false);
                      if (replaceExisting) {
                          @SuppressWarnings("unchecked")
                          List<Element> children = node.getChildren();
                          for (int i = 0; i < children.size(); i++) {
                              el.addContent(children.get(i).detach());
                          }
                      } else {
                          el.addContent(node);
                      }
                  }
              }
          }
      }
    }

    /**
     * This does exactly the same thing as
     * {@link #addElementOrFragmentFromXpath(org.jdom.Element, org.fao.geonet.kernel.schema.MetadataSchema, String, AddElemValue, boolean)}
     * except that it is done multiple times, once for each element in the map
     *
     * @param metadataRecord the record to update
     * @param xmlAndXpathInputs the xpaths and new values
     * @param metadataSchema the schema of the metadata record
     * @param createXpathNodeIfNotExist if true then xpaths will be created if they don't indentify an existing element or attribute.
     *                                  Otherwise only existing xpaths will be updated.
     * @return the number of updates.
     */
    public int addElementOrFragmentFromXpaths(Element metadataRecord, Map<String, AddElemValue> xmlAndXpathInputs,
                                              MetadataSchema metadataSchema, boolean createXpathNodeIfNotExist) {


        int numUpdated = 0;
        // Loop over each XML fragments to insert or replace
        for (Map.Entry<String, AddElemValue> entry : xmlAndXpathInputs.entrySet()) {
            String xpathProperty = entry.getKey();
            AddElemValue propertyValue = entry.getValue();
            boolean updated = addElementOrFragmentFromXpath(metadataRecord, metadataSchema, xpathProperty, propertyValue,
                    createXpathNodeIfNotExist);
            if (updated) {
                numUpdated ++;
            }
        }

        return numUpdated;
    }



    private static interface XPathParserLocalConstants {
        int SQBRACKET_OPEN = 84;
        int TEXT = 78;
        int NAMESPACE_SEP = 79;
        int ATTRIBUTE = 86;
        int PARENT = 83;
        int DESCENDANT = 7;
        Set<Integer> ILLEGAL_KINDS = Sets.newHashSet(PARENT, DESCENDANT);
    }

    /**
     * Special tags for updating metadata element by xpath.
     */
    public static interface SpecialUpdateTags {
        String REPLACE = "gn_replace";
        String ADD = "gn_add";
    }


    /**
     * Update a metadata record for the xpath/value provided. The xpath (in accordance with JDOM x-path) does not start
     * with the root element for example:
     * <p/>
     * <code><pre>
     *     &lt;gmd:MD_Metadata>
     *         &lt;gmd:fileIdentifier>&lt;/gmd:fileIdentifier>
     *     &lt;gmd:MD_Metadata>
     * </pre></code>
     * <p/>
     * The xpath
     *      <pre><code>  gmd:MD_Metadata/gmd:fileIdentifier</code></pre>
     * will <b>NOT</b> select any elements.  Instead one must use the xpath:
     *      <pre><code>  gmd:fileIdentifier</code></pre>
     * to select the gmd:fileIdentifier element.
     * <p/>
     * To update the root element of the metadata use the xpath: "" (empty string)
     * <p/>
     * <p/>
     * The value could be a String to set the value of an element or
     * and XML fragment to be inserted for the element.
     * <p/>
     * If the xpath match an existing element, this element is updated.
     * Only the first one is updated if more than one match.
     * <p/>
     * <p/>
     * If it does not, each missing nodes of the xpath are created and
     * the element inserted according to the schema definition.
     * <p/>
     * If the end of the xpath is an attribute:
     * <code><pre>elem/@att</pre></code>
     * <p/>
     * Then the attribute of the element will be set instead of the text of the element.
     * <p/>
     * The rules for updating a node with Xml is as follows:
     * <ul>
     *     <li>
     *         If the xml's root element is the same as the element selected by the XPATH then node is replaced with the element.  For
     *         example:
     *         <code><pre>
     * Xpath: gmd:fileIdentifier
     * XML: &lt;gmd:fileIdentifier gco:nilReason='withheld'/>
     * Result: the gmd:fileIdentifier element in the metadata will be completely replaced with the new one.  All attributes in the metadata
     *         will be lost and replaced with the attributes in the new element.
     *         </pre></code>
     *     </li>
     *     <li>
     *         If the xml's root element == '{@value org.fao.geonet.kernel.EditLib.SpecialUpdateTags#REPLACE}' (a magic tag) then the
     *         children of that element will be replace the element selected from the metadata.
     *     </li>
     *     <li>
     *         If the xml's root element == '{@value org.fao.geonet.kernel.EditLib.SpecialUpdateTags#ADD}' (a magic tag) then the children of that element will be added to the
     *         element selected from the metadata.
     *     </li>
     *     <li>
     *         If the xml's root element != the name (and namespace) of the element selected from the metadata then the xml will replace
     *         the children of the element selected from the metadata.
     *     </li>
     * </ul>
     *
     * @param metadataRecord the metadata xml to update
     * @param metadataSchema the schema of the metadata
     * @param xpathProperty the xpath to the element to update/replace/add
     * @param value the string or xmlString to add/update/replace
     * @param createXpathNodeIfNotExist if the element identified by the xpath does not exist it will be create when this is true
     *
     * @return true if the metadata was modified
     */
    public boolean addElementOrFragmentFromXpath(Element metadataRecord, MetadataSchema metadataSchema,
                                              String xpathProperty, AddElemValue value, boolean createXpathNodeIfNotExist) {

        try {
            if (value.isXml() && xpathProperty.matches(".*@[^/\\]]+")) {
                throw new AssertionError("Cannot set Xml on an attribute.  Xpath:'"+xpathProperty+"' value: '"+value+"'");
            }
            if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
                Log.debug(Geonet.EDITORADDELEMENT, "Inserting at location " + xpathProperty + " the snippet or value " + value);
            }

            final Object propNode = trySelectNode(metadataRecord, metadataSchema, xpathProperty).result;

            if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
                Log.debug(Geonet.EDITORADDELEMENT, "XPath found in metadata: " + (propNode != null));
            }


            // If a property is not found in metadata, create it...
            if (propNode != null) {
                // Update element content with node
                if (propNode instanceof Element && value.isXml()) {
                    doAddFragmentFromXpath(metadataSchema, value.getNodeValue(), (Element) propNode);
                } else if (propNode instanceof Element && !value.isXml()) {
                    // Update element text with value
                    ((Element) propNode).setText(value.getStringValue());
                } else if (propNode instanceof Attribute && !value.isXml()) {
                    ((Attribute) propNode).setValue(value.getStringValue());
                } else {
                    return false;
                }
               
                return true;
            } else {
                if (createXpathNodeIfNotExist) {
                    int indexOfRequiredPortion = -1;
                    // Extract the XPath for the element to match. For:
                    //  * Relative XPath (*//gmd:RS_Identifier)[2]/gmd:code/gco:CharacterString
                    // xpath should be (*//gmd:RS_Identifier)[2]
                    // * Absolute XPath with condition
                    // gmd:identificationInfo/gmd:MD_DataIdentification/gmd:citation/gmd:CI_Citation/gmd:date[gmd:CI_Date/gmd:dateType/gmd:CI_DateTypeCode/@codeListValue = 'revision']
                    // xpath should be gmd:identificationInfo/gmd:MD_DataIdentification/gmd:citation/gmd:CI_Citation/gmd:date
                    boolean relativeXpath = xpathProperty.startsWith("(");
                   
                    for (int i = 0; i < xpathProperty.length(); i++) {
                        final char c = xpathProperty.charAt(i);
                        if ((relativeXpath && (c == ')' ||  c == ']')) || (!relativeXpath && c == '[')) {
                            indexOfRequiredPortion = i + (relativeXpath ? 1 : 0);
                        }
                    }
                    if(indexOfRequiredPortion > 0) {
                        final String requiredXPath = xpathProperty.substring(0, indexOfRequiredPortion);
                        Object elem = trySelectNode(metadataRecord, metadataSchema, requiredXPath).result;
                        if (elem == null) {
                            return createAndAddFromXPath(metadataRecord, metadataSchema, requiredXPath, value);
                        } else if (elem instanceof Element) {
                            Element element = (Element) elem;

                            return createAndAddFromXPath(element, metadataSchema, xpathProperty.substring(indexOfRequiredPortion), value);
                        } else {
                            return false;
                        }
                    } else {
                        return createAndAddFromXPath(metadataRecord, metadataSchema, xpathProperty, value);
                    }
                }
            }
        } catch (JaxenException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    /**
     * Performs the updating of the element selected from the metadata by the xpath.
     */
    private void doAddFragmentFromXpath(MetadataSchema metadataSchema, Element newValue, Element propEl) throws Exception {

        if (newValue.getName().equals(SpecialUpdateTags.REPLACE) || newValue.getName().equals(SpecialUpdateTags.ADD)) {
            final boolean isReplace = newValue.getName().equals(SpecialUpdateTags.REPLACE);
            if (isReplace) {
                propEl.removeContent();
            }

            @SuppressWarnings("unchecked")
            List<Element> children = Lists.newArrayList(newValue.getChildren());
            for (Element child : children) {
                if (Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
                    Log.debug(Geonet.EDITORADDELEMENT, " > add " + Xml.getString(child));
                }
                child.detach();
                if (isReplace) {
                    propEl.addContent(child);
                } else {
                    final Element newElement = addElement(metadataSchema, propEl, child.getQualifiedName());
                    if (newElement.getParent() != null) {
                        propEl.setContent(propEl.indexOf(newElement), child);
                    } else if (child.getParentElement() == null) {
                        propEl.addContent(child);
            }
                }
            }
        } else  if (newValue.getName().equals(propEl.getName()) && newValue.getNamespace().equals(propEl.getNamespace())) {
            int idx = propEl.getParentElement().indexOf(propEl);
            propEl.getParentElement().setContent(idx, newValue);
        } else {
            propEl.setContent(newValue);
        }
    }

    private boolean createAndAddFromXPath(Element metadataRecord, MetadataSchema metadataSchema, String xpathProperty, AddElemValue value) throws Exception {
        if (xpathProperty.startsWith("/")) {
            xpathProperty = xpathProperty.substring(1);
        }
        if (xpathProperty.startsWith(metadataRecord.getQualifiedName()+"/")) {
            xpathProperty = xpathProperty.substring(metadataRecord.getQualifiedName().length()+1);
        }
        List<String> xpathParts = Arrays.asList(xpathProperty.split("/"));
        SelectResult rootElem = trySelectNode(metadataRecord, metadataSchema, xpathParts.get(0));

        Pair<Element, String> result;
        if (rootElem.result instanceof Element) {
            result = findLongestMatch(metadataRecord, metadataSchema, xpathParts);
        } else {
            result = Pair.read(metadataRecord, SLASH_STRING_JOINER.join(xpathParts));
        }
        final Element elementToAttachTo = result.one();
        final Element clonedMetadata = (Element) elementToAttachTo.clone();

        // Creating the element at the xpath location
        // Walk the XPath from the start until the end or the start of a filter
        // expression.
        // Collect element namespace prefix and name, check element exist and
        // create them according to schema definition.
        final XPathParser xpathParser = new XPathParser(new StringReader(clonedMetadata.getQualifiedName()+"/"+result.two()));

        // Start from the root of the metadata document
        Token currentToken = xpathParser.getNextToken();
        Token previousToken = currentToken;

        int depth = 0;
        Element currentNode = clonedMetadata;
        boolean existingElement = true;
        boolean isAttribute = false;
        String currentElementName = "";
        String currentElementNamespacePrefix = "";

        // Stop when token is null, start of an expression is found ie. "["
        //
        // Stop when an expression [ starts
        // The expression is supposed to be part of the XML snippet to insert
        // If an existing element needs to be updated use the _Xref_replace mode
        // this mode is more precise with the geonet:element/@ref.
        while (currentToken != null &&
               currentToken.kind != 0 &&
               currentToken.kind != XPathParserLocalConstants.SQBRACKET_OPEN) {

            // TODO : check no .., descendant, ... are in the xpath
            // Only full xpath are supported.
            if (XPathParserLocalConstants.ILLEGAL_KINDS.contains(currentToken.kind)) {
                return false;
            }

            // build element name as the parser progress into the xpath ...
            if (currentToken.kind == XPathParserLocalConstants.ATTRIBUTE ) {
                isAttribute = true;
            }
            // Match namespace prefix
            if (currentToken.kind == XPathParserLocalConstants.TEXT && previousToken.kind == XPathParserConstants.SLASH) {
                // get element namespace if element is text and previous was /
                // means qualified name only is supported
                currentElementNamespacePrefix = currentToken.image;
            } else if (currentToken.kind == XPathParserLocalConstants.TEXT &&
                       previousToken.kind == XPathParserLocalConstants.NAMESPACE_SEP) {
                // get element name if element is text and previous was /
                currentElementName = currentToken.image;

                // Do not change anything to the root of the
                // metadata record which MUST be the root of
                // the xpath
                if (depth > 0) {
                    // If an element name is created
                    // Check the element exist in the metadata
                    // and create it if needed.
                    String qualifiedName = currentElementNamespacePrefix + ":" + currentElementName;
                    if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
                        Log.debug(Geonet.EDITORADDELEMENT,
                                "Check if " + qualifiedName + " exists in " + currentNode.getName());
                    }


                    Element nodeToCheck = currentNode.getChild(currentElementName,
                            Namespace.getNamespace(metadataSchema.getNS(currentElementNamespacePrefix)));

                    if (nodeToCheck != null) {
                        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
                            Log.debug(Geonet.EDITORADDELEMENT, " > " + qualifiedName + " found");
                        }
                        // Element found, no need to create it, continue walking the xpath.
                        currentNode = nodeToCheck;
                        existingElement &= true;
                    } else {
                        if(Log.isDebugEnabled(Geonet.EDITORADDELEMENT)) {
                            Log.debug(Geonet.EDITORADDELEMENT, " > add new node " +
                                                               qualifiedName + " inserted in " + currentNode.getName());
                        }

                        if (metadataSchema.getElementValues(qualifiedName, currentNode.getQualifiedName()) != null) {
                            currentNode = addElement(metadataSchema, currentNode, qualifiedName);
                            existingElement = false;
                        } else {
                            // element not in schema so stop!
                            return false;
                    }
                }
                }

                depth ++;
                // Reset current element props
                currentElementName = "";
                currentElementNamespacePrefix = "";
            }

            previousToken = currentToken;
            currentToken = xpathParser.getNextToken();
        }

        // The current node is an existing node or newly created one
        // Insert the XML value
        // TODO: deal with attribute ?
        if (value.isXml()) {
            // If current node match the node name to insert
            // Insert the new node in its parent
            if (existingElement) {
                currentNode = addElement(metadataSchema,
                        currentNode.getParentElement(),
                        currentNode.getQualifiedName());
            }

            // clean before update
            // when adding the fragment child nodes or suggestion may also be added.
            // In this case, the snippet only has to be inserted
            currentNode.removeContent();
            doAddFragmentFromXpath(metadataSchema, value.getNodeValue(), currentNode);
        } else {
            if (isAttribute) {
                currentNode.setAttribute(previousToken.image, value.getStringValue());
            } else {
                currentNode.setText(value.getStringValue());
            }
        }

        // update worked so now we can update original element...
        elementToAttachTo.removeContent();
        List<Content> toAdd = Lists.newArrayList(clonedMetadata.getContent());
        for (Content content : toAdd) {
            elementToAttachTo.addContent(content.detach());
        }
        return true;
    }

    private static final Joiner SLASH_STRING_JOINER = Joiner.on('/');
    @VisibleForTesting
    protected Pair<Element, String> findLongestMatch(final Element metadataRecord,
                                                     final MetadataSchema metadataSchema,
                                                     final List<String> xpathPropertyParts) {
        BitSet bitSet = new BitSet(xpathPropertyParts.size());
        return findLongestMatch(metadataRecord, metadataRecord, 0, metadataSchema,
                xpathPropertyParts.size() / 2, xpathPropertyParts, bitSet);
    }

    private Pair<Element, String> findLongestMatch(final Element metadataRecord, final Element bestMatch, final int indexOfBestMatch,
                                     final MetadataSchema metadataSchema,  final int nextIndex, final List<String> xpathPropertyParts,
                                     BitSet visited) {

        if (visited.get(nextIndex)) {
            return Pair.read(bestMatch, SLASH_STRING_JOINER.join(xpathPropertyParts.subList(indexOfBestMatch, xpathPropertyParts.size())));
        }
        visited.set(nextIndex);

        // do linear search when for last couple elements of xpath
        if (xpathPropertyParts.size() - nextIndex < 3) {
            for (int i = xpathPropertyParts.size() - 1; i > -1 ; i--) {
                final String xpath = SLASH_STRING_JOINER.join(xpathPropertyParts.subList(0, i));
                SelectResult result = trySelectNode(metadataRecord, metadataSchema, xpath);
                if (result.result instanceof Element) {
                    return Pair.read((Element) result.result, SLASH_STRING_JOINER.join(xpathPropertyParts.subList(i,
                            xpathPropertyParts.size())));
                }
            }
            return Pair.read(bestMatch, SLASH_STRING_JOINER.join(xpathPropertyParts.subList(indexOfBestMatch, xpathPropertyParts.size())));
        } else {
            final SelectResult found = trySelectNode(metadataRecord, metadataSchema, SLASH_STRING_JOINER.join(xpathPropertyParts
                    .subList(0,
                            nextIndex)));
            if (found.result instanceof Element) {
                Element newBest = (Element) found.result;
                int newIndex = nextIndex + ((xpathPropertyParts.size() - nextIndex) / 2);
                return findLongestMatch(metadataRecord, newBest, nextIndex, metadataSchema, newIndex, xpathPropertyParts, visited);
            } else if(!found.error) {
                int newNextIndex = indexOfBestMatch + ((nextIndex - indexOfBestMatch) / 2);
                return findLongestMatch(metadataRecord, bestMatch, indexOfBestMatch, metadataSchema,
                        newNextIndex, xpathPropertyParts, visited);
                } else {
                int newNextIndex = nextIndex + 1;
                return findLongestMatch(metadataRecord, bestMatch, indexOfBestMatch, metadataSchema,
                        newNextIndex, xpathPropertyParts, visited);
            }
        }
    }

    private static class SelectResult {
        private static final SelectResult ERROR = new SelectResult(null, true);

        final Object result;
        final boolean error;

        private SelectResult(Object result, boolean error) {
            this.result = result;
            this.error = error;
        }
        private static SelectResult of(Object result) {
            return new SelectResult(result, false);
        }
    }

    private SelectResult trySelectNode(Element metadataRecord, MetadataSchema metadataSchema, String xpathProperty)  {
        if (xpathProperty.trim().isEmpty()) {
            return SelectResult.of(metadataRecord);
        }

        // Initialize the Xpath with all schema namespaces
        Map<String, String> mapNs = metadataSchema.getSchemaNSWithPrefix();


        try {
            JDOMXPath xpath = new JDOMXPath(xpathProperty);
            xpath.setNamespaceContext(new SimpleNamespaceContext(mapNs));
            // Select the node to update and check it exists
            return SelectResult.of(xpath.selectSingleNode(metadataRecord));
        } catch (JaxenException e) {
            Log.warning(Geonet.EDITORADDELEMENT, "An illegal xpath was used to locate an element: " + xpathProperty);
            return SelectResult.ERROR;
        }
    }

    //--------------------------------------------------------------------------
  //---
  //--- Private methods
  //---
  //--------------------------------------------------------------------------

    /**
     * TODO javadoc.
     *
     * @param el
     * @param qname
     * @return
     */
  private List<Element> getChildren(Element el, String qname) {
    Vector<Element> result = new Vector<Element>();

    @SuppressWarnings("unchecked")
        List<Element> children = el.getChildren();

        for (Element child : children) {
            if (child.getQualifiedName().equals(qname)) {
                result.add(child);
            }
        }
    return result;
  }

    /**
     * Returns the version of a metadata, incrementing it if necessary.
     *
     * @param id
     * @param increment
     * @return
     */
  private synchronized int getVersion(String id, boolean increment) {
    Integer inVer = htVersions.get(id);

    if (inVer == null)
      inVer = 1;

    if (increment)
      inVer = inVer + 1;

    htVersions.put(id, inVer);

    return inVer;
  }

    /**
     * TODO javadoc.
     *
     * @param schema
     * @param sugg
     * @param parent
     * @param element
     * @throws Exception
     */
  private void fillElement(MetadataSchema schema, SchemaSuggestions sugg, Element parent, Element element) throws Exception {
        String parentName = parent.getQualifiedName();
        fillElement(schema, sugg, parentName, element);
    }

    /**
     * TODO javadoc.
     *
     * @param schema    The metadata schema
     * @param sugg  The suggestion configuration for the schema
     * @param parentName  The name of the parent
     * @param element    The element to fill
     *
     * @throws Exception
     */
    private void fillElement(MetadataSchema schema, SchemaSuggestions sugg, String parentName, Element element) throws Exception {
        String elemName = element.getQualifiedName();
       
        boolean isSimpleElement = schema.isSimpleElement(elemName,parentName);
       
        if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
            Log.debug(Geonet.EDITORFILLELEMENT,"#### Entering fillElement()");
            Log.debug(Geonet.EDITORFILLELEMENT,"#### - elemName = " + elemName);
            Log.debug(Geonet.EDITORFILLELEMENT,"#### - parentName = " + parentName);
            Log.debug(Geonet.EDITORFILLELEMENT,"#### - isSimpleElement(" + elemName + ") = " + isSimpleElement);
        }
       
       
        // Nothing to fill - eg. gco:CharacterString
        if (isSimpleElement) {
            return;
        }
       
        MetadataType type = schema.getTypeInfo(schema.getElementType(elemName, parentName));
        boolean hasSuggestion = sugg.hasSuggestion(elemName, type.getElementList());
//        List<String> elementSuggestion = sugg.getSuggestedElements(elemName);
//        boolean hasSuggestion = elementSuggestion.size() != 0;
       
       
        if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
            Log.debug(Geonet.EDITORFILLELEMENT,"#### - Type:");
            Log.debug(Geonet.EDITORFILLELEMENT,"####   - name = " + type.getName());
            Log.debug(Geonet.EDITORFILLELEMENT,"####   - # attributes = " + type.getAttributeCount());
            Log.debug(Geonet.EDITORFILLELEMENT,"####   - # elements = " + type.getElementCount());
            Log.debug(Geonet.EDITORFILLELEMENT,"####   - # isOrType = " + type.isOrType());
            Log.debug(Geonet.EDITORFILLELEMENT,"####   - type = " + type);
            Log.debug(Geonet.EDITORFILLELEMENT,"#### - Has suggestion = " + hasSuggestion);
        }
       
       
        //-----------------------------------------------------------------------
        //--- handle attributes if mandatory or suggested
        //
        for(int i=0; i<type.getAttributeCount(); i++) {
            MetadataAttribute attr = type.getAttributeAt(i);
           
            if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                Log.debug(Geonet.EDITORFILLELEMENT,"####   - " + i + " attribute = " + attr.name);
                Log.debug(Geonet.EDITORFILLELEMENT,"####     - required = " + attr.required);
                Log.debug(Geonet.EDITORFILLELEMENT,"####     - suggested = "+sugg.isSuggested(elemName, attr.name));
            }
           
            if (attr.required || sugg.isSuggested(elemName, attr.name)) {
                String value = "";
               
                if (attr.defValue != null) {
                    value = attr.defValue;
                    if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                        Log.debug(Geonet.EDITORFILLELEMENT,"####     - value = " + attr.defValue);
                    }
                }
               
                String uname = getUnqualifiedName(attr.name);
                String ns     = getNamespace(attr.name, element, schema);
                String prefix = getPrefix(attr.name);
                if (!prefix.equals(""))
                    element.setAttribute(new Attribute(uname, value, Namespace.getNamespace(prefix,ns)));
                else
                    element.setAttribute(new Attribute(uname, value));
            }
        }
       
       
        //-----------------------------------------------------------------------
        //--- add mandatory children
        //
        //     isOrType if element has substitutes and one of them should be chosen
        if (!type.isOrType()) {
            for(int i=0; i<type.getElementCount(); i++) {
                String childName = type.getElementAt(i);
                boolean childIsMandatory = type.getMinCardinAt(i) > 0;
                boolean childIsSuggested = sugg.isSuggested(elemName, childName);
               
                if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                    Log.debug(Geonet.EDITORFILLELEMENT,"####   - " + i + " element = " + childName);
                    Log.debug(Geonet.EDITORFILLELEMENT,"####     - suggested = " + childIsSuggested);
                    Log.debug(Geonet.EDITORFILLELEMENT,"####     - is mandatory = " + childIsMandatory);
                }
               
               
               
                if (childIsMandatory || childIsSuggested) {
                   
                    MetadataType elemType = schema.getTypeInfo(schema.getElementType(childName, elemName));
                    List<String> childSuggestion = sugg.getSuggestedElements(childName);
                    boolean childHasOneSuggestion = sugg.hasSuggestion(childName, elemType.getElementList()) && (CollectionUtils.intersection(elemType.getElementList(),childSuggestion).size()==1);
                    boolean childHasOnlyCharacterStringSuggestion = childSuggestion.size() == 1 && childSuggestion.contains("gco:CharacterString");
                   
                    if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                        Log.debug(Geonet.EDITORFILLELEMENT,"####     - is or type = "+ elemType.isOrType());
                        Log.debug(Geonet.EDITORFILLELEMENT,"####     - has suggestion = "+ childHasOneSuggestion);
                        Log.debug(Geonet.EDITORFILLELEMENT,"####     - elem type list = " + elemType.getElementList());
                        Log.debug(Geonet.EDITORFILLELEMENT,"####     - suggested types list = " + sugg.getSuggestedElements(childName));
                    }
                   
                    //--- There can be 'or' elements with other 'or' elements inside them.
                    //--- In this case we cannot expand the inner 'or' elements so the
                    //--- only way to solve the problem is to avoid the creation of them
                    if (
                        schema.isSimpleElement(elemName, childName) ||  // eg. gco:Decimal
                        !elemType.isOrType() ||                         // eg. gmd:EX_Extent
                        (elemType.isOrType() && (                       // eg. depends on schema-suggestions.xml
                            childHasOneSuggestion ||                    //   expand the only one suggestion - TODO - this needs improvements
                            (childSuggestion.size() == 0 && elemType.getElementList().contains("gco:CharacterString")))
                                                                        //   expand element which have no suggestion
                                                                        // and have a gco:CharacterString substitute.
                                                                        // gco:CharacterString is the default.
                        )
                    ) {
                        // Create the element
                        String name   = getUnqualifiedName(childName);
                        String ns     = getNamespace(childName, element, schema);
                        String prefix = getPrefix(childName);
                       
                        Element child = new Element(name, prefix, ns);
                       
                        // Add it to the element
                        element.addContent(child);
                       
                        if (childHasOnlyCharacterStringSuggestion) {
                            child.addContent(new Element("CharacterString", Namespaces.GCO));
                        }
                       
                        // Continue ....
                        fillElement(schema, sugg, element, child);
                    } else {
                        // Logging some cases to avoid
                        if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                            if (elemType.isOrType()) {
                                 if (elemType.getElementList().contains("gco:CharacterString")
                                         && !childHasOneSuggestion) {
                                    Log.debug(Geonet.EDITORFILLELEMENT,"####   - (INNER) Requested expansion of an OR element having gco:CharacterString substitute and no suggestion: " + element.getName());
                                 } else {
                                    Log.debug(Geonet.EDITORFILLELEMENT,"####   - WARNING (INNER): requested expansion of an OR element : " +childName);
                                }
                            }
                        }
                    }
                }
            }
        } else if (type.getElementList().contains("gco:CharacterString") && !hasSuggestion) {
            // expand element which have no suggestion
            // and have a gco:CharacterString substitute.
            // gco:CharacterString is the default.
            if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                Log.debug(Geonet.EDITORFILLELEMENT, "####   - Requested expansion of an OR element having gco:CharacterString substitute and no suggestion: " + element.getName());
            }
            Element child = new Element("CharacterString", Namespaces.GCO);
            element.addContent(child);
        } else {
            // TODO: this could be supported if only one suggestion defined for an or element ?
            // It will require to get the proper namespace for the element
            if(Log.isDebugEnabled(Geonet.EDITORFILLELEMENT)) {
                Log.debug(Geonet.EDITORFILLELEMENT, "####   - WARNING : requested expansion of an OR element : " + element.getName());
            }
        }
    }

  //--------------------------------------------------------------------------
  //---
  //--- Tree expansion methods
  //---
  //--------------------------------------------------------------------------

    /**
     * Searches children of container elements for containers.
     *
     * @param chName
     * @param md
     * @param schema
     * @return
     * @throws Exception
     */
  public List<Element> searchChildren(String chName, Element md, String schema) throws Exception  {

    // FIXME? CHOICE_ELEMENT containers can only have one element in them
    // if there are more then the container will need to be duplicated
    // and the elements distributed? Doesn't seem to hurt so we'll leave it
    // for now........
    //

        boolean hasContent = false;
    Vector<Element> holder = new Vector<Element>();

    MetadataSchema mdSchema = scm.getSchema(schema);
    String chUQname = getUnqualifiedName(chName);
    String chPrefix = getPrefix(chName);
    String chNS     = getNamespace(chName, md, mdSchema);
    Element container = new Element(chUQname, chPrefix, chNS);
    MetadataType containerType = mdSchema.getTypeInfo(chName);
    for (int k=0;k<containerType.getElementCount();k++) { 
      String elemName = containerType.getElementAt(k);
            if(Log.isDebugEnabled(Geonet.EDITOR))
                Log.debug(Geonet.EDITOR,"    -- Searching for child "+elemName);
      List<Element> elems;
      if (elemName.contains(Edit.RootChild.GROUP)||
          elemName.contains(Edit.RootChild.SEQUENCE)||
          elemName.contains(Edit.RootChild.CHOICE)) {
        elems = searchChildren(elemName,md,schema);
      } else {
        elems = getChildren(md,elemName);
      }
            for (Element elem : elems) {
                container.addContent((Element) elem.clone());
                hasContent = true;
            }
    }
    if (hasContent) {
      holder.add(container);
    } else {
      if (!chName.contains(Edit.RootChild.CHOICE)) {
        fillElement(schema,md,container);
        holder.add(container);
      }
    }
    return holder;
  }

    /**
     * Given an unexpanded tree, creates container elements and their children.
     *
     * @param schema
     * @param md
     * @throws Exception
     */
  public void expandElements(String schema, Element md) throws Exception {

    //--- create containers and fill them with elements using a depth first
    //--- search
   
    @SuppressWarnings("unchecked")
        List<Element> childs = md.getChildren();
        for (Element child : childs) {
            expandElements(schema, child);
        }
 
    String name = md.getQualifiedName();
    String parentName = getParentNameFromChild(md);
    MetadataSchema mdSchema = scm.getSchema(schema);
    String typeName = mdSchema.getElementType(name,parentName);
    MetadataType thisType = mdSchema.getTypeInfo(typeName);

    if (thisType.hasContainers) {
      Vector<Content> holder = new Vector<Content>();
     
      for (int i=0;i<thisType.getElementCount();i++) {
        String chName = thisType.getElementAt(i);
        if (chName.contains(Edit.RootChild.CHOICE)||
            chName.contains(Edit.RootChild.GROUP)||
            chName.contains(Edit.RootChild.SEQUENCE)) {
          List<Element> elems = searchChildren(chName,md,schema);
          if (elems.size() > 0) {
            holder.addAll(elems);
          }
        } else {
          List<Element> chElem = getChildren(md,chName);
                    for (Element elem : chElem) {
                        holder.add(elem.detach());
                    }
        }
      }
      md.removeContent();
      md.addContent(holder);
    }
  }

    /**
     * For each container element - descend and collect children.
     * @param md
     * @return
     */
  private Vector<Object> getContainerChildren(Element md) {
    Vector<Object> result = new Vector<Object>();

    @SuppressWarnings("unchecked")
        List<Element> chChilds = md.getChildren();
        for (Element chChild : chChilds) {
            String chName = chChild.getName();
            if (chName.contains(Edit.RootChild.CHOICE) ||
                    chName.contains(Edit.RootChild.GROUP) ||
                    chName.contains(Edit.RootChild.SEQUENCE)) {
                List<Object> moreChChilds = getContainerChildren(chChild);
                result.addAll(moreChChilds);
            }
            else {
                result.add(chChild.clone());
            }
        }
    return result;
  }

    /**
     * Contracts container elements.
     *
     * @param md
     */
  public void contractElements(Element md) {
    //--- contract container children at each level in the XML tree
   
    Vector<Object> children = new Vector<Object>();
    @SuppressWarnings("unchecked")
        List<Content> childs = md.getContent();
        for (Content obj : childs) {
            if (obj instanceof Element) {
                Element mdCh = (Element) obj;
                String mdName = mdCh.getName();
                if (mdName.contains(Edit.RootChild.CHOICE) ||
                        mdName.contains(Edit.RootChild.GROUP) ||
                        mdName.contains(Edit.RootChild.SEQUENCE)) {
                    if (mdCh.getChildren().size() > 0) {
                        Vector<Object> chChilds = getContainerChildren(mdCh);
                        if (chChilds.size() > 0) {
                            children.addAll(chChilds);
                        }
                    }
                }
                else {
                    children.add(mdCh.clone());
                }
            }
            else {
                children.add(obj);
            }
        }
    md.removeContent();
    md.addContent(children);

    //--- now move down to the next level

        for (Object obj : children) {
            if (obj instanceof Element) {
                contractElements((Element) obj);
            }
        }
  }

    /**
     * Does a pre-order visit enumerating each node.
     *
     * @param md
     * @param ref
     * @param parent
     * @return
     * @throws Exception
     */
  private int enumerateTree(Element md, int ref, int parent) throws Exception {

    int thisRef = ref;
    int thisParent = ref;

    @SuppressWarnings("unchecked")
        List<Element> list = md.getChildren();

        for (Element child : list) {
            if (!Edit.NAMESPACE.getPrefix().equals(child.getNamespacePrefix())) {
                ref = enumerateTree(child, ref + 1, thisParent);
            }
        }

    Element elem = new Element(Edit.RootChild.ELEMENT, Edit.NAMESPACE);
    elem.setAttribute(new Attribute(Edit.Element.Attr.REF, thisRef +""));
    elem.setAttribute(new Attribute(Edit.Element.Attr.PARENT, parent +""));
    elem.setAttribute(new Attribute(Edit.Element.Attr.UUID, md.getQualifiedName()+"_"+UUID.randomUUID().toString()));
    md.addContent(elem);

    return ref;
  }

    /**
     * Finds the ref element with the maximum ref value and returns it.
     *
     * @param md
     * @return
     */
  public int findMaximumRef(Element md) {
    int iRef = 0;
    @SuppressWarnings("unchecked")
        Iterator<Element> mdIt = md.getDescendants(new ElementFilter("element"));
    while (mdIt.hasNext()) {
      Element elem = mdIt.next();
      String ref = elem.getAttributeValue("ref");
      if (ref != null) {
        int i = Integer.parseInt(ref);
        if (i > iRef) iRef = i;  
      }
    }
    return iRef;
  }

    /**
     * Given a metadata, does a recursive scan adding information for editing.
     *
     * @param schema
     * @param md
     * @throws Exception
     */
  public void expandTree(MetadataSchema schema, Element md) throws Exception {
    expandElement(schema, md);

    @SuppressWarnings("unchecked")
        List<Element> list = md.getChildren();

        for (Element child : list) {
            if (!Edit.NAMESPACE.getPrefix().equals(child.getNamespacePrefix())) {
                expandTree(schema, child);
            }
        }
  }

    /**
     * TODO javadoc.
     *
     * @param child
     * @return
     */
  private String getParentNameFromChild(Element child) {
        String parentName = "root";
    Element parent = child.getParentElement();
    if (parent != null) {
      parentName = parent.getQualifiedName();
    }
    return parentName;
  }

    /**
     * Adds editing information to a single element.
     *
     * @param schema
     * @param md
     * @throws Exception
     */
  public void expandElement(MetadataSchema schema, Element md) throws Exception {
        if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT))
            Log.debug(Geonet.EDITOREXPANDELEMENT,"entering expandElement()");

    String elemName = md.getQualifiedName();
    String parentName = getParentNameFromChild(md);

        if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT)) {
            Log.debug(Geonet.EDITOREXPANDELEMENT,"elemName = " + elemName);
            Log.debug(Geonet.EDITOREXPANDELEMENT,"parentName = " + parentName);
        }

    String elemType = schema.getElementType(elemName,parentName);
        if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT))
            Log.debug(Geonet.EDITOREXPANDELEMENT,"elemType = " + elemType);

    Element elem = md.getChild(Edit.RootChild.ELEMENT, Edit.NAMESPACE);
    addValues(schema, elem, elemName, parentName);

    if (schema.isSimpleElement(elemName,parentName))
    {
            if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT))
                Log.debug(Geonet.EDITOREXPANDELEMENT,"is simple element");
      return;
    }
    MetadataType type = schema.getTypeInfo(elemType);
        if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT))
            Log.debug(Geonet.EDITOREXPANDELEMENT,"Type = "+type);

    for (int i=0; i<type.getElementCount(); i++) {
      String childQName = type.getElementAt(i);

            if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT))
                Log.debug(Geonet.EDITOREXPANDELEMENT,"- childName = " + childQName);
      if (childQName == null) continue; // schema extensions cause null types; just skip

      String childName   = getUnqualifiedName(childQName);
      String childPrefix = getPrefix(childQName);
      String childNS     = getNamespace(childQName, md, schema);

            if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT)) {
                Log.debug(Geonet.EDITOREXPANDELEMENT,"- name      = " + childName);
                Log.debug(Geonet.EDITOREXPANDELEMENT,"- prefix    = " + childPrefix);
                Log.debug(Geonet.EDITOREXPANDELEMENT,"- namespace = " + childNS);
            }

      List<?> list = md.getChildren(childName, Namespace.getNamespace(childNS));
      if (list.size() == 0 && !(type.isOrType())) {
                if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT))
                    Log.debug(Geonet.EDITOREXPANDELEMENT,"- no children of this type already present");

        Element newElem = createElement(schema, elemName, childQName, childNS, type.getMinCardinAt(i), type.getMaxCardinAt(i));

        if (i == 0insertFirst(md, newElem);
        else {
          String prevQName = type.getElementAt(i-1);
          String prevName = getUnqualifiedName(prevQName);
          String prevNS   = getNamespace(prevQName, md, schema);
          insertLast(md, prevName, prevNS, newElem);
        }
      } else {
                if(Log.isDebugEnabled(Geonet.EDITOREXPANDELEMENT)){
                    Log.debug(Geonet.EDITOREXPANDELEMENT,"- " + list.size() + " children of this type already present");
                    Log.debug(Geonet.EDITOREXPANDELEMENT,"- min cardinality = " + type.getMinCardinAt(i));
                    Log.debug(Geonet.EDITOREXPANDELEMENT,"- max cardinality = " + type.getMaxCardinAt(i));
                }


        for (int j=0; j<list.size(); j++) {
          Element listChild = (Element) list.get(j);
          Element listElem  = listChild.getChild(Edit.RootChild.ELEMENT, Edit.NAMESPACE);
          listElem.setAttribute(new Attribute(Edit.Element.Attr.UUID, listChild.getQualifiedName()+"_"+UUID.randomUUID().toString()));
          listElem.setAttribute(new Attribute(Edit.Element.Attr.MIN, ""+type.getMinCardinAt(i)));
          listElem.setAttribute(new Attribute(Edit.Element.Attr.MAX, ""+type.getMaxCardinAt(i)));

          if (j > 0)
            listElem.setAttribute(new Attribute(Edit.Element.Attr.UP, Edit.Value.TRUE));

          if (j<list.size() -1)
            listElem.setAttribute(new Attribute(Edit.Element.Attr.DOWN, Edit.Value.TRUE));

          if (list.size() > type.getMinCardinAt(i))
            listElem.setAttribute(new Attribute(Edit.Element.Attr.DEL, Edit.Value.TRUE));

          if (j < type.getMaxCardinAt(i)-1)
            listElem.setAttribute(new Attribute(Edit.Element.Attr.ADD, Edit.Value.TRUE));
        }
        if (list.size() < type.getMaxCardinAt(i))
          insertLast(md, childName, childNS, createElement(schema, elemName, childQName, childNS, type.getMinCardinAt(i), type.getMaxCardinAt(i)));
      }
    }
    addAttribs(type, md, schema);
  }

    /**
     * TODO javadoc.
     *
     * @param qname
     * @return
     */
  public String getUnqualifiedName(String qname) {
    int pos = qname.indexOf(':');
    if (pos < 0) return qname;
    else         return qname.substring(pos + 1);
  }

    /**
     * TODO javadoc.
     *
     * @param qname
     * @return
     */
  public String getPrefix(String qname) {
    int pos = qname.indexOf(':');
    if (pos < 0) return "";
    else         return qname.substring(0, pos);
  }

    /**
     * TODO javadoc.
     *
     * @param qname
     * @param md
     * @param schema
     * @return
     */
  public String getNamespace(String qname, Element md, MetadataSchema schema) {
    // check the element first to see whether the namespace is
    // declared locally
    String result = checkNamespaces(qname,md);
    if (result.equals("UNKNOWN")) {

      // find root element, where namespaces *must* be declared
      Element root = md;
      while (root.getParent() != null && root.getParent() instanceof Element) root = (Element)root.getParent();
      result = checkNamespaces(qname,root);
   
      // finally if it isn't on the root element then check the list
      // namespaces we collected as we parsed the schema
      if (result.equals("UNKNOWN")) {
        String prefix = getPrefix(qname);
        if (!prefix.equals("")) {
          result = schema.getNS(prefix);
          if (result == null) result="UNKNOWN";
        } else result="UNKNOWN";
      }
    }
    return result;
  }

    /**
     * TODO javadoc.
     *
     * @param qname
     * @param schema
     * @return
     */
  public String getNamespace(String qname, MetadataSchema schema) {
    // check the list of namespaces we collected as we parsed the schema
    String result;
    String prefix = getPrefix(qname);
    if (!prefix.equals("")) {
      result = schema.getNS(prefix);
      if (result == null) result="UNKNOWN";
    } else result="UNKNOWN";
    return result;
  }

    /**
     * TODO javadoc.
     *
     * @param qname
     * @param md
     * @return
     */
  public String checkNamespaces(String qname, Element md) {
    // get prefix
    String prefix = getPrefix(qname);

    // loop on namespaces to fine the one corresponding to prefix
    Namespace rns = md.getNamespace();
    if (prefix.equals(rns.getPrefix())) return rns.getURI();
        for (Object o : md.getAdditionalNamespaces()) {
            Namespace ns = (Namespace) o;
            if (prefix.equals(ns.getPrefix())) {
                return ns.getURI();
            }
        }
    return "UNKNOWN";
  }

    /**
     * TODO javadoc.
     *
     * @param md
     * @param child
     */
  private void insertFirst(Element md, Element child) {
    Vector<Element> v = new Vector<Element>();
    v.add(child);

    @SuppressWarnings("unchecked")
        List<Element> list = md.getChildren();

        for (Element elem : list) {
            v.add(elem);
        }

    //---

    md.removeContent();

        for (Element aV : v) {
            md.addContent(aV);
        }
  }

    /**
     * TODO javadoc.
     *
     * @param md
     * @param childName
     * @param childNS
     * @param child
     */
  private void insertLast(Element md, String childName, String childNS, Element child) {
    boolean added = false;

    @SuppressWarnings("unchecked")
        List<Element> list = md.getChildren();

    List<Element> v = new ArrayList<Element>();

    for(int i=0; i<list.size(); i++)
    {
      Element el = list.get(i);

      v.add(el);

      if (equal(childName, childNS, el) && !added)
      {
        if (i == list.size() -1)
        {
          v.add(child);
          added = true;
        }
        else
        {
          Element elNext = list.get(i+1);

          if (!equal(el, elNext))
          {
            v.add(child);
            added = true;
          }
        }
      }
    }

    md.removeContent();

        for (Element aV : v) {
            md.addContent(aV);
        }
  }

    /**
     * TODO javadoc.
     *
     * @param childName
     * @param childNS
     * @param el
     * @return
     */
  private boolean equal(String childName, String childNS, Element el) {
    if (Edit.NAMESPACE.getURI().equals(el.getNamespaceURI())) {
            return Edit.RootChild.CHILD.equals(el.getName())
                    && childName.equals(el.getAttributeValue(Edit.ChildElem.Attr.NAME))
                    && childNS.equals(el.getAttributeValue(Edit.ChildElem.Attr.NAMESPACE));
    }
    else
      return childName.equals(el.getName()) && childNS.equals(el.getNamespaceURI());
  }

    /**
     * TODO javadoc.
     *
     * @param el1
     * @param el2
     * @return
     */
  private boolean equal(Element el1, Element el2) {
    String elemNS1 = el1.getNamespaceURI();
    String elemNS2 = el2.getNamespaceURI();

    if (Edit.NAMESPACE.getURI().equals(elemNS1)) {
      if (Edit.NAMESPACE.getURI().equals(elemNS2)) {
        //--- both are geonet:child elements

        if (!Edit.RootChild.CHILD.equals(el1.getName()))
          return false;

        if (!Edit.RootChild.CHILD.equals(el2.getName()))
          return false;

        String name1 = el1.getAttributeValue(Edit.ChildElem.Attr.NAME);
        String name2 = el2.getAttributeValue(Edit.ChildElem.Attr.NAME);

        String ns1 = el1.getAttributeValue(Edit.ChildElem.Attr.NAMESPACE);
        String ns2 = el2.getAttributeValue(Edit.ChildElem.Attr.NAMESPACE);

        return name1.equals(name2) && ns1.equals(ns2);
      }
      else {
        //--- el1 is a geonet:child, el2 is not

        if (!Edit.RootChild.CHILD.equals(el1.getName()))
          return false;

        String name1 = el1.getAttributeValue(Edit.ChildElem.Attr.NAME);
        String ns1   = el1.getAttributeValue(Edit.ChildElem.Attr.NAMESPACE);

        return el2.getName().equals(name1) && el2.getNamespaceURI().equals(ns1);
      }
    }
    else {
      if (Edit.NAMESPACE.getURI().equals(elemNS2)) {
        //--- el2 is a geonet:child, el1 is not

        if (!Edit.RootChild.CHILD.equals(el2.getName()))
          return false;

        String name2 = el2.getAttributeValue(Edit.ChildElem.Attr.NAME);
        String ns2   = el2.getAttributeValue(Edit.ChildElem.Attr.NAMESPACE);

        return el1.getName().equals(name2) && el1.getNamespaceURI().equals(ns2);
      }
      else {
        //--- both not geonet:child elements
        return el1.getName().equals(el2.getName()) && el1.getNamespaceURI().equals(el2.getNamespaceURI());
      }
    }
  }

    /**
     * Returns MetadataType associated with an element.
     *
     * @param mds
     * @param elem
     * @return
     * @throws Exception
     */
  public MetadataType getType(MetadataSchema mds, Element elem) throws Exception {

    String elemName = elem.getQualifiedName();
    String parentName = getParentNameFromChild(elem);

    String elemType = mds.getElementType(elemName,parentName);
    return mds.getTypeInfo(elemType);
  }

    /**
     * Creates a new element for editing - used by Ajax new element addition.
     * @param schema
     * @param child
     * @param parent
     * @return
     * @throws Exception
     */
  public Element createElement(String schema, Element child, Element parent) throws Exception {

    String childQName = child.getQualifiedName();

    MetadataSchema mds = scm.getSchema(schema);
    MetadataType mdt = getType(mds, parent);
   
    int min = -1, max = -1;

    for (int i=0; i<mdt.getElementCount(); i++) {
      if (childQName.equals(mdt.getElementAt(i))) {
        min = mdt.getMinCardinAt(i);
        max = mdt.getMaxCardinAt(i);
      }
    }
    return createElement(mds,parent.getQualifiedName(),child.getQualifiedName(), child.getNamespaceURI(), min, max);
  }

    /**
     * Creates a new element for editing, adding all mandatory subtags.
     *
     * @param schema
     * @param parent
     * @param qname
     * @param childNS
     * @param min
     * @param max
     * @return
     * @throws Exception
     */
  private Element createElement(MetadataSchema schema, String parent, String qname, String childNS, int min, int max) throws Exception {

    Element child = new Element(Edit.RootChild.CHILD, Edit.NAMESPACE);
    SchemaSuggestions mdSugg   = scm.getSchemaSuggestions(schema.getName());
   
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.NAME, getUnqualifiedName(qname)));
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.PREFIX, getPrefix(qname)));
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.NAMESPACE, childNS));
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.UUID, Edit.RootChild.CHILD+"_"+qname+"_"+UUID.randomUUID().toString()));
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.MIN, ""+min));
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.MAX, ""+max));

    String action = "replace"; // js adds new elements in place of this child
    if (!schema.isSimpleElement(qname,parent)) {
      String elemType = schema.getElementType(qname,parent);

      MetadataType type = schema.getTypeInfo(elemType);
      // Choice elements will be added if present in suggestion only.
      boolean useSuggestion = mdSugg.hasSuggestion(qname, type.getElementList());
     
      if (type.isOrType()) {
        // Here we handle elements with potential substitute suggested.
        // In most of the cases, elements have gco:CharacterString as one of the possible substitute.
        // gco:CharacterString is then used as a default substitute to use for those
        // elements. It could be a good idea to have that information in configuration file
        // (eg. like schema-substitute) in order to define the default substitute to use
        // for a type. TODO
        if (type.getElementList().contains("gco:CharacterString") && !useSuggestion) {
                    if(Log.isDebugEnabled(Geonet.EDITOR))
                        Log.debug(Geonet.EDITOR,"OR element having gco:CharacterString substitute and no suggestion: " + qname);

          Element newElem = createElement(schema, qname,
              "gco:CharacterString",
                            "http://www.isotc211.org/2005/gco", 1, 1);
          child.addContent(newElem);
        } else {
          action = "before"; // js adds new elements before this child
          for(int l=0; l<type.getElementCount(); l++) {
            String chElem = type.getElementAt(l);
            if (chElem.contains(Edit.RootChild.CHOICE)) {
              List<String> chElems = recurseOnNestedChoices(schema,chElem,parent);

                            for (String chElem1 : chElems) {
                                chElem = chElem1;
                                if (!useSuggestion
                                        || (mdSugg.isSuggested(qname, chElem))) {
                                    // Add all substitute found in the schema or all suggested if suggestion
                                    createAndAddChoose(child, chElem);
                                }
                            }
            } else {
             
              if (!useSuggestion
                  || (mdSugg.isSuggested(qname, chElem))){
                // Add all substitute found in the schema or all suggested if suggestion
                createAndAddChoose(child,chElem);
              }
            }
          }
        }
      }
    }

    if (max == 1) action = "replace"; // force replace because one only
    child.setAttribute(new Attribute(Edit.ChildElem.Attr.ACTION, action));

    return child;
  }

    /**
     * TODO javadoc.
     *
     * @param schema
     * @param chElem
     * @param parent
     * @return
     * @throws Exception
     */
  private List<String> recurseOnNestedChoices(MetadataSchema schema,String chElem,String parent) throws Exception {
    List<String> chElems = new ArrayList<String>();
    String elemType = schema.getElementType(chElem,parent);
    MetadataType type = schema.getTypeInfo(elemType);
    for(int l=0; l<type.getElementCount(); l++) {
      String subChElem = type.getElementAt(l);
      if (subChElem.contains(Edit.RootChild.CHOICE)) {
        List<String> subChElems = recurseOnNestedChoices(schema,subChElem,chElem);
        chElems.addAll(subChElems);
      }
      else { chElems.add(subChElem); }
    }
    return chElems;
  }

    /**
     * TODO javadoc.
     *
     * @param child
     * @param chType
     */
  private void createAndAddChoose(Element child,String chType) {
    Element choose = new Element(Edit.ChildElem.Child.CHOOSE, Edit.NAMESPACE);
    choose.setAttribute(new Attribute(Edit.Choose.Attr.NAME, chType));
    child.addContent(choose);
  }

    /**
     * TODO javadoc.
     *
     * @param schema
     * @param elem
     * @param name
     * @param parent
     * @throws Exception
     */
  private void addValues(MetadataSchema schema, Element elem, String name, String parent) throws Exception {
    List<String> values = schema.getElementValues(name,parent);
    if (values != null)
            for (Object value : values) {
                Element text = new Element(Edit.Element.Child.TEXT, Edit.NAMESPACE);
                text.setAttribute(Edit.Attribute.Attr.VALUE, (String) value);

                elem.addContent(text);
            }
  }

    /**
     * TODO javadoc.
     *
     * @param type
     * @param md
     * @param schema
     */
  private void addAttribs(MetadataType type, Element md, MetadataSchema schema) {
    for(int i=0; i<type.getAttributeCount(); i++) {
      MetadataAttribute attr = type.getAttributeAt(i);

      Element attribute = new Element(Edit.RootChild.ATTRIBUTE, Edit.NAMESPACE);

      attribute.setAttribute(new Attribute(Edit.Attribute.Attr.NAME, attr.name));
      //--- add default value (if any)

      if (attr.defValue != null) {
        Element def = new Element(Edit.Attribute.Child.DEFAULT, Edit.NAMESPACE);
        def.setAttribute(Edit.Attribute.Attr.VALUE, attr.defValue);

        attribute.addContent(def);
      }

      for(String value : attr.values) {
                Element text = new Element(Edit.Attribute.Child.TEXT, Edit.NAMESPACE);
        text.setAttribute(Edit.Attribute.Attr.VALUE, value);

        attribute.addContent(text);
      }

      //--- handle 'add' and 'del' attribs

      boolean present;
      String uname = getUnqualifiedName(attr.name);
      String ns     = getNamespace(attr.name, md, schema);
      String prefix = getPrefix(attr.name);
      if (!prefix.equals("")) {
        present = (md.getAttributeValue(uname,Namespace.getNamespace(prefix,ns)) != null);
        if (!present && attr.required && (attr.defValue != null)) { // Add it
          md.setAttribute(new Attribute(uname,attr.defValue,Namespace.getNamespace(prefix,ns)));
        }
      } else {
        present = (md.getAttributeValue(attr.name) != null);
        if (!present && attr.required && (attr.defValue != null)) { // Add it
          md.setAttribute(new Attribute(attr.name,attr.defValue));
        }
      }

      if (!present)
        attribute.setAttribute(new Attribute(Edit.Attribute.Attr.ADD, Edit.Value.TRUE));

      else if (!attr.required)
        attribute.setAttribute(new Attribute(Edit.Attribute.Attr.DEL, Edit.Value.TRUE));

      md.addContent(attribute);
    }
  }

  /**
     * Adds missing namespace (ie. GML) to XML inputs. It should be done by the client side
     * but add a check in here.
     *
     * @param fragment     The fragment to be checked and processed.
     *
     * @return         The updated fragment.
     */
    public static String addNamespaceToFragment(String fragment) {
        //add the gml namespace if its missing
        if (fragment.contains("<gml:") && !fragment.contains("xmlns:gml=\"")) {
            if(Log.isDebugEnabled(Geonet.EDITOR))
                Log.debug(Geonet.EDITOR, "  Add missing GML namespace.");
          fragment = fragment.replaceFirst("<gml:([^ >]+)", "<gml:$1 xmlns:gml=\"http://www.opengis.net/gml\"");
        }
      return fragment;
    }

  // -- The following methods are used by services that use metadata-edit-embedded so the
  // -- classes know which element to transform
  /**
   * Tag the element so the metaata-edit-embedded.xsl know which element is the element for display
   */
    public static void tagForDisplay(Element elem) {
        elem.setAttribute("addedObj","true", Edit.NAMESPACE);
    }
    /**
     * Remove the tag element so the tag does not stay in the actual metadata.
     */
    public static void removeDisplayTag(Element elem) {
        elem.removeAttribute("addedObj", Edit.NAMESPACE);
    }

}
TOP

Related Classes of org.fao.geonet.kernel.EditLib$SelectResult

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.