All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.internal.xmp.impl.XMPUtilsImpl Maven / Gradle / Ivy

Go to download

The XMP Library for Java is based on the C++ XMPCore library and the API is similar.

There is a newer version: 6.1.11
Show newest version
// =================================================================================================
// 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.internal.xmp.impl;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import com.adobe.internal.xmp.XMPConst;
import com.adobe.internal.xmp.XMPError;
import com.adobe.internal.xmp.XMPException;
import com.adobe.internal.xmp.XMPMeta;
import com.adobe.internal.xmp.XMPMetaFactory;
import com.adobe.internal.xmp.XMPSchemaRegistry;
import com.adobe.internal.xmp.XMPUtils;
import com.adobe.internal.xmp.impl.xpath.XMPPath;
import com.adobe.internal.xmp.impl.xpath.XMPPathParser;
import com.adobe.internal.xmp.options.PropertyOptions;
import com.adobe.internal.xmp.options.SerializeOptions;
import com.adobe.internal.xmp.options.TemplateOptions;
import com.adobe.internal.xmp.properties.XMPAliasInfo;



/**
 * @author Stefan Makswit
 * @version $Revision$
 * @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);
		
		int arrayElementLimit = Integer.MAX_VALUE;
		if(arrayNode != null)
		{
			if(arrayOptions != null) 
			{
				arrayElementLimit = arrayOptions.getArrayElementsLimit();
				if(arrayElementLimit == -1 )
				{
					arrayElementLimit = Integer.MAX_VALUE;
				}
			}
		}
		// Extract the item values one at a time, until the whole input string is done.
		StringBuilder itemValue = new StringBuilder("");
		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.
			
			if( arrayNode.getChildrenLength() >= arrayElementLimit)
			{
				break;
			}
			
			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 = new StringBuilder(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 = new StringBuilder("");

				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.append(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.append(ch);
							// Loop will add in charSize.
							itemEnd++;
						}
						else if (!isClosingingQuote(ch, openQuote, closeQuote))
						{
							// This is an undoubled, non-closing quote, copy it.
							itemValue.append(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.toString().equals(arrayNode.getChild(oldChild).getValue()))
				{
					foundIndex = oldChild;
					break;
				}
			}

			XMPNode newItem = null;
			if (foundIndex < 0)
			{
				newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue.toString(), null);
				arrayNode.addChild(newItem);
			}
// <#AdobePrivate>
//			else
//			{
//				newItem = arrayNode.getChild(foundIndex);
//				// Don't match again, let duplicates be seen.
//				arrayNode.getChild(foundIndex).setValue(null);
//			}
//			 
		}
	}


	/**
	 * Utility to find or create the array used by separateArrayItems().
	 * @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, false ,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
	 * doAllProperties. Empty schemas are automatically remove
	 * by XMPNode
	 *
	 * @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 mergeCompound, 
			boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException
	{
		XMPNode destNode = XMPNodeUtils.findChildNode(destParent, sourceNode.getName(), false);

		boolean valueIsEmpty = false;
		
		valueIsEmpty = sourceNode.getOptions().isSimple() ?
				sourceNode.getValue() == null  ||  sourceNode.getValue().length() == 0 :
				!sourceNode.hasChildren();
		
		
		if(valueIsEmpty){
			if ( deleteEmptyValues && (destNode != null) ) {
				destParent.removeChild(destNode);
			}
			return; // ! Done, empty values are either ignored or cause deletions.
		}
		
		if (destNode == null)
		{
			// The one easy case, the destination does not exist.
			XMPNode tempNode = (XMPNode) sourceNode.clone(true);
			if(tempNode != null)
				destParent.addChild(tempNode);
			return;
		}
		
		PropertyOptions sourceForm = sourceNode.getOptions();
		
		boolean replaceThis = replaceOldValues;	// ! Don't modify replaceOld, it gets passed to inner calls.
		if ( mergeCompound && (! sourceForm.isSimple()) ) {
			replaceThis = false;
		}
		
		if (replaceThis)
		{
			// The destination exists and should be replaced.
		//	destXMP.setNode(destNode, sourceNode.getValue(), sourceNode.getOptions(), true);
			destParent.removeChild(destNode);
			//destNode = (XMPNode) sourceNode.clone();
			XMPNode tempNode = (XMPNode) sourceNode.clone(true);
			if(tempNode != null)
				destParent.addChild(tempNode);
			return;
		}
		
		// The destination exists and is not totally replaced. Structs and
		// arrays are merged.

		
		PropertyOptions destForm = destNode.getOptions();
		if (sourceForm.getOptions() != destForm.getOptions() || sourceForm.isSimple())
		{
			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, mergeCompound,
					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(sourceItem.getValue() == null || sourceItem.getValue().length() == 0){
					if ( deleteEmptyValues && (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()) {
							XMPNode tempNode = (XMPNode) sourceItem.clone(true);
							if (tempNode != null)
								destNode.addChild(tempNode);
							// sourceItem.cloneSubtree(destNode);
						} else {
							XMPNode destItem = new XMPNode(sourceItem.getName(), sourceItem.getValue(),
									sourceItem.getOptions());
							sourceItem.cloneSubtree(destItem, true);
							destNode.addChild(1, destItem);
						}
					}
					else{
						if(replaceOldValues)
							destNode.getChild(destIndex).setValue(sourceItem.getValue());
					}
				}
			}
		}
		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;
						break;
						}
					}
					if (!match)
					{
						XMPNode tempNode = (XMPNode) sourceItem.clone(true);
						if(tempNode != null)
							destNode.addChild(tempNode);
					}
				}
			}
		}
	


	/**
	 * Compares two nodes including its children and qualifier.
	 * @param leftNode an XMPNode
	 * @param rightNode an XMPNode
	 * @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.isSimple())
		{
			// 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.
	}

	public static void duplicateSubtree(XMPMeta source, XMPMeta dest, String sourceNS,
		String sourceRoot, String destNS, String destRoot, PropertyOptions options) throws XMPException{
		
		boolean fullSourceTree = false;
		boolean fullDestTree   = false;
		XMPPath sourcePath, destPath; 
		XMPNode sourceNode = null;
		XMPNode destNode = null;
		
		ParameterAsserts.assertSchemaNS(sourceNS);
		ParameterAsserts.assertSchemaNS(sourceRoot);
		ParameterAsserts.assertNotNull(dest);
		ParameterAsserts.assertNotNull(destNS);
		ParameterAsserts.assertNotNull(destRoot);
		
		if(destNS.length() == 0){
			destNS = sourceNS;
		}
		
		if(destRoot.length() == 0){
			destRoot = sourceRoot;
		}
		
		if(sourceNS.equals("*")){
			fullSourceTree = true;
		}
		
		if(destNS.equals("*")){
			fullDestTree = true;
		}
		
		if ( (source == dest) && (fullSourceTree | fullDestTree) ) {
			throw new XMPException("Can't duplicate tree onto itself", XMPError.BADPARAM);
		}
		if ( fullSourceTree & fullDestTree ) {
			throw new XMPException( "Use Clone for full tree to full tree", XMPError.BADPARAM);
		}
		if(fullSourceTree){
			destPath = XMPPathParser.expandXPath(destNS, destRoot);
			XMPMetaImpl destImpl = (XMPMetaImpl)dest;
			destNode = XMPNodeUtils.findNode(destImpl.getRoot(), destPath, false, null);
			if(destNode == null || !(destNode.getOptions().isStruct()))
				throw new XMPException("Destination must be an existing struct", XMPError.BADXPATH);
			
			if(destNode.hasChildren()){
				if((options != null) && ((options.getOptions() & PropertyOptions.DELETE_EXISTING) != 0)){
					destNode.removeChildren();
				}
				else{
					throw new XMPException("Destination must be an existing struct", XMPError.BADXPATH);
				}
			}
			
			XMPMetaImpl sourceImpl = (XMPMetaImpl)source;
			for ( int schemaNum = 1, schemaLim = sourceImpl.getRoot().getChildrenLength(); schemaNum <= schemaLim; ++schemaNum ) {

				 XMPNode  currSchema = sourceImpl.getRoot().getChild(schemaNum);

				for ( int propNum = 1, propLim = currSchema.getChildrenLength(); propNum <= propLim; ++propNum ) {
					sourceNode = currSchema.getChild(propNum);
					destNode.addChild((XMPNode)sourceNode.clone(false));
					
					/*XMP_Node * copyNode = new XMP_Node ( destNode, sourceNode->name, sourceNode->value, sourceNode->options );
					destNode->children.push_back ( copyNode );
					CloneOffspring ( sourceNode, copyNode );*/  //implemented above
				}

			}
			
		}
		else if(fullDestTree){
			
			// The source node must be an existing struct, copy all of the fields to the dest top level.

			XMPMetaImpl srcImpl = (XMPMetaImpl)source;
			XMPMetaImpl dstImpl = (XMPMetaImpl)dest;
			sourcePath = XMPPathParser.expandXPath ( sourceNS, sourceRoot );
			sourceNode = XMPNodeUtils.findNode ( srcImpl.getRoot() , sourcePath , false, null);

			if ( (sourceNode == null) || !( sourceNode.getOptions().isStruct()) ) {
				throw new XMPException("Source must be an existing struct", XMPError.BADXPATH);
			}
			
			destNode = dstImpl.getRoot();
			
			if (  destNode.hasChildren() ) {
				if((options != null) && ((options.getOptions() & PropertyOptions.DELETE_EXISTING) != 0)) {
					destNode.removeChildren();
				} else {
					throw new XMPException("Source must be an existing struct", XMPError.BADXPATH);
				}
			}
			
			String nsPrefix;
			
			for ( int fieldNum = 1, fieldLim = sourceNode.getChildrenLength(); fieldNum <= fieldLim; ++fieldNum ) {

				XMPNode currField = sourceNode.getChild(fieldNum);

				int colonPos = currField.getName().indexOf(':');
				if (  colonPos == -1 ) continue;
				nsPrefix = new String ( currField.getName().substring(0, colonPos+1));
				
				XMPSchemaRegistry nsRegister = XMPMetaFactory.getSchemaRegistry();
				String nsURI = nsRegister.getNamespaceURI(nsPrefix);
				
				if(nsURI == null){
					throw new XMPException("Source field namespace is not global", XMPError.BADSCHEMA);
				}
				
				XMPNode destSchema = XMPNodeUtils.findSchemaNode ( dstImpl.getRoot(), nsURI, true );
				if ( destSchema == null){
					throw new XMPException("Failed to find destination schema", XMPError.BADSCHEMA);
				}
				
				destSchema.addChild((XMPNode)currField.clone(false));
			}
		}
		else{
			sourcePath = XMPPathParser.expandXPath(sourceNS, sourceRoot);
			destPath = XMPPathParser.expandXPath(destNS, destRoot);
			XMPMetaImpl sourceImpl = (XMPMetaImpl)source;
			XMPMetaImpl destImpl = (XMPMetaImpl)dest;
			sourceNode = XMPNodeUtils.findNode(sourceImpl.getRoot(), sourcePath, false, null);
			if(sourceNode == null)
				throw new XMPException("Can't find source subtree", XMPError.BADXPATH);
			destNode = XMPNodeUtils.findNode(destImpl.getRoot(), destPath, false, null);
			if(destNode != null)
				throw new XMPException("Destination subtree must not exist", XMPError.BADXPATH);
			destNode = XMPNodeUtils.findNode(destImpl.getRoot(), destPath, true, null);
			if(destNode == null)
				throw new XMPException("Can't create destination root node", XMPError.BADXPATH);
			if ( source == dest ) {
				for ( XMPNode testNode = destNode; testNode != null; testNode = testNode.getParent() ) {
					if ( testNode == sourceNode ) {
						// *** delete the just-created dest root node
						throw new XMPException("Destination subtree is within the source subtree", XMPError.BADXPATH);
					}
				}
			}
			destNode.setValue(sourceNode.getValue());
			destNode.setOptions(sourceNode.getOptions());
			sourceNode.cloneSubtree(destNode, false);
		}
		
	}
	
	
	
	
	
	
	/**
	 * 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.
//		Not interpreted as brackets anymore
//		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
* U+3000, ideographic space
* U+303F, ideographic half fill space
* U+2000..U+200B, en quad through zero width space */ private static final String SPACES = "\u0020\u3000\u303F"; /** * U+002C, ASCII comma
* U+FF0C, full width comma
* U+FF64, half width ideographic comma
* U+FE50, small comma
* U+FE51, small ideographic comma
* U+3001, ideographic comma
* U+060C, Arabic comma
* U+055D, Armenian comma */ private static final String COMMAS = "\u002C\uFF0C\uFF64\uFE50\uFE51\u3001\u060C\u055D"; /** * U+003B, ASCII semicolon
* U+FF1B, full width semicolon
* U+FE54, small semicolon
* U+061B, Arabic semicolon
* U+037E, Greek "semicolon" (really a question mark) */ private static final String SEMICOLA = "\u003B\uFF1B\uFE54\u061B\u037E"; /** * U+0022 ASCII quote
* The square brackets are not interpreted as quotes anymore (bug #2674672) * (ASCII '[' (0x5B) and ']' (0x5D) are used as quotes in Chinese and * Korean.)
* U+00AB and U+00BB, guillemet quotes
* U+3008..U+300F, various quotes.
* U+301D..U+301F, double prime quotes.
* U+2015, dash quote.
* U+2018..U+201F, various quotes.
* U+2039 and U+203A, guillemet quotes. */ private static final String QUOTES = "\"\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A"; // "\"\u005B\u005D\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A"; /** * U+0000..U+001F ASCII controls
* U+2028, line separator.
* U+2029, paragraph separator. */ private static final String CONTROLS = "\u2028\u2029"; /** * Moves the specified Property from one Meta to another. * * @param stdXMP * Meta Object from where the property needs to move * @param extXMP * Meta Object to where the property needs to move * @param schemaURI * Schema of the specified property * @param propName * Name of the property * * @return true in case of success otherwise false. * * @throws XMPException in case of failure */ static boolean moveOneProperty(XMPMetaImpl stdXMP, XMPMetaImpl extXMP, String schemaURI, String propName) throws XMPException { XMPNode propNode = null; XMPNode stdSchema = XMPNodeUtils.findSchemaNode(stdXMP.getRoot(), schemaURI, false); if (stdSchema != null) { propNode = XMPNodeUtils.findChildNode(stdSchema, propName, false); } if (propNode == null) return false; XMPNode extSchema = XMPNodeUtils.findSchemaNode(extXMP.getRoot(), schemaURI, true); propNode.setParent(extSchema); extSchema.setImplicit(false); extSchema.addChild(propNode); stdSchema.removeChild(propNode); if (stdSchema.hasChildren() == false) { XMPNode xmpTree = stdSchema.getParent(); xmpTree.removeChild(stdSchema); } return true; } /** * estimates the size of an xmp node * * @param xmpNode * XMP Node Object * * @return the estimated size of the node * */ static int estimateSizeForJPEG(XMPNode xmpNode) { int estSize = 0; int nameSize = xmpNode.getName().length(); boolean includeName = (!xmpNode.getOptions().isArray()); if (xmpNode.getOptions().isSimple()) { if (includeName) estSize += (nameSize + 3); // Assume attribute form. estSize += xmpNode.getValue().length(); } else if (xmpNode.getOptions().isArray()) { // The form of the value portion is: // ...... if (includeName) estSize += (2 * nameSize + 5); int arraySize = xmpNode.getChildrenLength(); estSize += 9 + 10; // The rdf:Xyz tags. estSize += arraySize * (8 + 9); // The rdf:li tags. for (int i = 1; i <= arraySize; ++i) { estSize += estimateSizeForJPEG(xmpNode.getChild(i)); } } else { // The form is: ...fields... if (includeName) estSize += (2 * nameSize + 5); estSize += 25; // The rdf:parseType="Resource" attribute. int fieldCount = xmpNode.getChildrenLength(); for (int i = 1; i <= fieldCount; ++i) { estSize += estimateSizeForJPEG(xmpNode.getChild(i)); } } return estSize; } /** * Utility function for placing objects in a Map. It behaves like a multi map. * * @param multiMap * A Map object which takes Integer as a key and list of list of String as value * @param key * A Key for the map * @param stringPair * A value for the map * */ private static void putObjectsInMultiMap(Map>> multiMap, Integer key, List stringPair) { if (multiMap == null) return; List> tempList = multiMap.get(key); if (tempList == null) { tempList = new ArrayList>(); multiMap.put(key, tempList); } tempList.add(stringPair); } /** * Utility function for retrieving biggest entry in the multimap * * @see estimateSizeForJPEG for size calculation * * @param multiMap * A Map object which takes Integer as a key and list of list of String as value * * @return the list with the maximum size. * */ private static List getBiggestEntryInMultiMap(Map>> multiMap) { if (multiMap == null || multiMap.isEmpty()) return null; List> myList = multiMap.get(((TreeMap) multiMap).lastKey()); List myList1 = myList.get(0); myList.remove(0); if (myList.isEmpty()) { multiMap.remove(((TreeMap) multiMap).lastKey()); } return myList1; } /** * Utility function for creating esimated size map for different properties of XMP Packet. * * @see packageForJPEG * * @param stdXMP * Meta Object whose property sizes needs to calculate. * * @param propSizes * A treeMap Object which takes Integer as a key and list of list of String as values * */ static void createEstimatedSizeMap(XMPMetaImpl stdXMP, Map>> propSizes) { for (int s = stdXMP.getRoot().getChildrenLength(); s > 0; --s) { XMPNode stdSchema = stdXMP.getRoot().getChild(s); for (int p = stdSchema.getChildrenLength(); p > 0; --p) { XMPNode stdProp = stdSchema.getChild(p); if ((stdSchema.getName().equals(XMPConst.NS_XMP_NOTE)) && (stdProp.getName().equals("xmpNote:HasExtendedXMP"))) continue; // ! Don't move xmpNote:HasExtendedXMP. int propSize = estimateSizeForJPEG(stdProp); List namePair = new ArrayList(); namePair.add(stdSchema.getName()); namePair.add(stdProp.getName()); putObjectsInMultiMap(propSizes, propSize, namePair); } } } /** * Utility function for moving the largest property from One XMP Packet to another. * * @see moveOneProperty * @see packageForJPEG * * @param stdXMP * Meta Object from where property moves. * * @param extXMP * Meta Object to where property moves. * * @param propSizes * A treeMap Object which holds the estimated sizes of the property of stdXMP as a key and * their string representation as map values. * */ static int moveLargestProperty(XMPMetaImpl stdXMP, XMPMetaImpl extXMP, Map>> propSizes) throws XMPException { assert (!propSizes.isEmpty()); @SuppressWarnings("rawtypes") int propSize = (Integer) ((TreeMap) propSizes).lastKey(); List tempList = getBiggestEntryInMultiMap(propSizes); String schemaURI = tempList.get(0); String propName = tempList.get(1); boolean moved = moveOneProperty(stdXMP, extXMP, schemaURI, propName); assert (moved); return propSize; } /** * creates XMP serializations appropriate for a JPEG file. * * The standard XMP in a JPEG file is limited to 64K bytes. This function * serializes the XMP metadata in an XMP object into a string of RDF . If * the data does not fit into the 64K byte limit, it creates a second packet * string with the extended data. * * @param origXMPImpl * The XMP object containing the metadata. * * @param stdStr * A string object in which to return the full standard XMP * packet. * * @param extStr * A string object in which to return the serialized extended * XMP, empty if not needed. * * @param digestStr * A string object in which to return an MD5 digest of the * serialized extended XMP, empty if not needed. * * @throws NoSuchAlgorithmException if fail to find algorithm for MD5 * @throws XMPException in case of internal error occurs. * */ public static void packageForJPEG ( XMPMeta origXMPImpl, StringBuilder stdStr, StringBuilder extStr, StringBuilder digestStr ) throws XMPException, NoSuchAlgorithmException { XMPMetaImpl origXMP = (XMPMetaImpl) origXMPImpl; assert ( (stdStr != null) && (extStr != null) && (digestStr != null) ); // ! Enforced by wrapper. final int kStdXMPLimit = 65000 ; final String kPacketTrailer = ""; final int kTrailerLen = kPacketTrailer.length(); String tempStr = null; XMPMetaImpl stdXMP = new XMPMetaImpl(); XMPMetaImpl extXMP = new XMPMetaImpl(); SerializeOptions keepItSmall = new SerializeOptions(SerializeOptions.USE_COMPACT_FORMAT); keepItSmall.setPadding(1); keepItSmall.setIndent(""); keepItSmall.setBaseIndent(0); keepItSmall.setNewline(" "); // Try to serialize everything. Note that we're making internal calls to SerializeToBuffer, so // we'll be getting back the pointer and length for its internal string. tempStr = XMPMetaFactory.serializeToString(origXMP,keepItSmall); if ( tempStr.length() > kStdXMPLimit ) { // Couldn't fit everything, make a copy of the input XMP and make sure there is no xmp:Thumbnails property. stdXMP.getRoot().setOptions(origXMP.getRoot().getOptions()) ; stdXMP.getRoot().setName(origXMP.getRoot().getName()) ; stdXMP.getRoot().setValue(origXMP.getRoot().getValue()); origXMP.getRoot().cloneSubtree(stdXMP.getRoot(), false); if ( stdXMP.doesPropertyExist ( XMPConst.NS_XMP, "Thumbnails" ) ) { stdXMP.deleteProperty ( XMPConst.NS_XMP, "Thumbnails" ); tempStr = XMPMetaFactory.serializeToString(stdXMP,keepItSmall); } } if ( tempStr.length() > kStdXMPLimit ) { // Still doesn't fit, move all of the Camera Raw namespace. Add a dummy value for xmpNote:HasExtendedXMP. stdXMP.setProperty ( XMPConst.NS_XMP_NOTE, "HasExtendedXMP", "123456789-123456789-123456789-12", new PropertyOptions(PropertyOptions.NO_OPTIONS)); XMPNode crSchema = XMPNodeUtils.findSchemaNode(stdXMP.getRoot(), XMPConst.NS_CAMERARAW, false); if ( crSchema != null ) { crSchema.setParent(extXMP.getRoot()); extXMP.getRoot().addChild(crSchema); stdXMP.getRoot().removeChild(crSchema); tempStr = XMPMetaFactory.serializeToString(stdXMP,keepItSmall); } } if ( tempStr.length() > kStdXMPLimit ) { // Still doesn't fit, move photoshop:History. boolean moved = moveOneProperty ( stdXMP, extXMP, XMPConst.NS_PHOTOSHOP, "photoshop:History" ); if ( moved ) { tempStr = XMPMetaFactory.serializeToString(stdXMP,keepItSmall); } } if ( tempStr.length() > kStdXMPLimit ) { // Still doesn't fit, move top level properties in order of estimated size. This is done by // creating a multi-map that maps the serialized size to the string pair for the schema URI // and top level property name. Since maps are inherently ordered, a reverse iteration of // the map can be done to move the largest things first. We use a double loop to keep going // until the serialization actually fits, in case the estimates are off. Map>> propSizes = new TreeMap>>(); createEstimatedSizeMap ( stdXMP, propSizes ); // Outer loop to make sure enough is actually moved. while ( (tempStr.length() > kStdXMPLimit) && (! propSizes.isEmpty()) ) { // Inner loop, move what seems to be enough according to the estimates. int tempLen = tempStr.length(); while ( (tempLen > kStdXMPLimit) && (! propSizes.isEmpty()) ) { int propSize = moveLargestProperty ( stdXMP, extXMP, propSizes ); assert ( propSize > 0 ); if ( propSize > tempLen ) propSize = tempLen; // ! Don't go negative. tempLen -= propSize; } // Reserialize the remaining standard XMP. tempStr = XMPMetaFactory.serializeToString(stdXMP,keepItSmall); } } if ( tempStr.length() > kStdXMPLimit ) { // Still doesn't fit, throw an exception and let the client decide what to do. // ! This should never happen with the policy of moving any and all top level properties. throw new XMPException( "Can't reduce XMP enough for JPEG file", XMPError.INTERNALFAILURE ); } // Set the static output strings. if ( extXMP.getRoot().getChildrenLength() == 0 ) { // Just have the standard XMP. stdStr.append(tempStr); } else { // Have extended XMP. Serialize it, compute the digest, reset xmpNote:HasExtendedXMP, and // reserialize the standard XMP. tempStr = XMPMetaFactory.serializeToString(extXMP, new SerializeOptions(SerializeOptions.USE_COMPACT_FORMAT | SerializeOptions.OMIT_PACKET_WRAPPER)); extStr.append(tempStr); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(tempStr.getBytes()); byte byteData[] = md.digest(); for (int i = 0; i < byteData.length; i++) { digestStr.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1)); } stdXMP.setProperty ( XMPConst.NS_XMP_NOTE, "HasExtendedXMP", digestStr.toString(), new PropertyOptions(PropertyOptions.NO_OPTIONS)); tempStr = XMPMetaFactory.serializeToString(stdXMP,keepItSmall); stdStr.append(tempStr); } // Adjust the standard XMP padding to be up to 2KB. assert ( (stdStr.length() > kTrailerLen) && (stdStr.length() <= kStdXMPLimit) ); int extraPadding = kStdXMPLimit - stdStr.length(); // ! Do this before erasing the trailer. if ( extraPadding > 2047 ) extraPadding = 2047; stdStr.delete(stdStr.toString().indexOf(kPacketTrailer),stdStr.length()); for(int i = 0; i < extraPadding; ++i) { stdStr.append(' '); } stdStr.append(kPacketTrailer).toString(); } /** * merges standard and extended XMP retrieved from a JPEG file. * * When an extended partition stores properties that do not fit into the * JPEG file limitation of 64K bytes, this function integrates those * properties back into the same XMP object with those from the standard XMP * packet. * * @param fullXMP * An XMP object which the caller has initialized from the * standard XMP packet in a JPEG file. The extended XMP is added * to this object. * * @param extendedXMP * An XMP object which the caller has initialized from the * extended XMP packet in a JPEG file. * * @throws XMPException * in case of internal error occurs. */ public static void mergeFromJPEG ( XMPMeta fullXMP, XMPMeta extendedXMP ) throws XMPException { TemplateOptions flags = new TemplateOptions(TemplateOptions.REPLACE_EXISTING_PROPERTIES |TemplateOptions.INCLUDE_INTERNAL_PROPERTIES); applyTemplate ( (XMPMetaImpl)fullXMP, (XMPMetaImpl)extendedXMP, flags ); fullXMP.deleteProperty ( XMPConst.NS_XMP_NOTE, "HasExtendedXMP" ); } /** * modifies a working XMP object according to a template object. * * The XMP template can be used to add, replace or delete properties from * the working XMP object. The actions that you specify determine how the * template is applied. Each action can be applied individually or combined; * if you do not specify any actions, the properties and values in the * working XMP object do not change. * * @param OrigXMP * The destination XMP object. * * @param tempXMP * The template to apply to the destination XMP object. * * @param actions * Option flags to control the copying. If none are specified, * the properties and values in the working XMP do not change. A * logical OR of these bit-flag constants: *
    *
  • CLEAR_UNNAMED_PROPERTIES Delete anything * that is not in the template. *
  • ADD_NEW_PROPERTIES Add properties; see * detailed description. *
  • REPLACE_EXISTING_PROPERTIES Replace the * values of existing properties. *
  • REPLACE_WITH_DELETE_EMPTY Replace the * values of existing properties and delete properties if the new * value is empty. *
  • INCLUDE_INTERNAL_PROPERTIES Operate on * internal properties as well as external properties. *
* * @throws XMPException * in case of internal error occurs. */ static public void applyTemplate(XMPMeta OrigXMP, XMPMeta tempXMP, TemplateOptions actions) throws XMPException { XMPMetaImpl workingXMP = (XMPMetaImpl) OrigXMP; XMPMetaImpl templateXMP = (XMPMetaImpl) tempXMP; boolean doClear = (actions.getOptions() & TemplateOptions.CLEAR_UNNAMED_PROPERTIES) != 0; boolean doAdd = (actions.getOptions() & TemplateOptions.ADD_NEW_PROPERTIES) != 0; boolean doReplace = (actions.getOptions() & TemplateOptions.REPLACE_EXISTING_PROPERTIES) != 0; boolean deleteEmpty = (actions.getOptions() & TemplateOptions.REPLACE_WITH_DELETE_EMPTY) != 0; doReplace |= deleteEmpty; // Delete-empty implies Replace. deleteEmpty &= (!doClear); // Clear implies not delete-empty, but keep // the implicit Replace. boolean doAll = (actions.getOptions() & TemplateOptions.INCLUDE_INTERNAL_PROPERTIES) != 0; // ! In several places we do loops backwards so that deletions do not // perturb the remaining indices. // ! These loops use ordinals (size .. 1), we must use a zero based // index inside the loop. if (doClear) { // Visit the top level working properties, delete if not in the // template. for (int schemaOrdinal = workingXMP.getRoot().getChildrenLength(); schemaOrdinal > 0; --schemaOrdinal) { XMPNode workingSchema = workingXMP.getRoot().getChild(schemaOrdinal); XMPNode templateSchema = XMPNodeUtils.findSchemaNode(templateXMP.getRoot(), workingSchema.getName(), false); if (templateSchema == null) { // The schema is not in the template, delete all properties // or just all external ones. if (doAll) { workingSchema.removeChildren(); // Remove the properties here, delete the schema below. } else { for (int propOrdinal = workingSchema.getChildrenLength(); propOrdinal > 0; --propOrdinal) { XMPNode workingProp = workingSchema.getChild(propOrdinal); if (!Utils.isInternalProperty(workingSchema.getName(), workingProp.getName())) { workingSchema.removeChild(propOrdinal); } } } } else { // Check each of the working XMP's properties to see if it is in the template. for (int propOrdinal = workingSchema.getChildrenLength(); propOrdinal > 0; --propOrdinal) { XMPNode workingProp = workingSchema.getChild(propOrdinal); if ((doAll || !Utils.isInternalProperty(workingSchema.getName(), workingProp.getName())) && (XMPNodeUtils.findChildNode(templateSchema, workingProp.getName(), false) == null)) { workingSchema.removeChild(propOrdinal); } } } if (workingSchema.hasChildren() == false) { workingXMP.getRoot().removeChild(schemaOrdinal); } } } if (doAdd | doReplace) { for (int schemaNum = 0, schemaLim = templateXMP.getRoot() .getChildrenLength(); schemaNum < schemaLim; ++schemaNum) { XMPNode templateSchema = templateXMP.getRoot().getChild(schemaNum + 1); // Make sure we have an output schema node, then process the top level template properties. XMPNode workingSchema = XMPNodeUtils.findSchemaNode(workingXMP.getRoot(), templateSchema.getName(), false); if (workingSchema == null) { workingSchema = new XMPNode(templateSchema.getName(), templateSchema.getValue(), new PropertyOptions(PropertyOptions.SCHEMA_NODE)); workingXMP.getRoot().addChild(workingSchema); workingSchema.setParent(workingXMP.getRoot()); } for (int propNum = 1, propLim = templateSchema.getChildrenLength(); propNum <= propLim; ++propNum) { XMPNode templateProp = templateSchema.getChild(propNum); if (doAll || !Utils.isInternalProperty(templateSchema.getName(), templateProp.getName())) { appendSubtree(workingXMP, templateProp, workingSchema, doAdd, doReplace, deleteEmpty); } } if (workingSchema.hasChildren() == false) { workingXMP.getRoot().removeChild(workingSchema); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy