/** * @param path * @param pos * @throws XmpException */ internal static void SkipPathDelimiter(string path, PathPosition pos) { if (path[pos.StepBegin] == '/') { // skip slash pos.StepBegin++; // added for Java if (pos.StepBegin >= path.Length) { throw new XmpException("Empty XmpPath segment", XmpError.BADXPATH); } } if (path[pos.StepBegin] == '*') { // skip asterisk pos.StepBegin++; if (pos.StepBegin >= path.Length || path[pos.StepBegin] != '[') { throw new XmpException("Missing '[' after '*'", XmpError.BADXPATH); } } }
/** * 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. */ internal static void ParseRootNode(string schemaNs, PathPosition pos, XmpPath expandedXPath) { while (pos.StepEnd < pos.Path.Length && "/[*".IndexOf(pos.Path[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 - pos.StepBegin)); IXmpAliasInfo aliasInfo = XmpMetaFactory.SchemaRegistry.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.Namespace, XmpPath.SCHEMA_NODE)); XmpPathSegment rootStep = new XmpPathSegment(VerifyXPathRoot(aliasInfo.Namespace, aliasInfo.PropName), XmpPath.STRUCT_FIELD_STEP); rootStep.Alias = true; rootStep.AliasForm = aliasInfo.AliasForm.Options; expandedXPath.Add(rootStep); if (aliasInfo.AliasForm.ArrayAltText) { XmpPathSegment qualSelectorStep = new XmpPathSegment("[?xml:lang='x-default']", XmpPath.QUAL_SELECTOR_STEP); qualSelectorStep.Alias = true; qualSelectorStep.AliasForm = aliasInfo.AliasForm.Options; expandedXPath.Add(qualSelectorStep); } else if (aliasInfo.AliasForm.Array) { XmpPathSegment indexStep = new XmpPathSegment("[1]", XmpPath.ARRAY_INDEX_STEP); indexStep.Alias = true; indexStep.AliasForm = aliasInfo.AliasForm.Options; expandedXPath.Add(indexStep); } } }
/** * 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 */ internal static XmpPathSegment ParseStructSegment(PathPosition pos) { pos.NameStart = pos.StepBegin; while (pos.StepEnd < pos.Path.Length && "/[*".IndexOf(pos.Path[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 - pos.StepBegin), XmpPath.STRUCT_FIELD_STEP); return(segment); }
/** * 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="value"] * <dd> An element in an array of structs, chosen by a field value. * <dt>[@xml:lang="value"] * <dd> An element in an alt-text array, chosen by the xml:lang qualifier. * <dt>[?qualName="value"] * <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) { 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 = path[pos.StepBegin] != '[' ? ParseStructSegment(pos) : ParseIndexSegment(pos); if (segment.Kind == XmpPath.STRUCT_FIELD_STEP) { if (segment.Name[0] == '@') { segment.Name = "?" + segment.Name.Substring(1); if (!"?xml:lang".Equals(segment.Name)) { throw new XmpException("Only xml:lang allowed with '@'", XmpError.BADXPATH); } } if (segment.Name[0] == '?') { pos.NameStart++; segment.Kind = XmpPath.QUALIFIER_STEP; } VerifyQualName(pos.Path.Substring(pos.NameStart, pos.NameEnd - pos.NameStart)); } else if (segment.Kind == XmpPath.FIELD_SELECTOR_STEP) { if (segment.Name[1] == '@') { segment.Name = "[?" + segment.Name.Substring(2); if (!segment.Name.StartsWith("[?xml:lang=")) { throw new XmpException("Only xml:lang allowed with '@'", XmpError.BADXPATH); } } if (segment.Name[1] == '?') { pos.NameStart++; segment.Kind = XmpPath.QUAL_SELECTOR_STEP; VerifyQualName(pos.Path.Substring(pos.NameStart, pos.NameEnd - pos.NameStart)); } } expandedXPath.Add(segment); } return expandedXPath; }
/** * Parses an array index segment. * * @param pos the xmp path * @return Returns the segment or an error * @throws XmpException thrown on xmp path errors * */ internal static XmpPathSegment ParseIndexSegment(PathPosition pos) { XmpPathSegment segment; pos.StepEnd++; // Look at the character after the leading '['. if ('0' <= pos.Path[pos.StepEnd] && pos.Path[pos.StepEnd] <= '9') { // A numeric (decimal integer) array index. while (pos.StepEnd < pos.Path.Length && '0' <= pos.Path[pos.StepEnd] && pos.Path[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[pos.StepEnd] != ']' && pos.Path[pos.StepEnd] != '=') { pos.StepEnd++; } if (pos.StepEnd >= pos.Path.Length) { throw new XmpException("Missing ']' or '=' for array index", XmpError.BADXPATH); } if (pos.Path[pos.StepEnd] == ']') { if (!"[last()".Equals(pos.Path.Substring(pos.StepBegin, pos.StepEnd - pos.StepBegin))) { 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[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[pos.StepEnd] == quote) { // check for escaped quote if (pos.StepEnd + 1 >= pos.Path.Length || pos.Path[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[pos.StepEnd] != ']') { throw new XmpException("Missing ']' for array index", XmpError.BADXPATH); } pos.StepEnd++; segment.Name = pos.Path.Substring(pos.StepBegin, pos.StepEnd - pos.StepBegin); return segment; }
/** * 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 */ internal static XmpPathSegment ParseStructSegment(PathPosition pos) { pos.NameStart = pos.StepBegin; while (pos.StepEnd < pos.Path.Length && "/[*".IndexOf(pos.Path[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 - pos.StepBegin), XmpPath.STRUCT_FIELD_STEP); return segment; }
/** * 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="value"] * <dd> An element in an array of structs, chosen by a field value. * <dt>[@xml:lang="value"] * <dd> An element in an alt-text array, chosen by the xml:lang qualifier. * <dt>[?qualName="value"] * <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) { 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 = path[pos.StepBegin] != '[' ? ParseStructSegment(pos) : ParseIndexSegment(pos); if (segment.Kind == XmpPath.STRUCT_FIELD_STEP) { if (segment.Name[0] == '@') { segment.Name = "?" + segment.Name.Substring(1); if (!"?xml:lang".Equals(segment.Name)) { throw new XmpException("Only xml:lang allowed with '@'", XmpError.BADXPATH); } } if (segment.Name[0] == '?') { pos.NameStart++; segment.Kind = XmpPath.QUALIFIER_STEP; } VerifyQualName(pos.Path.Substring(pos.NameStart, pos.NameEnd - pos.NameStart)); } else if (segment.Kind == XmpPath.FIELD_SELECTOR_STEP) { if (segment.Name[1] == '@') { segment.Name = "[?" + segment.Name.Substring(2); if (!segment.Name.StartsWith("[?xml:lang=")) { throw new XmpException("Only xml:lang allowed with '@'", XmpError.BADXPATH); } } if (segment.Name[1] == '?') { pos.NameStart++; segment.Kind = XmpPath.QUAL_SELECTOR_STEP; VerifyQualName(pos.Path.Substring(pos.NameStart, pos.NameEnd - pos.NameStart)); } } expandedXPath.Add(segment); } return(expandedXPath); }
/** * Parses an array index segment. * * @param pos the xmp path * @return Returns the segment or an error * @throws XmpException thrown on xmp path errors * */ internal static XmpPathSegment ParseIndexSegment(PathPosition pos) { XmpPathSegment segment; pos.StepEnd++; // Look at the character after the leading '['. if ('0' <= pos.Path[pos.StepEnd] && pos.Path[pos.StepEnd] <= '9') { // A numeric (decimal integer) array index. while (pos.StepEnd < pos.Path.Length && '0' <= pos.Path[pos.StepEnd] && pos.Path[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[pos.StepEnd] != ']' && pos.Path[pos.StepEnd] != '=') { pos.StepEnd++; } if (pos.StepEnd >= pos.Path.Length) { throw new XmpException("Missing ']' or '=' for array index", XmpError.BADXPATH); } if (pos.Path[pos.StepEnd] == ']') { if (!"[last()".Equals(pos.Path.Substring(pos.StepBegin, pos.StepEnd - pos.StepBegin))) { 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[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[pos.StepEnd] == quote) { // check for escaped quote if (pos.StepEnd + 1 >= pos.Path.Length || pos.Path[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[pos.StepEnd] != ']') { throw new XmpException("Missing ']' for array index", XmpError.BADXPATH); } pos.StepEnd++; segment.Name = pos.Path.Substring(pos.StepBegin, pos.StepEnd - pos.StepBegin); return(segment); }