/* 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.types; 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 org.fcrepo.common.Constants; import org.fcrepo.common.Models; import org.fcrepo.common.PID; import org.fcrepo.common.rdf.JRDF; import org.fcrepo.server.errors.ServerException; import org.fcrepo.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 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>>(); // since extProperties are rare, initialize lazily m_extProperties = null; 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) { if (map == null || map.size() == 0) { return Collections.emptySet(); } Set<String> set = new LinkedHashSet<String>(map.size()); 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 Collections.emptyList(); } 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) { 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) { Iterator<String> iter = null; if (m_datastreams.containsKey(id)) { List<String> versionIDs = new ArrayList<String>(m_datastreams.get(id).size()); for (Datastream ds: m_datastreams.get(id)) { versionIDs.add(ds.DSVersionID); } iter = versionIDs.iterator(); } return newID(iter, id.concat(".")); } public String newAuditRecordID() { ArrayList<String> auditIDs = new ArrayList<String>(m_auditRecords.size()); for (AuditRecord record: m_auditRecords) { auditIDs.add(record.id); } return newID(auditIDs.iterator(), "AUDREC"); } /** * Sets an extended property on the object. * * @param propName * The extended property name, either a string, or URI as string. */ public void setExtProperty(String propName, String propValue) { if (m_extProperties == null) { m_extProperties = new HashMap<String, String>(); } m_extProperties.put(propName, propValue); } /** * Gets an extended property value, given the property name. * * @return The property value. */ public String getExtProperty(String propName) { if (m_extProperties == null) { return null; } 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() { if (m_extProperties == null) { return Collections.emptyMap(); } 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 */ if (m_rels == null) { readRels(); } boolean hasRel = false; 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, t.language)) { continue; } } hasRel = true; } // If necessary, add the current basic cmodel to the set of matches if (!hasRel && !basicExplicit && (subject == null || JRDF.sameSubject(subject, PID.toURI(m_pid))) && (predicate == null || JRDF.samePredicate(predicate, Constants.MODEL.HAS_MODEL)) && (object == null || JRDF.sameObject(object, Models.FEDORA_OBJECT_CURRENT))) { return true; } return hasRel; } // 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) { if (m_rels == null) { readRels(); } boolean basicExplicit = false; // Iterate explicit relationships, finding matches and // determining whether the object has an explicit basic cmodel. Set<RelationshipTuple> foundRels = new HashSet<RelationshipTuple>(m_rels.size()); 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, t.language)) { continue; } } foundRels.add(t); } // If necessary, add the current basic cmodel to the set of matches if (!basicExplicit && (subject == null || JRDF.sameSubject(subject, 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.concat(m_pid), Constants.MODEL.HAS_MODEL.uri, Models.FEDORA_OBJECT_CURRENT.uri, false, null)); } return foundRels; } public List<String> getContentModels() { Set<RelationshipTuple> cmTubles = getRelationships(Constants.MODEL.HAS_MODEL, null); List<String> cms = new ArrayList<String>(cmTubles.size()); 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; if (iter != null) { 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 == null){ latestRels = v; break; } 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 (d.DSCreateDT == null || 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; } } } }