/* * Copyright (C) 2012 Jan Pokorsky * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package cz.cas.lib.proarc.common.fedora; import com.yourmediashelf.fedora.client.FedoraClientException; import com.yourmediashelf.fedora.generated.foxml.DatastreamType; import com.yourmediashelf.fedora.generated.foxml.DatastreamVersionType; import com.yourmediashelf.fedora.generated.foxml.DigitalObject; import com.yourmediashelf.fedora.generated.foxml.ObjectFactory; import com.yourmediashelf.fedora.generated.foxml.ObjectPropertiesType; import com.yourmediashelf.fedora.generated.foxml.PropertyType; import com.yourmediashelf.fedora.generated.foxml.StateType; import com.yourmediashelf.fedora.generated.management.DatastreamProfile; import cz.cas.lib.proarc.oaidublincore.DcConstants; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response.Status; import javax.xml.XMLConstants; import javax.xml.bind.DataBindingException; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlSchema; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.w3c.dom.Element; /** * * @author Jan Pokorsky */ public final class FoxmlUtils { public static final String FOXML_NAMESPACE; /** The audit data stream ID. */ public static final String DS_AUDIT_ID = "AUDIT"; public static final String PID_PREFIX = "uuid:"; static final String LOCAL_FEDORA_OBJ_PATH = "http://local.fedora.server/fedora/get/"; static { XmlSchema schema = ObjectFactory.class.getPackage().getAnnotation(XmlSchema.class); FOXML_NAMESPACE = schema.namespace(); assert FOXML_NAMESPACE != null; } public static final String PROPERTY_CREATEDATE = "info:fedora/fedora-system:def/model#createdDate"; public static final String PROPERTY_LABEL = "info:fedora/fedora-system:def/model#label"; public static final String PROPERTY_LASTMODIFIED = "info:fedora/fedora-system:def/view#lastModifiedDate"; public static final String PROPERTY_OWNER = "info:fedora/fedora-system:def/model#ownerId"; public static final String PROPERTY_STATE = "info:fedora/fedora-system:def/model#state"; private static final Logger LOG = Logger.getLogger(FoxmlUtils.class.getName()); private static JAXBContext defaultJaxbContext; private static ThreadLocal<Marshaller> defaultMarshaller = new ThreadLocal<Marshaller>(); private static ThreadLocal<Unmarshaller> defaultUnmarshaller = new ThreadLocal<Unmarshaller>(); private static final Pattern PID_PATTERN = Pattern.compile( "^([A-Za-z0-9]|-|\\.)+:(([A-Za-z0-9])|-|\\.|~|_|(%[0-9A-F]{2}))+$"); /** * Default FOXML context. Oracle JAXB RI's context should be thread safe. * @see <a href='http://jaxb.java.net/faq/index.html#threadSafety'>Are the JAXB runtime API's thread safe?</a> */ public static JAXBContext defaultJaxbContext() throws JAXBException { if (defaultJaxbContext == null) { defaultJaxbContext = JAXBContext.newInstance(ObjectFactory.class); } return defaultJaxbContext; } /** * Default FOXML marshaller for current thread. */ public static Marshaller defaultMarshaller(boolean indent) throws JAXBException { Marshaller m = defaultMarshaller.get(); if (m == null) { // later we could use a pool to minimize Marshaller instances m = defaultJaxbContext().createMarshaller(); defaultMarshaller.set(m); m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, FOXML_NAMESPACE + " http://www.fedora.info/definitions/1/0/foxml1-1.xsd"); m.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); } m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, indent); return m; } /** * Default FOXML marshaller for current thread. */ public static Unmarshaller defaultUnmarshaller() throws JAXBException { Unmarshaller m = defaultUnmarshaller.get(); if (m == null) { m = defaultJaxbContext().createUnmarshaller(); defaultUnmarshaller.set(m); } return m; } public static void setProperty(DigitalObject dobj, String name, String value) { PropertyType propery = findProperty(dobj, name); if (value == null) { if (propery != null) { dobj.getObjectProperties().getProperty().remove(propery); } return ; } if (propery == null) { propery = createProperty(dobj, name); } propery.setVALUE(value); } private static PropertyType createProperty(DigitalObject dobj, String name) { ObjectPropertiesType objectProperties = dobj.getObjectProperties(); if (objectProperties == null) { objectProperties = new ObjectPropertiesType(); dobj.setObjectProperties(objectProperties); } PropertyType property = new PropertyType(); property.setNAME(name); objectProperties.getProperty().add(property); return property; } public static PropertyType findProperty(DigitalObject dobj, String name) { ObjectPropertiesType objectProperties = dobj.getObjectProperties(); if (objectProperties != null) { return findProperty(objectProperties, name); } return null; } public static PropertyType findProperty(ObjectPropertiesType objectProperties, String name) { for (PropertyType property : objectProperties.getProperty()) { if (property.getNAME().equals(name)) { return property; } } return null; } public static DatastreamVersionType createDataStreamVersion(DigitalObject dobj, String dsId, ControlGroup controlGroup, boolean versionable, StateType state) { DatastreamVersionType datastreamVersion = null; DatastreamType datastream = findDatastream(dobj, dsId); if (datastream == null) { datastream = createDatastream(dsId, controlGroup, versionable, state); dobj.getDatastream().add(datastream); } List<DatastreamVersionType> versions = datastream.getDatastreamVersion(); datastreamVersion = findDatastreamVersion(datastream); if (datastreamVersion == null) { datastreamVersion = new DatastreamVersionType(); versions.add(datastreamVersion); // for now expect version ordering from oldest to newest datastreamVersion.setID(versionNewId(dsId, versions.size() - 1)); } return datastreamVersion; } public static String versionDefaultId(String dsId) { return versionNewId(dsId, 0); } private static String versionNewId(String dsId, int existingVersionCount) { return String.format("%s.%s", dsId, existingVersionCount); } public static DatastreamVersionType findDataStreamVersion(DigitalObject dobj, String dsId) { DatastreamVersionType datastreamVersion = null; if (dobj != null) { DatastreamType datastream = findDatastream(dobj, dsId); if (datastream != null) { datastreamVersion = findDatastreamVersion(datastream); } } return datastreamVersion; } public static DatastreamType findDatastream(DigitalObject dobj, String dsId) { for (DatastreamType datastream : dobj.getDatastream()) { String id = datastream.getID(); if (dsId.equals(id)) { return datastream; } } return null; } /** * Finds newest version. * For now expects versions ordering from oldest to newest. */ private static DatastreamVersionType findDatastreamVersion(DatastreamType datastream) { List<DatastreamVersionType> versions = datastream.getDatastreamVersion(); return versions.isEmpty() ? null : versions.get(versions.size() - 1); } /** * Translates {@link DatastreamType} to {@link DatastreamProfile}. It searches * for the newest version. * <p>For now it fills only description! */ public static DatastreamProfile toDatastreamProfile(String pid, DatastreamType datastream) { DatastreamVersionType dv = findDatastreamVersion(datastream); return dv == null ? null : toDatastreamProfile(pid, dv, datastream); } /** * Translates {@link DatastreamType} to {@link DatastreamProfile}. * For now it fills only description! */ public static DatastreamProfile toDatastreamProfile(String pid, DatastreamVersionType version, DatastreamType datastream) { DatastreamProfile profile = new DatastreamProfile(); profile.setDsID(datastream.getID()); profile.setDsLabel(version.getLABEL()); profile.setDsCreateDate(version.getCREATED()); profile.setDsFormatURI(version.getFORMATURI()); profile.setDsMIME(version.getMIMETYPE()); profile.setDateTime(version.getCREATED()); // profile.setDsChecksum(); profile.setDsControlGroup(datastream.getCONTROLGROUP()); profile.setDsState(datastream.getSTATE().value()); // profile.setDsVersionID(); // profile.setPid(datastream.); return profile; } /** * Translates {@link com.yourmediashelf.fedora.generated.access.DatastreamType} * to {@link DatastreamProfile}. It searches * for the newest version. * <p>For now it fills only description! */ public static DatastreamProfile toDatastreamProfile(String pid, com.yourmediashelf.fedora.generated.access.DatastreamType datastream) { DatastreamProfile profile = new DatastreamProfile(); profile.setDsID(datastream.getDsid()); profile.setDsLabel(datastream.getLabel()); profile.setDsMIME(datastream.getMimeType()); return profile; } /** * Dumps FOXML object to XML string. */ public static String toXml(DigitalObject dobj, boolean indent) { StringWriter dump = new StringWriter(); marshal(new StreamResult(dump), dobj, indent); return dump.toString(); } public static void marshal(Result target, DigitalObject dobj, boolean indent) { try { Marshaller m = defaultMarshaller(indent); m.marshal(dobj, target); } catch (JAXBException ex) { throw new DataBindingException(ex); } } public static <T> T unmarshal(String source, Class<T> type) { return unmarshal(new StreamSource(new StringReader(source)), type); } public static <T> T unmarshal(URL source, Class<T> type) { return unmarshal(new StreamSource(source.toExternalForm()), type); } public static <T> T unmarshal(Source source, Class<T> type) { try { JAXBElement<T> item = defaultUnmarshaller().unmarshal(source, type); return item.getValue(); } catch (JAXBException ex) { throw new DataBindingException(ex); } } public static XMLGregorianCalendar createXmlDate() { return createXmlDate(new Date()); } public static XMLGregorianCalendar createXmlDate(Date d) { try { DatatypeFactory xmlDataFactory = DatatypeFactory.newInstance(); GregorianCalendar gcNow = new GregorianCalendar(); gcNow.setTime(d); return xmlDataFactory.newXMLGregorianCalendar(gcNow); } catch (DatatypeConfigurationException ex) { throw new IllegalStateException(ex); } } /** * Creates new unique PID for digital object. * * @return PID */ public static String createPid() { UUID uuid = UUID.randomUUID(); return pidFromUuid(uuid.toString()); } /** * Converts PID to UUID. * * @param pid PID of digital object * @return UUID */ public static String pidAsUuid(String pid) { if (pid == null) { throw new NullPointerException("pid"); } else if (!pid.startsWith(PID_PREFIX)) { throw new IllegalArgumentException("Invalid PID format: '" + pid + "'!"); } return pid.substring(PID_PREFIX.length()); } /** * Is a valid Fedora PID? * @see <a href='https://wiki.duraspace.org/display/FEDORA37/Fedora+Identifiers#FedoraIdentifiers-PIDspids'>Fedora PID</a> */ public static boolean isValidPid(String pid) { return pid != null && pid.length() <= 64 && PID_PATTERN.matcher(pid).matches(); } /** * Converts UUID to PID. * * @param uuid UUID * @return PID of digital object */ public static String pidFromUuid(String uuid) { if (uuid == null) { throw new NullPointerException("uuid"); } return PID_PREFIX + uuid; } public static DigitalObject createFoxml(String pid) { DigitalObject digObj = new DigitalObject(); digObj.setPID(pid); digObj.setVERSION("1.1"); // state property is required by foxml1-1.xsd setProperty(digObj, PROPERTY_STATE, StateType.A.value()); return digObj; } private static DatastreamType createDatastream(String id, ControlGroup controlGroup, boolean versionable, StateType state) { DatastreamType ds = new DatastreamType(); ds.setID(id); ds.setCONTROLGROUP(controlGroup.toExternal()); ds.setVERSIONABLE(versionable); ds.setSTATE(state); return ds; } public static void closeQuietly(Closeable c, String description) { if (c != null) { try { c.close(); } catch (IOException ex) { LOG.log(Level.SEVERE, description, ex); } } } public static void copy(InputStream is, OutputStream os) throws IOException { byte[] buffer = new byte[2048]; for (int length; (length = is.read(buffer)) != -1; ) { os.write(buffer, 0, length); } } private static DatastreamProfile createProfileTemplate(String dsId, String formatUri, String label, MediaType mimetype, ControlGroup control) { DatastreamProfile df = new DatastreamProfile(); df.setDsMIME(mimetype.toString()); df.setDsID(dsId); df.setDsControlGroup(control.toExternal()); df.setDsFormatURI(formatUri); df.setDsLabel(label); df.setDsVersionID(FoxmlUtils.versionDefaultId(dsId)); df.setDsVersionable(Boolean.FALSE.toString()); return df; } /** * Use to embed XML content into FOXML. It expects you read and write * well formed XML. */ public static DatastreamProfile inlineProfile(String dsId, String formatUri, String label) { if (dsId == null || dsId.isEmpty()) { throw new IllegalArgumentException(); } return createProfileTemplate(dsId, formatUri, label, MediaType.TEXT_XML_TYPE, ControlGroup.INLINE); } /** * Use to store XML content outside FOXML. It expects you read and write * well formed XML. */ public static DatastreamProfile managedProfile(String dsId, String formatUri, String label) { if (dsId == null || dsId.isEmpty()) { throw new IllegalArgumentException(); } return createProfileTemplate(dsId, formatUri, label, MediaType.TEXT_XML_TYPE, ControlGroup.MANAGED); } /** * Use to store whatever content in repository. I */ public static DatastreamProfile managedProfile(String dsId, MediaType mimetype, String label) { if (dsId == null || dsId.isEmpty() || mimetype == null) { throw new IllegalArgumentException(); } return createProfileTemplate(dsId, null, label, mimetype, ControlGroup.MANAGED); } /** * Use to store whatever external content. The contents must be accessible * when its URL is being written to the stream! */ public static DatastreamProfile externalProfile(String dsId, MediaType mimetype, String label) { if (dsId == null || dsId.isEmpty() || mimetype == null) { throw new IllegalArgumentException(); } return createProfileTemplate(dsId, null, label, mimetype, ControlGroup.EXTERNAL); } public static URI localFedoraUri(String pid) throws URISyntaxException { return localFedoraUri(pid, null); } /** * Creates an object or datastream URI independent on a Fedora instance. * The URI is resolvable just inside a given Fedora context. * @param pid object PID * @param dsId datastream ID * @return the portable URI * @throws URISyntaxException failure */ public static URI localFedoraUri(String pid, String dsId) throws URISyntaxException { if (pid == null || pid.isEmpty()) { throw new URISyntaxException(pid, "Invalid PID."); } StringBuilder sb = new StringBuilder(LOCAL_FEDORA_OBJ_PATH.length() + 100); sb.append(LOCAL_FEDORA_OBJ_PATH).append(pid); if (dsId != null) { sb.append('/').append(dsId); } URI location = new URI(sb.toString()); return location; } /** * Check whether failure stands for a missing requested datastream. */ public static boolean missingDatastream(FedoraClientException ex) { if (ex.getStatus() == Status.NOT_FOUND.getStatusCode()) { // Missing datastream message: // HTTP 404 Error: No datastream could be found. Either there is no datastream for the digital object "uuid:5c3caa12-1e82-4670-a6aa-3d9ff8a7a3c5" with datastream ID of "TEXT_OCR" OR there are no datastreams that match the specified date/time value of "null". // Missing object message: // HTTP 404 Error: uuid:5c3caa12-1e82-4670-a6aa-3d9ff8a7a3c56 // To check message see fcrepo-server/src/main/java/org/fcrepo/server/rest/DatastreamResource.java return ex.getMessage().contains("No datastream"); } return false; } /** * Fixes DublinCore exported by Fedora Commons. * <p>The {@code schemaLocation} is removed. * <br>The namespace declaration is added to the root element. * @param dcRoot the root element of DC * @return the root element of DC */ public static Element fixFoxmlDc(Element dcRoot) { // remove xsi:schemaLocation attribute to make FOXML valid for Fedora ingest dcRoot.removeAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation"); // optimize XML namespace declaration dcRoot.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLConstants.XMLNS_ATTRIBUTE + ":" + DcConstants.PREFIX_NS_PURL, DcConstants.NS_PURL); return dcRoot; } public enum ControlGroup { INLINE("X"), MANAGED("M"), EXTERNAL("E"), REDIRECT("R"); private final String external; private ControlGroup(String external) { this.external = external; } public String toExternal() { return external; } public static ControlGroup fromExternal(String external) { for (ControlGroup group : values()) { if (group.toExternal().equals(external)) { return group; } } return null; } } }