/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package org.fcrepo.server.storage.translation; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.Iterator; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.abdera.Abdera; import org.apache.abdera.ext.thread.ThreadHelper; import org.apache.abdera.i18n.iri.IRI; import org.apache.abdera.model.Entry; import org.apache.abdera.model.Feed; import org.apache.abdera.model.Link; import org.apache.abdera.util.MimeTypeHelper; import org.apache.commons.io.IOUtils; import org.fcrepo.common.Constants; import org.fcrepo.common.Models; import org.fcrepo.common.PID; import org.fcrepo.common.xml.format.XMLFormat; import org.fcrepo.server.errors.ObjectIntegrityException; import org.fcrepo.server.errors.StreamIOException; import org.fcrepo.server.storage.types.Datastream; import org.fcrepo.server.storage.types.DatastreamXMLMetadata; import org.fcrepo.server.storage.types.DigitalObject; import org.fcrepo.server.utilities.StreamUtility; import org.fcrepo.utilities.DateUtility; import org.fcrepo.utilities.MimeTypeUtils; import org.fcrepo.utilities.ReadableCharArrayWriter; /** * <p>Serializes a Fedora Object in Atom with Threading Extensions.</p> * * <p>A Fedora Digital Object is represented as an atom:feed and * Datastreams are represented as an atom:entries.</p> * * <p>The hierarchy of Datastreams their Datastream Versions is * represented via the Atom Threading Extensions. * For convenience, a datastream entry references its latest datastream * version entry with an atom:link element. For example, a DC datastream * entry with a reference to its most recent version: <br> * <code><link href="info:fedora/demo:foo/DC/2008-04-01T12:30:15.123" rel="alternate"/></code></p> * * <p>Each datastream version refers to its parent datastream via a * thr:in-reply-to element. For example, the entry for a DC datastream * version would include:<br> * <code><thr:in-reply-to ref="info:fedora/demo:foo/DC"/></code></p> * * @see <a href="http://atomenabled.org/developers/syndication/atom-format-spec.php">The Atom Syndication Format</a> * @see <a href="http://www.ietf.org/rfc/rfc4685.txt">Atom Threading Extensions</a> * * @author Edwin Shin * @since 3.0 * @version $Id$ */ public class AtomDOSerializer implements DOSerializer, Constants { /** * The format this serializer will write if unspecified at construction. * This defaults to the latest ATOM format. */ public static final XMLFormat DEFAULT_FORMAT = ATOM1_1; private final static Abdera abdera = Abdera.getInstance(); /** The format this serializer writes. */ private final XMLFormat m_format; /** The translation utility is use */ private DOTranslationUtility m_translator; public AtomDOSerializer() { this(DEFAULT_FORMAT); } public AtomDOSerializer(XMLFormat format) { this(format, null); } public AtomDOSerializer(XMLFormat format, DOTranslationUtility translator) { if (format.equals(ATOM1_1) || format.equals(ATOM_ZIP1_1)) { m_format = format; } else { throw new IllegalArgumentException("Not an ATOM format: " + format.uri); } m_translator = (translator == null) ? DOTranslationUtility.defaultInstance() : translator; } /** * {@inheritDoc} */ public DOSerializer getInstance() { return this; } /** * {@inheritDoc} */ public void serialize(DigitalObject obj, OutputStream out, String encoding, int transContext) throws ObjectIntegrityException, StreamIOException, UnsupportedEncodingException { if (encoding == null || encoding == "")encoding = "UTF-8"; Feed feed = abdera.newFeed(); ZipOutputStream zout = null; if (m_format.equals(ATOM_ZIP1_1)) { zout = new ZipOutputStream(out); } addObjectProperties(obj, feed); feed.setIcon("http://www.fedora-commons.org/images/logo_vertical_transparent_200_251.png"); addDatastreams(feed, obj, zout, encoding, transContext); if (m_format.equals(ATOM_ZIP1_1)) { try { zout.putNextEntry(new ZipEntry("atommanifest.xml")); feed.writeTo("prettyxml", zout); zout.closeEntry(); zout.close(); } catch (IOException e) { throw new StreamIOException(e.getMessage(), e); } } else { try { feed.writeTo("prettyxml", out); } catch (IOException e) { throw new StreamIOException(e.getMessage(), e); } } } private void addObjectProperties(DigitalObject obj, Feed feed) throws ObjectIntegrityException { String state = DOTranslationUtility.getStateAttribute(obj); String ownerId = obj.getOwnerId(); String label = obj.getLabel(); Date cdate = obj.getCreateDate(); Date mdate = obj.getLastModDate(); feed.setId(PID.toURI(obj.getPid())); feed.setTitle(label == null ? "" : label); feed.setUpdated(mdate); feed.addAuthor(ownerId == null ? "" : StreamUtility.enc(ownerId)); feed.addCategory(MODEL.STATE.uri, state, null); if (cdate != null) { feed.addCategory(MODEL.CREATED_DATE.uri, DateUtility .convertDateToString(cdate), null); } // TODO not sure I'm satisfied with this representation of extProperties for (String extProp : obj.getExtProperties().keySet()) { feed.addCategory(MODEL.EXT_PROPERTY.uri, extProp, obj .getExtProperty(extProp)); } } private void addDatastreams(Feed feed, DigitalObject obj, ZipOutputStream zout, String encoding, int transContext) throws ObjectIntegrityException, UnsupportedEncodingException, StreamIOException { Iterator<String> iter = obj.datastreamIdIterator(); String dsid; while (iter.hasNext()) { dsid = iter.next(); // AUDIT datastream is rebuilt from the latest in-memory audit trail // which is a separate array list in the DigitalObject class. // So, ignore it here. if (dsid.equals("AUDIT") || dsid.equals("FEDORA-AUDITTRAIL")) { continue; } Entry dsEntry = feed.addEntry(); Datastream latestCreated = null; long latestCreateTime = -1; for (Datastream v : obj.datastreams(dsid)) { Datastream dsv = DOTranslationUtility.setDatastreamDefaults(v); // Keep track of the most recent datastream version if (dsv.DSCreateDT.getTime() > latestCreateTime) { latestCreateTime = dsv.DSCreateDT.getTime(); latestCreated = dsv; } Entry dsvEntry = feed.addEntry(); dsvEntry.setId(PID.toURI(obj.getPid()) + "/" + dsv.DatastreamID + "/" + DateUtility.convertDateToString(dsv.DSCreateDT)); dsvEntry.setTitle(dsv.DSVersionID); dsvEntry.setUpdated(dsv.DSCreateDT); ThreadHelper.addInReplyTo(dsvEntry, PID.toURI(obj.getPid()) + "/" + dsv.DatastreamID); String altIds = DOTranslationUtility.oneString(dsv.DatastreamAltIDs); if (altIds != null && !altIds.isEmpty()) { dsvEntry.addCategory(MODEL.ALT_IDS.uri, altIds, null); } if (dsv.DSFormatURI != null && !dsv.DSFormatURI.isEmpty()) { dsvEntry.addCategory(MODEL.FORMAT_URI.uri, dsv.DSFormatURI, null); } dsvEntry.addCategory(MODEL.LABEL.uri, dsv.DSLabel == null ? "" : dsv.DSLabel, null); // include checksum if it has a value String csType = dsv.getChecksumType(); if (csType != null && csType.length() > 0 && !csType.equals(Datastream.CHECKSUMTYPE_DISABLED)) { dsvEntry.addCategory(MODEL.DIGEST_TYPE.uri, csType, null); dsvEntry.addCategory(MODEL.DIGEST.uri, dsv.getChecksum(), null); } // include size if it's non-zero if (dsv.DSSize != 0) { dsvEntry.addCategory(MODEL.LENGTH.uri, Long .toString(dsv.DSSize), null); } setContent(dsvEntry, obj, dsv, zout, encoding, transContext); } // The "main" entry for the Datastream with a link to the atom:id // of the most recent datastream version dsEntry.setId(PID.toURI(obj.getPid()) + "/" + latestCreated.DatastreamID); dsEntry.setTitle(latestCreated.DatastreamID); dsEntry.setUpdated(latestCreated.DSCreateDT); dsEntry .addLink(PID.toURI(obj.getPid()) + "/" + latestCreated.DatastreamID + "/" + DateUtility .convertDateToString(latestCreated.DSCreateDT), Link.REL_ALTERNATE); dsEntry.addCategory(MODEL.STATE.uri, latestCreated.DSState, null); dsEntry.addCategory(MODEL.CONTROL_GROUP.uri, latestCreated.DSControlGrp, null); dsEntry.addCategory(MODEL.VERSIONABLE.uri, Boolean .toString(latestCreated.DSVersionable), null); } addAuditDatastream(feed, obj, zout, encoding); } /** * AUDIT datastream is rebuilt from the latest in-memory audit trail which * is a separate array list in the DigitalObject class. Audit trail * datastream re-created from audit records. There is only ONE version of * the audit trail datastream * * @throws ObjectIntegrityException * @throws StreamIOException */ private void addAuditDatastream(Feed feed, DigitalObject obj, ZipOutputStream zout, String encoding) throws ObjectIntegrityException, StreamIOException { if (obj.getAuditRecords().size() == 0) { return; } String dsId = PID.toURI(obj.getPid()) + "/AUDIT"; String dsvId = dsId + "/" + DateUtility .convertDateToString(obj.getCreateDate()); Entry dsEntry = feed.addEntry(); dsEntry.setId(dsId); dsEntry.setTitle("AUDIT"); dsEntry.setUpdated(obj.getCreateDate()); // create date? dsEntry.addCategory(MODEL.STATE.uri, "A", null); dsEntry.addCategory(MODEL.CONTROL_GROUP.uri, "X", null); dsEntry.addCategory(MODEL.VERSIONABLE.uri, "false", null); dsEntry.addLink(dsvId, Link.REL_ALTERNATE); Entry dsvEntry = feed.addEntry(); dsvEntry.setId(dsvId); dsvEntry.setTitle("AUDIT.0"); dsvEntry.setUpdated(obj.getCreateDate()); ThreadHelper.addInReplyTo(dsvEntry, PID.toURI(obj.getPid()) + "/AUDIT"); dsvEntry.addCategory(MODEL.FORMAT_URI.uri, AUDIT1_0.uri, null); dsvEntry .addCategory(MODEL.LABEL.uri, "Audit Trail for this object", null); if (m_format.equals(ATOM_ZIP1_1)) { String name = "AUDIT.0.xml"; try { zout.putNextEntry(new ZipEntry(name)); ReadableCharArrayWriter buf = new ReadableCharArrayWriter(512); PrintWriter pw = new PrintWriter(buf); DOTranslationUtility.appendAuditTrail(obj, pw); pw.close(); IOUtils.copy(buf.toReader(), zout, encoding); zout.closeEntry(); } catch(IOException e) { throw new StreamIOException(e.getMessage(), e); } IRI iri = new IRI(name); dsvEntry.setSummary("AUDIT.0"); dsvEntry.setContent(iri, "text/xml"); } else { dsvEntry.setContent(DOTranslationUtility.getAuditTrail(obj), "text/xml"); } } private void setContent(Entry entry, DigitalObject obj, Datastream vds, ZipOutputStream zout, String encoding, int transContext) throws UnsupportedEncodingException, StreamIOException { if (vds.DSControlGrp.equalsIgnoreCase("X")) { setInlineXML(entry, obj,(DatastreamXMLMetadata) vds, zout, encoding, transContext); } else if (vds.DSControlGrp.equalsIgnoreCase("E") || vds.DSControlGrp.equalsIgnoreCase("R")) { setReferencedContent(entry, obj, vds, transContext); } else if (vds.DSControlGrp.equalsIgnoreCase("M")) { setManagedContent(entry, obj, vds, zout, encoding, transContext); } } private void setInlineXML(Entry entry, DigitalObject obj, DatastreamXMLMetadata ds, ZipOutputStream zout, String encoding, int transContext) throws UnsupportedEncodingException, StreamIOException { byte[] content; if (obj.hasContentModel( Models.SERVICE_DEPLOYMENT_3_0) && (ds.DatastreamID.equals("SERVICE-PROFILE") || ds.DatastreamID .equals("WSDL"))) { content = m_translator .normalizeInlineXML(new String(ds.xmlContent, encoding), transContext).getBytes(encoding); } else { content = ds.xmlContent; } if (m_format.equals(ATOM_ZIP1_1)) { String name = ds.DSVersionID + ".xml"; try { zout.putNextEntry(new ZipEntry(name)); InputStream is = new ByteArrayInputStream(content); IOUtils.copy(is, zout); zout.closeEntry(); is.close(); } catch(IOException e) { throw new StreamIOException(e.getMessage(), e); } IRI iri = new IRI(name); entry.setSummary(ds.DSVersionID); entry.setContent(iri, ds.DSMIME); } else { entry.setContent(new String(content, encoding), ds.DSMIME); } } private void setReferencedContent(Entry entry, DigitalObject obj, Datastream vds, int transContext) throws StreamIOException { entry.setSummary(vds.DSVersionID); String dsLocation = StreamUtility.enc(m_translator .normalizeDSLocationURLs(obj.getPid(), vds, transContext).DSLocation); IRI iri = new IRI(dsLocation); entry.setContent(iri, vds.DSMIME); } private void setManagedContent(Entry entry, DigitalObject obj, Datastream vds, ZipOutputStream zout, String encoding, int transContext) throws StreamIOException { // If the ARCHIVE context is selected, inline & base64 encode the content, // unless the format is ZIP. if (transContext == DOTranslationUtility.SERIALIZE_EXPORT_ARCHIVE && !m_format.equals(ATOM_ZIP1_1)) { String mimeType = vds.DSMIME; if (MimeTypeHelper.isText(mimeType) || MimeTypeHelper.isXml(mimeType)) { try { entry.setContent(IOUtils.toString(vds.getContentStream(), encoding), mimeType); } catch (IOException e) { throw new StreamIOException(e.getMessage(), e); } } else { entry.setContent(vds.getContentStream(), mimeType); } } else { String dsLocation; IRI iri; if (m_format.equals(ATOM_ZIP1_1) && transContext != DOTranslationUtility.AS_IS) { dsLocation = vds.DSVersionID + "." + MimeTypeUtils.fileExtensionForMIMEType(vds.DSMIME); try { zout.putNextEntry(new ZipEntry(dsLocation)); InputStream is = vds.getContentStream(); IOUtils.copy(is, zout); is.close(); zout.closeEntry(); } catch(IOException e) { throw new StreamIOException(e.getMessage(), e); } } else { dsLocation = StreamUtility.enc(m_translator .normalizeDSLocationURLs(obj.getPid(), vds, transContext).DSLocation); } iri = new IRI(dsLocation); entry.setSummary(vds.DSVersionID); entry.setContent(iri, vds.DSMIME); } } }