/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.esri.gpt.catalog.schema.indexable; import com.esri.gpt.catalog.discovery.Discoverable; import com.esri.gpt.catalog.discovery.PropertyMeaning; import com.esri.gpt.catalog.discovery.PropertyValueType; import com.esri.gpt.catalog.schema.CfgContext; import com.esri.gpt.catalog.schema.EsriTags; import com.esri.gpt.catalog.schema.Schema; import com.esri.gpt.catalog.search.ResourceIdentifier; import com.esri.gpt.framework.geometry.Envelope; import com.esri.gpt.framework.util.Val; import com.esri.gpt.framework.xml.DomUtil; import java.util.ArrayList; import java.util.List; import javax.xml.namespace.QName; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * An indexable property associated with a metadata schema. */ public class IndexableProperty { /** instance variables ====================================================== */ private List<IndexableProperty> children; private List<String> evaluatedValues; private String meaningName; private String xpathExpression; private String xpathType; /** constructors ============================================================ */ /** Default constructor. */ public IndexableProperty() {} /** * Construct by duplicating an existing object. * @param objectToDuplicate the object to duplicate */ public IndexableProperty(IndexableProperty objectToDuplicate) { if (objectToDuplicate == null) { } else { this.setMeaningName(objectToDuplicate.getMeaningName()); this.setXPathExpression(objectToDuplicate.getXPathExpression()); this.setXPathType(objectToDuplicate.getXPathType()); if (objectToDuplicate.getChildren() != null) { if (this.getChildren() == null) { this.setChildren(new ArrayList<IndexableProperty>()); } for (IndexableProperty child: objectToDuplicate.getChildren()) { this.getChildren().add(new IndexableProperty(child)); } } } } /** properties ============================================================== */ /** * Gets the child properties. * @return the child properties */ public List<IndexableProperty> getChildren() { return this.children; } /** * Sets the child properties. * @param children the child properties */ public void setChildren(List<IndexableProperty> children) { this.children = children; } /** * Gets the first evaluated value. * @return the first evaluated value (can be null) */ public String getEvaluatedValue() { if (this.getEvaluatedValues() != null) { for (String value: this.getEvaluatedValues()) { value = Val.chkStr(value); if (value.length() > 0) { return value; } } } return null; } /** * Gets the evaluated values. * @return the evaluated values */ public List<String> getEvaluatedValues() { return this.evaluatedValues; } /** * Sets the evaluated values. * @param values the evaluated values */ public void setEvaluatedValues(List<String> values) { this.evaluatedValues = values; } /** * Gets the meaning name. * @return the meaning name */ public String getMeaningName() { return this.meaningName; } /** * Sets the meaning name. * @param name the meaning name */ public void setMeaningName(String name) { this.meaningName = name; } /** * Gets the XPath expression. * @return the XPath expression */ public String getXPathExpression() { return this.xpathExpression; } /** * Sets the XPath expression. * @param expression the XPath expression */ public void setXPathExpression(String expression) { this.xpathExpression = expression; } /** * Gets the XPath expression result type. * <br/>BOOLEAN NODE NODESET NUMBER STRING * @return the XPath expression result type */ public String getXPathType() { return this.xpathType; } /** * Sets the XPath expression result type. * <br/>BOOLEAN NODE NODESET NUMBER STRING * @param xpathType the expression result type */ public void setXPathType(String xpathType) { this.xpathType = xpathType; } /** methods ================================================================= */ /** * Adds an evaluated value to the collection. * @param context the active indexable context * @param meaning the associated property meaning * @param value the evaluated value */ protected void addValue(IndexableContext context, PropertyMeaning meaning, String value) { value = Val.chkStr(value); if (value.length() > 0) { if (this.getEvaluatedValues() == null) { this.setEvaluatedValues(new ArrayList<String>()); } this.getEvaluatedValues().add(value); } } /** * Configures the object based upon a node loaded from a * schema configuration XML. * <p/> * The following attributes are configured: * <br/>meaning xpathType xpath * <p/> * The following child nodes are configured: * <br/>property * @param context the configuration context * @param node the configuration node * @param attributes the attributes of the configuration node */ public void configure(CfgContext context, Node node, NamedNodeMap attributes) { String mName = DomUtil.getAttributeValue(attributes,"meaning"); if (Val.chkStr(mName).equalsIgnoreCase("anytext")) mName = "body"; if (Val.chkStr(mName).equalsIgnoreCase("resource.type")) mName = "contentType"; this.setMeaningName(mName); this.setXPathExpression(Val.chkStr(DomUtil.getAttributeValue(attributes,"xpath"))); this.setXPathType(Val.chkStr(DomUtil.getAttributeValue(attributes,"xpathType"))); // loop through the children NodeList nl = node.getChildNodes(); for (int i=0;i<nl.getLength();i++) { Node nd = nl.item(i); if (nd.getNodeType() == Node.ELEMENT_NODE) { String nodeName = Val.chkStr(nd.getNodeName()); if (nodeName.equalsIgnoreCase("property")) { if (this.getChildren() == null) { this.setChildren(new ArrayList<IndexableProperty>()); } IndexableProperty child = new IndexableProperty(); child.configure(context,nd,nd.getAttributes()); this.getChildren().add(child); } } } } /** * Evaluates the property based upon the supplied metadata document. * @param schema the schema being evaluated * @param context the active indexable context * @param dom the metadata document * @param parent the metadata document node the is actively being processed (can be null) * @param xpath an XPath object configured with an appropriate * Namespace context for the schema * @throws XPathExpressionException if an evaluation expression fails */ public void evaluate(Schema schema, IndexableContext context, Document dom, Node parent, XPath xpath) throws XPathExpressionException { // initialize String mName = Val.chkStr(this.getMeaningName()); String xpExpr = Val.chkStr(this.getXPathExpression()); String xpType = Val.chkStr(this.getXPathType()); boolean hasChildren = (this.getChildren() != null) && (this.getChildren().size() > 0); // determine the meaning PropertyMeaning meaning = null; if (mName.length() > 0) { meaning = context.getPropertyMeanings().get(mName); if (meaning == null) { Discoverable discoverable = context.getPropertyMeanings().getAllAliased().get(mName); if (discoverable != null) { meaning = discoverable.getMeaning(); } else { // TODO warn if the meaning is null } } } // loop through children if no XPath expression was supplied if (xpExpr.length() == 0) { if (hasChildren) { for (IndexableProperty child: this.getChildren()) { child.evaluate(schema,context,dom,null,xpath); } } } else { // determine the starting context for the expression Object startItem = dom; if (parent != null) { if (!xpExpr.startsWith("/")) { startItem = parent; } } // determine the return type, BOOLEAN NODE NODESET NUMBER STRING QName returnType = null; if (xpType.equalsIgnoreCase("BOOLEAN")) { returnType = XPathConstants.BOOLEAN; } else if (xpType.equalsIgnoreCase("NODE")) { returnType = XPathConstants.NODE; } else if (xpType.equalsIgnoreCase("NODESET") || xpType.equalsIgnoreCase("NODELIST") || xpType.equalsIgnoreCase("LIST")) { returnType = XPathConstants.NODESET; } else if (xpType.equalsIgnoreCase("NUMBER")) { returnType = XPathConstants.NUMBER; } else if (xpType.equalsIgnoreCase("STRING")) { returnType = XPathConstants.STRING; } else { if (hasChildren) { returnType = XPathConstants.NODESET; } else { if (xpExpr.toLowerCase().endsWith("text()")) { returnType = XPathConstants.STRING; } else { returnType = XPathConstants.NODESET; } } } // evaluate the expression, process the result Object result = xpath.evaluate(xpExpr,startItem,returnType); if (result != null) { if (returnType.equals(XPathConstants.STRING)) { String value = (String)result; this.addValue(context,meaning,value); } else if (returnType.equals(XPathConstants.NUMBER)) { String value = ""+((Number)result); this.addValue(context,meaning,value); } else if (returnType.equals(XPathConstants.BOOLEAN)) { String value = ""+((Boolean)result); this.addValue(context,meaning,value); } else if (returnType.equals(XPathConstants.NODE)) { Node nd = (Node)result; String value = this.getTextContent(nd,true); this.addValue(context,meaning,value); if (hasChildren) { for (IndexableProperty child: this.getChildren()) { child.evaluate(schema,context,dom,nd,xpath); } } } else if (returnType.equals(XPathConstants.NODESET)) { NodeList nl = (NodeList)result; for (int i=0;i<nl.getLength();i++) { Node nd = nl.item(i); String value = this.getTextContent(nd,true); this.addValue(context,meaning,value); if (hasChildren) { for (IndexableProperty child: this.getChildren()) { child.evaluate(schema,context,dom,nd,xpath); } } } } } } } /** * Gets the text associated with a node. * @param node the subject node * @param first if true, get the first text node associated with an element node * @return the text (can be null) */ protected String getTextContent(Node node, boolean first) { String text = null; if (node.getNodeType() == Node.ATTRIBUTE_NODE ) { text = node.getNodeValue(); } else if (node.getNodeType() == Node.TEXT_NODE) { text = node.getNodeValue(); } else if (node.getNodeType() == Node.CDATA_SECTION_NODE) { text = node.getNodeValue(); } else if (node.getNodeType() == Node.ELEMENT_NODE) { if (first) { NodeList nl = node.getChildNodes(); for (int i=0;i<nl.getLength();i++) { Node nd = nl.item(i); if (nd.getNodeType() == Node.TEXT_NODE) { text = nd.getNodeValue(); } else { break; } } } else { text = node.getTextContent(); } } if (text != null) { text = Val.chkStr(text); if (text.length() == 0) { text = null; } } return text; } /** * Resolves an evaluated property. * @param schema the schema being evaluated * @param context the active indexable context * @param parent the parent property */ public void resolve(Schema schema, IndexableContext context, IndexableProperty parent) { // initialize String mName = Val.chkStr(this.getMeaningName()); boolean hasChildren = (this.getChildren() != null) && (this.getChildren().size() > 0); List<String> values = this.getEvaluatedValues(); // determine the meaning PropertyMeaning meaning = null; if (mName.length() > 0) { meaning = context.getPropertyMeanings().get(mName); if (meaning == null) { Discoverable discoverable = context.getPropertyMeanings().getAllAliased().get(mName); if (discoverable != null) { meaning = discoverable.getMeaning(); } else { // TODO warn if the meaning is null } } } // handle geometries if ((meaning != null) && (meaning.getValueType() != null)) { if (meaning.getValueType().equals(PropertyValueType.GEOMETRY)) { if (hasChildren) { hasChildren = false; Envelope envelope = new Envelope(); for (IndexableProperty child: this.getChildren()) { String mn = Val.chkStr(child.getMeaningName()); String ev = Val.chkStr(child.getEvaluatedValue()); if (ev.length() > 0) { if (mn.equalsIgnoreCase("envelope.west")) { envelope.setMinX(ev); } else if (mn.equalsIgnoreCase("envelope.south")) { envelope.setMinY(ev); } else if (mn.equalsIgnoreCase("envelope.east")) { envelope.setMaxX(ev); } else if (mn.equalsIgnoreCase("envelope.north")) { envelope.setMaxY(ev); } else if (mn.equalsIgnoreCase("envelope.lowerCorner")) { String[] pt = ev.split(" "); if (pt.length == 2) { envelope.setMinX(pt[0]); envelope.setMinY(pt[1]); } } else if (mn.equalsIgnoreCase("envelope.upperCorner")) { String[] pt = ev.split(" "); if (pt.length == 2) { envelope.setMaxX(pt[0]); envelope.setMaxY(pt[1]); } } } } if (!envelope.isEmpty()) { context.addStoreableValue(meaning,envelope); } } return; } } // ArcIMS content type if ((meaning != null) && (values != null) && (values.size() > 0)) { if (Val.chkStr(meaning.getName()).equalsIgnoreCase("contentType")) { ArrayList<String> al = new ArrayList<String>(); boolean changed = false; ResourceIdentifier ri = context.ensureResourceIdentifier(); for (String value: values) { String s = Val.chkStr(ri.guessArcIMSContentTypeFromResourceType(value)); if ((s != null) && (s.length() > 0)) { al.add(s); changed = true; } else { // TODO: allow non-enumerated values? al.add(value); } } if (changed) { this.setEvaluatedValues(al); values = this.getEvaluatedValues(); } } } // data themes (ISO MD_TopicCategoryCode) if ((meaning != null) && (values != null) && (values.size() > 0)) { if (Val.chkStr(meaning.getName()).equalsIgnoreCase("dataTheme")) { ArrayList<String> al = new ArrayList<String>(); boolean changed = false; for (String value: values) { String s = EsriTags.DATA_THEME_CODES.get(value); if ((s != null) && (s.length() > 0)) { al.add(s); changed = true; } else { // TODO: allow non-enumerated values? al.add(value); } } if (changed) { this.setEvaluatedValues(al); values = this.getEvaluatedValues(); } } } // process the evaluated values if (this.getEvaluatedValues() != null) { boolean isSingleValued = false; if (isSingleValued) { String singleValue = Val.chkStr(this.getEvaluatedValue()); if (singleValue.length() == 0) { this.setEvaluatedValues(null); } else { this.getEvaluatedValues().clear(); this.addValue(context,meaning,singleValue); } } else { context.addStorableValues(meaning,this.getEvaluatedValues().toArray(new String[0])); } } // process the children if (hasChildren) { for (IndexableProperty child: this.getChildren()) { child.resolve(schema,context,this); } } } }