Example #1
0
        /// <exception cref="XmpException"> </exception>
        /// <seealso cref= XMPMeta#getLocalizedText(String, String, String, String) </seealso>
        public virtual IXmpProperty GetLocalizedText(string schemaNs, string altTextName, string genericLang,
                                                     string specificLang)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertArrayName(altTextName);
            ParameterAsserts.AssertSpecificLang(specificLang);

            genericLang = genericLang != null?Utils.NormalizeLangValue(genericLang) : null;

            specificLang = Utils.NormalizeLangValue(specificLang);

            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName);
            XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null);

            if (arrayNode == null)
            {
                return(null);
            }

            object[] result   = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang);
            int      match    = (int)((int?)result[0]);
            XmpNode  itemNode = (XmpNode)result[1];

            if (match != XmpNodeUtils.CLT_NO_VALUES)
            {
                return(new XmpPropertyImpl1(itemNode));
            }
            return(null);
        }
Example #2
0
        /// <summary>
        /// Tweak old XMP: Move an instance ID from rdf:about to the
        /// <em>xmpMM:InstanceID</em> property. An old instance ID usually looks
        /// like &quot;uuid:bac965c4-9d87-11d9-9a30-000d936b79c4&quot;, plus InDesign
        /// 3.0 wrote them like &quot;bac965c4-9d87-11d9-9a30-000d936b79c4&quot;. If
        /// the name looks like a UUID simply move it to <em>xmpMM:InstanceID</em>,
        /// don't worry about any existing <em>xmpMM:InstanceID</em>. Both will
        /// only be present when a newer file with the <em>xmpMM:InstanceID</em>
        /// property is updated by an old app that uses <em>rdf:about</em>.
        /// </summary>
        /// <param name="tree"> the root of the metadata tree </param>
        /// <exception cref="XmpException"> Thrown if tweaking fails.  </exception>
        private static void TweakOldXmp(XmpNode tree)
        {
            if (tree.Name != null && tree.Name.Length >= Utils.UUID_LENGTH)
            {
                string nameStr = tree.Name.ToLower();
                if (nameStr.StartsWith("uuid:"))
                {
                    nameStr = nameStr.Substring(5);
                }

                if (Utils.CheckUuidFormat(nameStr))
                {
                    // move UUID to xmpMM:InstanceID and remove it from the root node
                    XmpPath path   = XmpPathParser.ExpandXPath(XmpConst.NS_XMP_MM, "InstanceID");
                    XmpNode idNode = XmpNodeUtils.FindNode(tree, path, true, null);
                    if (idNode != null)
                    {
                        idNode.Options = null; // Clobber any existing xmpMM:InstanceID.
                        idNode.Value   = "uuid:" + nameStr;
                        idNode.RemoveChildren();
                        idNode.RemoveQualifiers();
                        tree.Name = null;
                    }
                    else
                    {
                        throw new XmpException("Failure creating xmpMM:InstanceID", XmpError.INTERNALFAILURE);
                    }
                }
            }
        }
        /// <summary>
        /// Constructor with optionsl initial values. If <code>propName</code> is provided,
        /// <code>schemaNs</code> has also be provided. </summary>
        /// <param name="xmp"> the iterated metadata object. </param>
        /// <param name="schemaNs"> the iteration is reduced to this schema (optional) </param>
        /// <param name="propPath"> the iteration is redurce to this property within the <code>schemaNs</code> </param>
        /// <param name="options"> advanced iteration options, see <seealso cref="IteratorOptions"/> </param>
        /// <exception cref="XmpException"> If the node defined by the paramters is not existing.  </exception>
        public XmpIteratorImpl(XmpMetaImpl xmp, string schemaNs, string propPath, IteratorOptions options)
        {
            // make sure that options is defined at least with defaults
            _options = options ?? new IteratorOptions();

            // the start node of the iteration depending on the schema and property filter
            XmpNode startNode;
            string  initialPath  = null;
            bool    baseSchema   = !String.IsNullOrEmpty(schemaNs);
            bool    baseProperty = !String.IsNullOrEmpty(propPath);

            if (!baseSchema && !baseProperty)
            {
                // complete tree will be iterated
                startNode = xmp.Root;
            }
            else if (baseSchema && baseProperty)
            {
                // Schema and property node provided
                XmpPath path = XmpPathParser.ExpandXPath(schemaNs, propPath);

                // base path is the prop path without the property leaf
                XmpPath basePath = new XmpPath();
                for (int i = 0; i < path.Size() - 1; i++)
                {
                    basePath.Add(path.GetSegment(i));
                }

                startNode   = XmpNodeUtils.FindNode(xmp.Root, path, false, null);
                _baseNs     = schemaNs;
                initialPath = basePath.ToString();
            }
            else if (baseSchema && !baseProperty)
            {
                // Only Schema provided
                startNode = XmpNodeUtils.FindSchemaNode(xmp.Root, schemaNs, false);
            }
            else // !baseSchema  &&  baseProperty
            {
                // No schema but property provided -> error
                throw new XmpException("Schema namespace URI is required", XmpError.BADSCHEMA);
            }


            // create iterator
            if (startNode != null)
            {
                _nodeIterator = (!_options.JustChildren)
                                    ? new NodeIterator(this, startNode, initialPath, 1)
                                    : new NodeIteratorChildren(this, startNode, initialPath);
            }
            else
            {
                // create null iterator
                _nodeIterator = EmptyList.GetEnumerator();
            }
        }
