blob: 9936c06f0eeb2978cc383b1a0135e7e04cd462ec [file] [log] [blame]
// =================================================================================================
// ADOBE SYSTEMS INCORPORATED
// Copyright 2006 Adobe Systems Incorporated
// All Rights Reserved
//
// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
// of the Adobe license agreement accompanying it.
// =================================================================================================
package com.adobe.xmp.impl.xpath;
import com.adobe.xmp.XMPError;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPMetaFactory;
import com.adobe.xmp.impl.Utils;
import com.adobe.xmp.properties.XMPAliasInfo;
/**
* Parser for XMP XPaths.
*
* @since 01.03.2006
*/
public final class XMPPathParser
{
/**
* Private constructor
*/
private XMPPathParser()
{
// empty
}
/**
* Split an XMPPath expression apart at the conceptual steps, adding the
* root namespace prefix to the first property component. The schema URI is
* put in the first (0th) slot in the expanded XMPPath. Check if the top
* level component is an alias, but don't resolve it.
* <p>
* In the most verbose case steps are separated by '/', and each step can be
* of these forms:
* <dl>
* <dt>prefix:name
* <dd> A top level property or struct field.
* <dt>[index]
* <dd> An element of an array.
* <dt>[last()]
* <dd> The last element of an array.
* <dt>[fieldName=&quot;value&quot;]
* <dd> An element in an array of structs, chosen by a field value.
* <dt>[@xml:lang=&quot;value&quot;]
* <dd> An element in an alt-text array, chosen by the xml:lang qualifier.
* <dt>[?qualName=&quot;value&quot;]
* <dd> An element in an array, chosen by a qualifier value.
* <dt>@xml:lang
* <dd> An xml:lang qualifier.
* <dt>?qualName
* <dd> A general qualifier.
* </dl>
* <p>
* The logic is complicated though by shorthand for arrays, the separating
* '/' and leading '*' are optional. These are all equivalent: array/*[2]
* array/[2] array*[2] array[2] All of these are broken into the 2 steps
* "array" and "[2]".
* <p>
* The value portion in the array selector forms is a string quoted by '''
* or '"'. The value may contain any character including a doubled quoting
* character. The value may be empty.
* <p>
* The syntax isn't checked, but an XML name begins with a letter or '_',
* and contains letters, digits, '.', '-', '_', and a bunch of special
* non-ASCII Unicode characters. An XML qualified name is a pair of names
* separated by a colon.
* @param schemaNS
* schema namespace
* @param path
* property name
* @return Returns the expandet XMPPath.
* @throws XMPException
* Thrown if the format is not correct somehow.
*
*/
public static XMPPath expandXPath(String schemaNS, String path) throws XMPException
{
if (schemaNS == null || path == null)
{
throw new XMPException("Parameter must not be null", XMPError.BADPARAM);
}
XMPPath expandedXPath = new XMPPath();
PathPosition pos = new PathPosition();
pos.path = path;
// Pull out the first component and do some special processing on it: add the schema
// namespace prefix and and see if it is an alias. The start must be a "qualName".
parseRootNode(schemaNS, pos, expandedXPath);
// Now continue to process the rest of the XMPPath string.
while (pos.stepEnd < path.length())
{
pos.stepBegin = pos.stepEnd;
skipPathDelimiter(path, pos);
pos.stepEnd = pos.stepBegin;
XMPPathSegment segment;
if (path.charAt(pos.stepBegin) != '[')
{
// A struct field or qualifier.
segment = parseStructSegment(pos);
}
else
{
// One of the array forms.
segment = parseIndexSegment(pos);
}
if (segment.getKind() == XMPPath.STRUCT_FIELD_STEP)
{
if (segment.getName().charAt(0) == '@')
{
segment.setName("?" + segment.getName().substring(1));
if (!"?xml:lang".equals(segment.getName()))
{
throw new XMPException("Only xml:lang allowed with '@'",
XMPError.BADXPATH);
}
}
if (segment.getName().charAt(0) == '?')
{
pos.nameStart++;
segment.setKind(XMPPath.QUALIFIER_STEP);
}
verifyQualName(pos.path.substring(pos.nameStart, pos.nameEnd));
}
else if (segment.getKind() == XMPPath.FIELD_SELECTOR_STEP)
{
if (segment.getName().charAt(1) == '@')
{
segment.setName("[?" + segment.getName().substring(2));
if (!segment.getName().startsWith("[?xml:lang="))
{
throw new XMPException("Only xml:lang allowed with '@'",
XMPError.BADXPATH);
}
}
if (segment.getName().charAt(1) == '?')
{
pos.nameStart++;
segment.setKind(XMPPath.QUAL_SELECTOR_STEP);
verifyQualName(pos.path.substring(pos.nameStart, pos.nameEnd));
}
}
expandedXPath.add(segment);
}
return expandedXPath;
}
/**
* @param path
* @param pos
* @throws XMPException
*/
private static void skipPathDelimiter(String path, PathPosition pos) throws XMPException
{
if (path.charAt(pos.stepBegin) == '/')
{
// skip slash
pos.stepBegin++;
// added for Java
if (pos.stepBegin >= path.length())
{
throw new XMPException("Empty XMPPath segment", XMPError.BADXPATH);
}
}
if (path.charAt(pos.stepBegin) == '*')
{
// skip asterisk
pos.stepBegin++;
if (pos.stepBegin >= path.length() || path.charAt(pos.stepBegin) != '[')
{
throw new XMPException("Missing '[' after '*'", XMPError.BADXPATH);
}
}
}
/**
* Parses a struct segment
* @param pos the current position in the path
* @return Retusn the segment or an errror
* @throws XMPException If the sement is empty
*/
private static XMPPathSegment parseStructSegment(PathPosition pos) throws XMPException
{
pos.nameStart = pos.stepBegin;
while (pos.stepEnd < pos.path.length() && "/[*".indexOf(pos.path.charAt(pos.stepEnd)) < 0)
{
pos.stepEnd++;
}
pos.nameEnd = pos.stepEnd;
if (pos.stepEnd == pos.stepBegin)
{
throw new XMPException("Empty XMPPath segment", XMPError.BADXPATH);
}
// ! Touch up later, also changing '@' to '?'.
XMPPathSegment segment = new XMPPathSegment(pos.path.substring(pos.stepBegin, pos.stepEnd),
XMPPath.STRUCT_FIELD_STEP);
return segment;
}
/**
* Parses an array index segment.
*
* @param pos the xmp path
* @return Returns the segment or an error
* @throws XMPException thrown on xmp path errors
*
*/
private static XMPPathSegment parseIndexSegment(PathPosition pos) throws XMPException
{
XMPPathSegment segment;
pos.stepEnd++; // Look at the character after the leading '['.
if ('0' <= pos.path.charAt(pos.stepEnd) && pos.path.charAt(pos.stepEnd) <= '9')
{
// A numeric (decimal integer) array index.
while (pos.stepEnd < pos.path.length() && '0' <= pos.path.charAt(pos.stepEnd)
&& pos.path.charAt(pos.stepEnd) <= '9')
{
pos.stepEnd++;
}
segment = new XMPPathSegment(null, XMPPath.ARRAY_INDEX_STEP);
}
else
{
// Could be "[last()]" or one of the selector forms. Find the ']' or '='.
while (pos.stepEnd < pos.path.length() && pos.path.charAt(pos.stepEnd) != ']'
&& pos.path.charAt(pos.stepEnd) != '=')
{
pos.stepEnd++;
}
if (pos.stepEnd >= pos.path.length())
{
throw new XMPException("Missing ']' or '=' for array index", XMPError.BADXPATH);
}
if (pos.path.charAt(pos.stepEnd) == ']')
{
if (!"[last()".equals(pos.path.substring(pos.stepBegin, pos.stepEnd)))
{
throw new XMPException(
"Invalid non-numeric array index", XMPError.BADXPATH);
}
segment = new XMPPathSegment(null, XMPPath.ARRAY_LAST_STEP);
}
else
{
pos.nameStart = pos.stepBegin + 1;
pos.nameEnd = pos.stepEnd;
pos.stepEnd++; // Absorb the '=', remember the quote.
char quote = pos.path.charAt(pos.stepEnd);
if (quote != '\'' && quote != '"')
{
throw new XMPException(
"Invalid quote in array selector", XMPError.BADXPATH);
}
pos.stepEnd++; // Absorb the leading quote.
while (pos.stepEnd < pos.path.length())
{
if (pos.path.charAt(pos.stepEnd) == quote)
{
// check for escaped quote
if (pos.stepEnd + 1 >= pos.path.length()
|| pos.path.charAt(pos.stepEnd + 1) != quote)
{
break;
}
pos.stepEnd++;
}
pos.stepEnd++;
}
if (pos.stepEnd >= pos.path.length())
{
throw new XMPException("No terminating quote for array selector",
XMPError.BADXPATH);
}
pos.stepEnd++; // Absorb the trailing quote.
// ! Touch up later, also changing '@' to '?'.
segment = new XMPPathSegment(null, XMPPath.FIELD_SELECTOR_STEP);
}
}
if (pos.stepEnd >= pos.path.length() || pos.path.charAt(pos.stepEnd) != ']')
{
throw new XMPException("Missing ']' for array index", XMPError.BADXPATH);
}
pos.stepEnd++;
segment.setName(pos.path.substring(pos.stepBegin, pos.stepEnd));
return segment;
}
/**
* Parses the root node of an XMP Path, checks if namespace and prefix fit together
* and resolve the property to the base property if it is an alias.
* @param schemaNS the root namespace
* @param pos the parsing position helper
* @param expandedXPath the path to contribute to
* @throws XMPException If the path is not valid.
*/
private static void parseRootNode(String schemaNS, PathPosition pos, XMPPath expandedXPath)
throws XMPException
{
while (pos.stepEnd < pos.path.length() && "/[*".indexOf(pos.path.charAt(pos.stepEnd)) < 0)
{
pos.stepEnd++;
}
if (pos.stepEnd == pos.stepBegin)
{
throw new XMPException("Empty initial XMPPath step", XMPError.BADXPATH);
}
String rootProp = verifyXPathRoot(schemaNS, pos.path.substring(pos.stepBegin, pos.stepEnd));
XMPAliasInfo aliasInfo = XMPMetaFactory.getSchemaRegistry().findAlias(rootProp);
if (aliasInfo == null)
{
// add schema xpath step
expandedXPath.add(new XMPPathSegment(schemaNS, XMPPath.SCHEMA_NODE));
XMPPathSegment rootStep = new XMPPathSegment(rootProp, XMPPath.STRUCT_FIELD_STEP);
expandedXPath.add(rootStep);
}
else
{
// add schema xpath step and base step of alias
expandedXPath.add(new XMPPathSegment(aliasInfo.getNamespace(), XMPPath.SCHEMA_NODE));
XMPPathSegment rootStep = new XMPPathSegment(verifyXPathRoot(aliasInfo.getNamespace(),
aliasInfo.getPropName()),
XMPPath.STRUCT_FIELD_STEP);
rootStep.setAlias(true);
rootStep.setAliasForm(aliasInfo.getAliasForm().getOptions());
expandedXPath.add(rootStep);
if (aliasInfo.getAliasForm().isArrayAltText())
{
XMPPathSegment qualSelectorStep = new XMPPathSegment("[?xml:lang='x-default']",
XMPPath.QUAL_SELECTOR_STEP);
qualSelectorStep.setAlias(true);
qualSelectorStep.setAliasForm(aliasInfo.getAliasForm().getOptions());
expandedXPath.add(qualSelectorStep);
}
else if (aliasInfo.getAliasForm().isArray())
{
XMPPathSegment indexStep = new XMPPathSegment("[1]",
XMPPath.ARRAY_INDEX_STEP);
indexStep.setAlias(true);
indexStep.setAliasForm(aliasInfo.getAliasForm().getOptions());
expandedXPath.add(indexStep);
}
}
}
/**
* Verifies whether the qualifier name is not XML conformant or the
* namespace prefix has not been registered.
*
* @param qualName
* a qualifier name
* @throws XMPException
* If the name is not conformant
*/
private static void verifyQualName(String qualName) throws XMPException
{
int colonPos = qualName.indexOf(':');
if (colonPos > 0)
{
String prefix = qualName.substring(0, colonPos);
if (Utils.isXMLNameNS(prefix))
{
String regURI = XMPMetaFactory.getSchemaRegistry().getNamespaceURI(
prefix);
if (regURI != null)
{
return;
}
throw new XMPException("Unknown namespace prefix for qualified name",
XMPError.BADXPATH);
}
}
throw new XMPException("Ill-formed qualified name", XMPError.BADXPATH);
}
/**
* Verify if an XML name is conformant.
*
* @param name
* an XML name
* @throws XMPException
* When the name is not XML conformant
*/
private static void verifySimpleXMLName(String name) throws XMPException
{
if (!Utils.isXMLName(name))
{
throw new XMPException("Bad XML name", XMPError.BADXPATH);
}
}
/**
* Set up the first 2 components of the expanded XMPPath. Normalizes the various cases of using
* the full schema URI and/or a qualified root property name. Returns true for normal
* processing. If allowUnknownSchemaNS is true and the schema namespace is not registered, false
* is returned. If allowUnknownSchemaNS is false and the schema namespace is not registered, an
* exception is thrown
* <P>
* (Should someday check the full syntax:)
*
* @param schemaNS schema namespace
* @param rootProp the root xpath segment
* @return Returns root QName.
* @throws XMPException Thrown if the format is not correct somehow.
*/
private static String verifyXPathRoot(String schemaNS, String rootProp)
throws XMPException
{
// Do some basic checks on the URI and name. Try to lookup the URI. See if the name is
// qualified.
if (schemaNS == null || schemaNS.length() == 0)
{
throw new XMPException(
"Schema namespace URI is required", XMPError.BADSCHEMA);
}
if ((rootProp.charAt(0) == '?') || (rootProp.charAt(0) == '@'))
{
throw new XMPException("Top level name must not be a qualifier", XMPError.BADXPATH);
}
if (rootProp.indexOf('/') >= 0 || rootProp.indexOf('[') >= 0)
{
throw new XMPException("Top level name must be simple", XMPError.BADXPATH);
}
String prefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(schemaNS);
if (prefix == null)
{
throw new XMPException("Unregistered schema namespace URI", XMPError.BADSCHEMA);
}
// Verify the various URI and prefix combinations. Initialize the
// expanded XMPPath.
int colonPos = rootProp.indexOf(':');
if (colonPos < 0)
{
// The propName is unqualified, use the schemaURI and associated
// prefix.
verifySimpleXMLName(rootProp); // Verify the part before any colon
return prefix + rootProp;
}
else
{
// The propName is qualified. Make sure the prefix is legit. Use the associated URI and
// qualified name.
// Verify the part before any colon
verifySimpleXMLName(rootProp.substring(0, colonPos));
verifySimpleXMLName(rootProp.substring(colonPos));
prefix = rootProp.substring(0, colonPos + 1);
String regPrefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(schemaNS);
if (regPrefix == null)
{
throw new XMPException("Unknown schema namespace prefix", XMPError.BADSCHEMA);
}
if (!prefix.equals(regPrefix))
{
throw new XMPException("Schema namespace URI and prefix mismatch",
XMPError.BADSCHEMA);
}
return rootProp;
}
}
}
/**
* This objects contains all needed char positions to parse.
*/
class PathPosition
{
/** the complete path */
public String path = null;
/** the start of a segment name */
int nameStart = 0;
/** the end of a segment name */
int nameEnd = 0;
/** the begin of a step */
int stepBegin = 0;
/** the end of a step */
int stepEnd = 0;
}