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 
10 package com.adobe.xmp;
11 
12 import com.adobe.xmp.impl.Base64;
13 import com.adobe.xmp.impl.ISO8601Converter;
14 import com.adobe.xmp.impl.XMPUtilsImpl;
15 import 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  */
24 public class XMPUtils
25 {
26 	/** Private constructor */
XMPUtils()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 	 */
catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, String separator, String quotes, boolean allowCommas)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 	 */
separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, String catedStr, PropertyOptions arrayOptions, boolean preserveCommas)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 	 */
removeProperties(XMPMeta xmp, String schemaNS, String propName, boolean doAllProperties, boolean includeAliases)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 	 */
appendProperties(XMPMeta source, XMPMeta dest, boolean doAllProperties, boolean replaceOldValues)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 	 */
appendProperties(XMPMeta source, XMPMeta dest, boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues)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 	 */
convertToBoolean(String value)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 	 */
convertFromBoolean(boolean value)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 	 */
convertToInteger(String rawValue)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 	 */
convertFromInteger(int value)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 	 */
convertToLong(String rawValue)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 	 */
convertFromLong(long value)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 	 */
convertToDouble(String rawValue)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 	 */
convertFromDouble(double value)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 	 */
convertToDate(String rawValue)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 	 */
convertFromDate(XMPDateTime value)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 	  */
encodeBase64(byte[] buffer)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 	 */
decodeBase64(String base64String)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 }