Example #4
0
        /// <summary>Constructor with optional initial values.</summary>
        /// <remarks>If <c>propName</c> is provided, <c>schemaNS</c> has also be provided.</remarks>
        /// <param name="xmp">the iterated metadata object.</param>
        /// <param name="schemaNs">the iteration is reduced to this schema (optional)</param>
        /// <param name="propPath">the iteration is reduced to this property within the <c>schemaNS</c></param>
        /// <param name="options">advanced iteration options, see <see cref="IteratorOptions"/></param>
        /// <exception cref="XmpException">If the node defined by the parameters is not existing.</exception>
        public XmpIterator(XmpMeta xmp, string schemaNs, string propPath, IteratorOptions options)
        {
            // make sure that options is defined at least with defaults
            Options = options ?? new IteratorOptions();
            // the start node of the iteration depending on the schema and property filter
            XmpNode startNode    = null;
            string  initialPath  = null;
            var     baseSchema   = !string.IsNullOrEmpty(schemaNs);
            var     baseProperty = !string.IsNullOrEmpty(propPath);

            if (!baseSchema && !baseProperty)
            {
                // complete tree will be iterated
                startNode = xmp.GetRoot();
            }
            else
            {
                if (baseSchema && baseProperty)
                {
                    // Schema and property node provided
                    var path = XmpPathParser.ExpandXPath(schemaNs, propPath);
                    // base path is the prop path without the property leaf
                    var basePath = new XmpPath();
                    for (var i = 0; i < path.Size() - 1; i++)
                    {
                        basePath.Add(path.GetSegment(i));
                    }
                    startNode     = XmpNodeUtils.FindNode(xmp.GetRoot(), path, false, null);
                    BaseNamespace = schemaNs;
                    initialPath   = basePath.ToString();
                }
                else
                {
                    if (baseSchema && !baseProperty)
                    {
                        // Only Schema provided
                        startNode = XmpNodeUtils.FindSchemaNode(xmp.GetRoot(), schemaNs, false);
                    }
                    else
                    {
                        // !baseSchema  &&  baseProperty
                        // No schema but property provided -> error
                        throw new XmpException("Schema namespace URI is required", XmpErrorCode.BadSchema);
                    }
                }
            }
            // create iterator
            _nodeIterator = startNode != null
                ? (IIterator)(!Options.IsJustChildren
                    ? new NodeIterator(this, startNode, initialPath, 1)
                    : new NodeIteratorChildren(this, startNode, initialPath))
                : Enumerable.Empty <object>().Iterator();
        }
Example #5
0
        /// <summary>
        /// Compose the path expression to select an alternate item by a field's value. The path syntax
        /// allows two forms of &quot;content addressing&quot; that may be used to select an item in an
        /// array of alternatives. The form used in ComposeFieldSelector lets you select an item in an
        /// array of structs based on the value of one of the fields in the structs. The other form of
        /// content addressing is shown in ComposeLangSelector. For example, consider a simple struct
        /// that has two fields, the name of a city and the URI of an FTP site in that city. Use this to
        /// create an array of download alternatives. You can show the user a popup built from the values
        /// of the city fields. You can then get the corresponding URI as follows:
        /// <p>
        /// <blockquote>
        ///
        /// <pre>
        ///      String path = composeFieldSelector ( schemaNs, &quot;Downloads&quot;, fieldNs,
        ///          &quot;City&quot;, chosenCity );
        ///      XMPProperty prop = xmpObj.getStructField ( schemaNs, path, fieldNs, &quot;URI&quot; );
        /// </pre>
        ///
        /// </blockquote>
        /// </summary>
        /// <param name="arrayName"> The name of the array. May be a general path expression, must not be
        ///        <code>null</code> or the empty string. </param>
        /// <param name="fieldNs"> The namespace URI for the field used as the selector. Must not be
        ///        <code>null</code> or the empty string. </param>
        /// <param name="fieldName"> The name of the field used as the selector. Must be a simple XML name, must
        ///        not be <code>null</code> or the empty string. It must be the name of a field that is
        ///        itself simple. </param>
        /// <param name="fieldValue"> The desired value of the field. </param>
        /// <returns> Returns the composed path. This will be of the form
        ///         <tt>ns:arrayName[fNS:fieldName='fieldValue']</tt>, where &quot;ns&quot; is the
        ///         prefix for schemaNs and &quot;fNS&quot; is the prefix for fieldNs. </returns>
        /// <exception cref="XmpException"> Thrown if the path to create is not valid. </exception>
        public static string ComposeFieldSelector(string arrayName, string fieldNs, string fieldName, string fieldValue)
        {
            XmpPath fieldPath = XmpPathParser.ExpandXPath(fieldNs, fieldName);

            if (fieldPath.Size() != 2)
            {
                throw new XmpException("The fieldName name must be simple", XmpError.BADXPATH);
            }

            return(arrayName + '[' + fieldPath.GetSegment((int)XmpPath.STEP_ROOT_PROP).Name + "=\"" + fieldValue +
                   "\"]");
        }
Example #6
0
        /// <summary>
        /// Compose the path expression for a qualifier.
        /// </summary>
        /// <param name="qualNs"> The namespace URI for the qualifier. May be <code>null</code> or the empty
        ///        string if the qualifier is in the XML empty namespace. </param>
        /// <param name="qualName"> The name of the qualifier. Must be a simple XML name, must not be
        ///        <code>null</code> or the empty string. </param>
        /// <returns> Returns the composed path. This will be of the form
        ///         <tt>ns:propName/?qNS:qualName</tt>, where &quot;ns&quot; is the prefix for
        ///         schemaNs and &quot;qNS&quot; is the prefix for qualNs. </returns>
        /// <exception cref="XmpException"> Thrown if the path to create is not valid. </exception>
        public static string ComposeQualifierPath(string qualNs, string qualName)
        {
            AssertQualNs(qualNs);
            AssertQualName(qualName);

            XmpPath qualPath = XmpPathParser.ExpandXPath(qualNs, qualName);

            if (qualPath.Size() != 2)
            {
                throw new XmpException("The qualifier name must be simple", XmpError.BADXPATH);
            }

            return("/?" + qualPath.GetSegment((int)XmpPath.STEP_ROOT_PROP).Name);
        }
Example #7
0
        /// <seealso cref= XMPMeta#doesPropertyExist(String, String) </seealso>
        public virtual bool DoesPropertyExist(string schemaNs, string propName)
        {
            try {
                ParameterAsserts.AssertSchemaNs(schemaNs);
                ParameterAsserts.AssertPropName(propName);

                XmpPath expPath  = XmpPathParser.ExpandXPath(schemaNs, propName);
                XmpNode propNode = XmpNodeUtils.FindNode(_tree, expPath, false, null);
                return(propNode != null);
            }
            catch (XmpException) {
                return(false);
            }
        }
