/*
* 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.generated.foxml.ContentLocationType;
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.PropertyType;
import com.yourmediashelf.fedora.generated.foxml.StateType;
import com.yourmediashelf.fedora.generated.foxml.XmlContentType;
import com.yourmediashelf.fedora.generated.management.DatastreamProfile;
import cz.cas.lib.proarc.common.fedora.FoxmlUtils.ControlGroup;
import cz.cas.lib.proarc.common.fedora.XmlStreamEditor.EditorResult;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import javax.ws.rs.core.MediaType;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* Local storage supporting FOXML 1.1.
* It contains newly imported but not yet ingested files.
*
* @author Jan Pokorsky
*/
public final class LocalStorage {
private static final Logger LOG = Logger.getLogger(LocalStorage.class.getName());
public LocalObject load(String pid, File foxml) {
DigitalObject dobj = FoxmlUtils.unmarshal(new StreamSource(foxml), DigitalObject.class);
LocalObject result = new LocalObject(pid, foxml, dobj);
return result;
}
public LocalObject create() {
return create((String) null, null);
}
public LocalObject create(String pid) {
return create(pid, null);
}
public LocalObject create(DigitalObject dobj) {
return create(null, dobj);
}
public LocalObject create(File foxml, DigitalObject dobj) {
String pid = dobj.getPID();
pid = pid != null ? pid : FoxmlUtils.createPid();
return new LocalObject(pid, foxml, dobj);
}
public LocalObject create(File foxml) {
return create(null, foxml);
}
public LocalObject create(String pid, File foxml) {
pid = pid != null ? pid : FoxmlUtils.createPid();
DigitalObject dobj = FoxmlUtils.createFoxml(pid);
return new LocalObject(pid, foxml, dobj);
}
public static final class LocalObject extends AbstractFedoraObject {
/** The helper property to mark a local FOXML as a copy of the remote object. */
private static final String PROPERTY_REMOTE_COPY = "proarc:model#remoteCopy";
private DigitalObject dobj;
/** {@code null} for in memory object. */
private File foxml;
LocalObject(String pid, File foxml, DigitalObject dobj) {
super(pid);
this.foxml = foxml;
if (dobj == null) {
throw new NullPointerException("dobj");
}
this.dobj = dobj;
}
public String getOwner() {
PropertyType p = FoxmlUtils.findProperty(dobj, FoxmlUtils.PROPERTY_OWNER);
return p == null ? null : p.getVALUE();
}
public String getLabel() {
PropertyType p = FoxmlUtils.findProperty(dobj, FoxmlUtils.PROPERTY_LABEL);
return p == null ? null : p.getVALUE();
}
@Override
public void setLabel(String label) {
if (label == null) {
throw new NullPointerException();
} else if (label.length() > 255) {
// length 255 is Fedora limit
label = label.substring(0, 255);
}
FoxmlUtils.setProperty(dobj, FoxmlUtils.PROPERTY_LABEL, label);
}
public void setOwner(String owner) {
FoxmlUtils.setProperty(dobj, FoxmlUtils.PROPERTY_OWNER, owner);
}
/** The helper property to mark a local FOXML as a copy of the remote object. */
public void setRemoteCopy(boolean remote) {
String val = remote ? Boolean.TRUE.toString() : null;
FoxmlUtils.setProperty(dobj, PROPERTY_REMOTE_COPY, val);
}
/** The helper property to mark a local FOXML as a copy of the remote object. */
public boolean isRemoteCopy() {
return FoxmlUtils.findProperty(dobj, PROPERTY_REMOTE_COPY) != null;
}
@Override
public void flush() throws DigitalObjectException {
super.flush();
if (foxml != null) {
FoxmlUtils.marshal(new StreamResult(foxml), dobj, true);
}
}
public File getFoxml() {
return foxml;
}
@Override
public XmlStreamEditor getEditor(DatastreamProfile datastream) {
return new LocalXmlStreamEditor(this, datastream);
}
public DigitalObject getDigitalObject() {
return dobj;
}
@Override
public String asText() throws DigitalObjectException {
try {
return FoxmlUtils.toXml(dobj, true);
} catch (Exception ex) {
throw new DigitalObjectException(getPid(), ex);
}
}
@Override
public List<DatastreamProfile> getStreamProfile(String dsId) throws DigitalObjectException {
List<DatastreamType> datastreams;
if (dsId == null) {
datastreams = dobj.getDatastream();
} else {
DatastreamType datastream = FoxmlUtils.findDatastream(dobj, dsId);
datastreams = (datastream != null)
? Arrays.asList(datastream)
: Collections.<DatastreamType>emptyList();
}
List<DatastreamProfile> profiles = new ArrayList<DatastreamProfile>(datastreams.size());
for (DatastreamType datastream : datastreams) {
profiles.add(FoxmlUtils.toDatastreamProfile(getPid(), datastream));
}
return profiles;
}
}
public static final class LocalXmlStreamEditor implements XmlStreamEditor {
private final LocalObject object;
private DatastreamProfile defaultProfile;
private final boolean isXml;
private LocalXmlStreamEditor(LocalObject object, DatastreamProfile defaultProfile) {
this.object = object;
this.defaultProfile = defaultProfile;
String mime = defaultProfile.getDsMIME();
this.isXml = MediaType.TEXT_XML.equals(mime) || MediaType.APPLICATION_XML.equals(mime);
}
@Override
public Source read() {
// find version
DatastreamVersionType version = FoxmlUtils.findDataStreamVersion(object.getDigitalObject(), defaultProfile.getDsID());
return createSource(version);
}
private Source createSource(DatastreamVersionType version) {
if (version == null) {
return null;
}
XmlContentType xmlContent = version.getXmlContent();
if (xmlContent != null) {
Element elm = xmlContent.getAny().get(0);
return new DOMSource(elm);
} else {
byte[] binaryContent = version.getBinaryContent();
ContentLocationType contentLocation = version.getContentLocation();
if (binaryContent != null) {
return new StreamSource(new ByteArrayInputStream(binaryContent));
} else if (contentLocation != null) {
String ref = contentLocation.getREF();
if (ref != null) {
URI refUri = URI.create(ref);
return new StreamSource(new File(refUri));
}
}
}
return null;
}
@Override
public InputStream readStream() throws DigitalObjectException {
DatastreamVersionType version = FoxmlUtils.findDataStreamVersion(object.getDigitalObject(), defaultProfile.getDsID());
if (version != null) {
byte[] binaryContent = version.getBinaryContent();
ContentLocationType contentLocation = version.getContentLocation();
if (binaryContent != null) {
return new ByteArrayInputStream(binaryContent);
} else if (contentLocation != null) {
String ref = contentLocation.getREF();
if (ref != null) {
try {
URI refUri = new URI(ref);
return new FileInputStream(new File(refUri));
} catch (URISyntaxException ex) {
throw new DigitalObjectException(object.getPid(), ex);
} catch (FileNotFoundException ex) {
throw new DigitalObjectException(object.getPid(), ex);
}
}
} else if (version.getXmlContent() != null) {
throw new DigitalObjectException(object.getPid(),
"XML inlined! Use read() method.");
}
}
return null;
}
@Override
public long getLastModified() {
DatastreamVersionType version = FoxmlUtils.findDataStreamVersion(object.getDigitalObject(), defaultProfile.getDsID());
long lastModified = getLastModified(version);
return lastModified;
}
private long getLastModified(DatastreamVersionType version) {
long last = Long.MIN_VALUE;
if (version != null) {
last = version.getCREATED().toGregorianCalendar().getTimeInMillis();
}
return last;
}
/**
* Gets {@link DatastreamVersionType} as {@link DatastreamProfile} for
* {@link #setProfile setProfile}. Only dsId, dsLabel, dsCreateDate,
* dsFormatURI and dsMIME are translated.
* @return the stream profile
* @throws DigitalObjectException
*/
@Override
public DatastreamProfile getProfile() throws DigitalObjectException {
return getProfileImp();
}
public DatastreamProfile getProfileImp() {
String dsId = defaultProfile.getDsID();
DatastreamType datastream = FoxmlUtils.findDatastream(object.getDigitalObject(), dsId);
if (datastream == null) {
return defaultProfile;
} else {
return FoxmlUtils.toDatastreamProfile(object.getPid(), datastream);
}
}
/**
* Updates {@link DatastreamVersionType} properties with {@link DatastreamProfile}
* where it makes sense.
* @param profile profile
* @throws DigitalObjectException failure
*/
@Override
public void setProfile(DatastreamProfile profile) throws DigitalObjectException {
if (profile == null) {
throw new NullPointerException();
}
String dsId = defaultProfile.getDsID();
DatastreamVersionType version = FoxmlUtils.findDataStreamVersion(object.getDigitalObject(), dsId);
if (version == null) {
defaultProfile = profile;
return ;
}
version.setCREATED(profile.getDsCreateDate());
version.setFORMATURI(profile.getDsFormatURI());
version.setLABEL(profile.getDsLabel());
version.setMIMETYPE(profile.getDsMIME());
}
@Override
public void write(EditorResult data, long timestamp, String message) throws DigitalObjectException {
DatastreamVersionType version = getDatastreamVersionType(timestamp);
if (data instanceof EditorBinaryResult) {
version.setBinaryContent(null);
version.setXmlContent(null);
} else if (data instanceof EditorDomResult) {
writeXmlContent(version, (EditorDomResult) data);
} else {
throw new DigitalObjectException(object.getPid(), "Unsupported data: " + data);
}
version.setCREATED(FoxmlUtils.createXmlDate());
object.register(this);
}
@Override
public void write(byte[] data, long timestamp, String message) throws DigitalObjectException {
writeBytesOrStream(data, null, timestamp);
}
@Override
public void write(InputStream data, long timestamp, String message) throws DigitalObjectException {
try {
writeBytesOrStream(null, data, timestamp);
} finally {
FoxmlUtils.closeQuietly(data, object.getPid());
}
}
@Override
public void write(URI data, long timestamp, String message) throws DigitalObjectException {
ControlGroup control = ControlGroup.fromExternal(defaultProfile.getDsControlGroup());
if (control != ControlGroup.MANAGED) {
throw new UnsupportedOperationException("Not supported yet: " + control);
}
DatastreamVersionType version = getDatastreamVersionType(timestamp);
ContentLocationType contentLocation = new ContentLocationType();
contentLocation.setTYPE("URL");
contentLocation.setREF(data.toASCIIString());
version.setContentLocation(contentLocation);
version.setBinaryContent(null);
version.setCREATED(FoxmlUtils.createXmlDate());
object.register(this);
}
@Override
public EditorResult createResult() {
if (!isXml) {
throw new UnsupportedOperationException("requires */xml mime type");
}
String reference = getReference();
return reference == null ? new EditorDomResult() : new EditorBinaryResult(reference);
}
@Override
public void flush() {
// no op
}
private String getReference() {
String dsId = defaultProfile.getDsID();
DatastreamVersionType version = FoxmlUtils.findDataStreamVersion(object.getDigitalObject(), dsId);
if (version != null && version.getContentLocation() != null) {
return version.getContentLocation().getREF();
}
return null;
}
private DatastreamVersionType getDatastreamVersionType(long timestamp) throws DigitalObjectConcurrentModificationException {
String dsId = defaultProfile.getDsID();
DatastreamVersionType version = FoxmlUtils.findDataStreamVersion(object.getDigitalObject(), dsId);
if (version == null) {
ControlGroup cgroup = ControlGroup.fromExternal(defaultProfile.getDsControlGroup());
version = FoxmlUtils.createDataStreamVersion(
object.getDigitalObject(), dsId, cgroup, false, StateType.A);
version.setMIMETYPE(defaultProfile.getDsMIME());
version.setLABEL(defaultProfile.getDsLabel());
version.setFORMATURI(defaultProfile.getDsFormatURI());
} else if (timestamp != getLastModified(version)) {
throw new DigitalObjectConcurrentModificationException(object.getPid(), dsId);
}
return version;
}
private void writeXmlContent(DatastreamVersionType version, EditorDomResult data) {
XmlContentType xmlContent = new XmlContentType();
Node root = data.getNode();
Document doc = root.getOwnerDocument() == null ? (Document) root : root.getOwnerDocument();
xmlContent.getAny().add(doc.getDocumentElement());
version.setXmlContent(xmlContent);
}
private void writeBytesOrStream(byte[] bytes, InputStream stream, long timestamp) throws DigitalObjectException {
ControlGroup control = ControlGroup.fromExternal(defaultProfile.getDsControlGroup());
if (control != ControlGroup.MANAGED) {
throw new UnsupportedOperationException("Not supported yet: " + control);
}
DatastreamVersionType version = getDatastreamVersionType(timestamp);
String reference = getReference();
boolean storeExternally = reference != null;
if (storeExternally) {
writeBytesOrStreamExternally(version, bytes, stream, reference);
} else {
if (bytes != null) {
version.setBinaryContent(bytes);
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
try {
FoxmlUtils.copy(stream, baos);
version.setBinaryContent(baos.toByteArray());
} catch (IOException ex) {
throw new DigitalObjectException(object.getPid(), ex);
}
}
version.setContentLocation(null);
}
version.setCREATED(FoxmlUtils.createXmlDate());
object.register(this);
}
private void writeBytesOrStreamExternally(DatastreamVersionType version,
byte[] bytes, InputStream stream, String reference
) throws DigitalObjectException {
FileOutputStream fos = null;
try {
URI ref = new URI(reference);
File file = new File(ref);
fos = new FileOutputStream(file);
if (bytes != null) {
fos.write(bytes);
} else {
FoxmlUtils.copy(stream, fos);
}
version.setBinaryContent(null);
} catch (URISyntaxException ex) {
throw new DigitalObjectException(object.getPid(), ex);
} catch (IOException ex) {
throw new DigitalObjectException(object.getPid(), ex);
} finally {
FoxmlUtils.closeQuietly(fos, object.getPid());
}
}
@Override
public String toString() {
return String.format("%s{pid=%s, dsId=%s, mimetype=%s, controlGroup=%s,\nfoxml=%s}",
getClass().getSimpleName(),
object.getPid(),
defaultProfile.getDsID(),
getProfileImp().getDsMIME(),
defaultProfile.getDsControlGroup(),
object.getFoxml());
}
}
private static final class EditorDomResult extends DOMResult implements EditorResult {
}
private static final class EditorBinaryResult extends StreamResult implements EditorResult {
public EditorBinaryResult(String reference) {
super(reference);
}
}
}