| // ================================================================================================= |
| // ADOBE SYSTEMS INCORPORATED |
| // Copyright 2006 Adobe Systems Incorporated |
| // All Rights Reserved |
| // |
| // NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms |
| // of the Adobe license agreement accompanying it. |
| // ================================================================================================= |
| |
| |
| |
| package com.adobe.xmp.impl; |
| |
| import java.util.Iterator; |
| |
| import com.adobe.xmp.XMPConst; |
| import com.adobe.xmp.XMPError; |
| import com.adobe.xmp.XMPException; |
| import com.adobe.xmp.XMPMeta; |
| import com.adobe.xmp.XMPMetaFactory; |
| import com.adobe.xmp.XMPUtils; |
| import com.adobe.xmp.impl.xpath.XMPPath; |
| import com.adobe.xmp.impl.xpath.XMPPathParser; |
| import com.adobe.xmp.options.PropertyOptions; |
| import com.adobe.xmp.properties.XMPAliasInfo; |
| |
| |
| |
| /** |
| * @since 11.08.2006 |
| */ |
| public class XMPUtilsImpl implements XMPConst |
| { |
| /** */ |
| private static final int UCK_NORMAL = 0; |
| /** */ |
| private static final int UCK_SPACE = 1; |
| /** */ |
| private static final int UCK_COMMA = 2; |
| /** */ |
| private static final int UCK_SEMICOLON = 3; |
| /** */ |
| private static final int UCK_QUOTE = 4; |
| /** */ |
| private static final int UCK_CONTROL = 5; |
| |
| |
| /** |
| * Private constructor, as |
| */ |
| private XMPUtilsImpl() |
| { |
| // EMPTY |
| } |
| |
| |
| /** |
| * @see XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String, |
| * boolean) |
| * |
| * @param xmp |
| * The XMP object containing the array to be catenated. |
| * @param schemaNS |
| * The schema namespace URI for the array. Must not be null or |
| * the empty string. |
| * @param 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 separator |
| * The string to be used to separate the items in the catenated |
| * string. Defaults to "; ", ASCII semicolon and space |
| * (U+003B, U+0020). |
| * @param quotes |
| * The characters to be used as quotes around array items that |
| * contain a separator. Defaults to '"' |
| * @param allowCommas |
| * Option flag to control the catenation. |
| * @return Returns the string containing the catenated array items. |
| * @throws XMPException |
| * Forwards the Exceptions from the metadata processing |
| */ |
| public static String catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, |
| String separator, String quotes, boolean allowCommas) throws XMPException |
| { |
| ParameterAsserts.assertSchemaNS(schemaNS); |
| ParameterAsserts.assertArrayName(arrayName); |
| ParameterAsserts.assertImplementation(xmp); |
| if (separator == null || separator.length() == 0) |
| { |
| separator = "; "; |
| } |
| if (quotes == null || quotes.length() == 0) |
| { |
| quotes = "\""; |
| } |
| |
| XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; |
| XMPNode arrayNode = null; |
| XMPNode currItem = null; |
| |
| // Return an empty result if the array does not exist, |
| // hurl if it isn't the right form. |
| XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); |
| arrayNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), arrayPath, false, null); |
| if (arrayNode == null) |
| { |
| return ""; |
| } |
| else if (!arrayNode.getOptions().isArray() || arrayNode.getOptions().isArrayAlternate()) |
| { |
| 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.charAt(0); |
| char closeQuote = checkQuotes(quotes, openQuote); |
| |
| // Build the result, quoting the array items, adding separators. |
| // Hurl if any item isn't simple. |
| |
| StringBuffer catinatedString = new StringBuffer(); |
| |
| for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) |
| { |
| currItem = (XMPNode) it.next(); |
| if (currItem.getOptions().isCompositeProperty()) |
| { |
| throw new XMPException("Array items must be simple", XMPError.BADPARAM); |
| } |
| String str = applyQuotes(currItem.getValue(), openQuote, closeQuote, allowCommas); |
| |
| catinatedString.append(str); |
| if (it.hasNext()) |
| { |
| catinatedString.append(separator); |
| } |
| } |
| |
| return catinatedString.toString(); |
| } |
| |
| |
| /** |
| * see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String, |
| * PropertyOptions, boolean)} |
| * |
| * @param xmp |
| * The XMP object containing the array to be updated. |
| * @param schemaNS |
| * The schema namespace URI for the array. Must not be null or |
| * the empty string. |
| * @param 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 catedStr |
| * The string to be separated into the array items. |
| * @param arrayOptions |
| * Option flags to control the separation. |
| * @param preserveCommas |
| * Flag if commas shall be preserved |
| * |
| * @throws XMPException |
| * Forwards the Exceptions from the metadata processing |
| */ |
| public static void separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, |
| String catedStr, PropertyOptions arrayOptions, boolean preserveCommas) |
| throws XMPException |
| { |
| ParameterAsserts.assertSchemaNS(schemaNS); |
| ParameterAsserts.assertArrayName(arrayName); |
| if (catedStr == null) |
| { |
| throw new XMPException("Parameter must not be null", XMPError.BADPARAM); |
| } |
| ParameterAsserts.assertImplementation(xmp); |
| XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; |
| |
| // Keep a zero value, has special meaning below. |
| XMPNode arrayNode = separateFindCreateArray(schemaNS, arrayName, arrayOptions, xmpImpl); |
| |
| // Extract the item values one at a time, until the whole input string is done. |
| String itemValue; |
| int itemStart, itemEnd; |
| int nextKind = UCK_NORMAL, charKind = UCK_NORMAL; |
| char ch = 0, nextChar = 0; |
| |
| itemEnd = 0; |
| int endPos = catedStr.length(); |
| while (itemEnd < endPos) |
| { |
| // Skip any leading spaces and separation characters. Always skip commas here. |
| // They can be kept when within a value, but not when alone between values. |
| for (itemStart = itemEnd; itemStart < endPos; itemStart++) |
| { |
| ch = catedStr.charAt(itemStart); |
| charKind = classifyCharacter(ch); |
| if (charKind == UCK_NORMAL || charKind == UCK_QUOTE) |
| { |
| break; |
| } |
| } |
| if (itemStart >= endPos) |
| { |
| break; |
| } |
| |
| if (charKind != UCK_QUOTE) |
| { |
| // This is not a quoted value. Scan for the end, create an array |
| // item from the substring. |
| for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) |
| { |
| ch = catedStr.charAt(itemEnd); |
| charKind = classifyCharacter(ch); |
| |
| if (charKind == UCK_NORMAL || charKind == UCK_QUOTE || |
| (charKind == UCK_COMMA && preserveCommas)) |
| { |
| continue; |
| } |
| else if (charKind != UCK_SPACE) |
| { |
| break; |
| } |
| else if ((itemEnd + 1) < endPos) |
| { |
| ch = catedStr.charAt(itemEnd + 1); |
| nextKind = classifyCharacter(ch); |
| if (nextKind == UCK_NORMAL || nextKind == UCK_QUOTE || |
| (nextKind == UCK_COMMA && preserveCommas)) |
| { |
| continue; |
| } |
| } |
| |
| // Anything left? |
| break; // Have multiple spaces, or a space followed by a |
| // separator. |
| } |
| itemValue = catedStr.substring(itemStart, itemEnd); |
| } |
| else |
| { |
| // Accumulate quoted values into a local string, undoubling |
| // internal quotes that |
| // match the surrounding quotes. Do not undouble "unmatching" |
| // quotes. |
| |
| char openQuote = ch; |
| char closeQuote = getClosingQuote(openQuote); |
| |
| itemStart++; // Skip the opening quote; |
| itemValue = ""; |
| |
| for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) |
| { |
| ch = catedStr.charAt(itemEnd); |
| charKind = classifyCharacter(ch); |
| |
| if (charKind != UCK_QUOTE || !isSurroundingQuote(ch, openQuote, closeQuote)) |
| { |
| // This is not a matching quote, just append it to the |
| // item value. |
| itemValue += ch; |
| } |
| else |
| { |
| // This is a "matching" quote. Is it doubled, or the |
| // final closing quote? |
| // Tolerate various edge cases like undoubled opening |
| // (non-closing) quotes, |
| // or end of input. |
| |
| if ((itemEnd + 1) < endPos) |
| { |
| nextChar = catedStr.charAt(itemEnd + 1); |
| nextKind = classifyCharacter(nextChar); |
| } |
| else |
| { |
| nextKind = UCK_SEMICOLON; |
| nextChar = 0x3B; |
| } |
| |
| if (ch == nextChar) |
| { |
| // This is doubled, copy it and skip the double. |
| itemValue += ch; |
| // Loop will add in charSize. |
| itemEnd++; |
| } |
| else if (!isClosingingQuote(ch, openQuote, closeQuote)) |
| { |
| // This is an undoubled, non-closing quote, copy it. |
| itemValue += ch; |
| } |
| else |
| { |
| // This is an undoubled closing quote, skip it and |
| // exit the loop. |
| itemEnd++; |
| break; |
| } |
| } |
| } |
| } |
| |
| // Add the separated item to the array. |
| // Keep a matching old value in case it had separators. |
| int foundIndex = -1; |
| for (int oldChild = 1; oldChild <= arrayNode.getChildrenLength(); oldChild++) |
| { |
| if (itemValue.equals(arrayNode.getChild(oldChild).getValue())) |
| { |
| foundIndex = oldChild; |
| break; |
| } |
| } |
| |
| XMPNode newItem = null; |
| if (foundIndex < 0) |
| { |
| newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null); |
| arrayNode.addChild(newItem); |
| } |
| } |
| } |
| |
| |
| /** |
| * Utility to find or create the array used by <code>separateArrayItems()</code>. |
| * @param schemaNS a the namespace fo the array |
| * @param arrayName the name of the array |
| * @param arrayOptions the options for the array if newly created |
| * @param xmp the xmp object |
| * @return Returns the array node. |
| * @throws XMPException Forwards exceptions |
| */ |
| private static XMPNode separateFindCreateArray(String schemaNS, String arrayName, |
| PropertyOptions arrayOptions, XMPMetaImpl xmp) throws XMPException |
| { |
| arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null); |
| if (!arrayOptions.isOnlyArrayOptions()) |
| { |
| 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.getRoot(), arrayPath, false, null); |
| if (arrayNode != null) |
| { |
| // The array exists, make sure the form is compatible. Zero |
| // arrayForm means take what exists. |
| PropertyOptions arrayForm = arrayNode.getOptions(); |
| if (!arrayForm.isArray() || arrayForm.isArrayAlternate()) |
| { |
| 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 |
| arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, true, arrayOptions |
| .setArray(true)); |
| if (arrayNode == null) |
| { |
| throw new XMPException("Failed to create named array", XMPError.BADXPATH); |
| } |
| } |
| return arrayNode; |
| } |
| |
| |
| /** |
| * @see XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean) |
| * |
| * @param xmp |
| * The XMP object containing the properties to be removed. |
| * |
| * @param schemaNS |
| * Optional schema namespace URI for the properties to be |
| * removed. |
| * |
| * @param propName |
| * Optional path expression for the property to be removed. |
| * |
| * @param doAllProperties |
| * Option flag to control the deletion: do internal properties in |
| * addition to external properties. |
| * @param includeAliases |
| * Option flag to control the deletion: Include aliases in the |
| * "named schema" case above. |
| * @throws XMPException If metadata processing fails |
| */ |
| public static void removeProperties(XMPMeta xmp, String schemaNS, String propName, |
| boolean doAllProperties, boolean includeAliases) throws XMPException |
| { |
| ParameterAsserts.assertImplementation(xmp); |
| XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; |
| |
| if (propName != null && propName.length() > 0) |
| { |
| // 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 (schemaNS == null || schemaNS.length() == 0) |
| { |
| throw new XMPException("Property name requires schema namespace", |
| XMPError.BADPARAM); |
| } |
| |
| XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); |
| |
| XMPNode propNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), expPath, false, null); |
| if (propNode != null) |
| { |
| if (doAllProperties |
| || !Utils.isInternalProperty(expPath.getSegment(XMPPath.STEP_SCHEMA) |
| .getName(), expPath.getSegment(XMPPath.STEP_ROOT_PROP).getName())) |
| { |
| XMPNode parent = propNode.getParent(); |
| parent.removeChild(propNode); |
| if (parent.getOptions().isSchemaNode() && !parent.hasChildren()) |
| { |
| // remove empty schema node |
| parent.getParent().removeChild(parent); |
| } |
| |
| } |
| } |
| } |
| else if (schemaNS != null && schemaNS.length() > 0) |
| { |
| |
| // 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.getRoot(), schemaNS, false); |
| if (schemaNode != null) |
| { |
| if (removeSchemaChildren(schemaNode, doAllProperties)) |
| { |
| xmpImpl.getRoot().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. |
| |
| XMPAliasInfo[] aliases = XMPMetaFactory.getSchemaRegistry().findAliases(schemaNS); |
| for (int i = 0; i < aliases.length; i++) |
| { |
| XMPAliasInfo info = aliases[i]; |
| XMPPath path = XMPPathParser.expandXPath(info.getNamespace(), info |
| .getPropName()); |
| XMPNode actualProp = XMPNodeUtils |
| .findNode(xmpImpl.getRoot(), path, false, null); |
| if (actualProp != null) |
| { |
| XMPNode parent = actualProp.getParent(); |
| 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. |
| for (Iterator it = xmpImpl.getRoot().iterateChildren(); it.hasNext();) |
| { |
| XMPNode schema = (XMPNode) it.next(); |
| if (removeSchemaChildren(schema, doAllProperties)) |
| { |
| it.remove(); |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * @see XMPUtils#appendProperties(XMPMeta, XMPMeta, boolean, boolean) |
| * @param source The source XMP object. |
| * @param destination The destination XMP object. |
| * @param doAllProperties Do internal properties in addition to external properties. |
| * @param replaceOldValues Replace the values of existing properties. |
| * @param deleteEmptyValues Delete destination values if source property is empty. |
| * @throws XMPException Forwards the Exceptions from the metadata processing |
| */ |
| public static void appendProperties(XMPMeta source, XMPMeta destination, |
| boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues) |
| throws XMPException |
| { |
| ParameterAsserts.assertImplementation(source); |
| ParameterAsserts.assertImplementation(destination); |
| |
| XMPMetaImpl src = (XMPMetaImpl) source; |
| XMPMetaImpl dest = (XMPMetaImpl) destination; |
| |
| for (Iterator it = src.getRoot().iterateChildren(); it.hasNext();) |
| { |
| XMPNode sourceSchema = (XMPNode) it.next(); |
| |
| // Make sure we have a destination schema node |
| XMPNode destSchema = XMPNodeUtils.findSchemaNode(dest.getRoot(), |
| sourceSchema.getName(), false); |
| boolean createdSchema = false; |
| if (destSchema == null) |
| { |
| destSchema = new XMPNode(sourceSchema.getName(), sourceSchema.getValue(), |
| new PropertyOptions().setSchemaNode(true)); |
| dest.getRoot().addChild(destSchema); |
| createdSchema = true; |
| } |
| |
| // Process the source schema's children. |
| for (Iterator ic = sourceSchema.iterateChildren(); ic.hasNext();) |
| { |
| XMPNode sourceProp = (XMPNode) ic.next(); |
| if (doAllProperties |
| || !Utils.isInternalProperty(sourceSchema.getName(), sourceProp.getName())) |
| { |
| appendSubtree( |
| dest, sourceProp, destSchema, replaceOldValues, deleteEmptyValues); |
| } |
| } |
| |
| if (!destSchema.hasChildren() && (createdSchema || deleteEmptyValues)) |
| { |
| // Don't create an empty schema / remove empty schema. |
| dest.getRoot().removeChild(destSchema); |
| } |
| } |
| } |
| |
| |
| /** |
| * Remove all schema children according to the flag |
| * <code>doAllProperties</code>. Empty schemas are automatically remove |
| * by <code>XMPNode</code> |
| * |
| * @param schemaNode |
| * a schema node |
| * @param doAllProperties |
| * flag if all properties or only externals shall be removed. |
| * @return Returns true if the schema is empty after the operation. |
| */ |
| private static boolean removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties) |
| { |
| for (Iterator it = schemaNode.iterateChildren(); it.hasNext();) |
| { |
| XMPNode currProp = (XMPNode) it.next(); |
| if (doAllProperties |
| || !Utils.isInternalProperty(schemaNode.getName(), currProp.getName())) |
| { |
| it.remove(); |
| } |
| } |
| |
| return !schemaNode.hasChildren(); |
| } |
| |
| |
| /** |
| * @see XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) |
| * @param destXMP The destination XMP object. |
| * @param sourceNode the source node |
| * @param destParent the parent of the destination node |
| * @param replaceOldValues Replace the values of existing properties. |
| * @param deleteEmptyValues flag if properties with empty values should be deleted |
| * in the destination object. |
| * @throws XMPException |
| */ |
| private static void appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent, |
| boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException |
| { |
| XMPNode destNode = XMPNodeUtils.findChildNode(destParent, sourceNode.getName(), false); |
| |
| boolean valueIsEmpty = false; |
| if (deleteEmptyValues) |
| { |
| valueIsEmpty = sourceNode.getOptions().isSimple() ? |
| sourceNode.getValue() == null || sourceNode.getValue().length() == 0 : |
| !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. |
| destXMP.setNode(destNode, sourceNode.getValue(), sourceNode.getOptions(), 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. |
| |
| PropertyOptions sourceForm = sourceNode.getOptions(); |
| PropertyOptions destForm = destNode.getOptions(); |
| if (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 (Iterator it = sourceNode.iterateChildren(); it.hasNext();) |
| { |
| XMPNode 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 (Iterator it = sourceNode.iterateChildren(); it.hasNext();) |
| { |
| XMPNode sourceItem = (XMPNode) it.next(); |
| if (!sourceItem.hasQualifier() |
| || !XMPConst.XML_LANG.equals(sourceItem.getQualifier(1).getName())) |
| { |
| continue; |
| } |
| |
| int destIndex = XMPNodeUtils.lookupLanguageItem(destNode, |
| sourceItem.getQualifier(1).getValue()); |
| if (deleteEmptyValues && |
| (sourceItem.getValue() == null || |
| sourceItem.getValue().length() == 0)) |
| { |
| if (destIndex != -1) |
| { |
| destNode.removeChild(destIndex); |
| if (!destNode.hasChildren()) |
| { |
| destParent.removeChild(destNode); |
| } |
| } |
| } |
| else if (destIndex == -1) |
| { |
| // Not replacing, keep the existing item. |
| if (!XMPConst.X_DEFAULT.equals(sourceItem.getQualifier(1).getValue()) |
| || !destNode.hasChildren()) |
| { |
| sourceItem.cloneSubtree(destNode); |
| } |
| else |
| { |
| XMPNode destItem = new XMPNode( |
| sourceItem.getName(), |
| sourceItem.getValue(), |
| sourceItem.getOptions()); |
| 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 (Iterator is = sourceNode.iterateChildren(); is.hasNext();) |
| { |
| XMPNode sourceItem = (XMPNode) is.next(); |
| |
| boolean match = false; |
| for (Iterator id = destNode.iterateChildren(); id.hasNext();) |
| { |
| XMPNode destItem = (XMPNode) id.next(); |
| if (itemValuesMatch(sourceItem, destItem)) |
| { |
| match = true; |
| } |
| } |
| if (!match) |
| { |
| destNode = (XMPNode) sourceItem.clone(); |
| destParent.addChild(destNode); |
| } |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * Compares two nodes including its children and qualifier. |
| * @param leftNode an <code>XMPNode</code> |
| * @param rightNode an <code>XMPNode</code> |
| * @return Returns true if the nodes are equal, false otherwise. |
| * @throws XMPException Forwards exceptions to the calling method. |
| */ |
| private static boolean itemValuesMatch(XMPNode leftNode, XMPNode rightNode) throws XMPException |
| { |
| PropertyOptions leftForm = leftNode.getOptions(); |
| PropertyOptions rightForm = rightNode.getOptions(); |
| |
| if (leftForm.equals(rightForm)) |
| { |
| return false; |
| } |
| |
| if (leftForm.getOptions() == 0) |
| { |
| // Simple nodes, check the values and xml:lang qualifiers. |
| if (!leftNode.getValue().equals(rightNode.getValue())) |
| { |
| return false; |
| } |
| if (leftNode.getOptions().getHasLanguage() != rightNode.getOptions().getHasLanguage()) |
| { |
| return false; |
| } |
| if (leftNode.getOptions().getHasLanguage() |
| && !leftNode.getQualifier(1).getValue().equals( |
| rightNode.getQualifier(1).getValue())) |
| { |
| return false; |
| } |
| } |
| else if (leftForm.isStruct()) |
| { |
| // Struct nodes, see if all fields match, ignoring order. |
| |
| if (leftNode.getChildrenLength() != rightNode.getChildrenLength()) |
| { |
| return false; |
| } |
| |
| for (Iterator it = leftNode.iterateChildren(); it.hasNext();) |
| { |
| XMPNode leftField = (XMPNode) it.next(); |
| XMPNode rightField = XMPNodeUtils.findChildNode(rightNode, leftField.getName(), |
| 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. |
| |
| assert leftForm.isArray(); |
| |
| for (Iterator il = leftNode.iterateChildren(); il.hasNext();) |
| { |
| XMPNode leftItem = (XMPNode) il.next(); |
| |
| boolean match = false; |
| for (Iterator ir = rightNode.iterateChildren(); ir.hasNext();) |
| { |
| XMPNode rightItem = (XMPNode) ir.next(); |
| if (itemValuesMatch(leftItem, rightItem)) |
| { |
| match = true; |
| break; |
| } |
| } |
| if (!match) |
| { |
| return false; |
| } |
| } |
| } |
| return true; // All of the checks passed. |
| } |
| |
| |
| /** |
| * Make sure the separator is OK. It must be one semicolon surrounded by |
| * zero or more spaces. Any of the recognized semicolons or spaces are |
| * allowed. |
| * |
| * @param separator |
| * @throws XMPException |
| */ |
| private static void checkSeparator(String separator) throws XMPException |
| { |
| boolean haveSemicolon = false; |
| for (int i = 0; i < separator.length(); i++) |
| { |
| int charKind = classifyCharacter(separator.charAt(i)); |
| if (charKind == UCK_SEMICOLON) |
| { |
| if (haveSemicolon) |
| { |
| throw new XMPException("Separator can have only one semicolon", |
| XMPError.BADPARAM); |
| } |
| haveSemicolon = true; |
| } |
| else if (charKind != UCK_SPACE) |
| { |
| throw new XMPException("Separator can have only spaces and one semicolon", |
| XMPError.BADPARAM); |
| } |
| } |
| if (!haveSemicolon) |
| { |
| throw new XMPException("Separator must have one semicolon", XMPError.BADPARAM); |
| } |
| } |
| |
| |
| /** |
| * Make sure the open and close quotes are a legitimate pair and return the |
| * correct closing quote or an exception. |
| * |
| * @param quotes |
| * opened and closing quote in a string |
| * @param openQuote |
| * the open quote |
| * @return Returns a corresponding closing quote. |
| * @throws XMPException |
| */ |
| private static char checkQuotes(String quotes, char openQuote) throws XMPException |
| { |
| char closeQuote; |
| |
| int charKind = classifyCharacter(openQuote); |
| if (charKind != UCK_QUOTE) |
| { |
| throw new XMPException("Invalid quoting character", XMPError.BADPARAM); |
| } |
| |
| if (quotes.length() == 1) |
| { |
| closeQuote = openQuote; |
| } |
| else |
| { |
| closeQuote = quotes.charAt(1); |
| charKind = classifyCharacter(closeQuote); |
| if (charKind != UCK_QUOTE) |
| { |
| throw new XMPException("Invalid quoting character", XMPError.BADPARAM); |
| } |
| } |
| |
| if (closeQuote != getClosingQuote(openQuote)) |
| { |
| throw new XMPException("Mismatched quote pair", XMPError.BADPARAM); |
| } |
| return closeQuote; |
| } |
| |
| |
| /** |
| * Classifies the character into normal chars, spaces, semicola, quotes, |
| * control chars. |
| * |
| * @param ch |
| * a char |
| * @return Return the character kind. |
| */ |
| private static int classifyCharacter(char ch) |
| { |
| if (SPACES.indexOf(ch) >= 0 || (0x2000 <= ch && ch <= 0x200B)) |
| { |
| return UCK_SPACE; |
| } |
| else if (COMMAS.indexOf(ch) >= 0) |
| { |
| return UCK_COMMA; |
| } |
| else if (SEMICOLA.indexOf(ch) >= 0) |
| { |
| return UCK_SEMICOLON; |
| } |
| else if (QUOTES.indexOf(ch) >= 0 || (0x3008 <= ch && ch <= 0x300F) |
| || (0x2018 <= ch && ch <= 0x201F)) |
| { |
| return UCK_QUOTE; |
| } |
| else if (ch < 0x0020 || CONTROLS.indexOf(ch) >= 0) |
| { |
| return UCK_CONTROL; |
| } |
| else |
| { |
| // Assume typical case. |
| return UCK_NORMAL; |
| } |
| } |
| |
| |
| /** |
| * @param openQuote |
| * the open quote char |
| * @return Returns the matching closing quote for an open quote. |
| */ |
| private static char getClosingQuote(char openQuote) |
| { |
| switch (openQuote) |
| { |
| case 0x0022: |
| return 0x0022; // ! U+0022 is both opening and closing. |
| case 0x005B: |
| return 0x005D; |
| case 0x00AB: |
| return 0x00BB; // ! U+00AB and U+00BB are reversible. |
| case 0x00BB: |
| return 0x00AB; |
| case 0x2015: |
| return 0x2015; // ! U+2015 is both opening and closing. |
| case 0x2018: |
| return 0x2019; |
| case 0x201A: |
| return 0x201B; |
| case 0x201C: |
| return 0x201D; |
| case 0x201E: |
| return 0x201F; |
| case 0x2039: |
| return 0x203A; // ! U+2039 and U+203A are reversible. |
| case 0x203A: |
| return 0x2039; |
| case 0x3008: |
| return 0x3009; |
| case 0x300A: |
| return 0x300B; |
| case 0x300C: |
| return 0x300D; |
| case 0x300E: |
| return 0x300F; |
| case 0x301D: |
| return 0x301F; // ! U+301E also closes U+301D. |
| default: |
| return 0; |
| } |
| } |
| |
| |
| /** |
| * Add quotes to the item. |
| * |
| * @param item |
| * the array item |
| * @param openQuote |
| * the open quote character |
| * @param closeQuote |
| * the closing quote character |
| * @param allowCommas |
| * flag if commas are allowed |
| * @return Returns the value in quotes. |
| */ |
| private static String applyQuotes(String item, char openQuote, char closeQuote, |
| boolean allowCommas) |
| { |
| if (item == null) |
| { |
| item = ""; |
| } |
| |
| boolean prevSpace = false; |
| int charOffset; |
| int charKind; |
| |
| // See if there are any separators in the value. Stop at the first |
| // occurrance. This is a bit |
| // tricky in order to make typical typing work conveniently. The purpose |
| // of applying quotes |
| // is to preserve the values when splitting them back apart. That is |
| // CatenateContainerItems |
| // and SeparateContainerItems must round trip properly. For the most |
| // part we only look for |
| // separators here. Internal quotes, as in -- Irving "Bud" Jones -- |
| // won't cause problems in |
| // the separation. An initial quote will though, it will make the value |
| // look quoted. |
| |
| int i; |
| for (i = 0; i < item.length(); i++) |
| { |
| char ch = item.charAt(i); |
| charKind = classifyCharacter(ch); |
| if (i == 0 && charKind == UCK_QUOTE) |
| { |
| break; |
| } |
| |
| if (charKind == UCK_SPACE) |
| { |
| // Multiple spaces are a separator. |
| if (prevSpace) |
| { |
| break; |
| } |
| prevSpace = true; |
| } |
| else |
| { |
| prevSpace = false; |
| if ((charKind == UCK_SEMICOLON || charKind == UCK_CONTROL) |
| || (charKind == UCK_COMMA && !allowCommas)) |
| { |
| break; |
| } |
| } |
| } |
| |
| |
| if (i < item.length()) |
| { |
| // Create a quoted copy, doubling any internal quotes that match the |
| // outer ones. Internal quotes did not stop the "needs quoting" |
| // search, but they do need |
| // doubling. So we have to rescan the front of the string for |
| // quotes. Handle the special |
| // case of U+301D being closed by either U+301E or U+301F. |
| |
| StringBuffer newItem = new StringBuffer(item.length() + 2); |
| int splitPoint; |
| for (splitPoint = 0; splitPoint <= i; splitPoint++) |
| { |
| if (classifyCharacter(item.charAt(i)) == UCK_QUOTE) |
| { |
| break; |
| } |
| } |
| |
| // Copy the leading "normal" portion. |
| newItem.append(openQuote).append(item.substring(0, splitPoint)); |
| |
| for (charOffset = splitPoint; charOffset < item.length(); charOffset++) |
| { |
| newItem.append(item.charAt(charOffset)); |
| if (classifyCharacter(item.charAt(charOffset)) == UCK_QUOTE |
| && isSurroundingQuote(item.charAt(charOffset), openQuote, closeQuote)) |
| { |
| newItem.append(item.charAt(charOffset)); |
| } |
| } |
| |
| newItem.append(closeQuote); |
| |
| item = newItem.toString(); |
| } |
| |
| return item; |
| } |
| |
| |
| /** |
| * @param ch a character |
| * @param openQuote the opening quote char |
| * @param closeQuote the closing quote char |
| * @return Return it the character is a surrounding quote. |
| */ |
| private static boolean isSurroundingQuote(char ch, char openQuote, char closeQuote) |
| { |
| return ch == openQuote || isClosingingQuote(ch, openQuote, closeQuote); |
| } |
| |
| |
| /** |
| * @param ch a character |
| * @param openQuote the opening quote char |
| * @param closeQuote the closing quote char |
| * @return Returns true if the character is a closing quote. |
| */ |
| private static boolean isClosingingQuote(char ch, char openQuote, char closeQuote) |
| { |
| return ch == closeQuote || (openQuote == 0x301D && ch == 0x301E || ch == 0x301F); |
| } |
| |
| |
| |
| /** |
| * U+0022 ASCII space<br> |
| * U+3000, ideographic space<br> |
| * U+303F, ideographic half fill space<br> |
| * U+2000..U+200B, en quad through zero width space |
| */ |
| private static final String SPACES = "\u0020\u3000\u303F"; |
| /** |
| * U+002C, ASCII comma<br> |
| * U+FF0C, full width comma<br> |
| * U+FF64, half width ideographic comma<br> |
| * U+FE50, small comma<br> |
| * U+FE51, small ideographic comma<br> |
| * U+3001, ideographic comma<br> |
| * U+060C, Arabic comma<br> |
| * U+055D, Armenian comma |
| */ |
| private static final String COMMAS = "\u002C\uFF0C\uFF64\uFE50\uFE51\u3001\u060C\u055D"; |
| /** |
| * U+003B, ASCII semicolon<br> |
| * U+FF1B, full width semicolon<br> |
| * U+FE54, small semicolon<br> |
| * U+061B, Arabic semicolon<br> |
| * U+037E, Greek "semicolon" (really a question mark) |
| */ |
| private static final String SEMICOLA = "\u003B\uFF1B\uFE54\u061B\u037E"; |
| /** |
| * U+0022 ASCII quote<br> |
| * ASCII '[' (0x5B) and ']' (0x5D) are used as quotes in Chinese and |
| * Korean.<br> |
| * U+00AB and U+00BB, guillemet quotes<br> |
| * U+3008..U+300F, various quotes.<br> |
| * U+301D..U+301F, double prime quotes.<br> |
| * U+2015, dash quote.<br> |
| * U+2018..U+201F, various quotes.<br> |
| * U+2039 and U+203A, guillemet quotes. |
| */ |
| private static final String QUOTES = |
| "\"\u005B\u005D\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A"; |
| /** |
| * U+0000..U+001F ASCII controls<br> |
| * U+2028, line separator.<br> |
| * U+2029, paragraph separator. |
| */ |
| private static final String CONTROLS = "\u2028\u2029"; |
| } |