Example #8
0
        /// <summary>
        /// Compose the path expression for a field in a struct. The result can be added to the
        /// path of
        ///
        /// </summary>
        /// <param name="fieldNs"> The namespace URI for the field. Must not be <code>null</code> or the empty
        ///        string. </param>
        /// <param name="fieldName"> The name of the field. Must be a simple XML name, must not be
        ///        <code>null</code> or the empty string. </param>
        /// <returns> Returns the composed path. This will be of the form
        ///         <tt>ns:structName/fNS:fieldName</tt>, where &quot;ns&quot; is the prefix for
        ///         schemaNs and &quot;fNS&quot; is the prefix for fieldNs. </returns>
        /// <exception cref="XmpException"> Thrown if the path to create is not valid. </exception>
        public static string ComposeStructFieldPath(string fieldNs, string fieldName)
        {
            AssertFieldNs(fieldNs);
            AssertFieldName(fieldName);

            XmpPath fieldPath = XmpPathParser.ExpandXPath(fieldNs, fieldName);

            if (fieldPath.Size() != 2)
            {
                throw new XmpException("The field name must be simple", XmpError.BADXPATH);
            }

            return('/' + fieldPath.GetSegment((int)XmpPath.STEP_ROOT_PROP).Name);
        }
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="schemaNs">the root namespace</param>
        /// <param name="pos">the parsing position helper</param>
        /// <param name="expandedXPath">the path to contribute to</param>
        /// <exception cref="XmpException">If the path is not valid.</exception>
        private 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", XmpErrorCode.BadXPath);
            }

            var rootProp  = VerifyXPathRoot(schemaNs, pos.Path.Substring(pos.StepBegin, pos.StepEnd - pos.StepBegin));
            var aliasInfo = XmpMetaFactory.SchemaRegistry.FindAlias(rootProp);

            if (aliasInfo == null)
            {
                // add schema xpath step
                expandedXPath.Add(new XmpPathSegment(schemaNs, XmpPathStepType.SchemaNode));
                var rootStep = new XmpPathSegment(rootProp, XmpPathStepType.StructFieldStep);
                expandedXPath.Add(rootStep);
                return;
            }

            // add schema xpath step and base step of alias
            expandedXPath.Add(new XmpPathSegment(aliasInfo.Namespace, XmpPathStepType.SchemaNode));
            expandedXPath.Add(new XmpPathSegment(VerifyXPathRoot(aliasInfo.Namespace, aliasInfo.PropName), XmpPathStepType.StructFieldStep)
            {
                IsAlias   = true,
                AliasForm = aliasInfo.AliasForm.GetOptions()
            });

            if (aliasInfo.AliasForm.IsArrayAltText)
            {
                expandedXPath.Add(new XmpPathSegment("[?xml:lang='x-default']", XmpPathStepType.QualSelectorStep)
                {
                    IsAlias   = true,
                    AliasForm = aliasInfo.AliasForm.GetOptions()
                });
            }
            else if (aliasInfo.AliasForm.IsArray)
            {
                expandedXPath.Add(new XmpPathSegment("[1]", XmpPathStepType.ArrayIndexStep)
                {
                    IsAlias   = true,
                    AliasForm = aliasInfo.AliasForm.GetOptions()
                });
            }
        }
Example #10
0
        /// <seealso cref= XMPMeta#deleteProperty(String, String) </seealso>
        public virtual void DeleteProperty(string schemaNs, string propName)
        {
            try {
                ParameterAsserts.AssertSchemaNs(schemaNs);
                ParameterAsserts.AssertPropName(propName);

                XmpPath expPath = XmpPathParser.ExpandXPath(schemaNs, propName);

                XmpNode propNode = XmpNodeUtils.FindNode(_tree, expPath, false, null);
                if (propNode != null)
                {
                    XmpNodeUtils.DeleteNode(propNode);
                }
            }
            catch (XmpException) {
                // EMPTY, exceptions are ignored within delete
            }
        }
Example #11
0
        /// <exception cref="XmpException"> </exception>
        /// <seealso cref= XMPMeta#insertArrayItem(String, String, int, String,
        ///      PropertyOptions) </seealso>
        public virtual void InsertArrayItem(string schemaNs, string arrayName, int itemIndex, string itemValue,
                                            PropertyOptions options)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertArrayName(arrayName);

            // Just lookup, don't try to create.
            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName);
            XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null);

            if (arrayNode != null)
            {
                DoSetArrayItem(arrayNode, itemIndex, itemValue, options, true);
            }
            else
            {
                throw new XmpException("Specified array does not exist", XmpError.BADXPATH);
            }
        }
Example #12
0
        /// <exception cref="XmpException"> </exception>
        /// <seealso cref= XMPMeta#countArrayItems(String, String) </seealso>
        public virtual int CountArrayItems(string schemaNs, string arrayName)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertArrayName(arrayName);

            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName);
            XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null);

            if (arrayNode == null)
            {
                return(0);
            }

            if (arrayNode.Options.Array)
            {
                return(arrayNode.ChildrenLength);
            }
            throw new XmpException("The named property is not an array", XmpError.BADXPATH);
        }
