/// <summary>Constructor for an empty metadata object.</summary> public XmpMeta() { // create root node _tree = new XmpNode(null, null, null); }
public XmpProperty(XmpNode itemNode) { _node = itemNode; _proptype = XmpPropertyType.item; }
/// <summary>Constructor for a cloned metadata tree.</summary> /// <param name="tree"> /// an prefilled metadata tree which fulfills all /// <c>XMPNode</c> contracts. /// </param> public XmpMeta(XmpNode tree) => _tree = tree;
/// <exception cref="XmpException"/> public 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); var arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName); // Find the array node and set the options if it was just created. var arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true, new PropertyOptions(PropertyOptions.ArrayFlag | PropertyOptions.ArrayOrderedFlag | PropertyOptions.ArrayAlternateFlag | PropertyOptions.ArrayAltTextFlag)); if (arrayNode == null) { throw new XmpException("Failed to find or create array node", XmpErrorCode.BadXPath); } if (!arrayNode.Options.IsArrayAltText) { if (!arrayNode.HasChildren && arrayNode.Options.IsArrayAlternate) { arrayNode.Options.IsArrayAltText = true; } else { throw new XmpException("Specified property is no alt-text array", XmpErrorCode.BadXPath); } } // Make sure the x-default item, if any, is first. var haveXDefault = false; XmpNode xdItem = null; for (var it = arrayNode.IterateChildren(); it.HasNext();) { var currItem = (XmpNode)it.Next(); if (!currItem.HasQualifier || currItem.GetQualifier(1).Name != XmpConstants.XmlLang) { throw new XmpException("Language qualifier must be first", XmpErrorCode.BadXPath); } if (currItem.GetQualifier(1).Value == XmpConstants.XDefault) { xdItem = currItem; haveXDefault = true; break; } } // Moves x-default to the beginning of the array if (xdItem != null && arrayNode.GetChildrenLength() > 1) { arrayNode.RemoveChild(xdItem); arrayNode.AddChild(1, xdItem); } // Find the appropriate item. // chooseLocalizedText will make sure the array is a language // alternative. var result = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang); var match = (int)result[0]; var itemNode = (XmpNode)result[1]; var specificXDefault = specificLang == XmpConstants.XDefault; switch (match) { case XmpNodeUtils.CltNoValues: { // Create the array items for the specificLang and x-default, with // x-default first. XmpNodeUtils.AppendLangItem(arrayNode, XmpConstants.XDefault, itemValue); haveXDefault = true; if (!specificXDefault) { XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); } break; } case XmpNodeUtils.CltSpecificMatch: { if (!specificXDefault) { // Update the specific item, update x-default if it matches the // old value. if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value == 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); for (var it1 = arrayNode.IterateChildren(); it1.HasNext();) { var currItem = (XmpNode)it1.Next(); if (currItem == xdItem || currItem.Value != xdItem?.Value) { continue; } currItem.Value = itemValue; } // And finally do the x-default item. if (xdItem != null) { xdItem.Value = itemValue; } } break; } case XmpNodeUtils.CltSingleGeneric: { // Update the generic item, update x-default if it matches the old // value. if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value == itemNode.Value) { xdItem.Value = itemValue; } itemNode.Value = itemValue; // ! Do this after // the x-default // check! break; } case XmpNodeUtils.CltMultipleGeneric: { // Create the specific language, ignore x-default. XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); if (specificXDefault) { haveXDefault = true; } break; } case XmpNodeUtils.CltXDefault: { // Create the specific language, update x-default if it was the only // item. if (xdItem != null && arrayNode.GetChildrenLength() == 1) { xdItem.Value = itemValue; } XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); break; } case XmpNodeUtils.CltFirstItem: { // 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", XmpErrorCode.InternalFailure); } } // Add an x-default at the front if needed. if (!haveXDefault && arrayNode.GetChildrenLength() == 1) { XmpNodeUtils.AppendLangItem(arrayNode, XmpConstants.XDefault, itemValue); } }
/// <summary> /// A node can be serialized as RDF-Attribute, if it meets the following conditions: /// <list type="bullet"> /// <item>is not array item</item> /// <item>don't has qualifier</item> /// <item>is no URI</item> /// <item>is no composite property</item> /// </list> /// </summary> /// <param name="node">an XMPNode</param> /// <returns>Returns true if the node serialized as RDF-Attribute</returns> private static bool CanBeRdfAttrProp(XmpNode node) { // FfF: other possibilities than []? if ( propNode->name[0] == '[' ) return false; return(!node.HasQualifier && !node.Options.IsUri && !node.Options.IsCompositeProperty && node.Name != XmpConstants.ArrayItemName); }
/// <summary>Moves an alias node of array form to another schema into an array</summary> /// <param name="propertyIt">the property iterator of the old schema (used to delete the property)</param> /// <param name="childNode">the node to be moved</param> /// <param name="baseArray">the base array for the array item</param> /// <exception cref="XmpException">Forwards XMP errors</exception> private static void TransplantArrayItemAlias(IIterator propertyIt, XmpNode childNode, XmpNode baseArray) { if (baseArray.Options.IsArrayAltText) { if (childNode.Options.HasLanguage) { throw new XmpException("Alias to x-default already has a language qualifier", XmpErrorCode.BadXmp); } var langQual = new XmpNode(XmpConstants.XmlLang, XmpConstants.XDefault, null); childNode.AddQualifier(langQual); } propertyIt.Remove(); childNode.Name = XmpConstants.ArrayItemName; baseArray.AddChild(childNode); }
/// <summary> /// </summary> /// <remarks> /// <list> /// <item>Look for an exact match with the specific language.</item> /// <item>If a generic language is given, look for partial matches.</item> /// <item>Look for an "x-default"-item.</item> /// <item>Choose the first item.</item> /// </list> /// </remarks> /// <param name="arrayNode">the alt text array node</param> /// <param name="genericLang">the generic language</param> /// <param name="specificLang">the specific language</param> /// <returns> /// Returns the kind of match as an Integer and the found node in an /// array. /// </returns> /// <exception cref="XmpException" /> internal static object[] ChooseLocalizedText(XmpNode arrayNode, string genericLang, string specificLang) { // See if the array has the right form. Allow empty alt arrays, // that is what parsing returns. if (!arrayNode.Options.IsArrayAltText) { throw new XmpException("Localized text array is not alt-text", XmpErrorCode.BadXPath); } if (!arrayNode.HasChildren) { return new object[] { CltNoValues, null } } ; var foundGenericMatches = 0; XmpNode resultNode = null; XmpNode xDefault = null; // Look for the first partial match with the generic language. for (var it = arrayNode.IterateChildren(); it.HasNext();) { var currItem = (XmpNode)it.Next(); // perform some checks on the current item if (currItem.Options.IsCompositeProperty) { throw new XmpException("Alt-text array item is not simple", XmpErrorCode.BadXPath); } if (!currItem.HasQualifier || currItem.GetQualifier(1).Name != XmpConstants.XmlLang) { throw new XmpException("Alt-text array item has no language qualifier", XmpErrorCode.BadXPath); } var currLang = currItem.GetQualifier(1).Value; // Look for an exact match with the specific language. if (currLang == specificLang) { return new object[] { CltSpecificMatch, currItem } } ; if (genericLang != null && currLang.StartsWith(genericLang)) { if (resultNode == null) { resultNode = currItem; } // ! Don't return/break, need to look for other matches. foundGenericMatches++; } else if (currLang == XmpConstants.XDefault) { xDefault = currItem; } } // evaluate loop if (foundGenericMatches == 1) { return new object[] { CltSingleGeneric, resultNode } } ; if (foundGenericMatches > 1) { return new object[] { CltMultipleGeneric, resultNode } } ; if (xDefault != null) { return new object[] { CltXDefault, xDefault } } ; // Everything failed, choose the first item. return(new object[] { CltFirstItem, arrayNode.GetChild(1) }); }
/// <summary>Recursively handles the "value" for a node.</summary> /// <remarks> /// Recursively handles the "value" for a node. It does not matter if it is a /// top level property, a field of a struct, or an item of an array. The /// indent is that for the property element. An xml:lang qualifier is written /// as an attribute of the property start tag, not by itself forcing the /// qualified property form. The patterns below mostly ignore attribute /// qualifiers like xml:lang. Except for the one struct case, attribute /// qualifiers don't affect the output form. /// <code> /// <ns:UnqualifiedSimpleProperty>value</ns:UnqualifiedSimpleProperty> /// <ns:UnqualifiedStructProperty> (If no rdf:resource qualifier) /// <rdf:Description> /// ... Fields, same forms as top level properties /// </rdf:Description> /// </ns:UnqualifiedStructProperty> /// <ns:ResourceStructProperty rdf:resource="URI" /// ... Fields as attributes /// > /// <ns:UnqualifiedArrayProperty> /// <rdf:Bag> or Seq or Alt /// ... Array items as rdf:li elements, same forms as top level properties /// </rdf:Bag> /// </ns:UnqualifiedArrayProperty> /// <ns:QualifiedProperty> /// <rdf:Description> /// <rdf:value> ... Property "value" following the unqualified /// forms ... </rdf:value> /// ... Qualifiers looking like named struct fields /// </rdf:Description> /// </ns:QualifiedProperty> /// </code> /// </remarks> /// <param name="node">the property node</param> /// <param name="emitAsRdfValue">property shall be rendered as attribute rather than tag</param> /// <param name="useCanonicalRdf"> /// use canonical form with inner description tag or /// the compact form with rdf:ParseType="resource" attribute. /// </param> /// <param name="indent">the current indent level</param> /// <exception cref="System.IO.IOException">Forwards all writer exceptions.</exception> /// <exception cref="XmpException">If "rdf:resource" and general qualifiers are mixed.</exception> private void SerializeCanonicalRdfProperty(XmpNode node, bool useCanonicalRdf, bool emitAsRdfValue, int indent) { var emitEndTag = true; var indentEndTag = true; // Determine the XML element name. Open the start tag with the name and // attribute qualifiers. var elemName = node.Name; if (emitAsRdfValue) { elemName = "rdf:value"; } else if (elemName == XmpConstants.ArrayItemName) { elemName = XmpConstants.RdfLi; } WriteIndent(indent); Write('<'); Write(elemName); var hasGeneralQualifiers = false; var hasRdfResourceQual = false; for (var it = node.IterateQualifier(); it.HasNext();) { var qualifier = (XmpNode)it.Next(); if (!RdfAttrQualifier.Contains(qualifier.Name)) { hasGeneralQualifiers = true; } else { hasRdfResourceQual = qualifier.Name == "rdf:resource"; if (!emitAsRdfValue) { Write(' '); Write(qualifier.Name); Write("=\""); AppendNodeValue(qualifier.Value, true); Write('"'); } } } // Process the property according to the standard patterns. if (hasGeneralQualifiers && !emitAsRdfValue) { // This node has general, non-attribute, qualifiers. Emit using the // qualified property form. // ! The value is output by a recursive call ON THE SAME NODE with // emitAsRDFValue set. if (hasRdfResourceQual) { throw new XmpException("Can't mix rdf:resource and general qualifiers", XmpErrorCode.BadRdf); } // Change serialization to canonical format with inner rdf:Description-tag // depending on option if (useCanonicalRdf) { Write(">"); WriteNewline(); indent++; WriteIndent(indent); Write(RdfStructStart); Write(">"); } else { Write(" rdf:parseType=\"Resource\">"); } WriteNewline(); SerializeCanonicalRdfProperty(node, useCanonicalRdf, true, indent + 1); for (var it = node.IterateQualifier(); it.HasNext();) { var qualifier = (XmpNode)it.Next(); if (!RdfAttrQualifier.Contains(qualifier.Name)) { SerializeCanonicalRdfProperty(qualifier, useCanonicalRdf, false, indent + 1); } } if (useCanonicalRdf) { WriteIndent(indent); Write(RdfStructEnd); WriteNewline(); indent--; } } else { // This node has no general qualifiers. Emit using an unqualified form. if (!node.Options.IsCompositeProperty) { // This is a simple property. if (node.Options.IsUri) { Write(" rdf:resource=\""); AppendNodeValue(node.Value, true); Write("\"/>"); WriteNewline(); emitEndTag = false; } else if (string.IsNullOrEmpty(node.Value)) { Write("/>"); WriteNewline(); emitEndTag = false; } else { Write('>'); AppendNodeValue(node.Value, false); indentEndTag = false; } } else { if (node.Options.IsArray) { // This is an array. Write('>'); WriteNewline(); EmitRdfArrayTag(node, true, indent + 1); if (node.Options.IsArrayAltText) { XmpNodeUtils.NormalizeLangArray(node); } for (var it1 = node.IterateChildren(); it1.HasNext();) { var child = (XmpNode)it1.Next(); SerializeCanonicalRdfProperty(child, useCanonicalRdf, false, indent + 2); } EmitRdfArrayTag(node, false, indent + 1); } else if (!hasRdfResourceQual) { // This is a "normal" struct, use the rdf:parseType="Resource" form. if (!node.HasChildren) { // Change serialization to canonical format with inner rdf:Description-tag // if option is set if (useCanonicalRdf) { Write(">"); WriteNewline(); WriteIndent(indent + 1); Write(RdfEmptyStruct); } else { Write(" rdf:parseType=\"Resource\"/>"); emitEndTag = false; } WriteNewline(); } else { // Change serialization to canonical format with inner rdf:Description-tag // if option is set if (useCanonicalRdf) { Write(">"); WriteNewline(); indent++; WriteIndent(indent); Write(RdfStructStart); Write(">"); } else { Write(" rdf:parseType=\"Resource\">"); } WriteNewline(); for (var it = node.IterateChildren(); it.HasNext();) { var child = (XmpNode)it.Next(); SerializeCanonicalRdfProperty(child, useCanonicalRdf, false, indent + 1); } if (useCanonicalRdf) { WriteIndent(indent); Write(RdfStructEnd); WriteNewline(); indent--; } } } else { // This is a struct with an rdf:resource attribute, use the // "empty property element" form. for (var it1 = node.IterateChildren(); it1.HasNext();) { var child = (XmpNode)it1.Next(); if (!CanBeRdfAttrProp(child)) { throw new XmpException("Can't mix rdf:resource and complex fields", XmpErrorCode.BadRdf); } WriteNewline(); WriteIndent(indent + 1); Write(' '); Write(child.Name); Write("=\""); AppendNodeValue(child.Value, true); Write('"'); } Write("/>"); WriteNewline(); emitEndTag = false; } } } // Emit the property element end tag. if (emitEndTag) { if (indentEndTag) { WriteIndent(indent); } Write("</"); Write(elemName); Write('>'); WriteNewline(); } }
/// <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); }
/// <summary>Find or create a schema node if <c>createNodes</c> is false and</summary> /// <param name="tree">the root of the xmp tree.</param> /// <param name="namespaceUri">a namespace</param> /// <param name="createNodes"> /// a flag indicating if the node shall be created if not found. /// <em>Note:</em> The namespace must be registered prior to this call. /// </param> /// <returns> /// Returns the schema node if found, <c>null</c> otherwise. /// Note: If <c>createNodes</c> is <c>true</c>, it is <b>always</b> /// returned a valid node. /// </returns> /// <exception cref="XmpException"> /// An exception is only thrown if an error occurred, not if a /// node was not found. /// </exception> internal static XmpNode FindSchemaNode(XmpNode tree, string namespaceUri, bool createNodes) { return(FindSchemaNode(tree, namespaceUri, null, createNodes)); }
/// <summary> /// After processing by ExpandXPath, a step can be of these forms: /// </summary> /// <remarks> /// After processing by ExpandXPath, a step can be of these forms: /// <list type="bullet"> /// <item>qualName - A top level property or struct field.</item> /// <item>[index] - An element of an array.</item> /// <item>[last()] - The last element of an array.</item> /// <item>[qualName="value"] - An element in an array of structs, chosen by a field value.</item> /// <item>[?qualName="value"] - An element in an array, chosen by a qualifier value.</item> /// <item>?qualName - A general qualifier.</item> /// </list> /// Find the appropriate child node, resolving aliases, and optionally creating nodes. /// </remarks> /// <param name="parentNode">the node to start to start from</param> /// <param name="nextStep">the xpath segment</param> /// <param name="createNodes"></param> /// <returns>returns the found or created XMPPath node</returns> /// <exception cref="XmpException"></exception> private static XmpNode FollowXPathStep(XmpNode parentNode, XmpPathSegment nextStep, bool createNodes) { XmpNode nextNode = null; var stepKind = nextStep.Kind; if (stepKind == XmpPath.StructFieldStep) { nextNode = FindChildNode(parentNode, nextStep.Name, createNodes); } else { if (stepKind == XmpPath.QualifierStep) { nextNode = FindQualifierNode(parentNode, nextStep.Name.Substring(1), createNodes); } else { // This is an array indexing step. First get the index, then get the node. if (!parentNode.Options.IsArray) { throw new XmpException("Indexing applied to non-array", XmpErrorCode.BadXPath); } var index = 0; if (stepKind == XmpPath.ArrayIndexStep) { index = FindIndexedItem(parentNode, nextStep.Name, createNodes); } else { if (stepKind == XmpPath.ArrayLastStep) { index = parentNode.GetChildrenLength(); } else { if (stepKind == XmpPath.FieldSelectorStep) { var result = Utils.SplitNameAndValue(nextStep.Name); var fieldName = result[0]; var fieldValue = result[1]; index = LookupFieldSelector(parentNode, fieldName, fieldValue); } else { if (stepKind == XmpPath.QualSelectorStep) { var result = Utils.SplitNameAndValue(nextStep.Name); var qualName = result[0]; var qualValue = result[1]; index = LookupQualSelector(parentNode, qualName, qualValue, nextStep.AliasForm); } else { throw new XmpException("Unknown array indexing step in FollowXPathStep", XmpErrorCode.InternalFailure); } } } } if (1 <= index && index <= parentNode.GetChildrenLength()) { nextNode = parentNode.GetChild(index); } } } return(nextNode); }
/// <summary>Compares two nodes including its children and qualifier.</summary> /// <param name="leftNode">an <c>XMPNode</c></param> /// <param name="rightNode">an <c>XMPNode</c></param> /// <returns>Returns true if the nodes are equal, false otherwise.</returns> /// <exception cref="XmpException">Forwards exceptions to the calling method.</exception> private static bool ItemValuesMatch(XmpNode leftNode, XmpNode rightNode) { var leftForm = leftNode.Options; var rightForm = rightNode.Options; if (leftForm.Equals(rightForm)) { return(false); } if (leftForm.GetOptions() == 0) { // Simple nodes, check the values and xml:lang qualifiers. if (leftNode.Value != rightNode.Value) { return(false); } if (leftNode.Options.HasLanguage != rightNode.Options.HasLanguage) { return(false); } if (leftNode.Options.HasLanguage && leftNode.GetQualifier(1).Value != rightNode.GetQualifier(1).Value) { return(false); } } else { if (leftForm.IsStruct) { // Struct nodes, see if all fields match, ignoring order. if (leftNode.GetChildrenLength() != rightNode.GetChildrenLength()) { return(false); } for (var it = leftNode.IterateChildren(); it.HasNext();) { var leftField = (XmpNode)it.Next(); var rightField = XmpNodeUtils.FindChildNode(rightNode, leftField.Name, false); if (rightField == null || !ItemValuesMatch(leftField, rightField)) { return(false); } } } else { // Array nodes, see if the "leftNode" values are present in the // "rightNode", ignoring order, duplicates, // and extra values in the rightNode-> The rightNode is the // destination for AppendProperties. Debug.Assert(leftForm.IsArray); for (var il = leftNode.IterateChildren(); il.HasNext();) { var leftItem = (XmpNode)il.Next(); var match = false; for (var ir = rightNode.IterateChildren(); ir.HasNext();) { var rightItem = (XmpNode)ir.Next(); if (ItemValuesMatch(leftItem, rightItem)) { match = true; break; } } if (!match) { return(false); } } } } return(true); }
/// <param name="destXmp">The destination XMP object.</param> /// <param name="sourceNode">the source node</param> /// <param name="destParent">the parent of the destination node</param> /// <param name="replaceOldValues">Replace the values of existing properties.</param> /// <param name="deleteEmptyValues">flag if properties with empty values should be deleted in the destination object.</param> /// <exception cref="XmpException" /> private static void AppendSubtree(XmpMeta destXmp, XmpNode sourceNode, XmpNode destParent, bool replaceOldValues, bool deleteEmptyValues) { var destNode = XmpNodeUtils.FindChildNode(destParent, sourceNode.Name, false); var valueIsEmpty = false; if (deleteEmptyValues) { valueIsEmpty = sourceNode.Options.IsSimple ? string.IsNullOrEmpty(sourceNode.Value) : !sourceNode.HasChildren; } if (deleteEmptyValues && valueIsEmpty) { if (destNode != null) { destParent.RemoveChild(destNode); } } else { if (destNode == null) { // The one easy case, the destination does not exist. destParent.AddChild((XmpNode)sourceNode.Clone()); } else { if (replaceOldValues) { // The destination exists and should be replaced. XmpMeta.SetNode(destNode, sourceNode.Value, sourceNode.Options, true); destParent.RemoveChild(destNode); destNode = (XmpNode)sourceNode.Clone(); destParent.AddChild(destNode); } else { // The destination exists and is not totally replaced. Structs and arrays are merged. var sourceForm = sourceNode.Options; var destForm = destNode.Options; if (!Equals(sourceForm, destForm)) { return; } if (sourceForm.IsStruct) { // To merge a struct process the fields recursively. E.g. add simple missing fields. // The recursive call to AppendSubtree will handle deletion for fields with empty // values. for (var it = sourceNode.IterateChildren(); it.HasNext();) { var sourceField = (XmpNode)it.Next(); AppendSubtree(destXmp, sourceField, destNode, replaceOldValues, deleteEmptyValues); if (deleteEmptyValues && !destNode.HasChildren) { destParent.RemoveChild(destNode); } } } else if (sourceForm.IsArrayAltText) { // Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first. // Make a special check for deletion of empty values. Meaningful in AltText arrays // because the "xml:lang" qualifier provides unambiguous source/dest correspondence. for (var it = sourceNode.IterateChildren(); it.HasNext();) { var sourceItem = (XmpNode)it.Next(); if (!sourceItem.HasQualifier || sourceItem.GetQualifier(1).Name != XmpConstants.XmlLang) { continue; } var destIndex = XmpNodeUtils.LookupLanguageItem(destNode, sourceItem.GetQualifier(1).Value); if (deleteEmptyValues && string.IsNullOrEmpty(sourceItem.Value)) { if (destIndex != -1) { destNode.RemoveChild(destIndex); if (!destNode.HasChildren) { destParent.RemoveChild(destNode); } } } else if (destIndex == -1) { // Not replacing, keep the existing item. if (sourceItem.GetQualifier(1).Value != XmpConstants.XDefault || !destNode.HasChildren) { sourceItem.CloneSubtree(destNode); } else { var destItem = new XmpNode(sourceItem.Name, sourceItem.Value, sourceItem.Options); sourceItem.CloneSubtree(destItem); destNode.AddChild(1, destItem); } } } } else if (sourceForm.IsArray) { // Merge other arrays by item values. Don't worry about order or duplicates. Source // items with empty values do not cause deletion, that conflicts horribly with // merging. for (var children = sourceNode.IterateChildren(); children.HasNext();) { var sourceItem = (XmpNode)children.Next(); var match = false; for (var id = destNode.IterateChildren(); id.HasNext();) { var destItem = (XmpNode)id.Next(); if (ItemValuesMatch(sourceItem, destItem)) { match = true; } } if (!match) { destNode = (XmpNode)sourceItem.Clone(); destParent.AddChild(destNode); } } } } } } }
/// <summary>Constructor for a cloned metadata tree.</summary> /// <param name="tree"> /// an prefilled metadata tree which fulfills all /// <c>XMPNode</c> contracts. /// </param> public XmpMeta(XmpNode tree) { _tree = tree; }
/// <summary>Adds a node as child to this node.</summary> /// <param name="index"> /// the index of the node <em>before</em> which the new one is inserted. /// <em>Note:</em> The node children are indexed from [1..size]! /// An index of size + 1 appends a node. /// </param> /// <param name="node">an XMPNode</param> /// <exception cref="XmpException"></exception> public void AddChild(int index, XmpNode node) { AssertChildNotExisting(node.Name); node.Parent = this; GetChildren().Insert(index - 1, node); }
/// <summary>Serializes a struct property.</summary> /// <param name="node">an XMPNode</param> /// <param name="indent">the current indent level</param> /// <param name="hasRdfResourceQual">Flag if the element has resource qualifier</param> /// <returns>Returns true if an end flag shall be emitted.</returns> /// <exception cref="System.IO.IOException">Forwards the writer exceptions.</exception> /// <exception cref="XmpException">If qualifier and element fields are mixed.</exception> private bool SerializeCompactRdfStructProp(XmpNode node, int indent, bool hasRdfResourceQual) { // This must be a struct. var hasAttrFields = false; var hasElemFields = false; var emitEndTag = true; for (var ic = node.IterateChildren(); ic.HasNext();) { var field = (XmpNode)ic.Next(); if (CanBeRdfAttrProp(field)) { hasAttrFields = true; } else { hasElemFields = true; } if (hasAttrFields && hasElemFields) { break; } } // No sense looking further. if (hasRdfResourceQual && hasElemFields) { throw new XmpException("Can't mix rdf:resource qualifier and element fields", XmpErrorCode.BadRdf); } if (!node.HasChildren) { // Catch an empty struct as a special case. The case // below would emit an empty // XML element, which gets reparsed as a simple property // with an empty value. Write(" rdf:parseType=\"Resource\"/>"); WriteNewline(); emitEndTag = false; } else if (!hasElemFields) { // All fields can be attributes, use the // emptyPropertyElt form. SerializeCompactRdfAttrProps(node, indent + 1); Write("/>"); WriteNewline(); emitEndTag = false; } else if (!hasAttrFields) { // All fields must be elements, use the // parseTypeResourcePropertyElt form. Write(" rdf:parseType=\"Resource\">"); WriteNewline(); SerializeCompactRdfElementProps(node, indent + 1); } else { // Have a mix of attributes and elements, use an inner rdf:Description. Write('>'); WriteNewline(); WriteIndent(indent + 1); Write(RdfStructStart); SerializeCompactRdfAttrProps(node, indent + 2); Write(">"); WriteNewline(); SerializeCompactRdfElementProps(node, indent + 1); WriteIndent(indent + 1); Write(RdfStructEnd); WriteNewline(); } return(emitEndTag); }
/// <summary>Replaces a node with another one.</summary> /// <param name="index"> /// the index of the node that will be replaced. /// <em>Note:</em> The node children are indexed from [1..size]! /// </param> /// <param name="node">the replacement XMPNode</param> public void ReplaceChild(int index, XmpNode node) { node.Parent = this; GetChildren()[index - 1] = node; }
/// <summary>Creates a property info object from an <c>XMPNode</c>.</summary> /// <param name="node">an <c>XMPNode</c></param> /// <param name="baseNs">the base namespace to report</param> /// <param name="path">the full property path</param> /// <returns>Returns a <c>XMPProperty</c>-object that serves representation of the node.</returns> protected static IXmpPropertyInfo CreatePropertyInfo(XmpNode node, string baseNs, string path) { var value = node.Options.IsSchemaNode ? null : node.Value; return(new XmpPropertyInfo(node, baseNs, path, value)); }
/// <summary>Removes a child node.</summary> /// <remarks> /// Removes a child node. /// If its a schema node and doesn't have any children anymore, its deleted. /// </remarks> /// <param name="node">the child node to delete.</param> public void RemoveChild(XmpNode node) { GetChildren().Remove(node); CleanupChildren(); }
/// <summary> /// Recursively handles the "value" for a node that must be written as an RDF /// property element. /// </summary> /// <remarks> /// Recursively handles the "value" for a node that must be written as an RDF /// property element. It does not matter if it is a top level property, a /// field of a struct, or an item of an array. The indent is that for the /// property element. The patterns below ignore attribute qualifiers such as /// xml:lang, they don't affect the output form. /// <code> /// <ns:UnqualifiedStructProperty-1 /// ... The fields as attributes, if all are simple and unqualified /// /> /// <ns:UnqualifiedStructProperty-2 rdf:parseType="Resource"> /// ... The fields as elements, if none are simple and unqualified /// </ns:UnqualifiedStructProperty-2> /// <ns:UnqualifiedStructProperty-3> /// <rdf:Description /// ... The simple and unqualified fields as attributes /// > /// ... The compound or qualified fields as elements /// </rdf:Description> /// </ns:UnqualifiedStructProperty-3> /// <ns:UnqualifiedArrayProperty> /// <rdf:Bag> or Seq or Alt /// ... Array items as rdf:li elements, same forms as top level properties /// </rdf:Bag> /// </ns:UnqualifiedArrayProperty> /// <ns:QualifiedProperty rdf:parseType="Resource"> /// <rdf:value> ... Property "value" /// following the unqualified forms ... </rdf:value> /// ... Qualifiers looking like named struct fields /// </ns:QualifiedProperty> /// </code> /// *** Consider numbered array items, but has compatibility problems. /// Consider qualified form with rdf:Description and attributes. /// </remarks> /// <param name="parentNode">the parent node</param> /// <param name="indent">the current indent level</param> /// <exception cref="System.IO.IOException">Forwards writer exceptions</exception> /// <exception cref="XmpException">If qualifier and element fields are mixed.</exception> private void SerializeCompactRdfElementProps(XmpNode parentNode, int indent) { for (var it = parentNode.IterateChildren(); it.HasNext();) { var node = (XmpNode)it.Next(); if (CanBeRdfAttrProp(node)) { continue; } var emitEndTag = true; var indentEndTag = true; // Determine the XML element name, write the name part of the start tag. Look over the // qualifiers to decide on "normal" versus "rdf:value" form. Emit the attribute // qualifiers at the same time. var elemName = node.Name; if (elemName == XmpConstants.ArrayItemName) { elemName = XmpConstants.RdfLi; } WriteIndent(indent); Write('<'); Write(elemName); var hasGeneralQualifiers = false; var hasRdfResourceQual = false; for (var iq = node.IterateQualifier(); iq.HasNext();) { var qualifier = (XmpNode)iq.Next(); if (!RdfAttrQualifier.Contains(qualifier.Name)) { hasGeneralQualifiers = true; } else { hasRdfResourceQual = qualifier.Name == "rdf:resource"; Write(' '); Write(qualifier.Name); Write("=\""); AppendNodeValue(qualifier.Value, true); Write('"'); } } // Process the property according to the standard patterns. if (hasGeneralQualifiers) { SerializeCompactRdfGeneralQualifier(indent, node); } else { // This node has only attribute qualifiers. Emit as a property element. if (!node.Options.IsCompositeProperty) { var result = SerializeCompactRdfSimpleProp(node); emitEndTag = (bool)result[0]; indentEndTag = (bool)result[1]; } else { if (node.Options.IsArray) { SerializeCompactRdfArrayProp(node, indent); } else { emitEndTag = SerializeCompactRdfStructProp(node, indent, hasRdfResourceQual); } } } // Emit the property element end tag. if (emitEndTag) { if (indentEndTag) { WriteIndent(indent); } Write("</"); Write(elemName); Write('>'); WriteNewline(); } } }
/// <summary>Visit all of the top level nodes looking for aliases.</summary> /// <remarks> /// Visit all of the top level nodes looking for aliases. If there is /// no base, transplant the alias subtree. If there is a base and strict /// aliasing is on, make sure the alias and base subtrees match. /// </remarks> /// <param name="tree">the root of the metadata tree</param> /// <param name="options">th parsing options</param> /// <exception cref="XmpException">Forwards XMP errors</exception> private static void MoveExplicitAliases(XmpNode tree, ParseOptions options) { if (!tree.HasAliases) { return; } tree.HasAliases = false; var strictAliasing = options.StrictAliasing; for (var schemaIt = tree.GetUnmodifiableChildren().Iterator(); schemaIt.HasNext();) { var currSchema = (XmpNode)schemaIt.Next(); if (!currSchema.HasAliases) { continue; } for (var propertyIt = currSchema.IterateChildren(); propertyIt.HasNext();) { var currProp = (XmpNode)propertyIt.Next(); if (!currProp.IsAlias) { continue; } currProp.IsAlias = false; // Find the base path, look for the base schema and root node. var info = XmpMetaFactory.SchemaRegistry.FindAlias(currProp.Name); if (info != null) { // find or create schema var baseSchema = XmpNodeUtils.FindSchemaNode(tree, info.Namespace, null, true); baseSchema.IsImplicit = false; var baseNode = XmpNodeUtils.FindChildNode(baseSchema, info.Prefix + info.PropName, false); if (baseNode == null) { if (info.AliasForm.IsSimple()) { // A top-to-top alias, transplant the property. // change the alias property name to the base name var qname = info.Prefix + info.PropName; currProp.Name = qname; baseSchema.AddChild(currProp); // remove the alias property propertyIt.Remove(); } else { // An alias to an array item, // create the array and transplant the property. baseNode = new XmpNode(info.Prefix + info.PropName, info.AliasForm.ToPropertyOptions()); baseSchema.AddChild(baseNode); TransplantArrayItemAlias(propertyIt, currProp, baseNode); } } else if (info.AliasForm.IsSimple()) { // The base node does exist and this is a top-to-top alias. // Check for conflicts if strict aliasing is on. // Remove and delete the alias subtree. if (strictAliasing) { CompareAliasedSubtrees(currProp, baseNode, true); } propertyIt.Remove(); } else { // This is an alias to an array item and the array exists. // Look for the aliased item. // Then transplant or check & delete as appropriate. XmpNode itemNode = null; if (info.AliasForm.IsArrayAltText) { var xdIndex = XmpNodeUtils.LookupLanguageItem(baseNode, XmpConstants.XDefault); if (xdIndex != -1) { itemNode = baseNode.GetChild(xdIndex); } } else if (baseNode.HasChildren) { itemNode = baseNode.GetChild(1); } if (itemNode == null) { TransplantArrayItemAlias(propertyIt, currProp, baseNode); } else if (strictAliasing) { CompareAliasedSubtrees(currProp, itemNode, true); } propertyIt.Remove(); } } } currSchema.HasAliases = false; } }