/* 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 fedora.server.storage.types;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jrdf.graph.ObjectNode;
import org.jrdf.graph.PredicateNode;
import org.jrdf.graph.SubjectNode;
import fedora.common.Constants;
import fedora.common.Models;
import fedora.common.PID;
import fedora.common.rdf.JRDF;
import fedora.common.rdf.SimpleURIReference;
import fedora.server.errors.ServerException;
import fedora.server.storage.RDFRelationshipReader;
/**
* A basic implementation of DigitalObject that stores things in memory.
*
* @author Chris Wilper
* @author Stephen Bayliss
*/
@SuppressWarnings("deprecation")
public class BasicDigitalObject
implements DigitalObject {
private boolean m_isNew;
private String m_pid;
private String m_state;
private String m_ownerId;
private String m_label;
private Set<RelationshipTuple> m_rels;
private Date m_createDate;
private Date m_lastModDate;
private final DatastreamProcessor m_datastreamProcessor;
private final ArrayList<AuditRecord> m_auditRecords;
/*
* Although not required, this will assure that datastreamsIDs may be
* iterated in insertion order.
*/
private final LinkedHashMap<String, List<Datastream>> m_datastreams;
private final HashMap<String, List<Disseminator>> m_disseminators;
private final Map<String, String> m_extProperties;
public BasicDigitalObject() {
m_auditRecords = new ArrayList<AuditRecord>();
m_datastreams = new LinkedHashMap<String, List<Datastream>>();
m_disseminators = new HashMap<String, List<Disseminator>>();
m_extProperties = new HashMap<String, String>();
m_datastreamProcessor = new RelationshipProcessor();
setNew(false);
}
public boolean isNew() {
return m_isNew;
}
public void setNew(boolean isNew) {
m_isNew = isNew;
}
public String getPid() {
return m_pid;
}
public void setPid(String pid) {
m_pid = pid;
}
public String getState() {
return m_state;
}
public void setState(String state) {
m_state = state;
}
public String getOwnerId() {
return m_ownerId;
}
public void setOwnerId(String owner) {
m_ownerId = owner;
}
public String getLabel() {
return m_label;
}
public void setLabel(String label) {
m_label = label;
}
public Date getCreateDate() {
return m_createDate;
}
public void setCreateDate(Date createDate) {
m_createDate = createDate;
}
public Date getLastModDate() {
return m_lastModDate;
}
public void setLastModDate(Date lastModDate) {
m_lastModDate = lastModDate;
}
public List<AuditRecord> getAuditRecords() {
return m_auditRecords;
}
public Iterator<String> datastreamIdIterator() {
return copyOfKeysForNonEmptyLists(m_datastreams).iterator();
}
private static <T> Set<String> copyOfKeysForNonEmptyLists(Map<String, List<T>> map) {
Set<String> set = new LinkedHashSet<String>();
for (Map.Entry<String, List<T>> e : map.entrySet()) {
if (!e.getValue().isEmpty()) {
set.add(e.getKey());
}
}
return set;
}
public Iterable<Datastream> datastreams(String id) {
if (!m_datastreams.containsKey(id)) {
return new ArrayList<Datastream>();
}
return Collections
.unmodifiableList(new ArrayList<Datastream>(m_datastreams
.get(id)));
}
public void removeDatastreamVersion(Datastream ds) {
remove(ds);
}
public void addDatastreamVersion(Datastream ds, boolean addNewVersion) {
if (!addNewVersion) {
Datastream latestCreated = null;
long latestCreateTime = -1;
for (Datastream d : datastreams(ds.DatastreamID)) {
if (d.DSCreateDT.getTime() > latestCreateTime) {
latestCreateTime = d.DSCreateDT.getTime();
latestCreated = d;
}
}
remove(latestCreated);
}
add(ds);
}
private void add(Datastream d) {
/*
* We determine the most recent datastream version by its created date.
* If a created date has not been supplied, give it one.
*/
if (d.DSCreateDT == null) {
d.DSCreateDT = new Date();
}
String id = d.DatastreamID;
if (!m_datastreams.containsKey(id)) {
m_datastreams.put(id, new ArrayList<Datastream>());
}
m_datastreams.get(id).add(d);
m_datastreamProcessor.processAdd(d);
}
private void remove(Datastream d) {
if (d == null) return;
List<Datastream> datastreams = m_datastreams.get(d.DatastreamID);
if (datastreams == null) {
return;
}
int size = datastreams.size();
for (int i = 0; i < size; i++) {
Datastream v = datastreams.get(i);
if (d.DSVersionID.equals(v.DSVersionID)) {
datastreams.remove(i);
m_datastreamProcessor.processRemove(v);
break;
}
}
/* If we've removed the last version, remove the ID from the map */
if (datastreams.size() == 0) {
m_datastreams.remove(d.DatastreamID);
}
}
@Deprecated
public Iterator<String> disseminatorIdIterator() {
return copyOfKeysForNonEmptyLists(m_disseminators).iterator();
}
@Deprecated
public List<Disseminator> disseminators(String id) {
ArrayList<Disseminator> ret =
(ArrayList<Disseminator>) m_disseminators.get(id);
if (ret == null) {
ret = new ArrayList<Disseminator>();
m_disseminators.put(id, ret);
}
return ret;
}
public String newDatastreamID() {
return newID(datastreamIdIterator(), "DS");
}
public String newDatastreamID(String id) {
List<String> versionIDs = new ArrayList<String>();
Iterator<Datastream> iter = (m_datastreams.get(id)).iterator();
while (iter.hasNext()) {
Datastream ds = iter.next();
versionIDs.add(ds.DSVersionID);
}
return newID(versionIDs.iterator(), id + ".");
}
public String newAuditRecordID() {
ArrayList<String> auditIDs = new ArrayList<String>();
Iterator<AuditRecord> iter = m_auditRecords.iterator();
while (iter.hasNext()) {
AuditRecord record = iter.next();
auditIDs.add(record.id);
}
return newID(auditIDs.iterator(), "AUDREC");
}
/**
* Sets an extended property on the object.
*
* @param propName
* The extende property name, either a string, or URI as string.
*/
public void setExtProperty(String propName, String propValue) {
m_extProperties.put(propName, propValue);
}
/**
* Gets an extended property value, given the property name.
*
* @return The property value.
*/
public String getExtProperty(String propName) {
return m_extProperties.get(propName);
}
/**
* Gets a Map containing all of the extended properties on the object. Map
* key is property name.
*
* @return The property Map.
*/
public Map<String, String> getExtProperties() {
return m_extProperties;
}
// assumes m_pid as subject; ie RELS-EXT only
public boolean hasRelationship(PredicateNode predicate, ObjectNode object) {
return hasRelationship(PID.toURIReference(m_pid), predicate, object);
}
public boolean hasRelationship(SubjectNode subject, PredicateNode predicate, ObjectNode object) {
/* Brute force */
return getRelationships(subject, predicate, object).size() > 0;
}
// assume m_pid as subject; ie RELS-EXT only
public Set<RelationshipTuple> getRelationships(PredicateNode predicate,
ObjectNode object) {
return getRelationships(PID.toURIReference(m_pid), predicate, object);
}
public Set<RelationshipTuple> getRelationships() {
return getRelationships(null, null, null);
}
public Set<RelationshipTuple> getRelationships(SubjectNode subject,
PredicateNode predicate,
ObjectNode object) {
Set<RelationshipTuple> foundRels = new HashSet<RelationshipTuple>();
if (m_rels == null) {
readRels();
}
boolean basicExplicit = false;
// Iterate explicit relationships, finding matches and
// determining whether the object has an explicit basic cmodel.
for (RelationshipTuple t : m_rels) {
// Do any hasModel rels point to a basic cmodel?
if (Constants.MODEL.HAS_MODEL.uri.equals(t.predicate)
&& Models.isBasicModel(t.object)) {
basicExplicit = true;
}
// Find matching relationships from those that are explicit
if (subject != null) {
if (!JRDF.sameSubject(subject, t.subject)) {
continue;
}
}
if (predicate != null) {
if (!JRDF.samePredicate(predicate, t.predicate)) {
continue;
}
}
if (object != null) {
if (!JRDF.sameObject(object,
t.object,
t.isLiteral,
t.datatype,
null)) {
continue;
}
}
foundRels.add(t);
}
// If necessary, add the current basic cmodel to the set of matches
try {
if (!basicExplicit
&& (subject == null ||
JRDF.sameSubject(subject, new SimpleURIReference(new URI(PID.toURI(m_pid)))))
&& (predicate == null ||
JRDF.samePredicate(predicate, Constants.MODEL.HAS_MODEL))
&& (object == null ||
JRDF.sameObject(object, Models.FEDORA_OBJECT_CURRENT))) {
foundRels.add(
new RelationshipTuple(Constants.FEDORA.uri + m_pid,
Constants.MODEL.HAS_MODEL.uri,
Models.FEDORA_OBJECT_CURRENT.uri,
false,
null));
}
} catch (URISyntaxException e) {
// assume that m_pid is a valid pid
}
return foundRels;
}
public List<String> getContentModels() {
Set<RelationshipTuple> cmTubles = getRelationships(Constants.MODEL.HAS_MODEL,
null);
List<String> cms = new ArrayList<String>();
for (RelationshipTuple cmTuble:cmTubles){
cms.add(cmTuble.object);
}
return cms;
}
public boolean hasContentModel(ObjectNode contentModel) {
return hasRelationship(Constants.MODEL.HAS_MODEL,contentModel);
}
/**
* Given an iterator of existing ids, return a new id that starts with
* <code>start</code> and is guaranteed to be unique. This algorithm adds
* one to the highest existing id that starts with <code>start</code>. If
* no such existing id exists, it will return <i>start</i> + "1".
*/
private String newID(Iterator<String> iter, String start) {
int highest = 0;
while (iter.hasNext()) {
String id = iter.next();
if (id.startsWith(start) && id.length() > start.length()) {
try {
int num = Integer.parseInt(id.substring(start.length()));
if (num > highest) {
highest = num;
}
} catch (NumberFormatException ignored) {
}
}
}
int newNum = highest + 1;
return start + newNum;
}
/**
* read relationships from RELS-EXT and RELS-INT datastreams
*/
private void readRels() {
m_rels = getRels("RELS-EXT");
m_rels.addAll(getRels("RELS-INT"));
}
/**
* Given a relationships datastream name, return the relationships contained
* in that datastream
*/
private Set<RelationshipTuple> getRels(String relsDatastreamName) {
List<Datastream> relsDatastreamVersions = m_datastreams.get(relsDatastreamName);
if (relsDatastreamVersions == null || relsDatastreamVersions.size() == 0) {
return new HashSet<RelationshipTuple>();
}
Datastream latestRels = relsDatastreamVersions.get(0);
for (Datastream v : relsDatastreamVersions) {
if (v.DSCreateDT.getTime() > latestRels.DSCreateDT.getTime()) {
latestRels = v;
}
}
try {
return RDFRelationshipReader.readRelationships(latestRels);
} catch (ServerException e) {
throw new RuntimeException("Error reading object relationships in " + relsDatastreamName, e);
}
}
private abstract class DatastreamProcessor {
abstract void processAdd(Datastream d);
abstract void processRemove(Datastream d);
boolean isLatestVersion(Datastream d) {
List<Datastream> versions = m_datastreams.get(d.DatastreamID);
if (versions == null || versions.size() == 0) return true;
long created = d.DSCreateDT.getTime();
for (Datastream v : versions) {
if (v.DSCreateDT.getTime() > created) {
return false;
}
}
return true;
}
}
/**
* If the latest RELS-EXT or RELS-INT is added or removed, invalidate the relationship
* cache so that it has to be re-read next time it is requested.
*/
private class RelationshipProcessor
extends DatastreamProcessor {
private static final String RELS_EXT = "RELS-EXT";
private static final String RELS_INT = "RELS-INT";
@Override
void processRemove(Datastream d) {
invalidateIfLatestRels(d);
}
@Override
void processAdd(Datastream d) {
invalidateIfLatestRels(d);
}
private void invalidateIfLatestRels(Datastream d) {
if ((d.DatastreamID.equals(RELS_EXT) || d.DatastreamID.equals(RELS_INT))&& isLatestVersion(d)) {
m_rels = null;
}
}
}
}