Example #13
0
        /// <summary>
        /// Utility to find or create the array used by <code>separateArrayItems()</code>. </summary>
        /// <param name="schemaNs"> a the namespace fo the array </param>
        /// <param name="arrayName"> the name of the array </param>
        /// <param name="arrayOptions"> the options for the array if newly created </param>
        /// <param name="xmp"> the xmp object </param>
        /// <returns> Returns the array node. </returns>
        /// <exception cref="XmpException"> Forwards exceptions </exception>
        private static XmpNode SeparateFindCreateArray(string schemaNs, string arrayName, PropertyOptions arrayOptions,
                                                       XmpMetaImpl xmp)
        {
            arrayOptions = XmpNodeUtils.VerifySetOptions(arrayOptions, null);
            if (!arrayOptions.OnlyArrayOptions)
            {
                throw new XmpException("Options can only provide array form", XmpError.BADOPTIONS);
            }

            // Find the array node, make sure it is OK. Move the current children
            // aside, to be readded later if kept.
            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName);
            XmpNode arrayNode = XmpNodeUtils.FindNode(xmp.Root, arrayPath, false, null);

            if (arrayNode != null)
            {
                // The array exists, make sure the form is compatible. Zero
                // arrayForm means take what exists.
                PropertyOptions arrayForm = arrayNode.Options;
                if (!arrayForm.Array || arrayForm.ArrayAlternate)
                {
                    throw new XmpException("Named property must be non-alternate array", XmpError.BADXPATH);
                }
                if (arrayOptions.EqualArrayTypes(arrayForm))
                {
                    throw new XmpException("Mismatch of specified and existing array form", XmpError.BADXPATH);
                    // *** Right error?
                }
            }
            else
            {
                // The array does not exist, try to create it.
                // don't modify the options handed into the method
                arrayOptions.Array = true;
                arrayNode          = XmpNodeUtils.FindNode(xmp.Root, arrayPath, true, arrayOptions);
                if (arrayNode == null)
                {
                    throw new XmpException("Failed to create named array", XmpError.BADXPATH);
                }
            }
            return(arrayNode);
        }
Example #14
0
        /// <summary>
        /// Returns a property, but the result value can be requested.
        /// </summary>
        /// <seealso cref= XMPMeta#GetProperty(String, String) </seealso>
        /// <param name="schemaNs">
        ///            a schema namespace </param>
        /// <param name="propName">
        ///            a property name or path </param>
        /// <param name="valueType">
        ///            the type of the value, see VALUE_... </param>
        /// <returns> Returns the node value as an object according to the
        ///         <code>valueType</code>. </returns>
        /// <exception cref="XmpException">
        ///             Collects any exception that occurs. </exception>
        protected internal virtual object GetPropertyObject(string schemaNs, string propName, int valueType)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertPropName(propName);

            XmpPath expPath  = XmpPathParser.ExpandXPath(schemaNs, propName);
            XmpNode propNode = XmpNodeUtils.FindNode(_tree, expPath, false, null);

            if (propNode != null)
            {
                if (valueType != VALUE_STRING && propNode.Options.CompositeProperty)
                {
                    throw new XmpException("Property must be simple when a value type is requested",
                                           XmpError.BADXPATH);
                }

                return(evaluateNodeValue(valueType, propNode));
            }
            return(null);
        }
Example #15
0
        /// <exception cref="XmpException"> </exception>
        /// <seealso cref= XMPMeta#SetProperty(String, String, Object, PropertyOptions) </seealso>
        public virtual void SetProperty(string schemaNs, string propName, object propValue, PropertyOptions options)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertPropName(propName);

            options = XmpNodeUtils.VerifySetOptions(options, propValue);

            XmpPath expPath = XmpPathParser.ExpandXPath(schemaNs, propName);

            XmpNode propNode = XmpNodeUtils.FindNode(_tree, expPath, true, options);

            if (propNode != null)
            {
                SetNode(propNode, propValue, options, false);
            }
            else
            {
                throw new XmpException("Specified property does not exist", XmpError.BADXPATH);
            }
        }
Example #16
0
        /// <seealso cref= XMPMeta#appendArrayItem(String, String, PropertyOptions, String,
        ///      PropertyOptions) </seealso>
        public virtual void AppendArrayItem(string schemaNs, string arrayName, PropertyOptions arrayOptions,
                                            string itemValue, PropertyOptions itemOptions)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertArrayName(arrayName);

            if (arrayOptions == null)
            {
                arrayOptions = new PropertyOptions();
            }
            if (!arrayOptions.OnlyArrayOptions)
            {
                throw new XmpException("Only array form flags allowed for arrayOptions", XmpError.BADOPTIONS);
            }

            // Check if array options are set correctly.
            arrayOptions = XmpNodeUtils.VerifySetOptions(arrayOptions, null);


            // Locate or create the array. If it already exists, make sure the array
            // form from the options
            // parameter is compatible with the current state.
            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName);


            // Just lookup, don't try to create.
            XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null);

            if (arrayNode != null)
            {
                // The array exists, make sure the form is compatible. Zero
                // arrayForm means take what exists.
                if (!arrayNode.Options.Array)
                {
                    throw new XmpException("The named property is not an array", XmpError.BADXPATH);
                }
                // if (arrayOptions != null && !arrayOptions.equalArrayTypes(arrayNode.getOptions()))
                // {
                // throw new XmpException("Mismatch of existing and specified array form", BADOPTIONS);
                // }
            }
            else
            {
                // The array does not exist, try to create it.
                if (arrayOptions.Array)
                {
                    arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true, arrayOptions);
                    if (arrayNode == null)
                    {
                        throw new XmpException("Failure creating array node", XmpError.BADXPATH);
                    }
                }
                else
                {
                    // array options missing
                    throw new XmpException("Explicit arrayOptions required to create new array",
                                           XmpError.BADOPTIONS);
                }
            }

            DoSetArrayItem(arrayNode, ARRAY_LAST_ITEM, itemValue, itemOptions, true);
        }
