Java tutorial
//============================================================================== //=== //=== 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 java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.Vector; import jeeves.utils.Log; import jeeves.utils.Xml; 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.apache.commons.collections.CollectionUtils; import org.fao.geonet.constants.Edit; import org.fao.geonet.constants.Geonet; import org.fao.geonet.constants.Geonet.Namespaces; 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.kernel.search.spatial.Pair; 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 com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; /** * 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" + jeeves.utils.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" + jeeves.utils.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 List listAtts = md.getAttributes(); for (int i = 0; i < listAtts.size(); i++) { Attribute attr = (Attribute) listAtts.get(i); if (Edit.NAMESPACE.getPrefix().equals(attr.getNamespacePrefix())) { attr.detach(); i--; } } //--- purge geonet: children List list = md.getChildren(); for (int i = 0; i < list.size(); i++) { Element child = (Element) 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 List list = md.getChildren(); for (Object aList : list) { Element child = (Element) aList; if (!Edit.NAMESPACE.getPrefix().equals(child.getNamespacePrefix())) { child = findElement(child, ref); if (child != null) { return child; } } } return null; } /** * TODO javadoc. * * @param schema * @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); } List childS = el.getChildren(); if (childS.size() > 0) { Element elChildS = (Element) 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); } } //-------------------------------------------------------------------------- //--- //--- Private methods //--- //-------------------------------------------------------------------------- /** * TODO javadoc. * * @param el * @param qname * @return */ private List<Element> getChildren(Element el, String qname) { Vector<Element> result = new Vector<Element>(); List children = el.getChildren(); for (Object aChildren : children) { Element child = (Element) aChildren; 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 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", Namespace.getNamespace("gco", schema.getNS("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", Namespace.getNamespace("gco", schema.getNS("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 List childs = md.getChildren(); for (Object child : childs) { expandElements(schema, (Element) 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>(); List chChilds = md.getChildren(); for (Object chChild1 : chChilds) { Element chChild = (Element) chChild1; 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>(); List childs = md.getContent(); for (Object 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; List list = md.getChildren(); for (Object aList : list) { Element child = (Element) aList; 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; Iterator mdIt = md.getDescendants(new ElementFilter("element")); while (mdIt.hasNext()) { Element elem = (Element) 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); List list = md.getChildren(); for (Object aList : list) { Element child = (Element) aList; 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 == 0) insertFirst(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); List list = md.getChildren(); for (Object aList : list) { v.add((Element) aList); } //--- 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; List list = md.getChildren(); Vector<Element> v = new Vector<Element>(); Set<String> hs = new HashSet<String>(); for (int i = 0; i < list.size(); i++) { Element el = (Element) list.get(i); addElToVector(el, v, hs); if (equal(childName, childNS, el) && !added) { if (i == list.size() - 1) { addElToVector(child, v, hs); added = true; } else { Element elNext = (Element) list.get(i + 1); if (!equal(el, elNext)) { addElToVector(child, v, hs); added = true; } } } } md.removeContent(); for (Element aV : v) { md.addContent(aV); } } private void addElToVector(Element el, Vector<Element> v, Set<String> hs) { if (Edit.NAMESPACE.getURI().equals(el.getNamespaceURI()) && Edit.RootChild.CHILD.equals(el.getName())) { String childName = el.getAttributeValue(Edit.ChildElem.Attr.NAME); if (hs.contains(childName)) return; hs.add(childName); } v.add(el); } /** * 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", schema.getNS("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 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 (int j = 0; j < attr.values.size(); j++) { Element text = new Element(Edit.Attribute.Child.TEXT, Edit.NAMESPACE); text.setAttribute(Edit.Attribute.Attr.VALUE, (String) attr.values.get(j)); 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); } } // -- 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); } 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; } /** * 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; } /** * 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> * <gmd:MD_Metadata> * <gmd:fileIdentifier></gmd:fileIdentifier> * <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: <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(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(Element newValue, Element propEl) { if (newValue.getName().equals(SpecialUpdateTags.REPLACE) || newValue.getName().equals(SpecialUpdateTags.ADD)) { if (newValue.getName().equals(SpecialUpdateTags.REPLACE)) { 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)); } propEl.addContent(child.detach()); } } 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("/")); Pair<Element, String> result = findLongestMatch(metadataRecord, metadataRecord, 0, metadataSchema, xpathParts.size() / 2, 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(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 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; } } /** * Special tags for updating metadata element by xpath. */ public static interface SpecialUpdateTags { String REPLACE = "gn_replace"; String ADD = "gn_add"; } 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); } private static final Joiner SLASH_STRING_JOINER = Joiner.on('/'); private Pair<Element, String> findLongestMatch(final Element metadataRecord, final Element bestMatch, final int indexOfBestMatch, final MetadataSchema metadataSchema, final int nextIndex, final List<String> xpathPropertyParts) { // 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 String currentXPath = SLASH_STRING_JOINER.join(xpathPropertyParts.subList(0, nextIndex)); final SelectResult found = trySelectNode(metadataRecord, metadataSchema, currentXPath); 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); } else if (!found.error) { int newNextIndex = indexOfBestMatch + ((nextIndex - indexOfBestMatch) / 2); if (newNextIndex == indexOfBestMatch) { String xpath = SLASH_STRING_JOINER .join(xpathPropertyParts.subList(indexOfBestMatch, xpathPropertyParts.size())); return Pair.read(bestMatch, xpath); } else { return findLongestMatch(metadataRecord, bestMatch, indexOfBestMatch, metadataSchema, newNextIndex, xpathPropertyParts); } } else { int newNextIndex = nextIndex + 1; return findLongestMatch(metadataRecord, bestMatch, indexOfBestMatch, metadataSchema, newNextIndex, xpathPropertyParts); } } } }