summaryrefslogtreecommitdiff
path: root/java/XMPCore/src/com/adobe
diff options
context:
space:
mode:
Diffstat (limited to 'java/XMPCore/src/com/adobe')
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPConst.java159
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPDateTime.java98
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java142
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPError.java44
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPException.java55
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPIterator.java82
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPMeta.java1152
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java327
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPPathFactory.java286
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java235
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPUtils.java506
-rw-r--r--java/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java45
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/Base64.java251
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java326
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java79
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java214
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java503
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java197
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java153
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java1349
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/QName.java80
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/Utils.java511
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java303
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java598
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java1389
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java361
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPNode.java921
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java930
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java696
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java467
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java102
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java1295
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java1167
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/package.html11
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java106
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java529
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java147
-rw-r--r--java/XMPCore/src/com/adobe/xmp/impl/xpath/package.html12
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/AliasOptions.java186
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java148
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/Options.java290
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/ParseOptions.java150
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java453
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java461
-rw-r--r--java/XMPCore/src/com/adobe/xmp/options/package.html20
-rw-r--r--java/XMPCore/src/com/adobe/xmp/package.html11
-rw-r--r--java/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java39
-rw-r--r--java/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java40
-rw-r--r--java/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java45
-rw-r--r--java/XMPCore/src/com/adobe/xmp/properties/package.html13
-rw-r--r--java/XMPCore/src/com/adobe/xmp/version.properties15
51 files changed, 17699 insertions, 0 deletions
diff --git a/java/XMPCore/src/com/adobe/xmp/XMPConst.java b/java/XMPCore/src/com/adobe/xmp/XMPConst.java
new file mode 100644
index 0000000..8180633
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPConst.java
@@ -0,0 +1,159 @@
+// =================================================================================================
+// 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;
+
+
+/**
+ * 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_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/";
+
+
+ // 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";
+
+ /** legaciy dublin core NS, will be converted to NS_DC */
+ String NS_DC_DEPRECATED = "http://purl.org/dc/1.1/";
+} \ No newline at end of file
diff --git a/java/XMPCore/src/com/adobe/xmp/XMPDateTime.java b/java/XMPCore/src/com/adobe/xmp/XMPDateTime.java
new file mode 100644
index 0000000..ae3a0fc
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPDateTime.java
@@ -0,0 +1,98 @@
+// =================================================================================================
+// 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.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 calendar (only with milli second precision). */
+ 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/java/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java b/java/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java
new file mode 100644
index 0000000..05b6f0f
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPDateTimeFactory.java
@@ -0,0 +1,142 @@
+// =================================================================================================
+// 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.util.Calendar;
+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
+{
+ /** 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
+ * @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)
+ {
+ Calendar cal = dateTime.getCalendar();
+ cal.setTimeZone(TimeZone.getTimeZone("UTC"));
+ 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)
+ {
+ Calendar cal = dateTime.getCalendar();
+ cal.setTimeZone(TimeZone.getDefault());
+ return new XMPDateTimeImpl(cal);
+ }
+} \ No newline at end of file
diff --git a/java/XMPCore/src/com/adobe/xmp/XMPError.java b/java/XMPCore/src/com/adobe/xmp/XMPError.java
new file mode 100644
index 0000000..ebf040d
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPError.java
@@ -0,0 +1,44 @@
+// =================================================================================================
+// 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;
+
+
+/**
+ * @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/java/XMPCore/src/com/adobe/xmp/XMPException.java b/java/XMPCore/src/com/adobe/xmp/XMPException.java
new file mode 100644
index 0000000..0210143
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPException.java
@@ -0,0 +1,55 @@
+// =================================================================================================
+// 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;
+
+/**
+ * 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/java/XMPCore/src/com/adobe/xmp/XMPIterator.java b/java/XMPCore/src/com/adobe/xmp/XMPIterator.java
new file mode 100644
index 0000000..b70e0a5
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPIterator.java
@@ -0,0 +1,82 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/XMPMeta.java b/java/XMPCore/src/com/adobe/xmp/XMPMeta.java
new file mode 100644
index 0000000..085915f
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPMeta.java
@@ -0,0 +1,1152 @@
+// =================================================================================================
+// 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.util.Calendar;
+
+import com.adobe.xmp.options.IteratorOptions;
+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 &quot;schema&quot; 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);
+
+
+ /**
+ * 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();
+
+
+ /**
+ * 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/java/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java b/java/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java
new file mode 100644
index 0000000..4b218ec
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPMetaFactory.java
@@ -0,0 +1,327 @@
+//=================================================================================================
+//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 java.util.Properties;
+
+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 &lt;x:xmpmeta&gt; XML element is required around
+ * <tt>&lt;rdf:RDF&gt;</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.
+ *
+ * @return Returns the version information.
+ */
+ public static XMPVersionInfo getVersionInfo()
+ {
+ if (versionInfo == null)
+ {
+ try
+ {
+ Properties versProperties = new Properties();
+ versProperties.load(XMPMetaFactory.class.getResourceAsStream("version.properties"));
+
+ final int major = Integer.parseInt(versProperties
+ .getProperty("implementation.version.major"));
+ final int minor = Integer.parseInt(versProperties
+ .getProperty("implementation.version.minor"));
+ final int micro = Integer.parseInt(versProperties
+ .getProperty("implementation.version.micro"));
+ final boolean debug = Boolean.valueOf(
+ versProperties.getProperty("implementation.version.debug")).booleanValue();
+ final String message;
+ final int engBuild;
+
+ message = versProperties.getProperty("implementation.version");
+ engBuild = Integer.parseInt(versProperties
+ .getProperty("implementation.version.engbuild"));
+
+ 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
+ }
+ }
+ return versionInfo;
+ }
+}
diff --git a/java/XMPCore/src/com/adobe/xmp/XMPPathFactory.java b/java/XMPCore/src/com/adobe/xmp/XMPPathFactory.java
new file mode 100644
index 0000000..e8fa6d2
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPPathFactory.java
@@ -0,0 +1,286 @@
+// =================================================================================================
+// 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 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, &quot;Struct&quot;, fieldNS,
+ * &quot;Array&quot;);
+ * String path += XMPPathFactory.composeArrayItemPath (schemaNS, &quot;Array&quot; 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, &quot;Struct&quot;, fieldNS,
+ * &quot;Array&quot;);
+ * 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 &quot;schema&quot; 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 &quot;ns&quot; is the prefix for schemaNS and
+ * &quot;i&quot; is the decimal representation of itemIndex.
+ */
+ public static String composeArrayItemPath(String arrayName, int itemIndex)
+ {
+ if (itemIndex > 0)
+ {
+ return arrayName + '[' + itemIndex + ']';
+ }
+ else
+ {
+ return arrayName + "[last()]";
+ }
+ }
+
+
+ /**
+ * 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 &quot;ns&quot; is the prefix for
+ * schemaNS and &quot;fNS&quot; 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 &quot;ns&quot; is the prefix for
+ * schemaNS and &quot;qNS&quot; 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 &quot;content addressing&quot; 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
+ * &quot;ns&quot; 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 &quot;content addressing&quot; 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, &quot;Downloads&quot;, fieldNS,
+ * &quot;City&quot;, chosenCity );
+ * XMPProperty prop = xmpObj.getStructField ( schemaNS, path, fieldNS, &quot;URI&quot; );
+ * </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 &quot;ns&quot; is the
+ * prefix for schemaNS and &quot;fNS&quot; 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/java/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java b/java/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java
new file mode 100644
index 0000000..a523d93
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPSchemaRegistry.java
@@ -0,0 +1,235 @@
+// =================================================================================================
+// 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.util.Map;
+
+import com.adobe.xmp.options.AliasOptions;
+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
+
+ /**
+ * 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.
+ *
+ * @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.
+ */
+ void registerAlias(String aliasNS, String aliasProp, String actualNS, String actualProp,
+ AliasOptions aliasForm) throws XMPException;
+
+
+ /**
+ * 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);
+
+
+ /**
+ * Delete an alias.
+ * <p>
+ * This only deletes the registration of the alias, it does not delete the
+ * actual property. It does delete any view of the property through the
+ * alias name. It is OK to attempt to delete an alias that does not exist,
+ * that is if the alias name is not registered as an alias.
+ *
+ * @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.
+ */
+ void deleteAlias(String aliasNS, String aliasProp);
+
+
+ /**
+ * @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/java/XMPCore/src/com/adobe/xmp/XMPUtils.java b/java/XMPCore/src/com/adobe/xmp/XMPUtils.java
new file mode 100644
index 0000000..b54aebb
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPUtils.java
@@ -0,0 +1,506 @@
+// =================================================================================================
+// 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 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 &quot;; &quot;, 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 &apos;&quot;&apos;
+ * @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>&quot;t&quot; and &quot;f&quot;
+ * <li>&quot;on&quot; and &quot;off&quot;
+ * <li>&quot;yes&quot; and &quot;no&quot;
+ * <li>&quot;value <> 0&quot; and &quot;value == 0&quot;
+ * </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/java/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java b/java/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java
new file mode 100644
index 0000000..ac8fa32
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/XMPVersionInfo.java
@@ -0,0 +1,45 @@
+// =================================================================================================
+// 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;
+
+/**
+ * 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 true if this is a debug build. */
+ boolean isDebug();
+
+
+ /** @return Returns a rolling build number, monotonically increasing in a release. */
+ int getBuild();
+
+
+ /** @return Returns a comprehensive version information string. */
+ String getMessage();
+} \ No newline at end of file
diff --git a/java/XMPCore/src/com/adobe/xmp/impl/Base64.java b/java/XMPCore/src/com/adobe/xmp/impl/Base64.java
new file mode 100644
index 0000000..c47d08c
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/Base64.java
@@ -0,0 +1,251 @@
+// =================================================================================================
+// ADOBE SYSTEMS INCORPORATED
+// Copyright 2001-2005 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/java/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java b/java/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java
new file mode 100644
index 0000000..c8ef03f
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/ByteBuffer.java
@@ -0,0 +1,326 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java b/java/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java
new file mode 100644
index 0000000..c70b653
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/CountOutputStream.java
@@ -0,0 +1,79 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java b/java/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java
new file mode 100644
index 0000000..d248e0d
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/FixASCIIControlsReader.java
@@ -0,0 +1,214 @@
+// =================================================================================================
+// 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.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 (&amp;#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/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java b/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java
new file mode 100644
index 0000000..2d1939d
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/ISO8601Converter.java
@@ -0,0 +1,503 @@
+// =================================================================================================
+// 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.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)
+ {
+ if (dateTime.getTimeZone().getRawOffset() == 0)
+ {
+ // UTC
+ buffer.append('Z');
+ }
+ else
+ {
+ int offset = dateTime.getTimeZone().getRawOffset();
+ 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/java/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java b/java/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java
new file mode 100644
index 0000000..a8707a5
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/Latin1Converter.java
@@ -0,0 +1,197 @@
+// =================================================================================================
+// 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.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 &apos;&lt;&apos;). 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/java/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java b/java/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java
new file mode 100644
index 0000000..51c156a
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/ParameterAsserts.java
@@ -0,0 +1,153 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java b/java/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java
new file mode 100644
index 0000000..1368a01
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/ParseRDF.java
@@ -0,0 +1,1349 @@
+// =================================================================================================
+// 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;
+ break;
+ }
+ 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);
+ boolean isCompact = "pxmp:compact".equals(name);
+
+ XMPNode newQual = null;
+
+ if (isCompact)
+ {
+ if (xmpParent.getOptions().isCompositeProperty())
+ {
+ throw new XMPException("Only structs and arrays can be compact", BADXMP);
+ }
+ xmpParent.getOptions().setCompact(true);
+ }
+ else
+ {
+ // 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);
+
+ if ("pxmp:compact".equals(qualifier.getName()))
+ {
+ // Compactness is an option bit, not an actual qualifier.
+ if (!xmpParent.getOptions().isCompositeProperty())
+ {
+ throw new XMPException("Only structs and arrays can be compact", BADXMP);
+ }
+ xmpParent.getOptions().setCompact(true);
+ }
+ else
+ {
+ 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/java/XMPCore/src/com/adobe/xmp/impl/QName.java b/java/XMPCore/src/com/adobe/xmp/impl/QName.java
new file mode 100644
index 0000000..0e9e868
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/QName.java
@@ -0,0 +1,80 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/Utils.java b/java/XMPCore/src/com/adobe/xmp/impl/Utils.java
new file mode 100644
index 0000000..009dbe7
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/Utils.java
@@ -0,0 +1,511 @@
+// =================================================================================================
+// 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.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 &quot;Internal
+ * Property&quot;, 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 ("xap:BaseURL".equals(prop) || "xap:CreatorTool".equals(prop)
+ || "xap:Format".equals(prop) || "xap:Locale".equals(prop)
+ || "xap:MetadataDate".equals(prop) || "xap: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>&amp;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: &apos; not serialized as only outer qoutes are used
+ case '<': buffer.append("&lt;"); continue;
+ case '>': buffer.append("&gt;"); continue;
+ case '&': buffer.append("&amp;"); continue;
+ case '"': buffer.append(forAttribute ? "&quot;" : "\""); 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/java/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java
new file mode 100644
index 0000000..e0d0573
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPDateTimeImpl.java
@@ -0,0 +1,303 @@
+// =================================================================================================
+// 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.impl;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+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)
+ {
+ year = calendar.get(Calendar.YEAR);
+ month = calendar.get(Calendar.MONTH) + 1; // cal is from 0..12
+ day = calendar.get(Calendar.DAY_OF_MONTH);
+ hour = calendar.get(Calendar.HOUR_OF_DAY);
+ minute = calendar.get(Calendar.MINUTE);
+ second = calendar.get(Calendar.SECOND);
+ nanoSeconds = calendar.get(Calendar.MILLISECOND) * 1000000;
+ timeZone = calendar.getTimeZone();
+ }
+
+
+ /**
+ * 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()
+ {
+ Calendar calendar = new GregorianCalendar(year, month - 1, day, hour, minute, second);
+ calendar.set(Calendar.MILLISECOND, nanoSeconds / 1000000);
+ calendar.setTimeZone(timeZone);
+ 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/java/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java
new file mode 100644
index 0000000..53ea83e
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPIteratorImpl.java
@@ -0,0 +1,598 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java
new file mode 100644
index 0000000..251154d
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPMetaImpl.java
@@ -0,0 +1,1389 @@
+// =================================================================================================
+// 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.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.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;
+
+
+ /**
+ * 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);
+ }
+
+
+ /**
+ * 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();
+ }
+
+
+ /**
+ * @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/java/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java
new file mode 100644
index 0000000..35d929e
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPMetaParser.java
@@ -0,0 +1,361 @@
+// =================================================================================================
+// 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.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.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.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 = findRootNode(document, xmpmetaRequired);
+
+ if (result != null && result[1] == XMP_RDF)
+ {
+ XMPMetaImpl xmp = ParseRDF.parse((Node) result[0]);
+ return XMPNormalizer.process(xmp, options);
+ }
+ 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)
+ {
+ 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
+ * @return Returns the rdf:RDF-node or <code>null</code>.
+ */
+ private static Object[] findRootNode(Node root, boolean xmpmetaRequired)
+ {
+ // 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.TEXT_NODE != root.getNodeType() &&
+ Node.PROCESSING_INSTRUCTION_NODE != root.getNodeType())
+ {
+ String rootNS = root.getNamespaceURI();
+ String rootLocal = root.getLocalName();
+ if (
+ ("xmpmeta".equals(rootLocal) || "xapmeta".equals(rootLocal)) &&
+ XMPConst.NS_X.equals(rootNS)
+ )
+ {
+ // by not passing the RequireXMPMeta-option, the rdf-Node will be valid
+ return findRootNode(root, false);
+ }
+ else if (!xmpmetaRequired &&
+ "RDF".equals(rootLocal) &&
+ XMPConst.NS_RDF.equals(rootNS))
+ {
+ return new Object[] {root, XMP_RDF};
+ }
+ else
+ {
+ // continue searching
+ Object[] result = findRootNode(root, xmpmetaRequired);
+ if (result != null)
+ {
+ return result;
+ }
+ 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);
+ factory.setExpandEntityReferences(true);
+ return factory;
+ }
+} \ No newline at end of file
diff --git a/java/XMPCore/src/com/adobe/xmp/impl/XMPNode.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPNode.java
new file mode 100644
index 0000000..3342514
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPNode.java
@@ -0,0 +1,921 @@
+//=================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java
new file mode 100644
index 0000000..283db03
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPNodeUtils.java
@@ -0,0 +1,930 @@
+// =================================================================================================
+// 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.impl;
+
+import java.util.Calendar;
+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 Calendar)
+ {
+ XMPDateTime dt = XMPDateTimeFactory.createFromCalendar((Calendar) 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 (index == arrayNode.getChildrenLength() + 1 && createNodes)
+ {
+ // Append a new last + 1 node.
+ XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, null);
+ newItem.setImplicit(true);
+ arrayNode.addChild(newItem);
+ }
+
+ // Don't throw here for a too large index. setProperty() will throw,
+ // getProperty() will not.
+ if (index > arrayNode.getChildrenLength())
+ {
+ index = -1;
+ }
+ 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 &quot;single value&quot;
+ * 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 &quot;x-default&quot; 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/java/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java
new file mode 100644
index 0000000..b812324
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPNormalizer.java
@@ -0,0 +1,696 @@
+// =================================================================================================
+// 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.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 &quot;uuid:bac965c4-9d87-11d9-9a30-000d936b79c4&quot;, plus InDesign
+ * 3.0 wrote them like &quot;bac965c4-9d87-11d9-9a30-000d936b79c4&quot;. 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, "xapRights: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/java/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java
new file mode 100644
index 0000000..c6135ca
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPSchemaRegistryImpl.java
@@ -0,0 +1,467 @@
+// =================================================================================================
+// 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.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, "xap");
+ registerNamespace(NS_XMP_RIGHTS, "xapRights");
+ registerNamespace(NS_XMP_MM, "xapMM");
+ registerNamespace(NS_XMP_BJ, "xapBJ");
+ 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_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, "xapT");
+ registerNamespace(TYPE_PAGEDFILE, "xapTPg");
+ registerNamespace(TYPE_GRAPHICS, "xapG");
+ registerNamespace(TYPE_IMAGE, "xapGImg");
+ 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()]);
+ }
+
+
+ /**
+ * @see XMPSchemaRegistry#registerAlias(String, String, String, String,
+ * AliasOptions)
+ */
+ public 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#deleteAlias(String, String)
+ */
+ public synchronized void deleteAlias(String aliasNS, String aliasProp)
+ {
+ String aliasPrefix = getNamespacePrefix(aliasNS);
+ aliasMap.remove(aliasPrefix + aliasProp);
+ }
+
+
+ /**
+ * @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/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java
new file mode 100644
index 0000000..9b6c7af
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerHelper.java
@@ -0,0 +1,102 @@
+// =================================================================================================
+// 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.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 &quot;exact packet size&quot;.
+ *
+ * @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/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java
new file mode 100644
index 0000000..24cafd3
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPSerializerRDF.java
@@ -0,0 +1,1295 @@
+// =================================================================================================
+// 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.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>
+ * &lt;ns:UnqualifiedStructProperty-1
+ * ... The fields as attributes, if all are simple and unqualified
+ * /&gt;
+ *
+ * &lt;ns:UnqualifiedStructProperty-2 rdf:parseType=&quot;Resource&quot;&gt;
+ * ... The fields as elements, if none are simple and unqualified
+ * &lt;/ns:UnqualifiedStructProperty-2&gt;
+ *
+ * &lt;ns:UnqualifiedStructProperty-3&gt;
+ * &lt;rdf:Description
+ * ... The simple and unqualified fields as attributes
+ * &gt;
+ * ... The compound or qualified fields as elements
+ * &lt;/rdf:Description&gt;
+ * &lt;/ns:UnqualifiedStructProperty-3&gt;
+ *
+ * &lt;ns:UnqualifiedArrayProperty&gt;
+ * &lt;rdf:Bag&gt; or Seq or Alt
+ * ... Array items as rdf:li elements, same forms as top level properties
+ * &lt;/rdf:Bag&gt;
+ * &lt;/ns:UnqualifiedArrayProperty&gt;
+ *
+ * &lt;ns:QualifiedProperty rdf:parseType=&quot;Resource&quot;&gt;
+ * &lt;rdf:value&gt; ... Property &quot;value&quot;
+ * following the unqualified forms ... &lt;/rdf:value&gt;
+ * ... Qualifiers looking like named struct fields
+ * &lt;/ns:QualifiedProperty&gt;
+ * </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 isCompact = node.getOptions().isCompact();
+ boolean hasGeneralQualifiers = isCompact; // Might also become true later.
+ 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, isCompact);
+ }
+ 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
+ * @param isCompact flag if qual shall be renderen in compact form.
+ * @throws IOException Forwards all writer exceptions.
+ * @throws XMPException If qualifier and element fields are mixed.
+ */
+ private void serializeCompactRDFGeneralQualifier(int indent, XMPNode node, boolean isCompact)
+ 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);
+
+ if (isCompact)
+ {
+ // Emit a "pxmp:compact" fake qualifier.
+ writeIndent(1);
+ write("<pxmp:compact/>");
+ writeNewline();
+ }
+
+ 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>
+ * &lt;rdf:Description rdf:about=&quot;TreeName&quot; xmlns:ns=&quot;URI&quot; ... &gt;
+ *
+ * ... The actual properties of the schema, see SerializePrettyRDFProperty
+ *
+ * &lt;!-- ns1:Alias is aliased to ns2:Actual --&gt; ... If alias comments are wanted
+ *
+ * &lt;/rdf:Description&gt;
+ * </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>
+ * &lt;ns:UnqualifiedSimpleProperty&gt;value&lt;/ns:UnqualifiedSimpleProperty&gt;
+ *
+ * &lt;ns:UnqualifiedStructProperty rdf:parseType=&quot;Resource&quot;&gt;
+ * (If no rdf:resource qualifier)
+ * ... Fields, same forms as top level properties
+ * &lt;/ns:UnqualifiedStructProperty&gt;
+ *
+ * &lt;ns:ResourceStructProperty rdf:resource=&quot;URI&quot;
+ * ... Fields as attributes
+ * &gt;
+ *
+ * &lt;ns:UnqualifiedArrayProperty&gt;
+ * &lt;rdf:Bag&gt; or Seq or Alt
+ * ... Array items as rdf:li elements, same forms as top level properties
+ * &lt;/rdf:Bag&gt;
+ * &lt;/ns:UnqualifiedArrayProperty&gt;
+ *
+ * &lt;ns:QualifiedProperty rdf:parseType=&quot;Resource&quot;&gt;
+ * &lt;rdf:value&gt; ... Property &quot;value&quot; following the unqualified
+ * forms ... &lt;/rdf:value&gt;
+ * ... Qualifiers looking like named struct fields
+ * &lt;/ns:QualifiedProperty&gt;
+ * </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 &quot;rdf:resource&quot; 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 isCompact = node.getOptions().isCompact();
+ boolean hasGeneralQualifiers = isCompact; // Might also become true later.
+ 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);
+
+ if (isCompact)
+ {
+ // Emit a "pxmp:compact" fake qualifier.
+ writeIndent(indent);
+ write("<pxmp:compact/>");
+ writeNewline();
+ }
+
+ 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>&amp;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/java/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java b/java/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java
new file mode 100644
index 0000000..4af9564
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java
@@ -0,0 +1,1167 @@
+// =================================================================================================
+// 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.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 &quot;; &quot;, 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 &apos;&quot;&apos;
+ * @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);
+ ParameterAsserts.assertNotNull(catedStr);
+ 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 = sourceNode.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 = leftNode.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/java/XMPCore/src/com/adobe/xmp/impl/package.html b/java/XMPCore/src/com/adobe/xmp/impl/package.html
new file mode 100644
index 0000000..aea0dfa
--- /dev/null
+++ b/java/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/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java b/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java
new file mode 100644
index 0000000..d6979a5
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPath.java
@@ -0,0 +1,106 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java b/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java
new file mode 100644
index 0000000..2f01ad3
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathParser.java
@@ -0,0 +1,529 @@
+
+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=&quot;value&quot;]
+ * <dd> An element in an array of structs, chosen by a field value.
+ * <dt>[@xml:lang=&quot;value&quot;]
+ * <dd> An element in an alt-text array, chosen by the xml:lang qualifier.
+ * <dt>[?qualName=&quot;value&quot;]
+ * <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/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java b/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java
new file mode 100644
index 0000000..b025170
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/impl/xpath/XMPPathSegment.java
@@ -0,0 +1,147 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/impl/xpath/package.html b/java/XMPCore/src/com/adobe/xmp/impl/xpath/package.html
new file mode 100644
index 0000000..e444a87
--- /dev/null
+++ b/java/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/java/XMPCore/src/com/adobe/xmp/options/AliasOptions.java b/java/XMPCore/src/com/adobe/xmp/options/AliasOptions.java
new file mode 100644
index 0000000..ccd3921
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/options/AliasOptions.java
@@ -0,0 +1,186 @@
+// =================================================================================================
+// 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.options;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPSchemaRegistry;
+
+
+/**
+ * Options for {@link XMPSchemaRegistry#registerAlias( String, String, String, String,
+ * AliasOptions)}.
+ *
+ * @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/java/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java b/java/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java
new file mode 100644
index 0000000..5affb05
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/options/IteratorOptions.java
@@ -0,0 +1,148 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/options/Options.java b/java/XMPCore/src/com/adobe/xmp/options/Options.java
new file mode 100644
index 0000000..278db73
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/options/Options.java
@@ -0,0 +1,290 @@
+// =================================================================================================
+// 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.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 &quot;option1 | option4&quot;.
+ */
+ 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/java/XMPCore/src/com/adobe/xmp/options/ParseOptions.java b/java/XMPCore/src/com/adobe/xmp/options/ParseOptions.java
new file mode 100644
index 0000000..04d831d
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/options/ParseOptions.java
@@ -0,0 +1,150 @@
+// =================================================================================================
+// 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.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 &quot;x:xmpmeta&quot; 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;
+
+
+ /**
+ * 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 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";
+ default: return null;
+ }
+ }
+
+
+ /**
+ * @see Options#getValidOptions()
+ */
+ protected int getValidOptions()
+ {
+ return
+ REQUIRE_XMP_META |
+ STRICT_ALIASING |
+ FIX_CONTROL_CHARS |
+ ACCEPT_LATIN_1;
+ }
+} \ No newline at end of file
diff --git a/java/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java b/java/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java
new file mode 100644
index 0000000..53d2cb1
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/options/PropertyOptions.java
@@ -0,0 +1,453 @@
+// =================================================================================================
+// 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.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 COMPACT = 0x00002000;
+ /** */
+ 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;
+ }
+
+
+ /**
+ * @return Return whether the value is a compact struct or array.
+ */
+ public boolean isCompact()
+ {
+ return getOption(COMPACT);
+ }
+
+
+ /**
+ * @param value the value to set
+ * @return Returns this to enable cascaded options.
+ */
+ public PropertyOptions setCompact(boolean value)
+ {
+ setOption(COMPACT, 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 |
+ COMPACT |
+ 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 COMPACT : return "COMPACT";
+ 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/java/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java b/java/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java
new file mode 100644
index 0000000..c4d4f94
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/options/SerializeOptions.java
@@ -0,0 +1,461 @@
+// =================================================================================================
+// 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.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;
+ /** Show aliases as XML comments. <em>Note:</em> This option is currently not supported. */
+ public static final int WRITE_ALIAS_COMMENTS = 0x0400;
+ /** 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 getWriteAliasComments()
+ {
+ return getOption(WRITE_ALIAS_COMMENTS);
+ }
+
+
+ /**
+ * <em>Note:</em> This options is not supported at the moment.
+ * @param value the value to set
+ * @return Returns the instance to call more set-methods.
+ */
+ public SerializeOptions setWriteAliasComments(boolean value)
+ {
+ setOption(WRITE_ALIAS_COMMENTS, 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 WRITE_ALIAS_COMMENTS : return "WRITE_ALIAS_COMMENTS";
+ 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 |
+ WRITE_ALIAS_COMMENTS |
+ SORT;
+ }
+} \ No newline at end of file
diff --git a/java/XMPCore/src/com/adobe/xmp/options/package.html b/java/XMPCore/src/com/adobe/xmp/options/package.html
new file mode 100644
index 0000000..d2e56d0
--- /dev/null
+++ b/java/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/java/XMPCore/src/com/adobe/xmp/package.html b/java/XMPCore/src/com/adobe/xmp/package.html
new file mode 100644
index 0000000..8afb896
--- /dev/null
+++ b/java/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/java/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java b/java/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java
new file mode 100644
index 0000000..5eb707e
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/properties/XMPAliasInfo.java
@@ -0,0 +1,39 @@
+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/java/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java b/java/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java
new file mode 100644
index 0000000..ad0c075
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/properties/XMPProperty.java
@@ -0,0 +1,40 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java b/java/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java
new file mode 100644
index 0000000..bf1150b
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/properties/XMPPropertyInfo.java
@@ -0,0 +1,45 @@
+// =================================================================================================
+// 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.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/java/XMPCore/src/com/adobe/xmp/properties/package.html b/java/XMPCore/src/com/adobe/xmp/properties/package.html
new file mode 100644
index 0000000..19e7427
--- /dev/null
+++ b/java/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>
diff --git a/java/XMPCore/src/com/adobe/xmp/version.properties b/java/XMPCore/src/com/adobe/xmp/version.properties
new file mode 100644
index 0000000..0bc14bc
--- /dev/null
+++ b/java/XMPCore/src/com/adobe/xmp/version.properties
@@ -0,0 +1,15 @@
+# ==================================================================================================
+# 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.
+# ==================================================================================================
+
+implementation.version Adobe XMP Core 4.1.1
+implementation.version.major 4
+implementation.version.minor 1
+implementation.version.micro 1
+implementation.version.engbuild 0
+implementation.version.debug true \ No newline at end of file