Example #17
0
        /// <seealso cref= XMPMeta#setLocalizedText(String, String, String, String, String,
        ///      PropertyOptions) </seealso>
        public virtual void SetLocalizedText(string schemaNs, string altTextName, string genericLang,
                                             string specificLang, string itemValue, PropertyOptions options)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertArrayName(altTextName);
            ParameterAsserts.AssertSpecificLang(specificLang);

            genericLang = genericLang != null?Utils.NormalizeLangValue(genericLang) : null;

            specificLang = Utils.NormalizeLangValue(specificLang);

            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName);

            // Find the array node and set the options if it was just created.
            XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true,
                                                      new PropertyOptions(PropertyOptions.ARRAY |
                                                                          PropertyOptions.ARRAY_ORDERED |
                                                                          PropertyOptions.ARRAY_ALTERNATE |
                                                                          PropertyOptions.ARRAY_ALT_TEXT));

            if (arrayNode == null)
            {
                throw new XmpException("Failed to find or create array node", XmpError.BADXPATH);
            }
            if (!arrayNode.Options.ArrayAltText)
            {
                if (!arrayNode.HasChildren() && arrayNode.Options.ArrayAlternate)
                {
                    arrayNode.Options.ArrayAltText = true;
                }
                else
                {
                    throw new XmpException("Specified property is no alt-text array", XmpError.BADXPATH);
                }
            }

            // Make sure the x-default item, if any, is first.
            bool    haveXDefault = false;
            XmpNode xdItem       = null;

            foreach (XmpNode currItem in arrayNode.Children)
            {
                if (!currItem.HasQualifier() || !XML_LANG.Equals(currItem.GetQualifier(1).Name))
                {
                    throw new XmpException("Language qualifier must be first", XmpError.BADXPATH);
                }
                if (X_DEFAULT.Equals(currItem.GetQualifier(1).Value))
                {
                    xdItem       = currItem;
                    haveXDefault = true;
                    break;
                }
            }

            // Moves x-default to the beginning of the array
            if (xdItem != null && arrayNode.ChildrenLength > 1)
            {
                arrayNode.RemoveChild(xdItem);
                arrayNode.AddChild(1, xdItem);
            }

            // Find the appropriate item.
            // chooseLocalizedText will make sure the array is a language
            // alternative.
            object[] result   = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang);
            int      match    = (int)((int?)result[0]);
            XmpNode  itemNode = (XmpNode)result[1];

            bool specificXDefault = X_DEFAULT.Equals(specificLang);

            switch (match)
            {
            case XmpNodeUtils.CLT_NO_VALUES:

                // Create the array items for the specificLang and x-default, with
                // x-default first.
                XmpNodeUtils.AppendLangItem(arrayNode, X_DEFAULT, itemValue);
                haveXDefault = true;
                if (!specificXDefault)
                {
                    XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue);
                }
                break;

            case XmpNodeUtils.CLT_SPECIFIC_MATCH:

                if (!specificXDefault)
                {
                    // Update the specific item, update x-default if it matches the
                    // old value.
                    if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value.Equals(itemNode.Value))
                    {
                        xdItem.Value = itemValue;
                    }
                    // ! Do this after the x-default check!
                    itemNode.Value = itemValue;
                }
                else
                {
                    // Update all items whose values match the old x-default value.
                    Debug.Assert(haveXDefault && xdItem == itemNode);
                    foreach (XmpNode currItem in arrayNode.Children)
                    {
                        if (currItem == xdItem || !currItem.Value.Equals(xdItem != null ? xdItem.Value : null))
                        {
                            continue;
                        }
                        currItem.Value = itemValue;
                    }
                    // And finally do the x-default item.
                    if (xdItem != null)
                    {
                        xdItem.Value = itemValue;
                    }
                }
                break;

            case XmpNodeUtils.CLT_SINGLE_GENERIC:

                // Update the generic item, update x-default if it matches the old
                // value.
                if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value.Equals(itemNode.Value))
                {
                    xdItem.Value = itemValue;
                }
                itemNode.Value = itemValue;     // ! Do this after
                // the x-default
                // check!
                break;

            case XmpNodeUtils.CLT_MULTIPLE_GENERIC:

                // Create the specific language, ignore x-default.
                XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue);
                if (specificXDefault)
                {
                    haveXDefault = true;
                }
                break;

            case XmpNodeUtils.CLT_XDEFAULT:

                // Create the specific language, update x-default if it was the only
                // item.
                if (xdItem != null && arrayNode.ChildrenLength == 1)
                {
                    xdItem.Value = itemValue;
                }
                XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue);
                break;

            case XmpNodeUtils.CLT_FIRST_ITEM:

                // Create the specific language, don't add an x-default item.
                XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue);
                if (specificXDefault)
                {
                    haveXDefault = true;
                }
                break;

            default:
                // does not happen under normal circumstances
                throw new XmpException("Unexpected result from ChooseLocalizedText",
                                       XmpError.INTERNALFAILURE);
            }

            // Add an x-default at the front if needed.
            if (!haveXDefault && arrayNode.ChildrenLength == 1)
            {
                XmpNodeUtils.AppendLangItem(arrayNode, X_DEFAULT, itemValue);
            }
        }
