/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jempbox.xmp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.transform.TransformerException; import org.apache.jempbox.impl.XMLUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.xml.sax.InputSource; /** * This class represents the top level XMP data structure and gives access to * the various schemas that are available as part of the XMP specification. * * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a> * @version $Revision: 1.10 $ */ public class XMPMetadata { /** * Supported encoding for persisted XML. */ public static final String ENCODING_UTF8 = "UTF-8"; /** * Supported encoding for persisted XML. */ public static final String ENCODING_UTF16BE = "UTF-16BE"; /** * Supported encoding for persisted XML. */ public static final String ENCODING_UTF16LE = "UTF-16LE"; /** * The DOM representation of the metadata. */ protected Document xmpDocument; /** * The encoding of the XMP document. Default is UTF8. */ protected String encoding = ENCODING_UTF8; /** * A mapping of namespaces. */ protected Map<String,Class<?>> nsMappings = new HashMap<String,Class<?>>(); /** * Default constructor, creates blank XMP doc. * * @throws IOException * If there is an error creating the initial document. */ public XMPMetadata() throws IOException { xmpDocument = XMLUtil.newDocument(); ProcessingInstruction beginXPacket = xmpDocument .createProcessingInstruction("xpacket", "begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\""); xmpDocument.appendChild(beginXPacket); Element xmpMeta = xmpDocument.createElementNS("adobe:ns:meta/", "x:xmpmeta"); xmpMeta.setAttributeNS(XMPSchema.NS_NAMESPACE, "xmlns:x", "adobe:ns:meta/"); xmpDocument.appendChild(xmpMeta); Element rdf = xmpDocument.createElement("rdf:RDF"); rdf.setAttributeNS(XMPSchema.NS_NAMESPACE, "xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); xmpMeta.appendChild(rdf); ProcessingInstruction endXPacket = xmpDocument .createProcessingInstruction("xpacket", "end=\"w\""); xmpDocument.appendChild(endXPacket); init(); } /** * Constructor from an existing XML document. * * @param doc * The root XMP document. */ public XMPMetadata(Document doc) { xmpDocument = doc; init(); } private void init() { nsMappings.put(XMPSchemaPDF.NAMESPACE, XMPSchemaPDF.class); nsMappings.put(XMPSchemaBasic.NAMESPACE, XMPSchemaBasic.class); nsMappings .put(XMPSchemaDublinCore.NAMESPACE, XMPSchemaDublinCore.class); nsMappings.put(XMPSchemaMediaManagement.NAMESPACE, XMPSchemaMediaManagement.class); nsMappings.put(XMPSchemaRightsManagement.NAMESPACE, XMPSchemaRightsManagement.class); nsMappings.put(XMPSchemaBasicJobTicket.NAMESPACE, XMPSchemaBasicJobTicket.class); nsMappings.put(XMPSchemaDynamicMedia.NAMESPACE, XMPSchemaDynamicMedia.class); nsMappings.put(XMPSchemaPagedText.NAMESPACE, XMPSchemaPagedText.class); nsMappings.put(XMPSchemaIptc4xmpCore.NAMESPACE, XMPSchemaIptc4xmpCore.class); nsMappings.put(XMPSchemaPhotoshop.NAMESPACE, XMPSchemaPhotoshop.class); } /** * Will add a XMPSchema to the set of identified schemas. * * The class needs to have a constructor with parameter Element * * @param namespace * The namespace URI of the schmema for instance * http://purl.org/dc/elements/1.1/. * @param xmpSchema * The schema to associated this identifier with. */ public void addXMLNSMapping(String namespace, Class<?> xmpSchema) { if (!(XMPSchema.class.isAssignableFrom(xmpSchema))) { throw new IllegalArgumentException( "Only XMPSchemas can be mapped to."); } nsMappings.put(namespace, xmpSchema); } /** * Get the PDF Schema. * * @return The first PDF schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaPDF getPDFSchema() throws IOException { return (XMPSchemaPDF) getSchemaByClass(XMPSchemaPDF.class); } /** * Get the Basic Schema. * * @return The first Basic schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaBasic getBasicSchema() throws IOException { return (XMPSchemaBasic) getSchemaByClass(XMPSchemaBasic.class); } /** * Get the Dublin Core Schema. * * @return The first Dublin schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaDublinCore getDublinCoreSchema() throws IOException { return (XMPSchemaDublinCore) getSchemaByClass(XMPSchemaDublinCore.class); } /** * Get the Media Management Schema. * * @return The first Media Management schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaMediaManagement getMediaManagementSchema() throws IOException { return (XMPSchemaMediaManagement) getSchemaByClass(XMPSchemaMediaManagement.class); } /** * Get the Schema Rights Schema. * * @return The first Schema Rights schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaRightsManagement getRightsManagementSchema() throws IOException { return (XMPSchemaRightsManagement) getSchemaByClass(XMPSchemaRightsManagement.class); } /** * Get the Job Ticket Schema. * * @return The first Job Ticket schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaBasicJobTicket getBasicJobTicketSchema() throws IOException { return (XMPSchemaBasicJobTicket) getSchemaByClass(XMPSchemaBasicJobTicket.class); } /** * Get the Dynamic Media Schema. * * @return The first Dynamic Media schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaDynamicMedia getDynamicMediaSchema() throws IOException { return (XMPSchemaDynamicMedia) getSchemaByClass(XMPSchemaDynamicMedia.class); } /** * Get the Paged Text Schema. * * @return The first Paged Text schema in the list. * * @throws IOException * If there is an error accessing the schema. */ public XMPSchemaPagedText getPagedTextSchema() throws IOException { return (XMPSchemaPagedText) getSchemaByClass(XMPSchemaPagedText.class); } /** * Add a new Media Management schema. * * @return The newly added schema. */ public XMPSchemaMediaManagement addMediaManagementSchema() { XMPSchemaMediaManagement schema = new XMPSchemaMediaManagement(this); return (XMPSchemaMediaManagement) basicAddSchema(schema); } /** * Add a new Rights Managment schema. * * @return The newly added schema. */ public XMPSchemaRightsManagement addRightsManagementSchema() { XMPSchemaRightsManagement schema = new XMPSchemaRightsManagement(this); return (XMPSchemaRightsManagement) basicAddSchema(schema); } /** * Add a new Job Ticket schema. * * @return The newly added schema. */ public XMPSchemaBasicJobTicket addBasicJobTicketSchema() { XMPSchemaBasicJobTicket schema = new XMPSchemaBasicJobTicket(this); return (XMPSchemaBasicJobTicket) basicAddSchema(schema); } /** * Add a new Dynamic Media schema. * * @return The newly added schema. */ public XMPSchemaDynamicMedia addDynamicMediaSchema() { XMPSchemaDynamicMedia schema = new XMPSchemaDynamicMedia(this); return (XMPSchemaDynamicMedia) basicAddSchema(schema); } /** * Add a new Paged Text schema. * * @return The newly added schema. */ public XMPSchemaPagedText addPagedTextSchema() { XMPSchemaPagedText schema = new XMPSchemaPagedText(this); return (XMPSchemaPagedText) basicAddSchema(schema); } /** * Add a custom schema to the root rdf. The schema has to have been created * as a child of this XMPMetadata. * * @param schema * The schema to add. */ public void addSchema(XMPSchema schema) { Element rdf = getRDFElement(); rdf.appendChild(schema.getElement()); } /** * Save the XMP document to a file. * * @param file * The file to save the XMP document to. * * @throws Exception * If there is an error while writing to the stream. */ public void save(String file) throws Exception { XMLUtil.save(xmpDocument, file, encoding); } /** * Save the XMP document to a stream. * * @param outStream * The stream to save the XMP document to. * * @throws TransformerException * If there is an error while writing to the stream. */ public void save(OutputStream outStream) throws TransformerException { XMLUtil.save(xmpDocument, outStream, encoding); } /** * Get the XML document as a byte array. * * @return The metadata as an XML byte stream. * @throws Exception * If there is an error creating the stream. */ public byte[] asByteArray() throws Exception { return XMLUtil.asByteArray(xmpDocument, encoding); } /** * Get the XML document from this object. * * @return This object as an XML document. */ public Document getXMPDocument() { return xmpDocument; } /** * Generic add schema method. * * @param schema * The schema to add. * * @return The newly added schema. */ protected XMPSchema basicAddSchema(XMPSchema schema) { Element rdf = getRDFElement(); rdf.appendChild(schema.getElement()); return schema; } /** * Create and add a new PDF Schema to this metadata. Typically a XMP * document will only have one PDF schema (but multiple are supported) so it * is recommended that you first check the existence of a PDF scheme by * using getPDFSchema() * * @return A new blank PDF schema that is now part of the metadata. */ public XMPSchemaPDF addPDFSchema() { XMPSchemaPDF schema = new XMPSchemaPDF(this); return (XMPSchemaPDF) basicAddSchema(schema); } /** * Create and add a new Dublin Core Schema to this metadata. Typically a XMP * document will only have one schema for each type (but multiple are * supported) so it is recommended that you first check the existence of a * this scheme by using getDublinCoreSchema() * * @return A new blank PDF schema that is now part of the metadata. */ public XMPSchemaDublinCore addDublinCoreSchema() { XMPSchemaDublinCore schema = new XMPSchemaDublinCore(this); return (XMPSchemaDublinCore) basicAddSchema(schema); } /** * Create and add a new Basic Schema to this metadata. Typically a XMP * document will only have one schema for each type (but multiple are * supported) so it is recommended that you first check the existence of a * this scheme by using getDublinCoreSchema() * * @return A new blank PDF schema that is now part of the metadata. */ public XMPSchemaBasic addBasicSchema() { XMPSchemaBasic schema = new XMPSchemaBasic(this); return (XMPSchemaBasic) basicAddSchema(schema); } /** * Create and add a new IPTC schema to this metadata. * * @return A new blank IPTC schema that is now part of the metadata. */ public XMPSchemaIptc4xmpCore addIptc4xmpCoreSchema() { XMPSchemaIptc4xmpCore schema = new XMPSchemaIptc4xmpCore(this); return (XMPSchemaIptc4xmpCore) basicAddSchema(schema); } /** * Create and add a new Photoshop schema to this metadata. * * @return A new blank Photoshop schema that is now part of the metadata. */ public XMPSchemaPhotoshop addPhotoshopSchema() { XMPSchemaPhotoshop schema = new XMPSchemaPhotoshop(this); return (XMPSchemaPhotoshop) basicAddSchema(schema); } /** * The encoding used to write the XML. Default value:UTF-8<br/> See the * ENCODING_XXX constants * * @param xmlEncoding * The encoding to write the XML as. */ public void setEncoding(String xmlEncoding) { encoding = xmlEncoding; } /** * Get the current encoding that will be used to write the XML. * * @return The current encoding to write the XML to. */ public String getEncoding() { return encoding; } /** * Get the root RDF element. * * @return The root RDF element. */ private Element getRDFElement() { Element rdf = null; NodeList nodes = xmpDocument.getElementsByTagName("rdf:RDF"); if (nodes.getLength() > 0) { rdf = (Element) nodes.item(0); } return rdf; } /** * Load metadata from the filesystem. * * @param file * The file to load the metadata from. * * @return The loaded XMP document. * * @throws IOException * If there is an error reading the data. */ public static XMPMetadata load(String file) throws IOException { return new XMPMetadata(XMLUtil.parse(file)); } /** * Load a schema from an input source. * * @param is * The input source to load the schema from. * * @return The loaded/parsed schema. * * @throws IOException * If there was an error while loading the schema. */ public static XMPMetadata load(InputSource is) throws IOException { return new XMPMetadata(XMLUtil.parse(is)); } /** * Load metadata from the filesystem. * * @param is * The stream to load the data from. * * @return The loaded XMP document. * * @throws IOException * If there is an error reading the data. */ public static XMPMetadata load(InputStream is) throws IOException { return new XMPMetadata(XMLUtil.parse(is)); } /** * Test main program. * * @param args * The command line arguments. * @throws Exception * If there is an error. */ public static void main(String[] args) throws Exception { XMPMetadata metadata = new XMPMetadata(); XMPSchemaPDF pdf = metadata.addPDFSchema(); pdf.setAbout("uuid:b8659d3a-369e-11d9-b951-000393c97fd8"); pdf.setKeywords("ben,bob,pdf"); pdf.setPDFVersion("1.3"); pdf.setProducer("Acrobat Distiller 6.0.1 for Macintosh"); XMPSchemaDublinCore dc = metadata.addDublinCoreSchema(); dc.addContributor("Ben Litchfield"); dc.addContributor("Solar Eclipse"); dc.addContributor("Some Other Guy"); XMPSchemaBasic basic = metadata.addBasicSchema(); Thumbnail t = new Thumbnail(metadata); t.setFormat(Thumbnail.FORMAT_JPEG); t.setImage("IMAGE_DATA"); t.setHeight(new Integer(100)); t.setWidth(new Integer(200)); basic.setThumbnail(t); basic.setBaseURL("http://www.pdfbox.org/"); List<XMPSchema> schemas = metadata.getSchemas(); System.out.println("schemas=" + schemas); metadata.save("test.xmp"); } /** * This will get a list of XMPSchema(or subclass) objects. * * @return A non null read-only list of schemas that are part of this * metadata. * * @throws IOException * If there is an error creating a specific schema. */ public List<XMPSchema> getSchemas() throws IOException { NodeList schemaList = xmpDocument .getElementsByTagName("rdf:Description"); List<XMPSchema> retval = new ArrayList<XMPSchema>(schemaList.getLength()); for (int i = 0; i < schemaList.getLength(); i++) { Element schema = (Element) schemaList.item(i); boolean found = false; NamedNodeMap attributes = schema.getAttributes(); for (int j = 0; j < attributes.getLength(); j++) { Node attribute = attributes.item(j); String name = attribute.getNodeName(); String value = attribute.getNodeValue(); if (name.startsWith("xmlns:") && nsMappings.containsKey(value)) { Class<?> schemaClass = nsMappings.get(value); try { Constructor<?> ctor = schemaClass .getConstructor(new Class[] { Element.class, String.class }); retval.add((XMPSchema)ctor.newInstance(new Object[] { schema, name.substring(6) })); found = true; } catch(NoSuchMethodException e) { throw new IOException( "Error: Class " + schemaClass.getName() + " must have a constructor with the signature of " + schemaClass.getName() + "( org.w3c.dom.Element, java.lang.String )"); } catch(Exception e) { e.printStackTrace(); throw new IOException(e.getMessage()); } } } if (!found) { retval.add(new XMPSchema(schema, null)); } } return retval; } /** * Will return all schemas that fit the given namespaceURI. Which is only * done by using the namespace mapping (nsMapping) and not by actually * checking the xmlns property. * * @param namespaceURI * The namespaceURI to filter for. * @return A list containing the found schemas or an empty list if non are * found or the namespaceURI could not be found in the namespace * mapping. * @throws IOException * If an operation on the document fails. */ public List<XMPSchema> getSchemasByNamespaceURI(String namespaceURI) throws IOException { List<XMPSchema> l = getSchemas(); List<XMPSchema> result = new LinkedList<XMPSchema>(); Class<?> schemaClass = nsMappings.get(namespaceURI); if (schemaClass == null) { return result; } Iterator<XMPSchema> i = l.iterator(); while (i.hasNext()) { XMPSchema schema = i.next(); if (schemaClass.isAssignableFrom(schema.getClass())) { result.add(schema); } } return result; } /** * This will return true if the XMP contains an unknown schema. * * @return True if an unknown schema is found, false otherwise * * @throws IOException * If there is an error */ public boolean hasUnknownSchema() throws IOException { NodeList schemaList = xmpDocument .getElementsByTagName("rdf:Description"); for (int i = 0; i < schemaList.getLength(); i++) { Element schema = (Element) schemaList.item(i); NamedNodeMap attributes = schema.getAttributes(); for (int j = 0; j < attributes.getLength(); j++) { Node attribute = attributes.item(j); String name = attribute.getNodeName(); String value = attribute.getNodeValue(); if (name.startsWith("xmlns:") && !nsMappings.containsKey(value) && !value.equals(ResourceEvent.NAMESPACE)) { return true; } } } return false; } /** * Tries to retrieve a schema from this by classname. * * @param targetSchema * Class for targetSchema. * * @return XMPSchema or null if no target is found. * * @throws IOException * if there was an error creating the schemas of this. */ public XMPSchema getSchemaByClass(Class<?> targetSchema) throws IOException { Iterator<XMPSchema> iter = getSchemas().iterator(); while (iter.hasNext()) { XMPSchema element = (XMPSchema) iter.next(); if (element.getClass().getName().equals(targetSchema.getName())) { return element; } } // not found return null; } /** * Merge this metadata with the given metadata object. * * @param metadata The metadata to merge with this document. * * @throws IOException If there is an error merging the data. */ public void merge(XMPMetadata metadata) throws IOException { List<XMPSchema> schemas2 = metadata.getSchemas(); for (Iterator<XMPSchema> iterator = schemas2.iterator(); iterator.hasNext();) { XMPSchema schema2 = iterator.next(); XMPSchema schema1 = getSchemaByClass(schema2.getClass()); if (schema1 == null) { Element rdf = getRDFElement(); rdf.appendChild(xmpDocument.importNode(schema2.getElement(), true)); } else { schema1.merge(schema2); } } } }