| // ================================================================================================= |
| // ADOBE SYSTEMS INCORPORATED |
| // Copyright 2006 Adobe Systems Incorporated |
| // All Rights Reserved |
| // |
| // NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms |
| // of the Adobe license agreement accompanying it. |
| // ================================================================================================= |
| |
| package com.adobe.xmp.impl; |
| |
| import java.util.GregorianCalendar; |
| import java.util.Iterator; |
| |
| import com.adobe.xmp.XMPConst; |
| import com.adobe.xmp.XMPDateTime; |
| import com.adobe.xmp.XMPDateTimeFactory; |
| import com.adobe.xmp.XMPError; |
| import com.adobe.xmp.XMPException; |
| import com.adobe.xmp.XMPMetaFactory; |
| import com.adobe.xmp.XMPUtils; |
| import com.adobe.xmp.impl.xpath.XMPPath; |
| import com.adobe.xmp.impl.xpath.XMPPathSegment; |
| import com.adobe.xmp.options.AliasOptions; |
| import com.adobe.xmp.options.PropertyOptions; |
| |
| |
| /** |
| * Utilities for <code>XMPNode</code>. |
| * |
| * @since Aug 28, 2006 |
| */ |
| public class XMPNodeUtils implements XMPConst |
| { |
| /** */ |
| static final int CLT_NO_VALUES = 0; |
| /** */ |
| static final int CLT_SPECIFIC_MATCH = 1; |
| /** */ |
| static final int CLT_SINGLE_GENERIC = 2; |
| /** */ |
| static final int CLT_MULTIPLE_GENERIC = 3; |
| /** */ |
| static final int CLT_XDEFAULT = 4; |
| /** */ |
| static final int CLT_FIRST_ITEM = 5; |
| |
| |
| /** |
| * Private Constructor |
| */ |
| private XMPNodeUtils() |
| { |
| // EMPTY |
| } |
| |
| |
| /** |
| * Find or create a schema node if <code>createNodes</code> is false and |
| * |
| * @param tree the root of the xmp tree. |
| * @param namespaceURI a namespace |
| * @param createNodes a flag indicating if the node shall be created if not found. |
| * <em>Note:</em> The namespace must be registered prior to this call. |
| * |
| * @return Returns the schema node if found, <code>null</code> otherwise. |
| * Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> |
| * returned a valid node. |
| * @throws XMPException An exception is only thrown if an error occurred, not if a |
| * node was not found. |
| */ |
| static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, |
| boolean createNodes) |
| throws XMPException |
| { |
| return findSchemaNode(tree, namespaceURI, null, createNodes); |
| } |
| |
| |
| /** |
| * Find or create a schema node if <code>createNodes</code> is true. |
| * |
| * @param tree the root of the xmp tree. |
| * @param namespaceURI a namespace |
| * @param suggestedPrefix If a prefix is suggested, the namespace is allowed to be registered. |
| * @param createNodes a flag indicating if the node shall be created if not found. |
| * <em>Note:</em> The namespace must be registered prior to this call. |
| * |
| * @return Returns the schema node if found, <code>null</code> otherwise. |
| * Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> |
| * returned a valid node. |
| * @throws XMPException An exception is only thrown if an error occurred, not if a |
| * node was not found. |
| */ |
| static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, String suggestedPrefix, |
| boolean createNodes) |
| throws XMPException |
| { |
| assert tree.getParent() == null; // make sure that its the root |
| XMPNode schemaNode = tree.findChildByName(namespaceURI); |
| |
| if (schemaNode == null && createNodes) |
| { |
| schemaNode = new XMPNode(namespaceURI, |
| new PropertyOptions() |
| .setSchemaNode(true)); |
| schemaNode.setImplicit(true); |
| |
| // only previously registered schema namespaces are allowed in the XMP tree. |
| String prefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(namespaceURI); |
| if (prefix == null) |
| { |
| if (suggestedPrefix != null && suggestedPrefix.length() != 0) |
| { |
| prefix = XMPMetaFactory.getSchemaRegistry().registerNamespace(namespaceURI, |
| suggestedPrefix); |
| } |
| else |
| { |
| throw new XMPException("Unregistered schema namespace URI", |
| XMPError.BADSCHEMA); |
| } |
| } |
| |
| schemaNode.setValue(prefix); |
| |
| tree.addChild(schemaNode); |
| } |
| |
| return schemaNode; |
| } |
| |
| |
| /** |
| * Find or create a child node under a given parent node. If the parent node is no |
| * Returns the found or created child node. |
| * |
| * @param parent |
| * the parent node |
| * @param childName |
| * the node name to find |
| * @param createNodes |
| * flag, if new nodes shall be created. |
| * @return Returns the found or created node or <code>null</code>. |
| * @throws XMPException Thrown if |
| */ |
| static XMPNode findChildNode(XMPNode parent, String childName, boolean createNodes) |
| throws XMPException |
| { |
| if (!parent.getOptions().isSchemaNode() && !parent.getOptions().isStruct()) |
| { |
| if (!parent.isImplicit()) |
| { |
| throw new XMPException("Named children only allowed for schemas and structs", |
| XMPError.BADXPATH); |
| } |
| else if (parent.getOptions().isArray()) |
| { |
| throw new XMPException("Named children not allowed for arrays", |
| XMPError.BADXPATH); |
| } |
| else if (createNodes) |
| { |
| parent.getOptions().setStruct(true); |
| } |
| } |
| |
| XMPNode childNode = parent.findChildByName(childName); |
| |
| if (childNode == null && createNodes) |
| { |
| PropertyOptions options = new PropertyOptions(); |
| childNode = new XMPNode(childName, options); |
| childNode.setImplicit(true); |
| parent.addChild(childNode); |
| } |
| |
| assert childNode != null || !createNodes; |
| |
| return childNode; |
| } |
| |
| |
| /** |
| * Follow an expanded path expression to find or create a node. |
| * |
| * @param xmpTree the node to begin the search. |
| * @param xpath the complete xpath |
| * @param createNodes flag if nodes shall be created |
| * (when called by <code>setProperty()</code>) |
| * @param leafOptions the options for the created leaf nodes (only when |
| * <code>createNodes == true</code>). |
| * @return Returns the node if found or created or <code>null</code>. |
| * @throws XMPException An exception is only thrown if an error occurred, |
| * not if a node was not found. |
| */ |
| static XMPNode findNode(XMPNode xmpTree, XMPPath xpath, boolean createNodes, |
| PropertyOptions leafOptions) throws XMPException |
| { |
| // check if xpath is set. |
| if (xpath == null || xpath.size() == 0) |
| { |
| throw new XMPException("Empty XMPPath", XMPError.BADXPATH); |
| } |
| |
| // Root of implicitly created subtree to possible delete it later. |
| // Valid only if leaf is new. |
| XMPNode rootImplicitNode = null; |
| XMPNode currNode = null; |
| |
| // resolve schema step |
| currNode = findSchemaNode(xmpTree, |
| xpath.getSegment(XMPPath.STEP_SCHEMA).getName(), createNodes); |
| if (currNode == null) |
| { |
| return null; |
| } |
| else if (currNode.isImplicit()) |
| { |
| currNode.setImplicit(false); // Clear the implicit node bit. |
| rootImplicitNode = currNode; // Save the top most implicit node. |
| } |
| |
| |
| // Now follow the remaining steps of the original XMPPath. |
| try |
| { |
| for (int i = 1; i < xpath.size(); i++) |
| { |
| currNode = followXPathStep(currNode, xpath.getSegment(i), createNodes); |
| if (currNode == null) |
| { |
| if (createNodes) |
| { |
| // delete implicitly created nodes |
| deleteNode(rootImplicitNode); |
| } |
| return null; |
| } |
| else if (currNode.isImplicit()) |
| { |
| // clear the implicit node flag |
| currNode.setImplicit(false); |
| |
| // if node is an ALIAS (can be only in root step, auto-create array |
| // when the path has been resolved from a not simple alias type |
| if (i == 1 && |
| xpath.getSegment(i).isAlias() && |
| xpath.getSegment(i).getAliasForm() != 0) |
| { |
| currNode.getOptions().setOption(xpath.getSegment(i).getAliasForm(), true); |
| } |
| // "CheckImplicitStruct" in C++ |
| else if (i < xpath.size() - 1 && |
| xpath.getSegment(i).getKind() == XMPPath.STRUCT_FIELD_STEP && |
| !currNode.getOptions().isCompositeProperty()) |
| { |
| currNode.getOptions().setStruct(true); |
| } |
| |
| if (rootImplicitNode == null) |
| { |
| rootImplicitNode = currNode; // Save the top most implicit node. |
| } |
| } |
| } |
| } |
| catch (XMPException e) |
| { |
| // if new notes have been created prior to the error, delete them |
| if (rootImplicitNode != null) |
| { |
| deleteNode(rootImplicitNode); |
| } |
| throw e; |
| } |
| |
| |
| if (rootImplicitNode != null) |
| { |
| // set options only if a node has been successful created |
| currNode.getOptions().mergeWith(leafOptions); |
| currNode.setOptions(currNode.getOptions()); |
| } |
| |
| return currNode; |
| } |
| |
| |
| /** |
| * Deletes the the given node and its children from its parent. |
| * Takes care about adjusting the flags. |
| * @param node the top-most node to delete. |
| */ |
| static void deleteNode(XMPNode node) |
| { |
| XMPNode parent = node.getParent(); |
| |
| if (node.getOptions().isQualifier()) |
| { |
| // root is qualifier |
| parent.removeQualifier(node); |
| } |
| else |
| { |
| // root is NO qualifier |
| parent.removeChild(node); |
| } |
| |
| // delete empty Schema nodes |
| if (!parent.hasChildren() && parent.getOptions().isSchemaNode()) |
| { |
| parent.getParent().removeChild(parent); |
| } |
| } |
| |
| |
| /** |
| * This is setting the value of a leaf node. |
| * |
| * @param node an XMPNode |
| * @param value a value |
| */ |
| static void setNodeValue(XMPNode node, Object value) |
| { |
| String strValue = serializeNodeValue(value); |
| if (!(node.getOptions().isQualifier() && XML_LANG.equals(node.getName()))) |
| { |
| node.setValue(strValue); |
| } |
| else |
| { |
| node.setValue(Utils.normalizeLangValue(strValue)); |
| } |
| } |
| |
| |
| /** |
| * Verifies the PropertyOptions for consistancy and updates them as needed. |
| * If options are <code>null</code> they are created with default values. |
| * |
| * @param options the <code>PropertyOptions</code> |
| * @param itemValue the node value to set |
| * @return Returns the updated options. |
| * @throws XMPException If the options are not consistant. |
| */ |
| static PropertyOptions verifySetOptions(PropertyOptions options, Object itemValue) |
| throws XMPException |
| { |
| // create empty and fix existing options |
| if (options == null) |
| { |
| // set default options |
| options = new PropertyOptions(); |
| } |
| |
| if (options.isArrayAltText()) |
| { |
| options.setArrayAlternate(true); |
| } |
| |
| if (options.isArrayAlternate()) |
| { |
| options.setArrayOrdered(true); |
| } |
| |
| if (options.isArrayOrdered()) |
| { |
| options.setArray(true); |
| } |
| |
| if (options.isCompositeProperty() && itemValue != null && itemValue.toString().length() > 0) |
| { |
| throw new XMPException("Structs and arrays can't have values", |
| XMPError.BADOPTIONS); |
| } |
| |
| options.assertConsistency(options.getOptions()); |
| |
| return options; |
| } |
| |
| |
| /** |
| * Converts the node value to String, apply special conversions for defined |
| * types in XMP. |
| * |
| * @param value |
| * the node value to set |
| * @return Returns the String representation of the node value. |
| */ |
| static String serializeNodeValue(Object value) |
| { |
| String strValue; |
| if (value == null) |
| { |
| strValue = null; |
| } |
| else if (value instanceof Boolean) |
| { |
| strValue = XMPUtils.convertFromBoolean(((Boolean) value).booleanValue()); |
| } |
| else if (value instanceof Integer) |
| { |
| strValue = XMPUtils.convertFromInteger(((Integer) value).intValue()); |
| } |
| else if (value instanceof Long) |
| { |
| strValue = XMPUtils.convertFromLong(((Long) value).longValue()); |
| } |
| else if (value instanceof Double) |
| { |
| strValue = XMPUtils.convertFromDouble(((Double) value).doubleValue()); |
| } |
| else if (value instanceof XMPDateTime) |
| { |
| strValue = XMPUtils.convertFromDate((XMPDateTime) value); |
| } |
| else if (value instanceof GregorianCalendar) |
| { |
| XMPDateTime dt = XMPDateTimeFactory.createFromCalendar((GregorianCalendar) value); |
| strValue = XMPUtils.convertFromDate(dt); |
| } |
| else if (value instanceof byte[]) |
| { |
| strValue = XMPUtils.encodeBase64((byte[]) value); |
| } |
| else |
| { |
| strValue = value.toString(); |
| } |
| |
| return strValue != null ? Utils.removeControlChars(strValue) : null; |
| } |
| |
| |
| /** |
| * After processing by ExpandXPath, a step can be of these forms: |
| * <ul> |
| * <li>qualName - A top level property or struct field. |
| * <li>[index] - An element of an array. |
| * <li>[last()] - The last element of an array. |
| * <li>[qualName="value"] - An element in an array of structs, chosen by a field value. |
| * <li>[?qualName="value"] - An element in an array, chosen by a qualifier value. |
| * <li>?qualName - A general qualifier. |
| * </ul> |
| * Find the appropriate child node, resolving aliases, and optionally creating nodes. |
| * |
| * @param parentNode the node to start to start from |
| * @param nextStep the xpath segment |
| * @param createNodes |
| * @return returns the found or created XMPPath node |
| * @throws XMPException |
| */ |
| private static XMPNode followXPathStep( |
| XMPNode parentNode, |
| XMPPathSegment nextStep, |
| boolean createNodes) throws XMPException |
| { |
| XMPNode nextNode = null; |
| int index = 0; |
| int stepKind = nextStep.getKind(); |
| |
| if (stepKind == XMPPath.STRUCT_FIELD_STEP) |
| { |
| nextNode = findChildNode(parentNode, nextStep.getName(), createNodes); |
| } |
| else if (stepKind == XMPPath.QUALIFIER_STEP) |
| { |
| nextNode = findQualifierNode( |
| parentNode, nextStep.getName().substring(1), createNodes); |
| } |
| else |
| { |
| // This is an array indexing step. First get the index, then get the node. |
| |
| if (!parentNode.getOptions().isArray()) |
| { |
| throw new XMPException("Indexing applied to non-array", XMPError.BADXPATH); |
| } |
| |
| if (stepKind == XMPPath.ARRAY_INDEX_STEP) |
| { |
| index = findIndexedItem(parentNode, nextStep.getName(), createNodes); |
| } |
| else if (stepKind == XMPPath.ARRAY_LAST_STEP) |
| { |
| index = parentNode.getChildrenLength(); |
| } |
| else if (stepKind == XMPPath.FIELD_SELECTOR_STEP) |
| { |
| String[] result = Utils.splitNameAndValue(nextStep.getName()); |
| String fieldName = result[0]; |
| String fieldValue = result[1]; |
| index = lookupFieldSelector(parentNode, fieldName, fieldValue); |
| } |
| else if (stepKind == XMPPath.QUAL_SELECTOR_STEP) |
| { |
| String[] result = Utils.splitNameAndValue(nextStep.getName()); |
| String qualName = result[0]; |
| String qualValue = result[1]; |
| index = lookupQualSelector( |
| parentNode, qualName, qualValue, nextStep.getAliasForm()); |
| } |
| else |
| { |
| throw new XMPException("Unknown array indexing step in FollowXPathStep", |
| XMPError.INTERNALFAILURE); |
| } |
| |
| if (1 <= index && index <= parentNode.getChildrenLength()) |
| { |
| nextNode = parentNode.getChild(index); |
| } |
| } |
| |
| return nextNode; |
| } |
| |
| |
| /** |
| * Find or create a qualifier node under a given parent node. Returns a pointer to the |
| * qualifier node, and optionally an iterator for the node's position in |
| * the parent's vector of qualifiers. The iterator is unchanged if no qualifier node (null) |
| * is returned. |
| * <em>Note:</em> On entry, the qualName parameter must not have the leading '?' from the |
| * XMPPath step. |
| * |
| * @param parent the parent XMPNode |
| * @param qualName the qualifier name |
| * @param createNodes flag if nodes shall be created |
| * @return Returns the qualifier node if found or created, <code>null</code> otherwise. |
| * @throws XMPException |
| */ |
| private static XMPNode findQualifierNode(XMPNode parent, String qualName, boolean createNodes) |
| throws XMPException |
| { |
| assert !qualName.startsWith("?"); |
| |
| XMPNode qualNode = parent.findQualifierByName(qualName); |
| |
| if (qualNode == null && createNodes) |
| { |
| qualNode = new XMPNode(qualName, null); |
| qualNode.setImplicit(true); |
| |
| parent.addQualifier(qualNode); |
| } |
| |
| return qualNode; |
| } |
| |
| |
| /** |
| * @param arrayNode an array node |
| * @param segment the segment containing the array index |
| * @param createNodes flag if new nodes are allowed to be created. |
| * @return Returns the index or index = -1 if not found |
| * @throws XMPException Throws Exceptions |
| */ |
| private static int findIndexedItem(XMPNode arrayNode, String segment, boolean createNodes) |
| throws XMPException |
| { |
| int index = 0; |
| |
| try |
| { |
| segment = segment.substring(1, segment.length() - 1); |
| index = Integer.parseInt(segment); |
| if (index < 1) |
| { |
| throw new XMPException("Array index must be larger than zero", |
| XMPError.BADXPATH); |
| } |
| } |
| catch (NumberFormatException e) |
| { |
| throw new XMPException("Array index not digits.", XMPError.BADXPATH); |
| } |
| |
| if (createNodes && index == arrayNode.getChildrenLength() + 1) |
| { |
| // Append a new last + 1 node. |
| XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, null); |
| newItem.setImplicit(true); |
| arrayNode.addChild(newItem); |
| } |
| |
| return index; |
| } |
| |
| |
| /** |
| * Searches for a field selector in a node: |
| * [fieldName="value] - an element in an array of structs, chosen by a field value. |
| * No implicit nodes are created by field selectors. |
| * |
| * @param arrayNode |
| * @param fieldName |
| * @param fieldValue |
| * @return Returns the index of the field if found, otherwise -1. |
| * @throws XMPException |
| */ |
| private static int lookupFieldSelector(XMPNode arrayNode, String fieldName, String fieldValue) |
| throws XMPException |
| { |
| int result = -1; |
| |
| for (int index = 1; index <= arrayNode.getChildrenLength() && result < 0; index++) |
| { |
| XMPNode currItem = arrayNode.getChild(index); |
| |
| if (!currItem.getOptions().isStruct()) |
| { |
| throw new XMPException("Field selector must be used on array of struct", |
| XMPError.BADXPATH); |
| } |
| |
| for (int f = 1; f <= currItem.getChildrenLength(); f++) |
| { |
| XMPNode currField = currItem.getChild(f); |
| if (!fieldName.equals(currField.getName())) |
| { |
| continue; |
| } |
| if (fieldValue.equals(currField.getValue())) |
| { |
| result = index; |
| break; |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| |
| /** |
| * Searches for a qualifier selector in a node: |
| * [?qualName="value"] - an element in an array, chosen by a qualifier value. |
| * No implicit nodes are created for qualifier selectors, |
| * except for an alias to an x-default item. |
| * |
| * @param arrayNode an array node |
| * @param qualName the qualifier name |
| * @param qualValue the qualifier value |
| * @param aliasForm in case the qual selector results from an alias, |
| * an x-default node is created if there has not been one. |
| * @return Returns the index of th |
| * @throws XMPException |
| */ |
| private static int lookupQualSelector(XMPNode arrayNode, String qualName, |
| String qualValue, int aliasForm) throws XMPException |
| { |
| if (XML_LANG.equals(qualName)) |
| { |
| qualValue = Utils.normalizeLangValue(qualValue); |
| int index = XMPNodeUtils.lookupLanguageItem(arrayNode, qualValue); |
| if (index < 0 && (aliasForm & AliasOptions.PROP_ARRAY_ALT_TEXT) > 0) |
| { |
| XMPNode langNode = new XMPNode(ARRAY_ITEM_NAME, null); |
| XMPNode xdefault = new XMPNode(XML_LANG, X_DEFAULT, null); |
| langNode.addQualifier(xdefault); |
| arrayNode.addChild(1, langNode); |
| return 1; |
| } |
| else |
| { |
| return index; |
| } |
| } |
| else |
| { |
| for (int index = 1; index < arrayNode.getChildrenLength(); index++) |
| { |
| XMPNode currItem = arrayNode.getChild(index); |
| |
| for (Iterator it = currItem.iterateQualifier(); it.hasNext();) |
| { |
| XMPNode qualifier = (XMPNode) it.next(); |
| if (qualName.equals(qualifier.getName()) && |
| qualValue.equals(qualifier.getValue())) |
| { |
| return index; |
| } |
| } |
| } |
| return -1; |
| } |
| } |
| |
| |
| /** |
| * Make sure the x-default item is first. Touch up "single value" |
| * arrays that have a default plus one real language. This case should have |
| * the same value for both items. Older Adobe apps were hardwired to only |
| * use the "x-default" item, so we copy that value to the other |
| * item. |
| * |
| * @param arrayNode |
| * an alt text array node |
| */ |
| static void normalizeLangArray(XMPNode arrayNode) |
| { |
| if (!arrayNode.getOptions().isArrayAltText()) |
| { |
| return; |
| } |
| |
| // check if node with x-default qual is first place |
| for (int i = 2; i <= arrayNode.getChildrenLength(); i++) |
| { |
| XMPNode child = arrayNode.getChild(i); |
| if (child.hasQualifier() && X_DEFAULT.equals(child.getQualifier(1).getValue())) |
| { |
| // move node to first place |
| try |
| { |
| arrayNode.removeChild(i); |
| arrayNode.addChild(1, child); |
| } |
| catch (XMPException e) |
| { |
| // cannot occur, because same child is removed before |
| assert false; |
| } |
| |
| if (i == 2) |
| { |
| arrayNode.getChild(2).setValue(child.getValue()); |
| } |
| break; |
| } |
| } |
| } |
| |
| |
| /** |
| * See if an array is an alt-text array. If so, make sure the x-default item |
| * is first. |
| * |
| * @param arrayNode |
| * the array node to check if its an alt-text array |
| */ |
| static void detectAltText(XMPNode arrayNode) |
| { |
| if (arrayNode.getOptions().isArrayAlternate() && arrayNode.hasChildren()) |
| { |
| boolean isAltText = false; |
| for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) |
| { |
| XMPNode child = (XMPNode) it.next(); |
| if (child.getOptions().getHasLanguage()) |
| { |
| isAltText = true; |
| break; |
| } |
| } |
| |
| if (isAltText) |
| { |
| arrayNode.getOptions().setArrayAltText(true); |
| normalizeLangArray(arrayNode); |
| } |
| } |
| } |
| |
| |
| /** |
| * Appends a language item to an alt text array. |
| * |
| * @param arrayNode the language array |
| * @param itemLang the language of the item |
| * @param itemValue the content of the item |
| * @throws XMPException Thrown if a duplicate property is added |
| */ |
| static void appendLangItem(XMPNode arrayNode, String itemLang, String itemValue) |
| throws XMPException |
| { |
| XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null); |
| XMPNode langQual = new XMPNode(XML_LANG, itemLang, null); |
| newItem.addQualifier(langQual); |
| |
| if (!X_DEFAULT.equals(langQual.getValue())) |
| { |
| arrayNode.addChild(newItem); |
| } |
| else |
| { |
| arrayNode.addChild(1, newItem); |
| } |
| } |
| |
| |
| /** |
| * <ol> |
| * <li>Look for an exact match with the specific language. |
| * <li>If a generic language is given, look for partial matches. |
| * <li>Look for an "x-default"-item. |
| * <li>Choose the first item. |
| * </ol> |
| * |
| * @param arrayNode |
| * the alt text array node |
| * @param genericLang |
| * the generic language |
| * @param specificLang |
| * the specific language |
| * @return Returns the kind of match as an Integer and the found node in an |
| * array. |
| * |
| * @throws XMPException |
| */ |
| static Object[] chooseLocalizedText(XMPNode arrayNode, String genericLang, String specificLang) |
| throws XMPException |
| { |
| // See if the array has the right form. Allow empty alt arrays, |
| // that is what parsing returns. |
| if (!arrayNode.getOptions().isArrayAltText()) |
| { |
| throw new XMPException("Localized text array is not alt-text", XMPError.BADXPATH); |
| } |
| else if (!arrayNode.hasChildren()) |
| { |
| return new Object[] { new Integer(XMPNodeUtils.CLT_NO_VALUES), null }; |
| } |
| |
| int foundGenericMatches = 0; |
| XMPNode resultNode = null; |
| XMPNode xDefault = null; |
| |
| // Look for the first partial match with the generic language. |
| for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) |
| { |
| XMPNode currItem = (XMPNode) it.next(); |
| |
| // perform some checks on the current item |
| if (currItem.getOptions().isCompositeProperty()) |
| { |
| throw new XMPException("Alt-text array item is not simple", XMPError.BADXPATH); |
| } |
| else if (!currItem.hasQualifier() |
| || !XML_LANG.equals(currItem.getQualifier(1).getName())) |
| { |
| throw new XMPException("Alt-text array item has no language qualifier", |
| XMPError.BADXPATH); |
| } |
| |
| String currLang = currItem.getQualifier(1).getValue(); |
| |
| // Look for an exact match with the specific language. |
| if (specificLang.equals(currLang)) |
| { |
| return new Object[] { new Integer(XMPNodeUtils.CLT_SPECIFIC_MATCH), currItem }; |
| } |
| else if (genericLang != null && currLang.startsWith(genericLang)) |
| { |
| if (resultNode == null) |
| { |
| resultNode = currItem; |
| } |
| // ! Don't return/break, need to look for other matches. |
| foundGenericMatches++; |
| } |
| else if (X_DEFAULT.equals(currLang)) |
| { |
| xDefault = currItem; |
| } |
| } |
| |
| // evaluate loop |
| if (foundGenericMatches == 1) |
| { |
| return new Object[] { new Integer(XMPNodeUtils.CLT_SINGLE_GENERIC), resultNode }; |
| } |
| else if (foundGenericMatches > 1) |
| { |
| return new Object[] { new Integer(XMPNodeUtils.CLT_MULTIPLE_GENERIC), resultNode }; |
| } |
| else if (xDefault != null) |
| { |
| return new Object[] { new Integer(XMPNodeUtils.CLT_XDEFAULT), xDefault }; |
| } |
| else |
| { |
| // Everything failed, choose the first item. |
| return new Object[] { new Integer(XMPNodeUtils.CLT_FIRST_ITEM), arrayNode.getChild(1) }; |
| } |
| } |
| |
| |
| /** |
| * Looks for the appropriate language item in a text alternative array.item |
| * |
| * @param arrayNode |
| * an array node |
| * @param language |
| * the requested language |
| * @return Returns the index if the language has been found, -1 otherwise. |
| * @throws XMPException |
| */ |
| static int lookupLanguageItem(XMPNode arrayNode, String language) throws XMPException |
| { |
| if (!arrayNode.getOptions().isArray()) |
| { |
| throw new XMPException("Language item must be used on array", XMPError.BADXPATH); |
| } |
| |
| for (int index = 1; index <= arrayNode.getChildrenLength(); index++) |
| { |
| XMPNode child = arrayNode.getChild(index); |
| if (!child.hasQualifier() || !XML_LANG.equals(child.getQualifier(1).getName())) |
| { |
| continue; |
| } |
| else if (language.equals(child.getQualifier(1).getValue())) |
| { |
| return index; |
| } |
| } |
| |
| return -1; |
| } |
| } |