Example #18
0
        /// <seealso cref= XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean)
        /// </seealso>
        /// <param name="xmp">
        ///            The XMP object containing the properties to be removed.
        /// </param>
        /// <param name="schemaNs">
        ///            Optional schema namespace URI for the properties to be
        ///            removed.
        /// </param>
        /// <param name="propName">
        ///            Optional path expression for the property to be removed.
        /// </param>
        /// <param name="doAllProperties">
        ///            Option flag to control the deletion: do internal properties in
        ///            addition to external properties. </param>
        /// <param name="includeAliases">
        ///            Option flag to control the deletion: Include aliases in the
        ///            "named schema" case above. </param>
        /// <exception cref="XmpException"> If metadata processing fails </exception>
        public static void RemoveProperties(IXmpMeta xmp, string schemaNs, string propName, bool doAllProperties,
                                            bool includeAliases)
        {
            ParameterAsserts.AssertImplementation(xmp);
            XmpMetaImpl xmpImpl = (XmpMetaImpl)xmp;

            if (!string.IsNullOrEmpty(propName))
            {
                // Remove just the one indicated property. This might be an alias,
                // the named schema might not actually exist. So don't lookup the
                // schema node.

                if (string.IsNullOrEmpty(schemaNs))
                {
                    throw new XmpException("Property name requires schema namespace", XmpError.BADPARAM);
                }

                XmpPath expPath = XmpPathParser.ExpandXPath(schemaNs, propName);

                XmpNode propNode = XmpNodeUtils.FindNode(xmpImpl.Root, expPath, false, null);
                if (propNode != null)
                {
                    if (doAllProperties ||
                        !Utils.IsInternalProperty(expPath.GetSegment((int)XmpPath.STEP_SCHEMA).Name,
                                                  expPath.GetSegment((int)XmpPath.STEP_ROOT_PROP).Name))
                    {
                        XmpNode parent = propNode.Parent;
                        parent.RemoveChild(propNode);
                        if (parent.Options.SchemaNode && !parent.HasChildren())
                        {
                            // remove empty schema node
                            parent.Parent.RemoveChild(parent);
                        }
                    }
                }
            }
            else if (!string.IsNullOrEmpty(schemaNs))
            {
                // Remove all properties from the named schema. Optionally include
                // aliases, in which case
                // there might not be an actual schema node.

                // XMP_NodePtrPos schemaPos;
                XmpNode schemaNode = XmpNodeUtils.FindSchemaNode(xmpImpl.Root, schemaNs, false);
                if (schemaNode != null)
                {
                    if (RemoveSchemaChildren(schemaNode, doAllProperties))
                    {
                        xmpImpl.Root.RemoveChild(schemaNode);
                    }
                }

                if (includeAliases)
                {
                    // We're removing the aliases also. Look them up by their
                    // namespace prefix.
                    // But that takes more code and the extra speed isn't worth it.
                    // Lookup the XMP node
                    // from the alias, to make sure the actual exists.

                    IXmpAliasInfo[] aliases = XmpMetaFactory.SchemaRegistry.FindAliases(schemaNs);
                    for (int i = 0; i < aliases.Length; i++)
                    {
                        IXmpAliasInfo info       = aliases[i];
                        XmpPath       path       = XmpPathParser.ExpandXPath(info.Namespace, info.PropName);
                        XmpNode       actualProp = XmpNodeUtils.FindNode(xmpImpl.Root, path, false, null);
                        if (actualProp != null)
                        {
                            XmpNode parent = actualProp.Parent;
                            parent.RemoveChild(actualProp);
                        }
                    }
                }
            }
            else
            {
                // Remove all appropriate properties from all schema. In this case
                // we don't have to be
                // concerned with aliases, they are handled implicitly from the
                // actual properties.
                ArrayList schemasToRemove = new ArrayList();
                for (IEnumerator it = xmpImpl.Root.IterateChildren(); it.MoveNext();)
                {
                    XmpNode schema = (XmpNode)it.Current;
                    if (schema == null)
                    {
                        continue;
                    }
                    if (RemoveSchemaChildren(schema, doAllProperties))
                    {
                        schemasToRemove.Add(schema);
                    }
                }
                foreach (XmpNode xmpNode in schemasToRemove)
                {
                    xmpImpl.Root.Children.Remove(xmpNode);
                }
                schemasToRemove.Clear();
            }
        }
Example #19
0
        /// <summary>Follow an expanded path expression to find or create a node.</summary>
        /// <param name="xmpTree">the node to begin the search.</param>
        /// <param name="xpath">the complete xpath</param>
        /// <param name="createNodes">
        /// flag if nodes shall be created
        /// (when called by <c>setProperty()</c>)
        /// </param>
        /// <param name="leafOptions">
        /// the options for the created leaf nodes (only when
        /// <c>createNodes == true</c>).
        /// </param>
        /// <returns>Returns the node if found or created or <c>null</c>.</returns>
        /// <exception cref="XmpException">
        /// An exception is only thrown if an error occurred,
        /// not if a node was not found.
        /// </exception>
        internal static XmpNode FindNode(XmpNode xmpTree, XmpPath xpath, bool createNodes, PropertyOptions leafOptions)
        {
            // check if xpath is set.
            if (xpath == null || xpath.Size() == 0)
            {
                throw new XmpException("Empty XMPPath", XmpErrorCode.BadXPath);
            }

            // Root of implicitly created subtree to possible delete it later.
            // Valid only if leaf is new.
            XmpNode rootImplicitNode = null;

            // resolve schema step
            var currNode = FindSchemaNode(xmpTree, xpath.GetSegment(XmpPath.StepSchema).Name, createNodes);

            if (currNode == null)
            {
                return(null);
            }

            if (currNode.IsImplicit)
            {
                currNode.IsImplicit = false;
                // Clear the implicit node bit.
                rootImplicitNode = currNode;
            }

            // Save the top most implicit node.
            // Now follow the remaining steps of the original XMPPath.
            try
            {
                for (var i = 1; i < xpath.Size(); i++)
                {
                    currNode = FollowXPathStep(currNode, xpath.GetSegment(i), createNodes);
                    if (currNode == null)
                    {
                        if (createNodes)
                        {
                            // delete implicitly created nodes
                            DeleteNode(rootImplicitNode);
                        }
                        return(null);
                    }

                    if (currNode.IsImplicit)
                    {
                        // clear the implicit node flag
                        currNode.IsImplicit = false;
                        // if node is an ALIAS (can be only in root step, auto-create array
                        // when the path has been resolved from a not simple alias type
                        if (i == 1 && xpath.GetSegment(i).IsAlias&& xpath.GetSegment(i).AliasForm != 0)
                        {
                            currNode.Options.SetOption(xpath.GetSegment(i).AliasForm, true);
                        }
                        else
                        {
                            // "CheckImplicitStruct" in C++
                            if (i < xpath.Size() - 1 && xpath.GetSegment(i).Kind == XmpPathStepType.StructFieldStep && !currNode.Options.IsCompositeProperty)
                            {
                                currNode.Options.IsStruct = true;
                            }
                        }

                        if (rootImplicitNode == null)
                        {
                            rootImplicitNode = currNode;
                        }
                    }
                }
            }
            catch (XmpException)
            {
                // Save the top most implicit node.
                // if new notes have been created prior to the error, delete them
                if (rootImplicitNode != null)
                {
                    DeleteNode(rootImplicitNode);
                }

                throw;
            }

            if (rootImplicitNode != null)
            {
                // set options only if a node has been successful created
                currNode.Options.MergeWith(leafOptions);
                currNode.Options = currNode.Options;
            }

            return(currNode);
        }
Example #20
0
        /// <summary>
        /// Split an XMPPath expression apart at the conceptual steps, adding the
        /// root namespace prefix to the first property component.
        /// </summary>
        /// <remarks>
        /// 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.
        /// <para />
        /// In the most verbose case steps are separated by '/', and each step can be
        /// of these forms:
        /// <list>
        ///   <item>
        ///     <term>prefix:name</term>
        ///     <description>A top level property or struct field.</description>
        ///   </item>
        ///   <item>
        ///     <term>[index]</term>
        ///     <description>An element of an array.</description>
        ///   </item>
        ///   <item>
        ///     <term>[last()]</term>
        ///     <description>The last element of an array.</description>
        ///   </item>
        ///   <item>
        ///     <term>[fieldName=&quot;value&quot;]</term>
        ///     <description>An element in an array of structs, chosen by a field value.</description>
        ///   </item>
        ///   <item>
        ///     <term>[@xml:lang=&quot;value&quot;]</term>
        ///     <description>An element in an alt-text array, chosen by the xml:lang qualifier.</description>
        ///   </item>
        ///   <item>
        ///     <term>[?qualName=&quot;value&quot;]</term>
        ///     <description>An element in an array, chosen by a qualifier value.</description>
        ///   </item>
        ///   <item>
        ///     <term>@xml:lang</term>
        ///     <description>An xml:lang qualifier.</description>
        ///   </item>
        ///   <item>
        ///     <term>?qualName</term>
        ///     <description>A general qualifier.</description>
        ///   </item>
        /// </list>
        /// <para />
        /// 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]".
        /// <para />
        /// 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.
        /// <para />
        /// 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.
        /// </remarks>
        /// <param name="schemaNs">schema namespace</param>
        /// <param name="path">property name</param>
        /// <returns>Returns the expandet XMPPath.</returns>
        /// <exception cref="XmpException">Thrown if the format is not correct somehow.</exception>
        public static XmpPath ExpandXPath(string schemaNs, string path)
        {
            if (schemaNs == null || path == null)
            {
                throw new XmpException("Parameter must not be null", XmpErrorCode.BadParam);
            }

            var expandedXPath = new XmpPath();
            var pos           = new PathPosition {
                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;
                var segment = path[pos.StepBegin] != '['
                    ? ParseStructSegment(pos)
                    : ParseIndexSegment(pos);

                if (segment.Kind == XmpPath.StructFieldStep)
                {
                    if (segment.Name[0] == '@')
                    {
                        segment.Name = "?" + segment.Name.Substring(1);
                        if (!"?xml:lang".Equals(segment.Name))
                        {
                            throw new XmpException("Only xml:lang allowed with '@'", XmpErrorCode.BadXPath);
                        }
                    }

                    if (segment.Name[0] == '?')
                    {
                        pos.NameStart++;
                        segment.Kind = XmpPath.QualifierStep;
                    }

                    VerifyQualName(pos.Path.Substring(pos.NameStart, pos.NameEnd - pos.NameStart));
                }
                else if (segment.Kind == XmpPath.FieldSelectorStep)
                {
                    if (segment.Name[1] == '@')
                    {
                        segment.Name = "[?" + segment.Name.Substring(2);

                        if (!segment.Name.StartsWith("[?xml:lang="))
                        {
                            throw new XmpException("Only xml:lang allowed with '@'", XmpErrorCode.BadXPath);
                        }
                    }

                    if (segment.Name[1] == '?')
                    {
                        pos.NameStart++;
                        segment.Kind = XmpPath.QualSelectorStep;
                        VerifyQualName(pos.Path.Substring(pos.NameStart, pos.NameEnd - pos.NameStart));
                    }
                }

                expandedXPath.Add(segment);
            }

            return(expandedXPath);
        }
Example #21
0
        /// <seealso cref= XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String,
        ///      boolean)
        /// </seealso>
        /// <param name="xmp">
        ///            The XMP object containing the array to be catenated. </param>
        /// <param name="schemaNs">
        ///            The schema namespace URI for the array. Must not be null or
        ///            the empty string. </param>
        /// <param name="arrayName">
        ///            The name of the array. May be a general path expression, must
        ///            not be null or the empty string. Each item in the array must
        ///            be a simple string value. </param>
        /// <param name="separator">
        ///            The string to be used to separate the items in the catenated
        ///            string. Defaults to &quot;; &quot;, ASCII semicolon and space
        ///            (U+003B, U+0020). </param>
        /// <param name="quotes">
        ///            The characters to be used as quotes around array items that
        ///            contain a separator. Defaults to &apos;&quot;&apos; </param>
        /// <param name="allowCommas">
        ///            Option flag to control the catenation. </param>
        /// <returns> Returns the string containing the catenated array items. </returns>
        /// <exception cref="XmpException">
        ///             Forwards the Exceptions from the metadata processing </exception>
        public static string CatenateArrayItems(IXmpMeta xmp, string schemaNs, string arrayName, string separator,
                                                string quotes, bool allowCommas)
        {
            ParameterAsserts.AssertSchemaNs(schemaNs);
            ParameterAsserts.AssertArrayName(arrayName);
            ParameterAsserts.AssertImplementation(xmp);
            if (string.IsNullOrEmpty(separator))
            {
                separator = "; ";
            }
            if (string.IsNullOrEmpty(quotes))
            {
                quotes = "\"";
            }

            XmpMetaImpl xmpImpl = (XmpMetaImpl)xmp;

            // Return an empty result if the array does not exist,
            // hurl if it isn't the right form.
            XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName);
            XmpNode arrayNode = XmpNodeUtils.FindNode(xmpImpl.Root, arrayPath, false, null);

            if (arrayNode == null)
            {
                return("");
            }
            if (!arrayNode.Options.Array || arrayNode.Options.ArrayAlternate)
            {
                throw new XmpException("Named property must be non-alternate array", XmpError.BADPARAM);
            }

            // Make sure the separator is OK.
            CheckSeparator(separator);
            // Make sure the open and close quotes are a legitimate pair.
            char openQuote  = quotes[0];
            char closeQuote = CheckQuotes(quotes, openQuote);

            // Build the result, quoting the array items, adding separators.
            // Hurl if any item isn't simple.

            StringBuilder catinatedString = new StringBuilder();

            for (IEnumerator it = arrayNode.IterateChildren(); it.MoveNext();)
            {
                XmpNode currItem = (XmpNode)it.Current;
                if (currItem == null)
                {
                    continue;
                }
                if (currItem.Options.CompositeProperty)
                {
                    throw new XmpException("Array items must be simple", XmpError.BADPARAM);
                }
                string str = ApplyQuotes(currItem.Value, openQuote, closeQuote, allowCommas);

                catinatedString.Append(str);
                if (it.MoveNext())
                {
                    catinatedString.Append(separator);
                }
            }

            return(catinatedString.ToString());
        }
