1// =================================================================================================
2// ADOBE SYSTEMS INCORPORATED
3// Copyright 2006 Adobe Systems Incorporated
4// All Rights Reserved
5//
6// NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
7// of the Adobe license agreement accompanying it.
8// =================================================================================================
9
10package com.adobe.xmp;
11
12import com.adobe.xmp.impl.Base64;
13import com.adobe.xmp.impl.ISO8601Converter;
14import com.adobe.xmp.impl.XMPUtilsImpl;
15import com.adobe.xmp.options.PropertyOptions;
16
17
18/**
19 * Utility methods for XMP. I included only those that are different from the
20 * Java default conversion utilities.
21 *
22 * @since 21.02.2006
23 */
24public class XMPUtils
25{
26	/** Private constructor */
27	private XMPUtils()
28	{
29		// EMPTY
30	}
31
32
33	/**
34	 * Create a single edit string from an array of strings.
35	 *
36	 * @param xmp
37	 *            The XMP object containing the array to be catenated.
38	 * @param schemaNS
39	 *            The schema namespace URI for the array. Must not be null or
40	 *            the empty string.
41	 * @param arrayName
42	 *            The name of the array. May be a general path expression, must
43	 *            not be null or the empty string. Each item in the array must
44	 *            be a simple string value.
45	 * @param separator
46	 *            The string to be used to separate the items in the catenated
47	 *            string. Defaults to "; ", ASCII semicolon and space
48	 *            (U+003B, U+0020).
49	 * @param quotes
50	 *            The characters to be used as quotes around array items that
51	 *            contain a separator. Defaults to '"'
52	 * @param allowCommas
53	 *            Option flag to control the catenation.
54	 * @return Returns the string containing the catenated array items.
55	 * @throws XMPException Forwards the Exceptions from the metadata processing
56	 */
57	public static String catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName,
58			String separator, String quotes, boolean allowCommas) throws XMPException
59	{
60		return XMPUtilsImpl
61				.catenateArrayItems(xmp, schemaNS, arrayName, separator, quotes, allowCommas);
62	}
63
64
65	/**
66	 * Separate a single edit string into an array of strings.
67	 *
68	 * @param xmp
69	 *            The XMP object containing the array to be updated.
70	 * @param schemaNS
71	 *            The schema namespace URI for the array. Must not be null or
72	 *            the empty string.
73	 * @param arrayName
74	 *            The name of the array. May be a general path expression, must
75	 *            not be null or the empty string. Each item in the array must
76	 *            be a simple string value.
77	 * @param catedStr
78	 *            The string to be separated into the array items.
79	 * @param arrayOptions Option flags to control the separation.
80	 * @param preserveCommas Flag if commas shall be preserved
81	 * @throws XMPException Forwards the Exceptions from the metadata processing
82	 */
83	public static void separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName,
84			String catedStr, PropertyOptions arrayOptions, boolean preserveCommas)
85				throws XMPException
86	{
87		XMPUtilsImpl.separateArrayItems(xmp, schemaNS, arrayName, catedStr, arrayOptions,
88				preserveCommas);
89	}
90
91
92	/**
93	 * Remove multiple properties from an XMP object.
94	 *
95	 * RemoveProperties was created to support the File Info dialog's Delete
96	 * button, and has been been generalized somewhat from those specific needs.
97	 * It operates in one of three main modes depending on the schemaNS and
98	 * propName parameters:
99	 *
100	 * <ul>
101	 * <li> Non-empty <code>schemaNS</code> and <code>propName</code> - The named property is
102	 * removed if it is an external property, or if the
103	 * flag <code>doAllProperties</code> option is true. It does not matter whether the
104	 * named property is an actual property or an alias.
105	 *
106	 * <li> Non-empty <code>schemaNS</code> and empty <code>propName</code> - The all external
107	 * properties in the named schema are removed. Internal properties are also
108	 * removed if the flag <code>doAllProperties</code> option is set. In addition,
109	 * aliases from the named schema will be removed if the flag <code>includeAliases</code>
110	 * option is set.
111	 *
112	 * <li> Empty <code>schemaNS</code> and empty <code>propName</code> - All external properties in
113	 * all schema are removed. Internal properties are also removed if the
114	 * flag <code>doAllProperties</code> option is passed. Aliases are implicitly handled
115	 * because the associated actuals are internal if the alias is.
116	 * </ul>
117	 *
118	 * It is an error to pass an empty <code>schemaNS</code> and non-empty <code>propName</code>.
119	 *
120	 * @param xmp
121	 *            The XMP object containing the properties to be removed.
122	 *
123	 * @param schemaNS
124	 *            Optional schema namespace URI for the properties to be
125	 *            removed.
126	 *
127	 * @param propName
128	 *            Optional path expression for the property to be removed.
129	 *
130	 * @param doAllProperties Option flag to control the deletion: do internal properties in
131	 *          addition to external properties.
132	 *
133	 * @param includeAliases Option flag to control the deletion:
134	 * 			Include aliases in the "named schema" case above.
135	 * 			<em>Note:</em> Currently not supported.
136	 * @throws XMPException Forwards the Exceptions from the metadata processing
137	 */
138	public static void removeProperties(XMPMeta xmp, String schemaNS, String propName,
139			boolean doAllProperties, boolean includeAliases) throws XMPException
140	{
141		XMPUtilsImpl.removeProperties(xmp, schemaNS, propName, doAllProperties, includeAliases);
142	}
143
144
145
146	/**
147	 * Alias without the new option <code>deleteEmptyValues</code>.
148	 * @param source The source XMP object.
149	 * @param dest The destination XMP object.
150	 * @param doAllProperties Do internal properties in addition to external properties.
151	 * @param replaceOldValues Replace the values of existing properties.
152	 * @throws XMPException Forwards the Exceptions from the metadata processing
153	 */
154	public static void appendProperties(XMPMeta source, XMPMeta dest, boolean doAllProperties,
155			boolean replaceOldValues) throws XMPException
156	{
157		appendProperties(source, dest, doAllProperties, replaceOldValues, false);
158	}
159
160
161	/**
162	 * <p>Append properties from one XMP object to another.
163	 *
164	 * <p>XMPUtils#appendProperties was created to support the File Info dialog's Append button, and
165	 * has been been generalized somewhat from those specific needs. It appends information from one
166	 * XMP object (source) to another (dest). The default operation is to append only external
167	 * properties that do not already exist in the destination. The flag
168	 * <code>doAllProperties</code> can be used to operate on all properties, external and internal.
169	 * The flag <code>replaceOldValues</code> option can be used to replace the values
170	 * of existing properties. The notion of external
171	 * versus internal applies only to top level properties. The keep-or-replace-old notion applies
172	 * within structs and arrays as described below.
173	 * <ul>
174	 * <li>If <code>replaceOldValues</code> is true then the processing is restricted to the top
175	 * level properties. The processed properties from the source (according to
176	 * <code>doAllProperties</code>) are propagated to the destination,
177	 * replacing any existing values.Properties in the destination that are not in the source
178	 * are left alone.
179	 *
180	 * <li>If <code>replaceOldValues</code> is not passed then the processing is more complicated.
181	 * Top level properties are added to the destination if they do not already exist.
182	 * If they do exist but differ in form (simple/struct/array) then the destination is left alone.
183	 * If the forms match, simple properties are left unchanged while structs and arrays are merged.
184	 *
185	 * <li>If <code>deleteEmptyValues</code> is passed then an empty value in the source XMP causes
186	 * the corresponding destination XMP property to be deleted. The default is to treat empty
187	 * values the same as non-empty values. An empty value is any of a simple empty string, an array
188	 * with no items, or a struct with no fields. Qualifiers are ignored.
189	 * </ul>
190	 *
191	 * <p>The detailed behavior is defined by the following pseudo-code:
192	 * <blockquote>
193	 * <pre>
194     *    appendProperties ( sourceXMP, destXMP, doAllProperties,
195     *    			replaceOldValues, deleteEmptyValues ):
196     *       for all source schema (top level namespaces):
197     *          for all top level properties in sourceSchema:
198     *             if doAllProperties or prop is external:
199     *                appendSubtree ( sourceNode, destSchema, replaceOldValues, deleteEmptyValues )
200     *
201     *    appendSubtree ( sourceNode, destParent, replaceOldValues, deleteEmptyValues ):
202     *        if deleteEmptyValues and source value is empty:
203     *            delete the corresponding child from destParent
204     *        else if sourceNode not in destParent (by name):
205     *           copy sourceNode's subtree to destParent
206     *        else if replaceOld:
207     *            delete subtree from destParent
208     *            copy sourceNode's subtree to destParent
209     *        else:
210     *            // Already exists in dest and not replacing, merge structs and arrays
211     *            if sourceNode and destNode forms differ:
212     *                return, leave the destNode alone
213     *            else if form is a struct:
214     *                for each field in sourceNode:
215     *                    AppendSubtree ( sourceNode.field, destNode, replaceOldValues )
216     *            else if form is an alt-text array:
217     *                copy new items by "xml:lang" value into the destination
218     *            else if form is an array:
219     *                copy new items by value into the destination, ignoring order and duplicates
220     * </pre>
221	 * </blockquote>
222	 *
223	 * <p><em>Note:</em> appendProperties can be expensive if replaceOldValues is not passed and
224	 * the XMP contains large arrays. The array item checking described above is n-squared.
225	 * Each source item is checked to see if it already exists in the destination,
226	 * without regard to order or duplicates.
227	 * <p>Simple items are compared by value and "xml:lang" qualifier, other qualifiers are ignored.
228	 * Structs are recursively compared by field names, without regard to field order. Arrays are
229	 * compared by recursively comparing all items.
230	 *
231	 * @param source The source XMP object.
232	 * @param dest The destination XMP object.
233	 * @param doAllProperties Do internal properties in addition to external properties.
234	 * @param replaceOldValues Replace the values of existing properties.
235	 * @param deleteEmptyValues Delete destination values if source property is empty.
236	 * @throws XMPException Forwards the Exceptions from the metadata processing
237	 */
238	public static void appendProperties(XMPMeta source, XMPMeta dest, boolean doAllProperties,
239			boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException
240	{
241		XMPUtilsImpl.appendProperties(source, dest, doAllProperties, replaceOldValues,
242			deleteEmptyValues);
243	}
244
245
246	/**
247	 * Convert from string to Boolean.
248	 *
249	 * @param value
250	 *            The string representation of the Boolean.
251	 * @return The appropriate boolean value for the string. The checked values
252	 *         for <code>true</code> and <code>false</code> are:
253	 *         <ul>
254	 *    	    	<li>{@link XMPConst#TRUESTR} and {@link XMPConst#FALSESTR}
255	 *    		    <li>&quot;t&quot; and &quot;f&quot;
256	 *    		    <li>&quot;on&quot; and &quot;off&quot;
257	 *    		    <li>&quot;yes&quot; and &quot;no&quot;
258	 *   		  	<li>&quot;value <> 0&quot; and &quot;value == 0&quot;
259	 *         </ul>
260	 * @throws XMPException If an empty string is passed.
261	 */
262	public static boolean convertToBoolean(String value) throws XMPException
263	{
264		if (value == null  ||  value.length() == 0)
265		{
266			throw new XMPException("Empty convert-string", XMPError.BADVALUE);
267		}
268		value = value.toLowerCase();
269
270		try
271		{
272			// First try interpretation as Integer (anything not 0 is true)
273			return Integer.parseInt(value) != 0;
274		}
275		catch (NumberFormatException e)
276		{
277			return
278				"true".equals(value)  ||
279				"t".equals(value)  ||
280				"on".equals(value)  ||
281				"yes".equals(value);
282		}
283	}
284
285
286	/**
287	 * Convert from boolean to string.
288	 *
289	 * @param value
290	 *            a boolean value
291	 * @return The XMP string representation of the boolean. The values used are
292	 *         given by the constnts {@link XMPConst#TRUESTR} and
293	 *         {@link XMPConst#FALSESTR}.
294	 */
295	public static String convertFromBoolean(boolean value)
296	{
297		return value ? XMPConst.TRUESTR : XMPConst.FALSESTR;
298	}
299
300
301	/**
302	 * Converts a string value to an <code>int</code>.
303	 *
304	 * @param rawValue
305	 *            the string value
306	 * @return Returns an int.
307	 * @throws XMPException
308	 *             If the <code>rawValue</code> is <code>null</code> or empty or the
309	 *             conversion fails.
310	 */
311	public static int convertToInteger(String rawValue) throws XMPException
312	{
313		try
314		{
315			if (rawValue == null  ||  rawValue.length() == 0)
316			{
317				throw new XMPException("Empty convert-string", XMPError.BADVALUE);
318			}
319			if (rawValue.startsWith("0x"))
320			{
321				return Integer.parseInt(rawValue.substring(2), 16);
322			}
323			else
324			{
325				return Integer.parseInt(rawValue);
326			}
327		}
328		catch (NumberFormatException e)
329		{
330			throw new XMPException("Invalid integer string", XMPError.BADVALUE);
331		}
332	}
333
334
335	/**
336	 * Convert from int to string.
337	 *
338	 * @param value
339	 *            an int value
340	 * @return The string representation of the int.
341	 */
342	public static String convertFromInteger(int value)
343	{
344		return String.valueOf(value);
345	}
346
347
348	/**
349	 * Converts a string value to a <code>long</code>.
350	 *
351	 * @param rawValue
352	 *            the string value
353	 * @return Returns a long.
354	 * @throws XMPException
355	 *             If the <code>rawValue</code> is <code>null</code> or empty or the
356	 *             conversion fails.
357	 */
358	public static long convertToLong(String rawValue) throws XMPException
359	{
360		try
361		{
362			if (rawValue == null  ||  rawValue.length() == 0)
363			{
364				throw new XMPException("Empty convert-string", XMPError.BADVALUE);
365			}
366			if (rawValue.startsWith("0x"))
367			{
368				return Long.parseLong(rawValue.substring(2), 16);
369			}
370			else
371			{
372				return Long.parseLong(rawValue);
373			}
374		}
375		catch (NumberFormatException e)
376		{
377			throw new XMPException("Invalid long string", XMPError.BADVALUE);
378		}
379	}
380
381
382	/**
383	 * Convert from long to string.
384	 *
385	 * @param value
386	 *            a long value
387	 * @return The string representation of the long.
388	 */
389	public static String convertFromLong(long value)
390	{
391		return String.valueOf(value);
392	}
393
394
395	/**
396	 * Converts a string value to a <code>double</code>.
397	 *
398	 * @param rawValue
399	 *            the string value
400	 * @return Returns a double.
401	 * @throws XMPException
402	 *             If the <code>rawValue</code> is <code>null</code> or empty or the
403	 *             conversion fails.
404	 */
405	public static double convertToDouble(String rawValue) throws XMPException
406	{
407		try
408		{
409			if (rawValue == null  ||  rawValue.length() == 0)
410			{
411				throw new XMPException("Empty convert-string", XMPError.BADVALUE);
412			}
413			else
414			{
415				return Double.parseDouble(rawValue);
416			}
417		}
418		catch (NumberFormatException e)
419		{
420			throw new XMPException("Invalid double string", XMPError.BADVALUE);
421		}
422	}
423
424
425	/**
426	 * Convert from long to string.
427	 *
428	 * @param value
429	 *            a long value
430	 * @return The string representation of the long.
431	 */
432	public static String convertFromDouble(double value)
433	{
434		return String.valueOf(value);
435	}
436
437
438	/**
439	 * Converts a string value to an <code>XMPDateTime</code>.
440	 *
441	 * @param rawValue
442	 *            the string value
443	 * @return Returns an <code>XMPDateTime</code>-object.
444	 * @throws XMPException
445	 *             If the <code>rawValue</code> is <code>null</code> or empty or the
446	 *             conversion fails.
447	 */
448	public static XMPDateTime convertToDate(String rawValue) throws XMPException
449	{
450		if (rawValue == null  ||  rawValue.length() == 0)
451		{
452			throw new XMPException("Empty convert-string", XMPError.BADVALUE);
453		}
454		else
455		{
456			return ISO8601Converter.parse(rawValue);
457		}
458	}
459
460
461	/**
462	 * Convert from <code>XMPDateTime</code> to string.
463	 *
464	 * @param value
465	 *            an <code>XMPDateTime</code>
466	 * @return The string representation of the long.
467	 */
468	public static String convertFromDate(XMPDateTime value)
469	{
470		return ISO8601Converter.render(value);
471	}
472
473
474	 /**
475	  * Convert from a byte array to a base64 encoded string.
476	  *
477	  * @param buffer
478	  *            the byte array to be converted
479	  * @return Returns the base64 string.
480	  */
481	public static String encodeBase64(byte[] buffer)
482	{
483		return new String(Base64.encode(buffer));
484	}
485
486
487	/**
488	 * Decode from Base64 encoded string to raw data.
489	 *
490	 * @param base64String
491	 *            a base64 encoded string
492	 * @return Returns a byte array containg the decoded string.
493	 * @throws XMPException Thrown if the given string is not property base64 encoded
494	 */
495	public static byte[] decodeBase64(String base64String) throws XMPException
496	{
497		try
498		{
499			return Base64.decode(base64String.getBytes());
500		}
501		catch (Throwable e)
502		{
503			throw new XMPException("Invalid base64 string", XMPError.BADVALUE, e);
504		}
505	}
506}