All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.movielabs.mddflib.util.xml.StructureValidation Maven / Gradle / Ivy

/**
 * Copyright (c) 2017 MovieLabs

 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.movielabs.mddflib.util.xml;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jdom2.Attribute;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;

import com.movielabs.mddf.MddfContext;
import com.movielabs.mddf.MddfContext.FILE_FMT;
import com.movielabs.mddflib.logging.IssueLogger;
import com.movielabs.mddflib.logging.LogEntryFolder;
import com.movielabs.mddflib.logging.LogMgmt;
import com.movielabs.mddflib.logging.LogReference;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

/**
 * A 'helper' class that supports the validation of an XML file against a set of
 * structural requirements not specified via an XSD.
 * 

* The MDDF specifications define many requirements for specific use cases as * well as recommended 'best practices'. These requirements and recommendations * specify relationships between XML elements that are not defined via the XSD. * In order to support validation they are instead formally specified via a * JSON-formatted structure definition file. The * StructureValidation class provides the functions that can interpret * the requirements and test an XML file for compliance. *

*

Semantics and Syntax:

*

* The semantics of JSON a structure definition is as follows: * *

 * {
 * 	  USAGE:
 * 		 {
 * 			"targetPath" : XPATH (optional)
 * 			"constraint" :
 * 			[
 * 				{
 				  "VARIABLE ID": XPATH, (optional)
				  "min": INTEGER, (optional)
				  "max": INTEGER, (optional)
				  "xpath": (XPATH | ARRAY[XPATH]),
				  "filter" : (optional)
				  	{
				  		 "values":  ARRAY[ STRING],
				  		 "negated" : ("true" | "false") (optional)
				  	}
				  "severity": ("Fatal" | "Error" | "Warning" | "Notice"),
				  "msg" : STRING,  (optional)
				  "docRef": STRING (optional)
 * 				}
 * 			]
 * 			"children": {.... } (optional)
 * 		 }
 * }
 * 
* * where: *
    *
  • USAGE is a string defining the key used by a validator to retrieve * the requirements appropriate to a use case.
  • *
  • targetPath: indicates the element(s) that provide the evaluation * context for the constraint xpath when invoking the * validateDocStructure() method. If not specified, a target element * must be provided when invoking the validateConstraint() method on a * target element that has been identified by other means.
  • *
  • constraint: one or more structural requirements associated with * the targeted element. *
      *
    • VARIABLE-ID: Variables are denoted by a "$" followed by * an ID (e.g., $FOO) and are assigned a value via a XPath.
    • *
    • xpath: defines one or more xpaths relative to the target element * that, when evaluated, the number of matching elements satisfy the min/max * cardinality constraints. If multiple xpaths are listed, the total number of * elements (or attributes) returned when each is separately evaluated must * satisfy the constraint.
    • *
    • filter: supplemental condition applied to results returned when * XPath is evaluated.
    • *
    • min: minimum number of matching objects that should be found * when evaluating the xpath(s). [OPTIONAL, default is 0]
    • *
    • max: maximum number of matching objects that should be found * when evaluating the xpath(s). [OPTIONAL, default is unlimited]
    • *
    • severity: must match one of the LogMgmt.level values *
    • *
    • msg: text to use for a log entry if the constraint is not met. * If not provided, a generic message is used.
    • *
    • docRef: is a value usable by LogReference that will * indicate any reference material documenting the requirement.
    • *
    *
  • *
*

* *

XPaths:

*

* An XPATH is defined using the standard XPath syntax with two * modifications. *

*

Indicating Namespaces:

*

* Namespaces are indicated using a variable with the name of an MDDF schema. * The appropriate namespace prefixes will be inserted by the software. * Supported namespaces are: *

*
    *
  • {avail}
  • *
  • {mdmec}
  • *
  • {manifest}
  • *
  • {md}
  • *
* For example: * *
 * 
		"POEST": 
		{
			"targetPath": ".//{avail}LicenseType[.='POEST']",
			"constraint": 
			[
				{ 
					"min": "1",
					"max": "1",
					"xpath": "../{avail}Term[@termName='SuppressionLiftDate']",
					"severity": "Error",
					"msg": "One SuppressionLiftDate is required for LicenseType 'POEST'"
				}
			]
		},
		
		"WorkType-Episode": 
		{
			"constraint": 
			[
				{
					"docRef": "AVAIL:avail00n",
					"min": "1",
					"max": "2",
					"xpath": 
					[
						"{avail}EpisodeMetadata/{avail}AltIdentifier",
						"{avail}EpisodeMetadata/{avail}EditEIDR-URN"
					]
				}
			]
		},
 *
 * 
* *

Support for MEC File Usage:

*

* A Manifest file may provide metadata in one of two ways: either * internally via a <BasicMetadata> element or * externally via a <ContainerReference> pointing to a MEC * file. Validating a constraint on Metadata may, therefore, require specifying * two different XPaths to cover both possible situations. *

*

* To handle this type of situation, a JSONArray with both paths is used. The * xpath that is to be applied when a MEC file is used will be prefixed with the * keyword {$$MEC}. For example: * *

 *  
"$CID": "./{manifest}ContentID",
"max": "0",
"severity": "Error",
"xpath": 
[
		"//{manifest}BasicMetadata[(descendant::{md}ArtReference) and (@ContentID = {$CID}) ]",						
		"{$$MEC}//{mdmec}Basic[(descendant::{md}ArtReference) and (@ContentID = {$CID}) ]"
],
 * 
 * 
*

* *

Filters:

*

* A 'Filter' may be used to supplement the matching criteria specified by the * XPaths. This is used when XPath criteria are insufficient, or too unwieldy, * to fully implement a constraint. Filters are (for now) defined as a set of * values and an optional 'negated' flag set to true or false. *

*

* For example, the following filter would identify Audio assets that * have a ChannelMapping that is inconsistent with the number of actual * channels: *

* *
		"MultiChannel": 
		{
			"targetPath": ".//{md}Channels[. < 1]",
			"constraint": 
			[
				{
					"xpath": 
					[
						"../{md}Encoding/{md}ChannelMapping"
					],

					"filter": 
					{
						"values": 
						[
							"Mono",
							"Left",
							"Center",
							"Right"
						],

						"negated": "false"
					},

					"max": "0",
					"severity": "Error",
					"msg": "ChannelMapping is not valid for multi-channel Audio"
				}
			]
		}
 * 
* *

Variables:

* * Variables are denoted by a "$" followed by an ID (e.g., $FOO) and * are assigned a value via a XPath. For example:
 *   "$CID": "./@ContentID"
 * 
They may be included in the constraint's XPath by using enclosing * the variable name in curly brackets. For example:
 *   "xpath": 
 *      [
 *         "../..//{manifest}Experience[@ExperienceID={$CID}]/{manifest}PictureGroupID"
 *      ],
 * 
*

* It is possible that the XPath used to determine a variable's value will * evaluate to a null. Any constraint XPath that references a null * variable will be skipped when evaluating the constraint criteria. *

* * *

Nested Requirements:

*

* Requirements can be defined in a way that reflects the nested structure of * the underlying XML. For example, assume the XSD specifies the following * syntax: *

*
    *
  • Foo: *
      *
    • Bar: *
        *
      • Flavor
      • *
      • Weight
      • *
      • Calories
      • *
      *
    • *
    *
  • *
* We wish to specify that if Flavor is present then Calories must also * be specified. There are two options: *
    *
  • use a targetPath pointing to Foo and constraints with * xpath that will resolve when Foo is the root for xpath * evaluation (e.g., "xpath": "./Foo/Calories")
  • *
  • the targetPath points to Foo, followed by a * child constraint with the targetPath pointing to Bar * and an xpath that will resolve when Bar is the root (e.g., * "xpath": "./Calories")
  • *
*

* Either option will work but in some situations one or the other may be more * efficient to evaluate or easier to write. *

*

Usage

*

* Validation modules should determine the appropriate JSON resource file based * on the type and version of the MDDF file. Requirements may then be retrieved * and individually checked using the USAGE key or the entire collection may be * iterated thru. * * @author L. Levin, Critical Architectures LLC * */ public class StructureValidation { private static final String KEY_MEC_REF = "{$$MEC}"; protected IssueLogger logger; protected String logMsgSrcId; /** * @param logger * @param logMsgSrcId */ public StructureValidation(IssueLogger logger, String logMsgSrcId) { this.logger = logger; } /** * Check to see if the XML satisfies the specified requirement. The * rootEL may either be the root of an entire document or the root of a * DOM tree forming a sub-tree within the overall document. *

* Any errors or warnings detected will be reported via the logger. The return * status will indicate to caller if an error was detected. *

* * @param rootEl * @param rqmt * @return */ // public boolean validateDocStructure(Element rootEl, JSONObject rqmt) { // return validateDocStructure(rootEl, rqmt, null); // } /** * Check to see if the XML satisfies the specified requirement. The * rootEL may either be the root of an entire document or the root of a * DOM tree forming a sub-tree within the overall document. A requirement's * constraint may include an XPath that is to be applied to * the supporting MEC files. *

* Any errors or warnings detected will be reported via the logger. The return * status will indicate to caller if an error was detected. *

* * @param rootEl * @param rqmt * @param primaryfTarget * @param supportingMECs (may be empty) * @return */ public boolean validateDocStructure(Element rootEl, JSONObject rqmt, MddfTarget primaryfTarget, Map supportingMECs) { String targetPath = rqmt.getString("targetPath"); ArrayList rootElList = new ArrayList(); Map contextMap = new HashMap(); boolean MEC_Target = false; if (targetPath.startsWith(KEY_MEC_REF)) { /* * Evaluation of 'targetPath' requires the xPath to be applied to a supporting * MEC file rather than the primary (i.e., Manifest) file. This is NOT the same * as evaluating an associated constraint xPath against the MEC. The two are * Independent of each other. */ MEC_Target = true; targetPath = targetPath.replace(KEY_MEC_REF, ""); if ((supportingMECs != null) && (!supportingMECs.isEmpty())) { for (MddfTarget targetSrc : supportingMECs.keySet()) { Element mecRootEl = targetSrc.getXmlDoc().getRootElement(); rootElList.add(mecRootEl); contextMap.put(mecRootEl, targetSrc); } } } else { // no need to mod the targetPath. rootElList.add(rootEl); contextMap.put(rootEl, primaryfTarget); } boolean allOk = true; for (Element nextDocRoot : rootElList) { FILE_FMT mddfFmt = MddfContext.identifyMddfFormat(nextDocRoot); XPathExpression xpExp = resolveXPath(targetPath, null, mddfFmt); List targetElList = (List) xpExp.evaluate(nextDocRoot); boolean isOk = true; if (rqmt.containsKey("constraint")) { JSONArray constraintSet = rqmt.getJSONArray("constraint"); /* * the 'contextEl' provides the focal point for the evaluation of a constraint. * They are located via the 'targetPath' in each JSON requirement spec. The * contextEl will be used to resolve any variables as well as for providing the * context for any error msgs added to the log. It *may* also provide the base * point for the evaluation of the XPaths in the requirement's 'constraint'. */ for (Element nextContextEl : targetElList) { for (int i = 0; i < constraintSet.size(); i++) { JSONObject constraint = constraintSet.getJSONObject(i); /* * variables are always resolved using the 'nextTargetEl'. */ Map varMap = resolveVariables(nextContextEl, constraint); /* * the 'contextEl' will be used as the starting point when evaluating the * constraint's xPath(s). If the 'nextContextEl' is in the main doc (i.e, * MEC_Target == false) then targetEl = nextContextEl. Otherwise the targetEl * should be set to the 'rootEl' passed as a calling argument to this method. */ Element targetEl; if (MEC_Target) { targetEl = rootEl; } else { targetEl = nextContextEl; } MddfTarget mddfContext = contextMap.get(nextDocRoot); LogEntryFolder logFolder = null; if (supportingMECs != null) { logFolder = supportingMECs.get(mddfContext); } isOk = evaluateConstraint(targetEl, constraint, nextContextEl, varMap, logFolder, supportingMECs) && isOk; } } } /* are there nested constraints?? */ if (rqmt.containsKey("children")) { JSONObject embeddedReqmts = rqmt.getJSONObject("children"); for (Element nextTargetEl : targetElList) { Iterator embeddedKeys = embeddedReqmts.keys(); while (embeddedKeys.hasNext()) { String key = embeddedKeys.next(); JSONObject childRqmtSpec = embeddedReqmts.getJSONObject(key); // NOTE: This block of code requires a 'targetPath' be defined if (childRqmtSpec.has("targetPath")) { // Recursive descent... isOk = validateDocStructure(nextTargetEl, childRqmtSpec, primaryfTarget, supportingMECs) && isOk; } } } } allOk = allOk && isOk; } return allOk; } /** * Evaluate a constraint in the context of a specific Element. This means * descendant Elements are not considered. *

* Note: This method is exposed as public to support unit * testing. Use of validateDocStructure() is preferred. * * @param target * @param constraint * @param contextEl * @param varMap [OPTIONAL] * @param targetFile * @param supportingMECs [OPTIONAL] * @return */ public boolean evaluateConstraint(Element target, JSONObject constraint, Element contextEl, Map varMap, LogEntryFolder logFolder, Map supportingMECs) { boolean passes = true; if (varMap == null) { varMap = resolveVariables(target, constraint); } int min = constraint.optInt("min", 0); int max = constraint.optInt("max", -1); String severity = constraint.optString("severity", "Error"); int logLevel = LogMgmt.text2Level(severity); String docRef = constraint.optString("docRef"); Object xpaths = constraint.opt("xpath"); List> xpeList = new ArrayList>(); List externalPathList = new ArrayList(); String targetList = ""; // for use if error msg is required String[] xpParts = null; if (xpaths instanceof String) { String xpathDef = (String) xpaths; if (xpathDef.startsWith(KEY_MEC_REF)) { xpathDef = xpathDef.replace(KEY_MEC_REF, ""); externalPathList.add(xpathDef); } else { xpeList.add(resolveXPath(xpathDef, varMap, target)); } xpParts = xpathDef.split("\\["); targetList = xpParts[0]; } else if (xpaths instanceof JSONArray) { JSONArray xpArray = (JSONArray) xpaths; for (int i = 0; i < xpArray.size(); i++) { String xpathDef = xpArray.getString(i); if (xpathDef.startsWith(KEY_MEC_REF)) { xpathDef = xpathDef.replace(KEY_MEC_REF, ""); externalPathList.add(xpathDef); } else { xpeList.add(resolveXPath(xpathDef, varMap, target)); } xpParts = xpathDef.split("\\["); if (i < 1) { targetList = xpParts[0]; } else if (i == (xpArray.size() - 1)) { targetList = targetList + ", or " + xpParts[0]; } else { targetList = targetList + ", " + xpParts[0]; } } } // reformat targetList for use in log msgs.... targetList = targetList.replaceAll("\\{\\w+\\}", ""); /* Has an OPTIONAL filter been included with the constraint? */ JSONObject filterDef = constraint.optJSONObject("filter"); List matchedElList = new ArrayList(); /* * The xpeLlist contains constraints that are based on an XPath that should be * applied to the SAME file that contains the 'target' element (i.e., the * primary MDDF file being evaluated). */ for (int i = 0; i < xpeList.size(); i++) { XPathExpression xpExp = (XPathExpression) xpeList.get(i); List nextElList = xpExp.evaluate(target); if (filterDef != null) { nextElList = applyFilter(nextElList, filterDef); } matchedElList.addAll(nextElList); } /* * The externalPathList contains constraints that are based on an XPath that * should be applied to a MEC file providing Metadata in support of the primary * MDDF file being evaluated). This list contains Strings rather than resolved * xpath expressions (a.k.a 'xpe'). The reason for this is that the xpath can * not be resolved to an xpe without knowing the namespaces used by the MEC file * it is being applied to. */ if ((supportingMECs != null) && (!supportingMECs.isEmpty())) { for (int i = 0; i < externalPathList.size(); i++) { String xpath = externalPathList.get(i); for (MddfTarget targetSrc : supportingMECs.keySet()) { Element rootEl = targetSrc.getXmlDoc().getRootElement(); XPathExpression xpExp = resolveXPath(xpath, varMap, rootEl); List nextElList = xpExp.evaluate(rootEl); if (filterDef != null) { nextElList = applyFilter(nextElList, filterDef); } matchedElList.addAll(nextElList); } } } String logMsg = constraint.optString("msg", ""); String details = constraint.optString("details", ""); // check cardinality int count = matchedElList.size(); if (min > 0 && (count < min)) { String elName = target.getName(); String msg; if (logMsg.isEmpty()) { msg = "Invalid " + elName + " structure: missing " + targetList + " elements"; } else { msg = logMsg; } if (details.isEmpty()) { details = elName + " requires minimum of " + min + " " + targetList + " elements"; if (xpParts.length > 1) { details = details + " matching the criteria [" + xpParts[1]; } } LogReference srcRef = resolveDocRef(docRef); logger.logIssue(LogMgmt.TAG_MD, logLevel, contextEl, logFolder, msg, details, srcRef, logMsgSrcId); if (logLevel > LogMgmt.LEV_WARN) { passes = false; } } if (max > -1 && (count > max)) { String elName = target.getName(); String msg; if (logMsg.isEmpty()) { msg = "Invalid " + elName + " structure: too many child " + targetList + "elements"; } else { msg = logMsg; } if (details.isEmpty()) { details = elName + " permits maximum of " + max + " " + targetList + " elements"; } LogReference srcRef = resolveDocRef(docRef); logger.logIssue(LogMgmt.TAG_MD, logLevel, contextEl, logFolder, msg, details, srcRef, logMsgSrcId); if (logLevel > LogMgmt.LEV_WARN) { passes = false; } } return passes; } /** * Assign values to any variables used by a constraint. * Variables are denoted by a "$" followed by an ID (e.g., $FOO) and * are assigned a value via a XPath. For example:
	 *   "$CID": "./@ContentID"
	 * 
They may be included in the constraint's XPath by using enclosing * the variable name in curly brackets. For example:
	 *   "xpath": 
	 *      [
	 *         "../..//{manifest}Experience[@ExperienceID={$CID}]/{manifest}PictureGroupID"
	 *      ],
	 * 
*

* It is possible that the XPath used to determine a variable's value will * evaluate to a null. Any constraint XPath that references a null * variable will be skipped when evaluating the constraint criteria. *

* * @param target * @param constraint * @return variable assignments as key/value entries in a Map */ private Map resolveVariables(Element target, JSONObject constraint) { Map varMap = new HashMap(); Set keySet = constraint.keySet(); for (String key : keySet) { if (key.startsWith("$")) { String xpath = constraint.getString(key); XPathExpression xpe = resolveXPath(xpath, null, target); String value = null; if (resolvesToAttribute(xpath)) { Attribute varSrc = (Attribute) xpe.evaluateFirst(target); if (varSrc != null) { value = varSrc.getValue(); } } else { Element varSrc = (Element) xpe.evaluateFirst(target); if (varSrc != null) { value = varSrc.getTextNormalize(); } } varMap.put(key, value); String msg = "resolveVariables(): Var " + key + "=" + value; logger.logIssue(LogMgmt.TAG_MD, LogMgmt.LEV_DEBUG, target, msg, xpath, null, logMsgSrcId); } } return varMap; } /** * Apply a filter to a list of Elements and/ro Attributes. This is used when * XPath criteria are insufficient, or too unwieldy, to fully implement a * constraint. *

* Filters are (for now) defined as a set of values and an optional 'negated' * flag set to true or false *

* NOTE: future implementations may include support for REGEX pattern * matching. * * @param elList of Elements and/or Attributes * @param filterDef * @return */ private List applyFilter(List inList, JSONObject filterDef) { List outList = new ArrayList(); JSONArray valueSet = filterDef.optJSONArray("values"); String negated = filterDef.optString("negated", "false"); boolean isNegated = negated.equals("true"); for (Object nextEl : inList) { String value; if (nextEl instanceof Element) { value = ((Element) nextEl).getValue(); } else { Attribute nextAt = (Attribute) nextEl; value = nextAt.getValue(); } if (valueSet.contains(value)) { if (!isNegated) { outList.add(nextEl); } } else { if (isNegated) { outList.add(nextEl); } } } return outList; } /** * Create a XPathExpression from a string representation. An * xpathDef is defined using the standard XPath syntax with one * modification. Namespaces are indicated using a variable indicating the name * of an MDDF schema. The appropriate namespace prefixes will be inserted by the * software. Supported namespaces are: *
    *
  • {avail}
  • *
  • {mdmec}
  • *
  • {manifest}
  • *
  • {md}
  • *
*

* The specific version-dependent Namespace instances used when * compiling the XPathExpression will be determined based on the * Namespaces used by the Document containing the * target Element argument. The returned XPathExpression may not, * therefore, evaluate properly when applied to a different Document. *

* * @param xpath * @param varMap * @param target * @return */ public XPathExpression resolveXPath(String xpath, Map varMap, Element target) { Element rootEl = target.getDocument().getRootElement(); FILE_FMT mddfFmt = MddfContext.identifyMddfFormat(rootEl); return resolveXPath(xpath, varMap, mddfFmt); } /** * Create a XPathExpression from a string representation. An * xpathDef is defined using the standard XPath syntax with one * modification. Namespaces are indicated using a variable indicating the name * of an MDDF schema. The appropriate namespace prefixes will be inserted by the * software. Supported namespaces are: *
    *
  • {avail}
  • *
  • {mdmec}
  • *
  • {manifest}
  • *
  • {md}
  • *
*

* The specific version-dependent Namespace instances used when * compiling the XPathExpression will be determined based on the value * of the targetMddfFmt argument. The returned XPathExpression may not, * therefore, evaluate properly when applied to a different Document with a * FILE_FMT *

* * @param xpathDef * @param varMap * @param targetMddfFmt * @return */ private XPathExpression resolveXPath(String xpathDef, Map varMap, FILE_FMT targetMddfFmt) { if (varMap != null) { Set keySet = varMap.keySet(); for (String varID : keySet) { String varValue = varMap.get(varID); String regExp = "\\{\\" + varID + "\\}"; xpathDef = xpathDef.replaceAll(regExp, "'" + varValue + "'"); } } Map uses = MddfContext.getRequiredNamespaces(targetMddfFmt); if (xpathDef.contains("{md}")) { xpathDef = xpathDef.replaceAll("\\{md\\}", uses.get("MD").getPrefix() + ":"); } if (xpathDef.contains("{avail}")) { xpathDef = xpathDef.replaceAll("\\{avail\\}", uses.get("AVAILS").getPrefix() + ":"); } if (xpathDef.contains("{manifest}")) { xpathDef = xpathDef.replaceAll("\\{manifest\\}", uses.get("MANIFEST").getPrefix() + ":"); } if (xpathDef.contains("{mdmec}")) { xpathDef = xpathDef.replaceAll("\\{mdmec\\}", uses.get("MDMEC").getPrefix() + ":"); } // Now compile the XPath XPathExpression xpExpression; /** * The following are examples of xpaths that return an attribute value and that * we therefore need to identity: *
    *
  • avail:Term/@termName
  • *
  • avail:Term[@termName[.='Tier' or .='WSP' or .='DMRP']
  • *
  • @contentID
  • *
* whereas the following should NOT match: *
    *
  • avail:Term/avail:Event[../@termName='AnnounceDate']
  • *
*/ XPathFactory xpfac = XPathFactory.instance(); Set nspaceSet = new HashSet(); nspaceSet.addAll(uses.values()); if (resolvesToAttribute(xpathDef)) { // must be an attribute value we're after.. xpExpression = xpfac.compile(xpathDef, Filters.attribute(), null, nspaceSet); } else { xpExpression = xpfac.compile(xpathDef, Filters.element(), null, nspaceSet); } return xpExpression; } private static boolean resolvesToAttribute(String xpathDef) { return xpathDef.matches(".*/@[\\w]++(\\[.+\\])?"); } private LogReference resolveDocRef(String docRef) { String docStandard = null; String docSection = null; LogReference srcRef = null; if (docRef != null && !docRef.isEmpty()) { String[] parts = docRef.split(":"); if (parts.length >= 2) { docStandard = parts[0]; docSection = parts[1]; srcRef = LogReference.getRef(docStandard, docSection); } } return srcRef; } }