diff options
author | Jeff Hamilton <jham@android.com> | 2012-10-04 22:19:41 -0500 |
---|---|---|
committer | Jeff Hamilton <jham@android.com> | 2012-10-04 22:19:41 -0500 |
commit | 25f0eb884d66f7eb0d16953fe3f54f96df544e28 (patch) | |
tree | 5dd81f14724b29d6e356e746ed2b1601f1951f52 | |
parent | 3960586f08fe6d3579586d4225f90bd00345254f (diff) | |
parent | f12f744843a67c910ec325fc6dfa73988f67b97c (diff) | |
download | xmp_toolkit-25f0eb884d66f7eb0d16953fe3f54f96df544e28.tar.gz |
Merge commit 'f12f744' into HEAD
54 files changed, 17815 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..3cc93fb --- /dev/null +++ b/Android.mk @@ -0,0 +1,2 @@ +# Include the makefiles under this directory. +include $(call all-makefiles-under,$(call my-dir)) diff --git a/MODULE_LICENSE_BSD b/MODULE_LICENSE_BSD new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_BSD @@ -0,0 +1,32 @@ +The BSD License + +Copyright (c) 1999 - 2010, Adobe Systems Incorporated +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: + +* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +* Neither the name of Adobe Systems Incorporated, nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/XMPCore/Android.mk b/XMPCore/Android.mk new file mode 100644 index 0000000..955d296 --- /dev/null +++ b/XMPCore/Android.mk @@ -0,0 +1,13 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +# Include all the java files. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_SDK_VERSION := 8 + +# The name of the jar file to create. +LOCAL_MODULE := xmp_toolkit + +# Build a static jar file. +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/XMPCore/src/com/adobe/xmp/XMPConst.java b/XMPCore/src/com/adobe/xmp/XMPConst.java new file mode 100644 index 0000000..61a5ca8 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPConst.java @@ -0,0 +1,167 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + + +/** + * Common constants for the XMP Toolkit. + * + * @since 20.01.2006 + */ +public interface XMPConst +{ + // --------------------------------------------------------------------------------------------- + // Standard namespace URI constants + + + // Standard namespaces + + /** The XML namespace for XML. */ + String NS_XML = "http://www.w3.org/XML/1998/namespace"; + /** The XML namespace for RDF. */ + String NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; + /** The XML namespace for the Dublin Core schema. */ + String NS_DC = "http://purl.org/dc/elements/1.1/"; + /** The XML namespace for the IPTC Core schema. */ + String NS_IPTCCORE = "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"; + + // Adobe standard namespaces + + /** The XML namespace Adobe XMP Metadata. */ + String NS_X = "adobe:ns:meta/"; + /** */ + String NS_IX = "http://ns.adobe.com/iX/1.0/"; + /** The XML namespace for the XMP "basic" schema. */ + String NS_XMP = "http://ns.adobe.com/xap/1.0/"; + /** The XML namespace for the XMP copyright schema. */ + String NS_XMP_RIGHTS = "http://ns.adobe.com/xap/1.0/rights/"; + /** The XML namespace for the XMP digital asset management schema. */ + String NS_XMP_MM = "http://ns.adobe.com/xap/1.0/mm/"; + /** The XML namespace for the job management schema. */ + String NS_XMP_BJ = "http://ns.adobe.com/xap/1.0/bj/"; + /** The XML namespace for the job management schema. */ + String NS_XMP_NOTE = "http://ns.adobe.com/xmp/note/"; + + /** The XML namespace for the PDF schema. */ + String NS_PDF = "http://ns.adobe.com/pdf/1.3/"; + /** The XML namespace for the PDF schema. */ + String NS_PDFX = "http://ns.adobe.com/pdfx/1.3/"; + /** */ + String NS_PDFX_ID = "http://www.npes.org/pdfx/ns/id/"; + /** */ + String NS_PDFA_SCHEMA = "http://www.aiim.org/pdfa/ns/schema#"; + /** */ + String NS_PDFA_PROPERTY = "http://www.aiim.org/pdfa/ns/property#"; + /** */ + String NS_PDFA_TYPE = "http://www.aiim.org/pdfa/ns/type#"; + /** */ + String NS_PDFA_FIELD = "http://www.aiim.org/pdfa/ns/field#"; + /** */ + String NS_PDFA_ID = "http://www.aiim.org/pdfa/ns/id/"; + /** */ + String NS_PDFA_EXTENSION = "http://www.aiim.org/pdfa/ns/extension/"; + /** The XML namespace for the Photoshop custom schema. */ + String NS_PHOTOSHOP = "http://ns.adobe.com/photoshop/1.0/"; + /** The XML namespace for the Photoshop Album schema. */ + String NS_PSALBUM = "http://ns.adobe.com/album/1.0/"; + /** The XML namespace for Adobe's EXIF schema. */ + String NS_EXIF = "http://ns.adobe.com/exif/1.0/"; + /** */ + String NS_EXIF_AUX = "http://ns.adobe.com/exif/1.0/aux/"; + /** The XML namespace for Adobe's TIFF schema. */ + String NS_TIFF = "http://ns.adobe.com/tiff/1.0/"; + /** */ + String NS_PNG = "http://ns.adobe.com/png/1.0/"; + /** */ + String NS_JPEG = "http://ns.adobe.com/jpeg/1.0/"; + /** */ + String NS_JP2K = "http://ns.adobe.com/jp2k/1.0/"; + /** */ + String NS_CAMERARAW = "http://ns.adobe.com/camera-raw-settings/1.0/"; + /** */ + String NS_ADOBESTOCKPHOTO = "http://ns.adobe.com/StockPhoto/1.0/"; + /** */ + String NS_CREATOR_ATOM = "http://ns.adobe.com/creatorAtom/1.0/"; + /** */ + String NS_ASF = "http://ns.adobe.com/asf/1.0/"; + /** */ + String NS_WAV = "http://ns.adobe.com/xmp/wav/1.0/"; + + + // XMP namespaces that are Adobe private + + /** */ + String NS_DM = "http://ns.adobe.com/xmp/1.0/DynamicMedia/"; + /** */ + String NS_TRANSIENT = "http://ns.adobe.com/xmp/transient/1.0/"; + /** legaciy dublin core NS, will be converted to NS_DC */ + String NS_DC_DEPRECATED = "http://purl.org/dc/1.1/"; + + + // XML namespace constants for qualifiers and structured property fields. + + /** The XML namespace for qualifiers of the xmp:Identifier property. */ + String TYPE_IDENTIFIERQUAL = "http://ns.adobe.com/xmp/Identifier/qual/1.0/"; + /** The XML namespace for fields of the Dimensions type. */ + String TYPE_DIMENSIONS = "http://ns.adobe.com/xap/1.0/sType/Dimensions#"; + /** */ + String TYPE_TEXT = "http://ns.adobe.com/xap/1.0/t/"; + /** */ + String TYPE_PAGEDFILE = "http://ns.adobe.com/xap/1.0/t/pg/"; + /** */ + String TYPE_GRAPHICS = "http://ns.adobe.com/xap/1.0/g/"; + /** The XML namespace for fields of a graphical image. Used for the Thumbnail type. */ + String TYPE_IMAGE = "http://ns.adobe.com/xap/1.0/g/img/"; + /** */ + String TYPE_FONT = "http://ns.adobe.com/xap/1.0/sType/Font#"; + /** The XML namespace for fields of the ResourceEvent type. */ + String TYPE_RESOURCEEVENT = "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"; + /** The XML namespace for fields of the ResourceRef type. */ + String TYPE_RESOURCEREF = "http://ns.adobe.com/xap/1.0/sType/ResourceRef#"; + /** The XML namespace for fields of the Version type. */ + String TYPE_ST_VERSION = "http://ns.adobe.com/xap/1.0/sType/Version#"; + /** The XML namespace for fields of the JobRef type. */ + String TYPE_ST_JOB = "http://ns.adobe.com/xap/1.0/sType/Job#"; + /** */ + String TYPE_MANIFESTITEM = "http://ns.adobe.com/xap/1.0/sType/ManifestItem#"; + + + + // --------------------------------------------------------------------------------------------- + // Basic types and constants + + /** + * The canonical true string value for Booleans in serialized XMP. Code that converts from the + * string to a bool should be case insensitive, and even allow "1". + */ + String TRUESTR = "True"; + /** + * The canonical false string value for Booleans in serialized XMP. Code that converts from the + * string to a bool should be case insensitive, and even allow "0". + */ + String FALSESTR = "False"; + /** Index that has the meaning to be always the last item in an array. */ + int ARRAY_LAST_ITEM = -1; + /** Node name of an array item. */ + String ARRAY_ITEM_NAME = "[]"; + /** The x-default string for localized properties */ + String X_DEFAULT = "x-default"; + /** xml:lang qualfifier */ + String XML_LANG = "xml:lang"; + /** rdf:type qualfifier */ + String RDF_TYPE = "rdf:type"; + + /** Processing Instruction (PI) for xmp packet */ + String XMP_PI = "xpacket"; + /** XMP meta tag version new */ + String TAG_XMPMETA = "xmpmeta"; + /** XMP meta tag version old */ + String TAG_XAPMETA = "xapmeta"; +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPDateTime.java b/XMPCore/src/com/adobe/xmp/XMPDateTime.java new file mode 100644 index 0000000..505d580 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPDateTime.java @@ -0,0 +1,102 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import java.util.Calendar; +import java.util.TimeZone; + + +/** + * The <code>XMPDateTime</code>-class represents a point in time up to a resolution of nano + * seconds. Dates and time in the serialized XMP are ISO 8601 strings. There are utility functions + * to convert to the ISO format, a <code>Calendar</code> or get the Timezone. The fields of + * <code>XMPDateTime</code> are: + * <ul> + * <li> month - The month in the range 1..12. + * <li> day - The day of the month in the range 1..31. + * <li> minute - The minute in the range 0..59. + * <li> hour - The time zone hour in the range 0..23. + * <li> minute - The time zone minute in the range 0..59. + * <li> nanoSecond - The nano seconds within a second. <em>Note:</em> if the XMPDateTime is + * converted into a calendar, the resolution is reduced to milli seconds. + * <li> timeZone - a <code>TimeZone</code>-object. + * </ul> + * DateTime values are occasionally used in cases with only a date or only a time component. A date + * without a time has zeros for all the time fields. A time without a date has zeros for all date + * fields (year, month, and day). + */ +public interface XMPDateTime extends Comparable +{ + /** @return Returns the year, can be negative. */ + int getYear(); + + /** @param year Sets the year */ + void setYear(int year); + + /** @return Returns The month in the range 1..12. */ + int getMonth(); + + /** @param month Sets the month 1..12 */ + void setMonth(int month); + + /** @return Returns the day of the month in the range 1..31. */ + int getDay(); + + /** @param day Sets the day 1..31 */ + void setDay(int day); + + /** @return Returns hour - The hour in the range 0..23. */ + int getHour(); + + /** @param hour Sets the hour in the range 0..23. */ + void setHour(int hour); + + /** @return Returns the minute in the range 0..59. */ + int getMinute(); + + /** @param minute Sets the minute in the range 0..59. */ + void setMinute(int minute); + + /** @return Returns the second in the range 0..59. */ + int getSecond(); + + /** @param second Sets the second in the range 0..59. */ + void setSecond(int second); + + /** + * @return Returns milli-, micro- and nano seconds. + * Nanoseconds within a second, often left as zero? + */ + int getNanoSecond(); + + /** + * @param nanoSecond Sets the milli-, micro- and nano seconds. + * Granularity goes down to milli seconds. + */ + void setNanoSecond(int nanoSecond); + + /** @return Returns the time zone. */ + TimeZone getTimeZone(); + + /** @param tz a time zone to set */ + void setTimeZone(TimeZone tz); + + /** + * @return Returns a <code>Calendar</code> (only with milli second precision). <br> + * <em>Note:</em> the dates before Oct 15th 1585 (which normally fall into validity of + * the Julian calendar) are also rendered internally as Gregorian dates. + */ + Calendar getCalendar(); + + /** + * @return Returns the ISO 8601 string representation of the date and time. + */ + String getISO8601String(); +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java b/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java new file mode 100644 index 0000000..ec0a116 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java @@ -0,0 +1,153 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import com.adobe.xmp.impl.XMPDateTimeImpl; + + +/** + * A factory to create <code>XMPDateTime</code>-instances from a <code>Calendar</code> or an + * ISO 8601 string or for the current time. + * + * @since 16.02.2006 + */ +public final class XMPDateTimeFactory +{ + /** The UTC TimeZone */ + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + + + /** Private constructor */ + private XMPDateTimeFactory() + { + // EMPTY + } + + + /** + * Creates an <code>XMPDateTime</code> from a <code>Calendar</code>-object. + * + * @param calendar a <code>Calendar</code>-object. + * @return An <code>XMPDateTime</code>-object. + */ + public static XMPDateTime createFromCalendar(Calendar calendar) + { + return new XMPDateTimeImpl(calendar); + } + + + /** + * Creates an <code>XMPDateTime</code>-object from initial values. + * @param year years + * @param month months from 1 to 12<br> + * <em>Note:</em> Remember that the month in {@link Calendar} is defined from 0 to 11. + * @param day days + * @param hour hours + * @param minute minutes + * @param second seconds + * @param nanoSecond nanoseconds + * @return Returns an <code>XMPDateTime</code>-object. + */ + public static XMPDateTime create(int year, int month, int day, + int hour, int minute, int second, int nanoSecond) + { + XMPDateTime dt = new XMPDateTimeImpl(); + dt.setYear(year); + dt.setMonth(month); + dt.setDay(day); + dt.setHour(hour); + dt.setMinute(minute); + dt.setSecond(second); + dt.setNanoSecond(nanoSecond); + return dt; + } + + + /** + * Creates an <code>XMPDateTime</code> from an ISO 8601 string. + * + * @param strValue The ISO 8601 string representation of the date/time. + * @return An <code>XMPDateTime</code>-object. + * @throws XMPException When the ISO 8601 string is non-conform + */ + public static XMPDateTime createFromISO8601(String strValue) throws XMPException + { + return new XMPDateTimeImpl(strValue); + } + + + /** + * Obtain the current date and time. + * + * @return Returns The returned time is UTC, properly adjusted for the local time zone. The + * resolution of the time is not guaranteed to be finer than seconds. + */ + public static XMPDateTime getCurrentDateTime() + { + return new XMPDateTimeImpl(new GregorianCalendar()); + } + + + /** + * Sets the local time zone without touching any other Any existing time zone value is replaced, + * the other date/time fields are not adjusted in any way. + * + * @param dateTime the <code>XMPDateTime</code> variable containing the value to be modified. + * @return Returns an updated <code>XMPDateTime</code>-object. + */ + public static XMPDateTime setLocalTimeZone(XMPDateTime dateTime) + { + Calendar cal = dateTime.getCalendar(); + cal.setTimeZone(TimeZone.getDefault()); + return new XMPDateTimeImpl(cal); + } + + + /** + * Make sure a time is UTC. If the time zone is not UTC, the time is + * adjusted and the time zone set to be UTC. + * + * @param dateTime + * the <code>XMPDateTime</code> variable containing the time to + * be modified. + * @return Returns an updated <code>XMPDateTime</code>-object. + */ + public static XMPDateTime convertToUTCTime(XMPDateTime dateTime) + { + long timeInMillis = dateTime.getCalendar().getTimeInMillis(); + GregorianCalendar cal = new GregorianCalendar(UTC); + cal.setGregorianChange(new Date(Long.MIN_VALUE)); + cal.setTimeInMillis(timeInMillis); + return new XMPDateTimeImpl(cal); + } + + + /** + * Make sure a time is local. If the time zone is not the local zone, the time is adjusted and + * the time zone set to be local. + * + * @param dateTime the <code>XMPDateTime</code> variable containing the time to be modified. + * @return Returns an updated <code>XMPDateTime</code>-object. + */ + public static XMPDateTime convertToLocalTime(XMPDateTime dateTime) + { + long timeInMillis = dateTime.getCalendar().getTimeInMillis(); + // has automatically local timezone + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(timeInMillis); + return new XMPDateTimeImpl(cal); + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPError.java b/XMPCore/src/com/adobe/xmp/XMPError.java new file mode 100644 index 0000000..c04c834 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPError.java @@ -0,0 +1,44 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + + +/** + * @since 21.09.2006 + */ +public interface XMPError +{ + /** */ + int UNKNOWN = 0; + /** */ + int BADPARAM = 4; + /** */ + int BADVALUE = 5; + /** */ + int INTERNALFAILURE = 9; + /** */ + int BADSCHEMA = 101; + /** */ + int BADXPATH = 102; + /** */ + int BADOPTIONS = 103; + /** */ + int BADINDEX = 104; + /** */ + int BADSERIALIZE = 107; + /** */ + int BADXML = 201; + /** */ + int BADRDF = 202; + /** */ + int BADXMP = 203; + /** <em>Note:</em> This is an error code introduced by Java. */ + int BADSTREAM = 204; +} diff --git a/XMPCore/src/com/adobe/xmp/XMPException.java b/XMPCore/src/com/adobe/xmp/XMPException.java new file mode 100644 index 0000000..087b0e8 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPException.java @@ -0,0 +1,55 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +/** + * This exception wraps all errors that occur in the XMP Toolkit. + * + * @since 16.02.2006 + */ +public class XMPException extends Exception +{ + /** the errorCode of the XMP toolkit */ + private int errorCode; + + + /** + * Constructs an exception with a message and an error code. + * @param message the message + * @param errorCode the error code + */ + public XMPException(String message, int errorCode) + { + super(message); + this.errorCode = errorCode; + } + + + /** + * Constructs an exception with a message, an error code and a <code>Throwable</code> + * @param message the error message. + * @param errorCode the error code + * @param t the exception source + */ + public XMPException(String message, int errorCode, Throwable t) + { + super(message, t); + this.errorCode = errorCode; + } + + + /** + * @return Returns the errorCode. + */ + public int getErrorCode() + { + return errorCode; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPIterator.java b/XMPCore/src/com/adobe/xmp/XMPIterator.java new file mode 100644 index 0000000..fa4edf6 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPIterator.java @@ -0,0 +1,82 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import java.util.Iterator; + + +/** + * Interface for the <code>XMPMeta</code> iteration services. + * <code>XMPIterator</code> provides a uniform means to iterate over the + * schema and properties within an XMP object. + * <p> + * The iteration over the schema and properties within an XMP object is very + * complex. It is helpful to have a thorough understanding of the XMP data tree. + * One way to learn this is to create some complex XMP and examine the output of + * <code>XMPMeta#toString</code>. This is also described in the XMP + * Specification, in the XMP Data Model chapter. + * <p> + * The top of the XMP data tree is a single root node. This does not explicitly + * appear in the dump and is never visited by an iterator (that is, it is never + * returned from <code>XMPIterator#next()</code>). Beneath the root are + * schema nodes. These are just collectors for top level properties in the same + * namespace. They are created and destroyed implicitly. Beneath the schema + * nodes are the property nodes. The nodes below a property node depend on its + * type (simple, struct, or array) and whether it has qualifiers. + * <p> + * An <code>XMPIterator</code> is created by XMPMeta#interator() constructor + * defines a starting point for the iteration and options that control how it + * proceeds. By default the iteration starts at the root and visits all nodes + * beneath it in a depth first manner. The root node is not visited, the first + * visited node is a schema node. You can provide a schema name or property path + * to select a different starting node. By default this visits the named root + * node first then all nodes beneath it in a depth first manner. + * <p> + * The <code>XMPIterator#next()</code> method delivers the schema URI, path, + * and option flags for the node being visited. If the node is simple it also + * delivers the value. Qualifiers for this node are visited next. The fields of + * a struct or items of an array are visited after the qualifiers of the parent. + * <p> + * The options to control the iteration are: + * <ul> + * <li>JUST_CHILDREN - Visit just the immediate children of the root. Skip + * the root itself and all nodes below the immediate children. This omits the + * qualifiers of the immediate children, the qualifier nodes being below what + * they qualify, default is to visit the complete subtree. + * <li>UST_LEAFNODES - Visit just the leaf property nodes and their + * qualifiers. + * <li>JUST_LEAFNAME - Return just the leaf component of the node names. + * The default is to return the full xmp path. + * <li>OMIT_QUALIFIERS - Do not visit the qualifiers. + * <li>INCLUDE_ALIASES - Adds known alias properties to the properties in the iteration. + * <em>Note:</em> Not supported in Java XMPCore! + * </ul> + * <p> + * <code>next()</code> returns <code>XMPPropertyInfo</code>-objects and throws + * a <code>NoSuchElementException</code> if there are no more properties to + * return. + * + * @since 25.01.2006 + */ +public interface XMPIterator extends Iterator +{ + /** + * Skip the subtree below the current node when <code>next()</code> is + * called. + */ + void skipSubtree(); + + + /** + * Skip the subtree below and remaining siblings of the current node when + * <code>next()</code> is called. + */ + void skipSiblings(); +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPMeta.java b/XMPCore/src/com/adobe/xmp/XMPMeta.java new file mode 100644 index 0000000..dab0502 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPMeta.java @@ -0,0 +1,1176 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import java.util.Calendar; + +import com.adobe.xmp.options.IteratorOptions; +import com.adobe.xmp.options.ParseOptions; +import com.adobe.xmp.options.PropertyOptions; +import com.adobe.xmp.properties.XMPProperty; + + +/** + * This class represents the set of XMP metadata as a DOM representation. It has methods to read and + * modify all kinds of properties, create an iterator over all properties and serialize the metadata + * to a String, byte-array or <code>OutputStream</code>. + * + * @since 20.01.2006 + */ +public interface XMPMeta extends Cloneable +{ + // --------------------------------------------------------------------------------------------- + // Basic property manipulation functions + + /** + * The property value getter-methods all take a property specification: the first two parameters + * are always the top level namespace URI (the "schema" namespace) and the basic name + * of the property being referenced. See the introductory discussion of path expression usage + * for more information. + * <p> + * All of the functions return an object inherited from <code>PropertyBase</code> or + * <code>null</code> if the property does not exists. The result object contains the value of + * the property and option flags describing the property. Arrays and the non-leaf levels of + * nodes do not have values. + * <p> + * See {@link PropertyOptions} for detailed information about the options. + * <p> + * This is the simplest property getter, mainly for top level simple properties or after using + * the path composition functions in XMPPathFactory. + * + * @param schemaNS The namespace URI for the property. May be <code>null</code> or the empty + * string if the first component of the propName path contains a namespace prefix. The + * URI must be for a registered namespace. + * @param propName The name of the property. May be a general path expression, must not be + * <code>null</code> or the empty string. Using a namespace prefix on the first + * component is optional. If present without a schemaNS value then the prefix specifies + * the namespace. The prefix must be for a registered namespace. If both a schemaNS URI + * and propName prefix are present, they must be corresponding parts of a registered + * namespace. + * @return Returns a <code>XMPProperty</code> containing the value and the options or + * <code>null</code> if the property does not exist. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPProperty getProperty(String schemaNS, String propName) throws XMPException; + + + /** + * Provides access to items within an array. The index is passed as an integer, you need not + * worry about the path string syntax for array items, convert a loop index to a string, etc. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in getProperty. + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param itemIndex The index of the desired item. Arrays in XMP are indexed from 1. The + * constant {@link XMPConst#ARRAY_LAST_ITEM} always refers to the last existing array + * item. + * @return Returns a <code>XMPProperty</code> containing the value and the options or + * <code>null</code> if the property does not exist. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPProperty getArrayItem(String schemaNS, String arrayName, int itemIndex) throws XMPException; + + + /** + * Returns the number of items in the array. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in getProperty. + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @return Returns the number of items in the array. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + int countArrayItems(String schemaNS, String arrayName) throws XMPException; + + + /** + * Provides access to fields within a nested structure. The namespace for the field is passed as + * a URI, you need not worry about the path string syntax. + * <p> + * The names of fields should be XML qualified names, that is within an XML namespace. The path + * syntax for a qualified name uses the namespace prefix. This is unreliable since the prefix is + * never guaranteed. The URI is the formal name, the prefix is just a local shorthand in a given + * sequence of XML text. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in getProperty. + * @param structName The name of the struct. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param fieldNS The namespace URI for the field. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param fieldName The name of the field. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * structName parameter. + * @return Returns a <code>XMPProperty</code> containing the value and the options or + * <code>null</code> if the property does not exist. Arrays and non-leaf levels of + * structs do not have values. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPProperty getStructField( + String schemaNS, + String structName, + String fieldNS, + String fieldName) throws XMPException; + + + /** + * Provides access to a qualifier attached to a property. The namespace for the qualifier is + * passed as a URI, you need not worry about the path string syntax. In many regards qualifiers + * are like struct fields. See the introductory discussion of qualified properties for more + * information. + * <p> + * The names of qualifiers should be XML qualified names, that is within an XML namespace. The + * path syntax for a qualified name uses the namespace prefix. This is unreliable since the + * prefix is never guaranteed. The URI is the formal name, the prefix is just a local shorthand + * in a given sequence of XML text. + * <p> + * <em>Note:</em> Qualifiers are only supported for simple leaf properties at this time. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in getProperty. + * @param propName The name of the property to which the qualifier is attached. May be a general + * path expression, must not be <code>null</code> or the empty string. Has the same + * namespace prefix usage as in <code>getProperty()</code>. + * @param qualNS The namespace URI for the qualifier. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param qualName The name of the qualifier. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * propName parameter. + * @return Returns a <code>XMPProperty</code> containing the value and the options of the + * qualifier or <code>null</code> if the property does not exist. The name of the + * qualifier must be a single XML name, must not be <code>null</code> or the empty + * string. Has the same namespace prefix usage as the propName parameter. + * <p> + * The value of the qualifier is only set if it has one (Arrays and non-leaf levels of + * structs do not have values). + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPProperty getQualifier( + String schemaNS, + String propName, + String qualNS, + String qualName) throws XMPException; + + + + // --------------------------------------------------------------------------------------------- + // Functions for setting property values + + /** + * The property value <code>setters</code> all take a property specification, their + * differences are in the form of this. The first two parameters are always the top level + * namespace URI (the <code>schema</code> namespace) and the basic name of the property being + * referenced. See the introductory discussion of path expression usage for more information. + * <p> + * All of the functions take a string value for the property and option flags describing the + * property. The value must be Unicode in UTF-8 encoding. Arrays and non-leaf levels of structs + * do not have values. Empty arrays and structs may be created using appropriate option flags. + * All levels of structs that is assigned implicitly are created if necessary. appendArayItem + * implicitly creates the named array if necessary. + * <p> + * See {@link PropertyOptions} for detailed information about the options. + * <p> + * This is the simplest property setter, mainly for top level simple properties or after using + * the path composition functions in {@link XMPPathFactory}. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in getProperty. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the value for the property (only leaf properties have a value). + * Arrays and non-leaf levels of structs do not have values. + * Must be <code>null</code> if the value is not relevant.<br/> + * The value is automatically detected: Boolean, Integer, Long, Double, XMPDateTime and + * byte[] are handled, on all other <code>toString()</code> is called. + * + * @param options Option flags describing the property. See the earlier description. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void setProperty( + String schemaNS, + String propName, + Object propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setProperty(String, String, Object, PropertyOptions) + * + * @param schemaNS The namespace URI + * @param propName The name of the property + * @param propValue the value for the property + * @throws XMPException Wraps all errors and exceptions + */ + void setProperty( + String schemaNS, + String propName, + Object propValue) throws XMPException; + + + /** + * Replaces an item within an array. The index is passed as an integer, you need not worry about + * the path string syntax for array items, convert a loop index to a string, etc. The array + * passed must already exist. In normal usage the selected array item is modified. A new item is + * automatically appended if the index is the array size plus 1. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in getProperty. + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in getProperty. + * @param itemIndex The index of the desired item. Arrays in XMP are indexed from 1. To address + * the last existing item, use {@link XMPMeta#countArrayItems(String, String)} to find + * out the length of the array. + * @param itemValue the new value of the array item. Has the same usage as propValue in + * <code>setProperty()</code>. + * @param options the set options for the item. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void setArrayItem( + String schemaNS, + String arrayName, + int itemIndex, + String itemValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setArrayItem(String, String, int, String, PropertyOptions) + * + * @param schemaNS The namespace URI + * @param arrayName The name of the array + * @param itemIndex The index to insert the new item + * @param itemValue the new value of the array item + * @throws XMPException Wraps all errors and exceptions + */ + void setArrayItem( + String schemaNS, + String arrayName, + int itemIndex, + String itemValue) throws XMPException; + + + /** + * Inserts an item into an array previous to the given index. The index is passed as an integer, + * you need not worry about the path string syntax for array items, convert a loop index to a + * string, etc. The array passed must already exist. In normal usage the selected array item is + * modified. A new item is automatically appended if the index is the array size plus 1. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in getProperty. + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in getProperty. + * @param itemIndex The index to insert the new item. Arrays in XMP are indexed from 1. Use + * <code>XMPConst.ARRAY_LAST_ITEM</code> to append items. + * @param itemValue the new value of the array item. Has the same usage as + * propValue in <code>setProperty()</code>. + * @param options the set options that decide about the kind of the node. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void insertArrayItem( + String schemaNS, + String arrayName, + int itemIndex, + String itemValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#insertArrayItem(String, String, int, String, PropertyOptions) + * + * @param schemaNS The namespace URI for the array + * @param arrayName The name of the array + * @param itemIndex The index to insert the new item + * @param itemValue the value of the array item + * @throws XMPException Wraps all errors and exceptions + */ + void insertArrayItem( + String schemaNS, + String arrayName, + int itemIndex, + String itemValue) throws XMPException; + + + /** + * Simplifies the construction of an array by not requiring that you pre-create an empty array. + * The array that is assigned is created automatically if it does not yet exist. Each call to + * appendArrayItem() appends an item to the array. The corresponding parameters have the same + * use as setArrayItem(). The arrayOptions parameter is used to specify what kind of array. If + * the array exists, it must have the specified form. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in getProperty. + * @param arrayName The name of the array. May be a general path expression, must not be null or + * the empty string. Has the same namespace prefix usage as propPath in getProperty. + * @param arrayOptions Option flags describing the array form. The only valid options are + * <ul> + * <li> {@link PropertyOptions#ARRAY}, + * <li> {@link PropertyOptions#ARRAY_ORDERED}, + * <li> {@link PropertyOptions#ARRAY_ALTERNATE} or + * <li> {@link PropertyOptions#ARRAY_ALT_TEXT}. + * </ul> + * <em>Note:</em> the array options only need to be provided if the array is not + * already existing, otherwise you can set them to <code>null</code> or use + * {@link XMPMeta#appendArrayItem(String, String, String)}. + * @param itemValue the value of the array item. Has the same usage as propValue in getProperty. + * @param itemOptions Option flags describing the item to append ({@link PropertyOptions}) + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void appendArrayItem( + String schemaNS, + String arrayName, + PropertyOptions arrayOptions, + String itemValue, + PropertyOptions itemOptions) throws XMPException; + + + /** + * @see XMPMeta#appendArrayItem(String, String, PropertyOptions, String, PropertyOptions) + * + * @param schemaNS The namespace URI for the array + * @param arrayName The name of the array + * @param itemValue the value of the array item + * @throws XMPException Wraps all errors and exceptions + */ + void appendArrayItem( + String schemaNS, + String arrayName, + String itemValue) throws XMPException; + + + /** + * Provides access to fields within a nested structure. The namespace for the field is passed as + * a URI, you need not worry about the path string syntax. The names of fields should be XML + * qualified names, that is within an XML namespace. The path syntax for a qualified name uses + * the namespace prefix, which is unreliable because the prefix is never guaranteed. The URI is + * the formal name, the prefix is just a local shorthand in a given sequence of XML text. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in getProperty. + * @param structName The name of the struct. May be a general path expression, must not be null + * or the empty string. Has the same namespace prefix usage as propName in getProperty. + * @param fieldNS The namespace URI for the field. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param fieldName The name of the field. Must be a single XML name, must not be null or the + * empty string. Has the same namespace prefix usage as the structName parameter. + * @param fieldValue the value of thefield, if the field has a value. + * Has the same usage as propValue in getProperty. + * @param options Option flags describing the field. See the earlier description. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void setStructField( + String schemaNS, + String structName, + String fieldNS, + String fieldName, + String fieldValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setStructField(String, String, String, String, String, PropertyOptions) + * + * @param schemaNS The namespace URI for the struct + * @param structName The name of the struct + * @param fieldNS The namespace URI for the field + * @param fieldName The name of the field + * @param fieldValue the value of the field + * @throws XMPException Wraps all errors and exceptions + */ + void setStructField( + String schemaNS, + String structName, + String fieldNS, + String fieldName, + String fieldValue) throws XMPException; + + + /** + * Provides access to a qualifier attached to a property. The namespace for the qualifier is + * passed as a URI, you need not worry about the path string syntax. In many regards qualifiers + * are like struct fields. See the introductory discussion of qualified properties for more + * information. The names of qualifiers should be XML qualified names, that is within an XML + * namespace. The path syntax for a qualified name uses the namespace prefix, which is + * unreliable because the prefix is never guaranteed. The URI is the formal name, the prefix is + * just a local shorthand in a given sequence of XML text. The property the qualifier + * will be attached has to exist. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in getProperty. + * @param propName The name of the property to which the qualifier is attached. Has the same + * usage as in getProperty. + * @param qualNS The namespace URI for the qualifier. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param qualName The name of the qualifier. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * propName parameter. + * @param qualValue A pointer to the <code>null</code> terminated UTF-8 string that is the + * value of the qualifier, if the qualifier has a value. Has the same usage as propValue + * in getProperty. + * @param options Option flags describing the qualifier. See the earlier description. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void setQualifier( + String schemaNS, + String propName, + String qualNS, + String qualName, + String qualValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setQualifier(String, String, String, String, String, PropertyOptions) + * + * @param schemaNS The namespace URI for the struct + * @param propName The name of the property to which the qualifier is attached + * @param qualNS The namespace URI for the qualifier + * @param qualName The name of the qualifier + * @param qualValue the value of the qualifier + * @throws XMPException Wraps all errors and exceptions + */ + void setQualifier( + String schemaNS, + String propName, + String qualNS, + String qualName, + String qualValue) throws XMPException; + + + + // --------------------------------------------------------------------------------------------- + // Functions for deleting and detecting properties. These should be obvious from the + // descriptions of the getters and setters. + + /** + * Deletes the given XMP subtree rooted at the given property. It is not an error if the + * property does not exist. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. Has the same usage as in getProperty. + */ + void deleteProperty(String schemaNS, String propName); + + + /** + * Deletes the given XMP subtree rooted at the given array item. It is not an error if the array + * item does not exist. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in getProperty. + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param itemIndex The index of the desired item. Arrays in XMP are indexed from 1. The + * constant <code>XMPConst.ARRAY_LAST_ITEM</code> always refers to the last + * existing array item. + */ + void deleteArrayItem(String schemaNS, String arrayName, int itemIndex); + + + /** + * Deletes the given XMP subtree rooted at the given struct field. It is not an error if the + * field does not exist. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in + * <code>getProperty()</code>. + * @param structName The name of the struct. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in getProperty. + * @param fieldNS The namespace URI for the field. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param fieldName The name of the field. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * structName parameter. + */ + void deleteStructField(String schemaNS, String structName, String fieldNS, String fieldName); + + + /** + * Deletes the given XMP subtree rooted at the given qualifier. It is not an error if the + * qualifier does not exist. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property to which the qualifier is attached. Has the same + * usage as in getProperty. + * @param qualNS The namespace URI for the qualifier. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param qualName The name of the qualifier. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * propName parameter. + */ + void deleteQualifier(String schemaNS, String propName, String qualNS, String qualName); + + + /** + * Returns whether the property exists. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns true if the property exists. + */ + boolean doesPropertyExist(String schemaNS, String propName); + + + /** + * Tells if the array item exists. + * + * @param schemaNS The namespace URI for the array. Has the same usage as in + * <code>getProperty()</code>. + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param itemIndex The index of the desired item. Arrays in XMP are indexed from 1. The + * constant <code>XMPConst.ARRAY_LAST_ITEM</code> always refers to the last + * existing array item. + * @return Returns <code>true</code> if the array exists, <code>false</code> otherwise. + */ + boolean doesArrayItemExist(String schemaNS, String arrayName, int itemIndex); + + + /** + * DoesStructFieldExist tells if the struct field exists. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in + * <code>getProperty()</code>. + * @param structName The name of the struct. May be a general path expression, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param fieldNS The namespace URI for the field. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param fieldName The name of the field. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * structName parameter. + * @return Returns true if the field exists. + */ + boolean doesStructFieldExist( + String schemaNS, + String structName, + String fieldNS, + String fieldName); + + + /** + * DoesQualifierExist tells if the qualifier exists. + * + * @param schemaNS The namespace URI for the struct. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property to which the qualifier is attached. Has the same + * usage as in <code>getProperty()</code>. + * @param qualNS The namespace URI for the qualifier. Has the same URI and prefix usage as the + * schemaNS parameter. + * @param qualName The name of the qualifier. Must be a single XML name, must not be + * <code>null</code> or the empty string. Has the same namespace prefix usage as the + * propName parameter. + * @return Returns true if the qualifier exists. + */ + boolean doesQualifierExist(String schemaNS, String propName, String qualNS, String qualName); + + + // --------------------------------------------------------------------------------------------- + // Specialized Get and Set functions + + /** + * These functions provide convenient support for localized text properties, including a number + * of special and obscure aspects. Localized text properties are stored in alt-text arrays. They + * allow multiple concurrent localizations of a property value, for example a document title or + * copyright in several languages. The most important aspect of these functions is that they + * select an appropriate array item based on one or two RFC 3066 language tags. One of these + * languages, the "specific" language, is preferred and selected if there is an exact match. For + * many languages it is also possible to define a "generic" language that may be used if there + * is no specific language match. The generic language must be a valid RFC 3066 primary subtag, + * or the empty string. For example, a specific language of "en-US" should be used in the US, + * and a specific language of "en-UK" should be used in England. It is also appropriate to use + * "en" as the generic language in each case. If a US document goes to England, the "en-US" + * title is selected by using the "en" generic language and the "en-UK" specific language. It is + * considered poor practice, but allowed, to pass a specific language that is just an RFC 3066 + * primary tag. For example "en" is not a good specific language, it should only be used as a + * generic language. Passing "i" or "x" as the generic language is also considered poor practice + * but allowed. Advice from the W3C about the use of RFC 3066 language tags can be found at: + * http://www.w3.org/International/articles/language-tags/ + * <p> + * <em>Note:</em> RFC 3066 language tags must be treated in a case insensitive manner. The XMP + * Toolkit does this by normalizing their capitalization: + * <ul> + * <li> The primary subtag is lower case, the suggested practice of ISO 639. + * <li> All 2 letter secondary subtags are upper case, the suggested practice of ISO 3166. + * <li> All other subtags are lower case. The XMP specification defines an artificial language, + * <li>"x-default", that is used to explicitly denote a default item in an alt-text array. + * </ul> + * The XMP toolkit normalizes alt-text arrays such that the x-default item is the first item. + * The SetLocalizedText function has several special features related to the x-default item, see + * its description for details. The selection of the array item is the same for GetLocalizedText + * and SetLocalizedText: + * <ul> + * <li> Look for an exact match with the specific language. + * <li> If a generic language is given, look for a partial match. + * <li> Look for an x-default item. + * <li> Choose the first item. + * </ul> + * A partial match with the generic language is where the start of the item's language matches + * the generic string and the next character is '-'. An exact match is also recognized as a + * degenerate case. It is fine to pass x-default as the specific language. In this case, + * selection of an x-default item is an exact match by the first rule, not a selection by the + * 3rd rule. The last 2 rules are fallbacks used when the specific and generic languages fail to + * produce a match. <code>getLocalizedText</code> returns information about a selected item in + * an alt-text array. The array item is selected according to the rules given above. + * + * <em>Note:</em> In a future version of this API a method + * using Java <code>java.lang.Locale</code> will be added. + * + * @param schemaNS The namespace URI for the alt-text array. Has the same usage as in + * <code>getProperty()</code>. + * @param altTextName The name of the alt-text array. May be a general path expression, must not + * be <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param genericLang The name of the generic language as an RFC 3066 primary subtag. May be + * <code>null</code> or the empty string if no generic language is wanted. + * @param specificLang The name of the specific language as an RFC 3066 tag. Must not be + * <code>null</code> or the empty string. + * @return Returns an <code>XMPProperty</code> containing the value, the actual language and + * the options if an appropriate alternate collection item exists, <code>null</code> + * if the property. + * does not exist. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPProperty getLocalizedText( + String schemaNS, + String altTextName, + String genericLang, + String specificLang) throws XMPException; + + + /** + * Modifies the value of a selected item in an alt-text array. Creates an appropriate array item + * if necessary, and handles special cases for the x-default item. If the selected item is from + * a match with the specific language, the value of that item is modified. If the existing value + * of that item matches the existing value of the x-default item, the x-default item is also + * modified. If the array only has 1 existing item (which is not x-default), an x-default item + * is added with the given value. If the selected item is from a match with the generic language + * and there are no other generic matches, the value of that item is modified. If the existing + * value of that item matches the existing value of the x-default item, the x-default item is + * also modified. If the array only has 1 existing item (which is not x-default), an x-default + * item is added with the given value. If the selected item is from a partial match with the + * generic language and there are other partial matches, a new item is created for the specific + * language. The x-default item is not modified. If the selected item is from the last 2 rules + * then a new item is created for the specific language. If the array only had an x-default + * item, the x-default item is also modified. If the array was empty, items are created for the + * specific language and x-default. + * + * <em>Note:</em> In a future version of this API a method + * using Java <code>java.lang.Locale</code> will be added. + * + * + * @param schemaNS The namespace URI for the alt-text array. Has the same usage as in + * <code>getProperty()</code>. + * @param altTextName The name of the alt-text array. May be a general path expression, must not + * be <code>null</code> or the empty string. Has the same namespace prefix usage as + * propName in <code>getProperty()</code>. + * @param genericLang The name of the generic language as an RFC 3066 primary subtag. May be + * <code>null</code> or the empty string if no generic language is wanted. + * @param specificLang The name of the specific language as an RFC 3066 tag. Must not be + * <code>null</code> or the empty string. + * @param itemValue A pointer to the <code>null</code> terminated UTF-8 string that is the new + * value for the appropriate array item. + * @param options Option flags, none are defined at present. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void setLocalizedText( + String schemaNS, + String altTextName, + String genericLang, + String specificLang, + String itemValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setLocalizedText(String, String, String, String, String, PropertyOptions) + * + * @param schemaNS The namespace URI for the alt-text array + * @param altTextName The name of the alt-text array + * @param genericLang The name of the generic language + * @param specificLang The name of the specific language + * @param itemValue the new value for the appropriate array item + * @throws XMPException Wraps all errors and exceptions + */ + void setLocalizedText( + String schemaNS, + String altTextName, + String genericLang, + String specificLang, + String itemValue) throws XMPException; + + + + // --------------------------------------------------------------------------------------------- + // Functions accessing properties as binary values. + + + /** + * These are very similar to <code>getProperty()</code> and <code>SetProperty()</code> above, + * but the value is returned or provided in a literal form instead of as a UTF-8 string. + * The path composition functions in <code>XMPPathFactory</code> may be used to compose an path + * expression for fields in nested structures, items in arrays, or qualifiers. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a <code>Boolean</code> value or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + Boolean getPropertyBoolean(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns an <code>Integer</code> value or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + Integer getPropertyInteger(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a <code>Long</code> value or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + Long getPropertyLong(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a <code>Double</code> value or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + Double getPropertyDouble(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a <code>XMPDateTime</code>-object or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + XMPDateTime getPropertyDate(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a Java <code>Calendar</code>-object or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + Calendar getPropertyCalendar(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a <code>byte[]</code>-array contained the decoded base64 value + * or <code>null</code> if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + byte[] getPropertyBase64(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to retrieve the literal value of a property. + * <em>Note:</em> There is no <code>setPropertyString()</code>, + * because <code>setProperty()</code> sets a string value. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>getProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @return Returns a <code>String</code> value or <code>null</code> + * if the property does not exist. + * @throws XMPException Wraps all exceptions that may occur, + * especially conversion errors. + */ + String getPropertyString(String schemaNS, String propName) throws XMPException; + + + /** + * Convenience method to set a property to a literal <code>boolean</code> value. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the literal property value as <code>boolean</code>. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyBoolean( + String schemaNS, + String propName, + boolean propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyBoolean(String, String, boolean, PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the literal property value as <code>boolean</code> + * @throws XMPException Wraps all exceptions + */ + void setPropertyBoolean( + String schemaNS, + String propName, + boolean propValue) throws XMPException; + + + /** + * Convenience method to set a property to a literal <code>int</code> value. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the literal property value as <code>int</code>. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyInteger( + String schemaNS, + String propName, + int propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyInteger(String, String, int, PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the literal property value as <code>int</code> + * @throws XMPException Wraps all exceptions + */ + void setPropertyInteger( + String schemaNS, + String propName, + int propValue) throws XMPException; + + + /** + * Convenience method to set a property to a literal <code>long</code> value. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the literal property value as <code>long</code>. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyLong( + String schemaNS, + String propName, + long propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyLong(String, String, long, PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the literal property value as <code>long</code> + * @throws XMPException Wraps all exceptions + */ + void setPropertyLong( + String schemaNS, + String propName, + long propValue) throws XMPException; + + + /** + * Convenience method to set a property to a literal <code>double</code> value. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the literal property value as <code>double</code>. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyDouble( + String schemaNS, + String propName, + double propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyDouble(String, String, double, PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the literal property value as <code>double</code> + * @throws XMPException Wraps all exceptions + */ + void setPropertyDouble( + String schemaNS, + String propName, + double propValue) throws XMPException; + + + /** + * Convenience method to set a property with an XMPDateTime-object, + * which is serialized to an ISO8601 date. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the property value as <code>XMPDateTime</code>. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyDate( + String schemaNS, + String propName, + XMPDateTime propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyDate(String, String, XMPDateTime, PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the property value as <code>XMPDateTime</code> + * @throws XMPException Wraps all exceptions + */ + void setPropertyDate( + String schemaNS, + String propName, + XMPDateTime propValue) throws XMPException; + + + /** + * Convenience method to set a property with a Java Calendar-object, + * which is serialized to an ISO8601 date. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the property value as Java <code>Calendar</code>. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyCalendar( + String schemaNS, + String propName, + Calendar propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyCalendar(String, String, Calendar, PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the property value as <code>Calendar</code> + * @throws XMPException Wraps all exceptions + */ + void setPropertyCalendar( + String schemaNS, + String propName, + Calendar propValue) throws XMPException; + + + /** + * Convenience method to set a property from a binary <code>byte[]</code>-array, + * which is serialized as base64-string. + * + * @param schemaNS The namespace URI for the property. Has the same usage as in + * <code>setProperty()</code>. + * @param propName The name of the property. + * Has the same usage as in <code>getProperty()</code>. + * @param propValue the literal property value as byte array. + * @param options options of the property to set (optional). + * @throws XMPException Wraps all exceptions that may occur. + */ + void setPropertyBase64( + String schemaNS, + String propName, + byte[] propValue, + PropertyOptions options) throws XMPException; + + + /** + * @see XMPMeta#setPropertyBase64(String, String, byte[], PropertyOptions) + * + * @param schemaNS The namespace URI for the property + * @param propName The name of the property + * @param propValue the literal property value as byte array + * @throws XMPException Wraps all exceptions + */ + void setPropertyBase64( + String schemaNS, + String propName, + byte[] propValue) throws XMPException; + + + /** + * Constructs an iterator for the properties within this XMP object. + * + * @return Returns an <code>XMPIterator</code>. + * @see XMPMeta#iterator(String, String, IteratorOptions) + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPIterator iterator() throws XMPException; + + + /** + * Constructs an iterator for the properties within this XMP object using some options. + * + * @param options Option flags to control the iteration. + * @return Returns an <code>XMPIterator</code>. + * @see XMPMeta#iterator(String, String, IteratorOptions) + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPIterator iterator(IteratorOptions options) throws XMPException; + + + /** + * Construct an iterator for the properties within an XMP object. The general operation of an + * XMP object iterator was. According to the parameters it iterates the entire data tree, + * properties within a specific schema, or a subtree rooted at a specific node. + * + * @param schemaNS Optional schema namespace URI to restrict the iteration. Omitted (visit all + * schema) by passing <code>null</code> or empty String. + * @param propName Optional property name to restrict the iteration. May be an arbitrary path + * expression. Omitted (visit all properties) by passing <code>null</code> or empty + * String. If no schema URI is given, it is ignored. + * @param options Option flags to control the iteration. See {@link IteratorOptions} for + * details. + * @return Returns an <code>XMPIterator</code> for this <code>XMPMeta</code>-object + * considering the given options. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + XMPIterator iterator( + String schemaNS, + String propName, + IteratorOptions options) throws XMPException; + + + /** + * This correlates to the about-attribute, + * returns the empty String if no name is set. + * + * @return Returns the name of the XMP object. + */ + String getObjectName(); + + + /** + * @param name Sets the name of the XMP object. + */ + void setObjectName(String name); + + + /** + * @return Returns the unparsed content of the <?xpacket> processing instruction. + * This contains normally the attribute-like elements 'begin="<BOM>" + * id="W5M0MpCehiHzreSzNTczkc9d"' and possibly the deprecated elements 'bytes="1234"' or + * 'encoding="XXX"'. If the parsed packet has not been wrapped into an xpacket, + * <code>null</code> is returned. + */ + String getPacketHeader(); + + + /** + * Clones the complete metadata tree. + * + * @return Returns a deep copy of this instance. + */ + Object clone(); + + + /** + * Sorts the complete datamodel according to the following rules: + * <ul> + * <li>Schema nodes are sorted by prefix. + * <li>Properties at top level and within structs are sorted by full name, that is + * prefix + local name. + * <li>Array items are not sorted, even if they have no certain order such as bags. + * <li>Qualifier are sorted, with the exception of "xml:lang" and/or "rdf:type" + * that stay at the top of the list in that order. + * </ul> + */ + void sort(); + + + /** + * Perform the normalization as a separate parsing step. + * Normally it is done during parsing, unless the parsing option + * {@link ParseOptions#OMIT_NORMALIZATION} is set to <code>true</code>. + * <em>Note:</em> It does no harm to call this method to an already normalized xmp object. + * It was a PDF/A requirement to get hand on the unnormalized <code>XMPMeta</code> object. + * + * @param options optional parsing options. + * @throws XMPException Wraps all errors and exceptions that may occur. + */ + void normalize(ParseOptions options) throws XMPException; + + + /** + * Renders this node and the tree unter this node in a human readable form. + * @return Returns a multiline string containing the dump. + */ + String dumpObject(); +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java b/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java new file mode 100644 index 0000000..afd437a --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java @@ -0,0 +1,320 @@ +//================================================================================================= +//ADOBE SYSTEMS INCORPORATED +//Copyright 2006-2007 Adobe Systems Incorporated +//All Rights Reserved +// +//NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +//of the Adobe license agreement accompanying it. +//================================================================================================= + +package com.adobe.xmp; + +import java.io.InputStream; +import java.io.OutputStream; + +import com.adobe.xmp.impl.XMPMetaImpl; +import com.adobe.xmp.impl.XMPMetaParser; +import com.adobe.xmp.impl.XMPSchemaRegistryImpl; +import com.adobe.xmp.impl.XMPSerializerHelper; +import com.adobe.xmp.options.ParseOptions; +import com.adobe.xmp.options.SerializeOptions; + + +/** + * Creates <code>XMPMeta</code>-instances from an <code>InputStream</code> + * + * @since 30.01.2006 + */ +public final class XMPMetaFactory +{ + /** The singleton instance of the <code>XMPSchemaRegistry</code>. */ + private static XMPSchemaRegistry schema = new XMPSchemaRegistryImpl(); + /** cache for version info */ + private static XMPVersionInfo versionInfo = null; + + /** + * Hides public constructor + */ + private XMPMetaFactory() + { + // EMPTY + } + + + /** + * @return Returns the singleton instance of the <code>XMPSchemaRegistry</code>. + */ + public static XMPSchemaRegistry getSchemaRegistry() + { + return schema; + } + + + /** + * @return Returns an empty <code>XMPMeta</code>-object. + */ + public static XMPMeta create() + { + return new XMPMetaImpl(); + } + + + /** + * Parsing with default options. + * @see XMPMetaFactory#parse(InputStream, ParseOptions) + * + * @param in an <code>InputStream</code> + * @return Returns the <code>XMPMeta</code>-object created from the input. + * @throws XMPException If the file is not well-formed XML or if the parsing fails. + */ + public static XMPMeta parse(InputStream in) throws XMPException + { + return parse(in, null); + } + + + /** + * These functions support parsing serialized RDF into an XMP object, and serailizing an XMP + * object into RDF. The input for parsing may be any valid Unicode + * encoding. ISO Latin-1 is also recognized, but its use is strongly discouraged. Serialization + * is always as UTF-8. + * <p> + * <code>parseFromBuffer()</code> parses RDF from an <code>InputStream</code>. The encoding + * is recognized automatically. + * + * @param in an <code>InputStream</code> + * @param options Options controlling the parsing.<br> + * The available options are: + * <ul> + * <li> XMP_REQUIRE_XMPMETA - The <x:xmpmeta> XML element is required around + * <tt><rdf:RDF></tt>. + * <li> XMP_STRICT_ALIASING - Do not reconcile alias differences, throw an exception. + * </ul> + * <em>Note:</em>The XMP_STRICT_ALIASING option is not yet implemented. + * @return Returns the <code>XMPMeta</code>-object created from the input. + * @throws XMPException If the file is not well-formed XML or if the parsing fails. + */ + public static XMPMeta parse(InputStream in, ParseOptions options) + throws XMPException + { + return XMPMetaParser.parse(in, options); + } + + + /** + * Parsing with default options. + * @see XMPMetaFactory#parse(InputStream) + * + * @param packet a String contain an XMP-file. + * @return Returns the <code>XMPMeta</code>-object created from the input. + * @throws XMPException If the file is not well-formed XML or if the parsing fails. + */ + public static XMPMeta parseFromString(String packet) throws XMPException + { + return parseFromString(packet, null); + } + + + /** + * Creates an <code>XMPMeta</code>-object from a string. + * @see XMPMetaFactory#parseFromString(String, ParseOptions) + * + * @param packet a String contain an XMP-file. + * @param options Options controlling the parsing. + * @return Returns the <code>XMPMeta</code>-object created from the input. + * @throws XMPException If the file is not well-formed XML or if the parsing fails. + */ + public static XMPMeta parseFromString(String packet, ParseOptions options) + throws XMPException + { + return XMPMetaParser.parse(packet, options); + } + + + /** + * Parsing with default options. + * @see XMPMetaFactory#parseFromBuffer(byte[], ParseOptions) + * + * @param buffer a String contain an XMP-file. + * @return Returns the <code>XMPMeta</code>-object created from the input. + * @throws XMPException If the file is not well-formed XML or if the parsing fails. + */ + public static XMPMeta parseFromBuffer(byte[] buffer) throws XMPException + { + return parseFromBuffer(buffer, null); + } + + + /** + * Creates an <code>XMPMeta</code>-object from a byte-buffer. + * @see XMPMetaFactory#parse(InputStream, ParseOptions) + * + * @param buffer a String contain an XMP-file. + * @param options Options controlling the parsing. + * @return Returns the <code>XMPMeta</code>-object created from the input. + * @throws XMPException If the file is not well-formed XML or if the parsing fails. + */ + public static XMPMeta parseFromBuffer(byte[] buffer, + ParseOptions options) throws XMPException + { + return XMPMetaParser.parse(buffer, options); + } + + + /** + * Serializes an <code>XMPMeta</code>-object as RDF into an <code>OutputStream</code> + * with default options. + * + * @param xmp a metadata object + * @param out an <code>OutputStream</code> to write the serialized RDF to. + * @throws XMPException on serializsation errors. + */ + public static void serialize(XMPMeta xmp, OutputStream out) throws XMPException + { + serialize(xmp, out, null); + } + + + /** + * Serializes an <code>XMPMeta</code>-object as RDF into an <code>OutputStream</code>. + * + * @param xmp a metadata object + * @param options Options to control the serialization (see {@link SerializeOptions}). + * @param out an <code>OutputStream</code> to write the serialized RDF to. + * @throws XMPException on serializsation errors. + */ + public static void serialize(XMPMeta xmp, OutputStream out, SerializeOptions options) + throws XMPException + { + assertImplementation(xmp); + XMPSerializerHelper.serialize((XMPMetaImpl) xmp, out, options); + } + + + /** + * Serializes an <code>XMPMeta</code>-object as RDF into a byte buffer. + * + * @param xmp a metadata object + * @param options Options to control the serialization (see {@link SerializeOptions}). + * @return Returns a byte buffer containing the serialized RDF. + * @throws XMPException on serializsation errors. + */ + public static byte[] serializeToBuffer(XMPMeta xmp, SerializeOptions options) + throws XMPException + { + assertImplementation(xmp); + return XMPSerializerHelper.serializeToBuffer((XMPMetaImpl) xmp, options); + } + + + /** + * Serializes an <code>XMPMeta</code>-object as RDF into a string. <em>Note:</em> Encoding + * is ignored when serializing to a string. + * + * @param xmp a metadata object + * @param options Options to control the serialization (see {@link SerializeOptions}). + * @return Returns a string containing the serialized RDF. + * @throws XMPException on serializsation errors. + */ + public static String serializeToString(XMPMeta xmp, SerializeOptions options) + throws XMPException + { + assertImplementation(xmp); + return XMPSerializerHelper.serializeToString((XMPMetaImpl) xmp, options); + } + + + /** + * @param xmp Asserts that xmp is compatible to <code>XMPMetaImpl</code>.s + */ + private static void assertImplementation(XMPMeta xmp) + { + if (!(xmp instanceof XMPMetaImpl)) + { + throw new UnsupportedOperationException("The serializing service works only" + + "with the XMPMeta implementation of this library"); + } + } + + + /** + * Resets the schema registry to its original state (creates a new one). + * Be careful this might break all existing XMPMeta-objects and should be used + * only for testing purpurses. + */ + public static void reset() + { + schema = new XMPSchemaRegistryImpl(); + } + + + /** + * Obtain version information. The XMPVersionInfo singleton is created the first time + * its requested. + * + * @return Returns the version information. + */ + public static synchronized XMPVersionInfo getVersionInfo() + { + if (versionInfo == null) + { + try + { + final int major = 5; + final int minor = 1; + final int micro = 0; + final int engBuild = 3; + final boolean debug = false; + + // Adobe XMP Core 5.0-jc001 DEBUG-<branch>.<changelist>, 2009 Jan 28 15:22:38-CET + final String message = "Adobe XMP Core 5.1.0-jc003"; + + + versionInfo = new XMPVersionInfo() + { + public int getMajor() + { + return major; + } + + public int getMinor() + { + return minor; + } + + public int getMicro() + { + return micro; + } + + public boolean isDebug() + { + return debug; + } + + public int getBuild() + { + return engBuild; + } + + public String getMessage() + { + return message; + } + + public String toString() + { + return message; + } + }; + + } + catch (Throwable e) + { + // EMTPY, severe error would be detected during the tests + System.out.println(e); + } + } + return versionInfo; + } +} diff --git a/XMPCore/src/com/adobe/xmp/XMPPathFactory.java b/XMPCore/src/com/adobe/xmp/XMPPathFactory.java new file mode 100644 index 0000000..9d8f632 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPPathFactory.java @@ -0,0 +1,291 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import com.adobe.xmp.impl.Utils; +import com.adobe.xmp.impl.xpath.XMPPath; +import com.adobe.xmp.impl.xpath.XMPPathParser; + +/** + * Utility services for the metadata object. It has only public static functions, you cannot create + * an object. These are all functions that layer cleanly on top of the core XMP toolkit. + * <p> + * These functions provide support for composing path expressions to deeply nested properties. The + * functions <code>XMPMeta</code> such as <code>getProperty()</code>, + * <code>getArrayItem()</code> and <code>getStructField()</code> provide easy access to top + * level simple properties, items in top level arrays, and fields of top level structs. They do not + * provide convenient access to more complex things like fields several levels deep in a complex + * struct, or fields within an array of structs, or items of an array that is a field of a struct. + * These functions can also be used to compose paths to top level array items or struct fields so + * that you can use the binary accessors like <code>getPropertyAsInteger()</code>. + * <p> + * You can use these functions is to compose a complete path expression, or all but the last + * component. Suppose you have a property that is an array of integers within a struct. You can + * access one of the array items like this: + * <p> + * <blockquote> + * + * <pre> + * String path = XMPPathFactory.composeStructFieldPath (schemaNS, "Struct", fieldNS, + * "Array"); + * String path += XMPPathFactory.composeArrayItemPath (schemaNS, "Array" index); + * PropertyInteger result = xmpObj.getPropertyAsInteger(schemaNS, path); + * </pre> + * + * </blockquote> You could also use this code if you want the string form of the integer: + * <blockquote> + * + * <pre> + * String path = XMPPathFactory.composeStructFieldPath (schemaNS, "Struct", fieldNS, + * "Array"); + * PropertyText xmpObj.getArrayItem (schemaNS, path, index); + * </pre> + * + * </blockquote> + * <p> + * <em>Note:</em> It might look confusing that the schemaNS is passed in all of the calls above. + * This is because the XMP toolkit keeps the top level "schema" namespace separate from + * the rest of the path expression. + * <em>Note:</em> These methods are much simpler than in the C++-API, they don't check the given + * path or array indices. + * + * @since 25.01.2006 + */ +public final class XMPPathFactory +{ + /** Private constructor */ + private XMPPathFactory() + { + // EMPTY + } + + + /** + * Compose the path expression for an item in an array. + * + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. + * @param itemIndex The index of the desired item. Arrays in XMP are indexed from 1. + * 0 and below means last array item and renders as <code>[last()]</code>. + * + * @return Returns the composed path basing on fullPath. This will be of the form + * <tt>ns:arrayName[i]</tt>, where "ns" is the prefix for schemaNS and + * "i" is the decimal representation of itemIndex. + * @throws XMPException Throws exeption if index zero is used. + */ + public static String composeArrayItemPath(String arrayName, int itemIndex) throws XMPException + { + if (itemIndex > 0) + { + return arrayName + '[' + itemIndex + ']'; + } + else if (itemIndex == XMPConst.ARRAY_LAST_ITEM) + { + return arrayName + "[last()]"; + } + else + { + throw new XMPException("Array index must be larger than zero", XMPError.BADINDEX); + } + } + + + /** + * Compose the path expression for a field in a struct. The result can be added to the + * path of + * + * + * @param fieldNS The namespace URI for the field. Must not be <code>null</code> or the empty + * string. + * @param fieldName The name of the field. Must be a simple XML name, must not be + * <code>null</code> or the empty string. + * @return Returns the composed path. This will be of the form + * <tt>ns:structName/fNS:fieldName</tt>, where "ns" is the prefix for + * schemaNS and "fNS" is the prefix for fieldNS. + * @throws XMPException Thrown if the path to create is not valid. + */ + public static String composeStructFieldPath(String fieldNS, + String fieldName) throws XMPException + { + assertFieldNS(fieldNS); + assertFieldName(fieldName); + + XMPPath fieldPath = XMPPathParser.expandXPath(fieldNS, fieldName); + if (fieldPath.size() != 2) + { + throw new XMPException("The field name must be simple", XMPError.BADXPATH); + } + + return '/' + fieldPath.getSegment(XMPPath.STEP_ROOT_PROP).getName(); + } + + + /** + * Compose the path expression for a qualifier. + * + * @param qualNS The namespace URI for the qualifier. May be <code>null</code> or the empty + * string if the qualifier is in the XML empty namespace. + * @param qualName The name of the qualifier. Must be a simple XML name, must not be + * <code>null</code> or the empty string. + * @return Returns the composed path. This will be of the form + * <tt>ns:propName/?qNS:qualName</tt>, where "ns" is the prefix for + * schemaNS and "qNS" is the prefix for qualNS. + * @throws XMPException Thrown if the path to create is not valid. + */ + public static String composeQualifierPath( + String qualNS, + String qualName) throws XMPException + { + assertQualNS(qualNS); + assertQualName(qualName); + + XMPPath qualPath = XMPPathParser.expandXPath(qualNS, qualName); + if (qualPath.size() != 2) + { + throw new XMPException("The qualifier name must be simple", XMPError.BADXPATH); + } + + return "/?" + qualPath.getSegment(XMPPath.STEP_ROOT_PROP).getName(); + } + + + /** + * Compose the path expression to select an alternate item by language. The + * path syntax allows two forms of "content addressing" that may + * be used to select an item in an array of alternatives. The form used in + * ComposeLangSelector lets you select an item in an alt-text array based on + * the value of its <tt>xml:lang</tt> qualifier. The other form of content + * addressing is shown in ComposeFieldSelector. \note ComposeLangSelector + * does not supplant SetLocalizedText or GetLocalizedText. They should + * generally be used, as they provide extra logic to choose the appropriate + * language and maintain consistency with the 'x-default' value. + * ComposeLangSelector gives you an path expression that is explicitly and + * only for the language given in the langName parameter. + * + * @param arrayName + * The name of the array. May be a general path expression, must + * not be <code>null</code> or the empty string. + * @param langName + * The RFC 3066 code for the desired language. + * @return Returns the composed path. This will be of the form + * <tt>ns:arrayName[@xml:lang='langName']</tt>, where + * "ns" is the prefix for schemaNS. + */ + public static String composeLangSelector(String arrayName, + String langName) + { + return arrayName + "[?xml:lang=\"" + Utils.normalizeLangValue(langName) + "\"]"; + } + + + /** + * Compose the path expression to select an alternate item by a field's value. The path syntax + * allows two forms of "content addressing" that may be used to select an item in an + * array of alternatives. The form used in ComposeFieldSelector lets you select an item in an + * array of structs based on the value of one of the fields in the structs. The other form of + * content addressing is shown in ComposeLangSelector. For example, consider a simple struct + * that has two fields, the name of a city and the URI of an FTP site in that city. Use this to + * create an array of download alternatives. You can show the user a popup built from the values + * of the city fields. You can then get the corresponding URI as follows: + * <p> + * <blockquote> + * + * <pre> + * String path = composeFieldSelector ( schemaNS, "Downloads", fieldNS, + * "City", chosenCity ); + * XMPProperty prop = xmpObj.getStructField ( schemaNS, path, fieldNS, "URI" ); + * </pre> + * + * </blockquote> + * + * @param arrayName The name of the array. May be a general path expression, must not be + * <code>null</code> or the empty string. + * @param fieldNS The namespace URI for the field used as the selector. Must not be + * <code>null</code> or the empty string. + * @param fieldName The name of the field used as the selector. Must be a simple XML name, must + * not be <code>null</code> or the empty string. It must be the name of a field that is + * itself simple. + * @param fieldValue The desired value of the field. + * @return Returns the composed path. This will be of the form + * <tt>ns:arrayName[fNS:fieldName='fieldValue']</tt>, where "ns" is the + * prefix for schemaNS and "fNS" is the prefix for fieldNS. + * @throws XMPException Thrown if the path to create is not valid. + */ + public static String composeFieldSelector(String arrayName, String fieldNS, + String fieldName, String fieldValue) throws XMPException + { + XMPPath fieldPath = XMPPathParser.expandXPath(fieldNS, fieldName); + if (fieldPath.size() != 2) + { + throw new XMPException("The fieldName name must be simple", XMPError.BADXPATH); + } + + return arrayName + '[' + fieldPath.getSegment(XMPPath.STEP_ROOT_PROP).getName() + + "=\"" + fieldValue + "\"]"; + } + + + /** + * ParameterAsserts that a qualifier namespace is set. + * @param qualNS a qualifier namespace + * @throws XMPException Qualifier schema is null or empty + */ + private static void assertQualNS(String qualNS) throws XMPException + { + if (qualNS == null || qualNS.length() == 0) + { + throw new XMPException("Empty qualifier namespace URI", XMPError.BADSCHEMA); + } + + } + + + /** + * ParameterAsserts that a qualifier name is set. + * @param qualName a qualifier name or path + * @throws XMPException Qualifier name is null or empty + */ + private static void assertQualName(String qualName) throws XMPException + { + if (qualName == null || qualName.length() == 0) + { + throw new XMPException("Empty qualifier name", XMPError.BADXPATH); + } + } + + + /** + * ParameterAsserts that a struct field namespace is set. + * @param fieldNS a struct field namespace + * @throws XMPException Struct field schema is null or empty + */ + private static void assertFieldNS(String fieldNS) throws XMPException + { + if (fieldNS == null || fieldNS.length() == 0) + { + throw new XMPException("Empty field namespace URI", XMPError.BADSCHEMA); + } + + } + + + /** + * ParameterAsserts that a struct field name is set. + * @param fieldName a struct field name or path + * @throws XMPException Struct field name is null or empty + */ + private static void assertFieldName(String fieldName) throws XMPException + { + if (fieldName == null || fieldName.length() == 0) + { + throw new XMPException("Empty f name", XMPError.BADXPATH); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java b/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java new file mode 100644 index 0000000..4608a8e --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java @@ -0,0 +1,180 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import java.util.Map; + +import com.adobe.xmp.properties.XMPAliasInfo; + +/** + * The schema registry keeps track of all namespaces and aliases used in the XMP + * metadata. At initialisation time, the default namespaces and default aliases + * are automatically registered. <b>Namespaces</b> must be registered before + * used in namespace URI parameters or path expressions. Within the XMP Toolkit + * the registered namespace URIs and prefixes must be unique. Additional + * namespaces encountered when parsing RDF are automatically registered. The + * namespace URI should always end in an XML name separator such as '/' or '#'. + * This is because some forms of RDF shorthand catenate a namespace URI with an + * element name to form a new URI. + * <p> + * <b>Aliases</b> in XMP serve the same purpose as Windows file shortcuts, + * Macintosh file aliases, or UNIX file symbolic links. The aliases are simply + * multiple names for the same property. One distinction of XMP aliases is that + * they are ordered, there is an alias name pointing to an actual name. The + * primary significance of the actual name is that it is the preferred name for + * output, generally the most widely recognized name. + * <p> + * The names that can be aliased in XMP are restricted. The alias must be a top + * level property name, not a field within a structure or an element within an + * array. The actual may be a top level property name, the first element within + * a top level array, or the default element in an alt-text array. This does not + * mean the alias can only be a simple property. It is OK to alias a top level + * structure or array to an identical top level structure or array, or to the + * first item of an array of structures. + * + * @since 27.01.2006 + */ +public interface XMPSchemaRegistry +{ + // --------------------------------------------------------------------------------------------- + // Namespace Functions + + /** + * Register a namespace URI with a suggested prefix. It is not an error if + * the URI is already registered, no matter what the prefix is. If the URI + * is not registered but the suggested prefix is in use, a unique prefix is + * created from the suggested one. The actual registeed prefix is always + * returned. The function result tells if the registered prefix is the + * suggested one. + * <p> + * Note: No checking is presently done on either the URI or the prefix. + * + * @param namespaceURI + * The URI for the namespace. Must be a valid XML URI. + * @param suggestedPrefix + * The suggested prefix to be used if the URI is not yet + * registered. Must be a valid XML name. + * @return Returns the registered prefix for this URI, is equal to the + * suggestedPrefix if the namespace hasn't been registered before, + * otherwise the existing prefix. + * @throws XMPException If the parameters are not accordingly set + */ + String registerNamespace(String namespaceURI, String suggestedPrefix) throws XMPException; + + + /** + * Obtain the prefix for a registered namespace URI. + * <p> + * It is not an error if the namespace URI is not registered. The output + * namespacePrefix string is not modified if the namespace URI is not + * registered. + * + * @param namespaceURI + * The URI for the namespace. Must not be null or the empty + * string. + * @return Returns true if the namespace URI is registered. + */ + String getNamespacePrefix(String namespaceURI); + + + /** + * Obtain the URI for a registered namespace prefix. + * <p> + * It is not an error if the namespace prefix is not registered. The output + * namespaceURI string is not modified if the namespace prefix is not + * registered. + * + * @param namespacePrefix + * The prefix for the namespace. Must not be null or the empty + * string. + * @return Returns the URI registered for this prefix. + */ + String getNamespaceURI(String namespacePrefix); + + + /** + * @return Returns the registered prefix/namespace-pairs as map, where the keys are the + * namespaces and the values are the prefixes. + */ + Map getNamespaces(); + + + /** + * @return Returns the registered namespace/prefix-pairs as map, where the keys are the + * prefixes and the values are the namespaces. + */ + Map getPrefixes(); + + + /** + * Deletes a namespace from the registry. + * <p> + * Does nothing if the URI is not registered, or if the namespaceURI + * parameter is null or the empty string. + * <p> + * Note: Not yet implemented. + * + * @param namespaceURI + * The URI for the namespace. + */ + void deleteNamespace(String namespaceURI); + + + + + + // --------------------------------------------------------------------------------------------- + // Alias Functions + + + /** + * Determines if a name is an alias, and what it is aliased to. + * + * @param aliasNS + * The namespace URI of the alias. Must not be <code>null</code> or the empty + * string. + * @param aliasProp + * The name of the alias. May be an arbitrary path expression + * path, must not be <code>null</code> or the empty string. + * @return Returns the <code>XMPAliasInfo</code> for the given alias namespace and property or + * <code>null</code> if there is no such alias. + */ + XMPAliasInfo resolveAlias(String aliasNS, String aliasProp); + + + /** + * Collects all aliases that are contained in the provided namespace. + * If nothing is found, an empty array is returned. + * + * @param aliasNS a schema namespace URI + * @return Returns all alias infos from aliases that are contained in the provided namespace. + */ + XMPAliasInfo[] findAliases(String aliasNS); + + + /** + * Searches for registered aliases. + * + * @param qname + * an XML conform qname + * @return Returns if an alias definition for the given qname to another + * schema and property is registered. + */ + XMPAliasInfo findAlias(String qname); + + + /** + * @return Returns the registered aliases as map, where the key is the "qname" (prefix and name) + * and the value an <code>XMPAliasInfo</code>-object. + */ + Map getAliases(); + + +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPUtils.java b/XMPCore/src/com/adobe/xmp/XMPUtils.java new file mode 100644 index 0000000..043d533 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPUtils.java @@ -0,0 +1,506 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +import com.adobe.xmp.impl.Base64; +import com.adobe.xmp.impl.ISO8601Converter; +import com.adobe.xmp.impl.XMPUtilsImpl; +import com.adobe.xmp.options.PropertyOptions; + + +/** + * Utility methods for XMP. I included only those that are different from the + * Java default conversion utilities. + * + * @since 21.02.2006 + */ +public class XMPUtils +{ + /** Private constructor */ + private XMPUtils() + { + // EMPTY + } + + + /** + * Create a single edit string from an array of strings. + * + * @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 + { + return XMPUtilsImpl + .catenateArrayItems(xmp, schemaNS, arrayName, separator, quotes, allowCommas); + } + + + /** + * Separate a single edit string into an array of strings. + * + * @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 + { + XMPUtilsImpl.separateArrayItems(xmp, schemaNS, arrayName, catedStr, arrayOptions, + preserveCommas); + } + + + /** + * Remove multiple properties from an XMP object. + * + * RemoveProperties was created to support the File Info dialog's Delete + * button, and has been been generalized somewhat from those specific needs. + * It operates in one of three main modes depending on the schemaNS and + * propName parameters: + * + * <ul> + * <li> Non-empty <code>schemaNS</code> and <code>propName</code> - The named property is + * removed if it is an external property, or if the + * flag <code>doAllProperties</code> option is true. It does not matter whether the + * named property is an actual property or an alias. + * + * <li> Non-empty <code>schemaNS</code> and empty <code>propName</code> - The all external + * properties in the named schema are removed. Internal properties are also + * removed if the flag <code>doAllProperties</code> option is set. In addition, + * aliases from the named schema will be removed if the flag <code>includeAliases</code> + * option is set. + * + * <li> Empty <code>schemaNS</code> and empty <code>propName</code> - All external properties in + * all schema are removed. Internal properties are also removed if the + * flag <code>doAllProperties</code> option is passed. Aliases are implicitly handled + * because the associated actuals are internal if the alias is. + * </ul> + * + * It is an error to pass an empty <code>schemaNS</code> and non-empty <code>propName</code>. + * + * @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. + * <em>Note:</em> Currently not supported. + * @throws XMPException Forwards the Exceptions from the metadata processing + */ + public static void removeProperties(XMPMeta xmp, String schemaNS, String propName, + boolean doAllProperties, boolean includeAliases) throws XMPException + { + XMPUtilsImpl.removeProperties(xmp, schemaNS, propName, doAllProperties, includeAliases); + } + + + + /** + * Alias without the new option <code>deleteEmptyValues</code>. + * @param source The source XMP object. + * @param dest The destination XMP object. + * @param doAllProperties Do internal properties in addition to external properties. + * @param replaceOldValues Replace the values of existing properties. + * @throws XMPException Forwards the Exceptions from the metadata processing + */ + public static void appendProperties(XMPMeta source, XMPMeta dest, boolean doAllProperties, + boolean replaceOldValues) throws XMPException + { + appendProperties(source, dest, doAllProperties, replaceOldValues, false); + } + + + /** + * <p>Append properties from one XMP object to another. + * + * <p>XMPUtils#appendProperties was created to support the File Info dialog's Append button, and + * has been been generalized somewhat from those specific needs. It appends information from one + * XMP object (source) to another (dest). The default operation is to append only external + * properties that do not already exist in the destination. The flag + * <code>doAllProperties</code> can be used to operate on all properties, external and internal. + * The flag <code>replaceOldValues</code> option can be used to replace the values + * of existing properties. The notion of external + * versus internal applies only to top level properties. The keep-or-replace-old notion applies + * within structs and arrays as described below. + * <ul> + * <li>If <code>replaceOldValues</code> is true then the processing is restricted to the top + * level properties. The processed properties from the source (according to + * <code>doAllProperties</code>) are propagated to the destination, + * replacing any existing values.Properties in the destination that are not in the source + * are left alone. + * + * <li>If <code>replaceOldValues</code> is not passed then the processing is more complicated. + * Top level properties are added to the destination if they do not already exist. + * If they do exist but differ in form (simple/struct/array) then the destination is left alone. + * If the forms match, simple properties are left unchanged while structs and arrays are merged. + * + * <li>If <code>deleteEmptyValues</code> is passed then an empty value in the source XMP causes + * the corresponding destination XMP property to be deleted. The default is to treat empty + * values the same as non-empty values. An empty value is any of a simple empty string, an array + * with no items, or a struct with no fields. Qualifiers are ignored. + * </ul> + * + * <p>The detailed behavior is defined by the following pseudo-code: + * <blockquote> + * <pre> + * appendProperties ( sourceXMP, destXMP, doAllProperties, + * replaceOldValues, deleteEmptyValues ): + * for all source schema (top level namespaces): + * for all top level properties in sourceSchema: + * if doAllProperties or prop is external: + * appendSubtree ( sourceNode, destSchema, replaceOldValues, deleteEmptyValues ) + * + * appendSubtree ( sourceNode, destParent, replaceOldValues, deleteEmptyValues ): + * if deleteEmptyValues and source value is empty: + * delete the corresponding child from destParent + * else if sourceNode not in destParent (by name): + * copy sourceNode's subtree to destParent + * else if replaceOld: + * delete subtree from destParent + * copy sourceNode's subtree to destParent + * else: + * // Already exists in dest and not replacing, merge structs and arrays + * if sourceNode and destNode forms differ: + * return, leave the destNode alone + * else if form is a struct: + * for each field in sourceNode: + * AppendSubtree ( sourceNode.field, destNode, replaceOldValues ) + * else if form is an alt-text array: + * copy new items by "xml:lang" value into the destination + * else if form is an array: + * copy new items by value into the destination, ignoring order and duplicates + * </pre> + * </blockquote> + * + * <p><em>Note:</em> appendProperties can be expensive if replaceOldValues is not passed and + * the XMP contains large arrays. The array item checking described above is n-squared. + * Each source item is checked to see if it already exists in the destination, + * without regard to order or duplicates. + * <p>Simple items are compared by value and "xml:lang" qualifier, other qualifiers are ignored. + * Structs are recursively compared by field names, without regard to field order. Arrays are + * compared by recursively comparing all items. + * + * @param source The source XMP object. + * @param dest 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 dest, boolean doAllProperties, + boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException + { + XMPUtilsImpl.appendProperties(source, dest, doAllProperties, replaceOldValues, + deleteEmptyValues); + } + + + /** + * Convert from string to Boolean. + * + * @param value + * The string representation of the Boolean. + * @return The appropriate boolean value for the string. The checked values + * for <code>true</code> and <code>false</code> are: + * <ul> + * <li>{@link XMPConst#TRUESTR} and {@link XMPConst#FALSESTR} + * <li>"t" and "f" + * <li>"on" and "off" + * <li>"yes" and "no" + * <li>"value <> 0" and "value == 0" + * </ul> + * @throws XMPException If an empty string is passed. + */ + public static boolean convertToBoolean(String value) throws XMPException + { + if (value == null || value.length() == 0) + { + throw new XMPException("Empty convert-string", XMPError.BADVALUE); + } + value = value.toLowerCase(); + + try + { + // First try interpretation as Integer (anything not 0 is true) + return Integer.parseInt(value) != 0; + } + catch (NumberFormatException e) + { + return + "true".equals(value) || + "t".equals(value) || + "on".equals(value) || + "yes".equals(value); + } + } + + + /** + * Convert from boolean to string. + * + * @param value + * a boolean value + * @return The XMP string representation of the boolean. The values used are + * given by the constnts {@link XMPConst#TRUESTR} and + * {@link XMPConst#FALSESTR}. + */ + public static String convertFromBoolean(boolean value) + { + return value ? XMPConst.TRUESTR : XMPConst.FALSESTR; + } + + + /** + * Converts a string value to an <code>int</code>. + * + * @param rawValue + * the string value + * @return Returns an int. + * @throws XMPException + * If the <code>rawValue</code> is <code>null</code> or empty or the + * conversion fails. + */ + public static int convertToInteger(String rawValue) throws XMPException + { + try + { + if (rawValue == null || rawValue.length() == 0) + { + throw new XMPException("Empty convert-string", XMPError.BADVALUE); + } + if (rawValue.startsWith("0x")) + { + return Integer.parseInt(rawValue.substring(2), 16); + } + else + { + return Integer.parseInt(rawValue); + } + } + catch (NumberFormatException e) + { + throw new XMPException("Invalid integer string", XMPError.BADVALUE); + } + } + + + /** + * Convert from int to string. + * + * @param value + * an int value + * @return The string representation of the int. + */ + public static String convertFromInteger(int value) + { + return String.valueOf(value); + } + + + /** + * Converts a string value to a <code>long</code>. + * + * @param rawValue + * the string value + * @return Returns a long. + * @throws XMPException + * If the <code>rawValue</code> is <code>null</code> or empty or the + * conversion fails. + */ + public static long convertToLong(String rawValue) throws XMPException + { + try + { + if (rawValue == null || rawValue.length() == 0) + { + throw new XMPException("Empty convert-string", XMPError.BADVALUE); + } + if (rawValue.startsWith("0x")) + { + return Long.parseLong(rawValue.substring(2), 16); + } + else + { + return Long.parseLong(rawValue); + } + } + catch (NumberFormatException e) + { + throw new XMPException("Invalid long string", XMPError.BADVALUE); + } + } + + + /** + * Convert from long to string. + * + * @param value + * a long value + * @return The string representation of the long. + */ + public static String convertFromLong(long value) + { + return String.valueOf(value); + } + + + /** + * Converts a string value to a <code>double</code>. + * + * @param rawValue + * the string value + * @return Returns a double. + * @throws XMPException + * If the <code>rawValue</code> is <code>null</code> or empty or the + * conversion fails. + */ + public static double convertToDouble(String rawValue) throws XMPException + { + try + { + if (rawValue == null || rawValue.length() == 0) + { + throw new XMPException("Empty convert-string", XMPError.BADVALUE); + } + else + { + return Double.parseDouble(rawValue); + } + } + catch (NumberFormatException e) + { + throw new XMPException("Invalid double string", XMPError.BADVALUE); + } + } + + + /** + * Convert from long to string. + * + * @param value + * a long value + * @return The string representation of the long. + */ + public static String convertFromDouble(double value) + { + return String.valueOf(value); + } + + + /** + * Converts a string value to an <code>XMPDateTime</code>. + * + * @param rawValue + * the string value + * @return Returns an <code>XMPDateTime</code>-object. + * @throws XMPException + * If the <code>rawValue</code> is <code>null</code> or empty or the + * conversion fails. + */ + public static XMPDateTime convertToDate(String rawValue) throws XMPException + { + if (rawValue == null || rawValue.length() == 0) + { + throw new XMPException("Empty convert-string", XMPError.BADVALUE); + } + else + { + return ISO8601Converter.parse(rawValue); + } + } + + + /** + * Convert from <code>XMPDateTime</code> to string. + * + * @param value + * an <code>XMPDateTime</code> + * @return The string representation of the long. + */ + public static String convertFromDate(XMPDateTime value) + { + return ISO8601Converter.render(value); + } + + + /** + * Convert from a byte array to a base64 encoded string. + * + * @param buffer + * the byte array to be converted + * @return Returns the base64 string. + */ + public static String encodeBase64(byte[] buffer) + { + return new String(Base64.encode(buffer)); + } + + + /** + * Decode from Base64 encoded string to raw data. + * + * @param base64String + * a base64 encoded string + * @return Returns a byte array containg the decoded string. + * @throws XMPException Thrown if the given string is not property base64 encoded + */ + public static byte[] decodeBase64(String base64String) throws XMPException + { + try + { + return Base64.decode(base64String.getBytes()); + } + catch (Throwable e) + { + throw new XMPException("Invalid base64 string", XMPError.BADVALUE, e); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java b/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java new file mode 100644 index 0000000..24b7c20 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java @@ -0,0 +1,45 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp; + +/** + * XMP Toolkit Version Information. + * <p> + * Version information for the XMP toolkit is stored in the jar-library and available through a + * runtime call, {@link XMPMetaFactory#getVersionInfo()}, addition static version numbers are + * defined in "version.properties". + * + * @since 23.01.2006 + */ +public interface XMPVersionInfo +{ + /** @return Returns the primary release number, the "1" in version "1.2.3". */ + int getMajor(); + + + /** @return Returns the secondary release number, the "2" in version "1.2.3". */ + int getMinor(); + + + /** @return Returns the tertiary release number, the "3" in version "1.2.3". */ + int getMicro(); + + + /** @return Returns a rolling build number, monotonically increasing in a release. */ + int getBuild(); + + + /** @return Returns true if this is a debug build. */ + boolean isDebug(); + + + /** @return Returns a comprehensive version information string. */ + String getMessage(); +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/Base64.java b/XMPCore/src/com/adobe/xmp/impl/Base64.java new file mode 100644 index 0000000..a327876 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/Base64.java @@ -0,0 +1,251 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2001 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + + + + +/** + * A utility class to perform base64 encoding and decoding as specified + * in RFC-1521. See also RFC 1421. + * + * @version $Revision: 1.4 $ + */ +public class Base64 +{ + /** marker for invalid bytes */ + private static final byte INVALID = -1; + /** marker for accepted whitespace bytes */ + private static final byte WHITESPACE = -2; + /** marker for an equal symbol */ + private static final byte EQUAL = -3; + + /** */ + private static byte[] base64 = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', // 0 to 3 + (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', // 4 to 7 + (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', // 8 to 11 + (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', // 11 to 15 + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', // 16 to 19 + (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', // 20 to 23 + (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', // 24 to 27 + (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', // 28 to 31 + (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', // 32 to 35 + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', // 36 to 39 + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', // 40 to 43 + (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', // 44 to 47 + (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', // 48 to 51 + (byte) '0', (byte) '1', (byte) '2', (byte) '3', // 52 to 55 + (byte) '4', (byte) '5', (byte) '6', (byte) '7', // 56 to 59 + (byte) '8', (byte) '9', (byte) '+', (byte) '/' // 60 to 63 + }; + /** */ + private static byte[] ascii = new byte[255]; + /** */ + static { + // not valid bytes + for (int idx = 0; idx < 255; idx++) + { + ascii[idx] = INVALID; + } + // valid bytes + for (int idx = 0; idx < base64.length; idx++) + { + ascii[base64[idx]] = (byte) idx; + } + // whitespaces + ascii[0x09] = WHITESPACE; + ascii[0x0A] = WHITESPACE; + ascii[0x0D] = WHITESPACE; + ascii[0x20] = WHITESPACE; + + // trailing equals + ascii[0x3d] = EQUAL; + } + + + /** + * Encode the given byte[]. + * + * @param src the source string. + * @return the base64-encoded data. + */ + public static final byte[] encode(byte[] src) + { + return encode(src, 0); + } + + + /** + * Encode the given byte[]. + * + * @param src the source string. + * @param lineFeed a linefeed is added after <code>linefeed</code> characters; + * must be dividable by four; 0 means no linefeeds + * @return the base64-encoded data. + */ + public static final byte[] encode(byte[] src, int lineFeed) + { + // linefeed must be dividable by 4 + lineFeed = lineFeed / 4 * 4; + if (lineFeed < 0) + { + lineFeed = 0; + } + + // determine code length + int codeLength = ((src.length + 2) / 3) * 4; + if (lineFeed > 0) + { + codeLength += (codeLength - 1) / lineFeed; + } + + byte[] dst = new byte[codeLength]; + int bits24; + int bits6; + // + // Do 3-byte to 4-byte conversion + 0-63 to ascii printable conversion + // + int didx = 0; + int sidx = 0; + int lf = 0; + while (sidx + 3 <= src.length) + { + bits24 = (src[sidx++] & 0xFF) << 16; + bits24 |= (src[sidx++] & 0xFF) << 8; + bits24 |= (src[sidx++] & 0xFF) << 0; + bits6 = (bits24 & 0x00FC0000) >> 18; + dst[didx++] = base64[bits6]; + bits6 = (bits24 & 0x0003F000) >> 12; + dst[didx++] = base64[bits6]; + bits6 = (bits24 & 0x00000FC0) >> 6; + dst[didx++] = base64[bits6]; + bits6 = (bits24 & 0x0000003F); + dst[didx++] = base64[bits6]; + + lf += 4; + if (didx < codeLength && lineFeed > 0 && lf % lineFeed == 0) + { + dst[didx++] = 0x0A; + } + } + if (src.length - sidx == 2) + { + bits24 = (src[sidx ] & 0xFF) << 16; + bits24 |= (src[sidx + 1] & 0xFF) << 8; + bits6 = (bits24 & 0x00FC0000) >> 18; + dst[didx++] = base64[bits6]; + bits6 = (bits24 & 0x0003F000) >> 12; + dst[didx++] = base64[bits6]; + bits6 = (bits24 & 0x00000FC0) >> 6; + dst[didx++] = base64[bits6]; + dst[didx++] = (byte) '='; + } + else if (src.length - sidx == 1) + { + bits24 = (src[sidx] & 0xFF) << 16; + bits6 = (bits24 & 0x00FC0000) >> 18; + dst[didx++] = base64[bits6]; + bits6 = (bits24 & 0x0003F000) >> 12; + dst[didx++] = base64[bits6]; + dst[didx++] = (byte) '='; + dst[didx++] = (byte) '='; + } + return dst; + } + + + /** + * Encode the given string. + * @param src the source string. + * @return the base64-encoded string. + */ + public static final String encode(String src) + { + return new String(encode(src.getBytes())); + } + + + /** + * Decode the given byte[]. + * + * @param src + * the base64-encoded data. + * @return the decoded data. + * @throws IllegalArgumentException Thrown if the base 64 strings contains non-valid characters, + * beside the bas64 chars, LF, CR, tab and space are accepted. + */ + public static final byte[] decode(byte[] src) throws IllegalArgumentException + { + // + // Do ascii printable to 0-63 conversion. + // + int sidx; + int srcLen = 0; + for (sidx = 0; sidx < src.length; sidx++) + { + byte val = ascii[src[sidx]]; + if (val >= 0) + { + src[srcLen++] = val; + } + else if (val == INVALID) + { + throw new IllegalArgumentException("Invalid base 64 string"); + } + } + + // + // Trim any padding. + // + while (srcLen > 0 && src[srcLen - 1] == EQUAL) + { + srcLen--; + } + byte[] dst = new byte[srcLen * 3 / 4]; + + // + // Do 4-byte to 3-byte conversion. + // + int didx; + for (sidx = 0, didx = 0; didx < dst.length - 2; sidx += 4, didx += 3) + { + dst[didx ] = (byte) (((src[sidx ] << 2) & 0xFF) + | ((src[sidx + 1] >>> 4) & 0x03)); + dst[didx + 1] = (byte) (((src[sidx + 1] << 4) & 0xFF) + | ((src[sidx + 2] >>> 2) & 0x0F)); + dst[didx + 2] = (byte) (((src[sidx + 2] << 6) & 0xFF) + | ((src[sidx + 3]) & 0x3F)); + } + if (didx < dst.length) + { + dst[didx] = (byte) (((src[sidx ] << 2) & 0xFF) + | ((src[sidx + 1] >>> 4) & 0x03)); + } + if (++didx < dst.length) + { + dst[didx] = (byte) (((src[sidx + 1] << 4) & 0xFF) + | ((src[sidx + 2] >>> 2) & 0x0F)); + } + return dst; + } + + + /** + * Decode the given string. + * + * @param src the base64-encoded string. + * @return the decoded string. + */ + public static final String decode(String src) + { + return new String(decode(src.getBytes())); + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java b/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java new file mode 100644 index 0000000..22cbc4b --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java @@ -0,0 +1,326 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + + + +package com.adobe.xmp.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + + +/** + * Byte buffer container including length of valid data. + * + * @since 11.10.2006 + */ +public class ByteBuffer +{ + /** */ + private byte[] buffer; + /** */ + private int length; + /** */ + private String encoding = null; + + + /** + * @param initialCapacity the initial capacity for this buffer + */ + public ByteBuffer(int initialCapacity) + { + this.buffer = new byte[initialCapacity]; + this.length = 0; + } + + + /** + * @param buffer a byte array that will be wrapped with <code>ByteBuffer</code>. + */ + public ByteBuffer(byte[] buffer) + { + this.buffer = buffer; + this.length = buffer.length; + } + + + /** + * @param buffer a byte array that will be wrapped with <code>ByteBuffer</code>. + * @param length the length of valid bytes in the array + */ + public ByteBuffer(byte[] buffer, int length) + { + if (length > buffer.length) + { + throw new ArrayIndexOutOfBoundsException("Valid length exceeds the buffer length."); + } + this.buffer = buffer; + this.length = length; + } + + + /** + * Loads the stream into a buffer. + * + * @param in an InputStream + * @throws IOException If the stream cannot be read. + */ + public ByteBuffer(InputStream in) throws IOException + { + // load stream into buffer + int chunk = 16384; + this.length = 0; + this.buffer = new byte[chunk]; + + int read; + while ((read = in.read(this.buffer, this.length, chunk)) > 0) + { + this.length += read; + if (read == chunk) + { + ensureCapacity(length + chunk); + } + else + { + break; + } + } + } + + + /** + * @param buffer a byte array that will be wrapped with <code>ByteBuffer</code>. + * @param offset the offset of the provided buffer. + * @param length the length of valid bytes in the array + */ + public ByteBuffer(byte[] buffer, int offset, int length) + { + if (length > buffer.length - offset) + { + throw new ArrayIndexOutOfBoundsException("Valid length exceeds the buffer length."); + } + this.buffer = new byte[length]; + System.arraycopy(buffer, offset, this.buffer, 0, length); + this.length = length; + } + + + /** + * @return Returns a byte stream that is limited to the valid amount of bytes. + */ + public InputStream getByteStream() + { + return new ByteArrayInputStream(buffer, 0, length); + } + + + /** + * @return Returns the length, that means the number of valid bytes, of the buffer; + * the inner byte array might be bigger than that. + */ + public int length() + { + return length; + } + + +// /** +// * <em>Note:</em> Only the byte up to length are valid! +// * @return Returns the inner byte buffer. +// */ +// public byte[] getBuffer() +// { +// return buffer; +// } + + + /** + * @param index the index to retrieve the byte from + * @return Returns a byte from the buffer + */ + public byte byteAt(int index) + { + if (index < length) + { + return buffer[index]; + } + else + { + throw new IndexOutOfBoundsException("The index exceeds the valid buffer area"); + } + } + + + /** + * @param index the index to retrieve a byte as int or char. + * @return Returns a byte from the buffer + */ + public int charAt(int index) + { + if (index < length) + { + return buffer[index] & 0xFF; + } + else + { + throw new IndexOutOfBoundsException("The index exceeds the valid buffer area"); + } + } + + + /** + * Appends a byte to the buffer. + * @param b a byte + */ + public void append(byte b) + { + ensureCapacity(length + 1); + buffer[length++] = b; + } + + + /** + * Appends a byte array or part of to the buffer. + * + * @param bytes a byte array + * @param offset an offset with + * @param len + */ + public void append(byte[] bytes, int offset, int len) + { + ensureCapacity(length + len); + System.arraycopy(bytes, offset, buffer, length, len); + length += len; + } + + + /** + * Append a byte array to the buffer + * @param bytes a byte array + */ + public void append(byte[] bytes) + { + append(bytes, 0, bytes.length); + } + + + /** + * Append another buffer to this buffer. + * @param anotherBuffer another <code>ByteBuffer</code> + */ + public void append(ByteBuffer anotherBuffer) + { + append(anotherBuffer.buffer, 0, anotherBuffer.length); + } + + + /** + * Detects the encoding of the byte buffer, stores and returns it. + * Only UTF-8, UTF-16LE/BE and UTF-32LE/BE are recognized. + * <em>Note:</em> UTF-32 flavors are not supported by Java, the XML-parser will complain. + * + * @return Returns the encoding string. + */ + public String getEncoding() + { + if (encoding == null) + { + // needs four byte at maximum to determine encoding + if (length < 2) + { + // only one byte length must be UTF-8 + encoding = "UTF-8"; + } + else if (buffer[0] == 0) + { + // These cases are: + // 00 nn -- -- - Big endian UTF-16 + // 00 00 00 nn - Big endian UTF-32 + // 00 00 FE FF - Big endian UTF 32 + + if (length < 4 || buffer[1] != 0) + { + encoding = "UTF-16BE"; + } + else if ((buffer[2] & 0xFF) == 0xFE && (buffer[3] & 0xFF) == 0xFF) + { + encoding = "UTF-32BE"; + } + else + { + encoding = "UTF-32"; + } + } + else if ((buffer[0] & 0xFF) < 0x80) + { + // These cases are: + // nn mm -- -- - UTF-8, includes EF BB BF case + // nn 00 -- -- - Little endian UTF-16 + + if (buffer[1] != 0) + { + encoding = "UTF-8"; + } + else if (length < 4 || buffer[2] != 0) + { + encoding = "UTF-16LE"; + } + else + { + encoding = "UTF-32LE"; + } + } + else + { + // These cases are: + // EF BB BF -- - UTF-8 + // FE FF -- -- - Big endian UTF-16 + // FF FE 00 00 - Little endian UTF-32 + // FF FE -- -- - Little endian UTF-16 + + if ((buffer[0] & 0xFF) == 0xEF) + { + encoding = "UTF-8"; + } + else if ((buffer[0] & 0xFF) == 0xFE) + { + encoding = "UTF-16"; // in fact BE + } + else if (length < 4 || buffer[2] != 0) + { + encoding = "UTF-16"; // in fact LE + } + else + { + encoding = "UTF-32"; // in fact LE + } + } + } + + return encoding; + } + + + /** + * Ensures the requested capacity by increasing the buffer size when the + * current length is exceeded. + * + * @param requestedLength requested new buffer length + */ + private void ensureCapacity(int requestedLength) + { + if (requestedLength > buffer.length) + { + byte[] oldBuf = buffer; + buffer = new byte[oldBuf.length * 2]; + System.arraycopy(oldBuf, 0, buffer, 0, oldBuf.length); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java b/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java new file mode 100644 index 0000000..ded1296 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java @@ -0,0 +1,79 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.io.IOException; +import java.io.OutputStream; + + +/** + * An <code>OutputStream</code> that counts the written bytes. + * + * @since 08.11.2006 + */ +public final class CountOutputStream extends OutputStream +{ + /** the decorated output stream */ + private final OutputStream out; + /** the byte counter */ + private int bytesWritten = 0; + + + /** + * Constructor with providing the output stream to decorate. + * @param out an <code>OutputStream</code> + */ + CountOutputStream(OutputStream out) + { + this.out = out; + } + + + /** + * Counts the written bytes. + * @see java.io.OutputStream#write(byte[], int, int) + */ + public void write(byte[] buf, int off, int len) throws IOException + { + out.write(buf, off, len); + bytesWritten += len; + } + + + /** + * Counts the written bytes. + * @see java.io.OutputStream#write(byte[]) + */ + public void write(byte[] buf) throws IOException + { + out.write(buf); + bytesWritten += buf.length; + } + + + /** + * Counts the written bytes. + * @see java.io.OutputStream#write(int) + */ + public void write(int b) throws IOException + { + out.write(b); + bytesWritten++; + } + + + /** + * @return the bytesWritten + */ + public int getBytesWritten() + { + return bytesWritten; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java b/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java new file mode 100644 index 0000000..c6ac7d4 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java @@ -0,0 +1,214 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.io.IOException; +import java.io.PushbackReader; +import java.io.Reader; + + +/** + * @since 22.08.2006 + */ +public class FixASCIIControlsReader extends PushbackReader +{ + /** */ + private static final int STATE_START = 0; + /** */ + private static final int STATE_AMP = 1; + /** */ + private static final int STATE_HASH = 2; + /** */ + private static final int STATE_HEX = 3; + /** */ + private static final int STATE_DIG1 = 4; + /** */ + private static final int STATE_ERROR = 5; + /** */ + private static final int BUFFER_SIZE = 8; + /** the state of the automaton */ + private int state = STATE_START; + /** the result of the escaping sequence */ + private int control = 0; + /** count the digits of the sequence */ + private int digits = 0; + + /** + * The look-ahead size is 6 at maximum (&#xAB;) + * @see PushbackReader#PushbackReader(Reader, int) + * @param in a Reader + */ + public FixASCIIControlsReader(Reader in) + { + super(in, BUFFER_SIZE); + } + + + /** + * @see Reader#read(char[], int, int) + */ + public int read(char[] cbuf, int off, int len) throws IOException + { + int readAhead = 0; + int read = 0; + int pos = off; + char[] readAheadBuffer = new char[BUFFER_SIZE]; + + boolean available = true; + while (available && read < len) + { + available = super.read(readAheadBuffer, readAhead, 1) == 1; + if (available) + { + char c = processChar(readAheadBuffer[readAhead]); + if (state == STATE_START) + { + // replace control chars with space + if (Utils.isControlChar(c)) + { + c = ' '; + } + cbuf[pos++] = c; + readAhead = 0; + read++; + } + else if (state == STATE_ERROR) + { + unread(readAheadBuffer, 0, readAhead + 1); + readAhead = 0; + } + else + { + readAhead++; + } + } + else if (readAhead > 0) + { + // handles case when file ends within excaped sequence + unread(readAheadBuffer, 0, readAhead); + state = STATE_ERROR; + readAhead = 0; + available = true; + } + } + + + return read > 0 || available ? read : -1; + } + + + /** + * Processes numeric escaped chars to find out if they are a control character. + * @param ch a char + * @return Returns the char directly or as replacement for the escaped sequence. + */ + private char processChar(char ch) + { + switch (state) + { + case STATE_START: + if (ch == '&') + { + state = STATE_AMP; + } + return ch; + + case STATE_AMP: + if (ch == '#') + { + state = STATE_HASH; + } + else + { + state = STATE_ERROR; + } + return ch; + + case STATE_HASH: + if (ch == 'x') + { + control = 0; + digits = 0; + state = STATE_HEX; + } + else if ('0' <= ch && ch <= '9') + { + control = Character.digit(ch, 10); + digits = 1; + state = STATE_DIG1; + } + else + { + state = STATE_ERROR; + } + return ch; + + case STATE_DIG1: + if ('0' <= ch && ch <= '9') + { + control = control * 10 + Character.digit(ch, 10); + digits++; + if (digits <= 5) + { + state = STATE_DIG1; + } + else + { + state = STATE_ERROR; // sequence too long + } + } + else if (ch == ';' && Utils.isControlChar((char) control)) + { + state = STATE_START; + return (char) control; + } + else + { + state = STATE_ERROR; + } + return ch; + + case STATE_HEX: + if (('0' <= ch && ch <= '9') || + ('a' <= ch && ch <= 'f') || + ('A' <= ch && ch <= 'F')) + { + control = control * 16 + Character.digit(ch, 16); + digits++; + if (digits <= 4) + { + state = STATE_HEX; + } + else + { + state = STATE_ERROR; // sequence too long + } + } + else if (ch == ';' && Utils.isControlChar((char) control)) + { + state = STATE_START; + return (char) control; + } + else + { + state = STATE_ERROR; + } + return ch; + + case STATE_ERROR: + state = STATE_START; + return ch; + + default: + // not reachable + return ch; + } + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java b/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java new file mode 100644 index 0000000..b38bb16 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java @@ -0,0 +1,505 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.SimpleTimeZone; + +import com.adobe.xmp.XMPDateTime; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; + + +/** + * Converts between ISO 8601 Strings and <code>Calendar</code> with millisecond resolution. + * + * @since 16.02.2006 + */ +public final class ISO8601Converter +{ + /** Hides public constructor */ + private ISO8601Converter() + { + // EMPTY + } + + + /** + * Converts an ISO 8601 string to an <code>XMPDateTime</code>. + * + * Parse a date according to ISO 8601 and + * http://www.w3.org/TR/NOTE-datetime: + * <ul> + * <li>YYYY + * <li>YYYY-MM + * <li>YYYY-MM-DD + * <li>YYYY-MM-DDThh:mmTZD + * <li>YYYY-MM-DDThh:mm:ssTZD + * <li>YYYY-MM-DDThh:mm:ss.sTZD + * </ul> + * + * Data fields: + * <ul> + * <li>YYYY = four-digit year + * <li>MM = two-digit month (01=January, etc.) + * <li>DD = two-digit day of month (01 through 31) + * <li>hh = two digits of hour (00 through 23) + * <li>mm = two digits of minute (00 through 59) + * <li>ss = two digits of second (00 through 59) + * <li>s = one or more digits representing a decimal fraction of a second + * <li>TZD = time zone designator (Z or +hh:mm or -hh:mm) + * </ul> + * + * Note that ISO 8601 does not seem to allow years less than 1000 or greater + * than 9999. We allow any year, even negative ones. The year is formatted + * as "%.4d". + * <p> + * <em>Note:</em> Tolerate missing TZD, assume is UTC. Photoshop 8 writes + * dates like this for exif:GPSTimeStamp.<br> + * <em>Note:</em> Tolerate missing date portion, in case someone foolishly + * writes a time-only value that way. + * + * @param iso8601String a date string that is ISO 8601 conform. + * @return Returns a <code>Calendar</code>. + * @throws XMPException Is thrown when the string is non-conform. + */ + public static XMPDateTime parse(String iso8601String) throws XMPException + { + return parse(iso8601String, new XMPDateTimeImpl()); + } + + + /** + * @param iso8601String a date string that is ISO 8601 conform. + * @param binValue an existing XMPDateTime to set with the parsed date + * @return Returns an XMPDateTime-object containing the ISO8601-date. + * @throws XMPException Is thrown when the string is non-conform. + */ + public static XMPDateTime parse(String iso8601String, XMPDateTime binValue) throws XMPException + { + ParameterAsserts.assertNotNull(iso8601String); + + ParseState input = new ParseState(iso8601String); + int value; + + boolean timeOnly = + input.ch(0) == 'T' || + (input.length() >= 2 && input.ch(1) == ':' || + (input.length() >= 3 && input.ch(2) == ':')); + + if (!timeOnly) + { + if (input.ch(0) == '-') + { + input.skip(); + } + + + // Extract the year. + value = input.gatherInt("Invalid year in date string", 9999); + if (input.hasNext() && input.ch() != '-') + { + throw new XMPException("Invalid date string, after year", XMPError.BADVALUE); + } + + if (input.ch(0) == '-') + { + value = -value; + } + binValue.setYear(value); + if (!input.hasNext()) + { + return binValue; + } + input.skip(); + + + // Extract the month. + value = input.gatherInt("Invalid month in date string", 12); + if (input.hasNext() && input.ch() != '-') + { + throw new XMPException("Invalid date string, after month", XMPError.BADVALUE); + } + binValue.setMonth(value); + if (!input.hasNext()) + { + return binValue; + } + input.skip(); + + + // Extract the day. + value = input.gatherInt("Invalid day in date string", 31); + if (input.hasNext() && input.ch() != 'T') + { + throw new XMPException("Invalid date string, after day", XMPError.BADVALUE); + } + binValue.setDay(value); + if (!input.hasNext()) + { + return binValue; + } + } + else + { + // set default day and month in the year 0000 + binValue.setMonth(1); + binValue.setDay(1); + } + + if (input.ch() == 'T') + { + input.skip(); + } + else if (!timeOnly) + { + throw new XMPException("Invalid date string, missing 'T' after date", + XMPError.BADVALUE); + } + + + // Extract the hour. + value = input.gatherInt("Invalid hour in date string", 23); + if (input.ch() != ':') + { + throw new XMPException("Invalid date string, after hour", XMPError.BADVALUE); + } + binValue.setHour(value); + + // Don't check for done, we have to work up to the time zone. + input.skip(); + + + // Extract the minute. + value = input.gatherInt("Invalid minute in date string", 59); + if (input.hasNext() && + input.ch() != ':' && input.ch() != 'Z' && input.ch() != '+' && input.ch() != '-') + { + throw new XMPException("Invalid date string, after minute", XMPError.BADVALUE); + } + binValue.setMinute(value); + + if (input.ch() == ':') + { + input.skip(); + value = input.gatherInt("Invalid whole seconds in date string", 59); + if (input.hasNext() && input.ch() != '.' && input.ch() != 'Z' && + input.ch() != '+' && input.ch() != '-') + { + throw new XMPException("Invalid date string, after whole seconds", + XMPError.BADVALUE); + } + binValue.setSecond(value); + if (input.ch() == '.') + { + input.skip(); + int digits = input.pos(); + value = input.gatherInt("Invalid fractional seconds in date string", 999999999); + if (input.ch() != 'Z' && input.ch() != '+' && input.ch() != '-') + { + throw new XMPException("Invalid date string, after fractional second", + XMPError.BADVALUE); + } + digits = input.pos() - digits; + for (; digits > 9; --digits) + { + value = value / 10; + } + for (; digits < 9; ++digits) + { + value = value * 10; + } + binValue.setNanoSecond(value); + } + } + + int tzSign = 0; + int tzHour = 0; + int tzMinute = 0; + if (input.ch() == 'Z') + { + input.skip(); + } + else if (input.hasNext()) + { + if (input.ch() == '+') + { + tzSign = 1; + } + else if (input.ch() == '-') + { + tzSign = -1; + } + else + { + throw new XMPException("Time zone must begin with 'Z', '+', or '-'", + XMPError.BADVALUE); + } + + input.skip(); + // Extract the time zone hour. + tzHour = input.gatherInt("Invalid time zone hour in date string", 23); + if (input.ch() != ':') + { + throw new XMPException("Invalid date string, after time zone hour", + XMPError.BADVALUE); + } + input.skip(); + + // Extract the time zone minute. + tzMinute = input.gatherInt("Invalid time zone minute in date string", 59); + } + + // create a corresponding TZ and set it time zone + int offset = (tzHour * 3600 * 1000 + tzMinute * 60 * 1000) * tzSign; + binValue.setTimeZone(new SimpleTimeZone(offset, "")); + + + if (input.hasNext()) + { + throw new XMPException( + "Invalid date string, extra chars at end", XMPError.BADVALUE); + } + + return binValue; + } + + + /** + * Converts a <code>Calendar</code> into an ISO 8601 string. + * Format a date according to ISO 8601 and http://www.w3.org/TR/NOTE-datetime: + * <ul> + * <li>YYYY + * <li>YYYY-MM + * <li>YYYY-MM-DD + * <li>YYYY-MM-DDThh:mmTZD + * <li>YYYY-MM-DDThh:mm:ssTZD + * <li>YYYY-MM-DDThh:mm:ss.sTZD + * </ul> + * + * Data fields: + * <ul> + * <li>YYYY = four-digit year + * <li>MM = two-digit month (01=January, etc.) + * <li>DD = two-digit day of month (01 through 31) + * <li>hh = two digits of hour (00 through 23) + * <li>mm = two digits of minute (00 through 59) + * <li>ss = two digits of second (00 through 59) + * <li>s = one or more digits representing a decimal fraction of a second + * <li>TZD = time zone designator (Z or +hh:mm or -hh:mm) + * </ul> + * <p> + * <em>Note:</em> ISO 8601 does not seem to allow years less than 1000 or greater than 9999. + * We allow any year, even negative ones. The year is formatted as "%.4d".<p> + * <em>Note:</em> Fix for bug 1269463 (silently fix out of range values) included in parsing. + * The quasi-bogus "time only" values from Photoshop CS are not supported. + * + * @param dateTime an XMPDateTime-object. + * @return Returns an ISO 8601 string. + */ + public static String render(XMPDateTime dateTime) + { + StringBuffer buffer = new StringBuffer(); + + // year is rendered in any case, even 0000 + DecimalFormat df = new DecimalFormat("0000", new DecimalFormatSymbols(Locale.ENGLISH)); + buffer.append(df.format(dateTime.getYear())); + if (dateTime.getMonth() == 0) + { + return buffer.toString(); + } + + // month + df.applyPattern("'-'00"); + buffer.append(df.format(dateTime.getMonth())); + if (dateTime.getDay() == 0) + { + return buffer.toString(); + } + + // day + buffer.append(df.format(dateTime.getDay())); + + // time, rendered if any time field is not zero + if (dateTime.getHour() != 0 || + dateTime.getMinute() != 0 || + dateTime.getSecond() != 0 || + dateTime.getNanoSecond() != 0 || + (dateTime.getTimeZone() != null && dateTime.getTimeZone().getRawOffset() != 0)) + { + // hours and minutes + buffer.append('T'); + df.applyPattern("00"); + buffer.append(df.format(dateTime.getHour())); + buffer.append(':'); + buffer.append(df.format(dateTime.getMinute())); + + // seconds and nanoseconds + if (dateTime.getSecond() != 0 || dateTime.getNanoSecond() != 0) + { + double seconds = dateTime.getSecond() + dateTime.getNanoSecond() / 1e9d; + + df.applyPattern(":00.#########"); + buffer.append(df.format(seconds)); + } + + // time zone + if (dateTime.getTimeZone() != null) + { + // used to calculate the time zone offset incl. Daylight Savings + long timeInMillis = dateTime.getCalendar().getTimeInMillis(); + int offset = dateTime.getTimeZone().getOffset(timeInMillis); + if (offset == 0) + { + // UTC + buffer.append('Z'); + } + else + { + int thours = offset / 3600000; + int tminutes = Math.abs(offset % 3600000 / 60000); + df.applyPattern("+00;-00"); + buffer.append(df.format(thours)); + df.applyPattern(":00"); + buffer.append(df.format(tminutes)); + } + } + } + return buffer.toString(); + } + + +} + + +/** + * @since 22.08.2006 + */ +class ParseState +{ + /** */ + private String str; + /** */ + private int pos = 0; + + + /** + * @param str initializes the parser container + */ + public ParseState(String str) + { + this.str = str; + } + + + /** + * @return Returns the length of the input. + */ + public int length() + { + return str.length(); + } + + + /** + * @return Returns whether there are more chars to come. + */ + public boolean hasNext() + { + return pos < str.length(); + } + + + /** + * @param index index of char + * @return Returns char at a certain index. + */ + public char ch(int index) + { + return index < str.length() ? + str.charAt(index) : + 0x0000; + } + + + /** + * @return Returns the current char or 0x0000 if there are no more chars. + */ + public char ch() + { + return pos < str.length() ? + str.charAt(pos) : + 0x0000; + } + + + /** + * Skips the next char. + */ + public void skip() + { + pos++; + } + + + /** + * @return Returns the current position. + */ + public int pos() + { + return pos; + } + + + /** + * Parses a integer from the source and sets the pointer after it. + * @param errorMsg Error message to put in the exception if no number can be found + * @param maxValue the max value of the number to return + * @return Returns the parsed integer. + * @throws XMPException Thrown if no integer can be found. + */ + public int gatherInt(String errorMsg, int maxValue) throws XMPException + { + int value = 0; + boolean success = false; + char ch = ch(pos); + while ('0' <= ch && ch <= '9') + { + value = (value * 10) + (ch - '0'); + success = true; + pos++; + ch = ch(pos); + } + + if (success) + { + if (value > maxValue) + { + return maxValue; + } + else if (value < 0) + { + return 0; + } + else + { + return value; + } + } + else + { + throw new XMPException(errorMsg, XMPError.BADVALUE); + } + } +} + + diff --git a/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java b/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java new file mode 100644 index 0000000..118d77d --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java @@ -0,0 +1,197 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + + + +package com.adobe.xmp.impl; + +import java.io.UnsupportedEncodingException; + + +/** + * @since 12.10.2006 + */ +public class Latin1Converter +{ + /** */ + private static final int STATE_START = 0; + /** */ + private static final int STATE_UTF8CHAR = 11; + + + /** + * Private constructor + */ + private Latin1Converter() + { + // EMPTY + } + + + /** + * A converter that processes a byte buffer containing a mix of UTF8 and Latin-1/Cp1252 chars. + * The result is a buffer where those chars have been converted to UTF-8; + * that means it contains only valid UTF-8 chars. + * <p> + * <em>Explanation of the processing:</em> First the encoding of the buffer is detected looking + * at the first four bytes (that works only if the buffer starts with an ASCII-char, + * like xmls '<'). UTF-16/32 flavours do not require further proccessing. + * <p> + * In the case, UTF-8 is detected, it assumes wrong UTF8 chars to be a sequence of + * Latin-1/Cp1252 encoded bytes and converts the chars to their corresponding UTF-8 byte + * sequence. + * <p> + * The 0x80..0x9F range is undefined in Latin-1, but is defined in Windows code + * page 1252. The bytes 0x81, 0x8D, 0x8F, 0x90, and 0x9D are formally undefined + * by Windows 1252. These are in XML's RestrictedChar set, so we map them to a + * space. + * <p> + * The official Latin-1 characters in the range 0xA0..0xFF are converted into + * the Unicode Latin Supplement range U+00A0 - U+00FF. + * <p> + * <em>Example:</em> If an Euro-symbol (€) appears in the byte buffer (0xE2, 0x82, 0xAC), + * it will be left as is. But if only the first two bytes are appearing, + * followed by an ASCII char a (0xE2 - 0x82 - 0x41), it will be converted to + * 0xC3, 0xA2 (â) - 0xE2, 0x80, 0x9A (‚) - 0x41 (a). + * + * @param buffer a byte buffer contain + * @return Returns a new buffer containing valid UTF-8 + */ + public static ByteBuffer convert(ByteBuffer buffer) + { + if ("UTF-8".equals(buffer.getEncoding())) + { + // the buffer containing one UTF-8 char (up to 8 bytes) + byte[] readAheadBuffer = new byte[8]; + // the number of bytes read ahead. + int readAhead = 0; + // expected UTF8 bytesto come + int expectedBytes = 0; + // output buffer with estimated length + ByteBuffer out = new ByteBuffer(buffer.length() * 4 / 3); + + int state = STATE_START; + for (int i = 0; i < buffer.length(); i++) + { + int b = buffer.charAt(i); + + switch (state) + { + default: + case STATE_START: + if (b < 0x7F) + { + out.append((byte) b); + } + else if (b >= 0xC0) + { + // start of UTF8 sequence + expectedBytes = -1; + int test = b; + for (; expectedBytes < 8 && (test & 0x80) == 0x80; test = test << 1) + { + expectedBytes++; + } + readAheadBuffer[readAhead++] = (byte) b; + state = STATE_UTF8CHAR; + } + else // implicitly: b >= 0x80 && b < 0xC0 + { + // invalid UTF8 start char, assume to be Latin-1 + byte[] utf8 = convertToUTF8((byte) b); + out.append(utf8); + } + break; + + case STATE_UTF8CHAR: + if (expectedBytes > 0 && (b & 0xC0) == 0x80) + { + // valid UTF8 char, add to readAheadBuffer + readAheadBuffer[readAhead++] = (byte) b; + expectedBytes--; + + if (expectedBytes == 0) + { + out.append(readAheadBuffer, 0, readAhead); + readAhead = 0; + + state = STATE_START; + } + } + else + { + // invalid UTF8 char: + // 1. convert first of seq to UTF8 + byte[] utf8 = convertToUTF8(readAheadBuffer[0]); + out.append(utf8); + + // 2. continue processing at second byte of sequence + i = i - readAhead; + readAhead = 0; + + state = STATE_START; + } + break; + } + } + + // loop ends with "half" Utf8 char --> assume that the bytes are Latin-1 + if (state == STATE_UTF8CHAR) + { + for (int j = 0; j < readAhead; j++) + { + byte b = readAheadBuffer[j]; + byte[] utf8 = convertToUTF8(b); + out.append(utf8); + } + } + + return out; + } + else + { + // Latin-1 fixing applies only to UTF-8 + return buffer; + } + } + + + /** + * Converts a Cp1252 char (contains all Latin-1 chars above 0x80) into a + * UTF-8 byte sequence. The bytes 0x81, 0x8D, 0x8F, 0x90, and 0x9D are + * formally undefined by Windows 1252 and therefore replaced by a space + * (0x20). + * + * @param ch + * an Cp1252 / Latin-1 byte + * @return Returns a byte array containing a UTF-8 byte sequence. + */ + private static byte[] convertToUTF8(byte ch) + { + int c = ch & 0xFF; + try + { + if (c >= 0x80) + { + if (c == 0x81 || c == 0x8D || c == 0x8F || c == 0x90 || c == 0x9D) + { + return new byte[] { 0x20 }; // space for undefined + } + + // interpret byte as Windows Cp1252 char + return new String(new byte[] { ch }, "cp1252").getBytes("UTF-8"); + } + } + catch (UnsupportedEncodingException e) + { + // EMPTY + } + return new byte[] { ch }; + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java b/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java new file mode 100644 index 0000000..78ae5d6 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java @@ -0,0 +1,153 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; + + +/** + * @since 11.08.2006 + */ +class ParameterAsserts implements XMPConst +{ + /** + * private constructor + */ + private ParameterAsserts() + { + // EMPTY + } + + + /** + * Asserts that an array name is set. + * @param arrayName an array name + * @throws XMPException Array name is null or empty + */ + public static void assertArrayName(String arrayName) throws XMPException + { + if (arrayName == null || arrayName.length() == 0) + { + throw new XMPException("Empty array name", XMPError.BADPARAM); + } + } + + + /** + * Asserts that a property name is set. + * @param propName a property name or path + * @throws XMPException Property name is null or empty + */ + public static void assertPropName(String propName) throws XMPException + { + if (propName == null || propName.length() == 0) + { + throw new XMPException("Empty property name", XMPError.BADPARAM); + } + } + + + /** + * Asserts that a schema namespace is set. + * @param schemaNS a schema namespace + * @throws XMPException Schema is null or empty + */ + public static void assertSchemaNS(String schemaNS) throws XMPException + { + if (schemaNS == null || schemaNS.length() == 0) + { + throw new XMPException("Empty schema namespace URI", XMPError.BADPARAM); + } + } + + + /** + * Asserts that a prefix is set. + * @param prefix a prefix + * @throws XMPException Prefix is null or empty + */ + public static void assertPrefix(String prefix) throws XMPException + { + if (prefix == null || prefix.length() == 0) + { + throw new XMPException("Empty prefix", XMPError.BADPARAM); + } + } + + + /** + * Asserts that a specific language is set. + * @param specificLang a specific lang + * @throws XMPException Specific language is null or empty + */ + public static void assertSpecificLang(String specificLang) throws XMPException + { + if (specificLang == null || specificLang.length() == 0) + { + throw new XMPException("Empty specific language", XMPError.BADPARAM); + } + } + + + /** + * Asserts that a struct name is set. + * @param structName a struct name + * @throws XMPException Struct name is null or empty + */ + public static void assertStructName(String structName) throws XMPException + { + if (structName == null || structName.length() == 0) + { + throw new XMPException("Empty array name", XMPError.BADPARAM); + } + } + + + /** + * Asserts that any string parameter is set. + * @param param any string parameter + * @throws XMPException Thrown if the parameter is null or has length 0. + */ + public static void assertNotNull(Object param) throws XMPException + { + if (param == null) + { + throw new XMPException("Parameter must not be null", XMPError.BADPARAM); + } + else if ((param instanceof String) && ((String) param).length() == 0) + { + throw new XMPException("Parameter must not be null or empty", XMPError.BADPARAM); + } + } + + + /** + * Asserts that the xmp object is of this implemention + * ({@link XMPMetaImpl}). + * @param xmp the XMP object + * @throws XMPException A wrong implentaion is used. + */ + public static void assertImplementation(XMPMeta xmp) throws XMPException + { + if (xmp == null) + { + throw new XMPException("Parameter must not be null", + XMPError.BADPARAM); + } + else if (!(xmp instanceof XMPMetaImpl)) + { + throw new XMPException("The XMPMeta-object is not compatible with this implementation", + XMPError.BADPARAM); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java b/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java new file mode 100644 index 0000000..2b96550 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java @@ -0,0 +1,1323 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import org.w3c.dom.Attr; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.XMPSchemaRegistry; +import com.adobe.xmp.options.PropertyOptions; + + +/** + * Parser for "normal" XML serialisation of RDF. + * + * @since 14.07.2006 + */ +public class ParseRDF implements XMPError, XMPConst +{ + /** */ + public static final int RDFTERM_OTHER = 0; + /** Start of coreSyntaxTerms. */ + public static final int RDFTERM_RDF = 1; + /** */ + public static final int RDFTERM_ID = 2; + /** */ + public static final int RDFTERM_ABOUT = 3; + /** */ + public static final int RDFTERM_PARSE_TYPE = 4; + /** */ + public static final int RDFTERM_RESOURCE = 5; + /** */ + public static final int RDFTERM_NODE_ID = 6; + /** End of coreSyntaxTerms */ + public static final int RDFTERM_DATATYPE = 7; + /** Start of additions for syntax Terms. */ + public static final int RDFTERM_DESCRIPTION = 8; + /** End of of additions for syntaxTerms. */ + public static final int RDFTERM_LI = 9; + /** Start of oldTerms. */ + public static final int RDFTERM_ABOUT_EACH = 10; + /** */ + public static final int RDFTERM_ABOUT_EACH_PREFIX = 11; + /** End of oldTerms. */ + public static final int RDFTERM_BAG_ID = 12; + /** */ + public static final int RDFTERM_FIRST_CORE = RDFTERM_RDF; + /** */ + public static final int RDFTERM_LAST_CORE = RDFTERM_DATATYPE; + /** ! Yes, the syntax terms include the core terms. */ + public static final int RDFTERM_FIRST_SYNTAX = RDFTERM_FIRST_CORE; + /** */ + public static final int RDFTERM_LAST_SYNTAX = RDFTERM_LI; + /** */ + public static final int RDFTERM_FIRST_OLD = RDFTERM_ABOUT_EACH; + /** */ + public static final int RDFTERM_LAST_OLD = RDFTERM_BAG_ID; + + /** this prefix is used for default namespaces */ + public static final String DEFAULT_PREFIX = "_dflt"; + + + + /** + * The main parsing method. The XML tree is walked through from the root node and and XMP tree + * is created. This is a raw parse, the normalisation of the XMP tree happens outside. + * + * @param xmlRoot the XML root node + * @return Returns an XMP metadata object (not normalized) + * @throws XMPException Occurs if the parsing fails for any reason. + */ + static XMPMetaImpl parse(Node xmlRoot) throws XMPException + { + XMPMetaImpl xmp = new XMPMetaImpl(); + rdf_RDF(xmp, xmlRoot); + return xmp; + } + + + /** + * Each of these parsing methods is responsible for recognizing an RDF + * syntax production and adding the appropriate structure to the XMP tree. + * They simply return for success, failures will throw an exception. + * + * @param xmp the xmp metadata object that is generated + * @param rdfRdfNode the top-level xml node + * @throws XMPException thown on parsing errors + */ + static void rdf_RDF(XMPMetaImpl xmp, Node rdfRdfNode) throws XMPException + { + if (rdfRdfNode.hasAttributes()) + { + rdf_NodeElementList (xmp, xmp.getRoot(), rdfRdfNode); + } + else + { + throw new XMPException("Invalid attributes of rdf:RDF element", BADRDF); + } + } + + + /** + * 7.2.10 nodeElementList<br> + * ws* ( nodeElement ws* )* + * + * Note: this method is only called from the rdf:RDF-node (top level) + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param rdfRdfNode the top-level xml node + * @throws XMPException thown on parsing errors + */ + private static void rdf_NodeElementList(XMPMetaImpl xmp, XMPNode xmpParent, Node rdfRdfNode) + throws XMPException + { + for (int i = 0; i < rdfRdfNode.getChildNodes().getLength(); i++) + { + Node child = rdfRdfNode.getChildNodes().item(i); + // filter whitespaces (and all text nodes) + if (!isWhitespaceNode(child)) + { + rdf_NodeElement (xmp, xmpParent, child, true); + } + } + } + + + /** + * 7.2.5 nodeElementURIs + * anyURI - ( coreSyntaxTerms | rdf:li | oldTerms ) + * + * 7.2.11 nodeElement + * start-element ( URI == nodeElementURIs, + * attributes == set ( ( idAttr | nodeIdAttr | aboutAttr )?, propertyAttr* ) ) + * propertyEltList + * end-element() + * + * A node element URI is rdf:Description or anything else that is not an RDF + * term. + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_NodeElement(XMPMetaImpl xmp, XMPNode xmpParent, Node xmlNode, + boolean isTopLevel) throws XMPException + { + int nodeTerm = getRDFTermKind (xmlNode); + if (nodeTerm != RDFTERM_DESCRIPTION && nodeTerm != RDFTERM_OTHER) + { + throw new XMPException("Node element must be rdf:Description or typed node", + BADRDF); + } + else if (isTopLevel && nodeTerm == RDFTERM_OTHER) + { + throw new XMPException("Top level typed node not allowed", BADXMP); + } + else + { + rdf_NodeElementAttrs (xmp, xmpParent, xmlNode, isTopLevel); + rdf_PropertyElementList (xmp, xmpParent, xmlNode, isTopLevel); + } + + } + + + /** + * + * 7.2.7 propertyAttributeURIs + * anyURI - ( coreSyntaxTerms | rdf:Description | rdf:li | oldTerms ) + * + * 7.2.11 nodeElement + * start-element ( URI == nodeElementURIs, + * attributes == set ( ( idAttr | nodeIdAttr | aboutAttr )?, propertyAttr* ) ) + * propertyEltList + * end-element() + * + * Process the attribute list for an RDF node element. A property attribute URI is + * anything other than an RDF term. The rdf:ID and rdf:nodeID attributes are simply ignored, + * as are rdf:about attributes on inner nodes. + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_NodeElementAttrs(XMPMetaImpl xmp, XMPNode xmpParent, Node xmlNode, + boolean isTopLevel) throws XMPException + { + // Used to detect attributes that are mutually exclusive. + int exclusiveAttrs = 0; + + for (int i = 0; i < xmlNode.getAttributes().getLength(); i++) + { + Node attribute = xmlNode.getAttributes().item(i); + + // quick hack, ns declarations do not appear in C++ + // ignore "ID" without namespace + if ("xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + continue; + } + + int attrTerm = getRDFTermKind(attribute); + + switch (attrTerm) + { + case RDFTERM_ID: + case RDFTERM_NODE_ID: + case RDFTERM_ABOUT: + if (exclusiveAttrs > 0) + { + throw new XMPException("Mutally exclusive about, ID, nodeID attributes", + BADRDF); + } + + exclusiveAttrs++; + + if (isTopLevel && (attrTerm == RDFTERM_ABOUT)) + { + // This is the rdf:about attribute on a top level node. Set + // the XMP tree name if + // it doesn't have a name yet. Make sure this name matches + // the XMP tree name. + if (xmpParent.getName() != null && xmpParent.getName().length() > 0) + { + if (!xmpParent.getName().equals(attribute.getNodeValue())) + { + throw new XMPException("Mismatched top level rdf:about values", + BADXMP); + } + } + else + { + xmpParent.setName(attribute.getNodeValue()); + } + } + break; + + case RDFTERM_OTHER: + addChildNode(xmp, xmpParent, attribute, attribute.getNodeValue(), isTopLevel); + break; + + default: + throw new XMPException("Invalid nodeElement attribute", BADRDF); + } + + } + } + + + /** + * 7.2.13 propertyEltList + * ws* ( propertyElt ws* )* + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlParent the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_PropertyElementList(XMPMetaImpl xmp, XMPNode xmpParent, Node xmlParent, + boolean isTopLevel) throws XMPException + { + for (int i = 0; i < xmlParent.getChildNodes().getLength(); i++) + { + Node currChild = xmlParent.getChildNodes().item(i); + if (isWhitespaceNode(currChild)) + { + continue; + } + else if (currChild.getNodeType() != Node.ELEMENT_NODE) + { + throw new XMPException("Expected property element node not found", BADRDF); + } + else + { + rdf_PropertyElement(xmp, xmpParent, currChild, isTopLevel); + } + } + } + + + /** + * 7.2.14 propertyElt + * + * resourcePropertyElt | literalPropertyElt | parseTypeLiteralPropertyElt | + * parseTypeResourcePropertyElt | parseTypeCollectionPropertyElt | + * parseTypeOtherPropertyElt | emptyPropertyElt + * + * 7.2.15 resourcePropertyElt + * start-element ( URI == propertyElementURIs, attributes == set ( idAttr? ) ) + * ws* nodeElement ws* + * end-element() + * + * 7.2.16 literalPropertyElt + * start-element ( + * URI == propertyElementURIs, attributes == set ( idAttr?, datatypeAttr?) ) + * text() + * end-element() + * + * 7.2.17 parseTypeLiteralPropertyElt + * start-element ( + * URI == propertyElementURIs, attributes == set ( idAttr?, parseLiteral ) ) + * literal + * end-element() + * + * 7.2.18 parseTypeResourcePropertyElt + * start-element ( + * URI == propertyElementURIs, attributes == set ( idAttr?, parseResource ) ) + * propertyEltList + * end-element() + * + * 7.2.19 parseTypeCollectionPropertyElt + * start-element ( + * URI == propertyElementURIs, attributes == set ( idAttr?, parseCollection ) ) + * nodeElementList + * end-element() + * + * 7.2.20 parseTypeOtherPropertyElt + * start-element ( URI == propertyElementURIs, attributes == set ( idAttr?, parseOther ) ) + * propertyEltList + * end-element() + * + * 7.2.21 emptyPropertyElt + * start-element ( URI == propertyElementURIs, + * attributes == set ( idAttr?, ( resourceAttr | nodeIdAttr )?, propertyAttr* ) ) + * end-element() + * + * The various property element forms are not distinguished by the XML element name, + * but by their attributes for the most part. The exceptions are resourcePropertyElt and + * literalPropertyElt. They are distinguished by their XML element content. + * + * NOTE: The RDF syntax does not explicitly include the xml:lang attribute although it can + * appear in many of these. We have to allow for it in the attibute counts below. + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_PropertyElement(XMPMetaImpl xmp, XMPNode xmpParent, Node xmlNode, + boolean isTopLevel) throws XMPException + { + int nodeTerm = getRDFTermKind (xmlNode); + if (!isPropertyElementName(nodeTerm)) + { + throw new XMPException("Invalid property element name", BADRDF); + } + + // remove the namespace-definitions from the list + NamedNodeMap attributes = xmlNode.getAttributes(); + List nsAttrs = null; + for (int i = 0; i < attributes.getLength(); i++) + { + Node attribute = attributes.item(i); + if ("xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + if (nsAttrs == null) + { + nsAttrs = new ArrayList(); + } + nsAttrs.add(attribute.getNodeName()); + } + } + if (nsAttrs != null) + { + for (Iterator it = nsAttrs.iterator(); it.hasNext();) + { + String ns = (String) it.next(); + attributes.removeNamedItem(ns); + } + } + + + if (attributes.getLength() > 3) + { + // Only an emptyPropertyElt can have more than 3 attributes. + rdf_EmptyPropertyElement(xmp, xmpParent, xmlNode, isTopLevel); + } + else + { + // Look through the attributes for one that isn't rdf:ID or xml:lang, + // it will usually tell what we should be dealing with. + // The called routines must verify their specific syntax! + + for (int i = 0; i < attributes.getLength(); i++) + { + Node attribute = attributes.item(i); + String attrLocal = attribute.getLocalName(); + String attrNS = attribute.getNamespaceURI(); + String attrValue = attribute.getNodeValue(); + if (!(XML_LANG.equals(attribute.getNodeName()) && + !("ID".equals(attrLocal) && NS_RDF.equals(attrNS)))) + { + if ("datatype".equals(attrLocal) && NS_RDF.equals(attrNS)) + { + rdf_LiteralPropertyElement (xmp, xmpParent, xmlNode, isTopLevel); + } + else if (!("parseType".equals(attrLocal) && NS_RDF.equals(attrNS))) + { + rdf_EmptyPropertyElement (xmp, xmpParent, xmlNode, isTopLevel); + } + else if ("Literal".equals(attrValue)) + { + rdf_ParseTypeLiteralPropertyElement(); + } + else if ("Resource".equals(attrValue)) + { + rdf_ParseTypeResourcePropertyElement(xmp, xmpParent, xmlNode, isTopLevel); + } + else if ("Collection".equals(attrValue)) + { + rdf_ParseTypeCollectionPropertyElement(); + } + else + { + rdf_ParseTypeOtherPropertyElement(); + } + + return; + } + } + + // Only rdf:ID and xml:lang, could be a resourcePropertyElt, a literalPropertyElt, + // or an emptyPropertyElt. Look at the child XML nodes to decide which. + + if (xmlNode.hasChildNodes()) + { + for (int i = 0; i < xmlNode.getChildNodes().getLength(); i++) + { + Node currChild = xmlNode.getChildNodes().item(i); + if (currChild.getNodeType() != Node.TEXT_NODE) + { + rdf_ResourcePropertyElement (xmp, xmpParent, xmlNode, isTopLevel); + return; + } + } + + rdf_LiteralPropertyElement (xmp, xmpParent, xmlNode, isTopLevel); + } + else + { + rdf_EmptyPropertyElement (xmp, xmpParent, xmlNode, isTopLevel); + } + } + } + + + /** + * 7.2.15 resourcePropertyElt + * start-element ( URI == propertyElementURIs, attributes == set ( idAttr? ) ) + * ws* nodeElement ws* + * end-element() + * + * This handles structs using an rdf:Description node, + * arrays using rdf:Bag/Seq/Alt, and typedNodes. It also catches and cleans up qualified + * properties written with rdf:Description and rdf:value. + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_ResourcePropertyElement(XMPMetaImpl xmp, XMPNode xmpParent, + Node xmlNode, boolean isTopLevel) throws XMPException + { + if (isTopLevel && "iX:changes".equals(xmlNode.getNodeName())) + { + // Strip old "punchcard" chaff which has on the prefix "iX:". + return; + } + + XMPNode newCompound = addChildNode(xmp, xmpParent, xmlNode, "", isTopLevel); + + // walk through the attributes + for (int i = 0; i < xmlNode.getAttributes().getLength(); i++) + { + Node attribute = xmlNode.getAttributes().item(i); + if ("xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + continue; + } + + String attrLocal = attribute.getLocalName(); + String attrNS = attribute.getNamespaceURI(); + if (XML_LANG.equals(attribute.getNodeName())) + { + addQualifierNode (newCompound, XML_LANG, attribute.getNodeValue()); + } + else if ("ID".equals(attrLocal) && NS_RDF.equals(attrNS)) + { + continue; // Ignore all rdf:ID attributes. + } + else + { + throw new XMPException( + "Invalid attribute for resource property element", BADRDF); + } + } + + // walk through the children + + Node currChild = null; + boolean found = false; + int i; + for (i = 0; i < xmlNode.getChildNodes().getLength(); i++) + { + currChild = xmlNode.getChildNodes().item(i); + if (!isWhitespaceNode(currChild)) + { + if (currChild.getNodeType() == Node.ELEMENT_NODE && !found) + { + boolean isRDF = NS_RDF.equals(currChild.getNamespaceURI()); + String childLocal = currChild.getLocalName(); + + if (isRDF && "Bag".equals(childLocal)) + { + newCompound.getOptions().setArray(true); + } + else if (isRDF && "Seq".equals(childLocal)) + { + newCompound.getOptions().setArray(true).setArrayOrdered(true); + } + else if (isRDF && "Alt".equals(childLocal)) + { + newCompound.getOptions().setArray(true).setArrayOrdered(true) + .setArrayAlternate(true); + } + else + { + newCompound.getOptions().setStruct(true); + if (!isRDF && !"Description".equals(childLocal)) + { + String typeName = currChild.getNamespaceURI(); + if (typeName == null) + { + throw new XMPException( + "All XML elements must be in a namespace", BADXMP); + } + typeName += ':' + childLocal; + addQualifierNode (newCompound, "rdf:type", typeName); + } + } + + rdf_NodeElement (xmp, newCompound, currChild, false); + + if (newCompound.getHasValueChild()) + { + fixupQualifiedNode (newCompound); + } + else if (newCompound.getOptions().isArrayAlternate()) + { + XMPNodeUtils.detectAltText(newCompound); + } + + found = true; + } + else if (found) + { + // found second child element + throw new XMPException( + "Invalid child of resource property element", BADRDF); + } + else + { + throw new XMPException( + "Children of resource property element must be XML elements", BADRDF); + } + } + } + + if (!found) + { + // didn't found any child elements + throw new XMPException("Missing child of resource property element", BADRDF); + } + } + + + /** + * 7.2.16 literalPropertyElt + * start-element ( URI == propertyElementURIs, + * attributes == set ( idAttr?, datatypeAttr?) ) + * text() + * end-element() + * + * Add a leaf node with the text value and qualifiers for the attributes. + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_LiteralPropertyElement(XMPMetaImpl xmp, XMPNode xmpParent, + Node xmlNode, boolean isTopLevel) throws XMPException + { + XMPNode newChild = addChildNode (xmp, xmpParent, xmlNode, null, isTopLevel); + + for (int i = 0; i < xmlNode.getAttributes().getLength(); i++) + { + Node attribute = xmlNode.getAttributes().item(i); + if ("xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + continue; + } + + String attrNS = attribute.getNamespaceURI(); + String attrLocal = attribute.getLocalName(); + if (XML_LANG.equals(attribute.getNodeName())) + { + addQualifierNode(newChild, XML_LANG, attribute.getNodeValue()); + } + else if (NS_RDF.equals(attrNS) && + ("ID".equals(attrLocal) || "datatype".equals(attrLocal))) + { + continue; // Ignore all rdf:ID and rdf:datatype attributes. + } + else + { + throw new XMPException( + "Invalid attribute for literal property element", BADRDF); + } + } + String textValue = ""; + for (int i = 0; i < xmlNode.getChildNodes().getLength(); i++) + { + Node child = xmlNode.getChildNodes().item(i); + if (child.getNodeType() == Node.TEXT_NODE) + { + textValue += child.getNodeValue(); + } + else + { + throw new XMPException("Invalid child of literal property element", BADRDF); + } + } + newChild.setValue(textValue); + } + + + /** + * 7.2.17 parseTypeLiteralPropertyElt + * start-element ( URI == propertyElementURIs, + * attributes == set ( idAttr?, parseLiteral ) ) + * literal + * end-element() + * + * @throws XMPException thown on parsing errors + */ + private static void rdf_ParseTypeLiteralPropertyElement() throws XMPException + { + throw new XMPException("ParseTypeLiteral property element not allowed", BADXMP); + } + + + /** + * 7.2.18 parseTypeResourcePropertyElt + * start-element ( URI == propertyElementURIs, + * attributes == set ( idAttr?, parseResource ) ) + * propertyEltList + * end-element() + * + * Add a new struct node with a qualifier for the possible rdf:ID attribute. + * Then process the XML child nodes to get the struct fields. + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_ParseTypeResourcePropertyElement(XMPMetaImpl xmp, XMPNode xmpParent, + Node xmlNode, boolean isTopLevel) throws XMPException + { + XMPNode newStruct = addChildNode (xmp, xmpParent, xmlNode, "", isTopLevel); + + newStruct.getOptions().setStruct(true); + + for (int i = 0; i < xmlNode.getAttributes().getLength(); i++) + { + Node attribute = xmlNode.getAttributes().item(i); + if ("xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + continue; + } + + String attrLocal = attribute.getLocalName(); + String attrNS = attribute.getNamespaceURI(); + if (XML_LANG.equals(attribute.getNodeName())) + { + addQualifierNode (newStruct, XML_LANG, attribute.getNodeValue()); + } + else if (NS_RDF.equals(attrNS) && + ("ID".equals(attrLocal) || "parseType".equals(attrLocal))) + { + continue; // The caller ensured the value is "Resource". + // Ignore all rdf:ID attributes. + } + else + { + throw new XMPException("Invalid attribute for ParseTypeResource property element", + BADRDF); + } + } + + rdf_PropertyElementList (xmp, newStruct, xmlNode, false); + + if (newStruct.getHasValueChild()) + { + fixupQualifiedNode (newStruct); + } + } + + + /** + * 7.2.19 parseTypeCollectionPropertyElt + * start-element ( URI == propertyElementURIs, + * attributes == set ( idAttr?, parseCollection ) ) + * nodeElementList + * end-element() + * + * @throws XMPException thown on parsing errors + */ + private static void rdf_ParseTypeCollectionPropertyElement() throws XMPException + { + throw new XMPException("ParseTypeCollection property element not allowed", BADXMP); + } + + + /** + * 7.2.20 parseTypeOtherPropertyElt + * start-element ( URI == propertyElementURIs, attributes == set ( idAttr?, parseOther ) ) + * propertyEltList + * end-element() + * + * @throws XMPException thown on parsing errors + */ + private static void rdf_ParseTypeOtherPropertyElement() throws XMPException + { + throw new XMPException("ParseTypeOther property element not allowed", BADXMP); + } + + + /** + * 7.2.21 emptyPropertyElt + * start-element ( URI == propertyElementURIs, + * attributes == set ( + * idAttr?, ( resourceAttr | nodeIdAttr )?, propertyAttr* ) ) + * end-element() + * + * <ns:Prop1/> <!-- a simple property with an empty value --> + * <ns:Prop2 rdf:resource="http: *www.adobe.com/"/> <!-- a URI value --> + * <ns:Prop3 rdf:value="..." ns:Qual="..."/> <!-- a simple qualified property --> + * <ns:Prop4 ns:Field1="..." ns:Field2="..."/> <!-- a struct with simple fields --> + * + * An emptyPropertyElt is an element with no contained content, just a possibly empty set of + * attributes. An emptyPropertyElt can represent three special cases of simple XMP properties: a + * simple property with an empty value (ns:Prop1), a simple property whose value is a URI + * (ns:Prop2), or a simple property with simple qualifiers (ns:Prop3). + * An emptyPropertyElt can also represent an XMP struct whose fields are all simple and + * unqualified (ns:Prop4). + * + * It is an error to use both rdf:value and rdf:resource - that can lead to invalid RDF in the + * verbose form written using a literalPropertyElt. + * + * The XMP mapping for an emptyPropertyElt is a bit different from generic RDF, partly for + * design reasons and partly for historical reasons. The XMP mapping rules are: + * <ol> + * <li> If there is an rdf:value attribute then this is a simple property + * with a text value. + * All other attributes are qualifiers. + * <li> If there is an rdf:resource attribute then this is a simple property + * with a URI value. + * All other attributes are qualifiers. + * <li> If there are no attributes other than xml:lang, rdf:ID, or rdf:nodeID + * then this is a simple + * property with an empty value. + * <li> Otherwise this is a struct, the attributes other than xml:lang, rdf:ID, + * or rdf:nodeID are fields. + * </ol> + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param isTopLevel Flag if the node is a top-level node + * @throws XMPException thown on parsing errors + */ + private static void rdf_EmptyPropertyElement(XMPMetaImpl xmp, XMPNode xmpParent, Node xmlNode, + boolean isTopLevel) throws XMPException + { + boolean hasPropertyAttrs = false; + boolean hasResourceAttr = false; + boolean hasNodeIDAttr = false; + boolean hasValueAttr = false; + + Node valueNode = null; // ! Can come from rdf:value or rdf:resource. + + if (xmlNode.hasChildNodes()) + { + throw new XMPException( + "Nested content not allowed with rdf:resource or property attributes", + BADRDF); + } + + // First figure out what XMP this maps to and remember the XML node for a simple value. + for (int i = 0; i < xmlNode.getAttributes().getLength(); i++) + { + Node attribute = xmlNode.getAttributes().item(i); + if ("xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + continue; + } + + int attrTerm = getRDFTermKind (attribute); + + switch (attrTerm) + { + case RDFTERM_ID : + // Nothing to do. + break; + + case RDFTERM_RESOURCE : + if (hasNodeIDAttr) + { + throw new XMPException( + "Empty property element can't have both rdf:resource and rdf:nodeID", + BADRDF); + } + else if (hasValueAttr) + { + throw new XMPException( + "Empty property element can't have both rdf:value and rdf:resource", + BADXMP); + } + + hasResourceAttr = true; + if (!hasValueAttr) + { + valueNode = attribute; + } + break; + + case RDFTERM_NODE_ID: + if (hasResourceAttr) + { + throw new XMPException( + "Empty property element can't have both rdf:resource and rdf:nodeID", + BADRDF); + } + hasNodeIDAttr = true; + break; + + case RDFTERM_OTHER: + if ("value".equals(attribute.getLocalName()) + && NS_RDF.equals(attribute.getNamespaceURI())) + { + if (hasResourceAttr) + { + throw new XMPException( + "Empty property element can't have both rdf:value and rdf:resource", + BADXMP); + } + hasValueAttr = true; + valueNode = attribute; + } + else if (!XML_LANG.equals(attribute.getNodeName())) + { + hasPropertyAttrs = true; + } + break; + + default: + throw new XMPException("Unrecognized attribute of empty property element", + BADRDF); + } + } + + // Create the right kind of child node and visit the attributes again + // to add the fields or qualifiers. + // ! Because of implementation vagaries, + // the xmpParent is the tree root for top level properties. + // ! The schema is found, created if necessary, by addChildNode. + + XMPNode childNode = addChildNode(xmp, xmpParent, xmlNode, "", isTopLevel); + boolean childIsStruct = false; + + if (hasValueAttr || hasResourceAttr) + { + childNode.setValue(valueNode != null ? valueNode.getNodeValue() : ""); + if (!hasValueAttr) + { + // ! Might have both rdf:value and rdf:resource. + childNode.getOptions().setURI(true); + } + } + else if (hasPropertyAttrs) + { + childNode.getOptions().setStruct(true); + childIsStruct = true; + } + + for (int i = 0; i < xmlNode.getAttributes().getLength(); i++) + { + Node attribute = xmlNode.getAttributes().item(i); + if (attribute == valueNode || + "xmlns".equals(attribute.getPrefix()) || + (attribute.getPrefix() == null && "xmlns".equals(attribute.getNodeName()))) + { + continue; // Skip the rdf:value or rdf:resource attribute holding the value. + } + + int attrTerm = getRDFTermKind (attribute); + + switch (attrTerm) + { + case RDFTERM_ID : + case RDFTERM_NODE_ID : + break; // Ignore all rdf:ID and rdf:nodeID attributes. + + case RDFTERM_RESOURCE : + addQualifierNode(childNode, "rdf:resource", attribute.getNodeValue()); + break; + + case RDFTERM_OTHER : + if (!childIsStruct) + { + addQualifierNode( + childNode, attribute.getNodeName(), attribute.getNodeValue()); + } + else if (XML_LANG.equals(attribute.getNodeName())) + { + addQualifierNode (childNode, XML_LANG, attribute.getNodeValue()); + } + else + { + addChildNode (xmp, childNode, attribute, attribute.getNodeValue(), false); + } + break; + + default : + throw new XMPException("Unrecognized attribute of empty property element", + BADRDF); + } + + } + } + + + /** + * Adds a child node. + * + * @param xmp the xmp metadata object that is generated + * @param xmpParent the parent xmp node + * @param xmlNode the currently processed XML node + * @param value Node value + * @param isTopLevel Flag if the node is a top-level node + * @return Returns the newly created child node. + * @throws XMPException thown on parsing errors + */ + private static XMPNode addChildNode(XMPMetaImpl xmp, XMPNode xmpParent, Node xmlNode, + String value, boolean isTopLevel) throws XMPException + { + XMPSchemaRegistry registry = XMPMetaFactory.getSchemaRegistry(); + String namespace = xmlNode.getNamespaceURI(); + String childName; + if (namespace != null) + { + if (NS_DC_DEPRECATED.equals(namespace)) + { + // Fix a legacy DC namespace + namespace = NS_DC; + } + + String prefix = registry.getNamespacePrefix(namespace); + if (prefix == null) + { + prefix = xmlNode.getPrefix() != null ? xmlNode.getPrefix() : DEFAULT_PREFIX; + prefix = registry.registerNamespace(namespace, prefix); + } + childName = prefix + xmlNode.getLocalName(); + } + else + { + throw new XMPException( + "XML namespace required for all elements and attributes", BADRDF); + } + + + // create schema node if not already there + PropertyOptions childOptions = new PropertyOptions(); + boolean isAlias = false; + if (isTopLevel) + { + // Lookup the schema node, adjust the XMP parent pointer. + // Incoming parent must be the tree root. + XMPNode schemaNode = XMPNodeUtils.findSchemaNode(xmp.getRoot(), namespace, + DEFAULT_PREFIX, true); + schemaNode.setImplicit(false); // Clear the implicit node bit. + // need runtime check for proper 32 bit code. + xmpParent = schemaNode; + + // If this is an alias set the alias flag in the node + // and the hasAliases flag in the tree. + if (registry.findAlias(childName) != null) + { + isAlias = true; + xmp.getRoot().setHasAliases(true); + schemaNode.setHasAliases(true); + } + } + + + // Make sure that this is not a duplicate of a named node. + boolean isArrayItem = "rdf:li".equals(childName); + boolean isValueNode = "rdf:value".equals(childName); + + // Create XMP node and so some checks + XMPNode newChild = new XMPNode( + childName, value, childOptions); + newChild.setAlias(isAlias); + + // Add the new child to the XMP parent node, a value node first. + if (!isValueNode) + { + xmpParent.addChild(newChild); + } + else + { + xmpParent.addChild(1, newChild); + } + + + if (isValueNode) + { + if (isTopLevel || !xmpParent.getOptions().isStruct()) + { + throw new XMPException("Misplaced rdf:value element", BADRDF); + } + xmpParent.setHasValueChild(true); + } + + if (isArrayItem) + { + if (!xmpParent.getOptions().isArray()) + { + throw new XMPException("Misplaced rdf:li element", BADRDF); + } + newChild.setName(ARRAY_ITEM_NAME); + } + + return newChild; + } + + + /** + * Adds a qualifier node. + * + * @param xmpParent the parent xmp node + * @param name the name of the qualifier which has to be + * QName including the <b>default prefix</b> + * @param value the value of the qualifier + * @return Returns the newly created child node. + * @throws XMPException thown on parsing errors + */ + private static XMPNode addQualifierNode(XMPNode xmpParent, String name, String value) + throws XMPException + { + boolean isLang = XML_LANG.equals(name); + + XMPNode newQual = null; + + // normalize value of language qualifiers + newQual = new XMPNode(name, isLang ? Utils.normalizeLangValue(value) : value, null); + xmpParent.addQualifier(newQual); + + return newQual; + } + + + /** + * The parent is an RDF pseudo-struct containing an rdf:value field. Fix the + * XMP data model. The rdf:value node must be the first child, the other + * children are qualifiers. The form, value, and children of the rdf:value + * node are the real ones. The rdf:value node's qualifiers must be added to + * the others. + * + * @param xmpParent the parent xmp node + * @throws XMPException thown on parsing errors + */ + private static void fixupQualifiedNode(XMPNode xmpParent) throws XMPException + { + assert xmpParent.getOptions().isStruct() && xmpParent.hasChildren(); + + XMPNode valueNode = xmpParent.getChild(1); + assert "rdf:value".equals(valueNode.getName()); + + // Move the qualifiers on the value node to the parent. + // Make sure an xml:lang qualifier stays at the front. + // Check for duplicate names between the value node's qualifiers and the parent's children. + // The parent's children are about to become qualifiers. Check here, between the groups. + // Intra-group duplicates are caught by XMPNode#addChild(...). + if (valueNode.getOptions().getHasLanguage()) + { + if (xmpParent.getOptions().getHasLanguage()) + { + throw new XMPException("Redundant xml:lang for rdf:value element", + BADXMP); + } + XMPNode langQual = valueNode.getQualifier(1); + valueNode.removeQualifier(langQual); + xmpParent.addQualifier(langQual); + } + + // Start the remaining copy after the xml:lang qualifier. + for (int i = 1; i <= valueNode.getQualifierLength(); i++) + { + XMPNode qualifier = valueNode.getQualifier(i); + xmpParent.addQualifier(qualifier); + } + + + // Change the parent's other children into qualifiers. + // This loop starts at 1, child 0 is the rdf:value node. + for (int i = 2; i <= xmpParent.getChildrenLength(); i++) + { + XMPNode qualifier = xmpParent.getChild(i); + xmpParent.addQualifier(qualifier); + } + + // Move the options and value last, other checks need the parent's original options. + // Move the value node's children to be the parent's children. + assert xmpParent.getOptions().isStruct() || xmpParent.getHasValueChild(); + + xmpParent.setHasValueChild(false); + xmpParent.getOptions().setStruct(false); + xmpParent.getOptions().mergeWith(valueNode.getOptions()); + xmpParent.setValue(valueNode.getValue()); + + xmpParent.removeChildren(); + for (Iterator it = valueNode.iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + xmpParent.addChild(child); + } + } + + + /** + * Checks if the node is a white space. + * @param node an XML-node + * @return Returns whether the node is a whitespace node, + * i.e. a text node that contains only whitespaces. + */ + private static boolean isWhitespaceNode(Node node) + { + if (node.getNodeType() != Node.TEXT_NODE) + { + return false; + } + + String value = node.getNodeValue(); + for (int i = 0; i < value.length(); i++) + { + if (!Character.isWhitespace(value.charAt(i))) + { + return false; + } + } + + return true; + } + + + /** + * 7.2.6 propertyElementURIs + * anyURI - ( coreSyntaxTerms | rdf:Description | oldTerms ) + * + * @param term the term id + * @return Return true if the term is a property element name. + */ + private static boolean isPropertyElementName(int term) + { + if (term == RDFTERM_DESCRIPTION || isOldTerm(term)) + { + return false; + } + else + { + return (!isCoreSyntaxTerm(term)); + } + } + + + /** + * 7.2.4 oldTerms<br> + * rdf:aboutEach | rdf:aboutEachPrefix | rdf:bagID + * + * @param term the term id + * @return Returns true if the term is an old term. + */ + private static boolean isOldTerm(int term) + { + return RDFTERM_FIRST_OLD <= term && term <= RDFTERM_LAST_OLD; + } + + + /** + * 7.2.2 coreSyntaxTerms<br> + * rdf:RDF | rdf:ID | rdf:about | rdf:parseType | rdf:resource | rdf:nodeID | + * rdf:datatype + * + * @param term the term id + * @return Return true if the term is a core syntax term + */ + private static boolean isCoreSyntaxTerm(int term) + { + return RDFTERM_FIRST_CORE <= term && term <= RDFTERM_LAST_CORE; + } + + + /** + * Determines the ID for a certain RDF Term. + * Arranged to hopefully minimize the parse time for large XMP. + * + * @param node an XML node + * @return Returns the term ID. + */ + private static int getRDFTermKind(Node node) + { + String localName = node.getLocalName(); + String namespace = node.getNamespaceURI(); + + if ( + namespace == null && + ("about".equals(localName) || "ID".equals(localName)) && + (node instanceof Attr) && + NS_RDF.equals(((Attr) node).getOwnerElement().getNamespaceURI()) + ) + { + namespace = NS_RDF; + } + + if (NS_RDF.equals(namespace)) + { + if ("li".equals(localName)) + { + return RDFTERM_LI; + } + else if ("parseType".equals(localName)) + { + return RDFTERM_PARSE_TYPE; + } + else if ("Description".equals(localName)) + { + return RDFTERM_DESCRIPTION; + } + else if ("about".equals(localName)) + { + return RDFTERM_ABOUT; + } + else if ("resource".equals(localName)) + { + return RDFTERM_RESOURCE; + } + else if ("RDF".equals(localName)) + { + return RDFTERM_RDF; + } + else if ("ID".equals(localName)) + { + return RDFTERM_ID; + } + else if ("nodeID".equals(localName)) + { + return RDFTERM_NODE_ID; + } + else if ("datatype".equals(localName)) + { + return RDFTERM_DATATYPE; + } + else if ("aboutEach".equals(localName)) + { + return RDFTERM_ABOUT_EACH; + } + else if ("aboutEachPrefix".equals(localName)) + { + return RDFTERM_ABOUT_EACH_PREFIX; + } + else if ("bagID".equals(localName)) + { + return RDFTERM_BAG_ID; + } + } + + return RDFTERM_OTHER; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/QName.java b/XMPCore/src/com/adobe/xmp/impl/QName.java new file mode 100644 index 0000000..783f792 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/QName.java @@ -0,0 +1,80 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +/** + * @since 09.11.2006 + */ +public class QName +{ + /** XML namespace prefix */ + private String prefix; + /** XML localname */ + private String localName; + + + /** + * Splits a qname into prefix and localname. + * @param qname a QName + */ + public QName(String qname) + { + int colon = qname.indexOf(':'); + + if (colon >= 0) + { + prefix = qname.substring(0, colon); + localName = qname.substring(colon + 1); + } + else + { + prefix = ""; + localName = qname; + } + } + + + /** Constructor that initializes the fields + * @param prefix the prefix + * @param localName the name + */ + public QName(String prefix, String localName) + { + this.prefix = prefix; + this.localName = localName; + } + + + /** + * @return Returns whether the QName has a prefix. + */ + public boolean hasPrefix() + { + return prefix != null && prefix.length() > 0; + } + + + /** + * @return the localName + */ + public String getLocalName() + { + return localName; + } + + + /** + * @return the prefix + */ + public String getPrefix() + { + return prefix; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/Utils.java b/XMPCore/src/com/adobe/xmp/impl/Utils.java new file mode 100644 index 0000000..7ca3b27 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/Utils.java @@ -0,0 +1,511 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + + +import com.adobe.xmp.XMPConst; + + +/** + * Utility functions for the XMPToolkit implementation. + * + * @since 06.06.2006 + */ +public class Utils implements XMPConst +{ + /** segments of a UUID */ + public static final int UUID_SEGMENT_COUNT = 4; + /** length of a UUID */ + public static final int UUID_LENGTH = 32 + UUID_SEGMENT_COUNT; + /** table of XML name start chars (<= 0xFF) */ + private static boolean[] xmlNameStartChars; + /** table of XML name chars (<= 0xFF) */ + private static boolean[] xmlNameChars; + /** init char tables */ + static + { + initCharTables(); + } + + + /** + * Private constructor + */ + private Utils() + { + // EMPTY + } + + + /** + * Normalize an xml:lang value so that comparisons are effectively case + * insensitive as required by RFC 3066 (which superceeds RFC 1766). The + * normalization rules: + * <ul> + * <li> The primary subtag is lower case, the suggested practice of ISO 639. + * <li> All 2 letter secondary subtags are upper case, the suggested + * practice of ISO 3166. + * <li> All other subtags are lower case. + * </ul> + * + * @param value + * raw value + * @return Returns the normalized value. + */ + public static String normalizeLangValue(String value) + { + // don't normalize x-default + if (XMPConst.X_DEFAULT.equals(value)) + { + return value; + } + + int subTag = 1; + StringBuffer buffer = new StringBuffer(); + + for (int i = 0; i < value.length(); i++) + { + switch (value.charAt(i)) + { + case '-': + case '_': + // move to next subtag and convert underscore to hyphen + buffer.append('-'); + subTag++; + break; + case ' ': + // remove spaces + break; + default: + // convert second subtag to uppercase, all other to lowercase + if (subTag != 2) + { + buffer.append(Character.toLowerCase(value.charAt(i))); + } + else + { + buffer.append(Character.toUpperCase(value.charAt(i))); + } + } + + } + return buffer.toString(); + } + + + /** + * Split the name and value parts for field and qualifier selectors: + * <ul> + * <li>[qualName="value"] - An element in an array of structs, chosen by a + * field value. + * <li>[?qualName="value"] - An element in an array, chosen by a qualifier + * value. + * </ul> + * The value portion is a string quoted by ''' or '"'. The value may contain + * any character including a doubled quoting character. The value may be + * empty. <em>Note:</em> It is assumed that the expression is formal + * correct + * + * @param selector + * the selector + * @return Returns an array where the first entry contains the name and the + * second the value. + */ + static String[] splitNameAndValue(String selector) + { + // get the name + int eq = selector.indexOf('='); + int pos = 1; + if (selector.charAt(pos) == '?') + { + pos++; + } + String name = selector.substring(pos, eq); + + // get the value + pos = eq + 1; + char quote = selector.charAt(pos); + pos++; + int end = selector.length() - 2; // quote and ] + StringBuffer value = new StringBuffer(end - eq); + while (pos < end) + { + value.append(selector.charAt(pos)); + pos++; + if (selector.charAt(pos) == quote) + { + // skip one quote in value + pos++; + } + } + return new String[] { name, value.toString() }; + } + + + /** + * + * @param schema + * a schema namespace + * @param prop + * an XMP Property + * @return Returns true if the property is defined as "Internal + * Property", see XMP Specification. + */ + static boolean isInternalProperty(String schema, String prop) + { + boolean isInternal = false; + + if (NS_DC.equals(schema)) + { + if ("dc:format".equals(prop) || "dc:language".equals(prop)) + { + isInternal = true; + } + } + else if (NS_XMP.equals(schema)) + { + if ("xmp:BaseURL".equals(prop) || "xmp:CreatorTool".equals(prop) + || "xmp:Format".equals(prop) || "xmp:Locale".equals(prop) + || "xmp:MetadataDate".equals(prop) || "xmp:ModifyDate".equals(prop)) + { + isInternal = true; + } + } + else if (NS_PDF.equals(schema)) + { + if ("pdf:BaseURL".equals(prop) || "pdf:Creator".equals(prop) + || "pdf:ModDate".equals(prop) || "pdf:PDFVersion".equals(prop) + || "pdf:Producer".equals(prop)) + { + isInternal = true; + } + } + else if (NS_TIFF.equals(schema)) + { + isInternal = true; + if ("tiff:ImageDescription".equals(prop) || "tiff:Artist".equals(prop) + || "tiff:Copyright".equals(prop)) + { + isInternal = false; + } + } + else if (NS_EXIF.equals(schema)) + { + isInternal = true; + if ("exif:UserComment".equals(prop)) + { + isInternal = false; + } + } + else if (NS_EXIF_AUX.equals(schema)) + { + isInternal = true; + } + else if (NS_PHOTOSHOP.equals(schema)) + { + if ("photoshop:ICCProfile".equals(prop)) + { + isInternal = true; + } + } + else if (NS_CAMERARAW.equals(schema)) + { + if ("crs:Version".equals(prop) || "crs:RawFileName".equals(prop) + || "crs:ToneCurveName".equals(prop)) + { + isInternal = true; + } + } + else if (NS_ADOBESTOCKPHOTO.equals(schema)) + { + isInternal = true; + } + else if (NS_XMP_MM.equals(schema)) + { + isInternal = true; + } + else if (TYPE_TEXT.equals(schema)) + { + isInternal = true; + } + else if (TYPE_PAGEDFILE.equals(schema)) + { + isInternal = true; + } + else if (TYPE_GRAPHICS.equals(schema)) + { + isInternal = true; + } + else if (TYPE_IMAGE.equals(schema)) + { + isInternal = true; + } + else if (TYPE_FONT.equals(schema)) + { + isInternal = true; + } + + return isInternal; + } + + + /** + * Check some requirements for an UUID: + * <ul> + * <li>Length of the UUID is 32</li> + * <li>The Delimiter count is 4 and all the 4 delimiter are on their right + * position (8,13,18,23)</li> + * </ul> + * + * + * @param uuid uuid to test + * @return true - this is a well formed UUID, false - UUID has not the expected format + */ + + static boolean checkUUIDFormat(String uuid) + { + boolean result = true; + int delimCnt = 0; + int delimPos = 0; + + if (uuid == null) + { + return false; + } + + for (delimPos = 0; delimPos < uuid.length(); delimPos++) + { + if (uuid.charAt(delimPos) == '-') + { + delimCnt++; + result = result && + (delimPos == 8 || delimPos == 13 || delimPos == 18 || delimPos == 23); + } + } + + return result && UUID_SEGMENT_COUNT == delimCnt && UUID_LENGTH == delimPos; + } + + + /** + * Simple check for valid XMLNames. Within ASCII range<br> + * ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6]<br> + * are accepted, above all characters (which is not entirely + * correct according to the XML Spec. + * + * @param name an XML Name + * @return Return <code>true</code> if the name is correct. + */ + public static boolean isXMLName(String name) + { + if (name.length() > 0 && !isNameStartChar(name.charAt(0))) + { + return false; + } + for (int i = 1; i < name.length(); i++) + { + if (!isNameChar(name.charAt(i))) + { + return false; + } + } + return true; + } + + + /** + * Checks if the value is a legal "unqualified" XML name, as + * defined in the XML Namespaces proposed recommendation. + * These are XML names, except that they must not contain a colon. + * @param name the value to check + * @return Returns true if the name is a valid "unqualified" XML name. + */ + public static boolean isXMLNameNS(String name) + { + if (name.length() > 0 && (!isNameStartChar(name.charAt(0)) || name.charAt(0) == ':')) + { + return false; + } + for (int i = 1; i < name.length(); i++) + { + if (!isNameChar(name.charAt(i)) || name.charAt(i) == ':') + { + return false; + } + } + return true; + } + + + /** + * @param c a char + * @return Returns true if the char is an ASCII control char. + */ + static boolean isControlChar(char c) + { + return (c <= 0x1F || c == 0x7F) && + c != 0x09 && c != 0x0A && c != 0x0D; + } + + + /** + * Serializes the node value in XML encoding. Its used for tag bodies and + * attributes.<br> + * <em>Note:</em> The attribute is always limited by quotes, + * thats why <code>&apos;</code> is never serialized.<br> + * <em>Note:</em> Control chars are written unescaped, but if the user uses others than tab, LF + * and CR the resulting XML will become invalid. + * @param value a string + * @param forAttribute flag if string is attribute value (need to additional escape quotes) + * @param escapeWhitespaces Decides if LF, CR and TAB are escaped. + * @return Returns the value ready for XML output. + */ + public static String escapeXML(String value, boolean forAttribute, boolean escapeWhitespaces) + { + // quick check if character are contained that need special treatment + boolean needsEscaping = false; + for (int i = 0; i < value.length (); i++) + { + char c = value.charAt (i); + if ( + c == '<' || c == '>' || c == '&' || // XML chars + (escapeWhitespaces && (c == '\t' || c == '\n' || c == '\r')) || + (forAttribute && c == '"')) + { + needsEscaping = true; + break; + } + } + + if (!needsEscaping) + { + // fast path + return value; + } + else + { + // slow path with escaping + StringBuffer buffer = new StringBuffer(value.length() * 4 / 3); + for (int i = 0; i < value.length (); i++) + { + char c = value.charAt (i); + if (!(escapeWhitespaces && (c == '\t' || c == '\n' || c == '\r'))) + { + switch (c) + { + // we do what "Canonical XML" expects + // AUDIT: ' not serialized as only outer qoutes are used + case '<': buffer.append("<"); continue; + case '>': buffer.append(">"); continue; + case '&': buffer.append("&"); continue; + case '"': buffer.append(forAttribute ? """ : "\""); continue; + default: buffer.append(c); continue; + } + } + else + { + // write control chars escaped, + // if there are others than tab, LF and CR the xml will become invalid. + buffer.append("&#x"); + buffer.append(Integer.toHexString(c).toUpperCase()); + buffer.append(';'); + } + } + return buffer.toString(); + } + } + + + /** + * Replaces the ASCII control chars with a space. + * + * @param value + * a node value + * @return Returns the cleaned up value + */ + static String removeControlChars(String value) + { + StringBuffer buffer = new StringBuffer(value); + for (int i = 0; i < buffer.length(); i++) + { + if (isControlChar(buffer.charAt(i))) + { + buffer.setCharAt(i, ' '); + } + } + return buffer.toString(); + } + + + /** + * Simple check if a character is a valid XML start name char. + * Within ASCII range<br> + * ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6]<br> + * are accepted, above all characters (which is not entirely + * correct according to the XML Spec) + * + * @param ch a character + * @return Returns true if the character is a valid first char of an XML name. + */ + private static boolean isNameStartChar(char ch) + { + return ch > 0xFF || xmlNameStartChars[ch]; + } + + + /** + * Simple check if a character is a valid XML name char + * (every char except the first one). + * Within ASCII range<br> + * ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6]<br> + * are accepted, above all characters (which is not entirely + * correct according to the XML Spec) + * + * @param ch a character + * @return Returns true if the character is a valid char of an XML name. + */ + private static boolean isNameChar(char ch) + { + return ch > 0xFF || xmlNameChars[ch]; + } + + + /** + * Initializes the char tables for later use. + */ + private static void initCharTables() + { + xmlNameChars = new boolean[0x0100]; + xmlNameStartChars = new boolean[0x0100]; + + for (char ch = 0; ch < xmlNameChars.length; ch++) + { + xmlNameStartChars[ch] = + ('a' <= ch && ch <= 'z') || + ('A' <= ch && ch <= 'Z') || + ch == ':' || + ch == '_' || + (0xC0 <= ch && ch <= 0xD6) || + (0xD8 <= ch && ch <= 0xF6); + + xmlNameChars[ch] = + ('a' <= ch && ch <= 'z') || + ('A' <= ch && ch <= 'Z') || + ('0' <= ch && ch <= '9') || + ch == ':' || + ch == '_' || + ch == '-' || + ch == '.' || + ch == 0xB7 || + (0xC0 <= ch && ch <= 0xD6) || + (0xD8 <= ch && ch <= 0xF6); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java b/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java new file mode 100644 index 0000000..f3a17e7 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java @@ -0,0 +1,346 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import com.adobe.xmp.XMPDateTime; +import com.adobe.xmp.XMPException; + + +/** + * The implementation of <code>XMPDateTime</code>. Internally a <code>calendar</code> is used + * plus an additional nano seconds field, because <code>Calendar</code> supports only milli + * seconds. The <code>nanoSeconds</code> convers only the resolution beyond a milli second. + * + * @since 16.02.2006 + */ +public class XMPDateTimeImpl implements XMPDateTime +{ + /** */ + private int year = 0; + /** */ + private int month = 0; + /** */ + private int day = 0; + /** */ + private int hour = 0; + /** */ + private int minute = 0; + /** */ + private int second = 0; + /** Use the unversal time as default */ + private TimeZone timeZone = TimeZone.getTimeZone("UTC"); + /** + * The nano seconds take micro and nano seconds, while the milli seconds are in the calendar. + */ + private int nanoSeconds; + + + /** + * Creates an <code>XMPDateTime</code>-instance with the current time in the default time + * zone. + */ + public XMPDateTimeImpl() + { + // EMPTY + } + + + /** + * Creates an <code>XMPDateTime</code>-instance from a calendar. + * + * @param calendar a <code>Calendar</code> + */ + public XMPDateTimeImpl(Calendar calendar) + { + // extract the date and timezone from the calendar provided + Date date = calendar.getTime(); + TimeZone zone = calendar.getTimeZone(); + + // put that date into a calendar the pretty much represents ISO8601 + // I use US because it is close to the "locale" for the ISO8601 spec + GregorianCalendar intCalendar = + (GregorianCalendar) Calendar.getInstance(Locale.US); + intCalendar.setGregorianChange(new Date(Long.MIN_VALUE)); + intCalendar.setTimeZone(zone); + intCalendar.setTime(date); + + this.year = intCalendar.get(Calendar.YEAR); + this.month = intCalendar.get(Calendar.MONTH) + 1; // cal is from 0..12 + this.day = intCalendar.get(Calendar.DAY_OF_MONTH); + this.hour = intCalendar.get(Calendar.HOUR_OF_DAY); + this.minute = intCalendar.get(Calendar.MINUTE); + this.second = intCalendar.get(Calendar.SECOND); + this.nanoSeconds = intCalendar.get(Calendar.MILLISECOND) * 1000000; + this.timeZone = intCalendar.getTimeZone(); + } + + + /** + * Creates an <code>XMPDateTime</code>-instance from + * a <code>Date</code> and a <code>TimeZone</code>. + * + * @param date a date describing an absolute point in time + * @param timeZone a TimeZone how to interpret the date + */ + public XMPDateTimeImpl(Date date, TimeZone timeZone) + { + GregorianCalendar calendar = new GregorianCalendar(timeZone); + calendar.setTime(date); + this.year = calendar.get(Calendar.YEAR); + this.month = calendar.get(Calendar.MONTH) + 1; // cal is from 0..12 + this.day = calendar.get(Calendar.DAY_OF_MONTH); + this.hour = calendar.get(Calendar.HOUR_OF_DAY); + this.minute = calendar.get(Calendar.MINUTE); + this.second = calendar.get(Calendar.SECOND); + this.nanoSeconds = calendar.get(Calendar.MILLISECOND) * 1000000; + this.timeZone = timeZone; + } + + + /** + * Creates an <code>XMPDateTime</code>-instance from an ISO 8601 string. + * + * @param strValue an ISO 8601 string + * @throws XMPException If the string is a non-conform ISO 8601 string, an exception is thrown + */ + public XMPDateTimeImpl(String strValue) throws XMPException + { + ISO8601Converter.parse(strValue, this); + } + + + /** + * @see XMPDateTime#getYear() + */ + public int getYear() + { + return year; + } + + + /** + * @see XMPDateTime#setYear(int) + */ + public void setYear(int year) + { + this.year = Math.min(Math.abs(year), 9999); + } + + + /** + * @see XMPDateTime#getMonth() + */ + public int getMonth() + { + return month; + } + + + /** + * @see XMPDateTime#setMonth(int) + */ + public void setMonth(int month) + { + if (month < 1) + { + this.month = 1; + } + else if (month > 12) + { + this.month = 12; + } + else + { + this.month = month; + } + } + + + /** + * @see XMPDateTime#getDay() + */ + public int getDay() + { + return day; + } + + + /** + * @see XMPDateTime#setDay(int) + */ + public void setDay(int day) + { + if (day < 1) + { + this.day = 1; + } + else if (day > 31) + { + this.day = 31; + } + else + { + this.day = day; + } + } + + + /** + * @see XMPDateTime#getHour() + */ + public int getHour() + { + return hour; + } + + + /** + * @see XMPDateTime#setHour(int) + */ + public void setHour(int hour) + { + this.hour = Math.min(Math.abs(hour), 23); + } + + + /** + * @see XMPDateTime#getMinute() + */ + public int getMinute() + { + return minute; + } + + + /** + * @see XMPDateTime#setMinute(int) + */ + public void setMinute(int minute) + { + this.minute = Math.min(Math.abs(minute), 59); + } + + + /** + * @see XMPDateTime#getSecond() + */ + public int getSecond() + { + return second; + } + + + /** + * @see XMPDateTime#setSecond(int) + */ + public void setSecond(int second) + { + this.second = Math.min(Math.abs(second), 59); + } + + + /** + * @see XMPDateTime#getNanoSecond() + */ + public int getNanoSecond() + { + return nanoSeconds; + } + + + /** + * @see XMPDateTime#setNanoSecond(int) + */ + public void setNanoSecond(int nanoSecond) + { + this.nanoSeconds = nanoSecond; + } + + + /** + * @see Comparable#compareTo(Object) + */ + public int compareTo(Object dt) + { + long d = getCalendar().getTimeInMillis() + - ((XMPDateTime) dt).getCalendar().getTimeInMillis(); + if (d != 0) + { + return (int) (d % 2); + } + else + { + // if millis are equal, compare nanoseconds + d = nanoSeconds - ((XMPDateTime) dt).getNanoSecond(); + return (int) (d % 2); + } + } + + + /** + * @see XMPDateTime#getTimeZone() + */ + public TimeZone getTimeZone() + { + return timeZone; + } + + + /** + * @see XMPDateTime#setTimeZone(TimeZone) + */ + public void setTimeZone(TimeZone timeZone) + { + this.timeZone = timeZone; + } + + + /** + * @see XMPDateTime#getCalendar() + */ + public Calendar getCalendar() + { + GregorianCalendar calendar = (GregorianCalendar) Calendar.getInstance(Locale.US); + calendar.setGregorianChange(new Date(Long.MIN_VALUE)); + calendar.setTimeZone(timeZone); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, nanoSeconds / 1000000); + return calendar; + } + + + /** + * @see XMPDateTime#getISO8601String() + */ + public String getISO8601String() + { + return ISO8601Converter.render(this); + } + + + /** + * @return Returns the ISO string representation. + */ + public String toString() + { + return getISO8601String(); + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java b/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java new file mode 100644 index 0000000..22ddebb --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java @@ -0,0 +1,598 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPIterator; +import com.adobe.xmp.impl.xpath.XMPPath; +import com.adobe.xmp.impl.xpath.XMPPathParser; +import com.adobe.xmp.options.IteratorOptions; +import com.adobe.xmp.options.PropertyOptions; +import com.adobe.xmp.properties.XMPPropertyInfo; + + +/** + * The <code>XMPIterator</code> implementation. + * Iterates the XMP Tree according to a set of options. + * During the iteration the XMPMeta-object must not be changed. + * Calls to <code>skipSubtree()</code> / <code>skipSiblings()</code> will affect the iteration. + * + * @since 29.06.2006 + */ +public class XMPIteratorImpl implements XMPIterator +{ + /** stores the iterator options */ + private IteratorOptions options; + /** the base namespace of the property path, will be changed during the iteration */ + private String baseNS = null; + /** flag to indicate that skipSiblings() has been called. */ + protected boolean skipSiblings = false; + /** flag to indicate that skipSiblings() has been called. */ + protected boolean skipSubtree = false; + /** the node iterator doing the work */ + private Iterator nodeIterator = null; + + + /** + * Constructor with optionsl initial values. If <code>propName</code> is provided, + * <code>schemaNS</code> has also be provided. + * @param xmp the iterated metadata object. + * @param schemaNS the iteration is reduced to this schema (optional) + * @param propPath the iteration is redurce to this property within the <code>schemaNS</code> + * @param options advanced iteration options, see {@link IteratorOptions} + * @throws XMPException If the node defined by the paramters is not existing. + */ + public XMPIteratorImpl(XMPMetaImpl xmp, String schemaNS, String propPath, + IteratorOptions options) throws XMPException + { + // make sure that options is defined at least with defaults + this.options = options != null ? options : new IteratorOptions(); + + // the start node of the iteration depending on the schema and property filter + XMPNode startNode = null; + String initialPath = null; + boolean baseSchema = schemaNS != null && schemaNS.length() > 0; + boolean baseProperty = propPath != null && propPath.length() > 0; + + if (!baseSchema && !baseProperty) + { + // complete tree will be iterated + startNode = xmp.getRoot(); + } + else if (baseSchema && baseProperty) + { + // Schema and property node provided + XMPPath path = XMPPathParser.expandXPath(schemaNS, propPath); + + // base path is the prop path without the property leaf + XMPPath basePath = new XMPPath(); + for (int i = 0; i < path.size() - 1; i++) + { + basePath.add(path.getSegment(i)); + } + + startNode = XMPNodeUtils.findNode(xmp.getRoot(), path, false, null); + baseNS = schemaNS; + initialPath = basePath.toString(); + } + else if (baseSchema && !baseProperty) + { + // Only Schema provided + startNode = XMPNodeUtils.findSchemaNode(xmp.getRoot(), schemaNS, false); + } + else // !baseSchema && baseProperty + { + // No schema but property provided -> error + throw new XMPException("Schema namespace URI is required", XMPError.BADSCHEMA); + } + + + // create iterator + if (startNode != null) + { + if (!this.options.isJustChildren()) + { + nodeIterator = new NodeIterator(startNode, initialPath, 1); + } + else + { + nodeIterator = new NodeIteratorChildren(startNode, initialPath); + } + } + else + { + // create null iterator + nodeIterator = Collections.EMPTY_LIST.iterator(); + } + } + + + /** + * @see XMPIterator#skipSubtree() + */ + public void skipSubtree() + { + this.skipSubtree = true; + } + + + /** + * @see XMPIterator#skipSiblings() + */ + public void skipSiblings() + { + skipSubtree(); + this.skipSiblings = true; + } + + + /** + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() + { + return nodeIterator.hasNext(); + } + + + /** + * @see java.util.Iterator#next() + */ + public Object next() + { + return nodeIterator.next(); + } + + + /** + * @see java.util.Iterator#remove() + */ + public void remove() + { + throw new UnsupportedOperationException("The XMPIterator does not support remove()."); + } + + + /** + * @return Exposes the options for inner class. + */ + protected IteratorOptions getOptions() + { + return options; + } + + + /** + * @return Exposes the options for inner class. + */ + protected String getBaseNS() + { + return baseNS; + } + + + /** + * @param baseNS sets the baseNS from the inner class. + */ + protected void setBaseNS(String baseNS) + { + this.baseNS = baseNS; + } + + + + + + + /** + * The <code>XMPIterator</code> implementation. + * It first returns the node itself, then recursivly the children and qualifier of the node. + * + * @since 29.06.2006 + */ + private class NodeIterator implements Iterator + { + /** iteration state */ + protected static final int ITERATE_NODE = 0; + /** iteration state */ + protected static final int ITERATE_CHILDREN = 1; + /** iteration state */ + protected static final int ITERATE_QUALIFIER = 2; + + /** the state of the iteration */ + private int state = ITERATE_NODE; + /** the currently visited node */ + private XMPNode visitedNode; + /** the recursively accumulated path */ + private String path; + /** the iterator that goes through the children and qualifier list */ + private Iterator childrenIterator = null; + /** index of node with parent, only interesting for arrays */ + private int index = 0; + /** the iterator for each child */ + private Iterator subIterator = Collections.EMPTY_LIST.iterator(); + /** the cached <code>PropertyInfo</code> to return */ + private XMPPropertyInfo returnProperty = null; + + + /** + * Default constructor + */ + public NodeIterator() + { + // EMPTY + } + + + /** + * Constructor for the node iterator. + * @param visitedNode the currently visited node + * @param parentPath the accumulated path of the node + * @param index the index within the parent node (only for arrays) + */ + public NodeIterator(XMPNode visitedNode, String parentPath, int index) + { + this.visitedNode = visitedNode; + this.state = NodeIterator.ITERATE_NODE; + if (visitedNode.getOptions().isSchemaNode()) + { + setBaseNS(visitedNode.getName()); + } + + // for all but the root node and schema nodes + path = accumulatePath(visitedNode, parentPath, index); + } + + + /** + * Prepares the next node to return if not already done. + * + * @see Iterator#hasNext() + */ + public boolean hasNext() + { + if (returnProperty != null) + { + // hasNext has been called before + return true; + } + + // find next node + if (state == ITERATE_NODE) + { + return reportNode(); + } + else if (state == ITERATE_CHILDREN) + { + if (childrenIterator == null) + { + childrenIterator = visitedNode.iterateChildren(); + } + + boolean hasNext = iterateChildren(childrenIterator); + + if (!hasNext && visitedNode.hasQualifier() && !getOptions().isOmitQualifiers()) + { + state = ITERATE_QUALIFIER; + childrenIterator = null; + hasNext = hasNext(); + } + return hasNext; + } + else + { + if (childrenIterator == null) + { + childrenIterator = visitedNode.iterateQualifier(); + } + + return iterateChildren(childrenIterator); + } + } + + + /** + * Sets the returnProperty as next item or recurses into <code>hasNext()</code>. + * @return Returns if there is a next item to return. + */ + protected boolean reportNode() + { + state = ITERATE_CHILDREN; + if (visitedNode.getParent() != null && + (!getOptions().isJustLeafnodes() || !visitedNode.hasChildren())) + { + returnProperty = createPropertyInfo(visitedNode, getBaseNS(), path); + return true; + } + else + { + return hasNext(); + } + } + + + /** + * Handles the iteration of the children or qualfier + * @param iterator an iterator + * @return Returns if there are more elements available. + */ + private boolean iterateChildren(Iterator iterator) + { + if (skipSiblings) + { + // setSkipSiblings(false); + skipSiblings = false; + subIterator = Collections.EMPTY_LIST.iterator(); + } + + // create sub iterator for every child, + // if its the first child visited or the former child is finished + if ((!subIterator.hasNext()) && iterator.hasNext()) + { + XMPNode child = (XMPNode) iterator.next(); + index++; + subIterator = new NodeIterator(child, path, index); + } + + if (subIterator.hasNext()) + { + returnProperty = (XMPPropertyInfo) subIterator.next(); + return true; + } + else + { + return false; + } + } + + + /** + * Calls hasNext() and returnes the prepared node. Afterwards its set to null. + * The existance of returnProperty indicates if there is a next node, otherwise + * an exceptio is thrown. + * + * @see Iterator#next() + */ + public Object next() + { + if (hasNext()) + { + XMPPropertyInfo result = returnProperty; + returnProperty = null; + return result; + } + else + { + throw new NoSuchElementException("There are no more nodes to return"); + } + } + + + /** + * Not supported. + * @see Iterator#remove() + */ + public void remove() + { + throw new UnsupportedOperationException(); + } + + + /** + * @param currNode the node that will be added to the path. + * @param parentPath the path up to this node. + * @param currentIndex the current array index if an arrey is traversed + * @return Returns the updated path. + */ + protected String accumulatePath(XMPNode currNode, String parentPath, int currentIndex) + { + String separator; + String segmentName; + if (currNode.getParent() == null || currNode.getOptions().isSchemaNode()) + { + return null; + } + else if (currNode.getParent().getOptions().isArray()) + { + separator = ""; + segmentName = "[" + String.valueOf(currentIndex) + "]"; + } + else + { + separator = "/"; + segmentName = currNode.getName(); + } + + + if (parentPath == null || parentPath.length() == 0) + { + return segmentName; + } + else if (getOptions().isJustLeafname()) + { + return !segmentName.startsWith("?") ? + segmentName : + segmentName.substring(1); // qualifier + } + else + { + return parentPath + separator + segmentName; + } + } + + + /** + * Creates a property info object from an <code>XMPNode</code>. + * @param node an <code>XMPNode</code> + * @param baseNS the base namespace to report + * @param path the full property path + * @return Returns a <code>XMPProperty</code>-object that serves representation of the node. + */ + protected XMPPropertyInfo createPropertyInfo(final XMPNode node, final String baseNS, + final String path) + { + final Object value = node.getOptions().isSchemaNode() ? null : node.getValue(); + + return new XMPPropertyInfo() + { + public String getNamespace() + { + return baseNS; + } + + public String getPath() + { + return path; + } + + public Object getValue() + { + return value; + } + + public PropertyOptions getOptions() + { + return node.getOptions(); + } + + public String getLanguage() + { + // the language is not reported + return null; + } + }; + } + + + /** + * @return the childrenIterator + */ + protected Iterator getChildrenIterator() + { + return childrenIterator; + } + + + /** + * @param childrenIterator the childrenIterator to set + */ + protected void setChildrenIterator(Iterator childrenIterator) + { + this.childrenIterator = childrenIterator; + } + + + /** + * @return Returns the returnProperty. + */ + protected XMPPropertyInfo getReturnProperty() + { + return returnProperty; + } + + + /** + * @param returnProperty the returnProperty to set + */ + protected void setReturnProperty(XMPPropertyInfo returnProperty) + { + this.returnProperty = returnProperty; + } + } + + + /** + * This iterator is derived from the default <code>NodeIterator</code>, + * and is only used for the option {@link IteratorOptions#JUST_CHILDREN}. + * + * @since 02.10.2006 + */ + private class NodeIteratorChildren extends NodeIterator + { + /** */ + private String parentPath; + /** */ + private Iterator childrenIterator; + /** */ + private int index = 0; + + + /** + * Constructor + * @param parentNode the node which children shall be iterated. + * @param parentPath the full path of the former node without the leaf node. + */ + public NodeIteratorChildren(XMPNode parentNode, String parentPath) + { + if (parentNode.getOptions().isSchemaNode()) + { + setBaseNS(parentNode.getName()); + } + this.parentPath = accumulatePath(parentNode, parentPath, 1); + + childrenIterator = parentNode.iterateChildren(); + } + + + /** + * Prepares the next node to return if not already done. + * + * @see Iterator#hasNext() + */ + public boolean hasNext() + { + if (getReturnProperty() != null) + { + // hasNext has been called before + return true; + } + else if (skipSiblings) + { + return false; + } + else if (childrenIterator.hasNext()) + { + XMPNode child = (XMPNode) childrenIterator.next(); + index++; + + String path = null; + if (child.getOptions().isSchemaNode()) + { + setBaseNS(child.getName()); + } + else if (child.getParent() != null) + { + // for all but the root node and schema nodes + path = accumulatePath(child, parentPath, index); + } + + // report next property, skip not-leaf nodes in case options is set + if (!getOptions().isJustLeafnodes() || !child.hasChildren()) + { + setReturnProperty(createPropertyInfo(child, getBaseNS(), path)); + return true; + } + else + { + return hasNext(); + } + } + else + { + return false; + } + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java b/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java new file mode 100644 index 0000000..a97de99 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java @@ -0,0 +1,1424 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.Calendar; +import java.util.Iterator; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPDateTime; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPIterator; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPPathFactory; +import com.adobe.xmp.XMPUtils; +import com.adobe.xmp.impl.xpath.XMPPath; +import com.adobe.xmp.impl.xpath.XMPPathParser; +import com.adobe.xmp.options.IteratorOptions; +import com.adobe.xmp.options.ParseOptions; +import com.adobe.xmp.options.PropertyOptions; +import com.adobe.xmp.properties.XMPProperty; + + +/** + * Implementation for {@link XMPMeta}. + * + * @since 17.02.2006 + */ +public class XMPMetaImpl implements XMPMeta, XMPConst +{ + /** Property values are Strings by default */ + private static final int VALUE_STRING = 0; + /** */ + private static final int VALUE_BOOLEAN = 1; + /** */ + private static final int VALUE_INTEGER = 2; + /** */ + private static final int VALUE_LONG = 3; + /** */ + private static final int VALUE_DOUBLE = 4; + /** */ + private static final int VALUE_DATE = 5; + /** */ + private static final int VALUE_CALENDAR = 6; + /** */ + private static final int VALUE_BASE64 = 7; + + /** root of the metadata tree */ + private XMPNode tree; + /** the xpacket processing instructions content */ + private String packetHeader = null; + + + /** + * Constructor for an empty metadata object. + */ + public XMPMetaImpl() + { + // create root node + tree = new XMPNode(null, null, null); + } + + + /** + * Constructor for a cloned metadata tree. + * + * @param tree + * an prefilled metadata tree which fulfills all + * <code>XMPNode</code> contracts. + */ + public XMPMetaImpl(XMPNode tree) + { + this.tree = tree; + } + + + /** + * @see XMPMeta#appendArrayItem(String, String, PropertyOptions, String, + * PropertyOptions) + */ + public void appendArrayItem(String schemaNS, String arrayName, PropertyOptions arrayOptions, + String itemValue, PropertyOptions itemOptions) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + if (arrayOptions == null) + { + arrayOptions = new PropertyOptions(); + } + if (!arrayOptions.isOnlyArrayOptions()) + { + throw new XMPException("Only array form flags allowed for arrayOptions", + XMPError.BADOPTIONS); + } + + // Check if array options are set correctly. + arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null); + + + // Locate or create the array. If it already exists, make sure the array + // form from the options + // parameter is compatible with the current state. + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); + + + // Just lookup, don't try to create. + XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null); + + if (arrayNode != null) + { + // The array exists, make sure the form is compatible. Zero + // arrayForm means take what exists. + if (!arrayNode.getOptions().isArray()) + { + throw new XMPException("The named property is not an array", XMPError.BADXPATH); + } + // if (arrayOptions != null && !arrayOptions.equalArrayTypes(arrayNode.getOptions())) + // { + // throw new XMPException("Mismatch of existing and specified array form", BADOPTIONS); + // } + } + else + { + // The array does not exist, try to create it. + if (arrayOptions.isArray()) + { + arrayNode = XMPNodeUtils.findNode(tree, arrayPath, true, arrayOptions); + if (arrayNode == null) + { + throw new XMPException("Failure creating array node", XMPError.BADXPATH); + } + } + else + { + // array options missing + throw new XMPException("Explicit arrayOptions required to create new array", + XMPError.BADOPTIONS); + } + } + + doSetArrayItem(arrayNode, ARRAY_LAST_ITEM, itemValue, itemOptions, true); + } + + + /** + * @see XMPMeta#appendArrayItem(String, String, String) + */ + public void appendArrayItem(String schemaNS, String arrayName, String itemValue) + throws XMPException + { + appendArrayItem(schemaNS, arrayName, null, itemValue, null); + } + + + /** + * @throws XMPException + * @see XMPMeta#countArrayItems(String, String) + */ + public int countArrayItems(String schemaNS, String arrayName) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); + XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null); + + if (arrayNode == null) + { + return 0; + } + + if (arrayNode.getOptions().isArray()) + { + return arrayNode.getChildrenLength(); + } + else + { + throw new XMPException("The named property is not an array", XMPError.BADXPATH); + } + } + + + /** + * @see XMPMeta#deleteArrayItem(String, String, int) + */ + public void deleteArrayItem(String schemaNS, String arrayName, int itemIndex) + { + try + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + String itemPath = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex); + deleteProperty(schemaNS, itemPath); + } + catch (XMPException e) + { + // EMPTY, exceptions are ignored within delete + } + } + + + /** + * @see XMPMeta#deleteProperty(String, String) + */ + public void deleteProperty(String schemaNS, String propName) + { + try + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); + + XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null); + if (propNode != null) + { + XMPNodeUtils.deleteNode(propNode); + } + } + catch (XMPException e) + { + // EMPTY, exceptions are ignored within delete + } + } + + + /** + * @see XMPMeta#deleteQualifier(String, String, String, String) + */ + public void deleteQualifier(String schemaNS, String propName, String qualNS, String qualName) + { + try + { + // Note: qualNS and qualName are checked inside composeQualfierPath + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName); + deleteProperty(schemaNS, qualPath); + } + catch (XMPException e) + { + // EMPTY, exceptions within delete are ignored + } + } + + + /** + * @see XMPMeta#deleteStructField(String, String, String, String) + */ + public void deleteStructField(String schemaNS, String structName, String fieldNS, + String fieldName) + { + try + { + // fieldNS and fieldName are checked inside composeStructFieldPath + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertStructName(structName); + + String fieldPath = structName + + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName); + deleteProperty(schemaNS, fieldPath); + } + catch (XMPException e) + { + // EMPTY, exceptions within delete are ignored + } + } + + + /** + * @see XMPMeta#doesPropertyExist(String, String) + */ + public boolean doesPropertyExist(String schemaNS, String propName) + { + try + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); + final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null); + return propNode != null; + } + catch (XMPException e) + { + return false; + } + } + + + /** + * @see XMPMeta#doesArrayItemExist(String, String, int) + */ + public boolean doesArrayItemExist(String schemaNS, String arrayName, int itemIndex) + { + try + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + String path = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex); + return doesPropertyExist(schemaNS, path); + } + catch (XMPException e) + { + return false; + } + } + + + /** + * @see XMPMeta#doesStructFieldExist(String, String, String, String) + */ + public boolean doesStructFieldExist(String schemaNS, String structName, String fieldNS, + String fieldName) + { + try + { + // fieldNS and fieldName are checked inside composeStructFieldPath() + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertStructName(structName); + + String path = XMPPathFactory.composeStructFieldPath(fieldNS, fieldName); + return doesPropertyExist(schemaNS, structName + path); + } + catch (XMPException e) + { + return false; + } + } + + + /** + * @see XMPMeta#doesQualifierExist(String, String, String, String) + */ + public boolean doesQualifierExist(String schemaNS, String propName, String qualNS, + String qualName) + { + try + { + // qualNS and qualName are checked inside composeQualifierPath() + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + String path = XMPPathFactory.composeQualifierPath(qualNS, qualName); + return doesPropertyExist(schemaNS, propName + path); + } + catch (XMPException e) + { + return false; + } + } + + + /** + * @see XMPMeta#getArrayItem(String, String, int) + */ + public XMPProperty getArrayItem(String schemaNS, String arrayName, int itemIndex) + throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + String itemPath = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex); + return getProperty(schemaNS, itemPath); + } + + + /** + * @throws XMPException + * @see XMPMeta#getLocalizedText(String, String, String, String) + */ + public XMPProperty getLocalizedText(String schemaNS, String altTextName, String genericLang, + String specificLang) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(altTextName); + ParameterAsserts.assertSpecificLang(specificLang); + + genericLang = genericLang != null ? Utils.normalizeLangValue(genericLang) : null; + specificLang = Utils.normalizeLangValue(specificLang); + + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, altTextName); + XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null); + if (arrayNode == null) + { + return null; + } + + Object[] result = XMPNodeUtils.chooseLocalizedText(arrayNode, genericLang, specificLang); + int match = ((Integer) result[0]).intValue(); + final XMPNode itemNode = (XMPNode) result[1]; + + if (match != XMPNodeUtils.CLT_NO_VALUES) + { + return new XMPProperty() + { + public Object getValue() + { + return itemNode.getValue(); + } + + + public PropertyOptions getOptions() + { + return itemNode.getOptions(); + } + + + public String getLanguage() + { + return itemNode.getQualifier(1).getValue(); + } + + + public String toString() + { + return itemNode.getValue().toString(); + } + }; + } + else + { + return null; + } + } + + + /** + * @see XMPMeta#setLocalizedText(String, String, String, String, String, + * PropertyOptions) + */ + public void setLocalizedText(String schemaNS, String altTextName, String genericLang, + String specificLang, String itemValue, PropertyOptions options) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(altTextName); + ParameterAsserts.assertSpecificLang(specificLang); + + genericLang = genericLang != null ? Utils.normalizeLangValue(genericLang) : null; + specificLang = Utils.normalizeLangValue(specificLang); + + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, altTextName); + + // Find the array node and set the options if it was just created. + XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, true, new PropertyOptions( + PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED + | PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT)); + + if (arrayNode == null) + { + throw new XMPException("Failed to find or create array node", XMPError.BADXPATH); + } + else if (!arrayNode.getOptions().isArrayAltText()) + { + if (!arrayNode.hasChildren() && arrayNode.getOptions().isArrayAlternate()) + { + arrayNode.getOptions().setArrayAltText(true); + } + else + { + throw new XMPException( + "Specified property is no alt-text array", XMPError.BADXPATH); + } + } + + // Make sure the x-default item, if any, is first. + boolean haveXDefault = false; + XMPNode xdItem = null; + + for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) + { + XMPNode currItem = (XMPNode) it.next(); + if (!currItem.hasQualifier() + || !XMPConst.XML_LANG.equals(currItem.getQualifier(1).getName())) + { + throw new XMPException("Language qualifier must be first", XMPError.BADXPATH); + } + else if (XMPConst.X_DEFAULT.equals(currItem.getQualifier(1).getValue())) + { + xdItem = currItem; + haveXDefault = true; + break; + } + } + + // Moves x-default to the beginning of the array + if (xdItem != null && arrayNode.getChildrenLength() > 1) + { + arrayNode.removeChild(xdItem); + arrayNode.addChild(1, xdItem); + } + + // Find the appropriate item. + // chooseLocalizedText will make sure the array is a language + // alternative. + Object[] result = XMPNodeUtils.chooseLocalizedText(arrayNode, genericLang, specificLang); + int match = ((Integer) result[0]).intValue(); + XMPNode itemNode = (XMPNode) result[1]; + + boolean specificXDefault = XMPConst.X_DEFAULT.equals(specificLang); + + switch (match) + { + case XMPNodeUtils.CLT_NO_VALUES: + + // Create the array items for the specificLang and x-default, with + // x-default first. + XMPNodeUtils.appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue); + haveXDefault = true; + if (!specificXDefault) + { + XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue); + } + break; + + case XMPNodeUtils.CLT_SPECIFIC_MATCH: + + if (!specificXDefault) + { + // Update the specific item, update x-default if it matches the + // old value. + if (haveXDefault && xdItem != itemNode && xdItem != null + && xdItem.getValue().equals(itemNode.getValue())) + { + xdItem.setValue(itemValue); + } + // ! Do this after the x-default check! + itemNode.setValue(itemValue); + } + else + { + // Update all items whose values match the old x-default value. + assert haveXDefault && xdItem == itemNode; + for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) + { + XMPNode currItem = (XMPNode) it.next(); + if (currItem == xdItem + || !currItem.getValue().equals( + xdItem != null ? xdItem.getValue() : null)) + { + continue; + } + currItem.setValue(itemValue); + } + // And finally do the x-default item. + if (xdItem != null) + { + xdItem.setValue(itemValue); + } + } + break; + + case XMPNodeUtils.CLT_SINGLE_GENERIC: + + // Update the generic item, update x-default if it matches the old + // value. + if (haveXDefault && xdItem != itemNode && xdItem != null + && xdItem.getValue().equals(itemNode.getValue())) + { + xdItem.setValue(itemValue); + } + itemNode.setValue(itemValue); // ! Do this after + // the x-default + // check! + break; + + case XMPNodeUtils.CLT_MULTIPLE_GENERIC: + + // Create the specific language, ignore x-default. + XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue); + if (specificXDefault) + { + haveXDefault = true; + } + break; + + case XMPNodeUtils.CLT_XDEFAULT: + + // Create the specific language, update x-default if it was the only + // item. + if (xdItem != null && arrayNode.getChildrenLength() == 1) + { + xdItem.setValue(itemValue); + } + XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue); + break; + + case XMPNodeUtils.CLT_FIRST_ITEM: + + // Create the specific language, don't add an x-default item. + XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue); + if (specificXDefault) + { + haveXDefault = true; + } + break; + + default: + // does not happen under normal circumstances + throw new XMPException("Unexpected result from ChooseLocalizedText", + XMPError.INTERNALFAILURE); + + } + + // Add an x-default at the front if needed. + if (!haveXDefault && arrayNode.getChildrenLength() == 1) + { + XMPNodeUtils.appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue); + } + } + + + /** + * @see XMPMeta#setLocalizedText(String, String, String, String, String) + */ + public void setLocalizedText(String schemaNS, String altTextName, String genericLang, + String specificLang, String itemValue) throws XMPException + { + setLocalizedText(schemaNS, altTextName, genericLang, specificLang, itemValue, null); + } + + + /** + * @throws XMPException + * @see XMPMeta#getProperty(String, String) + */ + public XMPProperty getProperty(String schemaNS, String propName) throws XMPException + { + return getProperty(schemaNS, propName, VALUE_STRING); + } + + + /** + * Returns a property, but the result value can be requested. It can be one + * of {@link XMPMetaImpl#VALUE_STRING}, {@link XMPMetaImpl#VALUE_BOOLEAN}, + * {@link XMPMetaImpl#VALUE_INTEGER}, {@link XMPMetaImpl#VALUE_LONG}, + * {@link XMPMetaImpl#VALUE_DOUBLE}, {@link XMPMetaImpl#VALUE_DATE}, + * {@link XMPMetaImpl#VALUE_CALENDAR}, {@link XMPMetaImpl#VALUE_BASE64}. + * + * @see XMPMeta#getProperty(String, String) + * @param schemaNS + * a schema namespace + * @param propName + * a property name or path + * @param valueType + * the type of the value, see VALUE_... + * @return Returns an <code>XMPProperty</code> + * @throws XMPException + * Collects any exception that occurs. + */ + protected XMPProperty getProperty(String schemaNS, String propName, int valueType) + throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + final XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); + final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null); + + if (propNode != null) + { + if (valueType != VALUE_STRING && propNode.getOptions().isCompositeProperty()) + { + throw new XMPException("Property must be simple when a value type is requested", + XMPError.BADXPATH); + } + + final Object value = evaluateNodeValue(valueType, propNode); + + return new XMPProperty() + { + public Object getValue() + { + return value; + } + + + public PropertyOptions getOptions() + { + return propNode.getOptions(); + } + + + public String getLanguage() + { + return null; + } + + + public String toString() + { + return value.toString(); + } + }; + } + else + { + return null; + } + } + + + /** + * Returns a property, but the result value can be requested. + * + * @see XMPMeta#getProperty(String, String) + * @param schemaNS + * a schema namespace + * @param propName + * a property name or path + * @param valueType + * the type of the value, see VALUE_... + * @return Returns the node value as an object according to the + * <code>valueType</code>. + * @throws XMPException + * Collects any exception that occurs. + */ + protected Object getPropertyObject(String schemaNS, String propName, int valueType) + throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + final XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); + final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null); + + if (propNode != null) + { + if (valueType != VALUE_STRING && propNode.getOptions().isCompositeProperty()) + { + throw new XMPException("Property must be simple when a value type is requested", + XMPError.BADXPATH); + } + + return evaluateNodeValue(valueType, propNode); + } + else + { + return null; + } + } + + + /** + * @see XMPMeta#getPropertyBoolean(String, String) + */ + public Boolean getPropertyBoolean(String schemaNS, String propName) throws XMPException + { + return (Boolean) getPropertyObject(schemaNS, propName, VALUE_BOOLEAN); + } + + + /** + * @throws XMPException + * @see XMPMeta#setPropertyBoolean(String, String, boolean, PropertyOptions) + */ + public void setPropertyBoolean(String schemaNS, String propName, boolean propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, propValue ? TRUESTR : FALSESTR, options); + } + + + /** + * @see XMPMeta#setPropertyBoolean(String, String, boolean) + */ + public void setPropertyBoolean(String schemaNS, String propName, boolean propValue) + throws XMPException + { + setProperty(schemaNS, propName, propValue ? TRUESTR : FALSESTR, null); + } + + + /** + * @see XMPMeta#getPropertyInteger(String, String) + */ + public Integer getPropertyInteger(String schemaNS, String propName) throws XMPException + { + return (Integer) getPropertyObject(schemaNS, propName, VALUE_INTEGER); + } + + + /** + * @see XMPMeta#setPropertyInteger(String, String, int, PropertyOptions) + */ + public void setPropertyInteger(String schemaNS, String propName, int propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, new Integer(propValue), options); + } + + + /** + * @see XMPMeta#setPropertyInteger(String, String, int) + */ + public void setPropertyInteger(String schemaNS, String propName, int propValue) + throws XMPException + { + setProperty(schemaNS, propName, new Integer(propValue), null); + } + + + /** + * @see XMPMeta#getPropertyLong(String, String) + */ + public Long getPropertyLong(String schemaNS, String propName) throws XMPException + { + return (Long) getPropertyObject(schemaNS, propName, VALUE_LONG); + } + + + /** + * @see XMPMeta#setPropertyLong(String, String, long, PropertyOptions) + */ + public void setPropertyLong(String schemaNS, String propName, long propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, new Long(propValue), options); + } + + + /** + * @see XMPMeta#setPropertyLong(String, String, long) + */ + public void setPropertyLong(String schemaNS, String propName, long propValue) + throws XMPException + { + setProperty(schemaNS, propName, new Long(propValue), null); + } + + + /** + * @see XMPMeta#getPropertyDouble(String, String) + */ + public Double getPropertyDouble(String schemaNS, String propName) throws XMPException + { + return (Double) getPropertyObject(schemaNS, propName, VALUE_DOUBLE); + } + + + /** + * @see XMPMeta#setPropertyDouble(String, String, double, PropertyOptions) + */ + public void setPropertyDouble(String schemaNS, String propName, double propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, new Double(propValue), options); + } + + + /** + * @see XMPMeta#setPropertyDouble(String, String, double) + */ + public void setPropertyDouble(String schemaNS, String propName, double propValue) + throws XMPException + { + setProperty(schemaNS, propName, new Double(propValue), null); + } + + + /** + * @see XMPMeta#getPropertyDate(String, String) + */ + public XMPDateTime getPropertyDate(String schemaNS, String propName) throws XMPException + { + return (XMPDateTime) getPropertyObject(schemaNS, propName, VALUE_DATE); + } + + + /** + * @see XMPMeta#setPropertyDate(String, String, XMPDateTime, + * PropertyOptions) + */ + public void setPropertyDate(String schemaNS, String propName, XMPDateTime propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, propValue, options); + } + + + /** + * @see XMPMeta#setPropertyDate(String, String, XMPDateTime) + */ + public void setPropertyDate(String schemaNS, String propName, XMPDateTime propValue) + throws XMPException + { + setProperty(schemaNS, propName, propValue, null); + } + + + /** + * @see XMPMeta#getPropertyCalendar(String, String) + */ + public Calendar getPropertyCalendar(String schemaNS, String propName) throws XMPException + { + return (Calendar) getPropertyObject(schemaNS, propName, VALUE_CALENDAR); + } + + + /** + * @see XMPMeta#setPropertyCalendar(String, String, Calendar, + * PropertyOptions) + */ + public void setPropertyCalendar(String schemaNS, String propName, Calendar propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, propValue, options); + } + + + /** + * @see XMPMeta#setPropertyCalendar(String, String, Calendar) + */ + public void setPropertyCalendar(String schemaNS, String propName, Calendar propValue) + throws XMPException + { + setProperty(schemaNS, propName, propValue, null); + } + + + /** + * @see XMPMeta#getPropertyBase64(String, String) + */ + public byte[] getPropertyBase64(String schemaNS, String propName) throws XMPException + { + return (byte[]) getPropertyObject(schemaNS, propName, VALUE_BASE64); + } + + + /** + * @see XMPMeta#getPropertyString(String, String) + */ + public String getPropertyString(String schemaNS, String propName) throws XMPException + { + return (String) getPropertyObject(schemaNS, propName, VALUE_STRING); + } + + + /** + * @see XMPMeta#setPropertyBase64(String, String, byte[], PropertyOptions) + */ + public void setPropertyBase64(String schemaNS, String propName, byte[] propValue, + PropertyOptions options) throws XMPException + { + setProperty(schemaNS, propName, propValue, options); + } + + + /** + * @see XMPMeta#setPropertyBase64(String, String, byte[]) + */ + public void setPropertyBase64(String schemaNS, String propName, byte[] propValue) + throws XMPException + { + setProperty(schemaNS, propName, propValue, null); + } + + + /** + * @throws XMPException + * @see XMPMeta#getQualifier(String, String, String, String) + */ + public XMPProperty getQualifier(String schemaNS, String propName, String qualNS, + String qualName) throws XMPException + { + // qualNS and qualName are checked inside composeQualfierPath + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName); + return getProperty(schemaNS, qualPath); + } + + + /** + * @see XMPMeta#getStructField(String, String, String, String) + */ + public XMPProperty getStructField(String schemaNS, String structName, String fieldNS, + String fieldName) throws XMPException + { + // fieldNS and fieldName are checked inside composeStructFieldPath + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertStructName(structName); + + String fieldPath = structName + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName); + return getProperty(schemaNS, fieldPath); + } + + + /** + * @throws XMPException + * @see XMPMeta#iterator() + */ + public XMPIterator iterator() throws XMPException + { + return iterator(null, null, null); + } + + + /** + * @see XMPMeta#iterator(IteratorOptions) + */ + public XMPIterator iterator(IteratorOptions options) throws XMPException + { + return iterator(null, null, options); + } + + + /** + * @see XMPMeta#iterator(String, String, IteratorOptions) + */ + public XMPIterator iterator(String schemaNS, String propName, IteratorOptions options) + throws XMPException + { + return new XMPIteratorImpl(this, schemaNS, propName, options); + } + + + /** + * @throws XMPException + * @see XMPMeta#setArrayItem(String, String, int, String, PropertyOptions) + */ + public void setArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue, + PropertyOptions options) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + // Just lookup, don't try to create. + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); + XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null); + + if (arrayNode != null) + { + doSetArrayItem(arrayNode, itemIndex, itemValue, options, false); + } + else + { + throw new XMPException("Specified array does not exist", XMPError.BADXPATH); + } + } + + + /** + * @see XMPMeta#setArrayItem(String, String, int, String) + */ + public void setArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue) + throws XMPException + { + setArrayItem(schemaNS, arrayName, itemIndex, itemValue, null); + } + + + /** + * @throws XMPException + * @see XMPMeta#insertArrayItem(String, String, int, String, + * PropertyOptions) + */ + public void insertArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue, + PropertyOptions options) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + + // Just lookup, don't try to create. + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); + XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null); + + if (arrayNode != null) + { + doSetArrayItem(arrayNode, itemIndex, itemValue, options, true); + } + else + { + throw new XMPException("Specified array does not exist", XMPError.BADXPATH); + } + } + + + /** + * @see XMPMeta#insertArrayItem(String, String, int, String) + */ + public void insertArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue) + throws XMPException + { + insertArrayItem(schemaNS, arrayName, itemIndex, itemValue, null); + } + + + /** + * @throws XMPException + * @see XMPMeta#setProperty(String, String, Object, PropertyOptions) + */ + public void setProperty(String schemaNS, String propName, Object propValue, + PropertyOptions options) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + options = XMPNodeUtils.verifySetOptions(options, propValue); + + XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); + + XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, true, options); + if (propNode != null) + { + setNode(propNode, propValue, options, false); + } + else + { + throw new XMPException("Specified property does not exist", XMPError.BADXPATH); + } + } + + + /** + * @see XMPMeta#setProperty(String, String, Object) + */ + public void setProperty(String schemaNS, String propName, Object propValue) throws XMPException + { + setProperty(schemaNS, propName, propValue, null); + } + + + /** + * @throws XMPException + * @see XMPMeta#setQualifier(String, String, String, String, String, + * PropertyOptions) + */ + public void setQualifier(String schemaNS, String propName, String qualNS, String qualName, + String qualValue, PropertyOptions options) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertPropName(propName); + + if (!doesPropertyExist(schemaNS, propName)) + { + throw new XMPException("Specified property does not exist!", XMPError.BADXPATH); + } + + String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName); + setProperty(schemaNS, qualPath, qualValue, options); + } + + + /** + * @see XMPMeta#setQualifier(String, String, String, String, String) + */ + public void setQualifier(String schemaNS, String propName, String qualNS, String qualName, + String qualValue) throws XMPException + { + setQualifier(schemaNS, propName, qualNS, qualName, qualValue, null); + + } + + + /** + * @see XMPMeta#setStructField(String, String, String, String, String, + * PropertyOptions) + */ + public void setStructField(String schemaNS, String structName, String fieldNS, + String fieldName, String fieldValue, PropertyOptions options) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertStructName(structName); + + String fieldPath = structName + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName); + setProperty(schemaNS, fieldPath, fieldValue, options); + } + + + /** + * @see XMPMeta#setStructField(String, String, String, String, String) + */ + public void setStructField(String schemaNS, String structName, String fieldNS, + String fieldName, String fieldValue) throws XMPException + { + setStructField(schemaNS, structName, fieldNS, fieldName, fieldValue, null); + } + + + /** + * @see XMPMeta#getObjectName() + */ + public String getObjectName() + { + return tree.getName() != null ? tree.getName() : ""; + } + + + /** + * @see XMPMeta#setObjectName(String) + */ + public void setObjectName(String name) + { + tree.setName(name); + } + + + /** + * @see XMPMeta#getPacketHeader() + */ + public String getPacketHeader() + { + return packetHeader; + } + + + /** + * Sets the packetHeader attributes, only used by the parser. + * @param packetHeader the processing instruction content + */ + public void setPacketHeader(String packetHeader) + { + this.packetHeader = packetHeader; + } + + + /** + * Performs a deep clone of the XMPMeta-object + * + * @see java.lang.Object#clone() + */ + public Object clone() + { + XMPNode clonedTree = (XMPNode) tree.clone(); + return new XMPMetaImpl(clonedTree); + } + + + /** + * @see XMPMeta#dumpObject() + */ + public String dumpObject() + { + // renders tree recursively + return getRoot().dumpNode(true); + } + + + /** + * @see XMPMeta#sort() + */ + public void sort() + { + this.tree.sort(); + } + + + /** + * @see XMPMeta#normalize(ParseOptions) + */ + public void normalize(ParseOptions options) throws XMPException + { + if (options == null) + { + options = new ParseOptions(); + } + XMPNormalizer.process(this, options); + } + + + /** + * @return Returns the root node of the XMP tree. + */ + public XMPNode getRoot() + { + return tree; + } + + + + // ------------------------------------------------------------------------------------- + // private + + + /** + * Locate or create the item node and set the value. Note the index + * parameter is one-based! The index can be in the range [1..size + 1] or + * "last()", normalize it and check the insert flags. The order of the + * normalization checks is important. If the array is empty we end up with + * an index and location to set item size + 1. + * + * @param arrayNode an array node + * @param itemIndex the index where to insert the item + * @param itemValue the item value + * @param itemOptions the options for the new item + * @param insert insert oder overwrite at index position? + * @throws XMPException + */ + private void doSetArrayItem(XMPNode arrayNode, int itemIndex, String itemValue, + PropertyOptions itemOptions, boolean insert) throws XMPException + { + XMPNode itemNode = new XMPNode(ARRAY_ITEM_NAME, null); + itemOptions = XMPNodeUtils.verifySetOptions(itemOptions, itemValue); + + // in insert mode the index after the last is allowed, + // even ARRAY_LAST_ITEM points to the index *after* the last. + int maxIndex = insert ? arrayNode.getChildrenLength() + 1 : arrayNode.getChildrenLength(); + if (itemIndex == ARRAY_LAST_ITEM) + { + itemIndex = maxIndex; + } + + if (1 <= itemIndex && itemIndex <= maxIndex) + { + if (!insert) + { + arrayNode.removeChild(itemIndex); + } + arrayNode.addChild(itemIndex, itemNode); + setNode(itemNode, itemValue, itemOptions, false); + } + else + { + throw new XMPException("Array index out of bounds", XMPError.BADINDEX); + } + } + + + /** + * The internals for setProperty() and related calls, used after the node is + * found or created. + * + * @param node + * the newly created node + * @param value + * the node value, can be <code>null</code> + * @param newOptions + * options for the new node, must not be <code>null</code>. + * @param deleteExisting flag if the existing value is to be overwritten + * @throws XMPException thrown if options and value do not correspond + */ + void setNode(XMPNode node, Object value, PropertyOptions newOptions, boolean deleteExisting) + throws XMPException + { + if (deleteExisting) + { + node.clear(); + } + + // its checked by setOptions(), if the merged result is a valid options set + node.getOptions().mergeWith(newOptions); + + if (!node.getOptions().isCompositeProperty()) + { + // This is setting the value of a leaf node. + XMPNodeUtils.setNodeValue(node, value); + } + else + { + if (value != null && value.toString().length() > 0) + { + throw new XMPException("Composite nodes can't have values", XMPError.BADXPATH); + } + + node.removeChildren(); + } + + } + + + /** + * Evaluates a raw node value to the given value type, apply special + * conversions for defined types in XMP. + * + * @param valueType + * an int indicating the value type + * @param propNode + * the node containing the value + * @return Returns a literal value for the node. + * @throws XMPException + */ + private Object evaluateNodeValue(int valueType, final XMPNode propNode) throws XMPException + { + final Object value; + String rawValue = propNode.getValue(); + switch (valueType) + { + case VALUE_BOOLEAN: + value = new Boolean(XMPUtils.convertToBoolean(rawValue)); + break; + case VALUE_INTEGER: + value = new Integer(XMPUtils.convertToInteger(rawValue)); + break; + case VALUE_LONG: + value = new Long(XMPUtils.convertToLong(rawValue)); + break; + case VALUE_DOUBLE: + value = new Double(XMPUtils.convertToDouble(rawValue)); + break; + case VALUE_DATE: + value = XMPUtils.convertToDate(rawValue); + break; + case VALUE_CALENDAR: + XMPDateTime dt = XMPUtils.convertToDate(rawValue); + value = dt.getCalendar(); + break; + case VALUE_BASE64: + value = XMPUtils.decodeBase64(rawValue); + break; + case VALUE_STRING: + default: + // leaf values return empty string instead of null + // for the other cases the converter methods provides a "null" + // value. + // a default value can only occur if this method is made public. + value = rawValue != null || propNode.getOptions().isCompositeProperty() ? rawValue : ""; + break; + } + return value; + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java b/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java new file mode 100644 index 0000000..40c0ce2 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java @@ -0,0 +1,411 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.ProcessingInstruction; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.options.ParseOptions; + + +/** + * This class replaces the <code>ExpatAdapter.cpp</code> and does the + * XML-parsing and fixes the prefix. After the parsing several normalisations + * are applied to the XMPTree. + * + * @since 01.02.2006 + */ +public class XMPMetaParser +{ + /** */ + private static final Object XMP_RDF = new Object(); + /** the DOM Parser Factory, options are set */ + private static DocumentBuilderFactory factory = createDocumentBuilderFactory(); + + /** + * Hidden constructor, initialises the SAX parser handler. + */ + private XMPMetaParser() + { + // EMPTY + } + + + + /** + * Parses the input source into an XMP metadata object, including + * de-aliasing and normalisation. + * + * @param input the input can be an <code>InputStream</code>, a <code>String</code> or + * a byte buffer containing the XMP packet. + * @param options the parse options + * @return Returns the resulting XMP metadata object + * @throws XMPException Thrown if parsing or normalisation fails. + */ + public static XMPMeta parse(Object input, ParseOptions options) throws XMPException + { + ParameterAsserts.assertNotNull(input); + options = options != null ? options : new ParseOptions(); + + Document document = parseXml(input, options); + + boolean xmpmetaRequired = options.getRequireXMPMeta(); + Object[] result = new Object[3]; + result = findRootNode(document, xmpmetaRequired, result); + + if (result != null && result[1] == XMP_RDF) + { + XMPMetaImpl xmp = ParseRDF.parse((Node) result[0]); + xmp.setPacketHeader((String) result[2]); + + // Check if the XMP object shall be normalized + if (!options.getOmitNormalization()) + { + return XMPNormalizer.process(xmp, options); + } + else + { + return xmp; + } + } + else + { + // no appropriate root node found, return empty metadata object + return new XMPMetaImpl(); + } + } + + + /** + * Parses the raw XML metadata packet considering the parsing options. + * Latin-1/ISO-8859-1 can be accepted when the input is a byte stream + * (some old toolkits versions such packets). The stream is + * then wrapped in another stream that converts Latin-1 to UTF-8. + * <p> + * If control characters shall be fixed, a reader is used that fixes the chars to spaces + * (if the input is a byte stream is has to be read as character stream). + * <p> + * Both options reduce the performance of the parser. + * + * @param input the input can be an <code>InputStream</code>, a <code>String</code> or + * a byte buffer containing the XMP packet. + * @param options the parsing options + * @return Returns the parsed XML document or an exception. + * @throws XMPException Thrown if the parsing fails for different reasons + */ + private static Document parseXml(Object input, ParseOptions options) + throws XMPException + { + if (input instanceof InputStream) + { + return parseXmlFromInputStream((InputStream) input, options); + } + else if (input instanceof byte[]) + { + return parseXmlFromBytebuffer(new ByteBuffer((byte[]) input), options); + } + else + { + return parseXmlFromString((String) input, options); + } + } + + + /** + * Parses XML from an {@link InputStream}, + * fixing the encoding (Latin-1 to UTF-8) and illegal control character optionally. + * + * @param stream an <code>InputStream</code> + * @param options the parsing options + * @return Returns an XML DOM-Document. + * @throws XMPException Thrown when the parsing fails. + */ + private static Document parseXmlFromInputStream(InputStream stream, ParseOptions options) + throws XMPException + { + if (!options.getAcceptLatin1() && !options.getFixControlChars()) + { + return parseInputSource(new InputSource(stream)); + } + else + { + // load stream into bytebuffer + try + { + ByteBuffer buffer = new ByteBuffer(stream); + return parseXmlFromBytebuffer(buffer, options); + } + catch (IOException e) + { + throw new XMPException("Error reading the XML-file", + XMPError.BADSTREAM, e); + } + } + } + + + /** + * Parses XML from a byte buffer, + * fixing the encoding (Latin-1 to UTF-8) and illegal control character optionally. + * + * @param buffer a byte buffer containing the XMP packet + * @param options the parsing options + * @return Returns an XML DOM-Document. + * @throws XMPException Thrown when the parsing fails. + */ + private static Document parseXmlFromBytebuffer(ByteBuffer buffer, ParseOptions options) + throws XMPException + { + InputSource source = new InputSource(buffer.getByteStream()); + try + { + return parseInputSource(source); + } + catch (XMPException e) + { + if (e.getErrorCode() == XMPError.BADXML || + e.getErrorCode() == XMPError.BADSTREAM) + { + if (options.getAcceptLatin1()) + { + buffer = Latin1Converter.convert(buffer); + } + + if (options.getFixControlChars()) + { + try + { + String encoding = buffer.getEncoding(); + Reader fixReader = new FixASCIIControlsReader( + new InputStreamReader( + buffer.getByteStream(), encoding)); + return parseInputSource(new InputSource(fixReader)); + } + catch (UnsupportedEncodingException e1) + { + // can normally not happen as the encoding is provided by a util function + throw new XMPException("Unsupported Encoding", + XMPError.INTERNALFAILURE, e); + } + } + source = new InputSource(buffer.getByteStream()); + return parseInputSource(source); + } + else + { + throw e; + } + } + } + + + /** + * Parses XML from a {@link String}, + * fixing the illegal control character optionally. + * + * @param input a <code>String</code> containing the XMP packet + * @param options the parsing options + * @return Returns an XML DOM-Document. + * @throws XMPException Thrown when the parsing fails. + */ + private static Document parseXmlFromString(String input, ParseOptions options) + throws XMPException + { + InputSource source = new InputSource(new StringReader(input)); + try + { + return parseInputSource(source); + } + catch (XMPException e) + { + if (e.getErrorCode() == XMPError.BADXML && options.getFixControlChars()) + { + source = new InputSource(new FixASCIIControlsReader(new StringReader(input))); + return parseInputSource(source); + } + else + { + throw e; + } + } + } + + + /** + * Runs the XML-Parser. + * @param source an <code>InputSource</code> + * @return Returns an XML DOM-Document. + * @throws XMPException Wraps parsing and I/O-exceptions into an XMPException. + */ + private static Document parseInputSource(InputSource source) throws XMPException + { + try + { + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setErrorHandler(null); + return builder.parse(source); + } + catch (SAXException e) + { + throw new XMPException("XML parsing failure", XMPError.BADXML, e); + } + catch (ParserConfigurationException e) + { + throw new XMPException("XML Parser not correctly configured", + XMPError.UNKNOWN, e); + } + catch (IOException e) + { + throw new XMPException("Error reading the XML-file", XMPError.BADSTREAM, e); + } + } + + + /** + * Find the XML node that is the root of the XMP data tree. Generally this + * will be an outer node, but it could be anywhere if a general XML document + * is parsed (e.g. SVG). The XML parser counted all rdf:RDF and + * pxmp:XMP_Packet nodes, and kept a pointer to the last one. If there is + * more than one possible root use PickBestRoot to choose among them. + * <p> + * If there is a root node, try to extract the version of the previous XMP + * toolkit. + * <p> + * Pick the first x:xmpmeta among multiple root candidates. If there aren't + * any, pick the first bare rdf:RDF if that is allowed. The returned root is + * the rdf:RDF child if an x:xmpmeta element was chosen. The search is + * breadth first, so a higher level candiate is chosen over a lower level + * one that was textually earlier in the serialized XML. + * + * @param root the root of the xml document + * @param xmpmetaRequired flag if the xmpmeta-tag is still required, might be set + * initially to <code>true</code>, if the parse option "REQUIRE_XMP_META" is set + * @param result The result array that is filled during the recursive process. + * @return Returns an array that contains the result or <code>null</code>. + * The array contains: + * <ol> + * <li>the rdf:RDF-node + * <li>an object that is either XMP_RDF or XMP_PLAIN + * <li>a flag that is true if a <?xpacket..> processing instruction has been found + * <li>the body text of the xpacket-instruction. + * </ol> + * + */ + private static Object[] findRootNode(Node root, boolean xmpmetaRequired, Object[] result) + { + // Look among this parent's content for x:xapmeta or x:xmpmeta. + // The recursion for x:xmpmeta is broader than the strictly defined choice, + // but gives us smaller code. + NodeList children = root.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) + { + root = children.item(i); + if (Node.PROCESSING_INSTRUCTION_NODE == root.getNodeType() && + ((ProcessingInstruction) root).getTarget() == XMPConst.XMP_PI) + { + // Store the processing instructions content + if (result != null) + { + result[2] = ((ProcessingInstruction) root).getData(); + } + } + else if (Node.TEXT_NODE != root.getNodeType() && + Node.PROCESSING_INSTRUCTION_NODE != root.getNodeType()) + { + String rootNS = root.getNamespaceURI(); + String rootLocal = root.getLocalName(); + if ( + ( + XMPConst.TAG_XMPMETA.equals(rootLocal) || + XMPConst.TAG_XAPMETA.equals(rootLocal) + ) && + XMPConst.NS_X.equals(rootNS) + ) + { + // by not passing the RequireXMPMeta-option, the rdf-Node will be valid + return findRootNode(root, false, result); + } + else if (!xmpmetaRequired && + "RDF".equals(rootLocal) && + XMPConst.NS_RDF.equals(rootNS)) + { + if (result != null) + { + result[0] = root; + result[1] = XMP_RDF; + } + return result; + } + else + { + // continue searching + Object[] newResult = findRootNode(root, xmpmetaRequired, result); + if (newResult != null) + { + return newResult; + } + else + { + continue; + } + } + } + } + + // no appropriate node has been found + return null; + // is extracted here in the C++ Toolkit + } + + + /** + * @return Creates, configures and returnes the document builder factory for + * the Metadata Parser. + */ + private static DocumentBuilderFactory createDocumentBuilderFactory() + { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setIgnoringComments(true); + + try + { + // honor System parsing limits, e.g. + // System.setProperty("entityExpansionLimit", "10"); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + } + catch (Exception e) + { + // Ignore IllegalArgumentException and ParserConfigurationException + // in case the configured XML-Parser does not implement the feature. + } + return factory; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPNode.java b/XMPCore/src/com/adobe/xmp/impl/XMPNode.java new file mode 100644 index 0000000..9170efe --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPNode.java @@ -0,0 +1,921 @@ +//================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.options.PropertyOptions; + + +/** + * A node in the internally XMP tree, which can be a schema node, a property node, an array node, + * an array item, a struct node or a qualifier node (without '?'). + * + * Possible improvements: + * + * 1. The kind Node of node might be better represented by a class-hierarchy of different nodes. + * 2. The array type should be an enum + * 3. isImplicitNode should be removed completely and replaced by return values of fi. + * 4. hasLanguage, hasType should be automatically maintained by XMPNode + * + * @since 21.02.2006 + */ +class XMPNode implements Comparable +{ + /** name of the node, contains different information depending of the node kind */ + private String name; + /** value of the node, contains different information depending of the node kind */ + private String value; + /** link to the parent node */ + private XMPNode parent; + /** list of child nodes, lazy initialized */ + private List children = null; + /** list of qualifier of the node, lazy initialized */ + private List qualifier = null; + /** options describing the kind of the node */ + private PropertyOptions options = null; + + // internal processing options + + /** flag if the node is implicitly created */ + private boolean implicit; + /** flag if the node has aliases */ + private boolean hasAliases; + /** flag if the node is an alias */ + private boolean alias; + /** flag if the node has an "rdf:value" child node. */ + private boolean hasValueChild; + + + + /** + * Creates an <code>XMPNode</code> with initial values. + * + * @param name the name of the node + * @param value the value of the node + * @param options the options of the node + */ + public XMPNode(String name, String value, PropertyOptions options) + { + this.name = name; + this.value = value; + this.options = options; + } + + + /** + * Constructor for the node without value. + * + * @param name the name of the node + * @param options the options of the node + */ + public XMPNode(String name, PropertyOptions options) + { + this(name, null, options); + } + + + /** + * Resets the node. + */ + public void clear() + { + options = null; + name = null; + value = null; + children = null; + qualifier = null; + } + + + /** + * @return Returns the parent node. + */ + public XMPNode getParent() + { + return parent; + } + + + /** + * @param index an index [1..size] + * @return Returns the child with the requested index. + */ + public XMPNode getChild(int index) + { + return (XMPNode) getChildren().get(index - 1); + } + + + /** + * Adds a node as child to this node. + * @param node an XMPNode + * @throws XMPException + */ + public void addChild(XMPNode node) throws XMPException + { + // check for duplicate properties + assertChildNotExisting(node.getName()); + node.setParent(this); + getChildren().add(node); + } + + + /** + * Adds a node as child to this node. + * @param index the index of the node <em>before</em> which the new one is inserted. + * <em>Note:</em> The node children are indexed from [1..size]! + * An index of size + 1 appends a node. + * @param node an XMPNode + * @throws XMPException + */ + public void addChild(int index, XMPNode node) throws XMPException + { + assertChildNotExisting(node.getName()); + node.setParent(this); + getChildren().add(index - 1, node); + } + + + /** + * Replaces a node with another one. + * @param index the index of the node that will be replaced. + * <em>Note:</em> The node children are indexed from [1..size]! + * @param node the replacement XMPNode + */ + public void replaceChild(int index, XMPNode node) + { + node.setParent(this); + getChildren().set(index - 1, node); + } + + + /** + * Removes a child at the requested index. + * @param itemIndex the index to remove [1..size] + */ + public void removeChild(int itemIndex) + { + getChildren().remove(itemIndex - 1); + cleanupChildren(); + } + + + /** + * Removes a child node. + * If its a schema node and doesn't have any children anymore, its deleted. + * + * @param node the child node to delete. + */ + public void removeChild(XMPNode node) + { + getChildren().remove(node); + cleanupChildren(); + } + + + /** + * Removes the children list if this node has no children anymore; + * checks if the provided node is a schema node and doesn't have any children anymore, + * its deleted. + */ + protected void cleanupChildren() + { + if (children.isEmpty()) + { + children = null; + } + } + + + /** + * Removes all children from the node. + */ + public void removeChildren() + { + children = null; + } + + + /** + * @return Returns the number of children without neccessarily creating a list. + */ + public int getChildrenLength() + { + return children != null ? + children.size() : + 0; + } + + + /** + * @param expr child node name to look for + * @return Returns an <code>XMPNode</code> if node has been found, <code>null</code> otherwise. + */ + public XMPNode findChildByName(String expr) + { + return find(getChildren(), expr); + } + + + /** + * @param index an index [1..size] + * @return Returns the qualifier with the requested index. + */ + public XMPNode getQualifier(int index) + { + return (XMPNode) getQualifier().get(index - 1); + } + + + /** + * @return Returns the number of qualifier without neccessarily creating a list. + */ + public int getQualifierLength() + { + return qualifier != null ? + qualifier.size() : + 0; + } + + + /** + * Appends a qualifier to the qualifier list and sets respective options. + * @param qualNode a qualifier node. + * @throws XMPException + */ + public void addQualifier(XMPNode qualNode) throws XMPException + { + assertQualifierNotExisting(qualNode.getName()); + qualNode.setParent(this); + qualNode.getOptions().setQualifier(true); + getOptions().setHasQualifiers(true); + + // contraints + if (qualNode.isLanguageNode()) + { + // "xml:lang" is always first and the option "hasLanguage" is set + options.setHasLanguage(true); + getQualifier().add(0, qualNode); + } + else if (qualNode.isTypeNode()) + { + // "rdf:type" must be first or second after "xml:lang" and the option "hasType" is set + options.setHasType(true); + getQualifier().add( + !options.getHasLanguage() ? 0 : 1, + qualNode); + } + else + { + // other qualifiers are appended + getQualifier().add(qualNode); + } + } + + + /** + * Removes one qualifier node and fixes the options. + * @param qualNode qualifier to remove + */ + public void removeQualifier(XMPNode qualNode) + { + PropertyOptions opts = getOptions(); + if (qualNode.isLanguageNode()) + { + // if "xml:lang" is removed, remove hasLanguage-flag too + opts.setHasLanguage(false); + } + else if (qualNode.isTypeNode()) + { + // if "rdf:type" is removed, remove hasType-flag too + opts.setHasType(false); + } + + getQualifier().remove(qualNode); + if (qualifier.isEmpty()) + { + opts.setHasQualifiers(false); + qualifier = null; + } + + } + + + /** + * Removes all qualifiers from the node and sets the options appropriate. + */ + public void removeQualifiers() + { + PropertyOptions opts = getOptions(); + // clear qualifier related options + opts.setHasQualifiers(false); + opts.setHasLanguage(false); + opts.setHasType(false); + qualifier = null; + } + + + /** + * @param expr qualifier node name to look for + * @return Returns a qualifier <code>XMPNode</code> if node has been found, + * <code>null</code> otherwise. + */ + public XMPNode findQualifierByName(String expr) + { + return find(qualifier, expr); + } + + + /** + * @return Returns whether the node has children. + */ + public boolean hasChildren() + { + return children != null && children.size() > 0; + } + + + /** + * @return Returns an iterator for the children. + * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case. + */ + public Iterator iterateChildren() + { + if (children != null) + { + return getChildren().iterator(); + } + else + { + return Collections.EMPTY_LIST.listIterator(); + } + } + + + /** + * @return Returns whether the node has qualifier attached. + */ + public boolean hasQualifier() + { + return qualifier != null && qualifier.size() > 0; + } + + + /** + * @return Returns an iterator for the qualifier. + * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case. + */ + public Iterator iterateQualifier() + { + if (qualifier != null) + { + final Iterator it = getQualifier().iterator(); + + return new Iterator() + { + public boolean hasNext() + { + return it.hasNext(); + } + + public Object next() + { + return it.next(); + } + + public void remove() + { + throw new UnsupportedOperationException( + "remove() is not allowed due to the internal contraints"); + } + + }; + } + else + { + return Collections.EMPTY_LIST.iterator(); + } + } + + + /** + * Performs a <b>deep clone</b> of the node and the complete subtree. + * + * @see java.lang.Object#clone() + */ + public Object clone() + { + PropertyOptions newOptions; + try + { + newOptions = new PropertyOptions(getOptions().getOptions()); + } + catch (XMPException e) + { + // cannot happen + newOptions = new PropertyOptions(); + } + + XMPNode newNode = new XMPNode(name, value, newOptions); + cloneSubtree(newNode); + + return newNode; + } + + + /** + * Performs a <b>deep clone</b> of the complete subtree (children and + * qualifier )into and add it to the destination node. + * + * @param destination the node to add the cloned subtree + */ + public void cloneSubtree(XMPNode destination) + { + try + { + for (Iterator it = iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + destination.addChild((XMPNode) child.clone()); + } + + for (Iterator it = iterateQualifier(); it.hasNext();) + { + XMPNode qualifier = (XMPNode) it.next(); + destination.addQualifier((XMPNode) qualifier.clone()); + } + } + catch (XMPException e) + { + // cannot happen (duplicate childs/quals do not exist in this node) + assert false; + } + + } + + + /** + * Renders this node and the tree unter this node in a human readable form. + * @param recursive Flag is qualifier and child nodes shall be rendered too + * @return Returns a multiline string containing the dump. + */ + public String dumpNode(boolean recursive) + { + StringBuffer result = new StringBuffer(512); + this.dumpNode(result, recursive, 0, 0); + return result.toString(); + } + + + /** + * @see Comparable#compareTo(Object) + */ + public int compareTo(Object xmpNode) + { + if (getOptions().isSchemaNode()) + { + return this.value.compareTo(((XMPNode) xmpNode).getValue()); + } + else + { + return this.name.compareTo(((XMPNode) xmpNode).getName()); + } + } + + + /** + * @return Returns the name. + */ + public String getName() + { + return name; + } + + + /** + * @param name The name to set. + */ + public void setName(String name) + { + this.name = name; + } + + + /** + * @return Returns the value. + */ + public String getValue() + { + return value; + } + + + /** + * @param value The value to set. + */ + public void setValue(String value) + { + this.value = value; + } + + + /** + * @return Returns the options. + */ + public PropertyOptions getOptions() + { + if (options == null) + { + options = new PropertyOptions(); + } + return options; + } + + + /** + * Updates the options of the node. + * @param options the options to set. + */ + public void setOptions(PropertyOptions options) + { + this.options = options; + } + + + /** + * @return Returns the implicit flag + */ + public boolean isImplicit() + { + return implicit; + } + + + /** + * @param implicit Sets the implicit node flag + */ + public void setImplicit(boolean implicit) + { + this.implicit = implicit; + } + + + /** + * @return Returns if the node contains aliases (applies only to schema nodes) + */ + public boolean getHasAliases() + { + return hasAliases; + } + + + /** + * @param hasAliases sets the flag that the node contains aliases + */ + public void setHasAliases(boolean hasAliases) + { + this.hasAliases = hasAliases; + } + + + /** + * @return Returns if the node contains aliases (applies only to schema nodes) + */ + public boolean isAlias() + { + return alias; + } + + + /** + * @param alias sets the flag that the node is an alias + */ + public void setAlias(boolean alias) + { + this.alias = alias; + } + + + /** + * @return the hasValueChild + */ + public boolean getHasValueChild() + { + return hasValueChild; + } + + + /** + * @param hasValueChild the hasValueChild to set + */ + public void setHasValueChild(boolean hasValueChild) + { + this.hasValueChild = hasValueChild; + } + + + + /** + * Sorts the complete datamodel according to the following rules: + * <ul> + * <li>Nodes at one level are sorted by name, that is prefix + local name + * <li>Starting at the root node the children and qualifier are sorted recursively, + * which the following exceptions. + * <li>Sorting will not be used for arrays. + * <li>Within qualifier "xml:lang" and/or "rdf:type" stay at the top in that order, + * all others are sorted. + * </ul> + */ + public void sort() + { + // sort qualifier + if (hasQualifier()) + { + XMPNode[] quals = (XMPNode[]) getQualifier() + .toArray(new XMPNode[getQualifierLength()]); + int sortFrom = 0; + while ( + quals.length > sortFrom && + (XMPConst.XML_LANG.equals(quals[sortFrom].getName()) || + "rdf:type".equals(quals[sortFrom].getName())) + ) + { + quals[sortFrom].sort(); + sortFrom++; + } + + Arrays.sort(quals, sortFrom, quals.length); + ListIterator it = qualifier.listIterator(); + for (int j = 0; j < quals.length; j++) + { + it.next(); + it.set(quals[j]); + quals[j].sort(); + } + } + + // sort children + if (hasChildren()) + { + if (!getOptions().isArray()) + { + Collections.sort(children); + } + for (Iterator it = iterateChildren(); it.hasNext();) + { + ((XMPNode) it.next()).sort(); + + } + } + } + + + + //------------------------------------------------------------------------------ private methods + + + /** + * Dumps this node and its qualifier and children recursively. + * <em>Note:</em> It creats empty options on every node. + * + * @param result the buffer to append the dump. + * @param recursive Flag is qualifier and child nodes shall be rendered too + * @param indent the current indent level. + * @param index the index within the parent node (important for arrays) + */ + private void dumpNode(StringBuffer result, boolean recursive, int indent, int index) + { + // write indent + for (int i = 0; i < indent; i++) + { + result.append('\t'); + } + + // render Node + if (parent != null) + { + if (getOptions().isQualifier()) + { + result.append('?'); + result.append(name); + } + else if (getParent().getOptions().isArray()) + { + result.append('['); + result.append(index); + result.append(']'); + } + else + { + result.append(name); + } + } + else + { + // applies only to the root node + result.append("ROOT NODE"); + if (name != null && name.length() > 0) + { + // the "about" attribute + result.append(" ("); + result.append(name); + result.append(')'); + } + } + + if (value != null && value.length() > 0) + { + result.append(" = \""); + result.append(value); + result.append('"'); + } + + // render options if at least one is set + if (getOptions().containsOneOf(0xffffffff)) + { + result.append("\t("); + result.append(getOptions().toString()); + result.append(" : "); + result.append(getOptions().getOptionsString()); + result.append(')'); + } + + result.append('\n'); + + // render qualifier + if (recursive && hasQualifier()) + { + XMPNode[] quals = (XMPNode[]) getQualifier() + .toArray(new XMPNode[getQualifierLength()]); + int i = 0; + while (quals.length > i && + (XMPConst.XML_LANG.equals(quals[i].getName()) || + "rdf:type".equals(quals[i].getName())) + ) + { + i++; + } + Arrays.sort(quals, i, quals.length); + for (i = 0; i < quals.length; i++) + { + XMPNode qualifier = quals[i]; + qualifier.dumpNode(result, recursive, indent + 2, i + 1); + } + } + + // render children + if (recursive && hasChildren()) + { + XMPNode[] children = (XMPNode[]) getChildren() + .toArray(new XMPNode[getChildrenLength()]); + if (!getOptions().isArray()) + { + Arrays.sort(children); + } + for (int i = 0; i < children.length; i++) + { + XMPNode child = children[i]; + child.dumpNode(result, recursive, indent + 1, i + 1); + } + } + } + + + /** + * @return Returns whether this node is a language qualifier. + */ + private boolean isLanguageNode() + { + return XMPConst.XML_LANG.equals(name); + } + + + /** + * @return Returns whether this node is a type qualifier. + */ + private boolean isTypeNode() + { + return "rdf:type".equals(name); + } + + + /** + * <em>Note:</em> This method should always be called when accessing 'children' to be sure + * that its initialized. + * @return Returns list of children that is lazy initialized. + */ + private List getChildren() + { + if (children == null) + { + children = new ArrayList(0); + } + return children; + } + + + /** + * @return Returns a read-only copy of child nodes list. + */ + public List getUnmodifiableChildren() + { + return Collections.unmodifiableList(new ArrayList(getChildren())); + } + + + /** + * @return Returns list of qualifier that is lazy initialized. + */ + private List getQualifier() + { + if (qualifier == null) + { + qualifier = new ArrayList(0); + } + return qualifier; + } + + + /** + * Sets the parent node, this is solely done by <code>addChild(...)</code> + * and <code>addQualifier()</code>. + * + * @param parent + * Sets the parent node. + */ + protected void setParent(XMPNode parent) + { + this.parent = parent; + } + + + /** + * Internal find. + * @param list the list to search in + * @param expr the search expression + * @return Returns the found node or <code>nulls</code>. + */ + private XMPNode find(List list, String expr) + { + + if (list != null) + { + for (Iterator it = list.iterator(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + if (child.getName().equals(expr)) + { + return child; + } + } + } + return null; + } + + + /** + * Checks that a node name is not existing on the same level, except for array items. + * @param childName the node name to check + * @throws XMPException Thrown if a node with the same name is existing. + */ + private void assertChildNotExisting(String childName) throws XMPException + { + if (!XMPConst.ARRAY_ITEM_NAME.equals(childName) && + findChildByName(childName) != null) + { + throw new XMPException("Duplicate property or field node '" + childName + "'", + XMPError.BADXMP); + } + } + + + /** + * Checks that a qualifier name is not existing on the same level. + * @param qualifierName the new qualifier name + * @throws XMPException Thrown if a node with the same name is existing. + */ + private void assertQualifierNotExisting(String qualifierName) throws XMPException + { + if (!XMPConst.ARRAY_ITEM_NAME.equals(qualifierName) && + findQualifierByName(qualifierName) != null) + { + throw new XMPException("Duplicate '" + qualifierName + "' qualifier", XMPError.BADXMP); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java b/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java new file mode 100644 index 0000000..8537469 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java @@ -0,0 +1,924 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.GregorianCalendar; +import java.util.Iterator; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPDateTime; +import com.adobe.xmp.XMPDateTimeFactory; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.XMPUtils; +import com.adobe.xmp.impl.xpath.XMPPath; +import com.adobe.xmp.impl.xpath.XMPPathSegment; +import com.adobe.xmp.options.AliasOptions; +import com.adobe.xmp.options.PropertyOptions; + + +/** + * Utilities for <code>XMPNode</code>. + * + * @since Aug 28, 2006 + */ +public class XMPNodeUtils implements XMPConst +{ + /** */ + static final int CLT_NO_VALUES = 0; + /** */ + static final int CLT_SPECIFIC_MATCH = 1; + /** */ + static final int CLT_SINGLE_GENERIC = 2; + /** */ + static final int CLT_MULTIPLE_GENERIC = 3; + /** */ + static final int CLT_XDEFAULT = 4; + /** */ + static final int CLT_FIRST_ITEM = 5; + + + /** + * Private Constructor + */ + private XMPNodeUtils() + { + // EMPTY + } + + + /** + * Find or create a schema node if <code>createNodes</code> is false and + * + * @param tree the root of the xmp tree. + * @param namespaceURI a namespace + * @param createNodes a flag indicating if the node shall be created if not found. + * <em>Note:</em> The namespace must be registered prior to this call. + * + * @return Returns the schema node if found, <code>null</code> otherwise. + * Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> + * returned a valid node. + * @throws XMPException An exception is only thrown if an error occurred, not if a + * node was not found. + */ + static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, + boolean createNodes) + throws XMPException + { + return findSchemaNode(tree, namespaceURI, null, createNodes); + } + + + /** + * Find or create a schema node if <code>createNodes</code> is true. + * + * @param tree the root of the xmp tree. + * @param namespaceURI a namespace + * @param suggestedPrefix If a prefix is suggested, the namespace is allowed to be registered. + * @param createNodes a flag indicating if the node shall be created if not found. + * <em>Note:</em> The namespace must be registered prior to this call. + * + * @return Returns the schema node if found, <code>null</code> otherwise. + * Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> + * returned a valid node. + * @throws XMPException An exception is only thrown if an error occurred, not if a + * node was not found. + */ + static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, String suggestedPrefix, + boolean createNodes) + throws XMPException + { + assert tree.getParent() == null; // make sure that its the root + XMPNode schemaNode = tree.findChildByName(namespaceURI); + + if (schemaNode == null && createNodes) + { + schemaNode = new XMPNode(namespaceURI, + new PropertyOptions() + .setSchemaNode(true)); + schemaNode.setImplicit(true); + + // only previously registered schema namespaces are allowed in the XMP tree. + String prefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(namespaceURI); + if (prefix == null) + { + if (suggestedPrefix != null && suggestedPrefix.length() != 0) + { + prefix = XMPMetaFactory.getSchemaRegistry().registerNamespace(namespaceURI, + suggestedPrefix); + } + else + { + throw new XMPException("Unregistered schema namespace URI", + XMPError.BADSCHEMA); + } + } + + schemaNode.setValue(prefix); + + tree.addChild(schemaNode); + } + + return schemaNode; + } + + + /** + * Find or create a child node under a given parent node. If the parent node is no + * Returns the found or created child node. + * + * @param parent + * the parent node + * @param childName + * the node name to find + * @param createNodes + * flag, if new nodes shall be created. + * @return Returns the found or created node or <code>null</code>. + * @throws XMPException Thrown if + */ + static XMPNode findChildNode(XMPNode parent, String childName, boolean createNodes) + throws XMPException + { + if (!parent.getOptions().isSchemaNode() && !parent.getOptions().isStruct()) + { + if (!parent.isImplicit()) + { + throw new XMPException("Named children only allowed for schemas and structs", + XMPError.BADXPATH); + } + else if (parent.getOptions().isArray()) + { + throw new XMPException("Named children not allowed for arrays", + XMPError.BADXPATH); + } + else if (createNodes) + { + parent.getOptions().setStruct(true); + } + } + + XMPNode childNode = parent.findChildByName(childName); + + if (childNode == null && createNodes) + { + PropertyOptions options = new PropertyOptions(); + childNode = new XMPNode(childName, options); + childNode.setImplicit(true); + parent.addChild(childNode); + } + + assert childNode != null || !createNodes; + + return childNode; + } + + + /** + * Follow an expanded path expression to find or create a node. + * + * @param xmpTree the node to begin the search. + * @param xpath the complete xpath + * @param createNodes flag if nodes shall be created + * (when called by <code>setProperty()</code>) + * @param leafOptions the options for the created leaf nodes (only when + * <code>createNodes == true</code>). + * @return Returns the node if found or created or <code>null</code>. + * @throws XMPException An exception is only thrown if an error occurred, + * not if a node was not found. + */ + static XMPNode findNode(XMPNode xmpTree, XMPPath xpath, boolean createNodes, + PropertyOptions leafOptions) throws XMPException + { + // check if xpath is set. + if (xpath == null || xpath.size() == 0) + { + throw new XMPException("Empty XMPPath", XMPError.BADXPATH); + } + + // Root of implicitly created subtree to possible delete it later. + // Valid only if leaf is new. + XMPNode rootImplicitNode = null; + XMPNode currNode = null; + + // resolve schema step + currNode = findSchemaNode(xmpTree, + xpath.getSegment(XMPPath.STEP_SCHEMA).getName(), createNodes); + if (currNode == null) + { + return null; + } + else if (currNode.isImplicit()) + { + currNode.setImplicit(false); // Clear the implicit node bit. + rootImplicitNode = currNode; // Save the top most implicit node. + } + + + // Now follow the remaining steps of the original XMPPath. + try + { + for (int i = 1; i < xpath.size(); i++) + { + currNode = followXPathStep(currNode, xpath.getSegment(i), createNodes); + if (currNode == null) + { + if (createNodes) + { + // delete implicitly created nodes + deleteNode(rootImplicitNode); + } + return null; + } + else if (currNode.isImplicit()) + { + // clear the implicit node flag + currNode.setImplicit(false); + + // if node is an ALIAS (can be only in root step, auto-create array + // when the path has been resolved from a not simple alias type + if (i == 1 && + xpath.getSegment(i).isAlias() && + xpath.getSegment(i).getAliasForm() != 0) + { + currNode.getOptions().setOption(xpath.getSegment(i).getAliasForm(), true); + } + // "CheckImplicitStruct" in C++ + else if (i < xpath.size() - 1 && + xpath.getSegment(i).getKind() == XMPPath.STRUCT_FIELD_STEP && + !currNode.getOptions().isCompositeProperty()) + { + currNode.getOptions().setStruct(true); + } + + if (rootImplicitNode == null) + { + rootImplicitNode = currNode; // Save the top most implicit node. + } + } + } + } + catch (XMPException e) + { + // if new notes have been created prior to the error, delete them + if (rootImplicitNode != null) + { + deleteNode(rootImplicitNode); + } + throw e; + } + + + if (rootImplicitNode != null) + { + // set options only if a node has been successful created + currNode.getOptions().mergeWith(leafOptions); + currNode.setOptions(currNode.getOptions()); + } + + return currNode; + } + + + /** + * Deletes the the given node and its children from its parent. + * Takes care about adjusting the flags. + * @param node the top-most node to delete. + */ + static void deleteNode(XMPNode node) + { + XMPNode parent = node.getParent(); + + if (node.getOptions().isQualifier()) + { + // root is qualifier + parent.removeQualifier(node); + } + else + { + // root is NO qualifier + parent.removeChild(node); + } + + // delete empty Schema nodes + if (!parent.hasChildren() && parent.getOptions().isSchemaNode()) + { + parent.getParent().removeChild(parent); + } + } + + + /** + * This is setting the value of a leaf node. + * + * @param node an XMPNode + * @param value a value + */ + static void setNodeValue(XMPNode node, Object value) + { + String strValue = serializeNodeValue(value); + if (!(node.getOptions().isQualifier() && XML_LANG.equals(node.getName()))) + { + node.setValue(strValue); + } + else + { + node.setValue(Utils.normalizeLangValue(strValue)); + } + } + + + /** + * Verifies the PropertyOptions for consistancy and updates them as needed. + * If options are <code>null</code> they are created with default values. + * + * @param options the <code>PropertyOptions</code> + * @param itemValue the node value to set + * @return Returns the updated options. + * @throws XMPException If the options are not consistant. + */ + static PropertyOptions verifySetOptions(PropertyOptions options, Object itemValue) + throws XMPException + { + // create empty and fix existing options + if (options == null) + { + // set default options + options = new PropertyOptions(); + } + + if (options.isArrayAltText()) + { + options.setArrayAlternate(true); + } + + if (options.isArrayAlternate()) + { + options.setArrayOrdered(true); + } + + if (options.isArrayOrdered()) + { + options.setArray(true); + } + + if (options.isCompositeProperty() && itemValue != null && itemValue.toString().length() > 0) + { + throw new XMPException("Structs and arrays can't have values", + XMPError.BADOPTIONS); + } + + options.assertConsistency(options.getOptions()); + + return options; + } + + + /** + * Converts the node value to String, apply special conversions for defined + * types in XMP. + * + * @param value + * the node value to set + * @return Returns the String representation of the node value. + */ + static String serializeNodeValue(Object value) + { + String strValue; + if (value == null) + { + strValue = null; + } + else if (value instanceof Boolean) + { + strValue = XMPUtils.convertFromBoolean(((Boolean) value).booleanValue()); + } + else if (value instanceof Integer) + { + strValue = XMPUtils.convertFromInteger(((Integer) value).intValue()); + } + else if (value instanceof Long) + { + strValue = XMPUtils.convertFromLong(((Long) value).longValue()); + } + else if (value instanceof Double) + { + strValue = XMPUtils.convertFromDouble(((Double) value).doubleValue()); + } + else if (value instanceof XMPDateTime) + { + strValue = XMPUtils.convertFromDate((XMPDateTime) value); + } + else if (value instanceof GregorianCalendar) + { + XMPDateTime dt = XMPDateTimeFactory.createFromCalendar((GregorianCalendar) value); + strValue = XMPUtils.convertFromDate(dt); + } + else if (value instanceof byte[]) + { + strValue = XMPUtils.encodeBase64((byte[]) value); + } + else + { + strValue = value.toString(); + } + + return strValue != null ? Utils.removeControlChars(strValue) : null; + } + + + /** + * After processing by ExpandXPath, a step can be of these forms: + * <ul> + * <li>qualName - A top level property or struct field. + * <li>[index] - An element of an array. + * <li>[last()] - The last element of an array. + * <li>[qualName="value"] - An element in an array of structs, chosen by a field value. + * <li>[?qualName="value"] - An element in an array, chosen by a qualifier value. + * <li>?qualName - A general qualifier. + * </ul> + * Find the appropriate child node, resolving aliases, and optionally creating nodes. + * + * @param parentNode the node to start to start from + * @param nextStep the xpath segment + * @param createNodes + * @return returns the found or created XMPPath node + * @throws XMPException + */ + private static XMPNode followXPathStep( + XMPNode parentNode, + XMPPathSegment nextStep, + boolean createNodes) throws XMPException + { + XMPNode nextNode = null; + int index = 0; + int stepKind = nextStep.getKind(); + + if (stepKind == XMPPath.STRUCT_FIELD_STEP) + { + nextNode = findChildNode(parentNode, nextStep.getName(), createNodes); + } + else if (stepKind == XMPPath.QUALIFIER_STEP) + { + nextNode = findQualifierNode( + parentNode, nextStep.getName().substring(1), createNodes); + } + else + { + // This is an array indexing step. First get the index, then get the node. + + if (!parentNode.getOptions().isArray()) + { + throw new XMPException("Indexing applied to non-array", XMPError.BADXPATH); + } + + if (stepKind == XMPPath.ARRAY_INDEX_STEP) + { + index = findIndexedItem(parentNode, nextStep.getName(), createNodes); + } + else if (stepKind == XMPPath.ARRAY_LAST_STEP) + { + index = parentNode.getChildrenLength(); + } + else if (stepKind == XMPPath.FIELD_SELECTOR_STEP) + { + String[] result = Utils.splitNameAndValue(nextStep.getName()); + String fieldName = result[0]; + String fieldValue = result[1]; + index = lookupFieldSelector(parentNode, fieldName, fieldValue); + } + else if (stepKind == XMPPath.QUAL_SELECTOR_STEP) + { + String[] result = Utils.splitNameAndValue(nextStep.getName()); + String qualName = result[0]; + String qualValue = result[1]; + index = lookupQualSelector( + parentNode, qualName, qualValue, nextStep.getAliasForm()); + } + else + { + throw new XMPException("Unknown array indexing step in FollowXPathStep", + XMPError.INTERNALFAILURE); + } + + if (1 <= index && index <= parentNode.getChildrenLength()) + { + nextNode = parentNode.getChild(index); + } + } + + return nextNode; + } + + + /** + * Find or create a qualifier node under a given parent node. Returns a pointer to the + * qualifier node, and optionally an iterator for the node's position in + * the parent's vector of qualifiers. The iterator is unchanged if no qualifier node (null) + * is returned. + * <em>Note:</em> On entry, the qualName parameter must not have the leading '?' from the + * XMPPath step. + * + * @param parent the parent XMPNode + * @param qualName the qualifier name + * @param createNodes flag if nodes shall be created + * @return Returns the qualifier node if found or created, <code>null</code> otherwise. + * @throws XMPException + */ + private static XMPNode findQualifierNode(XMPNode parent, String qualName, boolean createNodes) + throws XMPException + { + assert !qualName.startsWith("?"); + + XMPNode qualNode = parent.findQualifierByName(qualName); + + if (qualNode == null && createNodes) + { + qualNode = new XMPNode(qualName, null); + qualNode.setImplicit(true); + + parent.addQualifier(qualNode); + } + + return qualNode; + } + + + /** + * @param arrayNode an array node + * @param segment the segment containing the array index + * @param createNodes flag if new nodes are allowed to be created. + * @return Returns the index or index = -1 if not found + * @throws XMPException Throws Exceptions + */ + private static int findIndexedItem(XMPNode arrayNode, String segment, boolean createNodes) + throws XMPException + { + int index = 0; + + try + { + segment = segment.substring(1, segment.length() - 1); + index = Integer.parseInt(segment); + if (index < 1) + { + throw new XMPException("Array index must be larger than zero", + XMPError.BADXPATH); + } + } + catch (NumberFormatException e) + { + throw new XMPException("Array index not digits.", XMPError.BADXPATH); + } + + if (createNodes && index == arrayNode.getChildrenLength() + 1) + { + // Append a new last + 1 node. + XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, null); + newItem.setImplicit(true); + arrayNode.addChild(newItem); + } + + return index; + } + + + /** + * Searches for a field selector in a node: + * [fieldName="value] - an element in an array of structs, chosen by a field value. + * No implicit nodes are created by field selectors. + * + * @param arrayNode + * @param fieldName + * @param fieldValue + * @return Returns the index of the field if found, otherwise -1. + * @throws XMPException + */ + private static int lookupFieldSelector(XMPNode arrayNode, String fieldName, String fieldValue) + throws XMPException + { + int result = -1; + + for (int index = 1; index <= arrayNode.getChildrenLength() && result < 0; index++) + { + XMPNode currItem = arrayNode.getChild(index); + + if (!currItem.getOptions().isStruct()) + { + throw new XMPException("Field selector must be used on array of struct", + XMPError.BADXPATH); + } + + for (int f = 1; f <= currItem.getChildrenLength(); f++) + { + XMPNode currField = currItem.getChild(f); + if (!fieldName.equals(currField.getName())) + { + continue; + } + if (fieldValue.equals(currField.getValue())) + { + result = index; + break; + } + } + } + + return result; + } + + + /** + * Searches for a qualifier selector in a node: + * [?qualName="value"] - an element in an array, chosen by a qualifier value. + * No implicit nodes are created for qualifier selectors, + * except for an alias to an x-default item. + * + * @param arrayNode an array node + * @param qualName the qualifier name + * @param qualValue the qualifier value + * @param aliasForm in case the qual selector results from an alias, + * an x-default node is created if there has not been one. + * @return Returns the index of th + * @throws XMPException + */ + private static int lookupQualSelector(XMPNode arrayNode, String qualName, + String qualValue, int aliasForm) throws XMPException + { + if (XML_LANG.equals(qualName)) + { + qualValue = Utils.normalizeLangValue(qualValue); + int index = XMPNodeUtils.lookupLanguageItem(arrayNode, qualValue); + if (index < 0 && (aliasForm & AliasOptions.PROP_ARRAY_ALT_TEXT) > 0) + { + XMPNode langNode = new XMPNode(ARRAY_ITEM_NAME, null); + XMPNode xdefault = new XMPNode(XML_LANG, X_DEFAULT, null); + langNode.addQualifier(xdefault); + arrayNode.addChild(1, langNode); + return 1; + } + else + { + return index; + } + } + else + { + for (int index = 1; index < arrayNode.getChildrenLength(); index++) + { + XMPNode currItem = arrayNode.getChild(index); + + for (Iterator it = currItem.iterateQualifier(); it.hasNext();) + { + XMPNode qualifier = (XMPNode) it.next(); + if (qualName.equals(qualifier.getName()) && + qualValue.equals(qualifier.getValue())) + { + return index; + } + } + } + return -1; + } + } + + + /** + * Make sure the x-default item is first. Touch up "single value" + * arrays that have a default plus one real language. This case should have + * the same value for both items. Older Adobe apps were hardwired to only + * use the "x-default" item, so we copy that value to the other + * item. + * + * @param arrayNode + * an alt text array node + */ + static void normalizeLangArray(XMPNode arrayNode) + { + if (!arrayNode.getOptions().isArrayAltText()) + { + return; + } + + // check if node with x-default qual is first place + for (int i = 2; i <= arrayNode.getChildrenLength(); i++) + { + XMPNode child = arrayNode.getChild(i); + if (child.hasQualifier() && X_DEFAULT.equals(child.getQualifier(1).getValue())) + { + // move node to first place + try + { + arrayNode.removeChild(i); + arrayNode.addChild(1, child); + } + catch (XMPException e) + { + // cannot occur, because same child is removed before + assert false; + } + + if (i == 2) + { + arrayNode.getChild(2).setValue(child.getValue()); + } + break; + } + } + } + + + /** + * See if an array is an alt-text array. If so, make sure the x-default item + * is first. + * + * @param arrayNode + * the array node to check if its an alt-text array + */ + static void detectAltText(XMPNode arrayNode) + { + if (arrayNode.getOptions().isArrayAlternate() && arrayNode.hasChildren()) + { + boolean isAltText = false; + for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + if (child.getOptions().getHasLanguage()) + { + isAltText = true; + break; + } + } + + if (isAltText) + { + arrayNode.getOptions().setArrayAltText(true); + normalizeLangArray(arrayNode); + } + } + } + + + /** + * Appends a language item to an alt text array. + * + * @param arrayNode the language array + * @param itemLang the language of the item + * @param itemValue the content of the item + * @throws XMPException Thrown if a duplicate property is added + */ + static void appendLangItem(XMPNode arrayNode, String itemLang, String itemValue) + throws XMPException + { + XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null); + XMPNode langQual = new XMPNode(XML_LANG, itemLang, null); + newItem.addQualifier(langQual); + + if (!X_DEFAULT.equals(langQual.getValue())) + { + arrayNode.addChild(newItem); + } + else + { + arrayNode.addChild(1, newItem); + } + } + + + /** + * <ol> + * <li>Look for an exact match with the specific language. + * <li>If a generic language is given, look for partial matches. + * <li>Look for an "x-default"-item. + * <li>Choose the first item. + * </ol> + * + * @param arrayNode + * the alt text array node + * @param genericLang + * the generic language + * @param specificLang + * the specific language + * @return Returns the kind of match as an Integer and the found node in an + * array. + * + * @throws XMPException + */ + static Object[] chooseLocalizedText(XMPNode arrayNode, String genericLang, String specificLang) + throws XMPException + { + // See if the array has the right form. Allow empty alt arrays, + // that is what parsing returns. + if (!arrayNode.getOptions().isArrayAltText()) + { + throw new XMPException("Localized text array is not alt-text", XMPError.BADXPATH); + } + else if (!arrayNode.hasChildren()) + { + return new Object[] { new Integer(XMPNodeUtils.CLT_NO_VALUES), null }; + } + + int foundGenericMatches = 0; + XMPNode resultNode = null; + XMPNode xDefault = null; + + // Look for the first partial match with the generic language. + for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) + { + XMPNode currItem = (XMPNode) it.next(); + + // perform some checks on the current item + if (currItem.getOptions().isCompositeProperty()) + { + throw new XMPException("Alt-text array item is not simple", XMPError.BADXPATH); + } + else if (!currItem.hasQualifier() + || !XML_LANG.equals(currItem.getQualifier(1).getName())) + { + throw new XMPException("Alt-text array item has no language qualifier", + XMPError.BADXPATH); + } + + String currLang = currItem.getQualifier(1).getValue(); + + // Look for an exact match with the specific language. + if (specificLang.equals(currLang)) + { + return new Object[] { new Integer(XMPNodeUtils.CLT_SPECIFIC_MATCH), currItem }; + } + else if (genericLang != null && currLang.startsWith(genericLang)) + { + if (resultNode == null) + { + resultNode = currItem; + } + // ! Don't return/break, need to look for other matches. + foundGenericMatches++; + } + else if (X_DEFAULT.equals(currLang)) + { + xDefault = currItem; + } + } + + // evaluate loop + if (foundGenericMatches == 1) + { + return new Object[] { new Integer(XMPNodeUtils.CLT_SINGLE_GENERIC), resultNode }; + } + else if (foundGenericMatches > 1) + { + return new Object[] { new Integer(XMPNodeUtils.CLT_MULTIPLE_GENERIC), resultNode }; + } + else if (xDefault != null) + { + return new Object[] { new Integer(XMPNodeUtils.CLT_XDEFAULT), xDefault }; + } + else + { + // Everything failed, choose the first item. + return new Object[] { new Integer(XMPNodeUtils.CLT_FIRST_ITEM), arrayNode.getChild(1) }; + } + } + + + /** + * Looks for the appropriate language item in a text alternative array.item + * + * @param arrayNode + * an array node + * @param language + * the requested language + * @return Returns the index if the language has been found, -1 otherwise. + * @throws XMPException + */ + static int lookupLanguageItem(XMPNode arrayNode, String language) throws XMPException + { + if (!arrayNode.getOptions().isArray()) + { + throw new XMPException("Language item must be used on array", XMPError.BADXPATH); + } + + for (int index = 1; index <= arrayNode.getChildrenLength(); index++) + { + XMPNode child = arrayNode.getChild(index); + if (!child.hasQualifier() || !XML_LANG.equals(child.getQualifier(1).getName())) + { + continue; + } + else if (language.equals(child.getQualifier(1).getValue())) + { + return index; + } + } + + return -1; + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java b/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java new file mode 100644 index 0000000..d51d7de --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java @@ -0,0 +1,695 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPDateTime; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.XMPUtils; +import com.adobe.xmp.impl.xpath.XMPPath; +import com.adobe.xmp.impl.xpath.XMPPathParser; +import com.adobe.xmp.options.ParseOptions; +import com.adobe.xmp.options.PropertyOptions; +import com.adobe.xmp.properties.XMPAliasInfo; + +/** + * @since Aug 18, 2006 + */ +public class XMPNormalizer +{ + /** caches the correct dc-property array forms */ + private static Map dcArrayForms; + /** init char tables */ + static + { + initDCArrays(); + } + + + /** + * Hidden constructor + */ + private XMPNormalizer() + { + // EMPTY + } + + + /** + * Normalizes a raw parsed XMPMeta-Object + * @param xmp the raw metadata object + * @param options the parsing options + * @return Returns the normalized metadata object + * @throws XMPException Collects all severe processing errors. + */ + static XMPMeta process(XMPMetaImpl xmp, ParseOptions options) throws XMPException + { + XMPNode tree = xmp.getRoot(); + + touchUpDataModel(xmp); + moveExplicitAliases(tree, options); + + tweakOldXMP(tree); + + deleteEmptySchemas(tree); + + return xmp; + } + + + /** + * Tweak old XMP: Move an instance ID from rdf:about to the + * <em>xmpMM:InstanceID</em> property. An old instance ID usually looks + * like "uuid:bac965c4-9d87-11d9-9a30-000d936b79c4", plus InDesign + * 3.0 wrote them like "bac965c4-9d87-11d9-9a30-000d936b79c4". If + * the name looks like a UUID simply move it to <em>xmpMM:InstanceID</em>, + * don't worry about any existing <em>xmpMM:InstanceID</em>. Both will + * only be present when a newer file with the <em>xmpMM:InstanceID</em> + * property is updated by an old app that uses <em>rdf:about</em>. + * + * @param tree the root of the metadata tree + * @throws XMPException Thrown if tweaking fails. + */ + private static void tweakOldXMP(XMPNode tree) throws XMPException + { + if (tree.getName() != null && tree.getName().length() >= Utils.UUID_LENGTH) + { + String nameStr = tree.getName().toLowerCase(); + if (nameStr.startsWith("uuid:")) + { + nameStr = nameStr.substring(5); + } + + if (Utils.checkUUIDFormat(nameStr)) + { + // move UUID to xmpMM:InstanceID and remove it from the root node + XMPPath path = XMPPathParser.expandXPath(XMPConst.NS_XMP_MM, "InstanceID"); + XMPNode idNode = XMPNodeUtils.findNode (tree, path, true, null); + if (idNode != null) + { + idNode.setOptions(null); // Clobber any existing xmpMM:InstanceID. + idNode.setValue("uuid:" + nameStr); + idNode.removeChildren(); + idNode.removeQualifiers(); + tree.setName(null); + } + else + { + throw new XMPException("Failure creating xmpMM:InstanceID", + XMPError.INTERNALFAILURE); + } + } + } + } + + + /** + * Visit all schemas to do general fixes and handle special cases. + * + * @param xmp the metadata object implementation + * @throws XMPException Thrown if the normalisation fails. + */ + private static void touchUpDataModel(XMPMetaImpl xmp) throws XMPException + { + // make sure the DC schema is existing, because it might be needed within the normalization + // if not touched it will be removed by removeEmptySchemas + XMPNodeUtils.findSchemaNode(xmp.getRoot(), XMPConst.NS_DC, true); + + // Do the special case fixes within each schema. + for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) + { + XMPNode currSchema = (XMPNode) it.next(); + if (XMPConst.NS_DC.equals(currSchema.getName())) + { + normalizeDCArrays(currSchema); + } + else if (XMPConst.NS_EXIF.equals(currSchema.getName())) + { + // Do a special case fix for exif:GPSTimeStamp. + fixGPSTimeStamp(currSchema); + XMPNode arrayNode = XMPNodeUtils.findChildNode(currSchema, "exif:UserComment", + false); + if (arrayNode != null) + { + repairAltText(arrayNode); + } + } + else if (XMPConst.NS_DM.equals(currSchema.getName())) + { + // Do a special case migration of xmpDM:copyright to + // dc:rights['x-default']. + XMPNode dmCopyright = XMPNodeUtils.findChildNode(currSchema, "xmpDM:copyright", + false); + if (dmCopyright != null) + { + migrateAudioCopyright(xmp, dmCopyright); + } + } + else if (XMPConst.NS_XMP_RIGHTS.equals(currSchema.getName())) + { + XMPNode arrayNode = XMPNodeUtils.findChildNode(currSchema, "xmpRights:UsageTerms", + false); + if (arrayNode != null) + { + repairAltText(arrayNode); + } + } + } + } + + + /** + * Undo the denormalization performed by the XMP used in Acrobat 5.<br> + * If a Dublin Core array had only one item, it was serialized as a simple + * property. <br> + * The <code>xml:lang</code> attribute was dropped from an + * <code>alt-text</code> item if the language was <code>x-default</code>. + * + * @param dcSchema the DC schema node + * @throws XMPException Thrown if normalization fails + */ + private static void normalizeDCArrays(XMPNode dcSchema) throws XMPException + { + for (int i = 1; i <= dcSchema.getChildrenLength(); i++) + { + XMPNode currProp = dcSchema.getChild(i); + + PropertyOptions arrayForm = (PropertyOptions) dcArrayForms.get(currProp.getName()); + if (arrayForm == null) + { + continue; + } + else if (currProp.getOptions().isSimple()) + { + // create a new array and add the current property as child, + // if it was formerly simple + XMPNode newArray = new XMPNode(currProp.getName(), arrayForm); + currProp.setName(XMPConst.ARRAY_ITEM_NAME); + newArray.addChild(currProp); + dcSchema.replaceChild(i, newArray); + + // fix language alternatives + if (arrayForm.isArrayAltText() && !currProp.getOptions().getHasLanguage()) + { + XMPNode newLang = new XMPNode(XMPConst.XML_LANG, XMPConst.X_DEFAULT, null); + currProp.addQualifier(newLang); + } + } + else + { + // clear array options and add corrected array form if it has been an array before + currProp.getOptions().setOption( + PropertyOptions.ARRAY | + PropertyOptions.ARRAY_ORDERED | + PropertyOptions.ARRAY_ALTERNATE | + PropertyOptions.ARRAY_ALT_TEXT, + false); + currProp.getOptions().mergeWith(arrayForm); + + if (arrayForm.isArrayAltText()) + { + // applying for "dc:description", "dc:rights", "dc:title" + repairAltText(currProp); + } + } + + } + } + + + /** + * Make sure that the array is well-formed AltText. Each item must be simple + * and have an "xml:lang" qualifier. If repairs are needed, keep simple + * non-empty items by adding the "xml:lang" with value "x-repair". + * @param arrayNode the property node of the array to repair. + * @throws XMPException Forwards unexpected exceptions. + */ + private static void repairAltText(XMPNode arrayNode) throws XMPException + { + if (arrayNode == null || + !arrayNode.getOptions().isArray()) + { + // Already OK or not even an array. + return; + } + + // fix options + arrayNode.getOptions().setArrayOrdered(true).setArrayAlternate(true).setArrayAltText(true); + + for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) + { + XMPNode currChild = (XMPNode) it.next(); + if (currChild.getOptions().isCompositeProperty()) + { + // Delete non-simple children. + it.remove(); + } + else if (!currChild.getOptions().getHasLanguage()) + { + String childValue = currChild.getValue(); + if (childValue == null || childValue.length() == 0) + { + // Delete empty valued children that have no xml:lang. + it.remove(); + } + else + { + // Add an xml:lang qualifier with the value "x-repair". + XMPNode repairLang = new XMPNode(XMPConst.XML_LANG, "x-repair", null); + currChild.addQualifier(repairLang); + } + } + } + } + + + /** + * Visit all of the top level nodes looking for aliases. If there is + * no base, transplant the alias subtree. If there is a base and strict + * aliasing is on, make sure the alias and base subtrees match. + * + * @param tree the root of the metadata tree + * @param options th parsing options + * @throws XMPException Forwards XMP errors + */ + private static void moveExplicitAliases(XMPNode tree, ParseOptions options) + throws XMPException + { + if (!tree.getHasAliases()) + { + return; + } + tree.setHasAliases(false); + + boolean strictAliasing = options.getStrictAliasing(); + + for (Iterator schemaIt = tree.getUnmodifiableChildren().iterator(); schemaIt.hasNext();) + { + XMPNode currSchema = (XMPNode) schemaIt.next(); + if (!currSchema.getHasAliases()) + { + continue; + } + + for (Iterator propertyIt = currSchema.iterateChildren(); propertyIt.hasNext();) + { + XMPNode currProp = (XMPNode) propertyIt.next(); + + if (!currProp.isAlias()) + { + continue; + } + + currProp.setAlias(false); + + // Find the base path, look for the base schema and root node. + XMPAliasInfo info = XMPMetaFactory.getSchemaRegistry() + .findAlias(currProp.getName()); + if (info != null) + { + // find or create schema + XMPNode baseSchema = XMPNodeUtils.findSchemaNode(tree, info + .getNamespace(), null, true); + baseSchema.setImplicit(false); + + XMPNode baseNode = XMPNodeUtils + .findChildNode(baseSchema, + info.getPrefix() + info.getPropName(), false); + if (baseNode == null) + { + if (info.getAliasForm().isSimple()) + { + // A top-to-top alias, transplant the property. + // change the alias property name to the base name + String qname = info.getPrefix() + info.getPropName(); + currProp.setName(qname); + baseSchema.addChild(currProp); + // remove the alias property + propertyIt.remove(); + } + else + { + // An alias to an array item, + // create the array and transplant the property. + baseNode = new XMPNode(info.getPrefix() + info.getPropName(), info + .getAliasForm().toPropertyOptions()); + baseSchema.addChild(baseNode); + transplantArrayItemAlias (propertyIt, currProp, baseNode); + } + + } + else if (info.getAliasForm().isSimple()) + { + // The base node does exist and this is a top-to-top alias. + // Check for conflicts if strict aliasing is on. + // Remove and delete the alias subtree. + if (strictAliasing) + { + compareAliasedSubtrees (currProp, baseNode, true); + } + + propertyIt.remove(); + } + else + { + // This is an alias to an array item and the array exists. + // Look for the aliased item. + // Then transplant or check & delete as appropriate. + + XMPNode itemNode = null; + if (info.getAliasForm().isArrayAltText()) + { + int xdIndex = XMPNodeUtils.lookupLanguageItem(baseNode, + XMPConst.X_DEFAULT); + if (xdIndex != -1) + { + itemNode = baseNode.getChild(xdIndex); + } + } + else if (baseNode.hasChildren()) + { + itemNode = baseNode.getChild(1); + } + + if (itemNode == null) + { + transplantArrayItemAlias (propertyIt, currProp, baseNode); + } + else + { + if (strictAliasing) + { + compareAliasedSubtrees (currProp, itemNode, true); + } + + propertyIt.remove(); + } + } + } + } + currSchema.setHasAliases(false); + } + } + + + /** + * Moves an alias node of array form to another schema into an array + * @param propertyIt the property iterator of the old schema (used to delete the property) + * @param childNode the node to be moved + * @param baseArray the base array for the array item + * @throws XMPException Forwards XMP errors + */ + private static void transplantArrayItemAlias(Iterator propertyIt, XMPNode childNode, + XMPNode baseArray) throws XMPException + { + if (baseArray.getOptions().isArrayAltText()) + { + if (childNode.getOptions().getHasLanguage()) + { + throw new XMPException("Alias to x-default already has a language qualifier", + XMPError.BADXMP); + } + + XMPNode langQual = new XMPNode(XMPConst.XML_LANG, XMPConst.X_DEFAULT, null); + childNode.addQualifier(langQual); + } + + propertyIt.remove(); + childNode.setName(XMPConst.ARRAY_ITEM_NAME); + baseArray.addChild(childNode); + } + + + /** + * Fixes the GPS Timestamp in EXIF. + * @param exifSchema the EXIF schema node + * @throws XMPException Thrown if the date conversion fails. + */ + private static void fixGPSTimeStamp(XMPNode exifSchema) + throws XMPException + { + // Note: if dates are not found the convert-methods throws an exceptions, + // and this methods returns. + XMPNode gpsDateTime = XMPNodeUtils.findChildNode(exifSchema, "exif:GPSTimeStamp", false); + if (gpsDateTime == null) + { + return; + } + + try + { + XMPDateTime binGPSStamp; + XMPDateTime binOtherDate; + + binGPSStamp = XMPUtils.convertToDate(gpsDateTime.getValue()); + if (binGPSStamp.getYear() != 0 || + binGPSStamp.getMonth() != 0 || + binGPSStamp.getDay() != 0) + { + return; + } + + XMPNode otherDate = XMPNodeUtils.findChildNode(exifSchema, "exif:DateTimeOriginal", + false); + if (otherDate == null) + { + otherDate = XMPNodeUtils.findChildNode(exifSchema, "exif:DateTimeDigitized", false); + } + + binOtherDate = XMPUtils.convertToDate(otherDate.getValue()); + Calendar cal = binGPSStamp.getCalendar(); + cal.set(Calendar.YEAR, binOtherDate.getYear()); + cal.set(Calendar.MONTH, binOtherDate.getMonth()); + cal.set(Calendar.DAY_OF_MONTH, binOtherDate.getDay()); + binGPSStamp = new XMPDateTimeImpl(cal); + gpsDateTime.setValue(XMPUtils.convertFromDate (binGPSStamp)); + } + catch (XMPException e) + { + // Don't let a missing or bad date stop other things. + return; + } + } + + + + /** + * Remove all empty schemas from the metadata tree that were generated during the rdf parsing. + * @param tree the root of the metadata tree + */ + private static void deleteEmptySchemas(XMPNode tree) + { + // Delete empty schema nodes. Do this last, other cleanup can make empty + // schema. + + for (Iterator it = tree.iterateChildren(); it.hasNext();) + { + XMPNode schema = (XMPNode) it.next(); + if (!schema.hasChildren()) + { + it.remove(); + } + } + } + + + /** + * The outermost call is special. The names almost certainly differ. The + * qualifiers (and hence options) will differ for an alias to the x-default + * item of a langAlt array. + * + * @param aliasNode the alias node + * @param baseNode the base node of the alias + * @param outerCall marks the outer call of the recursion + * @throws XMPException Forwards XMP errors + */ + private static void compareAliasedSubtrees(XMPNode aliasNode, XMPNode baseNode, + boolean outerCall) throws XMPException + { + if (!aliasNode.getValue().equals(baseNode.getValue()) || + aliasNode.getChildrenLength() != baseNode.getChildrenLength()) + { + throw new XMPException("Mismatch between alias and base nodes", XMPError.BADXMP); + } + + if ( + !outerCall && + (!aliasNode.getName().equals(baseNode.getName()) || + !aliasNode.getOptions().equals(baseNode.getOptions()) || + aliasNode.getQualifierLength() != baseNode.getQualifierLength()) + ) + { + throw new XMPException("Mismatch between alias and base nodes", + XMPError.BADXMP); + } + + for (Iterator an = aliasNode.iterateChildren(), + bn = baseNode.iterateChildren(); + an.hasNext() && bn.hasNext();) + { + XMPNode aliasChild = (XMPNode) an.next(); + XMPNode baseChild = (XMPNode) bn.next(); + compareAliasedSubtrees (aliasChild, baseChild, false); + } + + + for (Iterator an = aliasNode.iterateQualifier(), + bn = baseNode.iterateQualifier(); + an.hasNext() && bn.hasNext();) + { + XMPNode aliasQual = (XMPNode) an.next(); + XMPNode baseQual = (XMPNode) bn.next(); + compareAliasedSubtrees (aliasQual, baseQual, false); + } + } + + + /** + * The initial support for WAV files mapped a legacy ID3 audio copyright + * into a new xmpDM:copyright property. This is special case code to migrate + * that into dc:rights['x-default']. The rules: + * + * <pre> + * 1. If there is no dc:rights array, or an empty array - + * Create one with dc:rights['x-default'] set from double linefeed and xmpDM:copyright. + * + * 2. If there is a dc:rights array but it has no x-default item - + * Create an x-default item as a copy of the first item then apply rule #3. + * + * 3. If there is a dc:rights array with an x-default item, + * Look for a double linefeed in the value. + * A. If no double linefeed, compare the x-default value to the xmpDM:copyright value. + * A1. If they match then leave the x-default value alone. + * A2. Otherwise, append a double linefeed and + * the xmpDM:copyright value to the x-default value. + * B. If there is a double linefeed, compare the trailing text to the xmpDM:copyright value. + * B1. If they match then leave the x-default value alone. + * B2. Otherwise, replace the trailing x-default text with the xmpDM:copyright value. + * + * 4. In all cases, delete the xmpDM:copyright property. + * </pre> + * + * @param xmp the metadata object + * @param dmCopyright the "dm:copyright"-property + */ + private static void migrateAudioCopyright (XMPMeta xmp, XMPNode dmCopyright) + { + try + { + XMPNode dcSchema = XMPNodeUtils.findSchemaNode( + ((XMPMetaImpl) xmp).getRoot(), XMPConst.NS_DC, true); + + String dmValue = dmCopyright.getValue(); + String doubleLF = "\n\n"; + + XMPNode dcRightsArray = XMPNodeUtils.findChildNode (dcSchema, "dc:rights", false); + + if (dcRightsArray == null || !dcRightsArray.hasChildren()) + { + // 1. No dc:rights array, create from double linefeed and xmpDM:copyright. + dmValue = doubleLF + dmValue; + xmp.setLocalizedText(XMPConst.NS_DC, "rights", "", XMPConst.X_DEFAULT, dmValue, + null); + } + else + { + int xdIndex = XMPNodeUtils.lookupLanguageItem(dcRightsArray, XMPConst.X_DEFAULT); + + if (xdIndex < 0) + { + // 2. No x-default item, create from the first item. + String firstValue = dcRightsArray.getChild(1).getValue(); + xmp.setLocalizedText (XMPConst.NS_DC, "rights", "", XMPConst.X_DEFAULT, + firstValue, null); + xdIndex = XMPNodeUtils.lookupLanguageItem(dcRightsArray, XMPConst.X_DEFAULT); + } + + // 3. Look for a double linefeed in the x-default value. + XMPNode defaultNode = dcRightsArray.getChild(xdIndex); + String defaultValue = defaultNode.getValue(); + int lfPos = defaultValue.indexOf(doubleLF); + + if (lfPos < 0) + { + // 3A. No double LF, compare whole values. + if (!dmValue.equals(defaultValue)) + { + // 3A2. Append the xmpDM:copyright to the x-default + // item. + defaultNode.setValue(defaultValue + doubleLF + dmValue); + } + } + else + { + // 3B. Has double LF, compare the tail. + if (!defaultValue.substring(lfPos + 2).equals(dmValue)) + { + // 3B2. Replace the x-default tail. + defaultNode.setValue(defaultValue.substring(0, lfPos + 2) + dmValue); + } + } + + } + + // 4. Get rid of the xmpDM:copyright. + dmCopyright.getParent().removeChild(dmCopyright); + } + catch (XMPException e) + { + // Don't let failures (like a bad dc:rights form) stop other + // cleanup. + } + } + + + /** + * Initializes the map that contains the known arrays, that are fixed by + * {@link XMPNormalizer#normalizeDCArrays(XMPNode)}. + */ + private static void initDCArrays() + { + dcArrayForms = new HashMap(); + + // Properties supposed to be a "Bag". + PropertyOptions bagForm = new PropertyOptions(); + bagForm.setArray(true); + dcArrayForms.put("dc:contributor", bagForm); + dcArrayForms.put("dc:language", bagForm); + dcArrayForms.put("dc:publisher", bagForm); + dcArrayForms.put("dc:relation", bagForm); + dcArrayForms.put("dc:subject", bagForm); + dcArrayForms.put("dc:type", bagForm); + + // Properties supposed to be a "Seq". + PropertyOptions seqForm = new PropertyOptions(); + seqForm.setArray(true); + seqForm.setArrayOrdered(true); + dcArrayForms.put("dc:creator", seqForm); + dcArrayForms.put("dc:date", seqForm); + + // Properties supposed to be an "Alt" in alternative-text form. + PropertyOptions altTextForm = new PropertyOptions(); + altTextForm.setArray(true); + altTextForm.setArrayOrdered(true); + altTextForm.setArrayAlternate(true); + altTextForm.setArrayAltText(true); + dcArrayForms.put("dc:description", altTextForm); + dcArrayForms.put("dc:rights", altTextForm); + dcArrayForms.put("dc:title", altTextForm); + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java b/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java new file mode 100644 index 0000000..1b8430d --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java @@ -0,0 +1,490 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPSchemaRegistry; +import com.adobe.xmp.options.AliasOptions; +import com.adobe.xmp.properties.XMPAliasInfo; + + +/** + * The schema registry handles the namespaces, aliases and global options for the XMP Toolkit. There + * is only one single instance used by the toolkit. + * + * @since 27.01.2006 + */ +public final class XMPSchemaRegistryImpl implements XMPSchemaRegistry, XMPConst +{ + /** a map from a namespace URI to its registered prefix */ + private Map namespaceToPrefixMap = new HashMap(); + + /** a map from a prefix to the associated namespace URI */ + private Map prefixToNamespaceMap = new HashMap(); + + /** a map of all registered aliases. + * The map is a relationship from a qname to an <code>XMPAliasInfo</code>-object. */ + private Map aliasMap = new HashMap(); + /** The pattern that must not be contained in simple properties */ + private Pattern p = Pattern.compile("[/*?\\[\\]]"); + + + /** + * Performs the initialisation of the registry with the default namespaces, aliases and global + * options. + */ + public XMPSchemaRegistryImpl() + { + try + { + registerStandardNamespaces(); + registerStandardAliases(); + } + catch (XMPException e) + { + throw new RuntimeException("The XMPSchemaRegistry cannot be initialized!"); + } + } + + + // --------------------------------------------------------------------------------------------- + // Namespace Functions + + + /** + * @see XMPSchemaRegistry#registerNamespace(String, String) + */ + public synchronized String registerNamespace(String namespaceURI, String suggestedPrefix) + throws XMPException + { + ParameterAsserts.assertSchemaNS(namespaceURI); + ParameterAsserts.assertPrefix(suggestedPrefix); + + if (suggestedPrefix.charAt(suggestedPrefix.length() - 1) != ':') + { + suggestedPrefix += ':'; + } + + if (!Utils.isXMLNameNS(suggestedPrefix.substring(0, + suggestedPrefix.length() - 1))) + { + throw new XMPException("The prefix is a bad XML name", XMPError.BADXML); + } + + String registeredPrefix = (String) namespaceToPrefixMap.get(namespaceURI); + String registeredNS = (String) prefixToNamespaceMap.get(suggestedPrefix); + if (registeredPrefix != null) + { + // Return the actual prefix + return registeredPrefix; + } + else + { + if (registeredNS != null) + { + // the namespace is new, but the prefix is already engaged, + // we generate a new prefix out of the suggested + String generatedPrefix = suggestedPrefix; + for (int i = 1; prefixToNamespaceMap.containsKey(generatedPrefix); i++) + { + generatedPrefix = suggestedPrefix + .substring(0, suggestedPrefix.length() - 1) + + "_" + i + "_:"; + } + suggestedPrefix = generatedPrefix; + } + prefixToNamespaceMap.put(suggestedPrefix, namespaceURI); + namespaceToPrefixMap.put(namespaceURI, suggestedPrefix); + + // Return the suggested prefix + return suggestedPrefix; + } + } + + + /** + * @see XMPSchemaRegistry#deleteNamespace(String) + */ + public synchronized void deleteNamespace(String namespaceURI) + { + String prefixToDelete = getNamespacePrefix(namespaceURI); + if (prefixToDelete != null) + { + namespaceToPrefixMap.remove(namespaceURI); + prefixToNamespaceMap.remove(prefixToDelete); + } + } + + + /** + * @see XMPSchemaRegistry#getNamespacePrefix(String) + */ + public synchronized String getNamespacePrefix(String namespaceURI) + { + return (String) namespaceToPrefixMap.get(namespaceURI); + } + + + /** + * @see XMPSchemaRegistry#getNamespaceURI(String) + */ + public synchronized String getNamespaceURI(String namespacePrefix) + { + if (namespacePrefix != null && !namespacePrefix.endsWith(":")) + { + namespacePrefix += ":"; + } + return (String) prefixToNamespaceMap.get(namespacePrefix); + } + + + /** + * @see XMPSchemaRegistry#getNamespaces() + */ + public synchronized Map getNamespaces() + { + return Collections.unmodifiableMap(new TreeMap(namespaceToPrefixMap)); + } + + + /** + * @see XMPSchemaRegistry#getPrefixes() + */ + public synchronized Map getPrefixes() + { + return Collections.unmodifiableMap(new TreeMap(prefixToNamespaceMap)); + } + + + /** + * Register the standard namespaces of schemas and types that are included in the XMP + * Specification and some other Adobe private namespaces. + * Note: This method is not lock because only called by the constructor. + * + * @throws XMPException Forwards processing exceptions + */ + private void registerStandardNamespaces() throws XMPException + { + // register standard namespaces + registerNamespace(NS_XML, "xml"); + registerNamespace(NS_RDF, "rdf"); + registerNamespace(NS_DC, "dc"); + registerNamespace(NS_IPTCCORE, "Iptc4xmpCore"); + + // register Adobe standard namespaces + registerNamespace(NS_X, "x"); + registerNamespace(NS_IX, "iX"); + + registerNamespace(NS_XMP, "xmp"); + registerNamespace(NS_XMP_RIGHTS, "xmpRights"); + registerNamespace(NS_XMP_MM, "xmpMM"); + registerNamespace(NS_XMP_BJ, "xmpBJ"); + registerNamespace(NS_XMP_NOTE, "xmpNote"); + + registerNamespace(NS_PDF, "pdf"); + registerNamespace(NS_PDFX, "pdfx"); + registerNamespace(NS_PDFX_ID, "pdfxid"); + registerNamespace(NS_PDFA_SCHEMA, "pdfaSchema"); + registerNamespace(NS_PDFA_PROPERTY, "pdfaProperty"); + registerNamespace(NS_PDFA_TYPE, "pdfaType"); + registerNamespace(NS_PDFA_FIELD, "pdfaField"); + registerNamespace(NS_PDFA_ID, "pdfaid"); + registerNamespace(NS_PDFA_EXTENSION, "pdfaExtension"); + registerNamespace(NS_PHOTOSHOP, "photoshop"); + registerNamespace(NS_PSALBUM, "album"); + registerNamespace(NS_EXIF, "exif"); + registerNamespace(NS_EXIF_AUX, "aux"); + registerNamespace(NS_TIFF, "tiff"); + registerNamespace(NS_PNG, "png"); + registerNamespace(NS_JPEG, "jpeg"); + registerNamespace(NS_JP2K, "jp2k"); + registerNamespace(NS_CAMERARAW, "crs"); + registerNamespace(NS_ADOBESTOCKPHOTO, "bmsp"); + registerNamespace(NS_CREATOR_ATOM, "creatorAtom"); + registerNamespace(NS_ASF, "asf"); + registerNamespace(NS_WAV, "wav"); + + // register Adobe private namespaces + registerNamespace(NS_DM, "xmpDM"); + registerNamespace(NS_TRANSIENT, "xmpx"); + + // register Adobe standard type namespaces + registerNamespace(TYPE_TEXT, "xmpT"); + registerNamespace(TYPE_PAGEDFILE, "xmpTPg"); + registerNamespace(TYPE_GRAPHICS, "xmpG"); + registerNamespace(TYPE_IMAGE, "xmpGImg"); + registerNamespace(TYPE_FONT, "stFNT"); + registerNamespace(TYPE_DIMENSIONS, "stDim"); + registerNamespace(TYPE_RESOURCEEVENT, "stEvt"); + registerNamespace(TYPE_RESOURCEREF, "stRef"); + registerNamespace(TYPE_ST_VERSION, "stVer"); + registerNamespace(TYPE_ST_JOB, "stJob"); + registerNamespace(TYPE_MANIFESTITEM, "stMfs"); + registerNamespace(TYPE_IDENTIFIERQUAL, "xmpidq"); + } + + + + // --------------------------------------------------------------------------------------------- + // Alias Functions + + + /** + * @see XMPSchemaRegistry#resolveAlias(String, String) + */ + public synchronized XMPAliasInfo resolveAlias(String aliasNS, String aliasProp) + { + String aliasPrefix = getNamespacePrefix(aliasNS); + if (aliasPrefix == null) + { + return null; + } + + return (XMPAliasInfo) aliasMap.get(aliasPrefix + aliasProp); + } + + + /** + * @see XMPSchemaRegistry#findAlias(java.lang.String) + */ + public synchronized XMPAliasInfo findAlias(String qname) + { + return (XMPAliasInfo) aliasMap.get(qname); + } + + + /** + * @see XMPSchemaRegistry#findAliases(String) + */ + public synchronized XMPAliasInfo[] findAliases(String aliasNS) + { + String prefix = getNamespacePrefix(aliasNS); + List result = new ArrayList(); + if (prefix != null) + { + for (Iterator it = aliasMap.keySet().iterator(); it.hasNext();) + { + String qname = (String) it.next(); + if (qname.startsWith(prefix)) + { + result.add(findAlias(qname)); + } + } + + } + return (XMPAliasInfo[]) result.toArray(new XMPAliasInfo[result.size()]); + } + + + /** + * Associates an alias name with an actual name. + * <p> + * Define a alias mapping from one namespace/property to another. Both + * property names must be simple names. An alias can be a direct mapping, + * where the alias and actual have the same data type. It is also possible + * to map a simple alias to an item in an array. This can either be to the + * first item in the array, or to the 'x-default' item in an alt-text array. + * Multiple alias names may map to the same actual, as long as the forms + * match. It is a no-op to reregister an alias in an identical fashion. + * Note: This method is not locking because only called by registerStandardAliases + * which is only called by the constructor. + * Note2: The method is only package-private so that it can be tested with unittests + * + * @param aliasNS + * The namespace URI for the alias. Must not be null or the empty + * string. + * @param aliasProp + * The name of the alias. Must be a simple name, not null or the + * empty string and not a general path expression. + * @param actualNS + * The namespace URI for the actual. Must not be null or the + * empty string. + * @param actualProp + * The name of the actual. Must be a simple name, not null or the + * empty string and not a general path expression. + * @param aliasForm + * Provides options for aliases for simple aliases to array + * items. This is needed to know what kind of array to create if + * set for the first time via the simple alias. Pass + * <code>XMP_NoOptions</code>, the default value, for all + * direct aliases regardless of whether the actual data type is + * an array or not (see {@link AliasOptions}). + * @throws XMPException + * for inconsistant aliases. + */ + synchronized void registerAlias(String aliasNS, String aliasProp, final String actualNS, + final String actualProp, final AliasOptions aliasForm) throws XMPException + { + ParameterAsserts.assertSchemaNS(aliasNS); + ParameterAsserts.assertPropName(aliasProp); + ParameterAsserts.assertSchemaNS(actualNS); + ParameterAsserts.assertPropName(actualProp); + + // Fix the alias options + final AliasOptions aliasOpts = aliasForm != null ? + new AliasOptions(XMPNodeUtils.verifySetOptions( + aliasForm.toPropertyOptions(), null).getOptions()) : + new AliasOptions(); + + if (p.matcher(aliasProp).find() || p.matcher(actualProp).find()) + { + throw new XMPException("Alias and actual property names must be simple", + XMPError.BADXPATH); + } + + // check if both namespaces are registered + final String aliasPrefix = getNamespacePrefix(aliasNS); + final String actualPrefix = getNamespacePrefix(actualNS); + if (aliasPrefix == null) + { + throw new XMPException("Alias namespace is not registered", XMPError.BADSCHEMA); + } + else if (actualPrefix == null) + { + throw new XMPException("Actual namespace is not registered", + XMPError.BADSCHEMA); + } + + String key = aliasPrefix + aliasProp; + + // check if alias is already existing + if (aliasMap.containsKey(key)) + { + throw new XMPException("Alias is already existing", XMPError.BADPARAM); + } + else if (aliasMap.containsKey(actualPrefix + actualProp)) + { + throw new XMPException( + "Actual property is already an alias, use the base property", + XMPError.BADPARAM); + } + + XMPAliasInfo aliasInfo = new XMPAliasInfo() + { + /** + * @see XMPAliasInfo#getNamespace() + */ + public String getNamespace() + { + return actualNS; + } + + /** + * @see XMPAliasInfo#getPrefix() + */ + public String getPrefix() + { + return actualPrefix; + } + + /** + * @see XMPAliasInfo#getPropName() + */ + public String getPropName() + { + return actualProp; + } + + /** + * @see XMPAliasInfo#getAliasForm() + */ + public AliasOptions getAliasForm() + { + return aliasOpts; + } + + public String toString() + { + return actualPrefix + actualProp + " NS(" + actualNS + "), FORM (" + + getAliasForm() + ")"; + } + }; + + aliasMap.put(key, aliasInfo); + } + + + /** + * @see XMPSchemaRegistry#getAliases() + */ + public synchronized Map getAliases() + { + return Collections.unmodifiableMap(new TreeMap(aliasMap)); + } + + + /** + * Register the standard aliases. + * Note: This method is not lock because only called by the constructor. + * + * @throws XMPException If the registrations of at least one alias fails. + */ + private void registerStandardAliases() throws XMPException + { + AliasOptions aliasToArrayOrdered = new AliasOptions().setArrayOrdered(true); + AliasOptions aliasToArrayAltText = new AliasOptions().setArrayAltText(true); + + + // Aliases from XMP to DC. + registerAlias(NS_XMP, "Author", NS_DC, "creator", aliasToArrayOrdered); + registerAlias(NS_XMP, "Authors", NS_DC, "creator", null); + registerAlias(NS_XMP, "Description", NS_DC, "description", null); + registerAlias(NS_XMP, "Format", NS_DC, "format", null); + registerAlias(NS_XMP, "Keywords", NS_DC, "subject", null); + registerAlias(NS_XMP, "Locale", NS_DC, "language", null); + registerAlias(NS_XMP, "Title", NS_DC, "title", null); + registerAlias(NS_XMP_RIGHTS, "Copyright", NS_DC, "rights", null); + + // Aliases from PDF to DC and XMP. + registerAlias(NS_PDF, "Author", NS_DC, "creator", aliasToArrayOrdered); + registerAlias(NS_PDF, "BaseURL", NS_XMP, "BaseURL", null); + registerAlias(NS_PDF, "CreationDate", NS_XMP, "CreateDate", null); + registerAlias(NS_PDF, "Creator", NS_XMP, "CreatorTool", null); + registerAlias(NS_PDF, "ModDate", NS_XMP, "ModifyDate", null); + registerAlias(NS_PDF, "Subject", NS_DC, "description", aliasToArrayAltText); + registerAlias(NS_PDF, "Title", NS_DC, "title", aliasToArrayAltText); + + // Aliases from PHOTOSHOP to DC and XMP. + registerAlias(NS_PHOTOSHOP, "Author", NS_DC, "creator", aliasToArrayOrdered); + registerAlias(NS_PHOTOSHOP, "Caption", NS_DC, "description", aliasToArrayAltText); + registerAlias(NS_PHOTOSHOP, "Copyright", NS_DC, "rights", aliasToArrayAltText); + registerAlias(NS_PHOTOSHOP, "Keywords", NS_DC, "subject", null); + registerAlias(NS_PHOTOSHOP, "Marked", NS_XMP_RIGHTS, "Marked", null); + registerAlias(NS_PHOTOSHOP, "Title", NS_DC, "title", aliasToArrayAltText); + registerAlias(NS_PHOTOSHOP, "WebStatement", NS_XMP_RIGHTS, "WebStatement", null); + + // Aliases from TIFF and EXIF to DC and XMP. + registerAlias(NS_TIFF, "Artist", NS_DC, "creator", aliasToArrayOrdered); + registerAlias(NS_TIFF, "Copyright", NS_DC, "rights", null); + registerAlias(NS_TIFF, "DateTime", NS_XMP, "ModifyDate", null); + registerAlias(NS_TIFF, "ImageDescription", NS_DC, "description", null); + registerAlias(NS_TIFF, "Software", NS_XMP, "CreatorTool", null); + + // Aliases from PNG (Acrobat ImageCapture) to DC and XMP. + registerAlias(NS_PNG, "Author", NS_DC, "creator", aliasToArrayOrdered); + registerAlias(NS_PNG, "Copyright", NS_DC, "rights", aliasToArrayAltText); + registerAlias(NS_PNG, "CreationTime", NS_XMP, "CreateDate", null); + registerAlias(NS_PNG, "Description", NS_DC, "description", aliasToArrayAltText); + registerAlias(NS_PNG, "ModificationTime", NS_XMP, "ModifyDate", null); + registerAlias(NS_PNG, "Software", NS_XMP, "CreatorTool", null); + registerAlias(NS_PNG, "Title", NS_DC, "title", aliasToArrayAltText); + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java b/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java new file mode 100644 index 0000000..ba43f01 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java @@ -0,0 +1,102 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.options.SerializeOptions; + + +/** + * Serializes the <code>XMPMeta</code>-object to an <code>OutputStream</code> according to the + * <code>SerializeOptions</code>. + * + * @since 11.07.2006 + */ +public class XMPSerializerHelper +{ + /** + * Static method to serialize the metadata object. For each serialisation, a new XMPSerializer + * instance is created, either XMPSerializerRDF or XMPSerializerPlain so thats its possible to + * serialialize the same XMPMeta objects in two threads. + * + * @param xmp a metadata implementation object + * @param out the output stream to serialize to + * @param options serialization options, can be <code>null</code> for default. + * @throws XMPException + */ + public static void serialize(XMPMetaImpl xmp, OutputStream out, + SerializeOptions options) + throws XMPException + { + options = options != null ? options : new SerializeOptions(); + + // sort the internal data model on demand + if (options.getSort()) + { + xmp.sort(); + } + new XMPSerializerRDF().serialize(xmp, out, options); + } + + + /** + * Serializes an <code>XMPMeta</code>-object as RDF into a string. + * <em>Note:</em> Encoding is forced to UTF-16 when serializing to a + * string to ensure the correctness of "exact packet size". + * + * @param xmp a metadata implementation object + * @param options Options to control the serialization (see + * {@link SerializeOptions}). + * @return Returns a string containing the serialized RDF. + * @throws XMPException on serializsation errors. + */ + public static String serializeToString(XMPMetaImpl xmp, SerializeOptions options) + throws XMPException + { + // forces the encoding to be UTF-16 to get the correct string length + options = options != null ? options : new SerializeOptions(); + options.setEncodeUTF16BE(true); + + ByteArrayOutputStream out = new ByteArrayOutputStream(2048); + serialize(xmp, out, options); + + try + { + return out.toString(options.getEncoding()); + } + catch (UnsupportedEncodingException e) + { + // cannot happen as UTF-8/16LE/BE is required to be implemented in + // Java + return out.toString(); + } + } + + + /** + * Serializes an <code>XMPMeta</code>-object as RDF into a byte buffer. + * + * @param xmp a metadata implementation object + * @param options Options to control the serialization (see {@link SerializeOptions}). + * @return Returns a byte buffer containing the serialized RDF. + * @throws XMPException on serializsation errors. + */ + public static byte[] serializeToBuffer(XMPMetaImpl xmp, SerializeOptions options) + throws XMPException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(2048); + serialize(xmp, out, options); + return out.toByteArray(); + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java b/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java new file mode 100644 index 0000000..b00877f --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java @@ -0,0 +1,1276 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.options.SerializeOptions; + + +/** + * Serializes the <code>XMPMeta</code>-object using the standard RDF serialization format. + * The output is written to an <code>OutputStream</code> + * according to the <code>SerializeOptions</code>. + * + * @since 11.07.2006 + */ +public class XMPSerializerRDF +{ + /** default padding */ + private static final int DEFAULT_PAD = 2048; + /** */ + private static final String PACKET_HEADER = + "<?xpacket begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>"; + /** The w/r is missing inbetween */ + private static final String PACKET_TRAILER = "<?xpacket end=\""; + /** */ + private static final String PACKET_TRAILER2 = "\"?>"; + /** */ + private static final String RDF_XMPMETA_START = + "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\""; + /** */ + private static final String RDF_XMPMETA_END = "</x:xmpmeta>"; + /** */ + private static final String RDF_RDF_START = + "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"; + /** */ + private static final String RDF_RDF_END = "</rdf:RDF>"; + + /** */ + private static final String RDF_SCHEMA_START = "<rdf:Description rdf:about="; + /** */ + private static final String RDF_SCHEMA_END = "</rdf:Description>"; + /** */ + private static final String RDF_STRUCT_START = "<rdf:Description"; + /** */ + private static final String RDF_STRUCT_END = "</rdf:Description>"; + /** a set of all rdf attribute qualifier */ + static final Set RDF_ATTR_QUALIFIER = new HashSet(Arrays.asList(new String[] { + XMPConst.XML_LANG, "rdf:resource", "rdf:ID", "rdf:bagID", "rdf:nodeID" })); + + /** the metadata object to be serialized. */ + private XMPMetaImpl xmp; + /** the output stream to serialize to */ + private CountOutputStream outputStream; + /** this writer is used to do the actual serialisation */ + private OutputStreamWriter writer; + /** the stored serialisation options */ + private SerializeOptions options; + /** the size of one unicode char, for UTF-8 set to 1 + * (Note: only valid for ASCII chars lower than 0x80), + * set to 2 in case of UTF-16 */ + private int unicodeSize = 1; // UTF-8 + /** the padding in the XMP Packet, or the length of the complete packet in + * case of option <em>exactPacketLength</em>. */ + private int padding; + + + /** + * The actual serialisation. + * + * @param xmp the metadata object to be serialized + * @param out outputStream the output stream to serialize to + * @param options the serialization options + * + * @throws XMPException If case of wrong options or any other serialisaton error. + */ + public void serialize(XMPMeta xmp, OutputStream out, + SerializeOptions options) throws XMPException + { + try + { + outputStream = new CountOutputStream(out); + writer = new OutputStreamWriter(outputStream, options.getEncoding()); + + this.xmp = (XMPMetaImpl) xmp; + this.options = options; + this.padding = options.getPadding(); + + writer = new OutputStreamWriter(outputStream, options.getEncoding()); + + checkOptionsConsistence(); + + // serializes the whole packet, but don't write the tail yet + // and flush to make sure that the written bytes are calculated correctly + String tailStr = serializeAsRDF(); + writer.flush(); + + // adds padding + addPadding(tailStr.length()); + + // writes the tail + write(tailStr); + writer.flush(); + + outputStream.close(); + } + catch (IOException e) + { + throw new XMPException("Error writing to the OutputStream", XMPError.UNKNOWN); + } + } + + + /** + * Calulates the padding according to the options and write it to the stream. + * @param tailLength the length of the tail string + * @throws XMPException thrown if packet size is to small to fit the padding + * @throws IOException forwards writer errors + */ + private void addPadding(int tailLength) throws XMPException, IOException + { + if (options.getExactPacketLength()) + { + // the string length is equal to the length of the UTF-8 encoding + int minSize = outputStream.getBytesWritten() + tailLength * unicodeSize; + if (minSize > padding) + { + throw new XMPException("Can't fit into specified packet size", + XMPError.BADSERIALIZE); + } + padding -= minSize; // Now the actual amount of padding to add. + } + + // fix rest of the padding according to Unicode unit size. + padding /= unicodeSize; + + int newlineLen = options.getNewline().length(); + if (padding >= newlineLen) + { + padding -= newlineLen; // Write this newline last. + while (padding >= (100 + newlineLen)) + { + writeChars(100, ' '); + writeNewline(); + padding -= (100 + newlineLen); + } + writeChars(padding, ' '); + writeNewline(); + } + else + { + writeChars(padding, ' '); + } + } + + + /** + * Checks if the supplied options are consistent. + * @throws XMPException Thrown if options are conflicting + */ + protected void checkOptionsConsistence() throws XMPException + { + if (options.getEncodeUTF16BE() | options.getEncodeUTF16LE()) + { + unicodeSize = 2; + } + + if (options.getExactPacketLength()) + { + if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad()) + { + throw new XMPException("Inconsistent options for exact size serialize", + XMPError.BADOPTIONS); + } + if ((options.getPadding() & (unicodeSize - 1)) != 0) + { + throw new XMPException("Exact size must be a multiple of the Unicode element", + XMPError.BADOPTIONS); + } + } + else if (options.getReadOnlyPacket()) + { + if (options.getOmitPacketWrapper() | options.getIncludeThumbnailPad()) + { + throw new XMPException("Inconsistent options for read-only packet", + XMPError.BADOPTIONS); + } + padding = 0; + } + else if (options.getOmitPacketWrapper()) + { + if (options.getIncludeThumbnailPad()) + { + throw new XMPException("Inconsistent options for non-packet serialize", + XMPError.BADOPTIONS); + } + padding = 0; + } + else + { + if (padding == 0) + { + padding = DEFAULT_PAD * unicodeSize; + } + + if (options.getIncludeThumbnailPad()) + { + if (!xmp.doesPropertyExist(XMPConst.NS_XMP, "Thumbnails")) + { + padding += 10000 * unicodeSize; + } + } + } + } + + + /** + * Writes the (optional) packet header and the outer rdf-tags. + * @return Returns the packet end processing instraction to be written after the padding. + * @throws IOException Forwarded writer exceptions. + * @throws XMPException + */ + private String serializeAsRDF() throws IOException, XMPException + { + // Write the packet header PI. + if (!options.getOmitPacketWrapper()) + { + writeIndent(0); + write(PACKET_HEADER); + writeNewline(); + } + + // Write the xmpmeta element's start tag. + writeIndent(0); + write(RDF_XMPMETA_START); + // Note: this flag can only be set by unit tests + if (!options.getOmitVersionAttribute()) + { + write(XMPMetaFactory.getVersionInfo().getMessage()); + } + write("\">"); + writeNewline(); + + // Write the rdf:RDF start tag. + writeIndent(1); + write(RDF_RDF_START); + writeNewline(); + + // Write all of the properties. + if (options.getUseCompactFormat()) + { + serializeCompactRDFSchemas(); + } + else + { + serializePrettyRDFSchemas(); + } + + // Write the rdf:RDF end tag. + writeIndent(1); + write(RDF_RDF_END); + writeNewline(); + + // Write the xmpmeta end tag. + writeIndent(0); + write(RDF_XMPMETA_END); + writeNewline(); + + // Write the packet trailer PI into the tail string as UTF-8. + String tailStr = ""; + if (!options.getOmitPacketWrapper()) + { + for (int level = options.getBaseIndent(); level > 0; level--) + { + tailStr += options.getIndent(); + } + + tailStr += PACKET_TRAILER; + tailStr += options.getReadOnlyPacket() ? 'r' : 'w'; + tailStr += PACKET_TRAILER2; + } + + return tailStr; + } + + + /** + * Serializes the metadata in pretty-printed manner. + * @throws IOException Forwarded writer exceptions + * @throws XMPException + */ + private void serializePrettyRDFSchemas() throws IOException, XMPException + { + if (xmp.getRoot().getChildrenLength() > 0) + { + for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext(); ) + { + XMPNode currSchema = (XMPNode) it.next(); + serializePrettyRDFSchema(currSchema); + } + } + else + { + writeIndent(2); + write(RDF_SCHEMA_START); // Special case an empty XMP object. + writeTreeName(); + write("/>"); + writeNewline(); + } + } + + + /** + * @throws IOException + */ + private void writeTreeName() throws IOException + { + write('"'); + String name = xmp.getRoot().getName(); + if (name != null) + { + appendNodeValue(name, true); + } + write('"'); + } + + + /** + * Serializes the metadata in compact manner. + * @throws IOException Forwarded writer exceptions + * @throws XMPException + */ + private void serializeCompactRDFSchemas() throws IOException, XMPException + { + // Begin the rdf:Description start tag. + writeIndent(2); + write(RDF_SCHEMA_START); + writeTreeName(); + + // Write all necessary xmlns attributes. + Set usedPrefixes = new HashSet(); + usedPrefixes.add("xml"); + usedPrefixes.add("rdf"); + + for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) + { + XMPNode schema = (XMPNode) it.next(); + declareUsedNamespaces(schema, usedPrefixes, 4); + } + + // Write the top level "attrProps" and close the rdf:Description start tag. + boolean allAreAttrs = true; + for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) + { + XMPNode schema = (XMPNode) it.next(); + allAreAttrs &= serializeCompactRDFAttrProps (schema, 3); + } + + if (!allAreAttrs) + { + write('>'); + writeNewline(); + } + else + { + write("/>"); + writeNewline(); + return; // ! Done if all properties in all schema are written as attributes. + } + + // Write the remaining properties for each schema. + for (Iterator it = xmp.getRoot().iterateChildren(); it.hasNext();) + { + XMPNode schema = (XMPNode) it.next(); + serializeCompactRDFElementProps (schema, 3); + } + + // Write the rdf:Description end tag. + writeIndent(2); + write(RDF_SCHEMA_END); + writeNewline(); + } + + + + /** + * Write each of the parent's simple unqualified properties as an attribute. Returns true if all + * of the properties are written as attributes. + * + * @param parentNode the parent property node + * @param indent the current indent level + * @return Returns true if all properties can be rendered as RDF attribute. + * @throws IOException + */ + private boolean serializeCompactRDFAttrProps(XMPNode parentNode, int indent) throws IOException + { + boolean allAreAttrs = true; + + for (Iterator it = parentNode.iterateChildren(); it.hasNext();) + { + XMPNode prop = (XMPNode) it.next(); + + if (canBeRDFAttrProp(prop)) + { + writeNewline(); + writeIndent(indent); + write(prop.getName()); + write("=\""); + appendNodeValue(prop.getValue(), true); + write('"'); + } + else + { + allAreAttrs = false; + } + } + return allAreAttrs; + } + + + /** + * Recursively handles the "value" for a node that must be written as an RDF + * property element. It does not matter if it is a top level property, a + * field of a struct, or an item of an array. The indent is that for the + * property element. The patterns bwlow ignore attribute qualifiers such as + * xml:lang, they don't affect the output form. + * + * <blockquote> + * + * <pre> + * <ns:UnqualifiedStructProperty-1 + * ... The fields as attributes, if all are simple and unqualified + * /> + * + * <ns:UnqualifiedStructProperty-2 rdf:parseType="Resource"> + * ... The fields as elements, if none are simple and unqualified + * </ns:UnqualifiedStructProperty-2> + * + * <ns:UnqualifiedStructProperty-3> + * <rdf:Description + * ... The simple and unqualified fields as attributes + * > + * ... The compound or qualified fields as elements + * </rdf:Description> + * </ns:UnqualifiedStructProperty-3> + * + * <ns:UnqualifiedArrayProperty> + * <rdf:Bag> or Seq or Alt + * ... Array items as rdf:li elements, same forms as top level properties + * </rdf:Bag> + * </ns:UnqualifiedArrayProperty> + * + * <ns:QualifiedProperty rdf:parseType="Resource"> + * <rdf:value> ... Property "value" + * following the unqualified forms ... </rdf:value> + * ... Qualifiers looking like named struct fields + * </ns:QualifiedProperty> + * </pre> + * + * </blockquote> + * + * *** Consider numbered array items, but has compatibility problems. *** + * Consider qualified form with rdf:Description and attributes. + * + * @param parentNode the parent node + * @param indent the current indent level + * @throws IOException Forwards writer exceptions + * @throws XMPException If qualifier and element fields are mixed. + */ + private void serializeCompactRDFElementProps(XMPNode parentNode, int indent) + throws IOException, XMPException + { + for (Iterator it = parentNode.iterateChildren(); it.hasNext();) + { + XMPNode node = (XMPNode) it.next(); + if (canBeRDFAttrProp (node)) + { + continue; + } + + boolean emitEndTag = true; + boolean indentEndTag = true; + + // Determine the XML element name, write the name part of the start tag. Look over the + // qualifiers to decide on "normal" versus "rdf:value" form. Emit the attribute + // qualifiers at the same time. + String elemName = node.getName(); + if (XMPConst.ARRAY_ITEM_NAME.equals(elemName)) + { + elemName = "rdf:li"; + } + + writeIndent(indent); + write('<'); + write(elemName); + + boolean hasGeneralQualifiers = false; + boolean hasRDFResourceQual = false; + + for (Iterator iq = node.iterateQualifier(); iq.hasNext();) + { + XMPNode qualifier = (XMPNode) iq.next(); + if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) + { + hasGeneralQualifiers = true; + } + else + { + hasRDFResourceQual = "rdf:resource".equals(qualifier.getName()); + write(' '); + write(qualifier.getName()); + write("=\""); + appendNodeValue(qualifier.getValue(), true); + write('"'); + } + } + + + // Process the property according to the standard patterns. + if (hasGeneralQualifiers) + { + serializeCompactRDFGeneralQualifier(indent, node); + } + else + { + // This node has only attribute qualifiers. Emit as a property element. + if (!node.getOptions().isCompositeProperty()) + { + Object[] result = serializeCompactRDFSimpleProp(node); + emitEndTag = ((Boolean) result[0]).booleanValue(); + indentEndTag = ((Boolean) result[1]).booleanValue(); + } + else if (node.getOptions().isArray()) + { + serializeCompactRDFArrayProp(node, indent); + } + else + { + emitEndTag = serializeCompactRDFStructProp( + node, indent, hasRDFResourceQual); + } + + } + + // Emit the property element end tag. + if (emitEndTag) + { + if (indentEndTag) + { + writeIndent(indent); + } + write("</"); + write(elemName); + write('>'); + writeNewline(); + } + + } + } + + + /** + * Serializes a simple property. + * + * @param node an XMPNode + * @return Returns an array containing the flags emitEndTag and indentEndTag. + * @throws IOException Forwards the writer exceptions. + */ + private Object[] serializeCompactRDFSimpleProp(XMPNode node) throws IOException + { + // This is a simple property. + Boolean emitEndTag = Boolean.TRUE; + Boolean indentEndTag = Boolean.TRUE; + + if (node.getOptions().isURI()) + { + write(" rdf:resource=\""); + appendNodeValue(node.getValue(), true); + write("\"/>"); + writeNewline(); + emitEndTag = Boolean.FALSE; + } + else if (node.getValue() == null || node.getValue().length() == 0) + { + write("/>"); + writeNewline(); + emitEndTag = Boolean.FALSE; + } + else + { + write('>'); + appendNodeValue (node.getValue(), false); + indentEndTag = Boolean.FALSE; + } + + return new Object[] {emitEndTag, indentEndTag}; + } + + + /** + * Serializes an array property. + * + * @param node an XMPNode + * @param indent the current indent level + * @throws IOException Forwards the writer exceptions. + * @throws XMPException If qualifier and element fields are mixed. + */ + private void serializeCompactRDFArrayProp(XMPNode node, int indent) throws IOException, + XMPException + { + // This is an array. + write('>'); + writeNewline(); + emitRDFArrayTag (node, true, indent + 1); + + if (node.getOptions().isArrayAltText()) + { + XMPNodeUtils.normalizeLangArray (node); + } + + serializeCompactRDFElementProps(node, indent + 2); + + emitRDFArrayTag(node, false, indent + 1); + } + + + /** + * Serializes a struct property. + * + * @param node an XMPNode + * @param indent the current indent level + * @param hasRDFResourceQual Flag if the element has resource qualifier + * @return Returns true if an end flag shall be emitted. + * @throws IOException Forwards the writer exceptions. + * @throws XMPException If qualifier and element fields are mixed. + */ + private boolean serializeCompactRDFStructProp(XMPNode node, int indent, + boolean hasRDFResourceQual) throws XMPException, IOException + { + // This must be a struct. + boolean hasAttrFields = false; + boolean hasElemFields = false; + boolean emitEndTag = true; + + for (Iterator ic = node.iterateChildren(); ic.hasNext(); ) + { + XMPNode field = (XMPNode) ic.next(); + if (canBeRDFAttrProp(field)) + { + hasAttrFields = true; + } + else + { + hasElemFields = true; + } + + if (hasAttrFields && hasElemFields) + { + break; // No sense looking further. + } + } + + if (hasRDFResourceQual && hasElemFields) + { + throw new XMPException( + "Can't mix rdf:resource qualifier and element fields", + XMPError.BADRDF); + } + + if (!node.hasChildren()) + { + // Catch an empty struct as a special case. The case + // below would emit an empty + // XML element, which gets reparsed as a simple property + // with an empty value. + write(" rdf:parseType=\"Resource\"/>"); + writeNewline(); + emitEndTag = false; + + } + else if (!hasElemFields) + { + // All fields can be attributes, use the + // emptyPropertyElt form. + serializeCompactRDFAttrProps(node, indent + 1); + write("/>"); + writeNewline(); + emitEndTag = false; + + } + else if (!hasAttrFields) + { + // All fields must be elements, use the + // parseTypeResourcePropertyElt form. + write(" rdf:parseType=\"Resource\">"); + writeNewline(); + serializeCompactRDFElementProps(node, indent + 1); + + } + else + { + // Have a mix of attributes and elements, use an inner rdf:Description. + write('>'); + writeNewline(); + writeIndent(indent + 1); + write(RDF_STRUCT_START); + serializeCompactRDFAttrProps(node, indent + 2); + write(">"); + writeNewline(); + serializeCompactRDFElementProps(node, indent + 1); + writeIndent(indent + 1); + write(RDF_STRUCT_END); + writeNewline(); + } + return emitEndTag; + } + + + /** + * Serializes the general qualifier. + * @param node the root node of the subtree + * @param indent the current indent level + * @throws IOException Forwards all writer exceptions. + * @throws XMPException If qualifier and element fields are mixed. + */ + private void serializeCompactRDFGeneralQualifier(int indent, XMPNode node) + throws IOException, XMPException + { + // The node has general qualifiers, ones that can't be + // attributes on a property element. + // Emit using the qualified property pseudo-struct form. The + // value is output by a call + // to SerializePrettyRDFProperty with emitAsRDFValue set. + write(" rdf:parseType=\"Resource\">"); + writeNewline(); + + serializePrettyRDFProperty(node, true, indent + 1); + + for (Iterator iq = node.iterateQualifier(); iq.hasNext();) + { + XMPNode qualifier = (XMPNode) iq.next(); + serializePrettyRDFProperty(qualifier, false, indent + 1); + } + } + + + /** + * Serializes one schema with all contained properties in pretty-printed + * manner.<br> + * Each schema's properties are written in a separate + * rdf:Description element. All of the necessary namespaces are declared in + * the rdf:Description element. The baseIndent is the base level for the + * entire serialization, that of the x:xmpmeta element. An xml:lang + * qualifier is written as an attribute of the property start tag, not by + * itself forcing the qualified property form. + * + * <blockquote> + * + * <pre> + * <rdf:Description rdf:about="TreeName" xmlns:ns="URI" ... > + * + * ... The actual properties of the schema, see SerializePrettyRDFProperty + * + * <!-- ns1:Alias is aliased to ns2:Actual --> ... If alias comments are wanted + * + * </rdf:Description> + * </pre> + * + * </blockquote> + * + * @param schemaNode a schema node + * @throws IOException Forwarded writer exceptions + * @throws XMPException + */ + private void serializePrettyRDFSchema(XMPNode schemaNode) throws IOException, XMPException + { + writeIndent(2); + write(RDF_SCHEMA_START); + writeTreeName(); + + Set usedPrefixes = new HashSet(); + usedPrefixes.add("xml"); + usedPrefixes.add("rdf"); + + declareUsedNamespaces(schemaNode, usedPrefixes, 4); + + write('>'); + writeNewline(); + + // Write each of the schema's actual properties. + for (Iterator it = schemaNode.iterateChildren(); it.hasNext();) + { + XMPNode propNode = (XMPNode) it.next(); + serializePrettyRDFProperty(propNode, false, 3); + } + + // Write the rdf:Description end tag. + writeIndent(2); + write(RDF_SCHEMA_END); + writeNewline(); + } + + + /** + * Writes all used namespaces of the subtree in node to the output. + * The subtree is recursivly traversed. + * @param node the root node of the subtree + * @param usedPrefixes a set containing currently used prefixes + * @param indent the current indent level + * @throws IOException Forwards all writer exceptions. + */ + private void declareUsedNamespaces(XMPNode node, Set usedPrefixes, int indent) + throws IOException + { + if (node.getOptions().isSchemaNode()) + { + // The schema node name is the URI, the value is the prefix. + String prefix = node.getValue().substring(0, node.getValue().length() - 1); + declareNamespace(prefix, node.getName(), usedPrefixes, indent); + } + else if (node.getOptions().isStruct()) + { + for (Iterator it = node.iterateChildren(); it.hasNext();) + { + XMPNode field = (XMPNode) it.next(); + declareNamespace(field.getName(), null, usedPrefixes, indent); + } + } + + for (Iterator it = node.iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + declareUsedNamespaces(child, usedPrefixes, indent); + } + + for (Iterator it = node.iterateQualifier(); it.hasNext();) + { + XMPNode qualifier = (XMPNode) it.next(); + declareNamespace(qualifier.getName(), null, usedPrefixes, indent); + declareUsedNamespaces(qualifier, usedPrefixes, indent); + } + } + + + /** + * Writes one namespace declaration to the output. + * @param prefix a namespace prefix (without colon) or a complete qname (when namespace == null) + * @param namespace the a namespace + * @param usedPrefixes a set containing currently used prefixes + * @param indent the current indent level + * @throws IOException Forwards all writer exceptions. + */ + private void declareNamespace(String prefix, String namespace, Set usedPrefixes, int indent) + throws IOException + { + if (namespace == null) + { + // prefix contains qname, extract prefix and lookup namespace with prefix + QName qname = new QName(prefix); + if (qname.hasPrefix()) + { + prefix = qname.getPrefix(); + // add colon for lookup + namespace = XMPMetaFactory.getSchemaRegistry().getNamespaceURI(prefix + ":"); + // prefix w/o colon + declareNamespace(prefix, namespace, usedPrefixes, indent); + } + else + { + return; + } + } + + if (!usedPrefixes.contains(prefix)) + { + writeNewline(); + writeIndent(indent); + write("xmlns:"); + write(prefix); + write("=\""); + write(namespace); + write('"'); + usedPrefixes.add(prefix); + } + } + + + /** + * Recursively handles the "value" for a node. It does not matter if it is a + * top level property, a field of a struct, or an item of an array. The + * indent is that for the property element. An xml:lang qualifier is written + * as an attribute of the property start tag, not by itself forcing the + * qualified property form. The patterns below mostly ignore attribute + * qualifiers like xml:lang. Except for the one struct case, attribute + * qualifiers don't affect the output form. + * + * <blockquote> + * + * <pre> + * <ns:UnqualifiedSimpleProperty>value</ns:UnqualifiedSimpleProperty> + * + * <ns:UnqualifiedStructProperty rdf:parseType="Resource"> + * (If no rdf:resource qualifier) + * ... Fields, same forms as top level properties + * </ns:UnqualifiedStructProperty> + * + * <ns:ResourceStructProperty rdf:resource="URI" + * ... Fields as attributes + * > + * + * <ns:UnqualifiedArrayProperty> + * <rdf:Bag> or Seq or Alt + * ... Array items as rdf:li elements, same forms as top level properties + * </rdf:Bag> + * </ns:UnqualifiedArrayProperty> + * + * <ns:QualifiedProperty rdf:parseType="Resource"> + * <rdf:value> ... Property "value" following the unqualified + * forms ... </rdf:value> + * ... Qualifiers looking like named struct fields + * </ns:QualifiedProperty> + * </pre> + * + * </blockquote> + * + * @param node the property node + * @param emitAsRDFValue property shall be renderes as attribute rather than tag + * @param indent the current indent level + * @throws IOException Forwards all writer exceptions. + * @throws XMPException If "rdf:resource" and general qualifiers are mixed. + */ + private void serializePrettyRDFProperty(XMPNode node, boolean emitAsRDFValue, int indent) + throws IOException, XMPException + { + boolean emitEndTag = true; + boolean indentEndTag = true; + + // Determine the XML element name. Open the start tag with the name and + // attribute qualifiers. + + String elemName = node.getName(); + if (emitAsRDFValue) + { + elemName = "rdf:value"; + } + else if (XMPConst.ARRAY_ITEM_NAME.equals(elemName)) + { + elemName = "rdf:li"; + } + + writeIndent(indent); + write('<'); + write(elemName); + + boolean hasGeneralQualifiers = false; + boolean hasRDFResourceQual = false; + + for (Iterator it = node.iterateQualifier(); it.hasNext();) + { + XMPNode qualifier = (XMPNode) it.next(); + if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) + { + hasGeneralQualifiers = true; + } + else + { + hasRDFResourceQual = "rdf:resource".equals(qualifier.getName()); + if (!emitAsRDFValue) + { + write(' '); + write(qualifier.getName()); + write("=\""); + appendNodeValue(qualifier.getValue(), true); + write('"'); + } + } + } + + // Process the property according to the standard patterns. + + if (hasGeneralQualifiers && !emitAsRDFValue) + { + // This node has general, non-attribute, qualifiers. Emit using the + // qualified property form. + // ! The value is output by a recursive call ON THE SAME NODE with + // emitAsRDFValue set. + + if (hasRDFResourceQual) + { + throw new XMPException("Can't mix rdf:resource and general qualifiers", + XMPError.BADRDF); + } + + write(" rdf:parseType=\"Resource\">"); + writeNewline(); + + serializePrettyRDFProperty(node, true, indent + 1); + + for (Iterator it = node.iterateQualifier(); it.hasNext();) + { + XMPNode qualifier = (XMPNode) it.next(); + if (!RDF_ATTR_QUALIFIER.contains(qualifier.getName())) + { + serializePrettyRDFProperty(qualifier, false, indent + 1); + } + } + } + else + { + // This node has no general qualifiers. Emit using an unqualified form. + + if (!node.getOptions().isCompositeProperty()) + { + // This is a simple property. + + if (node.getOptions().isURI()) + { + write(" rdf:resource=\""); + appendNodeValue(node.getValue(), true); + write("\"/>"); + writeNewline(); + emitEndTag = false; + } + else if (node.getValue() == null || "".equals(node.getValue())) + { + write("/>"); + writeNewline(); + emitEndTag = false; + } + else + { + write('>'); + appendNodeValue(node.getValue(), false); + indentEndTag = false; + } + } + else if (node.getOptions().isArray()) + { + // This is an array. + write('>'); + writeNewline(); + emitRDFArrayTag(node, true, indent + 1); + if (node.getOptions().isArrayAltText()) + { + XMPNodeUtils.normalizeLangArray(node); + } + for (Iterator it = node.iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + serializePrettyRDFProperty(child, false, indent + 2); + } + emitRDFArrayTag(node, false, indent + 1); + + + } + else if (!hasRDFResourceQual) + { + // This is a "normal" struct, use the rdf:parseType="Resource" form. + if (!node.hasChildren()) + { + write(" rdf:parseType=\"Resource\"/>"); + writeNewline(); + emitEndTag = false; + } + else + { + write(" rdf:parseType=\"Resource\">"); + writeNewline(); + for (Iterator it = node.iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + serializePrettyRDFProperty(child, false, indent + 1); + } + } + } + else + { + // This is a struct with an rdf:resource attribute, use the + // "empty property element" form. + for (Iterator it = node.iterateChildren(); it.hasNext();) + { + XMPNode child = (XMPNode) it.next(); + if (!canBeRDFAttrProp(child)) + { + throw new XMPException("Can't mix rdf:resource and complex fields", + XMPError.BADRDF); + } + writeNewline(); + writeIndent(indent + 1); + write(' '); + write(child.getName()); + write("=\""); + appendNodeValue(child.getValue(), true); + write('"'); + } + write("/>"); + writeNewline(); + emitEndTag = false; + } + } + + // Emit the property element end tag. + if (emitEndTag) + { + if (indentEndTag) + { + writeIndent(indent); + } + write("</"); + write(elemName); + write('>'); + writeNewline(); + } + } + + + /** + * Writes the array start and end tags. + * + * @param arrayNode an array node + * @param isStartTag flag if its the start or end tag + * @param indent the current indent level + * @throws IOException forwards writer exceptions + */ + private void emitRDFArrayTag(XMPNode arrayNode, boolean isStartTag, int indent) + throws IOException + { + if (isStartTag || arrayNode.hasChildren()) + { + writeIndent(indent); + write(isStartTag ? "<rdf:" : "</rdf:"); + + if (arrayNode.getOptions().isArrayAlternate()) + { + write("Alt"); + } + else if (arrayNode.getOptions().isArrayOrdered()) + { + write("Seq"); + } + else + { + write("Bag"); + } + + if (isStartTag && !arrayNode.hasChildren()) + { + write("/>"); + } + else + { + write(">"); + } + + writeNewline(); + } + } + + + /** + * Serializes the node value in XML encoding. Its used for tag bodies and + * attributes. <em>Note:</em> The attribute is always limited by quotes, + * thats why <code>&apos;</code> is never serialized. <em>Note:</em> + * Control chars are written unescaped, but if the user uses others than tab, LF + * and CR the resulting XML will become invalid. + * + * @param value the value of the node + * @param forAttribute flag if value is an attribute value + * @throws IOException + */ + private void appendNodeValue(String value, boolean forAttribute) throws IOException + { + write (Utils.escapeXML(value, forAttribute, true)); + } + + + /** + * A node can be serialized as RDF-Attribute, if it meets the following conditions: + * <ul> + * <li>is not array item + * <li>don't has qualifier + * <li>is no URI + * <li>is no composite property + * </ul> + * + * @param node an XMPNode + * @return Returns true if the node serialized as RDF-Attribute + */ + private boolean canBeRDFAttrProp(XMPNode node) + { + return + !node.hasQualifier() && + !node.getOptions().isURI() && + !node.getOptions().isCompositeProperty() && + !XMPConst.ARRAY_ITEM_NAME.equals(node.getName()); + } + + + /** + * Writes indents and automatically includes the baseindend from the options. + * @param times number of indents to write + * @throws IOException forwards exception + */ + private void writeIndent(int times) throws IOException + { + for (int i = options.getBaseIndent() + times; i > 0; i--) + { + writer.write(options.getIndent()); + } + } + + + /** + * Writes a char to the output. + * @param c a char + * @throws IOException forwards writer exceptions + */ + private void write(int c) throws IOException + { + writer.write(c); + } + + + /** + * Writes a String to the output. + * @param str a String + * @throws IOException forwards writer exceptions + */ + private void write(String str) throws IOException + { + writer.write(str); + } + + + /** + * Writes an amount of chars, mostly spaces + * @param number number of chars + * @param c a char + * @throws IOException + */ + private void writeChars(int number, char c) throws IOException + { + for (; number > 0; number--) + { + writer.write(c); + } + } + + + /** + * Writes a newline according to the options. + * @throws IOException Forwards exception + */ + private void writeNewline() throws IOException + { + writer.write(options.getNewline()); + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java b/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java new file mode 100644 index 0000000..ab36996 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java @@ -0,0 +1,1170 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + + + +package com.adobe.xmp.impl; + +import java.util.Iterator; + +import com.adobe.xmp.XMPConst; +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.XMPUtils; +import com.adobe.xmp.impl.xpath.XMPPath; +import com.adobe.xmp.impl.xpath.XMPPathParser; +import com.adobe.xmp.options.PropertyOptions; +import com.adobe.xmp.properties.XMPAliasInfo; + + + +/** + * @since 11.08.2006 + */ +public class XMPUtilsImpl implements XMPConst +{ + /** */ + private static final int UCK_NORMAL = 0; + /** */ + private static final int UCK_SPACE = 1; + /** */ + private static final int UCK_COMMA = 2; + /** */ + private static final int UCK_SEMICOLON = 3; + /** */ + private static final int UCK_QUOTE = 4; + /** */ + private static final int UCK_CONTROL = 5; + + + /** + * Private constructor, as + */ + private XMPUtilsImpl() + { + // EMPTY + } + + + /** + * @see XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String, + * boolean) + * + * @param xmp + * The XMP object containing the array to be catenated. + * @param schemaNS + * The schema namespace URI for the array. Must not be null or + * the empty string. + * @param arrayName + * The name of the array. May be a general path expression, must + * not be null or the empty string. Each item in the array must + * be a simple string value. + * @param separator + * The string to be used to separate the items in the catenated + * string. Defaults to "; ", ASCII semicolon and space + * (U+003B, U+0020). + * @param quotes + * The characters to be used as quotes around array items that + * contain a separator. Defaults to '"' + * @param allowCommas + * Option flag to control the catenation. + * @return Returns the string containing the catenated array items. + * @throws XMPException + * Forwards the Exceptions from the metadata processing + */ + public static String catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, + String separator, String quotes, boolean allowCommas) throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + ParameterAsserts.assertImplementation(xmp); + if (separator == null || separator.length() == 0) + { + separator = "; "; + } + if (quotes == null || quotes.length() == 0) + { + quotes = "\""; + } + + XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; + XMPNode arrayNode = null; + XMPNode currItem = null; + + // Return an empty result if the array does not exist, + // hurl if it isn't the right form. + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); + arrayNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), arrayPath, false, null); + if (arrayNode == null) + { + return ""; + } + else if (!arrayNode.getOptions().isArray() || arrayNode.getOptions().isArrayAlternate()) + { + throw new XMPException("Named property must be non-alternate array", XMPError.BADPARAM); + } + + // Make sure the separator is OK. + checkSeparator(separator); + // Make sure the open and close quotes are a legitimate pair. + char openQuote = quotes.charAt(0); + char closeQuote = checkQuotes(quotes, openQuote); + + // Build the result, quoting the array items, adding separators. + // Hurl if any item isn't simple. + + StringBuffer catinatedString = new StringBuffer(); + + for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) + { + currItem = (XMPNode) it.next(); + if (currItem.getOptions().isCompositeProperty()) + { + throw new XMPException("Array items must be simple", XMPError.BADPARAM); + } + String str = applyQuotes(currItem.getValue(), openQuote, closeQuote, allowCommas); + + catinatedString.append(str); + if (it.hasNext()) + { + catinatedString.append(separator); + } + } + + return catinatedString.toString(); + } + + + /** + * see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String, + * PropertyOptions, boolean)} + * + * @param xmp + * The XMP object containing the array to be updated. + * @param schemaNS + * The schema namespace URI for the array. Must not be null or + * the empty string. + * @param arrayName + * The name of the array. May be a general path expression, must + * not be null or the empty string. Each item in the array must + * be a simple string value. + * @param catedStr + * The string to be separated into the array items. + * @param arrayOptions + * Option flags to control the separation. + * @param preserveCommas + * Flag if commas shall be preserved + * + * @throws XMPException + * Forwards the Exceptions from the metadata processing + */ + public static void separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, + String catedStr, PropertyOptions arrayOptions, boolean preserveCommas) + throws XMPException + { + ParameterAsserts.assertSchemaNS(schemaNS); + ParameterAsserts.assertArrayName(arrayName); + if (catedStr == null) + { + throw new XMPException("Parameter must not be null", XMPError.BADPARAM); + } + ParameterAsserts.assertImplementation(xmp); + XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; + + // Keep a zero value, has special meaning below. + XMPNode arrayNode = separateFindCreateArray(schemaNS, arrayName, arrayOptions, xmpImpl); + + // Extract the item values one at a time, until the whole input string is done. + String itemValue; + int itemStart, itemEnd; + int nextKind = UCK_NORMAL, charKind = UCK_NORMAL; + char ch = 0, nextChar = 0; + + itemEnd = 0; + int endPos = catedStr.length(); + while (itemEnd < endPos) + { + // Skip any leading spaces and separation characters. Always skip commas here. + // They can be kept when within a value, but not when alone between values. + for (itemStart = itemEnd; itemStart < endPos; itemStart++) + { + ch = catedStr.charAt(itemStart); + charKind = classifyCharacter(ch); + if (charKind == UCK_NORMAL || charKind == UCK_QUOTE) + { + break; + } + } + if (itemStart >= endPos) + { + break; + } + + if (charKind != UCK_QUOTE) + { + // This is not a quoted value. Scan for the end, create an array + // item from the substring. + for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) + { + ch = catedStr.charAt(itemEnd); + charKind = classifyCharacter(ch); + + if (charKind == UCK_NORMAL || charKind == UCK_QUOTE || + (charKind == UCK_COMMA && preserveCommas)) + { + continue; + } + else if (charKind != UCK_SPACE) + { + break; + } + else if ((itemEnd + 1) < endPos) + { + ch = catedStr.charAt(itemEnd + 1); + nextKind = classifyCharacter(ch); + if (nextKind == UCK_NORMAL || nextKind == UCK_QUOTE || + (nextKind == UCK_COMMA && preserveCommas)) + { + continue; + } + } + + // Anything left? + break; // Have multiple spaces, or a space followed by a + // separator. + } + itemValue = catedStr.substring(itemStart, itemEnd); + } + else + { + // Accumulate quoted values into a local string, undoubling + // internal quotes that + // match the surrounding quotes. Do not undouble "unmatching" + // quotes. + + char openQuote = ch; + char closeQuote = getClosingQuote(openQuote); + + itemStart++; // Skip the opening quote; + itemValue = ""; + + for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) + { + ch = catedStr.charAt(itemEnd); + charKind = classifyCharacter(ch); + + if (charKind != UCK_QUOTE || !isSurroundingQuote(ch, openQuote, closeQuote)) + { + // This is not a matching quote, just append it to the + // item value. + itemValue += ch; + } + else + { + // This is a "matching" quote. Is it doubled, or the + // final closing quote? + // Tolerate various edge cases like undoubled opening + // (non-closing) quotes, + // or end of input. + + if ((itemEnd + 1) < endPos) + { + nextChar = catedStr.charAt(itemEnd + 1); + nextKind = classifyCharacter(nextChar); + } + else + { + nextKind = UCK_SEMICOLON; + nextChar = 0x3B; + } + + if (ch == nextChar) + { + // This is doubled, copy it and skip the double. + itemValue += ch; + // Loop will add in charSize. + itemEnd++; + } + else if (!isClosingingQuote(ch, openQuote, closeQuote)) + { + // This is an undoubled, non-closing quote, copy it. + itemValue += ch; + } + else + { + // This is an undoubled closing quote, skip it and + // exit the loop. + itemEnd++; + break; + } + } + } + } + + // Add the separated item to the array. + // Keep a matching old value in case it had separators. + int foundIndex = -1; + for (int oldChild = 1; oldChild <= arrayNode.getChildrenLength(); oldChild++) + { + if (itemValue.equals(arrayNode.getChild(oldChild).getValue())) + { + foundIndex = oldChild; + break; + } + } + + XMPNode newItem = null; + if (foundIndex < 0) + { + newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null); + arrayNode.addChild(newItem); + } + } + } + + + /** + * Utility to find or create the array used by <code>separateArrayItems()</code>. + * @param schemaNS a the namespace fo the array + * @param arrayName the name of the array + * @param arrayOptions the options for the array if newly created + * @param xmp the xmp object + * @return Returns the array node. + * @throws XMPException Forwards exceptions + */ + private static XMPNode separateFindCreateArray(String schemaNS, String arrayName, + PropertyOptions arrayOptions, XMPMetaImpl xmp) throws XMPException + { + arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null); + if (!arrayOptions.isOnlyArrayOptions()) + { + throw new XMPException("Options can only provide array form", XMPError.BADOPTIONS); + } + + // Find the array node, make sure it is OK. Move the current children + // aside, to be readded later if kept. + XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); + XMPNode arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, false, null); + if (arrayNode != null) + { + // The array exists, make sure the form is compatible. Zero + // arrayForm means take what exists. + PropertyOptions arrayForm = arrayNode.getOptions(); + if (!arrayForm.isArray() || arrayForm.isArrayAlternate()) + { + throw new XMPException("Named property must be non-alternate array", + XMPError.BADXPATH); + } + if (arrayOptions.equalArrayTypes(arrayForm)) + { + throw new XMPException("Mismatch of specified and existing array form", + XMPError.BADXPATH); // *** Right error? + } + } + else + { + // The array does not exist, try to create it. + // don't modify the options handed into the method + arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, true, arrayOptions + .setArray(true)); + if (arrayNode == null) + { + throw new XMPException("Failed to create named array", XMPError.BADXPATH); + } + } + return arrayNode; + } + + + /** + * @see XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean) + * + * @param xmp + * The XMP object containing the properties to be removed. + * + * @param schemaNS + * Optional schema namespace URI for the properties to be + * removed. + * + * @param propName + * Optional path expression for the property to be removed. + * + * @param doAllProperties + * Option flag to control the deletion: do internal properties in + * addition to external properties. + * @param includeAliases + * Option flag to control the deletion: Include aliases in the + * "named schema" case above. + * @throws XMPException If metadata processing fails + */ + public static void removeProperties(XMPMeta xmp, String schemaNS, String propName, + boolean doAllProperties, boolean includeAliases) throws XMPException + { + ParameterAsserts.assertImplementation(xmp); + XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; + + if (propName != null && propName.length() > 0) + { + // Remove just the one indicated property. This might be an alias, + // the named schema might not actually exist. So don't lookup the + // schema node. + + if (schemaNS == null || schemaNS.length() == 0) + { + throw new XMPException("Property name requires schema namespace", + XMPError.BADPARAM); + } + + XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); + + XMPNode propNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), expPath, false, null); + if (propNode != null) + { + if (doAllProperties + || !Utils.isInternalProperty(expPath.getSegment(XMPPath.STEP_SCHEMA) + .getName(), expPath.getSegment(XMPPath.STEP_ROOT_PROP).getName())) + { + XMPNode parent = propNode.getParent(); + parent.removeChild(propNode); + if (parent.getOptions().isSchemaNode() && !parent.hasChildren()) + { + // remove empty schema node + parent.getParent().removeChild(parent); + } + + } + } + } + else if (schemaNS != null && schemaNS.length() > 0) + { + + // Remove all properties from the named schema. Optionally include + // aliases, in which case + // there might not be an actual schema node. + + // XMP_NodePtrPos schemaPos; + XMPNode schemaNode = XMPNodeUtils.findSchemaNode(xmpImpl.getRoot(), schemaNS, false); + if (schemaNode != null) + { + if (removeSchemaChildren(schemaNode, doAllProperties)) + { + xmpImpl.getRoot().removeChild(schemaNode); + } + } + + if (includeAliases) + { + // We're removing the aliases also. Look them up by their + // namespace prefix. + // But that takes more code and the extra speed isn't worth it. + // Lookup the XMP node + // from the alias, to make sure the actual exists. + + XMPAliasInfo[] aliases = XMPMetaFactory.getSchemaRegistry().findAliases(schemaNS); + for (int i = 0; i < aliases.length; i++) + { + XMPAliasInfo info = aliases[i]; + XMPPath path = XMPPathParser.expandXPath(info.getNamespace(), info + .getPropName()); + XMPNode actualProp = XMPNodeUtils + .findNode(xmpImpl.getRoot(), path, false, null); + if (actualProp != null) + { + XMPNode parent = actualProp.getParent(); + parent.removeChild(actualProp); + } + } + } + } + else + { + // Remove all appropriate properties from all schema. In this case + // we don't have to be + // concerned with aliases, they are handled implicitly from the + // actual properties. + for (Iterator it = xmpImpl.getRoot().iterateChildren(); it.hasNext();) + { + XMPNode schema = (XMPNode) it.next(); + if (removeSchemaChildren(schema, doAllProperties)) + { + it.remove(); + } + } + } + } + + + /** + * @see XMPUtils#appendProperties(XMPMeta, XMPMeta, boolean, boolean) + * @param source The source XMP object. + * @param destination The destination XMP object. + * @param doAllProperties Do internal properties in addition to external properties. + * @param replaceOldValues Replace the values of existing properties. + * @param deleteEmptyValues Delete destination values if source property is empty. + * @throws XMPException Forwards the Exceptions from the metadata processing + */ + public static void appendProperties(XMPMeta source, XMPMeta destination, + boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues) + throws XMPException + { + ParameterAsserts.assertImplementation(source); + ParameterAsserts.assertImplementation(destination); + + XMPMetaImpl src = (XMPMetaImpl) source; + XMPMetaImpl dest = (XMPMetaImpl) destination; + + for (Iterator it = src.getRoot().iterateChildren(); it.hasNext();) + { + XMPNode sourceSchema = (XMPNode) it.next(); + + // Make sure we have a destination schema node + XMPNode destSchema = XMPNodeUtils.findSchemaNode(dest.getRoot(), + sourceSchema.getName(), false); + boolean createdSchema = false; + if (destSchema == null) + { + destSchema = new XMPNode(sourceSchema.getName(), sourceSchema.getValue(), + new PropertyOptions().setSchemaNode(true)); + dest.getRoot().addChild(destSchema); + createdSchema = true; + } + + // Process the source schema's children. + for (Iterator ic = sourceSchema.iterateChildren(); ic.hasNext();) + { + XMPNode sourceProp = (XMPNode) ic.next(); + if (doAllProperties + || !Utils.isInternalProperty(sourceSchema.getName(), sourceProp.getName())) + { + appendSubtree( + dest, sourceProp, destSchema, replaceOldValues, deleteEmptyValues); + } + } + + if (!destSchema.hasChildren() && (createdSchema || deleteEmptyValues)) + { + // Don't create an empty schema / remove empty schema. + dest.getRoot().removeChild(destSchema); + } + } + } + + + /** + * Remove all schema children according to the flag + * <code>doAllProperties</code>. Empty schemas are automatically remove + * by <code>XMPNode</code> + * + * @param schemaNode + * a schema node + * @param doAllProperties + * flag if all properties or only externals shall be removed. + * @return Returns true if the schema is empty after the operation. + */ + private static boolean removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties) + { + for (Iterator it = schemaNode.iterateChildren(); it.hasNext();) + { + XMPNode currProp = (XMPNode) it.next(); + if (doAllProperties + || !Utils.isInternalProperty(schemaNode.getName(), currProp.getName())) + { + it.remove(); + } + } + + return !schemaNode.hasChildren(); + } + + + /** + * @see XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) + * @param destXMP The destination XMP object. + * @param sourceNode the source node + * @param destParent the parent of the destination node + * @param replaceOldValues Replace the values of existing properties. + * @param deleteEmptyValues flag if properties with empty values should be deleted + * in the destination object. + * @throws XMPException + */ + private static void appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent, + boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException + { + XMPNode destNode = XMPNodeUtils.findChildNode(destParent, sourceNode.getName(), false); + + boolean valueIsEmpty = false; + if (deleteEmptyValues) + { + valueIsEmpty = sourceNode.getOptions().isSimple() ? + sourceNode.getValue() == null || sourceNode.getValue().length() == 0 : + !sourceNode.hasChildren(); + } + + if (deleteEmptyValues && valueIsEmpty) + { + if (destNode != null) + { + destParent.removeChild(destNode); + } + } + else if (destNode == null) + { + // The one easy case, the destination does not exist. + destParent.addChild((XMPNode) sourceNode.clone()); + } + else if (replaceOldValues) + { + // The destination exists and should be replaced. + destXMP.setNode(destNode, sourceNode.getValue(), sourceNode.getOptions(), true); + destParent.removeChild(destNode); + destNode = (XMPNode) sourceNode.clone(); + destParent.addChild(destNode); + } + else + { + // The destination exists and is not totally replaced. Structs and + // arrays are merged. + + PropertyOptions sourceForm = sourceNode.getOptions(); + PropertyOptions destForm = destNode.getOptions(); + if (sourceForm != destForm) + { + return; + } + if (sourceForm.isStruct()) + { + // To merge a struct process the fields recursively. E.g. add simple missing fields. + // The recursive call to AppendSubtree will handle deletion for fields with empty + // values. + for (Iterator it = sourceNode.iterateChildren(); it.hasNext();) + { + XMPNode sourceField = (XMPNode) it.next(); + appendSubtree(destXMP, sourceField, destNode, + replaceOldValues, deleteEmptyValues); + if (deleteEmptyValues && !destNode.hasChildren()) + { + destParent.removeChild(destNode); + } + } + } + else if (sourceForm.isArrayAltText()) + { + // Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first. + // Make a special check for deletion of empty values. Meaningful in AltText arrays + // because the "xml:lang" qualifier provides unambiguous source/dest correspondence. + for (Iterator it = sourceNode.iterateChildren(); it.hasNext();) + { + XMPNode sourceItem = (XMPNode) it.next(); + if (!sourceItem.hasQualifier() + || !XMPConst.XML_LANG.equals(sourceItem.getQualifier(1).getName())) + { + continue; + } + + int destIndex = XMPNodeUtils.lookupLanguageItem(destNode, + sourceItem.getQualifier(1).getValue()); + if (deleteEmptyValues && + (sourceItem.getValue() == null || + sourceItem.getValue().length() == 0)) + { + if (destIndex != -1) + { + destNode.removeChild(destIndex); + if (!destNode.hasChildren()) + { + destParent.removeChild(destNode); + } + } + } + else if (destIndex == -1) + { + // Not replacing, keep the existing item. + if (!XMPConst.X_DEFAULT.equals(sourceItem.getQualifier(1).getValue()) + || !destNode.hasChildren()) + { + sourceItem.cloneSubtree(destNode); + } + else + { + XMPNode destItem = new XMPNode( + sourceItem.getName(), + sourceItem.getValue(), + sourceItem.getOptions()); + sourceItem.cloneSubtree(destItem); + destNode.addChild(1, destItem); + } + } + } + } + else if (sourceForm.isArray()) + { + // Merge other arrays by item values. Don't worry about order or duplicates. Source + // items with empty values do not cause deletion, that conflicts horribly with + // merging. + + for (Iterator is = sourceNode.iterateChildren(); is.hasNext();) + { + XMPNode sourceItem = (XMPNode) is.next(); + + boolean match = false; + for (Iterator id = destNode.iterateChildren(); id.hasNext();) + { + XMPNode destItem = (XMPNode) id.next(); + if (itemValuesMatch(sourceItem, destItem)) + { + match = true; + } + } + if (!match) + { + destNode = (XMPNode) sourceItem.clone(); + destParent.addChild(destNode); + } + } + } + } + } + + + /** + * Compares two nodes including its children and qualifier. + * @param leftNode an <code>XMPNode</code> + * @param rightNode an <code>XMPNode</code> + * @return Returns true if the nodes are equal, false otherwise. + * @throws XMPException Forwards exceptions to the calling method. + */ + private static boolean itemValuesMatch(XMPNode leftNode, XMPNode rightNode) throws XMPException + { + PropertyOptions leftForm = leftNode.getOptions(); + PropertyOptions rightForm = rightNode.getOptions(); + + if (leftForm.equals(rightForm)) + { + return false; + } + + if (leftForm.getOptions() == 0) + { + // Simple nodes, check the values and xml:lang qualifiers. + if (!leftNode.getValue().equals(rightNode.getValue())) + { + return false; + } + if (leftNode.getOptions().getHasLanguage() != rightNode.getOptions().getHasLanguage()) + { + return false; + } + if (leftNode.getOptions().getHasLanguage() + && !leftNode.getQualifier(1).getValue().equals( + rightNode.getQualifier(1).getValue())) + { + return false; + } + } + else if (leftForm.isStruct()) + { + // Struct nodes, see if all fields match, ignoring order. + + if (leftNode.getChildrenLength() != rightNode.getChildrenLength()) + { + return false; + } + + for (Iterator it = leftNode.iterateChildren(); it.hasNext();) + { + XMPNode leftField = (XMPNode) it.next(); + XMPNode rightField = XMPNodeUtils.findChildNode(rightNode, leftField.getName(), + false); + if (rightField == null || !itemValuesMatch(leftField, rightField)) + { + return false; + } + } + } + else + { + // Array nodes, see if the "leftNode" values are present in the + // "rightNode", ignoring order, duplicates, + // and extra values in the rightNode-> The rightNode is the + // destination for AppendProperties. + + assert leftForm.isArray(); + + for (Iterator il = leftNode.iterateChildren(); il.hasNext();) + { + XMPNode leftItem = (XMPNode) il.next(); + + boolean match = false; + for (Iterator ir = rightNode.iterateChildren(); ir.hasNext();) + { + XMPNode rightItem = (XMPNode) ir.next(); + if (itemValuesMatch(leftItem, rightItem)) + { + match = true; + break; + } + } + if (!match) + { + return false; + } + } + } + return true; // All of the checks passed. + } + + + /** + * Make sure the separator is OK. It must be one semicolon surrounded by + * zero or more spaces. Any of the recognized semicolons or spaces are + * allowed. + * + * @param separator + * @throws XMPException + */ + private static void checkSeparator(String separator) throws XMPException + { + boolean haveSemicolon = false; + for (int i = 0; i < separator.length(); i++) + { + int charKind = classifyCharacter(separator.charAt(i)); + if (charKind == UCK_SEMICOLON) + { + if (haveSemicolon) + { + throw new XMPException("Separator can have only one semicolon", + XMPError.BADPARAM); + } + haveSemicolon = true; + } + else if (charKind != UCK_SPACE) + { + throw new XMPException("Separator can have only spaces and one semicolon", + XMPError.BADPARAM); + } + } + if (!haveSemicolon) + { + throw new XMPException("Separator must have one semicolon", XMPError.BADPARAM); + } + } + + + /** + * Make sure the open and close quotes are a legitimate pair and return the + * correct closing quote or an exception. + * + * @param quotes + * opened and closing quote in a string + * @param openQuote + * the open quote + * @return Returns a corresponding closing quote. + * @throws XMPException + */ + private static char checkQuotes(String quotes, char openQuote) throws XMPException + { + char closeQuote; + + int charKind = classifyCharacter(openQuote); + if (charKind != UCK_QUOTE) + { + throw new XMPException("Invalid quoting character", XMPError.BADPARAM); + } + + if (quotes.length() == 1) + { + closeQuote = openQuote; + } + else + { + closeQuote = quotes.charAt(1); + charKind = classifyCharacter(closeQuote); + if (charKind != UCK_QUOTE) + { + throw new XMPException("Invalid quoting character", XMPError.BADPARAM); + } + } + + if (closeQuote != getClosingQuote(openQuote)) + { + throw new XMPException("Mismatched quote pair", XMPError.BADPARAM); + } + return closeQuote; + } + + + /** + * Classifies the character into normal chars, spaces, semicola, quotes, + * control chars. + * + * @param ch + * a char + * @return Return the character kind. + */ + private static int classifyCharacter(char ch) + { + if (SPACES.indexOf(ch) >= 0 || (0x2000 <= ch && ch <= 0x200B)) + { + return UCK_SPACE; + } + else if (COMMAS.indexOf(ch) >= 0) + { + return UCK_COMMA; + } + else if (SEMICOLA.indexOf(ch) >= 0) + { + return UCK_SEMICOLON; + } + else if (QUOTES.indexOf(ch) >= 0 || (0x3008 <= ch && ch <= 0x300F) + || (0x2018 <= ch && ch <= 0x201F)) + { + return UCK_QUOTE; + } + else if (ch < 0x0020 || CONTROLS.indexOf(ch) >= 0) + { + return UCK_CONTROL; + } + else + { + // Assume typical case. + return UCK_NORMAL; + } + } + + + /** + * @param openQuote + * the open quote char + * @return Returns the matching closing quote for an open quote. + */ + private static char getClosingQuote(char openQuote) + { + switch (openQuote) + { + case 0x0022: + return 0x0022; // ! U+0022 is both opening and closing. + case 0x005B: + return 0x005D; + case 0x00AB: + return 0x00BB; // ! U+00AB and U+00BB are reversible. + case 0x00BB: + return 0x00AB; + case 0x2015: + return 0x2015; // ! U+2015 is both opening and closing. + case 0x2018: + return 0x2019; + case 0x201A: + return 0x201B; + case 0x201C: + return 0x201D; + case 0x201E: + return 0x201F; + case 0x2039: + return 0x203A; // ! U+2039 and U+203A are reversible. + case 0x203A: + return 0x2039; + case 0x3008: + return 0x3009; + case 0x300A: + return 0x300B; + case 0x300C: + return 0x300D; + case 0x300E: + return 0x300F; + case 0x301D: + return 0x301F; // ! U+301E also closes U+301D. + default: + return 0; + } + } + + + /** + * Add quotes to the item. + * + * @param item + * the array item + * @param openQuote + * the open quote character + * @param closeQuote + * the closing quote character + * @param allowCommas + * flag if commas are allowed + * @return Returns the value in quotes. + */ + private static String applyQuotes(String item, char openQuote, char closeQuote, + boolean allowCommas) + { + if (item == null) + { + item = ""; + } + + boolean prevSpace = false; + int charOffset; + int charKind; + + // See if there are any separators in the value. Stop at the first + // occurrance. This is a bit + // tricky in order to make typical typing work conveniently. The purpose + // of applying quotes + // is to preserve the values when splitting them back apart. That is + // CatenateContainerItems + // and SeparateContainerItems must round trip properly. For the most + // part we only look for + // separators here. Internal quotes, as in -- Irving "Bud" Jones -- + // won't cause problems in + // the separation. An initial quote will though, it will make the value + // look quoted. + + int i; + for (i = 0; i < item.length(); i++) + { + char ch = item.charAt(i); + charKind = classifyCharacter(ch); + if (i == 0 && charKind == UCK_QUOTE) + { + break; + } + + if (charKind == UCK_SPACE) + { + // Multiple spaces are a separator. + if (prevSpace) + { + break; + } + prevSpace = true; + } + else + { + prevSpace = false; + if ((charKind == UCK_SEMICOLON || charKind == UCK_CONTROL) + || (charKind == UCK_COMMA && !allowCommas)) + { + break; + } + } + } + + + if (i < item.length()) + { + // Create a quoted copy, doubling any internal quotes that match the + // outer ones. Internal quotes did not stop the "needs quoting" + // search, but they do need + // doubling. So we have to rescan the front of the string for + // quotes. Handle the special + // case of U+301D being closed by either U+301E or U+301F. + + StringBuffer newItem = new StringBuffer(item.length() + 2); + int splitPoint; + for (splitPoint = 0; splitPoint <= i; splitPoint++) + { + if (classifyCharacter(item.charAt(i)) == UCK_QUOTE) + { + break; + } + } + + // Copy the leading "normal" portion. + newItem.append(openQuote).append(item.substring(0, splitPoint)); + + for (charOffset = splitPoint; charOffset < item.length(); charOffset++) + { + newItem.append(item.charAt(charOffset)); + if (classifyCharacter(item.charAt(charOffset)) == UCK_QUOTE + && isSurroundingQuote(item.charAt(charOffset), openQuote, closeQuote)) + { + newItem.append(item.charAt(charOffset)); + } + } + + newItem.append(closeQuote); + + item = newItem.toString(); + } + + return item; + } + + + /** + * @param ch a character + * @param openQuote the opening quote char + * @param closeQuote the closing quote char + * @return Return it the character is a surrounding quote. + */ + private static boolean isSurroundingQuote(char ch, char openQuote, char closeQuote) + { + return ch == openQuote || isClosingingQuote(ch, openQuote, closeQuote); + } + + + /** + * @param ch a character + * @param openQuote the opening quote char + * @param closeQuote the closing quote char + * @return Returns true if the character is a closing quote. + */ + private static boolean isClosingingQuote(char ch, char openQuote, char closeQuote) + { + return ch == closeQuote || (openQuote == 0x301D && ch == 0x301E || ch == 0x301F); + } + + + + /** + * U+0022 ASCII space<br> + * U+3000, ideographic space<br> + * U+303F, ideographic half fill space<br> + * U+2000..U+200B, en quad through zero width space + */ + private static final String SPACES = "\u0020\u3000\u303F"; + /** + * U+002C, ASCII comma<br> + * U+FF0C, full width comma<br> + * U+FF64, half width ideographic comma<br> + * U+FE50, small comma<br> + * U+FE51, small ideographic comma<br> + * U+3001, ideographic comma<br> + * U+060C, Arabic comma<br> + * U+055D, Armenian comma + */ + private static final String COMMAS = "\u002C\uFF0C\uFF64\uFE50\uFE51\u3001\u060C\u055D"; + /** + * U+003B, ASCII semicolon<br> + * U+FF1B, full width semicolon<br> + * U+FE54, small semicolon<br> + * U+061B, Arabic semicolon<br> + * U+037E, Greek "semicolon" (really a question mark) + */ + private static final String SEMICOLA = "\u003B\uFF1B\uFE54\u061B\u037E"; + /** + * U+0022 ASCII quote<br> + * ASCII '[' (0x5B) and ']' (0x5D) are used as quotes in Chinese and + * Korean.<br> + * U+00AB and U+00BB, guillemet quotes<br> + * U+3008..U+300F, various quotes.<br> + * U+301D..U+301F, double prime quotes.<br> + * U+2015, dash quote.<br> + * U+2018..U+201F, various quotes.<br> + * U+2039 and U+203A, guillemet quotes. + */ + private static final String QUOTES = + "\"\u005B\u005D\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A"; + /** + * U+0000..U+001F ASCII controls<br> + * U+2028, line separator.<br> + * U+2029, paragraph separator. + */ + private static final String CONTROLS = "\u2028\u2029"; +} diff --git a/XMPCore/src/com/adobe/xmp/impl/package.html b/XMPCore/src/com/adobe/xmp/impl/package.html new file mode 100644 index 0000000..aea0dfa --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/package.html @@ -0,0 +1,11 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> + <title>Package overview</title> +</head> + +<body> + <p>Package containing the xmpcore implementation.</p> +</body> +</html> diff --git a/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java b/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java new file mode 100644 index 0000000..ff810dc --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java @@ -0,0 +1,106 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl.xpath; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Representates an XMP XMPPath with segment accessor methods. + * + * @since 28.02.2006 + */ +public class XMPPath +{ + // Bits for XPathStepInfo options. + + /** Marks a struct field step , also for top level nodes (schema "fields"). */ + public static final int STRUCT_FIELD_STEP = 0x01; + /** Marks a qualifier step. + * Note: Order is significant to separate struct/qual from array kinds! */ + public static final int QUALIFIER_STEP = 0x02; // + /** Marks an array index step */ + public static final int ARRAY_INDEX_STEP = 0x03; + /** */ + public static final int ARRAY_LAST_STEP = 0x04; + /** */ + public static final int QUAL_SELECTOR_STEP = 0x05; + /** */ + public static final int FIELD_SELECTOR_STEP = 0x06; + /** */ + public static final int SCHEMA_NODE = 0x80000000; + /** */ + public static final int STEP_SCHEMA = 0; + /** */ + public static final int STEP_ROOT_PROP = 1; + + + /** stores the segments of an XMPPath */ + private List segments = new ArrayList(5); + + + /** + * Append a path segment + * + * @param segment the segment to add + */ + public void add(XMPPathSegment segment) + { + segments.add(segment); + } + + + /** + * @param index the index of the segment to return + * @return Returns a path segment. + */ + public XMPPathSegment getSegment(int index) + { + return (XMPPathSegment) segments.get(index); + } + + + /** + * @return Returns the size of the xmp path. + */ + public int size() + { + return segments.size(); + } + + + /** + * Serializes the normalized XMP-path. + * @see Object#toString() + */ + public String toString() + { + StringBuffer result = new StringBuffer(); + int index = 1; + while (index < size()) + { + result.append(getSegment(index)); + if (index < size() - 1) + { + int kind = getSegment(index + 1).getKind(); + if (kind == STRUCT_FIELD_STEP || + kind == QUALIFIER_STEP) + { + // all but last and array indices + result.append('/'); + } + } + index++; + } + + return result.toString(); + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java b/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java new file mode 100644 index 0000000..9936c06 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java @@ -0,0 +1,537 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl.xpath; + +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.impl.Utils; +import com.adobe.xmp.properties.XMPAliasInfo; + + +/** + * Parser for XMP XPaths. + * + * @since 01.03.2006 + */ +public final class XMPPathParser +{ + /** + * Private constructor + */ + private XMPPathParser() + { + // empty + } + + + /** + * Split an XMPPath expression apart at the conceptual steps, adding the + * root namespace prefix to the first property component. The schema URI is + * put in the first (0th) slot in the expanded XMPPath. Check if the top + * level component is an alias, but don't resolve it. + * <p> + * In the most verbose case steps are separated by '/', and each step can be + * of these forms: + * <dl> + * <dt>prefix:name + * <dd> A top level property or struct field. + * <dt>[index] + * <dd> An element of an array. + * <dt>[last()] + * <dd> The last element of an array. + * <dt>[fieldName="value"] + * <dd> An element in an array of structs, chosen by a field value. + * <dt>[@xml:lang="value"] + * <dd> An element in an alt-text array, chosen by the xml:lang qualifier. + * <dt>[?qualName="value"] + * <dd> An element in an array, chosen by a qualifier value. + * <dt>@xml:lang + * <dd> An xml:lang qualifier. + * <dt>?qualName + * <dd> A general qualifier. + * </dl> + * <p> + * The logic is complicated though by shorthand for arrays, the separating + * '/' and leading '*' are optional. These are all equivalent: array/*[2] + * array/[2] array*[2] array[2] All of these are broken into the 2 steps + * "array" and "[2]". + * <p> + * The value portion in the array selector forms is a string quoted by ''' + * or '"'. The value may contain any character including a doubled quoting + * character. The value may be empty. + * <p> + * The syntax isn't checked, but an XML name begins with a letter or '_', + * and contains letters, digits, '.', '-', '_', and a bunch of special + * non-ASCII Unicode characters. An XML qualified name is a pair of names + * separated by a colon. + * @param schemaNS + * schema namespace + * @param path + * property name + * @return Returns the expandet XMPPath. + * @throws XMPException + * Thrown if the format is not correct somehow. + * + */ + public static XMPPath expandXPath(String schemaNS, String path) throws XMPException + { + if (schemaNS == null || path == null) + { + throw new XMPException("Parameter must not be null", XMPError.BADPARAM); + } + + XMPPath expandedXPath = new XMPPath(); + PathPosition pos = new PathPosition(); + pos.path = path; + + // Pull out the first component and do some special processing on it: add the schema + // namespace prefix and and see if it is an alias. The start must be a "qualName". + parseRootNode(schemaNS, pos, expandedXPath); + + // Now continue to process the rest of the XMPPath string. + while (pos.stepEnd < path.length()) + { + pos.stepBegin = pos.stepEnd; + + skipPathDelimiter(path, pos); + + pos.stepEnd = pos.stepBegin; + + + XMPPathSegment segment; + if (path.charAt(pos.stepBegin) != '[') + { + // A struct field or qualifier. + segment = parseStructSegment(pos); + } + else + { + // One of the array forms. + segment = parseIndexSegment(pos); + } + + + if (segment.getKind() == XMPPath.STRUCT_FIELD_STEP) + { + if (segment.getName().charAt(0) == '@') + { + segment.setName("?" + segment.getName().substring(1)); + if (!"?xml:lang".equals(segment.getName())) + { + throw new XMPException("Only xml:lang allowed with '@'", + XMPError.BADXPATH); + } + } + if (segment.getName().charAt(0) == '?') + { + pos.nameStart++; + segment.setKind(XMPPath.QUALIFIER_STEP); + } + + verifyQualName(pos.path.substring(pos.nameStart, pos.nameEnd)); + } + else if (segment.getKind() == XMPPath.FIELD_SELECTOR_STEP) + { + if (segment.getName().charAt(1) == '@') + { + segment.setName("[?" + segment.getName().substring(2)); + if (!segment.getName().startsWith("[?xml:lang=")) + { + throw new XMPException("Only xml:lang allowed with '@'", + XMPError.BADXPATH); + } + } + + if (segment.getName().charAt(1) == '?') + { + pos.nameStart++; + segment.setKind(XMPPath.QUAL_SELECTOR_STEP); + verifyQualName(pos.path.substring(pos.nameStart, pos.nameEnd)); + } + } + + expandedXPath.add(segment); + } + return expandedXPath; + } + + + /** + * @param path + * @param pos + * @throws XMPException + */ + private static void skipPathDelimiter(String path, PathPosition pos) throws XMPException + { + if (path.charAt(pos.stepBegin) == '/') + { + // skip slash + + pos.stepBegin++; + + // added for Java + if (pos.stepBegin >= path.length()) + { + throw new XMPException("Empty XMPPath segment", XMPError.BADXPATH); + } + } + + if (path.charAt(pos.stepBegin) == '*') + { + // skip asterisk + + pos.stepBegin++; + if (pos.stepBegin >= path.length() || path.charAt(pos.stepBegin) != '[') + { + throw new XMPException("Missing '[' after '*'", XMPError.BADXPATH); + } + } + } + + + /** + * Parses a struct segment + * @param pos the current position in the path + * @return Retusn the segment or an errror + * @throws XMPException If the sement is empty + */ + private static XMPPathSegment parseStructSegment(PathPosition pos) throws XMPException + { + pos.nameStart = pos.stepBegin; + while (pos.stepEnd < pos.path.length() && "/[*".indexOf(pos.path.charAt(pos.stepEnd)) < 0) + { + pos.stepEnd++; + } + pos.nameEnd = pos.stepEnd; + + if (pos.stepEnd == pos.stepBegin) + { + throw new XMPException("Empty XMPPath segment", XMPError.BADXPATH); + } + + // ! Touch up later, also changing '@' to '?'. + XMPPathSegment segment = new XMPPathSegment(pos.path.substring(pos.stepBegin, pos.stepEnd), + XMPPath.STRUCT_FIELD_STEP); + return segment; + } + + + /** + * Parses an array index segment. + * + * @param pos the xmp path + * @return Returns the segment or an error + * @throws XMPException thrown on xmp path errors + * + */ + private static XMPPathSegment parseIndexSegment(PathPosition pos) throws XMPException + { + XMPPathSegment segment; + pos.stepEnd++; // Look at the character after the leading '['. + + if ('0' <= pos.path.charAt(pos.stepEnd) && pos.path.charAt(pos.stepEnd) <= '9') + { + // A numeric (decimal integer) array index. + while (pos.stepEnd < pos.path.length() && '0' <= pos.path.charAt(pos.stepEnd) + && pos.path.charAt(pos.stepEnd) <= '9') + { + pos.stepEnd++; + } + + segment = new XMPPathSegment(null, XMPPath.ARRAY_INDEX_STEP); + } + else + { + // Could be "[last()]" or one of the selector forms. Find the ']' or '='. + + while (pos.stepEnd < pos.path.length() && pos.path.charAt(pos.stepEnd) != ']' + && pos.path.charAt(pos.stepEnd) != '=') + { + pos.stepEnd++; + } + + if (pos.stepEnd >= pos.path.length()) + { + throw new XMPException("Missing ']' or '=' for array index", XMPError.BADXPATH); + } + + if (pos.path.charAt(pos.stepEnd) == ']') + { + if (!"[last()".equals(pos.path.substring(pos.stepBegin, pos.stepEnd))) + { + throw new XMPException( + "Invalid non-numeric array index", XMPError.BADXPATH); + } + segment = new XMPPathSegment(null, XMPPath.ARRAY_LAST_STEP); + } + else + { + pos.nameStart = pos.stepBegin + 1; + pos.nameEnd = pos.stepEnd; + pos.stepEnd++; // Absorb the '=', remember the quote. + char quote = pos.path.charAt(pos.stepEnd); + if (quote != '\'' && quote != '"') + { + throw new XMPException( + "Invalid quote in array selector", XMPError.BADXPATH); + } + + pos.stepEnd++; // Absorb the leading quote. + while (pos.stepEnd < pos.path.length()) + { + if (pos.path.charAt(pos.stepEnd) == quote) + { + // check for escaped quote + if (pos.stepEnd + 1 >= pos.path.length() + || pos.path.charAt(pos.stepEnd + 1) != quote) + { + break; + } + pos.stepEnd++; + } + pos.stepEnd++; + } + + if (pos.stepEnd >= pos.path.length()) + { + throw new XMPException("No terminating quote for array selector", + XMPError.BADXPATH); + } + pos.stepEnd++; // Absorb the trailing quote. + + // ! Touch up later, also changing '@' to '?'. + segment = new XMPPathSegment(null, XMPPath.FIELD_SELECTOR_STEP); + } + } + + + if (pos.stepEnd >= pos.path.length() || pos.path.charAt(pos.stepEnd) != ']') + { + throw new XMPException("Missing ']' for array index", XMPError.BADXPATH); + } + pos.stepEnd++; + segment.setName(pos.path.substring(pos.stepBegin, pos.stepEnd)); + + return segment; + } + + + /** + * Parses the root node of an XMP Path, checks if namespace and prefix fit together + * and resolve the property to the base property if it is an alias. + * @param schemaNS the root namespace + * @param pos the parsing position helper + * @param expandedXPath the path to contribute to + * @throws XMPException If the path is not valid. + */ + private static void parseRootNode(String schemaNS, PathPosition pos, XMPPath expandedXPath) + throws XMPException + { + while (pos.stepEnd < pos.path.length() && "/[*".indexOf(pos.path.charAt(pos.stepEnd)) < 0) + { + pos.stepEnd++; + } + + if (pos.stepEnd == pos.stepBegin) + { + throw new XMPException("Empty initial XMPPath step", XMPError.BADXPATH); + } + + String rootProp = verifyXPathRoot(schemaNS, pos.path.substring(pos.stepBegin, pos.stepEnd)); + XMPAliasInfo aliasInfo = XMPMetaFactory.getSchemaRegistry().findAlias(rootProp); + if (aliasInfo == null) + { + // add schema xpath step + expandedXPath.add(new XMPPathSegment(schemaNS, XMPPath.SCHEMA_NODE)); + XMPPathSegment rootStep = new XMPPathSegment(rootProp, XMPPath.STRUCT_FIELD_STEP); + expandedXPath.add(rootStep); + } + else + { + // add schema xpath step and base step of alias + expandedXPath.add(new XMPPathSegment(aliasInfo.getNamespace(), XMPPath.SCHEMA_NODE)); + XMPPathSegment rootStep = new XMPPathSegment(verifyXPathRoot(aliasInfo.getNamespace(), + aliasInfo.getPropName()), + XMPPath.STRUCT_FIELD_STEP); + rootStep.setAlias(true); + rootStep.setAliasForm(aliasInfo.getAliasForm().getOptions()); + expandedXPath.add(rootStep); + + if (aliasInfo.getAliasForm().isArrayAltText()) + { + XMPPathSegment qualSelectorStep = new XMPPathSegment("[?xml:lang='x-default']", + XMPPath.QUAL_SELECTOR_STEP); + qualSelectorStep.setAlias(true); + qualSelectorStep.setAliasForm(aliasInfo.getAliasForm().getOptions()); + expandedXPath.add(qualSelectorStep); + } + else if (aliasInfo.getAliasForm().isArray()) + { + XMPPathSegment indexStep = new XMPPathSegment("[1]", + XMPPath.ARRAY_INDEX_STEP); + indexStep.setAlias(true); + indexStep.setAliasForm(aliasInfo.getAliasForm().getOptions()); + expandedXPath.add(indexStep); + } + } + } + + + /** + * Verifies whether the qualifier name is not XML conformant or the + * namespace prefix has not been registered. + * + * @param qualName + * a qualifier name + * @throws XMPException + * If the name is not conformant + */ + private static void verifyQualName(String qualName) throws XMPException + { + int colonPos = qualName.indexOf(':'); + if (colonPos > 0) + { + String prefix = qualName.substring(0, colonPos); + if (Utils.isXMLNameNS(prefix)) + { + String regURI = XMPMetaFactory.getSchemaRegistry().getNamespaceURI( + prefix); + if (regURI != null) + { + return; + } + + throw new XMPException("Unknown namespace prefix for qualified name", + XMPError.BADXPATH); + } + } + + throw new XMPException("Ill-formed qualified name", XMPError.BADXPATH); + } + + + /** + * Verify if an XML name is conformant. + * + * @param name + * an XML name + * @throws XMPException + * When the name is not XML conformant + */ + private static void verifySimpleXMLName(String name) throws XMPException + { + if (!Utils.isXMLName(name)) + { + throw new XMPException("Bad XML name", XMPError.BADXPATH); + } + } + + + /** + * Set up the first 2 components of the expanded XMPPath. Normalizes the various cases of using + * the full schema URI and/or a qualified root property name. Returns true for normal + * processing. If allowUnknownSchemaNS is true and the schema namespace is not registered, false + * is returned. If allowUnknownSchemaNS is false and the schema namespace is not registered, an + * exception is thrown + * <P> + * (Should someday check the full syntax:) + * + * @param schemaNS schema namespace + * @param rootProp the root xpath segment + * @return Returns root QName. + * @throws XMPException Thrown if the format is not correct somehow. + */ + private static String verifyXPathRoot(String schemaNS, String rootProp) + throws XMPException + { + // Do some basic checks on the URI and name. Try to lookup the URI. See if the name is + // qualified. + + if (schemaNS == null || schemaNS.length() == 0) + { + throw new XMPException( + "Schema namespace URI is required", XMPError.BADSCHEMA); + } + + if ((rootProp.charAt(0) == '?') || (rootProp.charAt(0) == '@')) + { + throw new XMPException("Top level name must not be a qualifier", XMPError.BADXPATH); + } + + if (rootProp.indexOf('/') >= 0 || rootProp.indexOf('[') >= 0) + { + throw new XMPException("Top level name must be simple", XMPError.BADXPATH); + } + + String prefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(schemaNS); + if (prefix == null) + { + throw new XMPException("Unregistered schema namespace URI", XMPError.BADSCHEMA); + } + + // Verify the various URI and prefix combinations. Initialize the + // expanded XMPPath. + int colonPos = rootProp.indexOf(':'); + if (colonPos < 0) + { + // The propName is unqualified, use the schemaURI and associated + // prefix. + verifySimpleXMLName(rootProp); // Verify the part before any colon + return prefix + rootProp; + } + else + { + // The propName is qualified. Make sure the prefix is legit. Use the associated URI and + // qualified name. + + // Verify the part before any colon + verifySimpleXMLName(rootProp.substring(0, colonPos)); + verifySimpleXMLName(rootProp.substring(colonPos)); + + prefix = rootProp.substring(0, colonPos + 1); + + String regPrefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(schemaNS); + if (regPrefix == null) + { + throw new XMPException("Unknown schema namespace prefix", XMPError.BADSCHEMA); + } + if (!prefix.equals(regPrefix)) + { + throw new XMPException("Schema namespace URI and prefix mismatch", + XMPError.BADSCHEMA); + } + + return rootProp; + } + } +} + + + + + +/** + * This objects contains all needed char positions to parse. + */ +class PathPosition +{ + /** the complete path */ + public String path = null; + /** the start of a segment name */ + int nameStart = 0; + /** the end of a segment name */ + int nameEnd = 0; + /** the begin of a step */ + int stepBegin = 0; + /** the end of a step */ + int stepEnd = 0; +} + diff --git a/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java b/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java new file mode 100644 index 0000000..88b42a1 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java @@ -0,0 +1,147 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.impl.xpath; + + +/** + * A segment of a parsed <code>XMPPath</code>. + * + * @since 23.06.2006 + */ +public class XMPPathSegment +{ + /** name of the path segment */ + private String name; + /** kind of the path segment */ + private int kind; + /** flag if segment is an alias */ + private boolean alias; + /** alias form if applicable */ + private int aliasForm; + + + /** + * Constructor with initial values. + * + * @param name the name of the segment + */ + public XMPPathSegment(String name) + { + this.name = name; + } + + + /** + * Constructor with initial values. + * + * @param name the name of the segment + * @param kind the kind of the segment + */ + public XMPPathSegment(String name, int kind) + { + this.name = name; + this.kind = kind; + } + + + /** + * @return Returns the kind. + */ + public int getKind() + { + return kind; + } + + + /** + * @param kind The kind to set. + */ + public void setKind(int kind) + { + this.kind = kind; + } + + + /** + * @return Returns the name. + */ + public String getName() + { + return name; + } + + + /** + * @param name The name to set. + */ + public void setName(String name) + { + this.name = name; + } + + + /** + * @param alias the flag to set + */ + public void setAlias(boolean alias) + { + this.alias = alias; + } + + + /** + * @return Returns the alias. + */ + public boolean isAlias() + { + return alias; + } + + + /** + * @return Returns the aliasForm if this segment has been created by an alias. + */ + public int getAliasForm() + { + return aliasForm; + } + + + /** + * @param aliasForm the aliasForm to set + */ + public void setAliasForm(int aliasForm) + { + this.aliasForm = aliasForm; + } + + + /** + * @see Object#toString() + */ + public String toString() + { + switch (kind) + { + case XMPPath.STRUCT_FIELD_STEP: + case XMPPath.ARRAY_INDEX_STEP: + case XMPPath.QUALIFIER_STEP: + case XMPPath.ARRAY_LAST_STEP: + return name; + case XMPPath.QUAL_SELECTOR_STEP: + case XMPPath.FIELD_SELECTOR_STEP: + return name; + + default: + // no defined step + return name; + } + } +} diff --git a/XMPCore/src/com/adobe/xmp/impl/xpath/package.html b/XMPCore/src/com/adobe/xmp/impl/xpath/package.html new file mode 100644 index 0000000..e444a87 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/impl/xpath/package.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> + <title>Package overview</title> +</head> + +<body> + <p>Package containing the XMPPath handling.</p> + <p>An XMPPath a simplified form of an XPath, used only to create or retrieve properties in an XMPMeta object.<p> +</body> +</html> diff --git a/XMPCore/src/com/adobe/xmp/options/AliasOptions.java b/XMPCore/src/com/adobe/xmp/options/AliasOptions.java new file mode 100644 index 0000000..cf58273 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/AliasOptions.java @@ -0,0 +1,184 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.options; + +import com.adobe.xmp.XMPException; + + +/** + * Options for XMPSchemaRegistryImpl#registerAlias. + * + * @since 20.02.2006 + */ +public final class AliasOptions extends Options +{ + /** This is a direct mapping. The actual data type does not matter. */ + public static final int PROP_DIRECT = 0; + /** The actual is an unordered array, the alias is to the first element of the array. */ + public static final int PROP_ARRAY = PropertyOptions.ARRAY; + /** The actual is an ordered array, the alias is to the first element of the array. */ + public static final int PROP_ARRAY_ORDERED = PropertyOptions.ARRAY_ORDERED; + /** The actual is an alternate array, the alias is to the first element of the array. */ + public static final int PROP_ARRAY_ALTERNATE = PropertyOptions.ARRAY_ALTERNATE; + /** + * The actual is an alternate text array, the alias is to the 'x-default' element of the array. + */ + public static final int PROP_ARRAY_ALT_TEXT = PropertyOptions.ARRAY_ALT_TEXT; + + + /** + * @see Options#Options() + */ + public AliasOptions() + { + // EMPTY + } + + + /** + * @param options the options to init with + * @throws XMPException If options are not consistant + */ + public AliasOptions(int options) throws XMPException + { + super(options); + } + + + /** + * @return Returns if the alias is of the simple form. + */ + public boolean isSimple() + { + return getOptions() == PROP_DIRECT; + } + + + /** + * @return Returns the option. + */ + public boolean isArray() + { + return getOption(PROP_ARRAY); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public AliasOptions setArray(boolean value) + { + setOption(PROP_ARRAY, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean isArrayOrdered() + { + return getOption(PROP_ARRAY_ORDERED); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public AliasOptions setArrayOrdered(boolean value) + { + setOption(PROP_ARRAY | PROP_ARRAY_ORDERED, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean isArrayAlternate() + { + return getOption(PROP_ARRAY_ALTERNATE); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public AliasOptions setArrayAlternate(boolean value) + { + setOption(PROP_ARRAY | PROP_ARRAY_ORDERED | PROP_ARRAY_ALTERNATE, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean isArrayAltText() + { + return getOption(PROP_ARRAY_ALT_TEXT); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public AliasOptions setArrayAltText(boolean value) + { + setOption(PROP_ARRAY | PROP_ARRAY_ORDERED | + PROP_ARRAY_ALTERNATE | PROP_ARRAY_ALT_TEXT, value); + return this; + } + + + /** + * @return returns a {@link PropertyOptions}s object + * @throws XMPException If the options are not consistant. + */ + public PropertyOptions toPropertyOptions() throws XMPException + { + return new PropertyOptions(getOptions()); + } + + + /** + * @see Options#defineOptionName(int) + */ + protected String defineOptionName(int option) + { + switch (option) + { + case PROP_DIRECT : return "PROP_DIRECT"; + case PROP_ARRAY : return "ARRAY"; + case PROP_ARRAY_ORDERED : return "ARRAY_ORDERED"; + case PROP_ARRAY_ALTERNATE : return "ARRAY_ALTERNATE"; + case PROP_ARRAY_ALT_TEXT : return "ARRAY_ALT_TEXT"; + default: return null; + } + } + + + /** + * @see Options#getValidOptions() + */ + protected int getValidOptions() + { + return + PROP_DIRECT | + PROP_ARRAY | + PROP_ARRAY_ORDERED | + PROP_ARRAY_ALTERNATE | + PROP_ARRAY_ALT_TEXT; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java b/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java new file mode 100644 index 0000000..3dcb524 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java @@ -0,0 +1,148 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.options; + + +/** + * Options for <code>XMPIterator</code> construction. + * + * @since 24.01.2006 + */ +public final class IteratorOptions extends Options +{ + /** Just do the immediate children of the root, default is subtree. */ + public static final int JUST_CHILDREN = 0x0100; + /** Just do the leaf nodes, default is all nodes in the subtree. */ + public static final int JUST_LEAFNODES = 0x0200; + /** Return just the leaf part of the path, default is the full path. */ + public static final int JUST_LEAFNAME = 0x0400; + /** Include aliases, default is just actual properties. <em>Note:</em> Not supported. + * @deprecated it is commonly preferred to work with the base properties */ + public static final int INCLUDE_ALIASES = 0x0800; + /** Omit all qualifiers. */ + public static final int OMIT_QUALIFIERS = 0x1000; + + + /** + * @return Returns whether the option is set. + */ + public boolean isJustChildren() + { + return getOption(JUST_CHILDREN); + } + + + /** + * @return Returns whether the option is set. + */ + public boolean isJustLeafname() + { + return getOption(JUST_LEAFNAME); + } + + + /** + * @return Returns whether the option is set. + */ + public boolean isJustLeafnodes() + { + return getOption(JUST_LEAFNODES); + } + + + /** + * @return Returns whether the option is set. + */ + public boolean isOmitQualifiers() + { + return getOption(OMIT_QUALIFIERS); + } + + + /** + * Sets the option and returns the instance. + * + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public IteratorOptions setJustChildren(boolean value) + { + setOption(JUST_CHILDREN, value); + return this; + } + + + /** + * Sets the option and returns the instance. + * + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public IteratorOptions setJustLeafname(boolean value) + { + setOption(JUST_LEAFNAME, value); + return this; + } + + + /** + * Sets the option and returns the instance. + * + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public IteratorOptions setJustLeafnodes(boolean value) + { + setOption(JUST_LEAFNODES, value); + return this; + } + + + /** + * Sets the option and returns the instance. + * + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public IteratorOptions setOmitQualifiers(boolean value) + { + setOption(OMIT_QUALIFIERS, value); + return this; + } + + + /** + * @see Options#defineOptionName(int) + */ + protected String defineOptionName(int option) + { + switch (option) + { + case JUST_CHILDREN : return "JUST_CHILDREN"; + case JUST_LEAFNODES : return "JUST_LEAFNODES"; + case JUST_LEAFNAME : return "JUST_LEAFNAME"; + case OMIT_QUALIFIERS : return "OMIT_QUALIFIERS"; + default: return null; + } + } + + + /** + * @see Options#getValidOptions() + */ + protected int getValidOptions() + { + return + JUST_CHILDREN | + JUST_LEAFNODES | + JUST_LEAFNAME | + OMIT_QUALIFIERS; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/options/Options.java b/XMPCore/src/com/adobe/xmp/options/Options.java new file mode 100644 index 0000000..6d6bd98 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/Options.java @@ -0,0 +1,290 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.options; + +import java.util.HashMap; +import java.util.Map; + +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; + +/** + * The base class for a collection of 32 flag bits. Individual flags are defined as enum value bit + * masks. Inheriting classes add convenience accessor methods. + * + * @since 24.01.2006 + */ +public abstract class Options +{ + /** the internal int containing all options */ + private int options = 0; + /** a map containing the bit names */ + private Map optionNames = null; + + + /** + * The default constructor. + */ + public Options() + { + // EMTPY + } + + + /** + * Constructor with the options bit mask. + * + * @param options the options bit mask + * @throws XMPException If the options are not correct + */ + public Options(int options) throws XMPException + { + assertOptionsValid(options); + setOptions(options); + } + + + /** + * Resets the options. + */ + public void clear() + { + options = 0; + } + + + /** + * @param optionBits an option bitmask + * @return Returns true, if this object is equal to the given options. + */ + public boolean isExactly(int optionBits) + { + return getOptions() == optionBits; + } + + + /** + * @param optionBits an option bitmask + * @return Returns true, if this object contains all given options. + */ + public boolean containsAllOptions(int optionBits) + { + return (getOptions() & optionBits) == optionBits; + } + + + /** + * @param optionBits an option bitmask + * @return Returns true, if this object contain at least one of the given options. + */ + public boolean containsOneOf(int optionBits) + { + return ((getOptions()) & optionBits) != 0; + } + + + /** + * @param optionBit the binary bit or bits that are requested + * @return Returns if <emp>all</emp> of the requested bits are set or not. + */ + protected boolean getOption(int optionBit) + { + return (options & optionBit) != 0; + } + + + /** + * @param optionBits the binary bit or bits that shall be set to the given value + * @param value the boolean value to set + */ + public void setOption(int optionBits, boolean value) + { + options = value ? options | optionBits : options & ~optionBits; + } + + + /** + * Is friendly to access it during the tests. + * @return Returns the options. + */ + public int getOptions() + { + return options; + } + + + /** + * @param options The options to set. + * @throws XMPException + */ + public void setOptions(int options) throws XMPException + { + assertOptionsValid(options); + this.options = options; + } + + + /** + * @see Object#equals(Object) + */ + public boolean equals(Object obj) + { + return getOptions() == ((Options) obj).getOptions(); + } + + + /** + * @see java.lang.Object#hashCode() + */ + public int hashCode() + { + return getOptions(); + } + + + /** + * Creates a human readable string from the set options. <em>Note:</em> This method is quite + * expensive and should only be used within tests or as + * @return Returns a String listing all options that are set to <code>true</code> by their name, + * like "option1 | option4". + */ + public String getOptionsString() + { + if (options != 0) + { + StringBuffer sb = new StringBuffer(); + int theBits = options; + while (theBits != 0) + { + int oneLessBit = theBits & (theBits - 1); // clear rightmost one bit + int singleBit = theBits ^ oneLessBit; + String bitName = getOptionName(singleBit); + sb.append(bitName); + if (oneLessBit != 0) + { + sb.append(" | "); + } + theBits = oneLessBit; + } + return sb.toString(); + } + else + { + return "<none>"; + } + } + + + /** + * @return Returns the options as hex bitmask. + */ + public String toString() + { + return "0x" + Integer.toHexString(options); + } + + + /** + * To be implemeted by inheritants. + * @return Returns a bit mask where all valid option bits are set. + */ + protected abstract int getValidOptions(); + + + /** + * To be implemeted by inheritants. + * @param option a single, valid option bit. + * @return Returns a human readable name for an option bit. + */ + protected abstract String defineOptionName(int option); + + + /** + * The inheriting option class can do additional checks on the options. + * <em>Note:</em> For performance reasons this method is only called + * when setting bitmasks directly. + * When get- and set-methods are used, this method must be called manually, + * normally only when the Options-object has been created from a client + * (it has to be made public therefore). + * + * @param options the bitmask to check. + * @throws XMPException Thrown if the options are not consistent. + */ + protected void assertConsistency(int options) throws XMPException + { + // empty, no checks + } + + + /** + * Checks options before they are set. + * First it is checked if only defined options are used, + * second the additional {@link Options#assertConsistency(int)}-method is called. + * + * @param options the options to check + * @throws XMPException Thrown if the options are invalid. + */ + private void assertOptionsValid(int options) throws XMPException + { + int invalidOptions = options & ~getValidOptions(); + if (invalidOptions == 0) + { + assertConsistency(options); + } + else + { + throw new XMPException("The option bit(s) 0x" + Integer.toHexString(invalidOptions) + + " are invalid!", XMPError.BADOPTIONS); + } + } + + + + /** + * Looks up or asks the inherited class for the name of an option bit. + * Its save that there is only one valid option handed into the method. + * @param option a single option bit + * @return Returns the option name or undefined. + */ + private String getOptionName(int option) + { + Map optionsNames = procureOptionNames(); + + Integer key = new Integer(option); + String result = (String) optionsNames.get(key); + if (result == null) + { + result = defineOptionName(option); + if (result != null) + { + optionsNames.put(key, result); + } + else + { + result = "<option name not defined>"; + } + } + + return result; + } + + + /** + * @return Returns the optionNames map and creates it if required. + */ + private Map procureOptionNames() + { + if (optionNames == null) + { + optionNames = new HashMap(); + } + return optionNames; + } +} diff --git a/XMPCore/src/com/adobe/xmp/options/ParseOptions.java b/XMPCore/src/com/adobe/xmp/options/ParseOptions.java new file mode 100644 index 0000000..c05f871 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/ParseOptions.java @@ -0,0 +1,174 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.options; + +import java.io.InputStream; + +import com.adobe.xmp.XMPMetaFactory; + + +/** + * Options for {@link XMPMetaFactory#parse(InputStream, ParseOptions)}. + * + * @since 24.01.2006 + */ +public final class ParseOptions extends Options +{ + /** Require a surrounding "x:xmpmeta" element in the xml-document. */ + public static final int REQUIRE_XMP_META = 0x0001; + /** Do not reconcile alias differences, throw an exception instead. */ + public static final int STRICT_ALIASING = 0x0004; + /** Convert ASCII control characters 0x01 - 0x1F (except tab, cr, and lf) to spaces. */ + public static final int FIX_CONTROL_CHARS = 0x0008; + /** If the input is not unicode, try to parse it as ISO-8859-1. */ + public static final int ACCEPT_LATIN_1 = 0x0010; + /** Do not carry run the XMPNormalizer on a packet, leave it as it is. */ + public static final int OMIT_NORMALIZATION = 0x0020; + + + /** + * Sets the options to the default values. + */ + public ParseOptions() + { + setOption(FIX_CONTROL_CHARS | ACCEPT_LATIN_1, true); + } + + + /** + * @return Returns the requireXMPMeta. + */ + public boolean getRequireXMPMeta() + { + return getOption(REQUIRE_XMP_META); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public ParseOptions setRequireXMPMeta(boolean value) + { + setOption(REQUIRE_XMP_META, value); + return this; + } + + + /** + * @return Returns the strictAliasing. + */ + public boolean getStrictAliasing() + { + return getOption(STRICT_ALIASING); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public ParseOptions setStrictAliasing(boolean value) + { + setOption(STRICT_ALIASING, value); + return this; + } + + + /** + * @return Returns the strictAliasing. + */ + public boolean getFixControlChars() + { + return getOption(FIX_CONTROL_CHARS); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public ParseOptions setFixControlChars(boolean value) + { + setOption(FIX_CONTROL_CHARS, value); + return this; + } + + + /** + * @return Returns the strictAliasing. + */ + public boolean getAcceptLatin1() + { + return getOption(ACCEPT_LATIN_1); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public ParseOptions setOmitNormalization(boolean value) + { + setOption(OMIT_NORMALIZATION, value); + return this; + } + + + /** + * @return Returns the option "omit normalization". + */ + public boolean getOmitNormalization() + { + return getOption(OMIT_NORMALIZATION); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public ParseOptions setAcceptLatin1(boolean value) + { + setOption(ACCEPT_LATIN_1, value); + return this; + } + + + /** + * @see Options#defineOptionName(int) + */ + protected String defineOptionName(int option) + { + switch (option) + { + case REQUIRE_XMP_META : return "REQUIRE_XMP_META"; + case STRICT_ALIASING : return "STRICT_ALIASING"; + case FIX_CONTROL_CHARS: return "FIX_CONTROL_CHARS"; + case ACCEPT_LATIN_1: return "ACCEPT_LATIN_1"; + case OMIT_NORMALIZATION: return "OMIT_NORMALIZATION"; + default: return null; + } + } + + + /** + * @see Options#getValidOptions() + */ + protected int getValidOptions() + { + return + REQUIRE_XMP_META | + STRICT_ALIASING | + FIX_CONTROL_CHARS | + ACCEPT_LATIN_1 | + OMIT_NORMALIZATION; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java b/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java new file mode 100644 index 0000000..ae280fe --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java @@ -0,0 +1,429 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.options; + +import com.adobe.xmp.XMPError; +import com.adobe.xmp.XMPException; + + +/** + * The property flags are used when properties are fetched from the <code>XMPMeta</code>-object + * and provide more detailed information about the property. + * + * @since 03.07.2006 + */ +public final class PropertyOptions extends Options +{ + /** */ + public static final int NO_OPTIONS = 0x00000000; + /** */ + public static final int URI = 0x00000002; + /** */ + public static final int HAS_QUALIFIERS = 0x00000010; + /** */ + public static final int QUALIFIER = 0x00000020; + /** */ + public static final int HAS_LANGUAGE = 0x00000040; + /** */ + public static final int HAS_TYPE = 0x00000080; + /** */ + public static final int STRUCT = 0x00000100; + /** */ + public static final int ARRAY = 0x00000200; + /** */ + public static final int ARRAY_ORDERED = 0x00000400; + /** */ + public static final int ARRAY_ALTERNATE = 0x00000800; + /** */ + public static final int ARRAY_ALT_TEXT = 0x00001000; + /** */ + public static final int SCHEMA_NODE = 0x80000000; + /** may be used in the future */ + public static final int DELETE_EXISTING = 0x20000000; + + + /** + * Default constructor + */ + public PropertyOptions() + { + // reveal default constructor + } + + + /** + * Intialization constructor + * + * @param options the initialization options + * @throws XMPException If the options are not valid + */ + public PropertyOptions(int options) throws XMPException + { + super(options); + } + + + /** + * @return Return whether the property value is a URI. It is serialized to RDF using the + * <tt>rdf:resource</tt> attribute. Not mandatory for URIs, but considered RDF-savvy. + */ + public boolean isURI() + { + return getOption(URI); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setURI(boolean value) + { + setOption(URI, value); + return this; + } + + + /** + * @return Return whether the property has qualifiers. These could be an <tt>xml:lang</tt> + * attribute, an <tt>rdf:type</tt> property, or a general qualifier. See the + * introductory discussion of qualified properties for more information. + */ + public boolean getHasQualifiers() + { + return getOption(HAS_QUALIFIERS); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setHasQualifiers(boolean value) + { + setOption(HAS_QUALIFIERS, value); + return this; + } + + + /** + * @return Return whether this property is a qualifier for some other property. Note that if the + * qualifier itself has a structured value, this flag is only set for the top node of + * the qualifier's subtree. Qualifiers may have arbitrary structure, and may even have + * qualifiers. + */ + public boolean isQualifier() + { + return getOption(QUALIFIER); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setQualifier(boolean value) + { + setOption(QUALIFIER, value); + return this; + } + + + /** @return Return whether this property has an <tt>xml:lang</tt> qualifier. */ + public boolean getHasLanguage() + { + return getOption(HAS_LANGUAGE); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setHasLanguage(boolean value) + { + setOption(HAS_LANGUAGE, value); + return this; + } + + + /** @return Return whether this property has an <tt>rdf:type</tt> qualifier. */ + public boolean getHasType() + { + return getOption(HAS_TYPE); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setHasType(boolean value) + { + setOption(HAS_TYPE, value); + return this; + } + + + /** @return Return whether this property contains nested fields. */ + public boolean isStruct() + { + return getOption(STRUCT); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setStruct(boolean value) + { + setOption(STRUCT, value); + return this; + } + + + /** + * @return Return whether this property is an array. By itself this indicates a general + * unordered array. It is serialized using an <tt>rdf:Bag</tt> container. + */ + public boolean isArray() + { + return getOption(ARRAY); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setArray(boolean value) + { + setOption(ARRAY, value); + return this; + } + + + /** + * @return Return whether this property is an ordered array. Appears in conjunction with + * getPropValueIsArray(). It is serialized using an <tt>rdf:Seq</tt> container. + */ + public boolean isArrayOrdered() + { + return getOption(ARRAY_ORDERED); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setArrayOrdered(boolean value) + { + setOption(ARRAY_ORDERED, value); + return this; + } + + + /** + * @return Return whether this property is an alternative array. Appears in conjunction with + * getPropValueIsArray(). It is serialized using an <tt>rdf:Alt</tt> container. + */ + public boolean isArrayAlternate() + { + return getOption(ARRAY_ALTERNATE); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setArrayAlternate(boolean value) + { + setOption(ARRAY_ALTERNATE, value); + return this; + } + + + /** + * @return Return whether this property is an alt-text array. Appears in conjunction with + * getPropArrayIsAlternate(). It is serialized using an <tt>rdf:Alt</tt> container. + * Each array element is a simple property with an <tt>xml:lang</tt> attribute. + */ + public boolean isArrayAltText() + { + return getOption(ARRAY_ALT_TEXT); + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setArrayAltText(boolean value) + { + setOption(ARRAY_ALT_TEXT, value); + return this; + } + + + /** + * @param value the value to set + * @return Returns this to enable cascaded options. + */ + + + /** + * @return Returns whether the SCHEMA_NODE option is set. + */ + public boolean isSchemaNode() + { + return getOption(SCHEMA_NODE); + } + + + /** + * @param value the option DELETE_EXISTING to set + * @return Returns this to enable cascaded options. + */ + public PropertyOptions setSchemaNode(boolean value) + { + setOption(SCHEMA_NODE, value); + return this; + } + + + //-------------------------------------------------------------------------- convenience methods + + /** + * @return Returns whether the property is of composite type - an array or a struct. + */ + public boolean isCompositeProperty() + { + return (getOptions() & (ARRAY | STRUCT)) > 0; + } + + + /** + * @return Returns whether the property is of composite type - an array or a struct. + */ + public boolean isSimple() + { + return (getOptions() & (ARRAY | STRUCT)) == 0; + } + + + /** + * Compares two options set for array compatibility. + * + * @param options other options + * @return Returns true if the array options of the sets are equal. + */ + public boolean equalArrayTypes(PropertyOptions options) + { + return + isArray() == options.isArray() && + isArrayOrdered() == options.isArrayOrdered() && + isArrayAlternate() == options.isArrayAlternate() && + isArrayAltText() == options.isArrayAltText(); + } + + + + /** + * Merges the set options of a another options object with this. + * If the other options set is null, this objects stays the same. + * @param options other options + * @throws XMPException If illegal options are provided + */ + public void mergeWith(PropertyOptions options) throws XMPException + { + if (options != null) + { + setOptions(getOptions() | options.getOptions()); + } + } + + + /** + * @return Returns true if only array options are set. + */ + public boolean isOnlyArrayOptions() + { + return (getOptions() & + ~(ARRAY | ARRAY_ORDERED | ARRAY_ALTERNATE | ARRAY_ALT_TEXT)) == 0; + } + + + /** + * @see Options#getValidOptions() + */ + protected int getValidOptions() + { + return + URI | + HAS_QUALIFIERS | + QUALIFIER | + HAS_LANGUAGE | + HAS_TYPE | + STRUCT | + ARRAY | + ARRAY_ORDERED | + ARRAY_ALTERNATE | + ARRAY_ALT_TEXT | + SCHEMA_NODE; + } + + + /** + * @see Options#defineOptionName(int) + */ + protected String defineOptionName(int option) + { + switch (option) + { + case URI : return "URI"; + case HAS_QUALIFIERS : return "HAS_QUALIFIER"; + case QUALIFIER : return "QUALIFIER"; + case HAS_LANGUAGE : return "HAS_LANGUAGE"; + case HAS_TYPE: return "HAS_TYPE"; + case STRUCT : return "STRUCT"; + case ARRAY : return "ARRAY"; + case ARRAY_ORDERED : return "ARRAY_ORDERED"; + case ARRAY_ALTERNATE : return "ARRAY_ALTERNATE"; + case ARRAY_ALT_TEXT : return "ARRAY_ALT_TEXT"; + case SCHEMA_NODE : return "SCHEMA_NODE"; + default: return null; + } + } + + + /** + * Checks that a node not a struct and array at the same time; + * and URI cannot be a struct. + * + * @param options the bitmask to check. + * @throws XMPException Thrown if the options are not consistent. + */ + public void assertConsistency(int options) throws XMPException + { + if ((options & STRUCT) > 0 && (options & ARRAY) > 0) + { + throw new XMPException("IsStruct and IsArray options are mutually exclusive", + XMPError.BADOPTIONS); + } + else if ((options & URI) > 0 && (options & (ARRAY | STRUCT)) > 0) + { + throw new XMPException("Structs and arrays can't have \"value\" options", + XMPError.BADOPTIONS); + } + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java b/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java new file mode 100644 index 0000000..37ed7bd --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java @@ -0,0 +1,436 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.options; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; + + +/** + * Options for {@link XMPMetaFactory#serializeToBuffer(XMPMeta, SerializeOptions)}. + * + * @since 24.01.2006 + */ +public final class SerializeOptions extends Options +{ + /** Omit the XML packet wrapper. */ + public static final int OMIT_PACKET_WRAPPER = 0x0010; + /** Mark packet as read-only. Default is a writeable packet. */ + public static final int READONLY_PACKET = 0x0020; + /** Use a compact form of RDF. */ + public static final int USE_COMPACT_FORMAT = 0x0040; + /** + * Include a padding allowance for a thumbnail image. If no <tt>xmp:Thumbnails</tt> property + * is present, the typical space for a JPEG thumbnail is used. + */ + public static final int INCLUDE_THUMBNAIL_PAD = 0x0100; + /** + * The padding parameter provides the overall packet length. The actual amount of padding is + * computed. An exception is thrown if the packet exceeds this length with no padding. + */ + public static final int EXACT_PACKET_LENGTH = 0x0200; + /** Sort the struct properties and qualifier before serializing */ + public static final int SORT = 0x1000; + + // --------------------------------------------------------------------------------------------- + // encoding bit constants + + /** Bit indicating little endian encoding, unset is big endian */ + private static final int LITTLEENDIAN_BIT = 0x0001; + /** Bit indication UTF16 encoding. */ + private static final int UTF16_BIT = 0x0002; + /** UTF8 encoding; this is the default */ + public static final int ENCODE_UTF8 = 0; + /** UTF16BE encoding */ + public static final int ENCODE_UTF16BE = UTF16_BIT; + /** UTF16LE encoding */ + public static final int ENCODE_UTF16LE = UTF16_BIT | LITTLEENDIAN_BIT; + /** */ + private static final int ENCODING_MASK = UTF16_BIT | LITTLEENDIAN_BIT; + + /** + * The amount of padding to be added if a writeable XML packet is created. If zero is passed + * (the default) an appropriate amount of padding is computed. + */ + private int padding = 2048; + /** + * The string to be used as a line terminator. If empty it defaults to; linefeed, U+000A, the + * standard XML newline. + */ + private String newline = "\n"; + /** + * The string to be used for each level of indentation in the serialized + * RDF. If empty it defaults to two ASCII spaces, U+0020. + */ + private String indent = " "; + /** + * The number of levels of indentation to be used for the outermost XML element in the + * serialized RDF. This is convenient when embedding the RDF in other text, defaults to 0. + */ + private int baseIndent = 0; + /** Omits the Toolkit version attribute, not published, only used for Unit tests. */ + private boolean omitVersionAttribute = false; + + + /** + * Default constructor. + */ + public SerializeOptions() + { + // reveal default constructor + } + + + /** + * Constructor using inital options + * @param options the inital options + * @throws XMPException Thrown if options are not consistant. + */ + public SerializeOptions(int options) throws XMPException + { + super(options); + } + + + /** + * @return Returns the option. + */ + public boolean getOmitPacketWrapper() + { + return getOption(OMIT_PACKET_WRAPPER); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setOmitPacketWrapper(boolean value) + { + setOption(OMIT_PACKET_WRAPPER, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean getReadOnlyPacket() + { + return getOption(READONLY_PACKET); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setReadOnlyPacket(boolean value) + { + setOption(READONLY_PACKET, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean getUseCompactFormat() + { + return getOption(USE_COMPACT_FORMAT); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setUseCompactFormat(boolean value) + { + setOption(USE_COMPACT_FORMAT, value); + return this; + } + + /** + * @return Returns the option. + */ + public boolean getIncludeThumbnailPad() + { + return getOption(INCLUDE_THUMBNAIL_PAD); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setIncludeThumbnailPad(boolean value) + { + setOption(INCLUDE_THUMBNAIL_PAD, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean getExactPacketLength() + { + return getOption(EXACT_PACKET_LENGTH); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setExactPacketLength(boolean value) + { + setOption(EXACT_PACKET_LENGTH, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean getSort() + { + return getOption(SORT); + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setSort(boolean value) + { + setOption(SORT, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean getEncodeUTF16BE() + { + return (getOptions() & ENCODING_MASK) == ENCODE_UTF16BE; + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setEncodeUTF16BE(boolean value) + { + // clear unicode bits + setOption(UTF16_BIT | LITTLEENDIAN_BIT, false); + setOption(ENCODE_UTF16BE, value); + return this; + } + + + /** + * @return Returns the option. + */ + public boolean getEncodeUTF16LE() + { + return (getOptions() & ENCODING_MASK) == ENCODE_UTF16LE; + } + + + /** + * @param value the value to set + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setEncodeUTF16LE(boolean value) + { + // clear unicode bits + setOption(UTF16_BIT | LITTLEENDIAN_BIT, false); + setOption(ENCODE_UTF16LE, value); + return this; + } + + + /** + * @return Returns the baseIndent. + */ + public int getBaseIndent() + { + return baseIndent; + } + + + /** + * @param baseIndent + * The baseIndent to set. + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setBaseIndent(int baseIndent) + { + this.baseIndent = baseIndent; + return this; + } + + + /** + * @return Returns the indent. + */ + public String getIndent() + { + return indent; + } + + + /** + * @param indent + * The indent to set. + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setIndent(String indent) + { + this.indent = indent; + return this; + } + + + /** + * @return Returns the newline. + */ + public String getNewline() + { + return newline; + } + + + /** + * @param newline + * The newline to set. + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setNewline(String newline) + { + this.newline = newline; + return this; + } + + + /** + * @return Returns the padding. + */ + public int getPadding() + { + return padding; + } + + + /** + * @param padding + * The padding to set. + * @return Returns the instance to call more set-methods. + */ + public SerializeOptions setPadding(int padding) + { + this.padding = padding; + return this; + } + + + /** + * @return Returns whether the Toolkit version attribute shall be omitted. + * <em>Note:</em> This options can only be set by unit tests. + */ + public boolean getOmitVersionAttribute() + { + return omitVersionAttribute; + } + + + /** + * @return Returns the encoding as Java encoding String. + */ + public String getEncoding() + { + if (getEncodeUTF16BE()) + { + return "UTF-16BE"; + } + else if (getEncodeUTF16LE()) + { + return "UTF-16LE"; + } + else + { + return "UTF-8"; + } + } + + + /** + * + * @return Returns clone of this SerializeOptions-object with the same options set. + * @throws CloneNotSupportedException Cannot happen in this place. + */ + public Object clone() throws CloneNotSupportedException + { + SerializeOptions clone; + try + { + clone = new SerializeOptions(getOptions()); + clone.setBaseIndent(baseIndent); + clone.setIndent(indent); + clone.setNewline(newline); + clone.setPadding(padding); + return clone; + } + catch (XMPException e) + { + // This cannot happen, the options are already checked in "this" object. + return null; + } + } + + + /** + * @see Options#defineOptionName(int) + */ + protected String defineOptionName(int option) + { + switch (option) + { + case OMIT_PACKET_WRAPPER : return "OMIT_PACKET_WRAPPER"; + case READONLY_PACKET : return "READONLY_PACKET"; + case USE_COMPACT_FORMAT : return "USE_COMPACT_FORMAT"; + case INCLUDE_THUMBNAIL_PAD : return "INCLUDE_THUMBNAIL_PAD"; + case EXACT_PACKET_LENGTH : return "EXACT_PACKET_LENGTH"; + case SORT : return "NORMALIZED"; + default: return null; + } + } + + + /** + * @see Options#getValidOptions() + */ + protected int getValidOptions() + { + return + OMIT_PACKET_WRAPPER | + READONLY_PACKET | + USE_COMPACT_FORMAT | + INCLUDE_THUMBNAIL_PAD | + EXACT_PACKET_LENGTH | + SORT; + } +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/options/package.html b/XMPCore/src/com/adobe/xmp/options/package.html new file mode 100644 index 0000000..d2e56d0 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/options/package.html @@ -0,0 +1,20 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> + <title>Package overview</title> +</head> + +<body> + <p>Package containing the option classes.</p> + <p>These are used to configure diverse function calls of xmpcore:<p> + <ul> + <li>PropertyOptions - these are used to create properties and also to retrieve information about simple, array or struct properties, as well as qualifiers + <li>ParseOptions - used to configure the parsing of xmp metadata packets + <li>SerializationOptions - used to control the serialization of xmp metadata packets + <li>AliasOptions - used by XMPSchemaRegistry#registerAlias() + <li>IteratorOptions - used to set up an XMPIterator + <li>Options - the base class of all option classes + </ul> +</body> +</html> diff --git a/XMPCore/src/com/adobe/xmp/package.html b/XMPCore/src/com/adobe/xmp/package.html new file mode 100644 index 0000000..8afb896 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/package.html @@ -0,0 +1,11 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> + <title>Package overview</title> +</head> + +<body> + <p>Package containing the xmpcore interface.</p> +</body> +</html> diff --git a/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java b/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java new file mode 100644 index 0000000..e9078d4 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java @@ -0,0 +1,48 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.properties; + +import com.adobe.xmp.options.AliasOptions; + + +/** + * This interface is used to return info about an alias. + * + * @since 27.01.2006 + */ +public interface XMPAliasInfo +{ + /** + * @return Returns Returns the namespace URI for the base property. + */ + String getNamespace(); + + + /** + * @return Returns the default prefix for the given base property. + */ + String getPrefix(); + + + /** + * @return Returns the path of the base property. + */ + String getPropName(); + + + /** + * @return Returns the kind of the alias. This can be a direct alias + * (ARRAY), a simple property to an ordered array + * (ARRAY_ORDERED), to an alternate array + * (ARRAY_ALTERNATE) or to an alternate text array + * (ARRAY_ALT_TEXT). + */ + AliasOptions getAliasForm(); +}
\ No newline at end of file diff --git a/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java b/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java new file mode 100644 index 0000000..767c94b --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java @@ -0,0 +1,40 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.properties; + +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.options.PropertyOptions; + + +/** + * This interface is used to return a text property together with its and options. + * + * @since 23.01.2006 + */ +public interface XMPProperty +{ + /** + * @return Returns the value of the property. + */ + Object getValue(); + + + /** + * @return Returns the options of the property. + */ + PropertyOptions getOptions(); + + + /** + * Only set by {@link XMPMeta#getLocalizedText(String, String, String, String)}. + * @return Returns the language of the alt-text item. + */ + String getLanguage(); +} diff --git a/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java b/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java new file mode 100644 index 0000000..8a9c519 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java @@ -0,0 +1,45 @@ +// ================================================================================================= +// ADOBE SYSTEMS INCORPORATED +// Copyright 2006 Adobe Systems Incorporated +// All Rights Reserved +// +// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms +// of the Adobe license agreement accompanying it. +// ================================================================================================= + +package com.adobe.xmp.properties; + +import com.adobe.xmp.options.PropertyOptions; + + +/** + * This interface is used to return a property together with its path and namespace. + * It is returned when properties are iterated with the <code>XMPIterator</code>. + * + * @since 06.07.2006 + */ +public interface XMPPropertyInfo extends XMPProperty +{ + /** + * @return Returns the namespace of the property + */ + String getNamespace(); + + + /** + * @return Returns the path of the property, but only if returned by the iterator. + */ + String getPath(); + + + /** + * @return Returns the value of the property. + */ + Object getValue(); + + + /** + * @return Returns the options of the property. + */ + PropertyOptions getOptions(); +} diff --git a/XMPCore/src/com/adobe/xmp/properties/package.html b/XMPCore/src/com/adobe/xmp/properties/package.html new file mode 100644 index 0000000..19e7427 --- /dev/null +++ b/XMPCore/src/com/adobe/xmp/properties/package.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> + <title>Package overview</title> +</head> + +<body> + <p>Package containing the property information classes.</p> + <p>XMPProperty and XMPPropertyInfo are used to report properties when they are retrieved by get-methods or by the iterator. + XMPAliasInfo informs about a certain property-to-property alias.<p> +</body> +</html> |