Example #22
0
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="schemaNs">the root namespace</param>
        /// <param name="pos">the parsing position helper</param>
        /// <param name="expandedXPath">the path to contribute to</param>
        /// <exception cref="XmpException">If the path is not valid.</exception>
        private 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", XmpErrorCode.BadXPath);

            var rootProp = VerifyXPathRoot(schemaNs, pos.Path.Substring (pos.StepBegin, pos.StepEnd - pos.StepBegin));
            var aliasInfo = XmpMetaFactory.SchemaRegistry.FindAlias(rootProp);

            if (aliasInfo == null)
            {
                // add schema xpath step
                expandedXPath.Add(new XmpPathSegment(schemaNs, XmpPath.SchemaNode));
                var rootStep = new XmpPathSegment(rootProp, XmpPath.StructFieldStep);
                expandedXPath.Add(rootStep);
            }
            else
            {
                // add schema xpath step and base step of alias
                expandedXPath.Add(new XmpPathSegment(aliasInfo.Namespace, XmpPath.SchemaNode));
                var rootStep = new XmpPathSegment(VerifyXPathRoot(aliasInfo.Namespace, aliasInfo.PropName), XmpPath.StructFieldStep);
                rootStep.IsAlias = true;
                rootStep.AliasForm = aliasInfo.AliasForm.GetOptions();
                expandedXPath.Add(rootStep);

                if (aliasInfo.AliasForm.IsArrayAltText)
                {
                    var qualSelectorStep = new XmpPathSegment("[?xml:lang='x-default']", XmpPath.QualSelectorStep);
                    qualSelectorStep.IsAlias = true;
                    qualSelectorStep.AliasForm = aliasInfo.AliasForm.GetOptions();
                    expandedXPath.Add(qualSelectorStep);
                }
                else if (aliasInfo.AliasForm.IsArray)
                {
                    var indexStep = new XmpPathSegment("[1]", XmpPath.ArrayIndexStep);
                    indexStep.IsAlias = true;
                    indexStep.AliasForm = aliasInfo.AliasForm.GetOptions();
                    expandedXPath.Add(indexStep);
                }
            }
        }
Example #23
0
        /// <summary>
        /// Split an XMPPath expression apart at the conceptual steps, adding the
        /// root namespace prefix to the first property component.
        /// </summary>
        /// <remarks>
        /// 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.
        /// <para />
        /// In the most verbose case steps are separated by '/', and each step can be
        /// of these forms:
        /// <list>
        ///   <item>
        ///     <term>prefix:name</term>
        ///     <description>A top level property or struct field.</description>
        ///   </item>
        ///   <item>
        ///     <term>[index]</term>
        ///     <description>An element of an array.</description>
        ///   </item>
        ///   <item>
        ///     <term>[last()]</term>
        ///     <description>The last element of an array.</description>
        ///   </item>
        ///   <item>
        ///     <term>[fieldName=&quot;value&quot;]</term>
        ///     <description>An element in an array of structs, chosen by a field value.</description>
        ///   </item>
        ///   <item>
        ///     <term>[@xml:lang=&quot;value&quot;]</term>
        ///     <description>An element in an alt-text array, chosen by the xml:lang qualifier.</description>
        ///   </item>
        ///   <item>
        ///     <term>[?qualName=&quot;value&quot;]</term>
        ///     <description>An element in an array, chosen by a qualifier value.</description>
        ///   </item>
        ///   <item>
        ///     <term>@xml:lang</term>
        ///     <description>An xml:lang qualifier.</description>
        ///   </item>
        ///   <item>
        ///     <term>?qualName</term>
        ///     <description>A general qualifier.</description>
        ///   </item>
        /// </list>
        /// <para />
        /// 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]".
        /// <para />
        /// 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.
        /// <para />
        /// 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.
        /// </remarks>
        /// <param name="schemaNs">schema namespace</param>
        /// <param name="path">property name</param>
        /// <returns>Returns the expandet XMPPath.</returns>
        /// <exception cref="XmpException">Thrown if the format is not correct somehow.</exception>
        public static XmpPath ExpandXPath(string schemaNs, string path)
        {
            if (schemaNs == null || path == null)
                throw new XmpException("Parameter must not be null", XmpErrorCode.BadParam);

            var expandedXPath = new XmpPath();
            var pos = new PathPosition { 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;
                var segment = path[pos.StepBegin] != '['
                    ? ParseStructSegment(pos)
                    : ParseIndexSegment(pos);

                if (segment.Kind == XmpPath.StructFieldStep)
                {
                    if (segment.Name[0] == '@')
                    {
                        segment.Name = "?" + segment.Name.Substring (1);
                        if (!"?xml:lang".Equals(segment.Name))
                            throw new XmpException("Only xml:lang allowed with '@'", XmpErrorCode.BadXPath);
                    }

                    if (segment.Name[0] == '?')
                    {
                        pos.NameStart++;
                        segment.Kind = XmpPath.QualifierStep;
                    }

                    VerifyQualName(pos.Path.Substring (pos.NameStart, pos.NameEnd - pos.NameStart));
                }
                else if (segment.Kind == XmpPath.FieldSelectorStep)
                {
                    if (segment.Name[1] == '@')
                    {
                        segment.Name = "[?" + segment.Name.Substring (2);

                        if (!segment.Name.StartsWith("[?xml:lang="))
                            throw new XmpException("Only xml:lang allowed with '@'", XmpErrorCode.BadXPath);
                    }

                    if (segment.Name[1] == '?')
                    {
                        pos.NameStart++;
                        segment.Kind = XmpPath.QualSelectorStep;
                        VerifyQualName(pos.Path.Substring (pos.NameStart, pos.NameEnd - pos.NameStart));
                    }
                }

                expandedXPath.Add(segment);
            }

            return expandedXPath;
        }