From 54c05109f8112394b6a5c56f45df50344f610402 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 09:27:15 +0100 Subject: [PATCH 01/17] Removed duplicated literal "Empty array name" --- .../kotlin/com/ashampoo/xmp/XMPError.kt | 1 + .../com/ashampoo/xmp/impl/XMPMetaImpl.kt | 26 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt index c5a19ba..c098b84 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt @@ -10,6 +10,7 @@ package com.ashampoo.xmp object XMPError { + const val EMPTY_ARRAY_NAME: String = "Empty array name" const val EMPTY_SCHEMA_TEXT: String = "Empty schema namespace URI" const val EMPTY_CONVERT_STRING_TEXT: String = "Empty convert-string" diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt index a6cc49b..9456ccd 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt @@ -76,7 +76,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) if (!arrayOptions.isOnlyArrayOptions()) throw XMPException("Only array form flags allowed for arrayOptions", XMPError.BADOPTIONS) @@ -123,7 +123,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val arrayPath = expandXPath(schemaNS, arrayName) val arrayNode = findNode(this.root, arrayPath, false, null) ?: return 0 @@ -140,7 +140,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val itemPath = composeArrayItemPath(arrayName, itemIndex) @@ -192,7 +192,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) @@ -223,7 +223,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val path = composeArrayItemPath(arrayName, itemIndex) @@ -243,7 +243,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val path = composeStructFieldPath(fieldNS, fieldName) @@ -276,7 +276,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val itemPath = composeArrayItemPath(arrayName, itemIndex) @@ -294,7 +294,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (altTextName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) if (specificLang.isEmpty()) throw XMPException("Empty specific language", XMPError.BADPARAM) @@ -345,7 +345,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (altTextName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) if (specificLang.isEmpty()) throw XMPException("Empty specific language", XMPError.BADPARAM) @@ -657,7 +657,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) @@ -689,7 +689,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) // Just lookup, don't try to create. val arrayPath = expandXPath(schemaNS, arrayName) @@ -713,7 +713,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) // Just lookup, don't try to create. val arrayPath = expandXPath(schemaNS, arrayName) @@ -786,7 +786,7 @@ class XMPMetaImpl : XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException("Empty array name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) From 6b9490414ccef6ba9370461878fecdb5f321d584 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 11:05:33 +0100 Subject: [PATCH 02/17] Removed XMPMeta interface and merged this with the real class. --- .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 882 ++++++++++++++-- .../kotlin/com/ashampoo/xmp/XMPMetaFactory.kt | 5 +- .../com/ashampoo/xmp/impl/XMPIteratorImpl.kt | 3 +- .../com/ashampoo/xmp/impl/XMPMetaImpl.kt | 961 ------------------ .../com/ashampoo/xmp/impl/XMPMetaParser.kt | 2 +- .../com/ashampoo/xmp/impl/XMPNormalizer.kt | 4 +- .../com/ashampoo/xmp/impl/XMPRDFParser.kt | 25 +- .../com/ashampoo/xmp/impl/XMPRDFWriter.kt | 3 +- 8 files changed, 842 insertions(+), 1043 deletions(-) delete mode 100644 src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index 02bdc5e..a72a906 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -8,18 +8,61 @@ // ================================================================================================= package com.ashampoo.xmp +import com.ashampoo.xmp.XMPPathFactory.composeArrayItemPath +import com.ashampoo.xmp.XMPPathFactory.composeQualifierPath +import com.ashampoo.xmp.XMPPathFactory.composeStructFieldPath +import com.ashampoo.xmp.XMPUtils.convertToBoolean +import com.ashampoo.xmp.XMPUtils.convertToDouble +import com.ashampoo.xmp.XMPUtils.convertToInteger +import com.ashampoo.xmp.XMPUtils.convertToLong +import com.ashampoo.xmp.XMPUtils.decodeBase64 +import com.ashampoo.xmp.impl.Utils.normalizeLangValue +import com.ashampoo.xmp.impl.XMPIteratorImpl +import com.ashampoo.xmp.impl.XMPNode +import com.ashampoo.xmp.impl.XMPNodeUtils +import com.ashampoo.xmp.impl.XMPNodeUtils.appendLangItem +import com.ashampoo.xmp.impl.XMPNodeUtils.chooseLocalizedText +import com.ashampoo.xmp.impl.XMPNodeUtils.deleteNode +import com.ashampoo.xmp.impl.XMPNodeUtils.findNode +import com.ashampoo.xmp.impl.XMPNodeUtils.setNodeValue +import com.ashampoo.xmp.impl.XMPNodeUtils.verifySetOptions +import com.ashampoo.xmp.impl.XMPNormalizer.normalize +import com.ashampoo.xmp.impl.xpath.XMPPathParser.expandXPath import com.ashampoo.xmp.options.IteratorOptions import com.ashampoo.xmp.options.ParseOptions import com.ashampoo.xmp.options.PropertyOptions import com.ashampoo.xmp.properties.XMPProperty +import com.ashampoo.xmp.properties.XMPPropertyInfo /** * 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 `OutputStream`. + * to a String. */ -@Suppress("ComplexInterface", "TooManyFunctions") -interface XMPMeta { +class XMPMeta { + + /** + * root of the metadata tree + */ + var root: XMPNode + private set + + /** + * the xpacket processing instructions content + */ + private var packetHeader: String? = null + + /** + * Constructor for an empty metadata object. + */ + constructor() { + // create root node + this.root = XMPNode(null, null, PropertyOptions()) + } + + constructor(tree: XMPNode) { + this.root = tree + } // --------------------------------------------------------------------------------------------- // Basic property manipulation functions @@ -52,7 +95,51 @@ interface XMPMeta { * @return Returns a `XMPProperty` containing the value and the options or `null` * if the property does not exist. */ - fun getProperty(schemaNS: String, propName: String): XMPProperty? + fun getProperty(schemaNS: String, propName: String): XMPProperty? = + getProperty(schemaNS, propName, VALUE_STRING) + + /** + * Returns a property, but the result value can be requested. It can be one + * of [XMPMeta.VALUE_STRING], [XMPMeta.VALUE_BOOLEAN], + * [XMPMeta.VALUE_INTEGER], [XMPMeta.VALUE_LONG], + * [XMPMeta.VALUE_DOUBLE], [XMPMeta.VALUE_DATE], + * [XMPMeta.VALUE_TIME_IN_MILLIS], [XMPMeta.VALUE_BASE64]. + */ + private fun getProperty(schemaNS: String, propName: String, valueType: Int): XMPProperty? { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val propNode = findNode( + xmpTree = this.root, + xpath = expandXPath(schemaNS, propName), + createNodes = false, + leafOptions = null + ) ?: return null + + if (valueType != VALUE_STRING && propNode.options.isCompositeProperty()) + throw XMPException("Property must be simple when a value type is requested", XMPError.BADXPATH) + + val value = evaluateNodeValue(valueType, propNode) + + return object : XMPProperty { + + override fun getValue(): String? = + value?.toString() + + override fun getOptions(): PropertyOptions = + propNode.options + + override fun getLanguage(): String? = + null + + override fun toString(): String = + value.toString() + } + } /** * Provides access to items within an array. The index is passed as an integer, you need not @@ -68,7 +155,18 @@ interface XMPMeta { * @return Returns a `XMPProperty` containing the value and the options or * `null` if the property does not exist. */ - fun getArrayItem(schemaNS: String, arrayName: String, itemIndex: Int): XMPProperty? + fun getArrayItem(schemaNS: String, arrayName: String, itemIndex: Int): XMPProperty? { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val itemPath = composeArrayItemPath(arrayName, itemIndex) + + return getProperty(schemaNS, itemPath) + } /** * Returns the number of items in the array. @@ -79,7 +177,22 @@ interface XMPMeta { * Has the same namespace prefix usage as propName in `getProperty()`. * @return Returns the number of items in the array. */ - fun countArrayItems(schemaNS: String, arrayName: String): Int + fun countArrayItems(schemaNS: String, arrayName: String): Int { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val arrayPath = expandXPath(schemaNS, arrayName) + val arrayNode = findNode(this.root, arrayPath, false, null) ?: return 0 + + if (!arrayNode.options.isArray()) + throw XMPException("The named property is not an array", XMPError.BADXPATH) + + return arrayNode.getChildrenLength() + } /** * Provides access to fields within a nested structure. The namespace for the field is passed as @@ -108,7 +221,20 @@ interface XMPMeta { structName: String, fieldNS: String, fieldName: String - ): XMPProperty? + ): XMPProperty? { + + /* Note that fieldNS and fieldName are checked inside composeStructFieldPath */ + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (structName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) + + return getProperty(schemaNS, fieldPath) + } /** * Provides access to a qualifier attached to a property. The namespace for the qualifier is @@ -145,7 +271,19 @@ interface XMPMeta { propName: String, qualNS: String, qualName: String - ): XMPProperty? + ): XMPProperty? { + + // qualNS and qualName are checked inside composeQualfierPath + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val qualPath = propName + composeQualifierPath(qualNS, qualName) + + return getProperty(schemaNS, qualPath) + } // --------------------------------------------------------------------------------------------- // Functions for setting property values @@ -180,8 +318,26 @@ interface XMPMeta { schemaNS: String, propName: String, propValue: Any?, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val verifiedOptions = verifySetOptions(options, propValue) + + val propNode = findNode( + xmpTree = this.root, + xpath = expandXPath(schemaNS, propName), + createNodes = true, + leafOptions = verifySetOptions(options, propValue) + ) ?: throw XMPException("Specified property does not exist", XMPError.BADXPATH) + + setNode(propNode, propValue, verifiedOptions, false) + } /** * Replaces an item within an array. The index is passed as an integer, you need not worry about @@ -205,8 +361,24 @@ interface XMPMeta { arrayName: String, itemIndex: Int, itemValue: String, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + // Just lookup, don't try to create. + val arrayPath = expandXPath(schemaNS, arrayName) + val arrayNode = findNode(this.root, arrayPath, false, null) + + if (arrayNode == null) + throw XMPException("Specified array does not exist", XMPError.BADXPATH) + + doSetArrayItem(arrayNode, itemIndex, itemValue, options, false) + } /** * Inserts an item into an array previous to the given index. The index is passed as an integer, @@ -229,8 +401,24 @@ interface XMPMeta { arrayName: String, itemIndex: Int, itemValue: String, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + // Just lookup, don't try to create. + val arrayPath = expandXPath(schemaNS, arrayName) + val arrayNode = findNode(this.root, arrayPath, false, null) + + if (arrayNode == null) + throw XMPException("Specified array does not exist", XMPError.BADXPATH) + + doSetArrayItem(arrayNode, itemIndex, itemValue, options, true) + } /** * Simplifies the construction of an array by not requiring that you pre-create an empty array. @@ -259,10 +447,55 @@ interface XMPMeta { fun appendArrayItem( schemaNS: String, arrayName: String, - arrayOptions: PropertyOptions = PropertyOptions(), + arrayOptions: PropertyOptions, itemValue: String, - itemOptions: PropertyOptions = PropertyOptions() - ) + itemOptions: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + if (!arrayOptions.isOnlyArrayOptions()) + throw XMPException("Only array form flags allowed for arrayOptions", XMPError.BADOPTIONS) + + // Check if array options are set correctly. + val verifiedArrayOptions = 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. + val arrayPath = expandXPath(schemaNS, arrayName) + + // Just lookup, don't try to create. + var arrayNode = findNode(this.root, arrayPath, false, null) + + if (arrayNode != null) { + + // The array exists, make sure the form is compatible. Zero arrayForm means take what exists. + if (!arrayNode.options.isArray()) + throw XMPException("The named property is not an array", XMPError.BADXPATH) + + } else { + + // The array does not exist, try to create it. + if (verifiedArrayOptions.isArray()) { + + arrayNode = findNode(this.root, arrayPath, true, verifiedArrayOptions) + + if (arrayNode == null) + throw XMPException("Failure creating array node", XMPError.BADXPATH) + + } else { + + // array options missing + throw XMPException("Explicit arrayOptions required to create new array", XMPError.BADOPTIONS) + } + } + + doSetArrayItem(arrayNode, XMPConst.ARRAY_LAST_ITEM, itemValue, itemOptions, true) + } /** * Provides access to fields within a nested structure. The namespace for the field is passed as @@ -288,8 +521,19 @@ interface XMPMeta { fieldNS: String, fieldName: String, fieldValue: String?, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (structName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) + + setProperty(schemaNS, fieldPath, fieldValue, options) + } /** * Provides access to a qualifier attached to a property. The namespace for the qualifier is @@ -320,8 +564,22 @@ interface XMPMeta { qualNS: String, qualName: String, qualValue: String, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + if (!doesPropertyExist(schemaNS, propName)) + throw XMPException("Specified property does not exist!", XMPError.BADXPATH) + + val qualPath = propName + composeQualifierPath(qualNS, qualName) + + setProperty(schemaNS, qualPath, qualValue, options) + } // --------------------------------------------------------------------------------------------- // Functions for deleting and detecting properties. @@ -334,7 +592,23 @@ interface XMPMeta { * @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 getProperty. */ - fun deleteProperty(schemaNS: String, propName: String) + fun deleteProperty(schemaNS: String, propName: String) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Can't delete empty property name.", XMPError.BADPARAM) + + val propNode = findNode( + xmpTree = this.root, + xpath = expandXPath(schemaNS, propName), + createNodes = false, + leafOptions = null + ) ?: return + + deleteNode(propNode) + } /** * Deletes the given XMP subtree rooted at the given array item. @@ -348,7 +622,18 @@ interface XMPMeta { * constant `XMPConst.ARRAY_LAST_ITEM` always refers to the last * existing array item. */ - fun deleteArrayItem(schemaNS: String, arrayName: String, itemIndex: Int) + fun deleteArrayItem(schemaNS: String, arrayName: String, itemIndex: Int) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val itemPath = composeArrayItemPath(arrayName, itemIndex) + + deleteProperty(schemaNS, itemPath) + } /** * Deletes the given XMP subtree rooted at the given struct field. @@ -364,7 +649,25 @@ interface XMPMeta { * `null` or the empty string. Has the same namespace prefix usage as the * structName parameter. */ - fun deleteStructField(schemaNS: String, structName: String, fieldNS: String, fieldName: String) + fun deleteStructField( + schemaNS: String, + structName: String, + fieldNS: String, + fieldName: String + ) { + + // fieldNS and fieldName are checked inside composeStructFieldPath + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (structName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) + + deleteProperty(schemaNS, fieldPath) + } /** * Deletes the given XMP subtree rooted at the given qualifier. @@ -379,7 +682,19 @@ interface XMPMeta { * `null` or the empty string. Has the same namespace prefix usage as the * propName parameter. */ - fun deleteQualifier(schemaNS: String, propName: String, qualNS: String, qualName: String) + fun deleteQualifier(schemaNS: String, propName: String, qualNS: String, qualName: String) { + + // Note: qualNS and qualName are checked inside composeQualfierPath + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val qualPath = propName + composeQualifierPath(qualNS, qualName) + + deleteProperty(schemaNS, qualPath) + } /** * Returns whether the property exists. @@ -388,7 +703,23 @@ interface XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @return Returns true if the property exists. */ - fun doesPropertyExist(schemaNS: String, propName: String): Boolean + fun doesPropertyExist(schemaNS: String, propName: String): Boolean { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val propNode = findNode( + xmpTree = this.root, + xpath = expandXPath(schemaNS, propName), + createNodes = false, + leafOptions = null + ) + + return propNode != null + } /** * Tells if the array item exists. @@ -402,7 +733,18 @@ interface XMPMeta { * existing array item. * @return Returns `true` if the array exists, `false` otherwise. */ - fun doesArrayItemExist(schemaNS: String, arrayName: String, itemIndex: Int): Boolean + fun doesArrayItemExist(schemaNS: String, arrayName: String, itemIndex: Int): Boolean { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (arrayName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val path = composeArrayItemPath(arrayName, itemIndex) + + return doesPropertyExist(schemaNS, path) + } /** * Tells if the struct field exists. @@ -423,7 +765,20 @@ interface XMPMeta { structName: String, fieldNS: String, fieldName: String - ): Boolean + ): Boolean { + + // fieldNS and fieldName are checked inside composeStructFieldPath() + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (structName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + val path = composeStructFieldPath(fieldNS, fieldName) + + return doesPropertyExist(schemaNS, structName + path) + } /** * Tells if the qualifier exists. @@ -438,7 +793,25 @@ interface XMPMeta { * propName parameter. * @return Returns true if the qualifier exists. */ - fun doesQualifierExist(schemaNS: String, propName: String, qualNS: String, qualName: String): Boolean + fun doesQualifierExist( + schemaNS: String, + propName: String, + qualNS: String, + qualName: String + ): Boolean { + + // qualNS and qualName are checked inside composeQualifierPath() + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val path = composeQualifierPath(qualNS, qualName) + + return doesPropertyExist(schemaNS, propName + path) + } // --------------------------------------------------------------------------------------------- // Specialized Get and Set functions @@ -498,16 +871,56 @@ interface XMPMeta { * `null` or the empty string. * @return Returns an `XMPProperty` containing the value, the actual language and * the options if an appropriate alternate collection item exists, `null` - * if the property. - * does not exist. - * + * if the property does not exist. */ fun getLocalizedText( schemaNS: String, altTextName: String, genericLang: String?, specificLang: String - ): XMPProperty? + ): XMPProperty? { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (altTextName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + if (specificLang.isEmpty()) + throw XMPException("Empty specific language", XMPError.BADPARAM) + + val normalizedGenericLang = genericLang?.let { normalizeLangValue(it) } + val normalizedSpecificLang = normalizeLangValue(specificLang) + + val arrayPath = expandXPath(schemaNS, altTextName) + + // *** This expand/find idiom is used in 3 Getters. + val arrayNode = findNode(this.root, arrayPath, false, null) ?: return null + val result = chooseLocalizedText(arrayNode, normalizedGenericLang, normalizedSpecificLang) + val match = result[0] as Int + val itemNode = result[1] as? XMPNode + + return if (match != XMPNodeUtils.CLT_NO_VALUES) { + + object : XMPProperty { + + override fun getValue(): String = + itemNode!!.value!! + + override fun getOptions(): PropertyOptions = + itemNode!!.options + + override fun getLanguage(): String = + itemNode!!.getQualifier(1).value!! + + override fun toString(): String = + itemNode!!.value.toString() + } + + } else { + null + } + } /** * Modifies the value of a selected item in an alt-text array. Creates an appropriate array item @@ -544,8 +957,154 @@ interface XMPMeta { genericLang: String?, specificLang: String, itemValue: String, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (altTextName.isEmpty()) + throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + + if (specificLang.isEmpty()) + throw XMPException("Empty specific language", XMPError.BADPARAM) + + val normalizedGenericLang = genericLang?.let { normalizeLangValue(it) } + val normalizedSpecificLang = normalizeLangValue(specificLang) + + val arrayPath = expandXPath(schemaNS, altTextName) + + // Find the array node and set the options if it was just created. + val arrayNode = findNode( + this.root, arrayPath, true, + PropertyOptions( + PropertyOptions.ARRAY or PropertyOptions.ARRAY_ORDERED + or PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ALT_TEXT + ) + ) + + if (arrayNode == null) { + + throw XMPException("Failed to find or create array node", XMPError.BADXPATH) + + } else if (!arrayNode.options.isArrayAltText()) { + + if (!arrayNode.hasChildren() && arrayNode.options.isArrayAlternate()) + arrayNode.options.setArrayAltText(true) + else + throw XMPException("Specified property is no alt-text array", XMPError.BADXPATH) + } + + // Make sure the x-default item, if any, is first. + var haveXDefault = false + var xdItem: XMPNode? = null + + for (item in arrayNode.iterateChildren()) { + + if (!item.hasQualifier() || XMPConst.XML_LANG != item.getQualifier(1).name) + throw XMPException("Language qualifier must be first", XMPError.BADXPATH) + + if (XMPConst.X_DEFAULT == item.getQualifier(1).value) { + xdItem = item + 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. + val result = chooseLocalizedText(arrayNode, normalizedGenericLang, normalizedSpecificLang) + val match = result[0] as Int + val itemNode = result[1] as? XMPNode + + val specificXDefault = XMPConst.X_DEFAULT == normalizedSpecificLang + + when (match) { + + XMPNodeUtils.CLT_NO_VALUES -> { + + // Create the array items for the specificLang and x-default, with x-default first. + appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue) + + haveXDefault = true + + if (!specificXDefault) + appendLangItem(arrayNode, normalizedSpecificLang, itemValue) + } + + 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.value == itemNode!!.value) + xdItem.value = itemValue + + // ! Do this after the x-default check! + itemNode!!.value = itemValue + + } else { + + // Update all items whose values match the old x-default value. + check(haveXDefault && xdItem == itemNode) + + val it = arrayNode.iterateChildren() + + while (it.hasNext()) { + + val currItem = it.next() + + if (currItem == xdItem || currItem.value != xdItem?.value) + continue + + currItem.value = itemValue + } + + // And finally do the x-default item. + if (xdItem != null) + xdItem.value = itemValue + } + + XMPNodeUtils.CLT_SINGLE_GENERIC -> { + + // Update the generic item, update x-default if it matches the old value. + if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.value == itemNode!!.value) + xdItem.value = itemValue + + // ! Do this after the x-default check! + itemNode!!.value = itemValue + } + + XMPNodeUtils.CLT_FIRST_ITEM, XMPNodeUtils.CLT_MULTIPLE_GENERIC -> { + + // Create the specific language, ignore x-default. + appendLangItem(arrayNode, normalizedSpecificLang, itemValue) + + if (specificXDefault) haveXDefault = true + } + + XMPNodeUtils.CLT_XDEFAULT -> { + + // Create the specific language, update x-default if it was the only item. + if (xdItem != null && arrayNode.getChildrenLength() == 1) + xdItem.value = itemValue + + appendLangItem(arrayNode, normalizedSpecificLang, itemValue) + } + + else -> // does not happen under normal circumstances + throw XMPException("Unexpected result from ChooseLocalizedText", XMPError.INTERNALFAILURE) + } + + // Add an x-default at the front if needed. + if (!haveXDefault && arrayNode.getChildrenLength() == 1) + appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue) + } // --------------------------------------------------------------------------------------------- // Functions accessing properties as binary values. @@ -560,7 +1119,8 @@ interface XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @return Returns a `Boolean` value or `null` if the property does not exist. */ - fun getPropertyBoolean(schemaNS: String, propName: String): Boolean? + fun getPropertyBoolean(schemaNS: String, propName: String): Boolean? = + getPropertyObject(schemaNS, propName, VALUE_BOOLEAN) as? Boolean /** * Convenience method to retrieve the literal value of a property. @@ -569,7 +1129,8 @@ interface XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @return Returns an `Integer` value or `null` if the property does not exist. */ - fun getPropertyInteger(schemaNS: String, propName: String): Int? + fun getPropertyInteger(schemaNS: String, propName: String): Int? = + getPropertyObject(schemaNS, propName, VALUE_INTEGER) as? Int /** * Convenience method to retrieve the literal value of a property. @@ -578,7 +1139,8 @@ interface XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @return Returns a `Long` value or `null` if the property does not exist. */ - fun getPropertyLong(schemaNS: String, propName: String): Long? + fun getPropertyLong(schemaNS: String, propName: String): Long? = + getPropertyObject(schemaNS, propName, VALUE_LONG) as? Long /** * Convenience method to retrieve the literal value of a property. @@ -587,7 +1149,8 @@ interface XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @return Returns a `Double` value or `null` if the property does not exist. */ - fun getPropertyDouble(schemaNS: String, propName: String): Double? + fun getPropertyDouble(schemaNS: String, propName: String): Double? = + getPropertyObject(schemaNS, propName, VALUE_DOUBLE) as? Double /** * Convenience method to retrieve the literal value of a property. @@ -597,7 +1160,8 @@ interface XMPMeta { * @return Returns a `byte[]`-array contained the decoded base64 value or `null` if the property does * not exist. */ - fun getPropertyBase64(schemaNS: String, propName: String): ByteArray? + fun getPropertyBase64(schemaNS: String, propName: String): ByteArray? = + getPropertyObject(schemaNS, propName, VALUE_BASE64) as? ByteArray /** * Convenience method to retrieve the literal value of a property. @@ -609,7 +1173,32 @@ interface XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @return Returns a `String` value or `null` if the property does not exist. */ - fun getPropertyString(schemaNS: String, propName: String): String? + fun getPropertyString(schemaNS: String, propName: String): String? = + getPropertyObject(schemaNS, propName, VALUE_STRING) as? String + + /** + * Returns a property, but the result value can be requested. + */ + private fun getPropertyObject(schemaNS: String, propName: String, valueType: Int): Any? { + + if (schemaNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (propName.isEmpty()) + throw XMPException("Empty property name", XMPError.BADPARAM) + + val propNode = findNode( + xmpTree = this.root, + xpath = expandXPath(schemaNS, propName), + createNodes = false, + leafOptions = null + ) ?: return null + + if (valueType != VALUE_STRING && propNode.options.isCompositeProperty()) + throw XMPException("Property must be simple when a value type is requested", XMPError.BADXPATH) + + return evaluateNodeValue(valueType, propNode) + } /** * Convenience method to set a property to a literal `boolean` value. @@ -623,8 +1212,15 @@ interface XMPMeta { schemaNS: String, propName: String, propValue: Boolean, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + setProperty( + schemaNS, + propName, + if (propValue) XMPConst.TRUE_STRING else XMPConst.FALSE_STRING, + options + ) + } /** * Convenience method to set a property to a literal `int` value. @@ -639,8 +1235,10 @@ interface XMPMeta { schemaNS: String, propName: String, propValue: Int, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + setProperty(schemaNS, propName, propValue, options) + } /** * Convenience method to set a property to a literal `long` value. @@ -654,8 +1252,10 @@ interface XMPMeta { schemaNS: String, propName: String, propValue: Long, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + setProperty(schemaNS, propName, propValue, options) + } /** * Convenience method to set a property to a literal `double` value. @@ -669,8 +1269,10 @@ interface XMPMeta { schemaNS: String, propName: String, propValue: Double, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + setProperty(schemaNS, propName, propValue, options) + } /** * Convenience method to set a property from a binary `byte[]`-array, @@ -685,15 +1287,18 @@ interface XMPMeta { schemaNS: String, propName: String, propValue: ByteArray, - options: PropertyOptions = PropertyOptions() - ) + options: PropertyOptions + ) { + setProperty(schemaNS, propName, propValue, options) + } /** * Constructs an iterator for the properties within this XMP object. * * @return Returns an `XMPIterator`. */ - fun iterator(): XMPIterator + fun iterator(): XMPIterator = + iterator(IteratorOptions()) /** * Constructs an iterator for the properties within this XMP object using some options. @@ -701,7 +1306,8 @@ interface XMPMeta { * @param options Option flags to control the iteration. * @return Returns an `XMPIterator`. */ - fun iterator(options: IteratorOptions): XMPIterator + fun iterator(options: IteratorOptions): com.ashampoo.xmp.XMPIterator = + iterator(null, null, options) /** * Construct an iterator for the properties within an XMP object. According to the parameters it iterates @@ -718,8 +1324,9 @@ interface XMPMeta { fun iterator( schemaNS: String?, propName: String?, - options: IteratorOptions = IteratorOptions() - ): XMPIterator + options: IteratorOptions + ): XMPIterator = + XMPIteratorImpl(this, schemaNS, propName, options) /** * This correlates to the about-attribute, @@ -727,12 +1334,15 @@ interface XMPMeta { * * @return Returns the name of the XMP object. */ - fun getObjectName(): String + fun getObjectName(): String = + root.name ?: "" /** * @param name Sets the name of the XMP object. */ - fun setObjectName(name: String) + fun setObjectName(name: String) { + root.name = name + } /** * @return Returns the unparsed content of the <?xpacket> processing instruction. @@ -741,7 +1351,15 @@ interface XMPMeta { * 'encoding="XXX"'. If the parsed packet has not been wrapped into an xpacket, * `null` is returned. */ - fun getPacketHeader(): String? + fun getPacketHeader(): String? = + packetHeader + + /** + * Sets the packetHeader attributes, only used by the parser. + */ + fun setPacketHeader(packetHeader: String?) { + this.packetHeader = packetHeader + } /** * Sorts the complete datamodel according to the following rules: @@ -752,7 +1370,9 @@ interface XMPMeta { * * Qualifier are sorted, with the exception of "xml:lang" and/or "rdf:type" * that stay at the top of the list in that order. */ - fun sort() + fun sort() { + this.root.sort() + } /** * Perform the normalization as a separate parsing step. @@ -761,8 +1381,148 @@ interface XMPMeta { * *Note:* It does no harm to call this method to an already normalized xmp object. * It was a PDF/A requirement to get hand on the unnormalized `XMPMeta` object. */ - fun normalize(options: ParseOptions = ParseOptions()) + fun normalize(options: ParseOptions) { + normalize(this, options) + } + + fun printAllToConsole() { + + val iterator: XMPIterator = iterator() + + while (iterator.hasNext()) { + + val propertyInfo = iterator.next() as? XMPPropertyInfo ?: continue + + println("${propertyInfo.getPath()} = ${propertyInfo.getValue()}") + } + } + + // ------------------------------------------------------------------------------------- + // 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. + */ + private fun doSetArrayItem( + arrayNode: XMPNode, + itemIndex: Int, + itemValue: String, + itemOptions: PropertyOptions, + insert: Boolean + ) { + + val itemNode = XMPNode(XMPConst.ARRAY_ITEM_NAME, null) + + val verifiedItemOptions = verifySetOptions(itemOptions, itemValue) + + // in insert mode the index after the last is allowed, + // even ARRAY_LAST_ITEM points to the index *after* the last. + val maxIndex = if (insert) + arrayNode.getChildrenLength() + 1 + else + arrayNode.getChildrenLength() + + val limitedItemIndex = if (itemIndex == XMPConst.ARRAY_LAST_ITEM) + maxIndex + else + itemIndex + + if (1 <= limitedItemIndex && limitedItemIndex <= maxIndex) { + + if (!insert) + arrayNode.removeChild(limitedItemIndex) + + arrayNode.addChild(limitedItemIndex, itemNode) + setNode(itemNode, itemValue, verifiedItemOptions, false) + + } else { + throw XMPException("Array index out of bounds", XMPError.BADINDEX) + } + } + + /** + * The internals for setProperty() and related calls, used after the node is found or created. + */ + private fun setNode(node: XMPNode, value: Any?, newOptions: PropertyOptions, deleteExisting: Boolean) { + + val compositeMask = PropertyOptions.ARRAY or PropertyOptions.ARRAY_ALT_TEXT or + PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ORDERED or PropertyOptions.STRUCT + + if (deleteExisting) + node.clear() + + // its checked by setOptions(), if the merged result is a valid options set + node.options.mergeWith(newOptions) + + if (node.options.getOptions() and compositeMask == 0) { + + // This is setting the value of a leaf node. + setNodeValue(node, value) + + } else { + + if (value != null && value.toString().isNotEmpty()) + throw XMPException("Composite nodes can't have values", XMPError.BADXPATH) + + // Can't change an array to a struct, or vice versa. + if (node.options.getOptions() and compositeMask != 0 && + newOptions.getOptions() and compositeMask != node.options.getOptions() and compositeMask + ) + throw XMPException("Requested and existing composite form mismatch", XMPError.BADXPATH) + + node.removeChildren() + } + } + + /** + * Evaluates a raw node value to the given value type, apply special + * conversions for defined types in XMP. + */ + private fun evaluateNodeValue(valueType: Int, propNode: XMPNode): Any? { + + val value: Any? + val rawValue = propNode.value + + value = when (valueType) { + + VALUE_BOOLEAN -> convertToBoolean(rawValue) + + VALUE_INTEGER -> convertToInteger(rawValue) + + VALUE_LONG -> convertToLong(rawValue) + + VALUE_DOUBLE -> convertToDouble(rawValue) + + VALUE_BASE64 -> decodeBase64(rawValue!!) + + // 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_STRING -> + if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" + + else -> + if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" + } + + return value + } + + companion object { - fun printAllToConsole() + /** + * Property values are Strings by default + */ + private const val VALUE_STRING = 0 + private const val VALUE_BOOLEAN = 1 + private const val VALUE_INTEGER = 2 + private const val VALUE_LONG = 3 + private const val VALUE_DOUBLE = 4 + private const val VALUE_BASE64 = 7 + } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt index 528d305..5b6f8ba 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt @@ -8,7 +8,6 @@ // ================================================================================================= package com.ashampoo.xmp -import com.ashampoo.xmp.impl.XMPMetaImpl import com.ashampoo.xmp.impl.XMPMetaParser import com.ashampoo.xmp.impl.XMPRDFWriter import com.ashampoo.xmp.impl.XMPSchemaRegistryImpl @@ -26,7 +25,7 @@ object XMPMetaFactory { @kotlin.jvm.JvmStatic val versionInfo = XMPVersionInfo - fun create(): XMPMeta = XMPMetaImpl() + fun create(): XMPMeta = XMPMeta() @Throws(XMPException::class) fun parseFromString( @@ -41,8 +40,6 @@ object XMPMetaFactory { options: SerializeOptions? = null ): String { - require(xmp is XMPMetaImpl) { "Serialization only works with XMPMetaImpl" } - val actualOptions = options ?: SerializeOptions() /* sort the internal data model on demand */ diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt index ac3bdf7..82a98e4 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt @@ -11,6 +11,7 @@ package com.ashampoo.xmp.impl import com.ashampoo.xmp.XMPError import com.ashampoo.xmp.XMPException import com.ashampoo.xmp.XMPIterator +import com.ashampoo.xmp.XMPMeta import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry import com.ashampoo.xmp.impl.XMPNodeUtils.findNode import com.ashampoo.xmp.impl.XMPNodeUtils.findSchemaNode @@ -27,7 +28,7 @@ import com.ashampoo.xmp.properties.XMPPropertyInfo * Calls to `skipSubtree()` / `skipSiblings()` will affect the iteration. */ class XMPIteratorImpl( - xmp: XMPMetaImpl, + xmp: XMPMeta, schemaNS: String?, propPath: String?, options: IteratorOptions? diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt deleted file mode 100644 index 9456ccd..0000000 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaImpl.kt +++ /dev/null @@ -1,961 +0,0 @@ -// ================================================================================================= -// 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.ashampoo.xmp.impl - -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPIterator -import com.ashampoo.xmp.XMPMeta -import com.ashampoo.xmp.XMPPathFactory.composeArrayItemPath -import com.ashampoo.xmp.XMPPathFactory.composeQualifierPath -import com.ashampoo.xmp.XMPPathFactory.composeStructFieldPath -import com.ashampoo.xmp.XMPUtils.convertToBoolean -import com.ashampoo.xmp.XMPUtils.convertToDouble -import com.ashampoo.xmp.XMPUtils.convertToInteger -import com.ashampoo.xmp.XMPUtils.convertToLong -import com.ashampoo.xmp.XMPUtils.decodeBase64 -import com.ashampoo.xmp.impl.Utils.normalizeLangValue -import com.ashampoo.xmp.impl.XMPNodeUtils.appendLangItem -import com.ashampoo.xmp.impl.XMPNodeUtils.chooseLocalizedText -import com.ashampoo.xmp.impl.XMPNodeUtils.deleteNode -import com.ashampoo.xmp.impl.XMPNodeUtils.findNode -import com.ashampoo.xmp.impl.XMPNodeUtils.setNodeValue -import com.ashampoo.xmp.impl.XMPNodeUtils.verifySetOptions -import com.ashampoo.xmp.impl.XMPNormalizer.normalize -import com.ashampoo.xmp.impl.xpath.XMPPathParser.expandXPath -import com.ashampoo.xmp.options.IteratorOptions -import com.ashampoo.xmp.options.ParseOptions -import com.ashampoo.xmp.options.PropertyOptions -import com.ashampoo.xmp.properties.XMPProperty -import com.ashampoo.xmp.properties.XMPPropertyInfo - -/** - * Implementation for [XMPMeta]. - */ -class XMPMetaImpl : XMPMeta { - - /** - * root of the metadata tree - */ - var root: XMPNode - private set - - /** - * the xpacket processing instructions content - */ - private var packetHeader: String? = null - - /** - * Constructor for an empty metadata object. - */ - constructor() { - // create root node - this.root = XMPNode(null, null, PropertyOptions()) - } - - constructor(tree: XMPNode) { - this.root = tree - } - - override fun appendArrayItem( - schemaNS: String, - arrayName: String, - arrayOptions: PropertyOptions, - itemValue: String, - itemOptions: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - if (!arrayOptions.isOnlyArrayOptions()) - throw XMPException("Only array form flags allowed for arrayOptions", XMPError.BADOPTIONS) - - // Check if array options are set correctly. - val verifiedArrayOptions = 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. - val arrayPath = expandXPath(schemaNS, arrayName) - - // Just lookup, don't try to create. - var arrayNode = findNode(this.root, arrayPath, false, null) - - if (arrayNode != null) { - - // The array exists, make sure the form is compatible. Zero arrayForm means take what exists. - if (!arrayNode.options.isArray()) - throw XMPException("The named property is not an array", XMPError.BADXPATH) - - } else { - - // The array does not exist, try to create it. - if (verifiedArrayOptions.isArray()) { - - arrayNode = findNode(this.root, arrayPath, true, verifiedArrayOptions) - - if (arrayNode == null) - throw XMPException("Failure creating array node", XMPError.BADXPATH) - - } else { - - // array options missing - throw XMPException("Explicit arrayOptions required to create new array", XMPError.BADOPTIONS) - } - } - - doSetArrayItem(arrayNode, XMPConst.ARRAY_LAST_ITEM, itemValue, itemOptions, true) - } - - override fun countArrayItems(schemaNS: String, arrayName: String): Int { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val arrayPath = expandXPath(schemaNS, arrayName) - val arrayNode = findNode(this.root, arrayPath, false, null) ?: return 0 - - if (!arrayNode.options.isArray()) - throw XMPException("The named property is not an array", XMPError.BADXPATH) - - return arrayNode.getChildrenLength() - } - - override fun deleteArrayItem(schemaNS: String, arrayName: String, itemIndex: Int) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val itemPath = composeArrayItemPath(arrayName, itemIndex) - - deleteProperty(schemaNS, itemPath) - } - - override fun deleteProperty(schemaNS: String, propName: String) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Can't delete empty property name.", XMPError.BADPARAM) - - val propNode = findNode( - xmpTree = this.root, - xpath = expandXPath(schemaNS, propName), - createNodes = false, - leafOptions = null - ) ?: return - - deleteNode(propNode) - } - - override fun deleteQualifier(schemaNS: String, propName: String, qualNS: String, qualName: String) { - - // Note: qualNS and qualName are checked inside composeQualfierPath - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val qualPath = propName + composeQualifierPath(qualNS, qualName) - - deleteProperty(schemaNS, qualPath) - } - - override fun deleteStructField( - schemaNS: String, - structName: String, - fieldNS: String, - fieldName: String - ) { - - // fieldNS and fieldName are checked inside composeStructFieldPath - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) - - deleteProperty(schemaNS, fieldPath) - } - - override fun doesPropertyExist(schemaNS: String, propName: String): Boolean { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val propNode = findNode( - xmpTree = this.root, - xpath = expandXPath(schemaNS, propName), - createNodes = false, - leafOptions = null - ) - - return propNode != null - } - - override fun doesArrayItemExist(schemaNS: String, arrayName: String, itemIndex: Int): Boolean { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val path = composeArrayItemPath(arrayName, itemIndex) - - return doesPropertyExist(schemaNS, path) - } - - override fun doesStructFieldExist( - schemaNS: String, - structName: String, - fieldNS: String, - fieldName: String - ): Boolean { - - // fieldNS and fieldName are checked inside composeStructFieldPath() - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val path = composeStructFieldPath(fieldNS, fieldName) - - return doesPropertyExist(schemaNS, structName + path) - } - - override fun doesQualifierExist( - schemaNS: String, - propName: String, - qualNS: String, - qualName: String - ): Boolean { - - // qualNS and qualName are checked inside composeQualifierPath() - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val path = composeQualifierPath(qualNS, qualName) - - return doesPropertyExist(schemaNS, propName + path) - } - - override fun getArrayItem(schemaNS: String, arrayName: String, itemIndex: Int): XMPProperty? { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val itemPath = composeArrayItemPath(arrayName, itemIndex) - - return getProperty(schemaNS, itemPath) - } - - override fun getLocalizedText( - schemaNS: String, - altTextName: String, - genericLang: String?, - specificLang: String - ): XMPProperty? { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (altTextName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - if (specificLang.isEmpty()) - throw XMPException("Empty specific language", XMPError.BADPARAM) - - val normalizedGenericLang = genericLang?.let { normalizeLangValue(it) } - val normalizedSpecificLang = normalizeLangValue(specificLang) - - val arrayPath = expandXPath(schemaNS, altTextName) - - // *** This expand/find idiom is used in 3 Getters. - val arrayNode = findNode(this.root, arrayPath, false, null) ?: return null - val result = chooseLocalizedText(arrayNode, normalizedGenericLang, normalizedSpecificLang) - val match = result[0] as Int - val itemNode = result[1] as? XMPNode - - return if (match != XMPNodeUtils.CLT_NO_VALUES) { - - object : XMPProperty { - - override fun getValue(): String = - itemNode!!.value!! - - override fun getOptions(): PropertyOptions = - itemNode!!.options - - override fun getLanguage(): String = - itemNode!!.getQualifier(1).value!! - - override fun toString(): String = - itemNode!!.value.toString() - } - - } else { - null - } - } - - override fun setLocalizedText( - schemaNS: String, - altTextName: String, - genericLang: String?, - specificLang: String, - itemValue: String, - options: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (altTextName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - if (specificLang.isEmpty()) - throw XMPException("Empty specific language", XMPError.BADPARAM) - - val normalizedGenericLang = genericLang?.let { normalizeLangValue(it) } - val normalizedSpecificLang = normalizeLangValue(specificLang) - - val arrayPath = expandXPath(schemaNS, altTextName) - - // Find the array node and set the options if it was just created. - val arrayNode = findNode( - this.root, arrayPath, true, - PropertyOptions( - PropertyOptions.ARRAY or PropertyOptions.ARRAY_ORDERED - or PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ALT_TEXT - ) - ) - - if (arrayNode == null) { - - throw XMPException("Failed to find or create array node", XMPError.BADXPATH) - - } else if (!arrayNode.options.isArrayAltText()) { - - if (!arrayNode.hasChildren() && arrayNode.options.isArrayAlternate()) - arrayNode.options.setArrayAltText(true) - else - throw XMPException("Specified property is no alt-text array", XMPError.BADXPATH) - } - - // Make sure the x-default item, if any, is first. - var haveXDefault = false - var xdItem: XMPNode? = null - - for (item in arrayNode.iterateChildren()) { - - if (!item.hasQualifier() || XMPConst.XML_LANG != item.getQualifier(1).name) - throw XMPException("Language qualifier must be first", XMPError.BADXPATH) - - if (XMPConst.X_DEFAULT == item.getQualifier(1).value) { - xdItem = item - 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. - val result = chooseLocalizedText(arrayNode, normalizedGenericLang, normalizedSpecificLang) - val match = result[0] as Int - val itemNode = result[1] as? XMPNode - - val specificXDefault = XMPConst.X_DEFAULT == normalizedSpecificLang - - when (match) { - - XMPNodeUtils.CLT_NO_VALUES -> { - - // Create the array items for the specificLang and x-default, with x-default first. - appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue) - - haveXDefault = true - - if (!specificXDefault) - appendLangItem(arrayNode, normalizedSpecificLang, itemValue) - } - - 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.value == itemNode!!.value) - xdItem.value = itemValue - - // ! Do this after the x-default check! - itemNode!!.value = itemValue - - } else { - - // Update all items whose values match the old x-default value. - check(haveXDefault && xdItem == itemNode) - - val it = arrayNode.iterateChildren() - - while (it.hasNext()) { - - val currItem = it.next() - - if (currItem == xdItem || currItem.value != xdItem?.value) - continue - - currItem.value = itemValue - } - - // And finally do the x-default item. - if (xdItem != null) - xdItem.value = itemValue - } - - XMPNodeUtils.CLT_SINGLE_GENERIC -> { - - // Update the generic item, update x-default if it matches the old value. - if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.value == itemNode!!.value) - xdItem.value = itemValue - - // ! Do this after the x-default check! - itemNode!!.value = itemValue - } - - XMPNodeUtils.CLT_FIRST_ITEM, XMPNodeUtils.CLT_MULTIPLE_GENERIC -> { - - // Create the specific language, ignore x-default. - appendLangItem(arrayNode, normalizedSpecificLang, itemValue) - - if (specificXDefault) haveXDefault = true - } - - XMPNodeUtils.CLT_XDEFAULT -> { - - // Create the specific language, update x-default if it was the only item. - if (xdItem != null && arrayNode.getChildrenLength() == 1) - xdItem.value = itemValue - - appendLangItem(arrayNode, normalizedSpecificLang, itemValue) - } - - else -> // does not happen under normal circumstances - throw XMPException("Unexpected result from ChooseLocalizedText", XMPError.INTERNALFAILURE) - } - - // Add an x-default at the front if needed. - if (!haveXDefault && arrayNode.getChildrenLength() == 1) - appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue) - } - - override fun getProperty(schemaNS: String, propName: String): XMPProperty? = - getProperty(schemaNS, propName, VALUE_STRING) - - /** - * Returns a property, but the result value can be requested. It can be one - * of [XMPMetaImpl.VALUE_STRING], [XMPMetaImpl.VALUE_BOOLEAN], - * [XMPMetaImpl.VALUE_INTEGER], [XMPMetaImpl.VALUE_LONG], - * [XMPMetaImpl.VALUE_DOUBLE], [XMPMetaImpl.VALUE_DATE], - * [XMPMetaImpl.VALUE_TIME_IN_MILLIS], [XMPMetaImpl.VALUE_BASE64]. - */ - private fun getProperty(schemaNS: String, propName: String, valueType: Int): XMPProperty? { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val propNode = findNode( - xmpTree = this.root, - xpath = expandXPath(schemaNS, propName), - createNodes = false, - leafOptions = null - ) ?: return null - - if (valueType != VALUE_STRING && propNode.options.isCompositeProperty()) - throw XMPException("Property must be simple when a value type is requested", XMPError.BADXPATH) - - val value = evaluateNodeValue(valueType, propNode) - - return object : XMPProperty { - - override fun getValue(): String? = - value?.toString() - - override fun getOptions(): PropertyOptions = - propNode.options - - override fun getLanguage(): String? = - null - - override fun toString(): String = - value.toString() - } - } - - /** - * Returns a property, but the result value can be requested. - */ - private fun getPropertyObject(schemaNS: String, propName: String, valueType: Int): Any? { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val propNode = findNode( - xmpTree = this.root, - xpath = expandXPath(schemaNS, propName), - createNodes = false, - leafOptions = null - ) ?: return null - - if (valueType != VALUE_STRING && propNode.options.isCompositeProperty()) - throw XMPException("Property must be simple when a value type is requested", XMPError.BADXPATH) - - return evaluateNodeValue(valueType, propNode) - } - - override fun getPropertyBoolean(schemaNS: String, propName: String): Boolean? = - getPropertyObject(schemaNS, propName, VALUE_BOOLEAN) as? Boolean - - override fun setPropertyBoolean( - schemaNS: String, - propName: String, - propValue: Boolean, - options: PropertyOptions - ) { - setProperty( - schemaNS, - propName, - if (propValue) XMPConst.TRUE_STRING else XMPConst.FALSE_STRING, - options - ) - } - - override fun getPropertyInteger(schemaNS: String, propName: String): Int? = - getPropertyObject(schemaNS, propName, VALUE_INTEGER) as? Int - - override fun setPropertyInteger( - schemaNS: String, - propName: String, - propValue: Int, - options: PropertyOptions - ) { - setProperty(schemaNS, propName, propValue, options) - } - - override fun getPropertyLong(schemaNS: String, propName: String): Long? = - getPropertyObject(schemaNS, propName, VALUE_LONG) as? Long - - override fun setPropertyLong( - schemaNS: String, - propName: String, - propValue: Long, - options: PropertyOptions - ) { - setProperty(schemaNS, propName, propValue, options) - } - - override fun getPropertyDouble(schemaNS: String, propName: String): Double? = - getPropertyObject(schemaNS, propName, VALUE_DOUBLE) as? Double - - override fun setPropertyDouble( - schemaNS: String, - propName: String, - propValue: Double, - options: PropertyOptions - ) { - setProperty(schemaNS, propName, propValue, options) - } - - override fun getPropertyBase64(schemaNS: String, propName: String): ByteArray? = - getPropertyObject(schemaNS, propName, VALUE_BASE64) as? ByteArray - - override fun getPropertyString(schemaNS: String, propName: String): String? = - getPropertyObject(schemaNS, propName, VALUE_STRING) as? String - - override fun setPropertyBase64( - schemaNS: String, - propName: String, - propValue: ByteArray, - options: PropertyOptions - ) { - setProperty(schemaNS, propName, propValue, options) - } - - override fun getQualifier( - schemaNS: String, - propName: String, - qualNS: String, - qualName: String - ): XMPProperty? { - - // qualNS and qualName are checked inside composeQualfierPath - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val qualPath = propName + composeQualifierPath(qualNS, qualName) - - return getProperty(schemaNS, qualPath) - } - - override fun getStructField( - schemaNS: String, - structName: String, - fieldNS: String, - fieldName: String - ): XMPProperty? { - - // fieldNS and fieldName are checked inside composeStructFieldPath - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) - - return getProperty(schemaNS, fieldPath) - } - - override fun iterator(): XMPIterator = - iterator(IteratorOptions()) - - override fun iterator(options: IteratorOptions): com.ashampoo.xmp.XMPIterator = - iterator(null, null, options) - - override fun iterator( - schemaNS: String?, - propName: String?, - options: IteratorOptions - ): XMPIterator = - XMPIteratorImpl(this, schemaNS, propName, options) - - override fun setArrayItem( - schemaNS: String, - arrayName: String, - itemIndex: Int, - itemValue: String, - options: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - // Just lookup, don't try to create. - val arrayPath = expandXPath(schemaNS, arrayName) - val arrayNode = findNode(this.root, arrayPath, false, null) - - if (arrayNode == null) - throw XMPException("Specified array does not exist", XMPError.BADXPATH) - - doSetArrayItem(arrayNode, itemIndex, itemValue, options, false) - } - - override fun insertArrayItem( - schemaNS: String, - arrayName: String, - itemIndex: Int, - itemValue: String, - options: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - // Just lookup, don't try to create. - val arrayPath = expandXPath(schemaNS, arrayName) - val arrayNode = findNode(this.root, arrayPath, false, null) - - if (arrayNode == null) - throw XMPException("Specified array does not exist", XMPError.BADXPATH) - - doSetArrayItem(arrayNode, itemIndex, itemValue, options, true) - } - - override fun setProperty( - schemaNS: String, - propName: String, - propValue: Any?, - options: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - val verifiedOptions = verifySetOptions(options, propValue) - - val propNode = findNode( - xmpTree = this.root, - xpath = expandXPath(schemaNS, propName), - createNodes = true, - leafOptions = verifySetOptions(options, propValue) - ) ?: throw XMPException("Specified property does not exist", XMPError.BADXPATH) - - setNode(propNode, propValue, verifiedOptions, false) - } - - override fun setQualifier( - schemaNS: String, - propName: String, - qualNS: String, - qualName: String, - qualValue: String, - options: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) - - if (!doesPropertyExist(schemaNS, propName)) - throw XMPException("Specified property does not exist!", XMPError.BADXPATH) - - val qualPath = propName + composeQualifierPath(qualNS, qualName) - - setProperty(schemaNS, qualPath, qualValue, options) - } - - override fun setStructField( - schemaNS: String, - structName: String, - fieldNS: String, - fieldName: String, - fieldValue: String?, - options: PropertyOptions - ) { - - if (schemaNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) - - val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) - - setProperty(schemaNS, fieldPath, fieldValue, options) - } - - override fun getObjectName(): String = - root.name ?: "" - - override fun setObjectName(name: String) { - root.name = name - } - - override fun getPacketHeader(): String? = - packetHeader - - /** - * Sets the packetHeader attributes, only used by the parser. - */ - fun setPacketHeader(packetHeader: String?) { - this.packetHeader = packetHeader - } - - override fun sort() { - this.root.sort() - } - - override fun normalize(options: ParseOptions) { - normalize(this, options) - } - - override fun printAllToConsole() { - - val iterator: XMPIterator = iterator() - - while (iterator.hasNext()) { - - val propertyInfo = iterator.next() as? XMPPropertyInfo ?: continue - - println("${propertyInfo.getPath()} = ${propertyInfo.getValue()}") - } - } - - // ------------------------------------------------------------------------------------- - // 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. - */ - private fun doSetArrayItem( - arrayNode: XMPNode, - itemIndex: Int, - itemValue: String, - itemOptions: PropertyOptions, - insert: Boolean - ) { - - val itemNode = XMPNode(XMPConst.ARRAY_ITEM_NAME, null) - - val verifiedItemOptions = verifySetOptions(itemOptions, itemValue) - - // in insert mode the index after the last is allowed, - // even ARRAY_LAST_ITEM points to the index *after* the last. - val maxIndex = if (insert) - arrayNode.getChildrenLength() + 1 - else - arrayNode.getChildrenLength() - - val limitedItemIndex = if (itemIndex == XMPConst.ARRAY_LAST_ITEM) - maxIndex - else - itemIndex - - if (1 <= limitedItemIndex && limitedItemIndex <= maxIndex) { - - if (!insert) - arrayNode.removeChild(limitedItemIndex) - - arrayNode.addChild(limitedItemIndex, itemNode) - setNode(itemNode, itemValue, verifiedItemOptions, false) - - } else { - throw XMPException("Array index out of bounds", XMPError.BADINDEX) - } - } - - /** - * The internals for setProperty() and related calls, used after the node is found or created. - */ - private fun setNode(node: XMPNode, value: Any?, newOptions: PropertyOptions, deleteExisting: Boolean) { - - val compositeMask = PropertyOptions.ARRAY or PropertyOptions.ARRAY_ALT_TEXT or - PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ORDERED or PropertyOptions.STRUCT - - if (deleteExisting) - node.clear() - - // its checked by setOptions(), if the merged result is a valid options set - node.options.mergeWith(newOptions) - - if (node.options.getOptions() and compositeMask == 0) { - - // This is setting the value of a leaf node. - setNodeValue(node, value) - - } else { - - if (value != null && value.toString().isNotEmpty()) - throw XMPException("Composite nodes can't have values", XMPError.BADXPATH) - - // Can't change an array to a struct, or vice versa. - if (node.options.getOptions() and compositeMask != 0 && - newOptions.getOptions() and compositeMask != node.options.getOptions() and compositeMask - ) - throw XMPException("Requested and existing composite form mismatch", XMPError.BADXPATH) - - node.removeChildren() - } - } - - /** - * Evaluates a raw node value to the given value type, apply special - * conversions for defined types in XMP. - */ - private fun evaluateNodeValue(valueType: Int, propNode: XMPNode): Any? { - - val value: Any? - val rawValue = propNode.value - - value = when (valueType) { - - VALUE_BOOLEAN -> convertToBoolean(rawValue) - - VALUE_INTEGER -> convertToInteger(rawValue) - - VALUE_LONG -> convertToLong(rawValue) - - VALUE_DOUBLE -> convertToDouble(rawValue) - - VALUE_BASE64 -> decodeBase64(rawValue!!) - - // 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_STRING -> - if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" - - else -> - if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" - } - - return value - } - - companion object { - - /** - * Property values are Strings by default - */ - - private const val VALUE_STRING = 0 - private const val VALUE_BOOLEAN = 1 - private const val VALUE_INTEGER = 2 - private const val VALUE_LONG = 3 - private const val VALUE_DOUBLE = 4 - private const val VALUE_BASE64 = 7 - } -} diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt index 16e8b4e..8a060c6 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt @@ -70,7 +70,7 @@ internal object XMPMetaParser { } else { /* No appropriate root node found, return empty metadata object */ - XMPMetaImpl() + XMPMeta() } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt index d5bb91c..43281e5 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt @@ -34,7 +34,7 @@ internal object XMPNormalizer { * */ @kotlin.jvm.JvmStatic - fun normalize(xmp: XMPMetaImpl, options: ParseOptions): XMPMeta { + fun normalize(xmp: XMPMeta, options: ParseOptions): XMPMeta { val tree = xmp.root @@ -88,7 +88,7 @@ internal object XMPNormalizer { /** * Visit all schemas to do general fixes and handle special cases. */ - private fun touchUpDataModel(xmp: XMPMetaImpl) { + private fun touchUpDataModel(xmp: XMPMeta) { // make sure the DC schema is existing, because it might be needed within the normalization // if not touched it will be removed by removeEmptySchemas diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt index fc89b8d..8aec68a 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt @@ -11,6 +11,7 @@ package com.ashampoo.xmp.impl import com.ashampoo.xmp.XMPConst import com.ashampoo.xmp.XMPError import com.ashampoo.xmp.XMPException +import com.ashampoo.xmp.XMPMeta import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry import com.ashampoo.xmp.options.ParseOptions import com.ashampoo.xmp.options.PropertyOptions @@ -106,9 +107,9 @@ internal object XMPRDFParser { * */ @kotlin.jvm.JvmStatic - fun parse(xmlRoot: Node, options: ParseOptions): XMPMetaImpl { + fun parse(xmlRoot: Node, options: ParseOptions): XMPMeta { - val xmp = XMPMetaImpl() + val xmp = XMPMeta() parseRdfRoot(xmp, xmlRoot, options) @@ -121,7 +122,7 @@ internal object XMPRDFParser { * They simply return for success, failures will throw an exception. */ @Suppress("ThrowsCount") - fun parseRdfRoot(xmp: XMPMetaImpl, rdfRdfNode: Node, options: ParseOptions) { + fun parseRdfRoot(xmp: XMPMeta, rdfRdfNode: Node, options: ParseOptions) { if (rdfRdfNode.nodeName != "rdf:RDF") throw XMPException("Root node should be of type rdf:RDF", XMPError.BADRDF) @@ -158,7 +159,7 @@ internal object XMPRDFParser { * term. */ private fun parseRdfNodeElement( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean, @@ -192,7 +193,7 @@ internal object XMPRDFParser { */ @Suppress("ThrowsCount") private fun parseRdfNodeElementAttrs( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean @@ -258,7 +259,7 @@ internal object XMPRDFParser { * */ private fun parseRdfPropertyElementList( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlParent: Node?, isTopLevel: Boolean, @@ -334,7 +335,7 @@ internal object XMPRDFParser { * */ private fun parseRdfPropertyElement( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean, @@ -454,7 +455,7 @@ internal object XMPRDFParser { * properties written with rdf:Description and rdf:value. */ private fun parseRdfResourcePropertyElement( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean, @@ -562,7 +563,7 @@ internal object XMPRDFParser { * Add a leaf node with the text value and qualifiers for the attributes. */ private fun parseRdfLiteralPropertyElement( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean @@ -615,7 +616,7 @@ internal object XMPRDFParser { * Then process the XML child nodes to get the struct fields. */ private fun parseTypeResourcePropertyElement( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean, @@ -688,7 +689,7 @@ internal object XMPRDFParser { * 4. Otherwise this is a struct, the attributes other than xml:lang, rdf:ID, or rdf:nodeID are fields. */ private fun parseEmptyPropertyElement( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Element, isTopLevel: Boolean @@ -846,7 +847,7 @@ internal object XMPRDFParser { } private fun addChildNode( - xmp: XMPMetaImpl, + xmp: XMPMeta, xmpParent: XMPNode, xmlNode: Node, value: String?, diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt index 8e3c6bd..7057450 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt @@ -11,6 +11,7 @@ package com.ashampoo.xmp.impl import com.ashampoo.xmp.XMPConst import com.ashampoo.xmp.XMPError import com.ashampoo.xmp.XMPException +import com.ashampoo.xmp.XMPMeta import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry import com.ashampoo.xmp.XMPMetaFactory.versionInfo import com.ashampoo.xmp.impl.Utils.escapeXML @@ -22,7 +23,7 @@ import com.ashampoo.xmp.options.SerializeOptions */ @Suppress("TooManyFunctions") internal class XMPRDFWriter( - val xmp: XMPMetaImpl, + val xmp: XMPMeta, val options: SerializeOptions ) { From b96ac574b43363d84d977aae149d8d1059ad5bdb Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 11:07:55 +0100 Subject: [PATCH 03/17] Removed duplicated literal "Empty property name" --- .../kotlin/com/ashampoo/xmp/XMPError.kt | 3 +- .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 42 +++++++++---------- .../xmp/impl/XMPSchemaRegistryImpl.kt | 4 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt index c098b84..fcae525 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPError.kt @@ -10,8 +10,9 @@ package com.ashampoo.xmp object XMPError { - const val EMPTY_ARRAY_NAME: String = "Empty array name" + const val EMPTY_ARRAY_NAME_TEXT: String = "Empty array name" const val EMPTY_SCHEMA_TEXT: String = "Empty schema namespace URI" + const val EMPTY_PROPERTY_NAME_TEXT: String = "Empty property name" const val EMPTY_CONVERT_STRING_TEXT: String = "Empty convert-string" const val UNKNOWN: Int = 0 diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index a72a906..f3a5a63 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -111,7 +111,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val propNode = findNode( xmpTree = this.root, @@ -161,7 +161,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val itemPath = composeArrayItemPath(arrayName, itemIndex) @@ -183,7 +183,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val arrayPath = expandXPath(schemaNS, arrayName) val arrayNode = findNode(this.root, arrayPath, false, null) ?: return 0 @@ -229,7 +229,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) @@ -278,7 +278,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val qualPath = propName + composeQualifierPath(qualNS, qualName) @@ -325,7 +325,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val verifiedOptions = verifySetOptions(options, propValue) @@ -368,7 +368,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) // Just lookup, don't try to create. val arrayPath = expandXPath(schemaNS, arrayName) @@ -408,7 +408,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) // Just lookup, don't try to create. val arrayPath = expandXPath(schemaNS, arrayName) @@ -456,7 +456,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) if (!arrayOptions.isOnlyArrayOptions()) throw XMPException("Only array form flags allowed for arrayOptions", XMPError.BADOPTIONS) @@ -528,7 +528,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) @@ -571,7 +571,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) if (!doesPropertyExist(schemaNS, propName)) throw XMPException("Specified property does not exist!", XMPError.BADXPATH) @@ -628,7 +628,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val itemPath = composeArrayItemPath(arrayName, itemIndex) @@ -662,7 +662,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val fieldPath = structName + composeStructFieldPath(fieldNS, fieldName) @@ -689,7 +689,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val qualPath = propName + composeQualifierPath(qualNS, qualName) @@ -709,7 +709,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val propNode = findNode( xmpTree = this.root, @@ -739,7 +739,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (arrayName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val path = composeArrayItemPath(arrayName, itemIndex) @@ -773,7 +773,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (structName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) val path = composeStructFieldPath(fieldNS, fieldName) @@ -806,7 +806,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val path = composeQualifierPath(qualNS, qualName) @@ -884,7 +884,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (altTextName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) if (specificLang.isEmpty()) throw XMPException("Empty specific language", XMPError.BADPARAM) @@ -964,7 +964,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (altTextName.isEmpty()) - throw XMPException(XMPError.EMPTY_ARRAY_NAME, XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_ARRAY_NAME_TEXT, XMPError.BADPARAM) if (specificLang.isEmpty()) throw XMPException("Empty specific language", XMPError.BADPARAM) @@ -1185,7 +1185,7 @@ class XMPMeta { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (propName.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) val propNode = findNode( xmpTree = this.root, diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt index 51077a3..cb05dad 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt @@ -288,13 +288,13 @@ object XMPSchemaRegistryImpl : XMPSchemaRegistry { throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (aliasProp.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) if (actualNS.isEmpty()) throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) if (actualProp.isEmpty()) - throw XMPException("Empty property name", XMPError.BADPARAM) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) // Fix the alias options val aliasOpts = if (aliasForm != null) From 4e851f201baa16d6839968b703e8abff46e5ea0d Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 11:11:14 +0100 Subject: [PATCH 04/17] Default value for PropertyOptions restored --- .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index f3a5a63..f25ee31 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -318,7 +318,7 @@ class XMPMeta { schemaNS: String, propName: String, propValue: Any?, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -361,7 +361,7 @@ class XMPMeta { arrayName: String, itemIndex: Int, itemValue: String, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -401,7 +401,7 @@ class XMPMeta { arrayName: String, itemIndex: Int, itemValue: String, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -447,9 +447,9 @@ class XMPMeta { fun appendArrayItem( schemaNS: String, arrayName: String, - arrayOptions: PropertyOptions, + arrayOptions: PropertyOptions = PropertyOptions(), itemValue: String, - itemOptions: PropertyOptions + itemOptions: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -521,7 +521,7 @@ class XMPMeta { fieldNS: String, fieldName: String, fieldValue: String?, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -564,7 +564,7 @@ class XMPMeta { qualNS: String, qualName: String, qualValue: String, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -957,7 +957,7 @@ class XMPMeta { genericLang: String?, specificLang: String, itemValue: String, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { if (schemaNS.isEmpty()) @@ -1212,7 +1212,7 @@ class XMPMeta { schemaNS: String, propName: String, propValue: Boolean, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { setProperty( schemaNS, @@ -1229,13 +1229,12 @@ class XMPMeta { * @param propName The name of the property. Has the same usage as in `getProperty()`. * @param propValue the literal property value as `int`. * @param options options of the property to set (optional). - * */ fun setPropertyInteger( schemaNS: String, propName: String, propValue: Int, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { setProperty(schemaNS, propName, propValue, options) } @@ -1252,7 +1251,7 @@ class XMPMeta { schemaNS: String, propName: String, propValue: Long, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { setProperty(schemaNS, propName, propValue, options) } @@ -1269,7 +1268,7 @@ class XMPMeta { schemaNS: String, propName: String, propValue: Double, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { setProperty(schemaNS, propName, propValue, options) } @@ -1287,7 +1286,7 @@ class XMPMeta { schemaNS: String, propName: String, propValue: ByteArray, - options: PropertyOptions + options: PropertyOptions = PropertyOptions() ) { setProperty(schemaNS, propName, propValue, options) } @@ -1411,7 +1410,7 @@ class XMPMeta { arrayNode: XMPNode, itemIndex: Int, itemValue: String, - itemOptions: PropertyOptions, + itemOptions: PropertyOptions = PropertyOptions(), insert: Boolean ) { @@ -1447,7 +1446,12 @@ class XMPMeta { /** * The internals for setProperty() and related calls, used after the node is found or created. */ - private fun setNode(node: XMPNode, value: Any?, newOptions: PropertyOptions, deleteExisting: Boolean) { + private fun setNode( + node: XMPNode, + value: Any?, + newOptions: PropertyOptions, + deleteExisting: Boolean + ) { val compositeMask = PropertyOptions.ARRAY or PropertyOptions.ARRAY_ALT_TEXT or PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ORDERED or PropertyOptions.STRUCT From 218a17376a340f20dc679ea28a73ca0e6bebdd07 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 11:24:08 +0100 Subject: [PATCH 05/17] @Suppress("TooManyFunctions") --- src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index f25ee31..0bbf083 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -39,6 +39,7 @@ import com.ashampoo.xmp.properties.XMPPropertyInfo * modify all kinds of properties, create an iterator over all properties and serialize the metadata * to a String. */ +@Suppress("TooManyFunctions") class XMPMeta { /** @@ -56,7 +57,6 @@ class XMPMeta { * Constructor for an empty metadata object. */ constructor() { - // create root node this.root = XMPNode(null, null, PropertyOptions()) } From 8d62cae36bf9180bf1fea84928c5a79498982551 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 12:08:03 +0100 Subject: [PATCH 06/17] Moved private methods to better places --- .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 237 +++++++++--------- 1 file changed, 117 insertions(+), 120 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index 0bbf083..2ae1862 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -141,6 +141,40 @@ class XMPMeta { } } + /** + * Evaluates a raw node value to the given value type, apply special + * conversions for defined types in XMP. + */ + private fun evaluateNodeValue(valueType: Int, propNode: XMPNode): Any? { + + val value: Any? + val rawValue = propNode.value + + value = when (valueType) { + + VALUE_BOOLEAN -> convertToBoolean(rawValue) + + VALUE_INTEGER -> convertToInteger(rawValue) + + VALUE_LONG -> convertToLong(rawValue) + + VALUE_DOUBLE -> convertToDouble(rawValue) + + VALUE_BASE64 -> decodeBase64(rawValue!!) + + // 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_STRING -> + if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" + + else -> + if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" + } + + return value + } + /** * 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. @@ -339,6 +373,45 @@ class XMPMeta { setNode(propNode, propValue, verifiedOptions, false) } + /** + * The internals for setProperty() and related calls, used after the node is found or created. + */ + private fun setNode( + node: XMPNode, + value: Any?, + newOptions: PropertyOptions, + deleteExisting: Boolean + ) { + + val compositeMask = PropertyOptions.ARRAY or PropertyOptions.ARRAY_ALT_TEXT or + PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ORDERED or PropertyOptions.STRUCT + + if (deleteExisting) + node.clear() + + // its checked by setOptions(), if the merged result is a valid options set + node.options.mergeWith(newOptions) + + if (node.options.getOptions() and compositeMask == 0) { + + // This is setting the value of a leaf node. + setNodeValue(node, value) + + } else { + + if (value != null && value.toString().isNotEmpty()) + throw XMPException("Composite nodes can't have values", XMPError.BADXPATH) + + // Can't change an array to a struct, or vice versa. + if (node.options.getOptions() and compositeMask != 0 && + newOptions.getOptions() and compositeMask != node.options.getOptions() and compositeMask + ) + throw XMPException("Requested and existing composite form mismatch", XMPError.BADXPATH) + + node.removeChildren() + } + } + /** * 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 @@ -497,6 +570,50 @@ class XMPMeta { doSetArrayItem(arrayNode, XMPConst.ARRAY_LAST_ITEM, itemValue, itemOptions, true) } + /** + * 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. + */ + private fun doSetArrayItem( + arrayNode: XMPNode, + itemIndex: Int, + itemValue: String, + itemOptions: PropertyOptions = PropertyOptions(), + insert: Boolean + ) { + + val itemNode = XMPNode(XMPConst.ARRAY_ITEM_NAME, null) + + val verifiedItemOptions = verifySetOptions(itemOptions, itemValue) + + // in insert mode the index after the last is allowed, + // even ARRAY_LAST_ITEM points to the index *after* the last. + val maxIndex = if (insert) + arrayNode.getChildrenLength() + 1 + else + arrayNode.getChildrenLength() + + val limitedItemIndex = if (itemIndex == XMPConst.ARRAY_LAST_ITEM) + maxIndex + else + itemIndex + + if (1 <= limitedItemIndex && limitedItemIndex <= maxIndex) { + + if (!insert) + arrayNode.removeChild(limitedItemIndex) + + arrayNode.addChild(limitedItemIndex, itemNode) + setNode(itemNode, itemValue, verifiedItemOptions, false) + + } else { + throw XMPException("Array index out of bounds", XMPError.BADINDEX) + } + } + /** * 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 @@ -1396,126 +1513,6 @@ class XMPMeta { } } - // ------------------------------------------------------------------------------------- - // 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. - */ - private fun doSetArrayItem( - arrayNode: XMPNode, - itemIndex: Int, - itemValue: String, - itemOptions: PropertyOptions = PropertyOptions(), - insert: Boolean - ) { - - val itemNode = XMPNode(XMPConst.ARRAY_ITEM_NAME, null) - - val verifiedItemOptions = verifySetOptions(itemOptions, itemValue) - - // in insert mode the index after the last is allowed, - // even ARRAY_LAST_ITEM points to the index *after* the last. - val maxIndex = if (insert) - arrayNode.getChildrenLength() + 1 - else - arrayNode.getChildrenLength() - - val limitedItemIndex = if (itemIndex == XMPConst.ARRAY_LAST_ITEM) - maxIndex - else - itemIndex - - if (1 <= limitedItemIndex && limitedItemIndex <= maxIndex) { - - if (!insert) - arrayNode.removeChild(limitedItemIndex) - - arrayNode.addChild(limitedItemIndex, itemNode) - setNode(itemNode, itemValue, verifiedItemOptions, false) - - } else { - throw XMPException("Array index out of bounds", XMPError.BADINDEX) - } - } - - /** - * The internals for setProperty() and related calls, used after the node is found or created. - */ - private fun setNode( - node: XMPNode, - value: Any?, - newOptions: PropertyOptions, - deleteExisting: Boolean - ) { - - val compositeMask = PropertyOptions.ARRAY or PropertyOptions.ARRAY_ALT_TEXT or - PropertyOptions.ARRAY_ALTERNATE or PropertyOptions.ARRAY_ORDERED or PropertyOptions.STRUCT - - if (deleteExisting) - node.clear() - - // its checked by setOptions(), if the merged result is a valid options set - node.options.mergeWith(newOptions) - - if (node.options.getOptions() and compositeMask == 0) { - - // This is setting the value of a leaf node. - setNodeValue(node, value) - - } else { - - if (value != null && value.toString().isNotEmpty()) - throw XMPException("Composite nodes can't have values", XMPError.BADXPATH) - - // Can't change an array to a struct, or vice versa. - if (node.options.getOptions() and compositeMask != 0 && - newOptions.getOptions() and compositeMask != node.options.getOptions() and compositeMask - ) - throw XMPException("Requested and existing composite form mismatch", XMPError.BADXPATH) - - node.removeChildren() - } - } - - /** - * Evaluates a raw node value to the given value type, apply special - * conversions for defined types in XMP. - */ - private fun evaluateNodeValue(valueType: Int, propNode: XMPNode): Any? { - - val value: Any? - val rawValue = propNode.value - - value = when (valueType) { - - VALUE_BOOLEAN -> convertToBoolean(rawValue) - - VALUE_INTEGER -> convertToInteger(rawValue) - - VALUE_LONG -> convertToLong(rawValue) - - VALUE_DOUBLE -> convertToDouble(rawValue) - - VALUE_BASE64 -> decodeBase64(rawValue!!) - - // 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_STRING -> - if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" - - else -> - if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else "" - } - - return value - } - companion object { /** From 1caa69e40834d606168956922705b0f6a3fa5e81 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 14:01:13 +0100 Subject: [PATCH 07/17] Added convenience methods for common properties --- README.md | 2 +- .../kotlin/com/ashampoo/xmp/XMPConst.kt | 2 + .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 235 ++++++++++++++++++ .../kotlin/com/ashampoo/xmp/XMPRegionArea.kt | 17 ++ 4 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/commonMain/kotlin/com/ashampoo/xmp/XMPRegionArea.kt diff --git a/README.md b/README.md index 050a49e..d781c40 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It's part of [Ashampoo Photos](https://ashampoo.com/photos). ## Installation ``` -implementation("com.ashampoo:xmpcore:0.1.7") +implementation("com.ashampoo:xmpcore:0.2") ``` ## How to use diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt index c708613..c69ed5b 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt @@ -290,6 +290,8 @@ object XMPConst { const val XMP_DC_SUBJECT: String = "subject" + const val XMP_ACDSEE_KEYWORDS: String = "keywords" + const val XMP_IPTC_EXT_PERSON_IN_IMAGE: String = "PersonInImage" } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index 2ae1862..1c70564 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -53,6 +53,8 @@ class XMPMeta { */ private var packetHeader: String? = null + private val arrayOptions = PropertyOptions().setArray(true) + /** * Constructor for an empty metadata object. */ @@ -1513,6 +1515,239 @@ class XMPMeta { } } + /* + * Convenience methods for commonly used fields + * + * Note that these are not standard API for XMP Core. + * This was added by Ashampoo. + */ + + /** Returns the ISO date string */ + fun getDateTimeOriginal(): String? = + getPropertyString(XMPConst.NS_EXIF, "DateTimeOriginal") + + fun setDateTimeOriginal(isoDate: String) = + setProperty(XMPConst.NS_EXIF, "DateTimeOriginal", isoDate) + + fun deleteDateTimeOriginal() = + deleteProperty(XMPConst.NS_EXIF, "DateTimeOriginal") + + fun getOrientation(): Int? = + getPropertyInteger(XMPConst.NS_TIFF, "Orientation") + + fun setOrientation(orientation: Int) = + setPropertyInteger(XMPConst.NS_TIFF, "Orientation", orientation) + + fun getRating(): Int? = + getPropertyInteger(XMPConst.NS_XMP, "Rating") + + fun setRating(rating: Int) = + setPropertyInteger(XMPConst.NS_XMP, "Rating", rating) + + fun getGpsLatitude(): String? = + getPropertyString(XMPConst.NS_EXIF, "GPSLatitude") + + fun getGpsLongitude(): String? = + getPropertyString(XMPConst.NS_EXIF, "GPSLongitude") + + fun setGpsCoordinates( + latitudeDdm: String, + longitudeDdm: String + ) { + + /* This was a mandatory flag in the past, so we write it. */ + setProperty(XMPConst.NS_EXIF, "GPSVersionID", XMPConst.DEFAULT_GPS_VERSION_ID) + + setProperty(XMPConst.NS_EXIF, "GPSLatitude", latitudeDdm) + setProperty(XMPConst.NS_EXIF, "GPSLongitude", longitudeDdm) + } + + fun deleteGpsCoordinates() { + + deleteProperty(XMPConst.NS_EXIF, "GPSVersionID") + deleteProperty(XMPConst.NS_EXIF, "GPSLatitude") + deleteProperty(XMPConst.NS_EXIF, "GPSLongitude") + } + + /** + * Gets the regular keywords specified by XMP standard. + */ + fun getKeywords(): Set { + + val subjectCount = countArrayItems(XMPConst.NS_DC, XMPConst.XMP_DC_SUBJECT) + + if (subjectCount == 0) + return emptySet() + + val keywords = mutableSetOf() + + for (index in 1..subjectCount) { + + val keyword = getPropertyString( + XMPConst.NS_DC, + "${XMPConst.XMP_DC_SUBJECT}[$index]" + ) ?: continue + + keywords.add(keyword) + } + + return keywords + } + + fun setKeywords(keywords: Set) { + + /* Delete existing entries, if any */ + deleteProperty(XMPConst.NS_DC, XMPConst.XMP_DC_SUBJECT) + + if (keywords.isEmpty()) + return + + /* Create a new array property. */ + setProperty( + XMPConst.NS_DC, + XMPConst.XMP_DC_SUBJECT, + null, + arrayOptions + ) + + /* Fill the new array with keywords. */ + for (keyword in keywords.sorted()) + appendArrayItem( + schemaNS = XMPConst.NS_DC, + arrayName = XMPConst.XMP_DC_SUBJECT, + itemValue = keyword + ) + } + + /** + * Gets ACDSee keywords from the ACDSee namespace. + * This can be used as an alternative if the regular keyword property is empty. + */ + fun getAcdSeeKeywords(): Set { + + val propertyExists = doesPropertyExist(XMPConst.NS_ACDSEE, XMPConst.XMP_ACDSEE_KEYWORDS) + + if (!propertyExists) + return emptySet() + + val keywordCount = countArrayItems(XMPConst.NS_ACDSEE, XMPConst.XMP_ACDSEE_KEYWORDS) + + if (keywordCount == 0) + return emptySet() + + val keywords = mutableSetOf() + + for (index in 1..keywordCount) { + + val keyword = getPropertyString( + XMPConst.NS_ACDSEE, + "${XMPConst.XMP_ACDSEE_KEYWORDS}[$index]" + ) ?: continue + + keywords.add(keyword) + } + + return keywords + } + + fun getFaces(): Map { + + val regionListExists = doesPropertyExist(XMPConst.NS_MWG_RS, "Regions/mwg-rs:RegionList") + + if (!regionListExists) + return emptyMap() + + val regionCount = countArrayItems(XMPConst.NS_MWG_RS, "Regions/mwg-rs:RegionList") + + if (regionCount == 0) + return emptyMap() + + val faces = mutableMapOf() + + for (index in 1..regionCount) { + + val prefix = "Regions/mwg-rs:RegionList[$index]/mwg-rs" + + val regionType = getPropertyString(XMPConst.NS_MWG_RS, "$prefix:Type") + + /* We only want faces. */ + if (regionType != "Face") + continue + + val name = getPropertyString(XMPConst.NS_MWG_RS, "$prefix:Name") + val xPos = getPropertyDouble(XMPConst.NS_MWG_RS, "$prefix:Area/stArea:x") + val yPos = getPropertyDouble(XMPConst.NS_MWG_RS, "$prefix:Area/stArea:y") + val width = getPropertyDouble(XMPConst.NS_MWG_RS, "$prefix:Area/stArea:w") + val height = getPropertyDouble(XMPConst.NS_MWG_RS, "$prefix:Area/stArea:h") + + /* Skip regions with missing data. */ + @Suppress("ComplexCondition") + if (name == null || xPos == null || yPos == null || width == null || height == null) + continue + + faces[name] = XMPRegionArea(xPos, yPos, width, height) + } + + return faces + } + +// fun setFaces(faces: Map) { +// +// /* Delete existing entries, if any */ +// deleteProperty(NS_MWG_RS, "Regions") +// +// // TODO Write faces +// } + + fun getPersonsInImage(): Set { + + val personsInImageCount = + countArrayItems(XMPConst.NS_IPTC_EXT, XMPConst.XMP_IPTC_EXT_PERSON_IN_IMAGE) + + if (personsInImageCount == 0) + return emptySet() + + val personsInImage = mutableSetOf() + + for (index in 1..personsInImageCount) { + + val personName = + getPropertyString( + XMPConst.NS_IPTC_EXT, + "${XMPConst.XMP_IPTC_EXT_PERSON_IN_IMAGE}[$index]" + ) ?: continue + + personsInImage.add(personName) + } + + return personsInImage + } + + fun setPersonsInImage(personsInImage: Set) { + + /* Delete existing entries, if any */ + deleteProperty(XMPConst.NS_IPTC_EXT, XMPConst.XMP_IPTC_EXT_PERSON_IN_IMAGE) + + if (personsInImage.isEmpty()) + return + + /* Create a new array property. */ + setProperty( + XMPConst.NS_IPTC_EXT, + XMPConst.XMP_IPTC_EXT_PERSON_IN_IMAGE, + null, + arrayOptions + ) + + /* Fill the new array with persons. */ + for (person in personsInImage.sorted()) + appendArrayItem( + schemaNS = XMPConst.NS_IPTC_EXT, + arrayName = XMPConst.XMP_IPTC_EXT_PERSON_IN_IMAGE, + itemValue = person + ) + } + companion object { /** diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRegionArea.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRegionArea.kt new file mode 100644 index 0000000..7848e46 --- /dev/null +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRegionArea.kt @@ -0,0 +1,17 @@ +// ================================================================================================= +// 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.ashampoo.xmp + +/** As used in XMP-mwg-rs */ +data class XMPRegionArea( + val xPos: Double, + val yPos: Double, + val width: Double, + val height: Double +) From be63d1b1d15b8ed0c4453df9f4d6db58629e3fff Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 14:07:41 +0100 Subject: [PATCH 08/17] ReadXmpTest.kt & WriteXmpTest.kt use convenience methods to get unit test coverage on that. --- .../kotlin/com/ashampoo/xmp/ReadXmpTest.kt | 17 +++++---- .../kotlin/com/ashampoo/xmp/WriteXmpTest.kt | 35 ++++++++----------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt b/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt index dd2c1ec..b54ec47 100644 --- a/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt @@ -10,7 +10,7 @@ import kotlin.test.assertEquals class ReadXmpTest { @Test - fun readXmp() { + fun testReadXmp() { val testXmp = """ @@ -37,17 +37,17 @@ class ReadXmpTest { assertEquals( expected = "1980-03-15T08:15:30", - actual = xmpMeta.getPropertyString(XMPConst.NS_EXIF, "DateTimeOriginal") + actual = xmpMeta.getDateTimeOriginal() ) assertEquals( expected = "53,13.1635N", - actual = xmpMeta.getPropertyString(XMPConst.NS_EXIF, "GPSLatitude") + actual = xmpMeta.getGpsLatitude() ) assertEquals( expected = "8,14.3797E", - actual = xmpMeta.getPropertyString(XMPConst.NS_EXIF, "GPSLongitude") + actual = xmpMeta.getGpsLongitude() ) assertEquals( @@ -56,8 +56,8 @@ class ReadXmpTest { ) assertEquals( - expected = "2", - actual = xmpMeta.getPropertyString(XMPConst.NS_XMP, "Rating") + expected = 2, + actual = xmpMeta.getRating() ) assertEquals( @@ -74,5 +74,10 @@ class ReadXmpTest { expected = "swiper", actual = xmpMeta.getPropertyString(XMPConst.NS_DC, "$XMP_DC_SUBJECT[2]") ) + + assertEquals( + expected = setOf("fox", "swiper"), + actual = xmpMeta.getKeywords() + ) } } diff --git a/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt b/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt index fc588f4..e11439b 100644 --- a/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt @@ -33,7 +33,7 @@ class WriteXmpTest { */ @OptIn(ExperimentalStdlibApi::class) @Test - fun createEmptyXmp() { + fun testCreateEmptyXmp() { val xmpMeta = XMPMetaFactory.create() @@ -61,11 +61,11 @@ class WriteXmpTest { */ @OptIn(ExperimentalStdlibApi::class) @Test - fun createRatingXmp() { + fun testCreateRatingXmp() { val xmpMeta = XMPMetaFactory.create() - xmpMeta.setPropertyInteger(XMPConst.NS_XMP, "Rating", 3) + xmpMeta.setRating(3) val actualXmp = XMPMetaFactory.serializeToString(xmpMeta, xmpSerializeOptionsCompact) @@ -91,7 +91,7 @@ class WriteXmpTest { */ @OptIn(ExperimentalStdlibApi::class) @Test - fun createNewXmp() { + fun testCreateNewXmp() { val xmpMeta = XMPMetaFactory.create() @@ -121,7 +121,7 @@ class WriteXmpTest { */ @OptIn(ExperimentalStdlibApi::class) @Test - fun updateXmp() { + fun testUpdateXmp() { val existingXmp = """ @@ -171,26 +171,19 @@ class WriteXmpTest { private fun writeTestValues(xmpMeta: XMPMeta) { /* Write rating. */ - xmpMeta.setPropertyInteger(XMPConst.NS_XMP, "Rating", 3) + xmpMeta.setRating(3) /* Write taken date. */ - xmpMeta.setProperty(XMPConst.NS_EXIF, "DateTimeOriginal", "2023-07-07T13:37:42") + xmpMeta.setDateTimeOriginal("2023-07-07T13:37:42") /* Write GPS coordinates. */ - xmpMeta.setProperty(XMPConst.NS_EXIF, "GPSVersionID", DEFAULT_GPS_VERSION_ID) - xmpMeta.setProperty(XMPConst.NS_EXIF, "GPSLatitude", "53,13.1635N") - xmpMeta.setProperty(XMPConst.NS_EXIF, "GPSLongitude", "8,14.3797E") - - /* Create a new array property for keywords. */ - xmpMeta.setProperty(XMPConst.NS_DC, XMP_DC_SUBJECT, null, arrayOptions) - - /* Fill the new array with keywords. */ - for (keyword in listOf("bird", "cat", "dog")) - xmpMeta.appendArrayItem( - schemaNS = XMPConst.NS_DC, - arrayName = XMP_DC_SUBJECT, - itemValue = keyword - ) + + xmpMeta.setGpsCoordinates( + latitudeDdm = "53,13.1635N", + longitudeDdm = "8,14.3797E" + ) + + xmpMeta.setKeywords(setOf("bird", "cat", "dog")) } private fun getXmp(name: String): String = From 65493cbb07257026d011e7a545364c75c8581ddd Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 14:17:42 +0100 Subject: [PATCH 09/17] Removed sub package "impl" --- .../com/ashampoo/xmp/{impl => }/DomParser.kt | 4 +--- .../com/ashampoo/xmp/{impl => }/QName.kt | 2 +- .../com/ashampoo/xmp/{impl => }/Utils.kt | 4 +--- .../xmp/{impl => }/XMPIteratorImpl.kt | 19 +++++++---------- .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 21 ++++++++----------- .../kotlin/com/ashampoo/xmp/XMPMetaFactory.kt | 3 --- .../ashampoo/xmp/{impl => }/XMPMetaParser.kt | 6 ++---- .../com/ashampoo/xmp/{impl => }/XMPNode.kt | 5 +---- .../ashampoo/xmp/{impl => }/XMPNodeUtils.kt | 20 +++++++----------- .../ashampoo/xmp/{impl => }/XMPNormalizer.kt | 15 +++++-------- .../kotlin/com/ashampoo/xmp/XMPPathFactory.kt | 5 ++--- .../ashampoo/xmp/{impl => }/XMPRDFParser.kt | 13 ++++-------- .../ashampoo/xmp/{impl => }/XMPRDFWriter.kt | 16 +++++--------- .../xmp/{impl => }/XMPSchemaRegistryImpl.kt | 8 ++----- .../xmp/{impl => }/xpath/PathPosition.kt | 2 +- .../ashampoo/xmp/{impl => }/xpath/XMPPath.kt | 2 +- .../xmp/{impl => }/xpath/XMPPathParser.kt | 4 ++-- .../xmp/{impl => }/xpath/XMPPathSegment.kt | 2 +- .../com/ashampoo/xmp/{impl => }/UtilsTest.kt | 3 ++- 19 files changed, 55 insertions(+), 99 deletions(-) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/DomParser.kt (92%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/QName.kt (97%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/Utils.kt (99%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPIteratorImpl.kt (96%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPMetaParser.kt (97%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPNode.kt (98%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPNodeUtils.kt (97%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPNormalizer.kt (97%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPRDFParser.kt (98%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPRDFWriter.kt (98%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/XMPSchemaRegistryImpl.kt (98%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/xpath/PathPosition.kt (96%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/xpath/XMPPath.kt (98%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/xpath/XMPPathParser.kt (99%) rename src/commonMain/kotlin/com/ashampoo/xmp/{impl => }/xpath/XMPPathSegment.kt (97%) rename src/commonTest/kotlin/com/ashampoo/xmp/{impl => }/UtilsTest.kt (98%) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/DomParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/DomParser.kt similarity index 92% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/DomParser.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/DomParser.kt index 8cf9714..8483076 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/DomParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/DomParser.kt @@ -1,7 +1,5 @@ -package com.ashampoo.xmp.impl +package com.ashampoo.xmp -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException import nl.adaptivity.xmlutil.DomWriter import nl.adaptivity.xmlutil.EventType import nl.adaptivity.xmlutil.XmlStreaming diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/QName.kt b/src/commonMain/kotlin/com/ashampoo/xmp/QName.kt similarity index 97% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/QName.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/QName.kt index 96a2d06..e776a56 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/QName.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/QName.kt @@ -6,7 +6,7 @@ // 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.ashampoo.xmp.impl +package com.ashampoo.xmp class QName { diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/Utils.kt b/src/commonMain/kotlin/com/ashampoo/xmp/Utils.kt similarity index 99% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/Utils.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/Utils.kt index 32f8f0b..3204243 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/Utils.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/Utils.kt @@ -6,9 +6,7 @@ // 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.ashampoo.xmp.impl - -import com.ashampoo.xmp.XMPConst +package com.ashampoo.xmp /** * Utility functions for the XMPToolkit implementation. diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt similarity index 96% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt index 82a98e4..96cfb41 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPIteratorImpl.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt @@ -6,17 +6,12 @@ // 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.ashampoo.xmp.impl - -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPIterator -import com.ashampoo.xmp.XMPMeta -import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry -import com.ashampoo.xmp.impl.XMPNodeUtils.findNode -import com.ashampoo.xmp.impl.XMPNodeUtils.findSchemaNode -import com.ashampoo.xmp.impl.xpath.XMPPath -import com.ashampoo.xmp.impl.xpath.XMPPathParser.expandXPath +package com.ashampoo.xmp + +import com.ashampoo.xmp.XMPNodeUtils.findNode +import com.ashampoo.xmp.XMPNodeUtils.findSchemaNode +import com.ashampoo.xmp.xpath.XMPPath +import com.ashampoo.xmp.xpath.XMPPathParser.expandXPath import com.ashampoo.xmp.options.IteratorOptions import com.ashampoo.xmp.options.PropertyOptions import com.ashampoo.xmp.properties.XMPPropertyInfo @@ -393,7 +388,7 @@ class XMPIteratorImpl( // determine namespace of leaf node val qname = QName(node.name!!) - return schemaRegistry.getNamespaceURI(qname.prefix!!)!! + return XMPSchemaRegistryImpl.getNamespaceURI(qname.prefix!!)!! } override fun getPath(): String = path diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index 1c70564..27b13d4 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -16,18 +16,15 @@ import com.ashampoo.xmp.XMPUtils.convertToDouble import com.ashampoo.xmp.XMPUtils.convertToInteger import com.ashampoo.xmp.XMPUtils.convertToLong import com.ashampoo.xmp.XMPUtils.decodeBase64 -import com.ashampoo.xmp.impl.Utils.normalizeLangValue -import com.ashampoo.xmp.impl.XMPIteratorImpl -import com.ashampoo.xmp.impl.XMPNode -import com.ashampoo.xmp.impl.XMPNodeUtils -import com.ashampoo.xmp.impl.XMPNodeUtils.appendLangItem -import com.ashampoo.xmp.impl.XMPNodeUtils.chooseLocalizedText -import com.ashampoo.xmp.impl.XMPNodeUtils.deleteNode -import com.ashampoo.xmp.impl.XMPNodeUtils.findNode -import com.ashampoo.xmp.impl.XMPNodeUtils.setNodeValue -import com.ashampoo.xmp.impl.XMPNodeUtils.verifySetOptions -import com.ashampoo.xmp.impl.XMPNormalizer.normalize -import com.ashampoo.xmp.impl.xpath.XMPPathParser.expandXPath +import com.ashampoo.xmp.Utils.normalizeLangValue +import com.ashampoo.xmp.XMPNodeUtils.appendLangItem +import com.ashampoo.xmp.XMPNodeUtils.chooseLocalizedText +import com.ashampoo.xmp.XMPNodeUtils.deleteNode +import com.ashampoo.xmp.XMPNodeUtils.findNode +import com.ashampoo.xmp.XMPNodeUtils.setNodeValue +import com.ashampoo.xmp.XMPNodeUtils.verifySetOptions +import com.ashampoo.xmp.XMPNormalizer.normalize +import com.ashampoo.xmp.xpath.XMPPathParser.expandXPath import com.ashampoo.xmp.options.IteratorOptions import com.ashampoo.xmp.options.ParseOptions import com.ashampoo.xmp.options.PropertyOptions diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt index 5b6f8ba..fca8f2e 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt @@ -8,9 +8,6 @@ // ================================================================================================= package com.ashampoo.xmp -import com.ashampoo.xmp.impl.XMPMetaParser -import com.ashampoo.xmp.impl.XMPRDFWriter -import com.ashampoo.xmp.impl.XMPSchemaRegistryImpl import com.ashampoo.xmp.options.ParseOptions import com.ashampoo.xmp.options.SerializeOptions diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt similarity index 97% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt index 8a060c6..8e7924b 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPMetaParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt @@ -6,11 +6,9 @@ // 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.ashampoo.xmp.impl +package com.ashampoo.xmp -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPMeta -import com.ashampoo.xmp.impl.XMPNormalizer.normalize +import com.ashampoo.xmp.XMPNormalizer.normalize import com.ashampoo.xmp.options.ParseOptions import nl.adaptivity.xmlutil.dom.Element import nl.adaptivity.xmlutil.dom.Node diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNode.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNode.kt similarity index 98% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNode.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPNode.kt index 1b2afad..3c42f5b 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNode.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNode.kt @@ -6,11 +6,8 @@ // 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.ashampoo.xmp.impl +package com.ashampoo.xmp -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException import com.ashampoo.xmp.options.PropertyOptions /** diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNodeUtils.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt similarity index 97% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNodeUtils.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt index b84fd7b..daa89d2 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNodeUtils.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt @@ -6,18 +6,14 @@ // 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.ashampoo.xmp.impl +package com.ashampoo.xmp -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry import com.ashampoo.xmp.XMPUtils.encodeBase64 -import com.ashampoo.xmp.impl.Utils.normalizeLangValue -import com.ashampoo.xmp.impl.Utils.replaceControlCharsWithSpace -import com.ashampoo.xmp.impl.Utils.splitNameAndValue -import com.ashampoo.xmp.impl.xpath.XMPPath -import com.ashampoo.xmp.impl.xpath.XMPPathSegment +import com.ashampoo.xmp.Utils.normalizeLangValue +import com.ashampoo.xmp.Utils.replaceControlCharsWithSpace +import com.ashampoo.xmp.Utils.splitNameAndValue +import com.ashampoo.xmp.xpath.XMPPath +import com.ashampoo.xmp.xpath.XMPPathSegment import com.ashampoo.xmp.options.AliasOptions import com.ashampoo.xmp.options.PropertyOptions @@ -75,12 +71,12 @@ object XMPNodeUtils { schemaNode.isImplicit = true // only previously registered schema namespaces are allowed in the XMP tree. - var prefix = schemaRegistry.getNamespacePrefix(namespaceURI!!) + var prefix = XMPSchemaRegistryImpl.getNamespacePrefix(namespaceURI!!) if (prefix == null) { prefix = if (!suggestedPrefix.isNullOrEmpty()) - schemaRegistry.registerNamespace(namespaceURI, suggestedPrefix) + XMPSchemaRegistryImpl.registerNamespace(namespaceURI, suggestedPrefix) else throw XMPException("Unregistered schema namespace URI", XMPError.BADSCHEMA) } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt similarity index 97% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt index 43281e5..3150b3b 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPNormalizer.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt @@ -6,15 +6,10 @@ // 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.ashampoo.xmp.impl - -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPMeta -import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry -import com.ashampoo.xmp.impl.Utils.checkUUIDFormat -import com.ashampoo.xmp.impl.xpath.XMPPathParser.expandXPath +package com.ashampoo.xmp + +import com.ashampoo.xmp.Utils.checkUUIDFormat +import com.ashampoo.xmp.xpath.XMPPathParser.expandXPath import com.ashampoo.xmp.options.ParseOptions import com.ashampoo.xmp.options.PropertyOptions @@ -255,7 +250,7 @@ internal object XMPNormalizer { currProp.isAlias = false // Find the base path, look for the base schema and root node. - val info = schemaRegistry.findAlias(currProp.name!!) + val info = XMPSchemaRegistryImpl.findAlias(currProp.name!!) if (info != null) { diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPPathFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPPathFactory.kt index bc0e32f..2e3fbd2 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPPathFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPPathFactory.kt @@ -8,9 +8,8 @@ // ================================================================================================= package com.ashampoo.xmp -import com.ashampoo.xmp.impl.Utils -import com.ashampoo.xmp.impl.xpath.XMPPath -import com.ashampoo.xmp.impl.xpath.XMPPathParser +import com.ashampoo.xmp.xpath.XMPPath +import com.ashampoo.xmp.xpath.XMPPathParser /** * Utility services for the metadata object. It has only public static functions, you cannot create diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt similarity index 98% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt index 8aec68a..1d88582 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt @@ -6,13 +6,8 @@ // 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.ashampoo.xmp.impl +package com.ashampoo.xmp -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPMeta -import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry import com.ashampoo.xmp.options.ParseOptions import com.ashampoo.xmp.options.PropertyOptions import nl.adaptivity.xmlutil.dom.Attr @@ -870,7 +865,7 @@ internal object XMPRDFParser { if (XMPConst.NS_DC_DEPRECATED == namespace) namespace = XMPConst.NS_DC - var prefix = schemaRegistry.getNamespacePrefix(namespace) + var prefix = XMPSchemaRegistryImpl.getNamespacePrefix(namespace) if (prefix == null) { @@ -885,7 +880,7 @@ internal object XMPRDFParser { else DEFAULT_PREFIX - prefix = schemaRegistry.registerNamespace(namespace, prefix) + prefix = XMPSchemaRegistryImpl.registerNamespace(namespace, prefix) } val xmlNodeLocalName = when (xmlNode) { @@ -922,7 +917,7 @@ internal object XMPRDFParser { // If this is an alias set the alias flag in the node // and the hasAliases flag in the tree. - if (schemaRegistry.findAlias(childName) != null) { + if (XMPSchemaRegistryImpl.findAlias(childName) != null) { isAlias = true xmp.root.hasAliases = true schemaNode.hasAliases = true diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt similarity index 98% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index 7057450..bab99ba 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -6,15 +6,9 @@ // 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.ashampoo.xmp.impl - -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPMeta -import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry -import com.ashampoo.xmp.XMPMetaFactory.versionInfo -import com.ashampoo.xmp.impl.Utils.escapeXML +package com.ashampoo.xmp + +import com.ashampoo.xmp.Utils.escapeXML import com.ashampoo.xmp.options.SerializeOptions /** @@ -66,7 +60,7 @@ internal class XMPRDFWriter( writeIndent(level) write(RDF_XMPMETA_START) - write(versionInfo.message) + write(XMPVersionInfo.message) write("\">") writeNewline() @@ -570,7 +564,7 @@ internal class XMPRDFWriter( actualPrefix = qname.prefix!! // add colon for lookup - actualNamespace = schemaRegistry.getNamespaceURI("$actualPrefix:") + actualNamespace = XMPSchemaRegistryImpl.getNamespaceURI("$actualPrefix:") // prefix w/o colon declareNamespace(actualPrefix, actualNamespace, usedPrefixes, indent) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt similarity index 98% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt index cb05dad..004df19 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/XMPSchemaRegistryImpl.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt @@ -6,13 +6,9 @@ // 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.ashampoo.xmp.impl +package com.ashampoo.xmp -import com.ashampoo.xmp.XMPConst -import com.ashampoo.xmp.XMPError -import com.ashampoo.xmp.XMPException -import com.ashampoo.xmp.XMPSchemaRegistry -import com.ashampoo.xmp.impl.Utils.isXMLNameNS +import com.ashampoo.xmp.Utils.isXMLNameNS import com.ashampoo.xmp.options.AliasOptions import com.ashampoo.xmp.properties.XMPAliasInfo diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/PathPosition.kt b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/PathPosition.kt similarity index 96% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/PathPosition.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/xpath/PathPosition.kt index fbd9a5d..c9b0049 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/PathPosition.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/PathPosition.kt @@ -6,7 +6,7 @@ // 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.ashampoo.xmp.impl.xpath +package com.ashampoo.xmp.xpath /** * This objects contains all needed char positions to parse. diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPath.kt b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPath.kt similarity index 98% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPath.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPath.kt index 4c1c502..95a4027 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPath.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPath.kt @@ -6,7 +6,7 @@ // 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.ashampoo.xmp.impl.xpath +package com.ashampoo.xmp.xpath /** * Representates an XMP XMPPath with segment accessor methods. diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPathParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPathParser.kt similarity index 99% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPathParser.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPathParser.kt index c733d8e..63092ff 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPathParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPathParser.kt @@ -6,12 +6,12 @@ // 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.ashampoo.xmp.impl.xpath +package com.ashampoo.xmp.xpath import com.ashampoo.xmp.XMPError import com.ashampoo.xmp.XMPException import com.ashampoo.xmp.XMPMetaFactory.schemaRegistry -import com.ashampoo.xmp.impl.Utils +import com.ashampoo.xmp.Utils /** * Parser for XMP XPaths. diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPathSegment.kt b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPathSegment.kt similarity index 97% rename from src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPathSegment.kt rename to src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPathSegment.kt index f63c420..e86343a 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/impl/xpath/XMPPathSegment.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/xpath/XMPPathSegment.kt @@ -6,7 +6,7 @@ // 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.ashampoo.xmp.impl.xpath +package com.ashampoo.xmp.xpath /** * A segment of a parsed `XMPPath`. diff --git a/src/commonTest/kotlin/com/ashampoo/xmp/impl/UtilsTest.kt b/src/commonTest/kotlin/com/ashampoo/xmp/UtilsTest.kt similarity index 98% rename from src/commonTest/kotlin/com/ashampoo/xmp/impl/UtilsTest.kt rename to src/commonTest/kotlin/com/ashampoo/xmp/UtilsTest.kt index 8dc3fe7..422c2a5 100644 --- a/src/commonTest/kotlin/com/ashampoo/xmp/impl/UtilsTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/xmp/UtilsTest.kt @@ -1,5 +1,6 @@ -package com.ashampoo.xmp.impl +package com.ashampoo.xmp +import com.ashampoo.xmp.Utils import kotlin.test.Test import kotlin.test.assertEquals From 218f1cfb8627b5f05c96ad39fd2c959bc041fd16 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 14:23:12 +0100 Subject: [PATCH 10/17] XMPMeta.kt: Style formattings --- .../kotlin/com/ashampoo/xmp/XMPMeta.kt | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt index 27b13d4..961da7c 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt @@ -1329,14 +1329,12 @@ class XMPMeta { propName: String, propValue: Boolean, options: PropertyOptions = PropertyOptions() - ) { - setProperty( - schemaNS, - propName, - if (propValue) XMPConst.TRUE_STRING else XMPConst.FALSE_STRING, - options - ) - } + ) = setProperty( + schemaNS, + propName, + if (propValue) XMPConst.TRUE_STRING else XMPConst.FALSE_STRING, + options + ) /** * Convenience method to set a property to a literal `int` value. @@ -1351,9 +1349,7 @@ class XMPMeta { propName: String, propValue: Int, options: PropertyOptions = PropertyOptions() - ) { - setProperty(schemaNS, propName, propValue, options) - } + ) = setProperty(schemaNS, propName, propValue, options) /** * Convenience method to set a property to a literal `long` value. @@ -1368,9 +1364,7 @@ class XMPMeta { propName: String, propValue: Long, options: PropertyOptions = PropertyOptions() - ) { - setProperty(schemaNS, propName, propValue, options) - } + ) = setProperty(schemaNS, propName, propValue, options) /** * Convenience method to set a property to a literal `double` value. @@ -1385,9 +1379,7 @@ class XMPMeta { propName: String, propValue: Double, options: PropertyOptions = PropertyOptions() - ) { - setProperty(schemaNS, propName, propValue, options) - } + ) = setProperty(schemaNS, propName, propValue, options) /** * Convenience method to set a property from a binary `byte[]`-array, @@ -1403,9 +1395,7 @@ class XMPMeta { propName: String, propValue: ByteArray, options: PropertyOptions = PropertyOptions() - ) { - setProperty(schemaNS, propName, propValue, options) - } + ) = setProperty(schemaNS, propName, propValue, options) /** * Constructs an iterator for the properties within this XMP object. @@ -1421,7 +1411,7 @@ class XMPMeta { * @param options Option flags to control the iteration. * @return Returns an `XMPIterator`. */ - fun iterator(options: IteratorOptions): com.ashampoo.xmp.XMPIterator = + fun iterator(options: IteratorOptions): XMPIterator = iterator(null, null, options) /** @@ -1441,7 +1431,7 @@ class XMPMeta { propName: String?, options: IteratorOptions ): XMPIterator = - XMPIteratorImpl(this, schemaNS, propName, options) + XMPIterator(this, schemaNS, propName, options) /** * This correlates to the about-attribute, @@ -1485,9 +1475,8 @@ class XMPMeta { * * Qualifier are sorted, with the exception of "xml:lang" and/or "rdf:type" * that stay at the top of the list in that order. */ - fun sort() { - this.root.sort() - } + fun sort() = + root.sort() /** * Perform the normalization as a separate parsing step. @@ -1496,9 +1485,8 @@ class XMPMeta { * *Note:* It does no harm to call this method to an already normalized xmp object. * It was a PDF/A requirement to get hand on the unnormalized `XMPMeta` object. */ - fun normalize(options: ParseOptions) { + fun normalize(options: ParseOptions) = normalize(this, options) - } fun printAllToConsole() { From 79e95052203b462ec895bede201547d9a2aaa692 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 14:23:36 +0100 Subject: [PATCH 11/17] Removed XMPIterator.kt interface as there is only one implementation. --- .../kotlin/com/ashampoo/xmp/XMPIterator.kt | 470 ++++++++++++++++- .../com/ashampoo/xmp/XMPIteratorImpl.kt | 489 ------------------ 2 files changed, 467 insertions(+), 492 deletions(-) delete mode 100644 src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt index d3959e2..f4b623e 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt @@ -8,6 +8,12 @@ // ================================================================================================= package com.ashampoo.xmp +import com.ashampoo.xmp.XMPNodeUtils.findNode +import com.ashampoo.xmp.XMPNodeUtils.findSchemaNode +import com.ashampoo.xmp.xpath.XMPPath +import com.ashampoo.xmp.xpath.XMPPathParser.expandXPath +import com.ashampoo.xmp.options.IteratorOptions +import com.ashampoo.xmp.options.PropertyOptions import com.ashampoo.xmp.properties.XMPPropertyInfo /** @@ -60,18 +66,476 @@ import com.ashampoo.xmp.properties.XMPPropertyInfo * a `NoSuchElementException` if there are no more properties to * return. */ -interface XMPIterator : Iterator { +class XMPIterator( + xmp: XMPMeta, + schemaNS: String?, + propPath: String?, + options: IteratorOptions? +) : Iterator { + + private val options: IteratorOptions + + /** + * the base namespace of the property path, will be changed during the iteration + */ + private var baseNS: String? = null + + /** + * flag to indicate that skipSiblings() has been called. + */ + private var skipSiblings = false + + /** + * flag to indicate that skipSubtree() has been called. + */ + private var skipSubtree = false + + /** + * the node iterator doing the work + */ + private var nodeIterator: Iterator? = null + + /** + * Constructor with optionsl initial values. If `propName` is provided, + * `schemaNS` 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 `schemaNS` + * @param options advanced iteration options, see [IteratorOptions] + * + */ + init { + + // make sure that options is defined at least with defaults + this.options = options ?: IteratorOptions() + + // the start node of the iteration depending on the schema and property filter + var startNode: XMPNode? + var initialPath: String? = null + val baseSchema = !schemaNS.isNullOrEmpty() + val baseProperty = !propPath.isNullOrEmpty() + + when { + + !baseSchema && !baseProperty -> { + + // complete tree will be iterated + startNode = xmp.root + } + + baseSchema && baseProperty -> { + + // Schema and property node provided + + val path = expandXPath(schemaNS, propPath) + + // base path is the prop path without the property leaf + val basePath = XMPPath() + + for (i in 0 until path.size() - 1) + basePath.add(path.getSegment(i)) + + startNode = findNode(xmp.root, path, false, null) + baseNS = schemaNS + initialPath = basePath.toString() + } + + baseSchema && !baseProperty -> { + + // Only Schema provided + startNode = findSchemaNode(xmp.root, schemaNS, false) + } + + else -> { + + // !baseSchema && baseProperty + // No schema but property provided -> error + throw XMPException("Schema namespace URI is required", XMPError.BADSCHEMA) + } + } + + // create iterator + if (startNode != null) { + + if (!this.options.isJustChildren()) + nodeIterator = NodeIterator(startNode, initialPath, 1) + else + nodeIterator = NodeIteratorChildren(startNode, initialPath) + + } else { + + nodeIterator = emptySequence().iterator() + } + } /** * Skip the subtree below the current node when `next()` is * called. */ - fun skipSubtree() + fun skipSubtree() { + skipSubtree = true + } /** * Skip the subtree below and remaining siblings of the current node when * `next()` is called. */ - fun skipSiblings() + fun skipSiblings() { + skipSubtree() + skipSiblings = true + } + + override fun hasNext(): Boolean = + nodeIterator!!.hasNext() + + override fun next(): XMPPropertyInfo = + nodeIterator!!.next() + + /** + * The `XMPIterator` implementation. + * It first returns the node itself, then recursivly the children and qualifier of the node. + */ + private open inner class NodeIterator : Iterator { + + /** + * the state of the iteration + */ + private var state = ITERATE_NODE + + /** + * the currently visited node + */ + private var visitedNode: XMPNode? = null + + /** + * the recursively accumulated path + */ + private var path: String? = null + + /** + * the iterator that goes through the children and qualifier list + */ + protected var childrenIterator: Iterator? = null + + /** + * index of node with parent, only interesting for arrays + */ + private var index = 0 + + /** + * the iterator for each child + */ + private var subIterator = emptySequence().iterator() + + /** + * the cached `PropertyInfo` to return + */ + protected var returnProperty: XMPPropertyInfo? = null + + /** + * Default constructor + */ + constructor() + + /** + * 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) + */ + constructor(visitedNode: XMPNode, parentPath: String?, index: Int) { + + this.visitedNode = visitedNode + state = ITERATE_NODE + + if (visitedNode.options.isSchemaNode()) + baseNS = visitedNode.name + + // 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 + */ + override fun hasNext(): Boolean { + + if (returnProperty != null) + return true // hasNext has been called before + + // find next node + return if (state == ITERATE_NODE) { + + reportNode() + + } else if (state == ITERATE_CHILDREN) { + + if (childrenIterator == null) + childrenIterator = visitedNode!!.iterateChildren() + + var hasNext = iterateChildren(childrenIterator!!) + + if (!hasNext && visitedNode!!.hasQualifier() && !options.isOmitQualifiers()) { + state = ITERATE_QUALIFIER + childrenIterator = null + hasNext = hasNext() + } + + hasNext + + } else { + + if (childrenIterator == null) + childrenIterator = visitedNode!!.iterateQualifier() + + iterateChildren(childrenIterator!!) + } + } + + /** + * Sets the returnProperty as next item or recurses into `hasNext()`. + * + * @return Returns if there is a next item to return. + */ + protected fun reportNode(): Boolean { + + state = ITERATE_CHILDREN + + return if (visitedNode!!.parent != null && + (!options.isJustLeafnodes() || !visitedNode!!.hasChildren()) + ) { + returnProperty = createPropertyInfo(visitedNode, baseNS!!, path!!) + true + } else { + hasNext() + } + } + + /** + * Handles the iteration of the children or qualfier + * + * @return Returns if there are more elements available. + */ + private fun iterateChildren(iterator: Iterator): Boolean { + + if (skipSiblings) { + + skipSiblings = false + + subIterator = emptySequence().iterator() + } + + /* + * Create sub iterator for every child, if its the first child + * visited or the former child is finished + */ + if (!subIterator.hasNext() && iterator.hasNext()) { + + val child = iterator.next() + + index++ + + subIterator = NodeIterator(child, path, index) + } + + if (subIterator.hasNext()) { + + returnProperty = subIterator.next() + + /* We have more available */ + return true + } + + /* There are no more children - end iteration. */ + return false + } + + /** + * Calls hasNext() and returnes the prepared node. Afterward its set to null. + * The existance of returnProperty indicates if there is a next node, otherwise + * an exceptio is thrown. + * + * @see Iterator.next + */ + override fun next(): XMPPropertyInfo { + + if (!hasNext()) + throw NoSuchElementException("There are no more nodes to return") + + val result = returnProperty + + returnProperty = null + + return result!! + } + + /** + * @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 fun accumulatePath(currNode: XMPNode, parentPath: String?, currentIndex: Int): String? { + + val separator: String + val segmentName: String? + + if (currNode.parent == null || currNode.options.isSchemaNode()) { + return null + } else if (currNode.parent!!.options.isArray()) { + separator = "" + segmentName = "[$currentIndex]" + } else { + separator = "/" + segmentName = currNode.name + } + + return if (parentPath.isNullOrEmpty()) { + + segmentName + + } else if (options.isJustLeafname()) { + + if (!segmentName!!.startsWith("?")) + segmentName + else + segmentName.substring(1) // qualifier + + } else { + + parentPath + separator + segmentName + } + } + + /** + * Creates a property info object from an `XMPNode`. + * + * @param node an `XMPNode` + * @param baseNS the base namespace to report + * @param path the full property path + * @return Returns a `XMPProperty`-object that serves representation of the node. + */ + protected fun createPropertyInfo( + node: XMPNode?, + baseNS: String, + path: String + ): XMPPropertyInfo { + + val value = if (node!!.options.isSchemaNode()) + null + else + node.value + + return object : XMPPropertyInfo { + + override fun getNamespace(): String { + + if (node.options.isSchemaNode()) + return baseNS + + // determine namespace of leaf node + val qname = QName(node.name!!) + + return XMPSchemaRegistryImpl.getNamespaceURI(qname.prefix!!)!! + } + + override fun getPath(): String = path + + override fun getValue(): String = value!! + + override fun getOptions(): PropertyOptions = node.options + + // the language is not reported + override fun getLanguage(): String? = null + } + } + } + + /** + * This iterator is derived from the default `NodeIterator`, + * and is only used for the option [IteratorOptions.JUST_CHILDREN]. + */ + private inner class NodeIteratorChildren(parentNode: XMPNode, parentPath: String?) : NodeIterator() { + + private val parentPath: String + + private val nodeChildrenIterator: Iterator + + private var 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. + */ + init { + + if (parentNode.options.isSchemaNode()) + baseNS = parentNode.name + + this.parentPath = accumulatePath(parentNode, parentPath, 1)!! + + nodeChildrenIterator = parentNode.iterateChildren() + } + + /** + * Prepares the next node to return if not already done. + * + * @see Iterator.hasNext + */ + override fun hasNext(): Boolean { + + // hasNext has been called before + if (returnProperty != null) + return true + + if (skipSiblings) + return false + + if (!nodeChildrenIterator.hasNext()) + return false + + val child = nodeChildrenIterator.next() + + index++ + + var path: String? = null + + if (child.options.isSchemaNode()) + baseNS = child.name + else if (child.parent != null) + path = accumulatePath(child, parentPath, index) + + // report next property, skip not-leaf nodes in case options is set + if (!options.isJustLeafnodes() || !child.hasChildren()) { + returnProperty = createPropertyInfo(child, baseNS!!, path!!) + return true + } + + return hasNext() + } + } + + companion object { + + /** + * iteration state + */ + const val ITERATE_NODE = 0 + + /** + * iteration state + */ + const val ITERATE_CHILDREN = 1 + /** + * iteration state + */ + const val ITERATE_QUALIFIER = 2 + } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt deleted file mode 100644 index 96cfb41..0000000 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPIteratorImpl.kt +++ /dev/null @@ -1,489 +0,0 @@ -// ================================================================================================= -// 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.ashampoo.xmp - -import com.ashampoo.xmp.XMPNodeUtils.findNode -import com.ashampoo.xmp.XMPNodeUtils.findSchemaNode -import com.ashampoo.xmp.xpath.XMPPath -import com.ashampoo.xmp.xpath.XMPPathParser.expandXPath -import com.ashampoo.xmp.options.IteratorOptions -import com.ashampoo.xmp.options.PropertyOptions -import com.ashampoo.xmp.properties.XMPPropertyInfo - -/** - * The `XMPIterator` implementation. - * Iterates the XMP Tree according to a set of options. - * During the iteration the XMPMeta-object must not be changed. - * Calls to `skipSubtree()` / `skipSiblings()` will affect the iteration. - */ -class XMPIteratorImpl( - xmp: XMPMeta, - schemaNS: String?, - propPath: String?, - options: IteratorOptions? -) : XMPIterator { - - private val options: IteratorOptions - - /** - * the base namespace of the property path, will be changed during the iteration - */ - private var baseNS: String? = null - - /** - * flag to indicate that skipSiblings() has been called. - */ - private var skipSiblings = false - - /** - * flag to indicate that skipSubtree() has been called. - */ - private var skipSubtree = false - - /** - * the node iterator doing the work - */ - private var nodeIterator: Iterator? = null - - /** - * Constructor with optionsl initial values. If `propName` is provided, - * `schemaNS` 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 `schemaNS` - * @param options advanced iteration options, see [IteratorOptions] - * - */ - init { - - // make sure that options is defined at least with defaults - this.options = options ?: IteratorOptions() - - // the start node of the iteration depending on the schema and property filter - var startNode: XMPNode? - var initialPath: String? = null - val baseSchema = !schemaNS.isNullOrEmpty() - val baseProperty = !propPath.isNullOrEmpty() - - when { - - !baseSchema && !baseProperty -> { - - // complete tree will be iterated - startNode = xmp.root - } - - baseSchema && baseProperty -> { - - // Schema and property node provided - - val path = expandXPath(schemaNS, propPath) - - // base path is the prop path without the property leaf - val basePath = XMPPath() - - for (i in 0 until path.size() - 1) - basePath.add(path.getSegment(i)) - - startNode = findNode(xmp.root, path, false, null) - baseNS = schemaNS - initialPath = basePath.toString() - } - - baseSchema && !baseProperty -> { - - // Only Schema provided - startNode = findSchemaNode(xmp.root, schemaNS, false) - } - - else -> { - - // !baseSchema && baseProperty - // No schema but property provided -> error - throw XMPException("Schema namespace URI is required", XMPError.BADSCHEMA) - } - } - - // create iterator - if (startNode != null) { - - if (!this.options.isJustChildren()) - nodeIterator = NodeIterator(startNode, initialPath, 1) - else - nodeIterator = NodeIteratorChildren(startNode, initialPath) - - } else { - - nodeIterator = emptySequence().iterator() - } - } - - override fun skipSubtree() { - skipSubtree = true - } - - override fun skipSiblings() { - skipSubtree() - skipSiblings = true - } - - override fun hasNext(): Boolean = - nodeIterator!!.hasNext() - - override fun next(): XMPPropertyInfo = - nodeIterator!!.next() - - /** - * The `XMPIterator` implementation. - * It first returns the node itself, then recursivly the children and qualifier of the node. - */ - private open inner class NodeIterator : Iterator { - - /** - * the state of the iteration - */ - private var state = ITERATE_NODE - - /** - * the currently visited node - */ - private var visitedNode: XMPNode? = null - - /** - * the recursively accumulated path - */ - private var path: String? = null - - /** - * the iterator that goes through the children and qualifier list - */ - protected var childrenIterator: Iterator? = null - - /** - * index of node with parent, only interesting for arrays - */ - private var index = 0 - - /** - * the iterator for each child - */ - private var subIterator = emptySequence().iterator() - - /** - * the cached `PropertyInfo` to return - */ - protected var returnProperty: XMPPropertyInfo? = null - - /** - * Default constructor - */ - constructor() - - /** - * 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) - */ - constructor(visitedNode: XMPNode, parentPath: String?, index: Int) { - - this.visitedNode = visitedNode - state = ITERATE_NODE - - if (visitedNode.options.isSchemaNode()) - baseNS = visitedNode.name - - // 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 - */ - override fun hasNext(): Boolean { - - if (returnProperty != null) - return true // hasNext has been called before - - // find next node - return if (state == ITERATE_NODE) { - - reportNode() - - } else if (state == ITERATE_CHILDREN) { - - if (childrenIterator == null) - childrenIterator = visitedNode!!.iterateChildren() - - var hasNext = iterateChildren(childrenIterator!!) - - if (!hasNext && visitedNode!!.hasQualifier() && !options.isOmitQualifiers()) { - state = ITERATE_QUALIFIER - childrenIterator = null - hasNext = hasNext() - } - - hasNext - - } else { - - if (childrenIterator == null) - childrenIterator = visitedNode!!.iterateQualifier() - - iterateChildren(childrenIterator!!) - } - } - - /** - * Sets the returnProperty as next item or recurses into `hasNext()`. - * - * @return Returns if there is a next item to return. - */ - protected fun reportNode(): Boolean { - - state = ITERATE_CHILDREN - - return if (visitedNode!!.parent != null && - (!options.isJustLeafnodes() || !visitedNode!!.hasChildren()) - ) { - returnProperty = createPropertyInfo(visitedNode, baseNS!!, path!!) - true - } else { - hasNext() - } - } - - /** - * Handles the iteration of the children or qualfier - * - * @return Returns if there are more elements available. - */ - private fun iterateChildren(iterator: Iterator): Boolean { - - if (skipSiblings) { - - skipSiblings = false - - subIterator = emptySequence().iterator() - } - - /* - * Create sub iterator for every child, if its the first child - * visited or the former child is finished - */ - if (!subIterator.hasNext() && iterator.hasNext()) { - - val child = iterator.next() - - index++ - - subIterator = NodeIterator(child, path, index) - } - - if (subIterator.hasNext()) { - - returnProperty = subIterator.next() - - /* We have more available */ - return true - } - - /* There are no more children - end iteration. */ - return false - } - - /** - * Calls hasNext() and returnes the prepared node. Afterward its set to null. - * The existance of returnProperty indicates if there is a next node, otherwise - * an exceptio is thrown. - * - * @see Iterator.next - */ - override fun next(): XMPPropertyInfo { - - if (!hasNext()) - throw NoSuchElementException("There are no more nodes to return") - - val result = returnProperty - - returnProperty = null - - return result!! - } - - /** - * @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 fun accumulatePath(currNode: XMPNode, parentPath: String?, currentIndex: Int): String? { - - val separator: String - val segmentName: String? - - if (currNode.parent == null || currNode.options.isSchemaNode()) { - return null - } else if (currNode.parent!!.options.isArray()) { - separator = "" - segmentName = "[$currentIndex]" - } else { - separator = "/" - segmentName = currNode.name - } - - return if (parentPath.isNullOrEmpty()) { - - segmentName - - } else if (options.isJustLeafname()) { - - if (!segmentName!!.startsWith("?")) - segmentName - else - segmentName.substring(1) // qualifier - - } else { - - parentPath + separator + segmentName - } - } - - /** - * Creates a property info object from an `XMPNode`. - * - * @param node an `XMPNode` - * @param baseNS the base namespace to report - * @param path the full property path - * @return Returns a `XMPProperty`-object that serves representation of the node. - */ - protected fun createPropertyInfo( - node: XMPNode?, - baseNS: String, - path: String - ): XMPPropertyInfo { - - val value = if (node!!.options.isSchemaNode()) - null - else - node.value - - return object : XMPPropertyInfo { - - override fun getNamespace(): String { - - if (node.options.isSchemaNode()) - return baseNS - - // determine namespace of leaf node - val qname = QName(node.name!!) - - return XMPSchemaRegistryImpl.getNamespaceURI(qname.prefix!!)!! - } - - override fun getPath(): String = path - - override fun getValue(): String = value!! - - override fun getOptions(): PropertyOptions = node.options - - // the language is not reported - override fun getLanguage(): String? = null - } - } - } - - /** - * This iterator is derived from the default `NodeIterator`, - * and is only used for the option [IteratorOptions.JUST_CHILDREN]. - */ - private inner class NodeIteratorChildren(parentNode: XMPNode, parentPath: String?) : NodeIterator() { - - private val parentPath: String - - private val nodeChildrenIterator: Iterator - - private var 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. - */ - init { - - if (parentNode.options.isSchemaNode()) - baseNS = parentNode.name - - this.parentPath = accumulatePath(parentNode, parentPath, 1)!! - - nodeChildrenIterator = parentNode.iterateChildren() - } - - /** - * Prepares the next node to return if not already done. - * - * @see Iterator.hasNext - */ - override fun hasNext(): Boolean { - - // hasNext has been called before - if (returnProperty != null) - return true - - if (skipSiblings) - return false - - if (!nodeChildrenIterator.hasNext()) - return false - - val child = nodeChildrenIterator.next() - - index++ - - var path: String? = null - - if (child.options.isSchemaNode()) - baseNS = child.name - else if (child.parent != null) - path = accumulatePath(child, parentPath, index) - - // report next property, skip not-leaf nodes in case options is set - if (!options.isJustLeafnodes() || !child.hasChildren()) { - returnProperty = createPropertyInfo(child, baseNS!!, path!!) - return true - } - - return hasNext() - } - } - - companion object { - - /** - * iteration state - */ - const val ITERATE_NODE = 0 - - /** - * iteration state - */ - const val ITERATE_CHILDREN = 1 - - /** - * iteration state - */ - const val ITERATE_QUALIFIER = 2 - } -} From 53509819252ce8bce307e18ced5151a3e3ab8890 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 14:33:48 +0100 Subject: [PATCH 12/17] Merged XMPSchemaRegistry.kt & XMPSchemaRegistryImpl.kt --- .../kotlin/com/ashampoo/xmp/XMPIterator.kt | 2 +- .../kotlin/com/ashampoo/xmp/XMPMetaFactory.kt | 2 +- .../kotlin/com/ashampoo/xmp/XMPNodeUtils.kt | 4 +- .../kotlin/com/ashampoo/xmp/XMPNormalizer.kt | 2 +- .../kotlin/com/ashampoo/xmp/XMPRDFParser.kt | 6 +- .../kotlin/com/ashampoo/xmp/XMPRDFWriter.kt | 2 +- .../com/ashampoo/xmp/XMPSchemaRegistry.kt | 607 ++++++++++++++++- .../com/ashampoo/xmp/XMPSchemaRegistryImpl.kt | 613 ------------------ .../com/ashampoo/xmp/options/AliasOptions.kt | 2 +- 9 files changed, 597 insertions(+), 643 deletions(-) delete mode 100644 src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt index f4b623e..7f3c9d4 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPIterator.kt @@ -440,7 +440,7 @@ class XMPIterator( // determine namespace of leaf node val qname = QName(node.name!!) - return XMPSchemaRegistryImpl.getNamespaceURI(qname.prefix!!)!! + return XMPSchemaRegistry.getNamespaceURI(qname.prefix!!)!! } override fun getPath(): String = path diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt index fca8f2e..27b95f6 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt @@ -17,7 +17,7 @@ import com.ashampoo.xmp.options.SerializeOptions object XMPMetaFactory { @kotlin.jvm.JvmStatic - val schemaRegistry = XMPSchemaRegistryImpl + val schemaRegistry = XMPSchemaRegistry @kotlin.jvm.JvmStatic val versionInfo = XMPVersionInfo diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt index daa89d2..182db23 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNodeUtils.kt @@ -71,12 +71,12 @@ object XMPNodeUtils { schemaNode.isImplicit = true // only previously registered schema namespaces are allowed in the XMP tree. - var prefix = XMPSchemaRegistryImpl.getNamespacePrefix(namespaceURI!!) + var prefix = XMPSchemaRegistry.getNamespacePrefix(namespaceURI!!) if (prefix == null) { prefix = if (!suggestedPrefix.isNullOrEmpty()) - XMPSchemaRegistryImpl.registerNamespace(namespaceURI, suggestedPrefix) + XMPSchemaRegistry.registerNamespace(namespaceURI, suggestedPrefix) else throw XMPException("Unregistered schema namespace URI", XMPError.BADSCHEMA) } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt index 3150b3b..a93bf7c 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPNormalizer.kt @@ -250,7 +250,7 @@ internal object XMPNormalizer { currProp.isAlias = false // Find the base path, look for the base schema and root node. - val info = XMPSchemaRegistryImpl.findAlias(currProp.name!!) + val info = XMPSchemaRegistry.findAlias(currProp.name!!) if (info != null) { diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt index 1d88582..bd3758b 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt @@ -865,7 +865,7 @@ internal object XMPRDFParser { if (XMPConst.NS_DC_DEPRECATED == namespace) namespace = XMPConst.NS_DC - var prefix = XMPSchemaRegistryImpl.getNamespacePrefix(namespace) + var prefix = XMPSchemaRegistry.getNamespacePrefix(namespace) if (prefix == null) { @@ -880,7 +880,7 @@ internal object XMPRDFParser { else DEFAULT_PREFIX - prefix = XMPSchemaRegistryImpl.registerNamespace(namespace, prefix) + prefix = XMPSchemaRegistry.registerNamespace(namespace, prefix) } val xmlNodeLocalName = when (xmlNode) { @@ -917,7 +917,7 @@ internal object XMPRDFParser { // If this is an alias set the alias flag in the node // and the hasAliases flag in the tree. - if (XMPSchemaRegistryImpl.findAlias(childName) != null) { + if (XMPSchemaRegistry.findAlias(childName) != null) { isAlias = true xmp.root.hasAliases = true schemaNode.hasAliases = true diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index bab99ba..734d8a4 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -564,7 +564,7 @@ internal class XMPRDFWriter( actualPrefix = qname.prefix!! // add colon for lookup - actualNamespace = XMPSchemaRegistryImpl.getNamespaceURI("$actualPrefix:") + actualNamespace = XMPSchemaRegistry.getNamespaceURI("$actualPrefix:") // prefix w/o colon declareNamespace(actualPrefix, actualNamespace, usedPrefixes, indent) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt index 26e1ff0..71632ff 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt @@ -8,6 +8,8 @@ // ================================================================================================= package com.ashampoo.xmp +import com.ashampoo.xmp.Utils.isXMLNameNS +import com.ashampoo.xmp.options.AliasOptions import com.ashampoo.xmp.properties.XMPAliasInfo /** @@ -35,8 +37,47 @@ import com.ashampoo.xmp.properties.XMPAliasInfo * 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. + * + * There is only one single instance used by the toolkit. */ -interface XMPSchemaRegistry { +@Suppress("TooManyFunctions") +object XMPSchemaRegistry { + + /** + * a map from a namespace URI to its registered prefix + */ + private val namespaceToPrefixMap: MutableMap = mutableMapOf() + + /** + * a map from a prefix to the associated namespace URI + */ + private val prefixToNamespaceMap: MutableMap = mutableMapOf() + + /** + * a map of all registered aliases. + * The map is a relationship from a qname to an `XMPAliasInfo`-object. + */ + private val aliasMap: MutableMap = mutableMapOf() + + /** + * The pattern that must not be contained in simple properties + */ + private val simpleProperyPattern = Regex("[/*?\\[\\]]") + + /** + * Performs the initialisation of the registry with the default namespaces, aliases and global + * options. + */ + init { + try { + + registerStandardNamespaces() + registerStandardAliases() + + } catch (ex: XMPException) { + throw IllegalStateException("The XMPSchemaRegistry cannot be initialized!", ex) + } + } // --------------------------------------------------------------------------------------------- // Namespace Functions @@ -57,7 +98,52 @@ interface XMPSchemaRegistry { * @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. */ - fun registerNamespace(namespaceURI: String, suggestedPrefix: String): String + fun registerNamespace(namespaceURI: String, suggestedPrefix: String): String { + + var actualSuggestedPrefix = suggestedPrefix + + if (namespaceURI.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (actualSuggestedPrefix.isEmpty()) + throw XMPException("Empty prefix", XMPError.BADPARAM) + + if (actualSuggestedPrefix[actualSuggestedPrefix.length - 1] != ':') + actualSuggestedPrefix += ':' + + if (!isXMLNameNS(actualSuggestedPrefix.substring(0, actualSuggestedPrefix.length - 1))) + throw XMPException("The prefix is a bad XML name", XMPError.BADXML) + + val registeredPrefix = namespaceToPrefixMap[namespaceURI] + val registeredNS = prefixToNamespaceMap[actualSuggestedPrefix] + + // Return the actual prefix + if (registeredPrefix != null) + return registeredPrefix + + if (registeredNS != null) { + + // the namespace is new, but the prefix is already engaged, + // we generate a new prefix out of the suggested + var generatedPrefix = actualSuggestedPrefix + + var i = 1 + + while (prefixToNamespaceMap.containsKey(generatedPrefix)) { + generatedPrefix = + actualSuggestedPrefix.substring(0, actualSuggestedPrefix.length - 1) + "_" + i + "_:" + i++ + } + + actualSuggestedPrefix = generatedPrefix + } + + prefixToNamespaceMap[actualSuggestedPrefix] = namespaceURI + namespaceToPrefixMap[namespaceURI] = actualSuggestedPrefix + + // Return the suggested prefix + return actualSuggestedPrefix + } /** * Obtain the prefix for a registered namespace URI. @@ -67,7 +153,8 @@ interface XMPSchemaRegistry { * @param namespaceURI The URI for the namespace. Must not be null or the empty string. * @return Returns the prefix registered for this namespace URI or null. */ - fun getNamespacePrefix(namespaceURI: String): String? + fun getNamespacePrefix(namespaceURI: String): String? = + namespaceToPrefixMap[namespaceURI] /** * Obtain the URI for a registered namespace prefix. @@ -77,19 +164,22 @@ interface XMPSchemaRegistry { * @param namespacePrefix The prefix for the namespace. Must not be null or the empty string. * @return Returns the URI registered for this prefix or null. */ - fun getNamespaceURI(namespacePrefix: String): String? + fun getNamespaceURI(namespacePrefix: String): String? { + + var actualNamespacePrefix = namespacePrefix + + if (!actualNamespacePrefix.endsWith(":")) + actualNamespacePrefix += ":" + + return prefixToNamespaceMap[actualNamespacePrefix] + } /** * @return Returns the registered prefix/namespace-pairs as map, where the keys are the * namespaces and the values are the prefixes. */ - fun getNamespaces(): Map - - /** - * @return Returns the registered namespace/prefix-pairs as map, where the keys are the - * prefixes and the values are the namespaces. - */ - fun getPrefixes(): Map + fun getNamespaces(): Map = + namespaceToPrefixMap /** * Deletes a namespace from the registry. @@ -99,7 +189,92 @@ interface XMPSchemaRegistry { * * @param namespaceURI The URI for the namespace. */ - fun deleteNamespace(namespaceURI: String) + fun deleteNamespace(namespaceURI: String) { + + val prefixToDelete = getNamespacePrefix(namespaceURI) ?: return + + namespaceToPrefixMap.remove(namespaceURI) + prefixToNamespaceMap.remove(prefixToDelete) + } + + fun getPrefixes(): Map = + 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. + */ + private fun registerStandardNamespaces() { + + // register standard namespaces + registerNamespace(XMPConst.NS_XML, "xml") + registerNamespace(XMPConst.NS_RDF, "rdf") + registerNamespace(XMPConst.NS_DC, "dc") + registerNamespace(XMPConst.NS_IPTC_CORE, "Iptc4xmpCore") + registerNamespace(XMPConst.NS_IPTC_EXT, "Iptc4xmpExt") + registerNamespace(XMPConst.NS_DICOM, "DICOM") + registerNamespace(XMPConst.NS_PLUS, "plus") + + // register other common schemas + registerNamespace(XMPConst.NS_MWG_RS, "mwg-rs") + registerNamespace(XMPConst.NS_ACDSEE, "acdsee") + + // register Adobe standard namespaces + registerNamespace(XMPConst.NS_X, "x") + registerNamespace(XMPConst.NS_IX, "iX") + registerNamespace(XMPConst.NS_XMP, "xmp") + registerNamespace(XMPConst.NS_XMP_RIGHTS, "xmpRights") + registerNamespace(XMPConst.NS_XMP_MM, "xmpMM") + registerNamespace(XMPConst.NS_XMP_BJ, "xmpBJ") + registerNamespace(XMPConst.NS_XMP_NOTE, "xmpNote") + registerNamespace(XMPConst.NS_PDF, "pdf") + registerNamespace(XMPConst.NS_PDFX, "pdfx") + registerNamespace(XMPConst.NS_PDFX_ID, "pdfxid") + registerNamespace(XMPConst.NS_PDFA_SCHEMA, "pdfaSchema") + registerNamespace(XMPConst.NS_PDFA_PROPERTY, "pdfaProperty") + registerNamespace(XMPConst.NS_PDFA_TYPE, "pdfaType") + registerNamespace(XMPConst.NS_PDFA_FIELD, "pdfaField") + registerNamespace(XMPConst.NS_PDFA_ID, "pdfaid") + registerNamespace(XMPConst.NS_PDFA_EXTENSION, "pdfaExtension") + registerNamespace(XMPConst.NS_PHOTOSHOP, "photoshop") + registerNamespace(XMPConst.NS_PS_ALBUM, "album") + registerNamespace(XMPConst.NS_EXIF, "exif") + registerNamespace(XMPConst.NS_EXIF_CIPA, "exifEX") + registerNamespace(XMPConst.NS_EXIF_AUX, "aux") + registerNamespace(XMPConst.NS_TIFF, "tiff") + registerNamespace(XMPConst.NS_PNG, "png") + registerNamespace(XMPConst.NS_JPEG, "jpeg") + registerNamespace(XMPConst.NS_JP2K, "jp2k") + registerNamespace(XMPConst.NS_CAMERA_RAW, "crs") + registerNamespace(XMPConst.NS_ADOBE_STOCK_PHOTO, "bmsp") + registerNamespace(XMPConst.NS_CREATOR_ATOM, "creatorAtom") + registerNamespace(XMPConst.NS_ASF, "asf") + registerNamespace(XMPConst.NS_WAV, "wav") + registerNamespace(XMPConst.NS_BWF, "bext") + registerNamespace(XMPConst.NS_RIFF_INFO, "riffinfo") + registerNamespace(XMPConst.NS_SCRIPT, "xmpScript") + registerNamespace(XMPConst.NS_TRANSFORM_XMP, "txmp") + registerNamespace(XMPConst.NS_SWF, "swf") + + // register Adobe private namespaces + registerNamespace(XMPConst.NS_DM, "xmpDM") + registerNamespace(XMPConst.NS_TRANSIENT, "xmpx") + + // register Adobe standard type namespaces + registerNamespace(XMPConst.TYPE_TEXT, "xmpT") + registerNamespace(XMPConst.TYPE_PAGED_FILE, "xmpTPg") + registerNamespace(XMPConst.TYPE_GRAPHICS, "xmpG") + registerNamespace(XMPConst.TYPE_IMAGE, "xmpGImg") + registerNamespace(XMPConst.TYPE_FONT, "stFnt") + registerNamespace(XMPConst.TYPE_DIMENSIONS, "stDim") + registerNamespace(XMPConst.TYPE_RESOURCE_EVENT, "stEvt") + registerNamespace(XMPConst.TYPE_RESOURCE_REF, "stRef") + registerNamespace(XMPConst.TYPE_ST_VERSION, "stVer") + registerNamespace(XMPConst.TYPE_ST_JOB, "stJob") + registerNamespace(XMPConst.TYPE_MANIFEST_ITEM, "stMfs") + registerNamespace(XMPConst.TYPE_IDENTIFIERQUAL, "xmpidq") + } // --------------------------------------------------------------------------------------------- // Alias Functions @@ -113,7 +288,22 @@ interface XMPSchemaRegistry { * @return Returns the `XMPAliasInfo` for the given alias namespace and property * or `null` if there is no such alias. */ - fun resolveAlias(aliasNS: String, aliasProp: String): XMPAliasInfo? + fun resolveAlias(aliasNS: String, aliasProp: String): XMPAliasInfo? { + + val aliasPrefix = getNamespacePrefix(aliasNS) ?: return null + + return aliasMap[aliasPrefix + aliasProp] + } + + /** + * 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. + */ + fun findAlias(qname: String): XMPAliasInfo? = + aliasMap[qname] /** * Collects all aliases that are contained in the provided namespace. @@ -122,21 +312,398 @@ interface XMPSchemaRegistry { * @param aliasNS a schema namespace URI * @return Returns all alias infos from aliases that are contained in the provided namespace. */ - fun findAliases(aliasNS: String): Set + fun findAliases(aliasNS: String): Set { + + val prefix = getNamespacePrefix(aliasNS) + + if (prefix == null) + return emptySet() + + val result = mutableSetOf() + + for (qname in aliasMap.keys) { + + if (qname.startsWith(prefix)) { + + val alias = findAlias(qname) ?: continue + + result.add(alias) + } + } + + return result + } /** - * Searches for registered aliases. + * Associates an alias name with an actual name. * - * @param qname an XML conform qname - * @return Returns if an alias definition for the given qname to another - * schema and property is registered. + * Define a alias mapping from one namespace/property to another. Both + * property names must be simple names. An alias can be a direct mapping, + * where the alias and actual have the same data type. It is also possible + * to map a simple alias to an item in an array. This can either be to the + * first item in the array, or to the 'x-default' item in an alt-text array. + * Multiple alias names may map to the same actual, as long as the forms + * match. It is a no-op to reregister an alias in an identical fashion. + * Note: This method is not locking because only called by registerStandardAliases + * which is only called by the constructor. + * Note2: The method is only package-private so that it can be tested with unittests + * + * @param aliasNS The namespace URI for the alias. Must not be null or the empty + * string. + * @param aliasProp The name of the alias. Must be a simple name, not null or the + * empty string and not a general path expression. + * @param actualNS The namespace URI for the actual. Must not be null or the + * empty string. + * @param actualProp The name of the actual. Must be a simple name, not null or the + * empty string and not a general path expression. + * @param aliasForm Provides options for aliases for simple aliases to array + * items. This is needed to know what kind of array to create if + * set for the first time via the simple alias. Pass + * `XMP_NoOptions`, the default value, for all + * direct aliases regardless of whether the actual data type is + * an array or not (see [AliasOptions]). */ - fun findAlias(qname: String): XMPAliasInfo? + @Suppress("ThrowsCount") + fun registerAlias( + aliasNS: String, + aliasProp: String, + actualNS: String, + actualProp: String, + aliasForm: AliasOptions? + ) { + + if (aliasNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (aliasProp.isEmpty()) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) + + if (actualNS.isEmpty()) + throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) + + if (actualProp.isEmpty()) + throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) + + // Fix the alias options + val aliasOpts = if (aliasForm != null) + AliasOptions( + XMPNodeUtils.verifySetOptions( + aliasForm.toPropertyOptions(), + null + ).getOptions() + ) + else + AliasOptions() + + if (simpleProperyPattern.matches(aliasProp) || simpleProperyPattern.matches(actualProp)) + throw XMPException("Alias and actual property names must be simple", XMPError.BADXPATH) + + // check if both namespaces are registered + val aliasPrefix = getNamespacePrefix(aliasNS) + val actualPrefix = getNamespacePrefix(actualNS) + + if (aliasPrefix == null) + throw XMPException("Alias namespace is not registered", XMPError.BADSCHEMA) + else if (actualPrefix == null) + throw XMPException("Actual namespace is not registered", XMPError.BADSCHEMA) + + val key = aliasPrefix + aliasProp + + // check if alias is already existing + if (aliasMap.containsKey(key)) + throw XMPException("Alias is already existing", XMPError.BADPARAM) + else if (aliasMap.containsKey(actualPrefix + actualProp)) + throw XMPException( + "Actual property is already an alias, use the base property", XMPError.BADPARAM + ) + + val aliasInfo: XMPAliasInfo = object : XMPAliasInfo { + + override fun getNamespace(): String = actualNS + + override fun getPrefix(): String = actualPrefix + + override fun getPropName(): String = actualProp + + override fun getAliasForm(): AliasOptions = aliasOpts + + override fun toString(): String = + actualPrefix + actualProp + " NS(" + actualNS + "), FORM (" + getAliasForm() + ")" + } + + aliasMap[key] = aliasInfo + } /** * @return Returns the registered aliases as map, where the key is the "qname" (prefix and name) * and the value an `XMPAliasInfo`-object. */ - fun getAliases(): Map + fun getAliases(): Map = + aliasMap + + /** + * Register the standard aliases. + * Note: This method is not lock because only called by the constructor. + */ + @Suppress("StringLiteralDuplication", "LongMethod") + private fun registerStandardAliases() { + + val aliasToArrayOrdered = AliasOptions().setArrayOrdered(true) + val aliasToArrayAltText = AliasOptions().setArrayAltText(true) + + // Aliases from XMP to DC. + registerAlias( + XMPConst.NS_XMP, + "Author", + XMPConst.NS_DC, + "creator", + aliasToArrayOrdered + ) + registerAlias( + XMPConst.NS_XMP, + "Authors", + XMPConst.NS_DC, + "creator", + null + ) + registerAlias( + XMPConst.NS_XMP, + "Description", + XMPConst.NS_DC, + "description", + null + ) + registerAlias( + XMPConst.NS_XMP, + "Format", + XMPConst.NS_DC, + "format", + null + ) + registerAlias( + XMPConst.NS_XMP, + "Keywords", + XMPConst.NS_DC, + "subject", + null + ) + registerAlias( + XMPConst.NS_XMP, + "Locale", + XMPConst.NS_DC, + "language", + null + ) + registerAlias( + XMPConst.NS_XMP, + "Title", + XMPConst.NS_DC, + "title", + null + ) + registerAlias( + XMPConst.NS_XMP_RIGHTS, + "Copyright", + XMPConst.NS_DC, + "rights", + null + ) + + // Aliases from PDF to DC and XMP. + registerAlias( + XMPConst.NS_PDF, + "Author", + XMPConst.NS_DC, + "creator", + aliasToArrayOrdered + ) + registerAlias( + XMPConst.NS_PDF, + "BaseURL", + XMPConst.NS_XMP, + "BaseURL", + null + ) + registerAlias( + XMPConst.NS_PDF, + "CreationDate", + XMPConst.NS_XMP, + "CreateDate", + null + ) + registerAlias( + XMPConst.NS_PDF, + "Creator", + XMPConst.NS_XMP, + "CreatorTool", + null + ) + registerAlias( + XMPConst.NS_PDF, + "ModDate", + XMPConst.NS_XMP, + "ModifyDate", + null + ) + registerAlias( + XMPConst.NS_PDF, + "Subject", + XMPConst.NS_DC, + "description", + aliasToArrayAltText + ) + registerAlias( + XMPConst.NS_PDF, + "Title", + XMPConst.NS_DC, + "title", + aliasToArrayAltText + ) + + // Aliases from PHOTOSHOP to DC and XMP. + registerAlias( + XMPConst.NS_PHOTOSHOP, + "Author", + XMPConst.NS_DC, + "creator", + aliasToArrayOrdered + ) + registerAlias( + XMPConst.NS_PHOTOSHOP, + "Caption", + XMPConst.NS_DC, + "description", + aliasToArrayAltText + ) + registerAlias( + XMPConst.NS_PHOTOSHOP, + "Copyright", + XMPConst.NS_DC, + "rights", + aliasToArrayAltText + ) + registerAlias( + XMPConst.NS_PHOTOSHOP, + "Keywords", + XMPConst.NS_DC, + "subject", + null + ) + registerAlias( + XMPConst.NS_PHOTOSHOP, + "Marked", + XMPConst.NS_XMP_RIGHTS, + "Marked", + null + ) + registerAlias( + XMPConst.NS_PHOTOSHOP, + "Title", + XMPConst.NS_DC, + "title", + aliasToArrayAltText + ) + registerAlias( + XMPConst.NS_PHOTOSHOP, + "WebStatement", + XMPConst.NS_XMP_RIGHTS, + "WebStatement", + null + ) + + // Aliases from TIFF and EXIF to DC and XMP. + registerAlias( + XMPConst.NS_TIFF, + "Artist", + XMPConst.NS_DC, + "creator", + aliasToArrayOrdered + ) + registerAlias( + XMPConst.NS_TIFF, + "Copyright", + XMPConst.NS_DC, + "rights", + null + ) + registerAlias( + XMPConst.NS_TIFF, + "DateTime", + XMPConst.NS_XMP, + "ModifyDate", + null + ) + registerAlias( + XMPConst.NS_EXIF, + "DateTimeDigitized", + XMPConst.NS_XMP, + "CreateDate", + null + ) + registerAlias( + XMPConst.NS_TIFF, + "ImageDescription", + XMPConst.NS_DC, + "description", + null + ) + registerAlias( + XMPConst.NS_TIFF, + "Software", + XMPConst.NS_XMP, + "CreatorTool", + null + ) + // Aliases from PNG (Acrobat ImageCapture) to DC and XMP. + registerAlias( + XMPConst.NS_PNG, + "Author", + XMPConst.NS_DC, + "creator", + aliasToArrayOrdered + ) + registerAlias( + XMPConst.NS_PNG, + "Copyright", + XMPConst.NS_DC, + "rights", + aliasToArrayAltText + ) + registerAlias( + XMPConst.NS_PNG, + "CreationTime", + XMPConst.NS_XMP, + "CreateDate", + null + ) + registerAlias( + XMPConst.NS_PNG, + "Description", + XMPConst.NS_DC, + "description", + aliasToArrayAltText + ) + registerAlias( + XMPConst.NS_PNG, + "ModificationTime", + XMPConst.NS_XMP, + "ModifyDate", + null + ) + registerAlias( + XMPConst.NS_PNG, + "Software", + XMPConst.NS_XMP, + "CreatorTool", + null + ) + registerAlias( + XMPConst.NS_PNG, + "Title", + XMPConst.NS_DC, + "title", + aliasToArrayAltText + ) + } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt deleted file mode 100644 index 004df19..0000000 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistryImpl.kt +++ /dev/null @@ -1,613 +0,0 @@ -// ================================================================================================= -// 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.ashampoo.xmp - -import com.ashampoo.xmp.Utils.isXMLNameNS -import com.ashampoo.xmp.options.AliasOptions -import com.ashampoo.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. - */ -@Suppress("TooManyFunctions") -object XMPSchemaRegistryImpl : XMPSchemaRegistry { - - /** - * a map from a namespace URI to its registered prefix - */ - private val namespaceToPrefixMap: MutableMap = mutableMapOf() - - /** - * a map from a prefix to the associated namespace URI - */ - private val prefixToNamespaceMap: MutableMap = mutableMapOf() - - /** - * a map of all registered aliases. - * The map is a relationship from a qname to an `XMPAliasInfo`-object. - */ - private val aliasMap: MutableMap = mutableMapOf() - - /** - * The pattern that must not be contained in simple properties - */ - private val simpleProperyPattern = Regex("[/*?\\[\\]]") - - /** - * Performs the initialisation of the registry with the default namespaces, aliases and global - * options. - */ - init { - try { - - registerStandardNamespaces() - registerStandardAliases() - - } catch (ex: XMPException) { - throw IllegalStateException("The XMPSchemaRegistry cannot be initialized!", ex) - } - } - - // --------------------------------------------------------------------------------------------- - // Namespace Functions - - override fun registerNamespace(namespaceURI: String, suggestedPrefix: String): String { - - var actualSuggestedPrefix = suggestedPrefix - - if (namespaceURI.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (actualSuggestedPrefix.isEmpty()) - throw XMPException("Empty prefix", XMPError.BADPARAM) - - if (actualSuggestedPrefix[actualSuggestedPrefix.length - 1] != ':') - actualSuggestedPrefix += ':' - - if (!isXMLNameNS(actualSuggestedPrefix.substring(0, actualSuggestedPrefix.length - 1))) - throw XMPException("The prefix is a bad XML name", XMPError.BADXML) - - val registeredPrefix = namespaceToPrefixMap[namespaceURI] - val registeredNS = prefixToNamespaceMap[actualSuggestedPrefix] - - // Return the actual prefix - if (registeredPrefix != null) - return registeredPrefix - - if (registeredNS != null) { - - // the namespace is new, but the prefix is already engaged, - // we generate a new prefix out of the suggested - var generatedPrefix = actualSuggestedPrefix - - var i = 1 - - while (prefixToNamespaceMap.containsKey(generatedPrefix)) { - generatedPrefix = - actualSuggestedPrefix.substring(0, actualSuggestedPrefix.length - 1) + "_" + i + "_:" - i++ - } - - actualSuggestedPrefix = generatedPrefix - } - - prefixToNamespaceMap[actualSuggestedPrefix] = namespaceURI - namespaceToPrefixMap[namespaceURI] = actualSuggestedPrefix - - // Return the suggested prefix - return actualSuggestedPrefix - } - - override fun deleteNamespace(namespaceURI: String) { - - val prefixToDelete = getNamespacePrefix(namespaceURI) ?: return - - namespaceToPrefixMap.remove(namespaceURI) - prefixToNamespaceMap.remove(prefixToDelete) - } - - override fun getNamespacePrefix(namespaceURI: String): String? = - namespaceToPrefixMap[namespaceURI] - - override fun getNamespaceURI(namespacePrefix: String): String? { - - var actualNamespacePrefix = namespacePrefix - - if (!actualNamespacePrefix.endsWith(":")) - actualNamespacePrefix += ":" - - return prefixToNamespaceMap[actualNamespacePrefix] - } - - override fun getNamespaces(): Map = - namespaceToPrefixMap - - override fun getPrefixes(): Map = - 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. - */ - private fun registerStandardNamespaces() { - - // register standard namespaces - registerNamespace(XMPConst.NS_XML, "xml") - registerNamespace(XMPConst.NS_RDF, "rdf") - registerNamespace(XMPConst.NS_DC, "dc") - registerNamespace(XMPConst.NS_IPTC_CORE, "Iptc4xmpCore") - registerNamespace(XMPConst.NS_IPTC_EXT, "Iptc4xmpExt") - registerNamespace(XMPConst.NS_DICOM, "DICOM") - registerNamespace(XMPConst.NS_PLUS, "plus") - - // register other common schemas - registerNamespace(XMPConst.NS_MWG_RS, "mwg-rs") - registerNamespace(XMPConst.NS_ACDSEE, "acdsee") - - // register Adobe standard namespaces - registerNamespace(XMPConst.NS_X, "x") - registerNamespace(XMPConst.NS_IX, "iX") - registerNamespace(XMPConst.NS_XMP, "xmp") - registerNamespace(XMPConst.NS_XMP_RIGHTS, "xmpRights") - registerNamespace(XMPConst.NS_XMP_MM, "xmpMM") - registerNamespace(XMPConst.NS_XMP_BJ, "xmpBJ") - registerNamespace(XMPConst.NS_XMP_NOTE, "xmpNote") - registerNamespace(XMPConst.NS_PDF, "pdf") - registerNamespace(XMPConst.NS_PDFX, "pdfx") - registerNamespace(XMPConst.NS_PDFX_ID, "pdfxid") - registerNamespace(XMPConst.NS_PDFA_SCHEMA, "pdfaSchema") - registerNamespace(XMPConst.NS_PDFA_PROPERTY, "pdfaProperty") - registerNamespace(XMPConst.NS_PDFA_TYPE, "pdfaType") - registerNamespace(XMPConst.NS_PDFA_FIELD, "pdfaField") - registerNamespace(XMPConst.NS_PDFA_ID, "pdfaid") - registerNamespace(XMPConst.NS_PDFA_EXTENSION, "pdfaExtension") - registerNamespace(XMPConst.NS_PHOTOSHOP, "photoshop") - registerNamespace(XMPConst.NS_PS_ALBUM, "album") - registerNamespace(XMPConst.NS_EXIF, "exif") - registerNamespace(XMPConst.NS_EXIF_CIPA, "exifEX") - registerNamespace(XMPConst.NS_EXIF_AUX, "aux") - registerNamespace(XMPConst.NS_TIFF, "tiff") - registerNamespace(XMPConst.NS_PNG, "png") - registerNamespace(XMPConst.NS_JPEG, "jpeg") - registerNamespace(XMPConst.NS_JP2K, "jp2k") - registerNamespace(XMPConst.NS_CAMERA_RAW, "crs") - registerNamespace(XMPConst.NS_ADOBE_STOCK_PHOTO, "bmsp") - registerNamespace(XMPConst.NS_CREATOR_ATOM, "creatorAtom") - registerNamespace(XMPConst.NS_ASF, "asf") - registerNamespace(XMPConst.NS_WAV, "wav") - registerNamespace(XMPConst.NS_BWF, "bext") - registerNamespace(XMPConst.NS_RIFF_INFO, "riffinfo") - registerNamespace(XMPConst.NS_SCRIPT, "xmpScript") - registerNamespace(XMPConst.NS_TRANSFORM_XMP, "txmp") - registerNamespace(XMPConst.NS_SWF, "swf") - - // register Adobe private namespaces - registerNamespace(XMPConst.NS_DM, "xmpDM") - registerNamespace(XMPConst.NS_TRANSIENT, "xmpx") - - // register Adobe standard type namespaces - registerNamespace(XMPConst.TYPE_TEXT, "xmpT") - registerNamespace(XMPConst.TYPE_PAGED_FILE, "xmpTPg") - registerNamespace(XMPConst.TYPE_GRAPHICS, "xmpG") - registerNamespace(XMPConst.TYPE_IMAGE, "xmpGImg") - registerNamespace(XMPConst.TYPE_FONT, "stFnt") - registerNamespace(XMPConst.TYPE_DIMENSIONS, "stDim") - registerNamespace(XMPConst.TYPE_RESOURCE_EVENT, "stEvt") - registerNamespace(XMPConst.TYPE_RESOURCE_REF, "stRef") - registerNamespace(XMPConst.TYPE_ST_VERSION, "stVer") - registerNamespace(XMPConst.TYPE_ST_JOB, "stJob") - registerNamespace(XMPConst.TYPE_MANIFEST_ITEM, "stMfs") - registerNamespace(XMPConst.TYPE_IDENTIFIERQUAL, "xmpidq") - } - - // --------------------------------------------------------------------------------------------- - // Alias Functions - - override fun resolveAlias(aliasNS: String, aliasProp: String): XMPAliasInfo? { - - val aliasPrefix = getNamespacePrefix(aliasNS) ?: return null - - return aliasMap[aliasPrefix + aliasProp] - } - - override fun findAlias(qname: String): XMPAliasInfo? = - aliasMap[qname] - - override fun findAliases(aliasNS: String): Set { - - val prefix = getNamespacePrefix(aliasNS) - - if (prefix == null) return emptySet() - - val result = mutableSetOf() - - for (qname in aliasMap.keys) { - - if (qname.startsWith(prefix)) { - - val alias = findAlias(qname) ?: continue - - result.add(alias) - } - } - - return result - } - - /** - * Associates an alias name with an actual name. - * - * Define a alias mapping from one namespace/property to another. Both - * property names must be simple names. An alias can be a direct mapping, - * where the alias and actual have the same data type. It is also possible - * to map a simple alias to an item in an array. This can either be to the - * first item in the array, or to the 'x-default' item in an alt-text array. - * Multiple alias names may map to the same actual, as long as the forms - * match. It is a no-op to reregister an alias in an identical fashion. - * Note: This method is not locking because only called by registerStandardAliases - * which is only called by the constructor. - * Note2: The method is only package-private so that it can be tested with unittests - * - * @param aliasNS The namespace URI for the alias. Must not be null or the empty - * string. - * @param aliasProp The name of the alias. Must be a simple name, not null or the - * empty string and not a general path expression. - * @param actualNS The namespace URI for the actual. Must not be null or the - * empty string. - * @param actualProp The name of the actual. Must be a simple name, not null or the - * empty string and not a general path expression. - * @param aliasForm Provides options for aliases for simple aliases to array - * items. This is needed to know what kind of array to create if - * set for the first time via the simple alias. Pass - * `XMP_NoOptions`, the default value, for all - * direct aliases regardless of whether the actual data type is - * an array or not (see [AliasOptions]). - */ - @Suppress("ThrowsCount") - fun registerAlias( - aliasNS: String, - aliasProp: String, - actualNS: String, - actualProp: String, - aliasForm: AliasOptions? - ) { - - if (aliasNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (aliasProp.isEmpty()) - throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) - - if (actualNS.isEmpty()) - throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM) - - if (actualProp.isEmpty()) - throw XMPException(XMPError.EMPTY_PROPERTY_NAME_TEXT, XMPError.BADPARAM) - - // Fix the alias options - val aliasOpts = if (aliasForm != null) - AliasOptions( - XMPNodeUtils.verifySetOptions( - aliasForm.toPropertyOptions(), - null - ).getOptions() - ) - else - AliasOptions() - - if (simpleProperyPattern.matches(aliasProp) || simpleProperyPattern.matches(actualProp)) - throw XMPException("Alias and actual property names must be simple", XMPError.BADXPATH) - - // check if both namespaces are registered - val aliasPrefix = getNamespacePrefix(aliasNS) - val actualPrefix = getNamespacePrefix(actualNS) - - if (aliasPrefix == null) - throw XMPException("Alias namespace is not registered", XMPError.BADSCHEMA) - else if (actualPrefix == null) - throw XMPException("Actual namespace is not registered", XMPError.BADSCHEMA) - - val key = aliasPrefix + aliasProp - - // check if alias is already existing - if (aliasMap.containsKey(key)) - throw XMPException("Alias is already existing", XMPError.BADPARAM) - else if (aliasMap.containsKey(actualPrefix + actualProp)) - throw XMPException( - "Actual property is already an alias, use the base property", XMPError.BADPARAM - ) - - val aliasInfo: XMPAliasInfo = object : XMPAliasInfo { - - override fun getNamespace(): String = actualNS - - override fun getPrefix(): String = actualPrefix - - override fun getPropName(): String = actualProp - - override fun getAliasForm(): AliasOptions = aliasOpts - - override fun toString(): String = - actualPrefix + actualProp + " NS(" + actualNS + "), FORM (" + getAliasForm() + ")" - } - - aliasMap[key] = aliasInfo - } - - override fun getAliases(): Map = - aliasMap - - /** - * Register the standard aliases. - * Note: This method is not lock because only called by the constructor. - */ - @Suppress("StringLiteralDuplication", "LongMethod") - private fun registerStandardAliases() { - - val aliasToArrayOrdered = AliasOptions().setArrayOrdered(true) - val aliasToArrayAltText = AliasOptions().setArrayAltText(true) - - // Aliases from XMP to DC. - registerAlias( - XMPConst.NS_XMP, - "Author", - XMPConst.NS_DC, - "creator", - aliasToArrayOrdered - ) - registerAlias( - XMPConst.NS_XMP, - "Authors", - XMPConst.NS_DC, - "creator", - null - ) - registerAlias( - XMPConst.NS_XMP, - "Description", - XMPConst.NS_DC, - "description", - null - ) - registerAlias( - XMPConst.NS_XMP, - "Format", - XMPConst.NS_DC, - "format", - null - ) - registerAlias( - XMPConst.NS_XMP, - "Keywords", - XMPConst.NS_DC, - "subject", - null - ) - registerAlias( - XMPConst.NS_XMP, - "Locale", - XMPConst.NS_DC, - "language", - null - ) - registerAlias( - XMPConst.NS_XMP, - "Title", - XMPConst.NS_DC, - "title", - null - ) - registerAlias( - XMPConst.NS_XMP_RIGHTS, - "Copyright", - XMPConst.NS_DC, - "rights", - null - ) - - // Aliases from PDF to DC and XMP. - registerAlias( - XMPConst.NS_PDF, - "Author", - XMPConst.NS_DC, - "creator", - aliasToArrayOrdered - ) - registerAlias( - XMPConst.NS_PDF, - "BaseURL", - XMPConst.NS_XMP, - "BaseURL", - null - ) - registerAlias( - XMPConst.NS_PDF, - "CreationDate", - XMPConst.NS_XMP, - "CreateDate", - null - ) - registerAlias( - XMPConst.NS_PDF, - "Creator", - XMPConst.NS_XMP, - "CreatorTool", - null - ) - registerAlias( - XMPConst.NS_PDF, - "ModDate", - XMPConst.NS_XMP, - "ModifyDate", - null - ) - registerAlias( - XMPConst.NS_PDF, - "Subject", - XMPConst.NS_DC, - "description", - aliasToArrayAltText - ) - registerAlias( - XMPConst.NS_PDF, - "Title", - XMPConst.NS_DC, - "title", - aliasToArrayAltText - ) - - // Aliases from PHOTOSHOP to DC and XMP. - registerAlias( - XMPConst.NS_PHOTOSHOP, - "Author", - XMPConst.NS_DC, - "creator", - aliasToArrayOrdered - ) - registerAlias( - XMPConst.NS_PHOTOSHOP, - "Caption", - XMPConst.NS_DC, - "description", - aliasToArrayAltText - ) - registerAlias( - XMPConst.NS_PHOTOSHOP, - "Copyright", - XMPConst.NS_DC, - "rights", - aliasToArrayAltText - ) - registerAlias( - XMPConst.NS_PHOTOSHOP, - "Keywords", - XMPConst.NS_DC, - "subject", - null - ) - registerAlias( - XMPConst.NS_PHOTOSHOP, - "Marked", - XMPConst.NS_XMP_RIGHTS, - "Marked", - null - ) - registerAlias( - XMPConst.NS_PHOTOSHOP, - "Title", - XMPConst.NS_DC, - "title", - aliasToArrayAltText - ) - registerAlias( - XMPConst.NS_PHOTOSHOP, - "WebStatement", - XMPConst.NS_XMP_RIGHTS, - "WebStatement", - null - ) - - // Aliases from TIFF and EXIF to DC and XMP. - registerAlias( - XMPConst.NS_TIFF, - "Artist", - XMPConst.NS_DC, - "creator", - aliasToArrayOrdered - ) - registerAlias( - XMPConst.NS_TIFF, - "Copyright", - XMPConst.NS_DC, - "rights", - null - ) - registerAlias( - XMPConst.NS_TIFF, - "DateTime", - XMPConst.NS_XMP, - "ModifyDate", - null - ) - registerAlias( - XMPConst.NS_EXIF, - "DateTimeDigitized", - XMPConst.NS_XMP, - "CreateDate", - null - ) - registerAlias( - XMPConst.NS_TIFF, - "ImageDescription", - XMPConst.NS_DC, - "description", - null - ) - registerAlias( - XMPConst.NS_TIFF, - "Software", - XMPConst.NS_XMP, - "CreatorTool", - null - ) - - // Aliases from PNG (Acrobat ImageCapture) to DC and XMP. - registerAlias( - XMPConst.NS_PNG, - "Author", - XMPConst.NS_DC, - "creator", - aliasToArrayOrdered - ) - registerAlias( - XMPConst.NS_PNG, - "Copyright", - XMPConst.NS_DC, - "rights", - aliasToArrayAltText - ) - registerAlias( - XMPConst.NS_PNG, - "CreationTime", - XMPConst.NS_XMP, - "CreateDate", - null - ) - registerAlias( - XMPConst.NS_PNG, - "Description", - XMPConst.NS_DC, - "description", - aliasToArrayAltText - ) - registerAlias( - XMPConst.NS_PNG, - "ModificationTime", - XMPConst.NS_XMP, - "ModifyDate", - null - ) - registerAlias( - XMPConst.NS_PNG, - "Software", - XMPConst.NS_XMP, - "CreatorTool", - null - ) - registerAlias( - XMPConst.NS_PNG, - "Title", - XMPConst.NS_DC, - "title", - aliasToArrayAltText - ) - } -} diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/options/AliasOptions.kt b/src/commonMain/kotlin/com/ashampoo/xmp/options/AliasOptions.kt index 21df954..17f6cb4 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/options/AliasOptions.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/options/AliasOptions.kt @@ -9,7 +9,7 @@ package com.ashampoo.xmp.options /** - * Options for XMPSchemaRegistryImpl#registerAlias. + * Options for XMPSchemaRegistry#registerAlias. */ class AliasOptions : Options { From 6986c36c3288141243f3408f7027a2c9c8ae342b Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 15:56:09 +0100 Subject: [PATCH 13/17] Refactor XMPRDFWriter: Pass StringBuilder into the methods --- .../kotlin/com/ashampoo/xmp/XMPRDFWriter.kt | 525 ++++++++++-------- 1 file changed, 296 insertions(+), 229 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index 734d8a4..600060c 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -21,8 +21,6 @@ internal class XMPRDFWriter( val options: SerializeOptions ) { - private val sb: StringBuilder = StringBuilder() - /** * The actual serialization. */ @@ -30,9 +28,9 @@ internal class XMPRDFWriter( try { - sb.clear() + val sb: StringBuilder = StringBuilder() - serializeAsRDF() + serializeAsRDF(sb) return sb.toString() @@ -44,53 +42,53 @@ internal class XMPRDFWriter( /** * Writes the (optional) packet header and the outer rdf-tags. */ - private fun serializeAsRDF() { + private fun serializeAsRDF(sb: StringBuilder) { var level = 0 // Write the packet header PI. if (!options.getOmitPacketWrapper()) { - writeIndent(level) - write(PACKET_HEADER) - writeNewline() + writeIndent(sb, level) + write(sb, PACKET_HEADER) + sb.append(XMP_DEFAULT_NEWLINE) } // Write the x:xmpmeta element's start tag. if (!options.getOmitXmpMetaElement()) { - writeIndent(level) - write(RDF_XMPMETA_START) - write(XMPVersionInfo.message) - write("\">") - writeNewline() + writeIndent(sb, level) + write(sb, RDF_XMPMETA_START) + write(sb, XMPVersionInfo.message) + write(sb, "\">") + sb.append(XMP_DEFAULT_NEWLINE) level++ } // Write the rdf:RDF start tag. - writeIndent(level) - write(RDF_RDF_START) - writeNewline() + writeIndent(sb, level) + write(sb, RDF_RDF_START) + sb.append(XMP_DEFAULT_NEWLINE) // Write all of the properties. if (options.getUseCanonicalFormat()) - serializeCanonicalRDFSchemas(level) + serializeCanonicalRDFSchemas(sb, level) else - serializeCompactRDFSchemas(level) + serializeCompactRDFSchemas(sb, level) // Write the rdf:RDF end tag. - writeIndent(level) - write(RDF_RDF_END) - writeNewline() + writeIndent(sb, level) + write(sb, RDF_RDF_END) + sb.append(XMP_DEFAULT_NEWLINE) // Write the xmpmeta end tag. if (!options.getOmitXmpMetaElement()) { level-- - writeIndent(level) - write(RDF_XMPMETA_END) - writeNewline() + writeIndent(sb, level) + write(sb, RDF_XMPMETA_END) + sb.append(XMP_DEFAULT_NEWLINE) } // Write the packet trailer PI into the tail string as UTF-8. @@ -110,7 +108,7 @@ internal class XMPRDFWriter( tailStr += PACKET_TRAILER2 } - write(tailStr) + write(sb, tailStr) } /** @@ -118,37 +116,40 @@ internal class XMPRDFWriter( * * @param level indent level */ - private fun serializeCanonicalRDFSchemas(level: Int) { + private fun serializeCanonicalRDFSchemas( + sb: StringBuilder, + level: Int + ) { if (xmp.root.hasChildren()) { - startOuterRDFDescription(xmp.root, level) + startOuterRDFDescription(sb, xmp.root, level) for (schema in xmp.root.getChildren()) - serializeCanonicalRDFSchema(schema, level) + serializeCanonicalRDFSchema(sb, schema, level) - endOuterRDFDescription(level) + endOuterRDFDescription(sb, level) } else { - writeIndent(level + 1) - write(RDF_SCHEMA_START) // Special case an empty XMP object. - writeTreeName() - write("/>") - writeNewline() + writeIndent(sb, level + 1) + write(sb, RDF_SCHEMA_START) // Special case an empty XMP object. + writeTreeName(sb) + write(sb, "/>") + sb.append(XMP_DEFAULT_NEWLINE) } } - private fun writeTreeName() { + private fun writeTreeName(sb: StringBuilder) { - write('"') + write(sb, '"') val name = xmp.root.name if (name != null) - appendNodeValue(name, true) + appendNodeValue(sb, name, true) - write('"') + write(sb, '"') } /** @@ -156,12 +157,15 @@ internal class XMPRDFWriter( * * @param level indent level to start with */ - private fun serializeCompactRDFSchemas(level: Int) { + private fun serializeCompactRDFSchemas( + sb: StringBuilder, + level: Int + ) { // Begin the rdf:Description start tag. - writeIndent(level + 1) - write(RDF_SCHEMA_START) - writeTreeName() + writeIndent(sb, level + 1) + write(sb, RDF_SCHEMA_START) + writeTreeName(sb) // Write all necessary xmlns attributes. val usedPrefixes: MutableSet = mutableSetOf() @@ -169,35 +173,35 @@ internal class XMPRDFWriter( usedPrefixes.add("rdf") for (schema in xmp.root.getChildren()) - declareUsedNamespaces(schema, usedPrefixes, level + 3) + declareUsedNamespaces(sb, schema, usedPrefixes, level + 3) // Write the top level "attrProps" and close the rdf:Description start tag. var allAreAttrs = true for (schema in xmp.root.getChildren()) - allAreAttrs = allAreAttrs and serializeCompactRDFAttrProps(schema, level + 2) + allAreAttrs = allAreAttrs and serializeCompactRDFAttrProps(sb, schema, level + 2) if (!allAreAttrs) { - write('>') - writeNewline() + write(sb, '>') + sb.append(XMP_DEFAULT_NEWLINE) } else { - write("/>") - writeNewline() + write(sb, "/>") + sb.append(XMP_DEFAULT_NEWLINE) return // ! Done if all properties in all schema are written as attributes. } // Write the remaining properties for each schema. for (schema in xmp.root.getChildren()) - serializeCompactRDFElementProps(schema, level + 2) + serializeCompactRDFElementProps(sb, schema, level + 2) // Write the rdf:Description end tag. // *** Elide the end tag if everything (all props in all schema) is an attr. - writeIndent(level + 1) - write(RDF_SCHEMA_END) - writeNewline() + writeIndent(sb, level + 1) + write(sb, RDF_SCHEMA_END) + sb.append(XMP_DEFAULT_NEWLINE) } /** @@ -208,7 +212,11 @@ internal class XMPRDFWriter( * @param indent the current indent level * @return Returns true if all properties can be rendered as RDF attribute. */ - private fun serializeCompactRDFAttrProps(parentNode: XMPNode, indent: Int): Boolean { + private fun serializeCompactRDFAttrProps( + sb: StringBuilder, + parentNode: XMPNode, + indent: Int + ): Boolean { var allAreAttrs = true @@ -216,12 +224,12 @@ internal class XMPRDFWriter( if (canBeRDFAttrProp(prop)) { - writeNewline() - writeIndent(indent) - write(prop.name!!) - write("=\"") - appendNodeValue(prop.value, true) - write('"') + sb.append(XMP_DEFAULT_NEWLINE) + writeIndent(sb, indent) + write(sb, prop.name!!) + write(sb, "=\"") + appendNodeValue(sb, prop.value, true) + write(sb, '"') } else { @@ -242,7 +250,11 @@ internal class XMPRDFWriter( * @param parentNode the parent node * @param indent the current indent level */ - private fun serializeCompactRDFElementProps(parentNode: XMPNode, indent: Int) { + private fun serializeCompactRDFElementProps( + sb: StringBuilder, + parentNode: XMPNode, + indent: Int + ) { for (node in parentNode.getChildren()) { @@ -260,9 +272,9 @@ internal class XMPRDFWriter( if (XMPConst.ARRAY_ITEM_NAME == elemName) elemName = "rdf:li" - writeIndent(indent) - write('<') - write(elemName!!) + writeIndent(sb, indent) + write(sb, '<') + write(sb, elemName!!) var hasGeneralQualifiers = false var hasRDFResourceQual = false @@ -276,36 +288,36 @@ internal class XMPRDFWriter( } else { hasRDFResourceQual = "rdf:resource" == qualifier.name - write(' ') - write(qualifier.name!!) - write("=\"") - appendNodeValue(qualifier.value, true) - write('"') + write(sb, ' ') + write(sb, qualifier.name!!) + write(sb, "=\"") + appendNodeValue(sb, qualifier.value, true) + write(sb, '"') } } // Process the property according to the standard patterns. if (hasGeneralQualifiers) { - serializeCompactRDFGeneralQualifier(indent, node) + serializeCompactRDFGeneralQualifier(sb, indent, node) } else { // This node has only attribute qualifiers. Emit as a property element. if (!node.options.isCompositeProperty()) { - val result = serializeCompactRDFSimpleProp(node) + val result = serializeCompactRDFSimpleProp(sb, node) emitEndTag = result[0] as Boolean indentEndTag = result[1] as Boolean } else if (node.options.isArray()) { - serializeCompactRDFArrayProp(node, indent) + serializeCompactRDFArrayProp(sb, node, indent) } else { - emitEndTag = serializeCompactRDFStructProp(node, indent, hasRDFResourceQual) + emitEndTag = serializeCompactRDFStructProp(sb, node, indent, hasRDFResourceQual) } } @@ -313,12 +325,12 @@ internal class XMPRDFWriter( if (emitEndTag) { if (indentEndTag) - writeIndent(indent) + writeIndent(sb, indent) - write("') - writeNewline() + write(sb, "') + sb.append(XMP_DEFAULT_NEWLINE) } } } @@ -329,7 +341,10 @@ internal class XMPRDFWriter( * @param node an XMPNode * @return Returns an array containing the flags emitEndTag and indentEndTag. */ - private fun serializeCompactRDFSimpleProp(node: XMPNode): Array { + private fun serializeCompactRDFSimpleProp( + sb: StringBuilder, + node: XMPNode + ): Array { // This is a simple property. var emitEndTag = true @@ -337,22 +352,22 @@ internal class XMPRDFWriter( if (node.options.isURI()) { - write(" rdf:resource=\"") - appendNodeValue(node.value, true) - write("\"/>") - writeNewline() + write(sb, " rdf:resource=\"") + appendNodeValue(sb, node.value, true) + write(sb, "\"/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else if (node.value == null || node.value?.length == 0) { - write("/>") - writeNewline() + write(sb, "/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else { - write('>') - appendNodeValue(node.value, false) + write(sb, '>') + appendNodeValue(sb, node.value, false) indentEndTag = false } @@ -365,18 +380,22 @@ internal class XMPRDFWriter( * @param node an XMPNode * @param indent the current indent level */ - private fun serializeCompactRDFArrayProp(node: XMPNode, indent: Int) { + private fun serializeCompactRDFArrayProp( + sb: StringBuilder, + node: XMPNode, + indent: Int + ) { // This is an array. - write('>') - writeNewline() - emitRDFArrayTag(node, true, indent + 1) + write(sb, '>') + sb.append(XMP_DEFAULT_NEWLINE) + emitRDFArrayTag(sb, node, true, indent + 1) if (node.options.isArrayAltText()) XMPNodeUtils.normalizeLangArray(node) - serializeCompactRDFElementProps(node, indent + 2) - emitRDFArrayTag(node, false, indent + 1) + serializeCompactRDFElementProps(sb, node, indent + 2) + emitRDFArrayTag(sb, node, false, indent + 1) } /** @@ -388,6 +407,7 @@ internal class XMPRDFWriter( * @return Returns true if an end flag shall be emitted. */ private fun serializeCompactRDFStructProp( + sb: StringBuilder, node: XMPNode, indent: Int, hasRDFResourceQual: Boolean @@ -420,8 +440,8 @@ internal class XMPRDFWriter( // below would emit an empty // XML element, which gets reparsed as a simple property // with an empty value. - write(" rdf:parseType=\"Resource\"/>") - writeNewline() + write(sb, " rdf:parseType=\"Resource\"/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } @@ -429,9 +449,9 @@ internal class XMPRDFWriter( // All fields can be attributes, use the // emptyPropertyElt form. - serializeCompactRDFAttrProps(node, indent + 1) - write("/>") - writeNewline() + serializeCompactRDFAttrProps(sb, node, indent + 1) + write(sb, "/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } @@ -439,25 +459,25 @@ internal class XMPRDFWriter( // All fields must be elements, use the // parseTypeResourcePropertyElt form. - write(" rdf:parseType=\"Resource\">") - writeNewline() - serializeCompactRDFElementProps(node, indent + 1) + write(sb, " rdf:parseType=\"Resource\">") + sb.append(XMP_DEFAULT_NEWLINE) + serializeCompactRDFElementProps(sb, 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() + write(sb, '>') + sb.append(XMP_DEFAULT_NEWLINE) + writeIndent(sb, indent + 1) + write(sb, RDF_STRUCT_START) + serializeCompactRDFAttrProps(sb, node, indent + 2) + write(sb, ">") + sb.append(XMP_DEFAULT_NEWLINE) + serializeCompactRDFElementProps(sb, node, indent + 1) + writeIndent(sb, indent + 1) + write(sb, RDF_STRUCT_END) + sb.append(XMP_DEFAULT_NEWLINE) } } @@ -470,7 +490,11 @@ internal class XMPRDFWriter( * @param indent the current indent level * @param node the root node of the subtree */ - private fun serializeCompactRDFGeneralQualifier(indent: Int, node: XMPNode) { + private fun serializeCompactRDFGeneralQualifier( + sb: StringBuilder, + indent: Int, + node: XMPNode + ) { // The node has general qualifiers, ones that can't be // attributes on a property element. @@ -480,12 +504,12 @@ internal class XMPRDFWriter( // *** We're losing compactness in the calls to SerializePrettyRDFProperty. // *** Should refactor to have SerializeCompactRDFProperty that does one node. - write(" rdf:parseType=\"Resource\">") - writeNewline() - serializeCanonicalRDFProperty(node, false, true, indent + 1) + write(sb, " rdf:parseType=\"Resource\">") + sb.append(XMP_DEFAULT_NEWLINE) + serializeCanonicalRDFProperty(sb, node, false, true, indent + 1) for (qualifier in node.getQualifier()) - serializeCanonicalRDFProperty(qualifier, false, false, indent + 1) + serializeCanonicalRDFProperty(sb, qualifier, false, false, indent + 1) } /** @@ -499,39 +523,54 @@ internal class XMPRDFWriter( * qualifier is written as an attribute of the property start tag, not by * itself forcing the qualified property form. */ - private fun serializeCanonicalRDFSchema(schemaNode: XMPNode, level: Int) { + private fun serializeCanonicalRDFSchema( + sb: StringBuilder, + schemaNode: XMPNode, + level: Int + ) { // Write each of the schema's actual properties. for (propNode in schemaNode.getChildren()) - serializeCanonicalRDFProperty(propNode, options.getUseCanonicalFormat(), false, level + 2) + serializeCanonicalRDFProperty( + sb, + propNode, + options.getUseCanonicalFormat(), + false, + level + 2 + ) } /** * Writes all used namespaces of the subtree in node to the output. * The subtree is recursivly traversed. */ - private fun declareUsedNamespaces(node: XMPNode, usedPrefixes: MutableSet, indent: Int) { + private fun declareUsedNamespaces( + sb: StringBuilder, + node: XMPNode, + usedPrefixes: MutableSet, + indent: Int + ) { if (node.options.isSchemaNode()) { // The schema node name is the URI, the value is the prefix. val prefix = node.value!!.substring(0, node.value!!.length - 1) - declareNamespace(prefix, node.name, usedPrefixes, indent) + declareNamespace(sb, prefix, node.name, usedPrefixes, indent) } else if (node.options.isStruct()) { for (field in node.getChildren()) - declareNamespace(field.name!!, null, usedPrefixes, indent) + declareNamespace(sb, field.name!!, null, usedPrefixes, indent) } for (child in node.getChildren()) - declareUsedNamespaces(child, usedPrefixes, indent) + declareUsedNamespaces(sb, child, usedPrefixes, indent) for (qualifier in node.getQualifier()) { - declareNamespace(qualifier.name!!, null, usedPrefixes, indent) - declareUsedNamespaces(qualifier, usedPrefixes, indent) + declareNamespace(sb, qualifier.name!!, null, usedPrefixes, indent) + declareUsedNamespaces(sb, qualifier, usedPrefixes, indent) } } @@ -544,6 +583,7 @@ internal class XMPRDFWriter( * @param indent the current indent level */ private fun declareNamespace( + sb: StringBuilder, prefix: String, namespace: String?, usedPrefixes: MutableSet, @@ -567,18 +607,18 @@ internal class XMPRDFWriter( actualNamespace = XMPSchemaRegistry.getNamespaceURI("$actualPrefix:") // prefix w/o colon - declareNamespace(actualPrefix, actualNamespace, usedPrefixes, indent) + declareNamespace(sb, actualPrefix, actualNamespace, usedPrefixes, indent) } if (!usedPrefixes.contains(actualPrefix)) { - writeNewline() - writeIndent(indent) - write("xmlns:") - write(actualPrefix) - write("=\"") - write(actualNamespace!!) - write('"') + sb.append(XMP_DEFAULT_NEWLINE) + writeIndent(sb, indent) + write(sb, "xmlns:") + write(sb, actualPrefix) + write(sb, "=\"") + write(sb, actualNamespace!!) + write(sb, '"') usedPrefixes.add(actualPrefix) } @@ -588,30 +628,34 @@ internal class XMPRDFWriter( * Start the outer rdf:Description element, including all needed xmlns attributes. * Leave the element open so that the compact form can add property attributes. */ - private fun startOuterRDFDescription(schemaNode: XMPNode, level: Int) { + private fun startOuterRDFDescription( + sb: StringBuilder, + schemaNode: XMPNode, + level: Int + ) { - writeIndent(level + 1) - write(RDF_SCHEMA_START) - writeTreeName() + writeIndent(sb, level + 1) + write(sb, RDF_SCHEMA_START) + writeTreeName(sb) val usedPrefixes: MutableSet = mutableSetOf() usedPrefixes.add("xml") usedPrefixes.add("rdf") - declareUsedNamespaces(schemaNode, usedPrefixes, level + 3) + declareUsedNamespaces(sb, schemaNode, usedPrefixes, level + 3) - write('>') - writeNewline() + write(sb, '>') + sb.append(XMP_DEFAULT_NEWLINE) } /** * Write the end tag. */ - private fun endOuterRDFDescription(level: Int) { + private fun endOuterRDFDescription(sb: StringBuilder, level: Int) { - writeIndent(level + 1) - write(RDF_SCHEMA_END) - writeNewline() + writeIndent(sb, level + 1) + write(sb, RDF_SCHEMA_END) + sb.append(XMP_DEFAULT_NEWLINE) } /** @@ -630,6 +674,7 @@ internal class XMPRDFWriter( * @param indent the current indent level */ private fun serializeCanonicalRDFProperty( + sb: StringBuilder, node: XMPNode, useCanonicalRDF: Boolean, emitAsRDFValue: Boolean, @@ -649,9 +694,9 @@ internal class XMPRDFWriter( else if (XMPConst.ARRAY_ITEM_NAME == elemName) elemName = "rdf:li" - writeIndent(actualIndent) - write('<') - write(elemName!!) + writeIndent(sb, actualIndent) + write(sb, '<') + write(sb, elemName!!) var hasGeneralQualifiers = false var hasRDFResourceQual = false @@ -672,11 +717,11 @@ internal class XMPRDFWriter( if (!emitAsRDFValue) { - write(' ') - write(qualifier.name!!) - write("=\"") - appendNodeValue(qualifier.value, true) - write('"') + write(sb, ' ') + write(sb, qualifier.name!!) + write(sb, "=\"") + appendNodeValue(sb, qualifier.value, true) + write(sb, '"') } } } @@ -695,30 +740,42 @@ internal class XMPRDFWriter( // depending on option if (useCanonicalRDF) { - write(">") - writeNewline() + write(sb, ">") + sb.append(XMP_DEFAULT_NEWLINE) actualIndent++ - writeIndent(actualIndent) - write(RDF_STRUCT_START) - write(">") + writeIndent(sb, actualIndent) + write(sb, RDF_STRUCT_START) + write(sb, ">") } else { - write(" rdf:parseType=\"Resource\">") + write(sb, " rdf:parseType=\"Resource\">") } - writeNewline() + sb.append(XMP_DEFAULT_NEWLINE) - serializeCanonicalRDFProperty(node, useCanonicalRDF, true, actualIndent + 1) + serializeCanonicalRDFProperty( + sb, + node, + useCanonicalRDF, + true, + actualIndent + 1 + ) for (qualifier in node.getQualifier()) if (!RDF_ATTR_QUALIFIER.contains(qualifier.name)) - serializeCanonicalRDFProperty(qualifier, useCanonicalRDF, false, actualIndent + 1) + serializeCanonicalRDFProperty( + sb, + qualifier, + useCanonicalRDF, + false, + actualIndent + 1 + ) if (useCanonicalRDF) { - writeIndent(actualIndent) - write(RDF_STRUCT_END) - writeNewline() + writeIndent(sb, actualIndent) + write(sb, RDF_STRUCT_END) + sb.append(XMP_DEFAULT_NEWLINE) actualIndent-- } @@ -732,24 +789,24 @@ internal class XMPRDFWriter( // This is a simple property. if (node.options.isURI()) { - write(" rdf:resource=\"") - appendNodeValue(node.value, true) - write("\"/>") - writeNewline() + write(sb, " rdf:resource=\"") + appendNodeValue(sb, node.value, true) + write(sb, "\"/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else if (node.value == null || "" == node.value) { - write("/>") - writeNewline() + write(sb, "/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else { - write('>') - appendNodeValue(node.value, false) + write(sb, '>') + appendNodeValue(sb, node.value, false) indentEndTag = false } @@ -759,17 +816,23 @@ internal class XMPRDFWriter( node.options.isArray() -> { // This is an array. - write('>') - writeNewline() - emitRDFArrayTag(node, true, actualIndent + 1) + write(sb, '>') + sb.append(XMP_DEFAULT_NEWLINE) + emitRDFArrayTag(sb, node, true, actualIndent + 1) if (node.options.isArrayAltText()) XMPNodeUtils.normalizeLangArray(node) for (child in node.getChildren()) - serializeCanonicalRDFProperty(child, useCanonicalRDF, false, actualIndent + 2) - - emitRDFArrayTag(node, false, actualIndent + 1) + serializeCanonicalRDFProperty( + sb, + child, + useCanonicalRDF, + false, + actualIndent + 2 + ) + + emitRDFArrayTag(sb, node, false, actualIndent + 1) } !hasRDFResourceQual -> { @@ -781,19 +844,19 @@ internal class XMPRDFWriter( // if option is set if (useCanonicalRDF) { - write(">") - writeNewline() - writeIndent(actualIndent + 1) - write(RDF_EMPTY_STRUCT) + write(sb, ">") + sb.append(XMP_DEFAULT_NEWLINE) + writeIndent(sb, actualIndent + 1) + write(sb, RDF_EMPTY_STRUCT) } else { - write(" rdf:parseType=\"Resource\"/>") + write(sb, " rdf:parseType=\"Resource\"/>") emitEndTag = false } - writeNewline() + sb.append(XMP_DEFAULT_NEWLINE) } else { @@ -801,27 +864,33 @@ internal class XMPRDFWriter( // if option is set if (useCanonicalRDF) { - write(">") - writeNewline() + write(sb, ">") + sb.append(XMP_DEFAULT_NEWLINE) actualIndent++ - writeIndent(actualIndent) - write(RDF_STRUCT_START) - write(">") + writeIndent(sb, actualIndent) + write(sb, RDF_STRUCT_START) + write(sb, ">") } else { - write(" rdf:parseType=\"Resource\">") + write(sb, " rdf:parseType=\"Resource\">") } - writeNewline() + sb.append(XMP_DEFAULT_NEWLINE) for (child in node.getChildren()) - serializeCanonicalRDFProperty(child, useCanonicalRDF, false, actualIndent + 1) + serializeCanonicalRDFProperty( + sb, + child, + useCanonicalRDF, + false, + actualIndent + 1 + ) if (useCanonicalRDF) { - writeIndent(actualIndent) - write(RDF_STRUCT_END) - writeNewline() + writeIndent(sb, actualIndent) + write(sb, RDF_STRUCT_END) + sb.append(XMP_DEFAULT_NEWLINE) actualIndent-- } } @@ -837,17 +906,17 @@ internal class XMPRDFWriter( if (!canBeRDFAttrProp(child)) throw XMPException("Can't mix rdf:resource and complex fields", XMPError.BADRDF) - writeNewline() - writeIndent(actualIndent + 1) - write(' ') - write(child.name!!) - write("=\"") - appendNodeValue(child.value, true) - write('"') + sb.append(XMP_DEFAULT_NEWLINE) + writeIndent(sb, actualIndent + 1) + write(sb, ' ') + write(sb, child.name!!) + write(sb, "=\"") + appendNodeValue(sb, child.value, true) + write(sb, '"') } - write("/>") - writeNewline() + write(sb, "/>") + sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } @@ -858,12 +927,12 @@ internal class XMPRDFWriter( if (emitEndTag) { if (indentEndTag) - writeIndent(actualIndent) + writeIndent(sb, actualIndent) - write("') - writeNewline() + write(sb, "') + sb.append(XMP_DEFAULT_NEWLINE) } } @@ -874,27 +943,32 @@ internal class XMPRDFWriter( * @param isStartTag flag if its the start or end tag * @param indent the current indent level */ - private fun emitRDFArrayTag(arrayNode: XMPNode, isStartTag: Boolean, indent: Int) { + private fun emitRDFArrayTag( + sb: StringBuilder, + arrayNode: XMPNode, + isStartTag: Boolean, + indent: Int + ) { if (isStartTag || arrayNode.hasChildren()) { - writeIndent(indent) + writeIndent(sb, indent) - write(if (isStartTag) "") + write(sb, "/>") else - write(">") + write(sb, ">") - writeNewline() + sb.append(XMP_DEFAULT_NEWLINE) } } @@ -909,8 +983,8 @@ internal class XMPRDFWriter( * @param forAttribute flag if value is an attribute value * */ - private fun appendNodeValue(value: String?, forAttribute: Boolean) = - write(escapeXML(value ?: "", forAttribute, true)) + private fun appendNodeValue(sb: StringBuilder, value: String?, forAttribute: Boolean) = + write(sb, escapeXML(value ?: "", forAttribute, true)) /** * A node can be serialized as RDF-Attribute, if it meets the following conditions: @@ -927,22 +1001,15 @@ internal class XMPRDFWriter( !node.hasQualifier() && !node.options.isURI() && !node.options.isCompositeProperty() && XMPConst.ARRAY_ITEM_NAME != node.name - private fun writeIndent(times: Int) = + private fun writeIndent(sb: StringBuilder, times: Int) = repeat(times) { sb.append(XMP_DEFAULT_INDENT) } - private fun write(c: Char) = + private fun write(sb: StringBuilder, c: Char) = sb.append(c) - private fun write(str: String) = + private fun write(sb: StringBuilder, str: String) = sb.append(str) - /** - * Writes a newline. - */ - private fun writeNewline() { - sb.append(XMP_DEFAULT_NEWLINE) - } - companion object { /** linefeed (U+000A) is the standard XML line terminator. XMP defaults to it. */ From e7857bc85ae3b819de9f8592c1074f0d7591bc0b Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 15:58:11 +0100 Subject: [PATCH 14/17] Refactor XMPRDFWriter: Inlined write methods --- .../kotlin/com/ashampoo/xmp/XMPRDFWriter.kt | 182 +++++++++--------- 1 file changed, 88 insertions(+), 94 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index 600060c..5b6dd00 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -49,7 +49,7 @@ internal class XMPRDFWriter( // Write the packet header PI. if (!options.getOmitPacketWrapper()) { writeIndent(sb, level) - write(sb, PACKET_HEADER) + sb.append(PACKET_HEADER) sb.append(XMP_DEFAULT_NEWLINE) } @@ -57,9 +57,9 @@ internal class XMPRDFWriter( if (!options.getOmitXmpMetaElement()) { writeIndent(sb, level) - write(sb, RDF_XMPMETA_START) - write(sb, XMPVersionInfo.message) - write(sb, "\">") + sb.append(RDF_XMPMETA_START) + sb.append(XMPVersionInfo.message) + sb.append("\">") sb.append(XMP_DEFAULT_NEWLINE) level++ @@ -67,7 +67,7 @@ internal class XMPRDFWriter( // Write the rdf:RDF start tag. writeIndent(sb, level) - write(sb, RDF_RDF_START) + sb.append(RDF_RDF_START) sb.append(XMP_DEFAULT_NEWLINE) // Write all of the properties. @@ -78,7 +78,7 @@ internal class XMPRDFWriter( // Write the rdf:RDF end tag. writeIndent(sb, level) - write(sb, RDF_RDF_END) + sb.append(RDF_RDF_END) sb.append(XMP_DEFAULT_NEWLINE) // Write the xmpmeta end tag. @@ -87,7 +87,7 @@ internal class XMPRDFWriter( level-- writeIndent(sb, level) - write(sb, RDF_XMPMETA_END) + sb.append(RDF_XMPMETA_END) sb.append(XMP_DEFAULT_NEWLINE) } @@ -108,7 +108,7 @@ internal class XMPRDFWriter( tailStr += PACKET_TRAILER2 } - write(sb, tailStr) + sb.append(tailStr) } /** @@ -133,23 +133,23 @@ internal class XMPRDFWriter( } else { writeIndent(sb, level + 1) - write(sb, RDF_SCHEMA_START) // Special case an empty XMP object. + sb.append(RDF_SCHEMA_START) // Special case an empty XMP object. writeTreeName(sb) - write(sb, "/>") + sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) } } private fun writeTreeName(sb: StringBuilder) { - write(sb, '"') + sb.append('"') val name = xmp.root.name if (name != null) appendNodeValue(sb, name, true) - write(sb, '"') + sb.append('"') } /** @@ -164,7 +164,7 @@ internal class XMPRDFWriter( // Begin the rdf:Description start tag. writeIndent(sb, level + 1) - write(sb, RDF_SCHEMA_START) + sb.append(RDF_SCHEMA_START) writeTreeName(sb) // Write all necessary xmlns attributes. @@ -183,12 +183,12 @@ internal class XMPRDFWriter( if (!allAreAttrs) { - write(sb, '>') + sb.append('>') sb.append(XMP_DEFAULT_NEWLINE) } else { - write(sb, "/>") + sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) return // ! Done if all properties in all schema are written as attributes. } @@ -200,7 +200,7 @@ internal class XMPRDFWriter( // Write the rdf:Description end tag. // *** Elide the end tag if everything (all props in all schema) is an attr. writeIndent(sb, level + 1) - write(sb, RDF_SCHEMA_END) + sb.append(RDF_SCHEMA_END) sb.append(XMP_DEFAULT_NEWLINE) } @@ -226,10 +226,10 @@ internal class XMPRDFWriter( sb.append(XMP_DEFAULT_NEWLINE) writeIndent(sb, indent) - write(sb, prop.name!!) - write(sb, "=\"") + sb.append(prop.name!!) + sb.append("=\"") appendNodeValue(sb, prop.value, true) - write(sb, '"') + sb.append('"') } else { @@ -273,8 +273,8 @@ internal class XMPRDFWriter( elemName = "rdf:li" writeIndent(sb, indent) - write(sb, '<') - write(sb, elemName!!) + sb.append('<') + sb.append(elemName!!) var hasGeneralQualifiers = false var hasRDFResourceQual = false @@ -288,11 +288,11 @@ internal class XMPRDFWriter( } else { hasRDFResourceQual = "rdf:resource" == qualifier.name - write(sb, ' ') - write(sb, qualifier.name!!) - write(sb, "=\"") + sb.append(' ') + sb.append(qualifier.name!!) + sb.append("=\"") appendNodeValue(sb, qualifier.value, true) - write(sb, '"') + sb.append('"') } } @@ -327,9 +327,9 @@ internal class XMPRDFWriter( if (indentEndTag) writeIndent(sb, indent) - write(sb, "') + sb.append("') sb.append(XMP_DEFAULT_NEWLINE) } } @@ -352,21 +352,21 @@ internal class XMPRDFWriter( if (node.options.isURI()) { - write(sb, " rdf:resource=\"") + sb.append(" rdf:resource=\"") appendNodeValue(sb, node.value, true) - write(sb, "\"/>") + sb.append("\"/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else if (node.value == null || node.value?.length == 0) { - write(sb, "/>") + sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else { - write(sb, '>') + sb.append('>') appendNodeValue(sb, node.value, false) indentEndTag = false } @@ -387,7 +387,7 @@ internal class XMPRDFWriter( ) { // This is an array. - write(sb, '>') + sb.append('>') sb.append(XMP_DEFAULT_NEWLINE) emitRDFArrayTag(sb, node, true, indent + 1) @@ -440,7 +440,7 @@ internal class XMPRDFWriter( // below would emit an empty // XML element, which gets reparsed as a simple property // with an empty value. - write(sb, " rdf:parseType=\"Resource\"/>") + sb.append(" rdf:parseType=\"Resource\"/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } @@ -450,7 +450,7 @@ internal class XMPRDFWriter( // All fields can be attributes, use the // emptyPropertyElt form. serializeCompactRDFAttrProps(sb, node, indent + 1) - write(sb, "/>") + sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } @@ -459,7 +459,7 @@ internal class XMPRDFWriter( // All fields must be elements, use the // parseTypeResourcePropertyElt form. - write(sb, " rdf:parseType=\"Resource\">") + sb.append(" rdf:parseType=\"Resource\">") sb.append(XMP_DEFAULT_NEWLINE) serializeCompactRDFElementProps(sb, node, indent + 1) } @@ -467,16 +467,16 @@ internal class XMPRDFWriter( else -> { // Have a mix of attributes and elements, use an inner rdf:Description. - write(sb, '>') + sb.append('>') sb.append(XMP_DEFAULT_NEWLINE) writeIndent(sb, indent + 1) - write(sb, RDF_STRUCT_START) + sb.append(RDF_STRUCT_START) serializeCompactRDFAttrProps(sb, node, indent + 2) - write(sb, ">") + sb.append(">") sb.append(XMP_DEFAULT_NEWLINE) serializeCompactRDFElementProps(sb, node, indent + 1) writeIndent(sb, indent + 1) - write(sb, RDF_STRUCT_END) + sb.append(RDF_STRUCT_END) sb.append(XMP_DEFAULT_NEWLINE) } } @@ -504,7 +504,7 @@ internal class XMPRDFWriter( // *** We're losing compactness in the calls to SerializePrettyRDFProperty. // *** Should refactor to have SerializeCompactRDFProperty that does one node. - write(sb, " rdf:parseType=\"Resource\">") + sb.append(" rdf:parseType=\"Resource\">") sb.append(XMP_DEFAULT_NEWLINE) serializeCanonicalRDFProperty(sb, node, false, true, indent + 1) @@ -614,11 +614,11 @@ internal class XMPRDFWriter( sb.append(XMP_DEFAULT_NEWLINE) writeIndent(sb, indent) - write(sb, "xmlns:") - write(sb, actualPrefix) - write(sb, "=\"") - write(sb, actualNamespace!!) - write(sb, '"') + sb.append("xmlns:") + sb.append(actualPrefix) + sb.append("=\"") + sb.append(actualNamespace!!) + sb.append('"') usedPrefixes.add(actualPrefix) } @@ -635,7 +635,7 @@ internal class XMPRDFWriter( ) { writeIndent(sb, level + 1) - write(sb, RDF_SCHEMA_START) + sb.append(RDF_SCHEMA_START) writeTreeName(sb) val usedPrefixes: MutableSet = mutableSetOf() @@ -644,7 +644,7 @@ internal class XMPRDFWriter( declareUsedNamespaces(sb, schemaNode, usedPrefixes, level + 3) - write(sb, '>') + sb.append('>') sb.append(XMP_DEFAULT_NEWLINE) } @@ -654,7 +654,7 @@ internal class XMPRDFWriter( private fun endOuterRDFDescription(sb: StringBuilder, level: Int) { writeIndent(sb, level + 1) - write(sb, RDF_SCHEMA_END) + sb.append(RDF_SCHEMA_END) sb.append(XMP_DEFAULT_NEWLINE) } @@ -695,8 +695,8 @@ internal class XMPRDFWriter( elemName = "rdf:li" writeIndent(sb, actualIndent) - write(sb, '<') - write(sb, elemName!!) + sb.append('<') + sb.append(elemName!!) var hasGeneralQualifiers = false var hasRDFResourceQual = false @@ -717,11 +717,11 @@ internal class XMPRDFWriter( if (!emitAsRDFValue) { - write(sb, ' ') - write(sb, qualifier.name!!) - write(sb, "=\"") + sb.append(' ') + sb.append(qualifier.name!!) + sb.append("=\"") appendNodeValue(sb, qualifier.value, true) - write(sb, '"') + sb.append('"') } } } @@ -740,15 +740,15 @@ internal class XMPRDFWriter( // depending on option if (useCanonicalRDF) { - write(sb, ">") + sb.append(">") sb.append(XMP_DEFAULT_NEWLINE) actualIndent++ writeIndent(sb, actualIndent) - write(sb, RDF_STRUCT_START) - write(sb, ">") + sb.append(RDF_STRUCT_START) + sb.append(">") } else { - write(sb, " rdf:parseType=\"Resource\">") + sb.append(" rdf:parseType=\"Resource\">") } sb.append(XMP_DEFAULT_NEWLINE) @@ -774,7 +774,7 @@ internal class XMPRDFWriter( if (useCanonicalRDF) { writeIndent(sb, actualIndent) - write(sb, RDF_STRUCT_END) + sb.append(RDF_STRUCT_END) sb.append(XMP_DEFAULT_NEWLINE) actualIndent-- } @@ -789,23 +789,23 @@ internal class XMPRDFWriter( // This is a simple property. if (node.options.isURI()) { - write(sb, " rdf:resource=\"") + sb.append(" rdf:resource=\"") appendNodeValue(sb, node.value, true) - write(sb, "\"/>") + sb.append("\"/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else if (node.value == null || "" == node.value) { - write(sb, "/>") + sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false } else { - write(sb, '>') + sb.append('>') appendNodeValue(sb, node.value, false) indentEndTag = false @@ -816,7 +816,7 @@ internal class XMPRDFWriter( node.options.isArray() -> { // This is an array. - write(sb, '>') + sb.append('>') sb.append(XMP_DEFAULT_NEWLINE) emitRDFArrayTag(sb, node, true, actualIndent + 1) @@ -844,14 +844,14 @@ internal class XMPRDFWriter( // if option is set if (useCanonicalRDF) { - write(sb, ">") + sb.append(">") sb.append(XMP_DEFAULT_NEWLINE) writeIndent(sb, actualIndent + 1) - write(sb, RDF_EMPTY_STRUCT) + sb.append(RDF_EMPTY_STRUCT) } else { - write(sb, " rdf:parseType=\"Resource\"/>") + sb.append(" rdf:parseType=\"Resource\"/>") emitEndTag = false } @@ -864,16 +864,16 @@ internal class XMPRDFWriter( // if option is set if (useCanonicalRDF) { - write(sb, ">") + sb.append(">") sb.append(XMP_DEFAULT_NEWLINE) actualIndent++ writeIndent(sb, actualIndent) - write(sb, RDF_STRUCT_START) - write(sb, ">") + sb.append(RDF_STRUCT_START) + sb.append(">") } else { - write(sb, " rdf:parseType=\"Resource\">") + sb.append(" rdf:parseType=\"Resource\">") } sb.append(XMP_DEFAULT_NEWLINE) @@ -889,7 +889,7 @@ internal class XMPRDFWriter( if (useCanonicalRDF) { writeIndent(sb, actualIndent) - write(sb, RDF_STRUCT_END) + sb.append(RDF_STRUCT_END) sb.append(XMP_DEFAULT_NEWLINE) actualIndent-- } @@ -908,14 +908,14 @@ internal class XMPRDFWriter( sb.append(XMP_DEFAULT_NEWLINE) writeIndent(sb, actualIndent + 1) - write(sb, ' ') - write(sb, child.name!!) - write(sb, "=\"") + sb.append(' ') + sb.append(child.name!!) + sb.append("=\"") appendNodeValue(sb, child.value, true) - write(sb, '"') + sb.append('"') } - write(sb, "/>") + sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) emitEndTag = false @@ -929,9 +929,9 @@ internal class XMPRDFWriter( if (indentEndTag) writeIndent(sb, actualIndent) - write(sb, "') + sb.append("') sb.append(XMP_DEFAULT_NEWLINE) } } @@ -954,19 +954,19 @@ internal class XMPRDFWriter( writeIndent(sb, indent) - write(sb, if (isStartTag) "") + sb.append("/>") else - write(sb, ">") + sb.append(">") sb.append(XMP_DEFAULT_NEWLINE) } @@ -984,7 +984,7 @@ internal class XMPRDFWriter( * */ private fun appendNodeValue(sb: StringBuilder, value: String?, forAttribute: Boolean) = - write(sb, escapeXML(value ?: "", forAttribute, true)) + sb.append(escapeXML(value ?: "", forAttribute, true)) /** * A node can be serialized as RDF-Attribute, if it meets the following conditions: @@ -1004,12 +1004,6 @@ internal class XMPRDFWriter( private fun writeIndent(sb: StringBuilder, times: Int) = repeat(times) { sb.append(XMP_DEFAULT_INDENT) } - private fun write(sb: StringBuilder, c: Char) = - sb.append(c) - - private fun write(sb: StringBuilder, str: String) = - sb.append(str) - companion object { /** linefeed (U+000A) is the standard XML line terminator. XMP defaults to it. */ From c60b2f4fefa6b689c4a9f2a05fa23838b4d514fb Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 16:01:56 +0100 Subject: [PATCH 15/17] Refactor XMPRDFWriter: Pass XMPMeta into serialize() --- .../kotlin/com/ashampoo/xmp/XMPMetaFactory.kt | 2 +- .../kotlin/com/ashampoo/xmp/XMPRDFWriter.kt | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt index 27b95f6..280c815 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt @@ -43,6 +43,6 @@ object XMPMetaFactory { if (actualOptions.getSort()) xmp.sort() - return XMPRDFWriter(xmp, actualOptions).serialize() + return XMPRDFWriter(actualOptions).serialize(xmp) } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index 5b6dd00..7d536bb 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -17,20 +17,19 @@ import com.ashampoo.xmp.options.SerializeOptions */ @Suppress("TooManyFunctions") internal class XMPRDFWriter( - val xmp: XMPMeta, val options: SerializeOptions ) { /** * The actual serialization. */ - fun serialize(): String { + fun serialize(xmp: XMPMeta): String { try { val sb: StringBuilder = StringBuilder() - serializeAsRDF(sb) + serializeAsRDF(sb, xmp) return sb.toString() @@ -42,7 +41,10 @@ internal class XMPRDFWriter( /** * Writes the (optional) packet header and the outer rdf-tags. */ - private fun serializeAsRDF(sb: StringBuilder) { + private fun serializeAsRDF( + sb: StringBuilder, + xmp: XMPMeta + ) { var level = 0 @@ -72,9 +74,9 @@ internal class XMPRDFWriter( // Write all of the properties. if (options.getUseCanonicalFormat()) - serializeCanonicalRDFSchemas(sb, level) + serializeCanonicalRDFSchemas(sb, xmp, level) else - serializeCompactRDFSchemas(sb, level) + serializeCompactRDFSchemas(sb, xmp, level) // Write the rdf:RDF end tag. writeIndent(sb, level) @@ -118,12 +120,13 @@ internal class XMPRDFWriter( */ private fun serializeCanonicalRDFSchemas( sb: StringBuilder, + xmp: XMPMeta, level: Int ) { if (xmp.root.hasChildren()) { - startOuterRDFDescription(sb, xmp.root, level) + startOuterRDFDescription(sb, xmp, xmp.root, level) for (schema in xmp.root.getChildren()) serializeCanonicalRDFSchema(sb, schema, level) @@ -134,13 +137,13 @@ internal class XMPRDFWriter( writeIndent(sb, level + 1) sb.append(RDF_SCHEMA_START) // Special case an empty XMP object. - writeTreeName(sb) + writeTreeName(sb, xmp) sb.append("/>") sb.append(XMP_DEFAULT_NEWLINE) } } - private fun writeTreeName(sb: StringBuilder) { + private fun writeTreeName(sb: StringBuilder, xmp: XMPMeta) { sb.append('"') @@ -159,13 +162,14 @@ internal class XMPRDFWriter( */ private fun serializeCompactRDFSchemas( sb: StringBuilder, + xmp: XMPMeta, level: Int ) { // Begin the rdf:Description start tag. writeIndent(sb, level + 1) sb.append(RDF_SCHEMA_START) - writeTreeName(sb) + writeTreeName(sb, xmp) // Write all necessary xmlns attributes. val usedPrefixes: MutableSet = mutableSetOf() @@ -630,13 +634,14 @@ internal class XMPRDFWriter( */ private fun startOuterRDFDescription( sb: StringBuilder, + xmp: XMPMeta, schemaNode: XMPNode, level: Int ) { writeIndent(sb, level + 1) sb.append(RDF_SCHEMA_START) - writeTreeName(sb) + writeTreeName(sb, xmp) val usedPrefixes: MutableSet = mutableSetOf() usedPrefixes.add("xml") From 9eb8fe5ccabc381466c24960a87f7b681a63d3b3 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 16:03:45 +0100 Subject: [PATCH 16/17] Refactor XMPRDFWriter: Pass SerializeOptions into serialize() --- .../kotlin/com/ashampoo/xmp/XMPMetaFactory.kt | 2 +- .../kotlin/com/ashampoo/xmp/XMPRDFWriter.kt | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt index 280c815..4c06989 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt @@ -43,6 +43,6 @@ object XMPMetaFactory { if (actualOptions.getSort()) xmp.sort() - return XMPRDFWriter(actualOptions).serialize(xmp) + return XMPRDFWriter().serialize(xmp, actualOptions) } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index 7d536bb..7975c91 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -16,20 +16,18 @@ import com.ashampoo.xmp.options.SerializeOptions * The output is a XMP String according to the `SerializeOptions`. */ @Suppress("TooManyFunctions") -internal class XMPRDFWriter( - val options: SerializeOptions -) { +internal class XMPRDFWriter { /** * The actual serialization. */ - fun serialize(xmp: XMPMeta): String { + fun serialize(xmp: XMPMeta, options: SerializeOptions): String { try { val sb: StringBuilder = StringBuilder() - serializeAsRDF(sb, xmp) + serializeAsRDF(sb, xmp, options) return sb.toString() @@ -43,7 +41,8 @@ internal class XMPRDFWriter( */ private fun serializeAsRDF( sb: StringBuilder, - xmp: XMPMeta + xmp: XMPMeta, + options: SerializeOptions ) { var level = 0 @@ -74,7 +73,7 @@ internal class XMPRDFWriter( // Write all of the properties. if (options.getUseCanonicalFormat()) - serializeCanonicalRDFSchemas(sb, xmp, level) + serializeCanonicalRDFSchemas(sb, xmp, options, level) else serializeCompactRDFSchemas(sb, xmp, level) @@ -121,6 +120,7 @@ internal class XMPRDFWriter( private fun serializeCanonicalRDFSchemas( sb: StringBuilder, xmp: XMPMeta, + options: SerializeOptions, level: Int ) { @@ -129,7 +129,7 @@ internal class XMPRDFWriter( startOuterRDFDescription(sb, xmp, xmp.root, level) for (schema in xmp.root.getChildren()) - serializeCanonicalRDFSchema(sb, schema, level) + serializeCanonicalRDFSchema(sb, options, schema, level) endOuterRDFDescription(sb, level) @@ -529,6 +529,7 @@ internal class XMPRDFWriter( */ private fun serializeCanonicalRDFSchema( sb: StringBuilder, + options: SerializeOptions, schemaNode: XMPNode, level: Int ) { From efa27d7f4cbce68a4561296d0112a6b1ab8038bf Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Wed, 8 Nov 2023 16:05:54 +0100 Subject: [PATCH 17/17] XMPRDFWriter is now a singleton object to avoid this instance creation --- .../kotlin/com/ashampoo/xmp/XMPMetaFactory.kt | 2 +- .../kotlin/com/ashampoo/xmp/XMPRDFWriter.kt | 87 +++++++++---------- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt index 4c06989..9ecad67 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaFactory.kt @@ -43,6 +43,6 @@ object XMPMetaFactory { if (actualOptions.getSort()) xmp.sort() - return XMPRDFWriter().serialize(xmp, actualOptions) + return XMPRDFWriter.serialize(xmp, actualOptions) } } diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt index 7975c91..9f592fd 100644 --- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFWriter.kt @@ -16,7 +16,48 @@ import com.ashampoo.xmp.options.SerializeOptions * The output is a XMP String according to the `SerializeOptions`. */ @Suppress("TooManyFunctions") -internal class XMPRDFWriter { +internal object XMPRDFWriter { + + /** linefeed (U+000A) is the standard XML line terminator. XMP defaults to it. */ + const val XMP_DEFAULT_NEWLINE = "\n" + + /** Two ASCII spaces (U+0020) are the default indent for XMP files. */ + const val XMP_DEFAULT_INDENT = " " + + private const val PACKET_HEADER = "" + + /** + * The w/r is missing inbetween + */ + private const val PACKET_TRAILER = "" + + private const val RDF_XMPMETA_START = "" + + private const val RDF_RDF_END = "" + + private const val RDF_SCHEMA_START = "" + + private const val RDF_STRUCT_START = " = setOf( + XMPConst.XML_LANG, "rdf:resource", "rdf:ID", "rdf:bagID", "rdf:nodeID" + ) /** * The actual serialization. @@ -1009,48 +1050,4 @@ internal class XMPRDFWriter { private fun writeIndent(sb: StringBuilder, times: Int) = repeat(times) { sb.append(XMP_DEFAULT_INDENT) } - - companion object { - - /** linefeed (U+000A) is the standard XML line terminator. XMP defaults to it. */ - const val XMP_DEFAULT_NEWLINE = "\n" - - /** Two ASCII spaces (U+0020) are the default indent for XMP files. */ - const val XMP_DEFAULT_INDENT = " " - - private const val PACKET_HEADER = "" - - /** - * The w/r is missing inbetween - */ - private const val PACKET_TRAILER = "" - - private const val RDF_XMPMETA_START = "" - - private const val RDF_RDF_END = "" - - private const val RDF_SCHEMA_START = "" - - private const val RDF_STRUCT_START = " = setOf( - XMPConst.XML_LANG, "rdf:resource", "rdf:ID", "rdf:bagID", "rdf:nodeID" - ) - } }