events = Collections.synchronizedList(ctx.getEvents());
- if (events == null)
+ if (!ctx.hasEvents())
{
return;
}
@@ -82,7 +79,7 @@ public class BasicDispatcher extends Dispatcher
if (log.isDebugEnabled())
{
log.debug("Processing queue of "
- + String.valueOf(events.size()) + " events.");
+ + String.valueOf(ctx.getEvents().size()) + " events.");
}
// transaction identifier applies to all events created in
@@ -90,8 +87,9 @@ public class BasicDispatcher extends Dispatcher
// some letters so RDF readers don't mistake it for an integer.
String tid = "TX" + Utils.generateKey();
- for (Event event : events)
+ while (ctx.hasEvents())
{
+ Event event = ctx.pollEvent();
event.setDispatcher(getIdentifier());
event.setTransactionID(tid);
diff --git a/dspace-api/src/main/java/org/dspace/handle/HandleManager.java b/dspace-api/src/main/java/org/dspace/handle/HandleManager.java
index 42ca647321..60c4dfe32a 100644
--- a/dspace-api/src/main/java/org/dspace/handle/HandleManager.java
+++ b/dspace-api/src/main/java/org/dspace/handle/HandleManager.java
@@ -27,13 +27,13 @@ import org.dspace.storage.rdbms.TableRowIterator;
/**
* Interface to the CNRI Handle
* System .
- *
+ *
*
* Currently, this class simply maps handles to local facilities; handles which
* are owned by other sites (including other DSpaces) are treated as
* non-existent.
*
- *
+ *
* @author Peter Breton
* @version $Revision$
*/
@@ -52,10 +52,10 @@ public class HandleManager
/**
* Return the local URL for handle, or null if handle cannot be found.
- *
+ *
* The returned URL is a (non-handle-based) location where a dissemination
* of the object referred to by handle can be obtained.
- *
+ *
* @param context
* DSpace context
* @param handle
@@ -87,17 +87,17 @@ public class HandleManager
/**
* Transforms handle into the canonical form hdl:handle.
- *
+ *
* No attempt is made to verify that handle is in fact valid.
- *
+ *
* @param handle
* The handle
* @return The canonical form
*/
public static String getCanonicalForm(String handle)
{
-
- // Let the admin define a new prefix, if not then we'll use the
+
+ // Let the admin define a new prefix, if not then we'll use the
// CNRI default. This allows the admin to use "hdl:" if they want to or
// use a locally branded prefix handle.myuni.edu.
String handlePrefix = ConfigurationManager.getProperty("handle.canonical.prefix");
@@ -105,7 +105,7 @@ public class HandleManager
{
handlePrefix = "http://hdl.handle.net/";
}
-
+
return handlePrefix + handle;
}
@@ -126,7 +126,7 @@ public class HandleManager
/**
* Creates a new handle in the database.
- *
+ *
* @param context
* DSpace context
* @param dso
@@ -158,7 +158,7 @@ public class HandleManager
/**
* Creates a handle entry, but with a handle supplied by the caller (new
* Handle not generated)
- *
+ *
* @param context
* DSpace context
* @param dso
@@ -234,21 +234,24 @@ public class HandleManager
public static void unbindHandle(Context context, DSpaceObject dso)
throws SQLException
{
- TableRow row = getHandleInternal(context, dso.getType(), dso.getID());
- if (row != null)
+ TableRowIterator rows = getInternalHandles(context, dso.getType(), dso.getID());
+ if (rows != null)
{
- //Only set the "resouce_id" column to null when unbinding a handle.
- // We want to keep around the "resource_type_id" value, so that we
- // can verify during a restore whether the same *type* of resource
- // is reusing this handle!
- row.setColumnNull("resource_id");
- DatabaseManager.update(context, row);
-
- if(log.isDebugEnabled())
+ while (rows.hasNext())
{
- log.debug("Unbound Handle " + row.getStringColumn("handle") + " from object " + Constants.typeText[dso.getType()] + " id=" + dso.getID());
- }
+ TableRow row = rows.next();
+ //Only set the "resouce_id" column to null when unbinding a handle.
+ // We want to keep around the "resource_type_id" value, so that we
+ // can verify during a restore whether the same *type* of resource
+ // is reusing this handle!
+ row.setColumnNull("resource_id");
+ DatabaseManager.update(context, row);
+ if(log.isDebugEnabled())
+ {
+ log.debug("Unbound Handle " + row.getStringColumn("handle") + " from object " + Constants.typeText[dso.getType()] + " id=" + dso.getID());
+ }
+ }
}
else
{
@@ -259,7 +262,7 @@ public class HandleManager
/**
* Return the object which handle maps to, or null. This is the object
* itself, not a URL which points to it.
- *
+ *
* @param context
* DSpace context
* @param handle
@@ -288,7 +291,7 @@ public class HandleManager
}
// check if handle was allocated previously, but is currently not
- // associated with a DSpaceObject
+ // associated with a DSpaceObject
// (this may occur when 'unbindHandle()' is called for an obj that was removed)
if ((dbhandle.isColumnNull("resource_type_id"))
|| (dbhandle.isColumnNull("resource_id")))
@@ -344,7 +347,7 @@ public class HandleManager
/**
* Return the handle for an Object, or null if the Object has no handle.
- *
+ *
* @param context
* DSpace context
* @param dso
@@ -356,8 +359,8 @@ public class HandleManager
public static String findHandle(Context context, DSpaceObject dso)
throws SQLException
{
- TableRow row = getHandleInternal(context, dso.getType(), dso.getID());
- if (row == null)
+ TableRowIterator rows = getInternalHandles(context, dso.getType(), dso.getID());
+ if (rows == null || !rows.hasNext())
{
if (dso.getType() == Constants.SITE)
{
@@ -370,13 +373,27 @@ public class HandleManager
}
else
{
- return row.getStringColumn("handle");
+ //TODO: Move this code away from the HandleManager & into the Identifier provider
+ //Attempt to retrieve a handle that does NOT look like {handle.part}/{handle.part}.{version}
+ String result = rows.next().getStringColumn("handle");
+ while (rows.hasNext())
+ {
+ TableRow row = rows.next();
+ //Ensure that the handle doesn't look like this 12346/213.{version}
+ //If we find a match that indicates that we have a proper handle
+ if(!row.getStringColumn("handle").matches(".*/.*\\.\\d+"))
+ {
+ result = row.getStringColumn("handle");
+ }
+ }
+
+ return result;
}
}
/**
* Return all the handles which start with prefix.
- *
+ *
* @param context
* DSpace context
* @param prefix
@@ -434,7 +451,7 @@ public class HandleManager
/**
* Return the handle for an Object, or null if the Object has no handle.
- *
+ *
* @param context
* DSpace context
* @param type
@@ -445,18 +462,18 @@ public class HandleManager
* @exception SQLException
* If a database error occurs
*/
- private static TableRow getHandleInternal(Context context, int type, int id)
+ private static TableRowIterator getInternalHandles(Context context, int type, int id)
throws SQLException
- {
+ {
String sql = "SELECT * FROM Handle WHERE resource_type_id = ? " +
"AND resource_id = ?";
- return DatabaseManager.querySingleTable(context, "Handle", sql, type, id);
+ return DatabaseManager.queryTable(context, "Handle", sql, type, id);
}
/**
* Find the database row corresponding to handle.
- *
+ *
* @param context
* DSpace context
* @param handle
diff --git a/dspace-api/src/main/java/org/dspace/identifier/Handle.java b/dspace-api/src/main/java/org/dspace/identifier/Handle.java
new file mode 100644
index 0000000000..e66b3806c6
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/Handle.java
@@ -0,0 +1,18 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class Handle implements Identifier {
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java
new file mode 100644
index 0000000000..371b0e83e1
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java
@@ -0,0 +1,448 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+import org.apache.log4j.Logger;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.*;
+import org.dspace.core.ConfigurationManager;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.core.LogManager;
+import org.dspace.storage.rdbms.DatabaseManager;
+import org.dspace.storage.rdbms.TableRow;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.sql.SQLException;
+
+/**
+ * The old DSpace handle identifier service, used to create handles or retrieve objects based on their handle
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+@Component
+public class HandleIdentifierProvider extends IdentifierProvider {
+ /** log4j category */
+ private static Logger log = Logger.getLogger(HandleIdentifierProvider.class);
+
+ /** Prefix registered to no one */
+ protected static final String EXAMPLE_PREFIX = "123456789";
+
+ protected String[] supportedPrefixes = new String[]{"info:hdl", "hdl", "http://"};
+
+ @Override
+ public boolean supports(Class extends Identifier> identifier) {
+ return Handle.class.isAssignableFrom(identifier);
+ }
+
+ public boolean supports(String identifier)
+ {
+ for(String prefix : supportedPrefixes){
+ if(identifier.startsWith(prefix))
+ {
+ return true;
+ }
+ }
+
+ try {
+ String outOfUrl = retrieveHandleOutOfUrl(identifier);
+ if(outOfUrl != null)
+ {
+ return true;
+ }
+ } catch (SQLException e) {
+ log.error(e.getMessage(), e);
+ }
+
+ return false;
+ }
+
+ public String register(Context context, DSpaceObject dso) {
+ try{
+ String id = mint(context, dso);
+
+ // move canonical to point the latest version
+ if(dso instanceof Item)
+ {
+ Item item = (Item)dso;
+ populateHandleMetadata(item, id);
+ }
+
+ return id;
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID(), e);
+ }
+ }
+
+ public void register(Context context, DSpaceObject dso, String identifier) {
+ try{
+ createNewIdentifier(context, dso, identifier);
+ if(dso instanceof Item)
+ {
+ Item item = (Item)dso;
+ populateHandleMetadata(item, identifier);
+ }
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID(), e);
+ }
+ }
+
+
+ public void reserve(Context context, DSpaceObject dso, String identifier) {
+ try{
+ TableRow handle = DatabaseManager.create(context, "Handle");
+ modifyHandleRecord(context, dso, handle, identifier);
+ }catch(Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID());
+ }
+ }
+
+
+ /**
+ * Creates a new handle in the database.
+ *
+ * @param context DSpace context
+ * @param dso The DSpaceObject to create a handle for
+ * @return The newly created handle
+ * @exception java.sql.SQLException If a database error occurs
+ */
+ public String mint(Context context, DSpaceObject dso) {
+ if(dso.getHandle() != null)
+ {
+ return dso.getHandle();
+ }
+
+ try{
+ return createNewIdentifier(context, dso, null);
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID());
+ }
+ }
+
+ public DSpaceObject resolve(Context context, String identifier, String... attributes) {
+ // We can do nothing with this, return null
+ try
+ {
+ TableRow dbhandle = findHandleInternal(context, identifier);
+
+ if (dbhandle == null)
+ {
+ //Check for an url
+ identifier = retrieveHandleOutOfUrl(identifier);
+ if(identifier != null)
+ {
+ dbhandle = findHandleInternal(context, identifier);
+ }
+
+ if(dbhandle == null)
+ {
+ return null;
+ }
+ }
+
+ if ((dbhandle.isColumnNull("resource_type_id")) || (dbhandle.isColumnNull("resource_id")))
+ {
+ throw new IllegalStateException("No associated resource type");
+ }
+
+ // What are we looking at here?
+ int handletypeid = dbhandle.getIntColumn("resource_type_id");
+ int resourceID = dbhandle.getIntColumn("resource_id");
+
+ if (handletypeid == Constants.ITEM)
+ {
+ Item item = Item.find(context, resourceID);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Resolved handle " + identifier + " to item "
+ + ((item == null) ? (-1) : item.getID()));
+ }
+
+ return item;
+ }
+ else if (handletypeid == Constants.COLLECTION)
+ {
+ Collection collection = Collection.find(context, resourceID);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Resolved handle " + identifier + " to collection "
+ + ((collection == null) ? (-1) : collection.getID()));
+ }
+
+ return collection;
+ }
+ else if (handletypeid == Constants.COMMUNITY)
+ {
+ Community community = Community.find(context, resourceID);
+
+ if (log.isDebugEnabled())
+ {
+ log.debug("Resolved handle " + identifier + " to community "
+ + ((community == null) ? (-1) : community.getID()));
+ }
+
+ return community;
+ }
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while resolving handle to item", "handle: " + identifier), e);
+ }
+// throw new IllegalStateException("Unsupported Handle Type "
+// + Constants.typeText[handletypeid]);
+ return null;
+ }
+
+ @Override
+ public String lookup(Context context, DSpaceObject dso) throws IdentifierNotFoundException, IdentifierNotResolvableException {
+
+ try
+ {
+ TableRow row = getHandleInternal(context, dso.getType(), dso.getID());
+ if (row == null)
+ {
+ if (dso.getType() == Constants.SITE)
+ {
+ return Site.getSiteHandle();
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ return row.getStringColumn("handle");
+ }
+ }catch(SQLException sqe){
+ throw new IdentifierNotResolvableException(sqe.getMessage(),sqe);
+ }
+ }
+
+ @Override
+ public void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException {
+ delete(context, dso);
+ }
+
+ public void delete(Context context, DSpaceObject dso) throws IdentifierException {
+ try{
+ TableRow row = getHandleInternal(context, dso.getType(), dso.getID());
+ if (row != null)
+ {
+ //Only set the "resouce_id" column to null when unbinding a handle.
+ // We want to keep around the "resource_type_id" value, so that we
+ // can verify during a restore whether the same *type* of resource
+ // is reusing this handle!
+ row.setColumnNull("resource_id");
+ DatabaseManager.update(context, row);
+
+ if(log.isDebugEnabled())
+ {
+ log.debug("Unbound Handle " + row.getStringColumn("handle") + " from object " + Constants.typeText[dso.getType()] + " id=" + dso.getID());
+ }
+
+ }
+ else
+ {
+ log.warn("Cannot find Handle entry to unbind for object " + Constants.typeText[dso.getType()] + " id=" + dso.getID());
+ }
+ }catch(SQLException sqe)
+ {
+ throw new IdentifierException(sqe.getMessage(),sqe);
+ }
+
+ }
+
+ public static String retrieveHandleOutOfUrl(String url)
+ throws SQLException {
+ // We can do nothing with this, return null
+ if (!url.contains("/"))
+ {
+ return null;
+ }
+
+ String[] splitUrl = url.split("/");
+
+ return splitUrl[splitUrl.length - 2] + "/" + splitUrl[splitUrl.length - 1];
+ }
+
+ /**
+ * Get the configured Handle prefix string, or a default
+ * @return configured prefix or "123456789"
+ */
+ public static String getPrefix()
+ {
+ String prefix = ConfigurationManager.getProperty("handle.prefix");
+ if (null == prefix)
+ {
+ prefix = EXAMPLE_PREFIX; // XXX no good way to exit cleanly
+ log.error("handle.prefix is not configured; using " + prefix);
+ }
+ return prefix;
+ }
+
+ protected static String getCanonicalForm(String handle)
+ {
+
+ // Let the admin define a new prefix, if not then we'll use the
+ // CNRI default. This allows the admin to use "hdl:" if they want to or
+ // use a locally branded prefix handle.myuni.edu.
+ String handlePrefix = ConfigurationManager.getProperty("handle.canonical.prefix");
+ if (handlePrefix == null || handlePrefix.length() == 0)
+ {
+ handlePrefix = "http://hdl.handle.net/";
+ }
+
+ return handlePrefix + handle;
+ }
+
+ protected String createNewIdentifier(Context context, DSpaceObject dso, String handleId) throws SQLException {
+ TableRow handle=null;
+ if(handleId != null)
+ {
+ handle = findHandleInternal(context, handleId);
+
+
+ if(handle!=null && !handle.isColumnNull("resource_id"))
+ {
+ //Check if this handle is already linked up to this specified DSpace Object
+ if(handle.getIntColumn("resource_id")==dso.getID() &&
+ handle.getIntColumn("resource_type_id")==dso.getType())
+ {
+ //This handle already links to this DSpace Object -- so, there's nothing else we need to do
+ return handleId;
+ }
+ else
+ {
+ //handle found in DB table & already in use by another existing resource
+ throw new IllegalStateException("Attempted to create a handle which is already in use: " + handleId);
+ }
+ }
+
+ }
+ else if(handle!=null && !handle.isColumnNull("resource_type_id"))
+ {
+ //If there is a 'resource_type_id' (but 'resource_id' is empty), then the object using
+ // this handle was previously unbound (see unbindHandle() method) -- likely because object was deleted
+ int previousType = handle.getIntColumn("resource_type_id");
+
+ //Since we are restoring an object to a pre-existing handle, double check we are restoring the same *type* of object
+ // (e.g. we will not allow an Item to be restored to a handle previously used by a Collection)
+ if(previousType != dso.getType())
+ {
+ throw new IllegalStateException("Attempted to reuse a handle previously used by a " +
+ Constants.typeText[previousType] + " for a new " +
+ Constants.typeText[dso.getType()]);
+ }
+ }
+
+ if(handle==null){
+ handle = DatabaseManager.create(context, "Handle");
+ handleId = createId(handle.getIntColumn("handle_id"));
+ }
+
+ modifyHandleRecord(context, dso, handle, handleId);
+ return handleId;
+ }
+
+ protected String modifyHandleRecord(Context context, DSpaceObject dso, TableRow handle, String handleId) throws SQLException {
+ handle.setColumn("handle", handleId);
+ handle.setColumn("resource_type_id", dso.getType());
+ handle.setColumn("resource_id", dso.getID());
+ DatabaseManager.update(context, handle);
+
+ if (log.isDebugEnabled())
+ {
+ log.debug("Created new handle for "
+ + Constants.typeText[dso.getType()] + " " + handleId);
+ }
+ return handleId;
+ }
+
+ /**
+ * Return the handle for an Object, or null if the Object has no handle.
+ *
+ * @param context
+ * DSpace context
+ * @param type
+ * The type of object
+ * @param id
+ * The id of object
+ * @return The handle for object, or null if the object has no handle.
+ * @exception java.sql.SQLException
+ * If a database error occurs
+ */
+ protected static TableRow getHandleInternal(Context context, int type, int id)
+ throws SQLException
+ {
+ String sql = "SELECT * FROM Handle WHERE resource_type_id = ? " +
+ "AND resource_id = ?";
+
+ return DatabaseManager.querySingleTable(context, "Handle", sql, type, id);
+ }
+
+ /**
+ * Find the database row corresponding to handle.
+ *
+ * @param context DSpace context
+ * @param handle The handle to resolve
+ * @return The database row corresponding to the handle
+ * @exception java.sql.SQLException If a database error occurs
+ */
+ protected static TableRow findHandleInternal(Context context, String handle)
+ throws SQLException {
+ if (handle == null)
+ {
+ throw new IllegalArgumentException("Handle is null");
+ }
+ return DatabaseManager.findByUnique(context, "Handle", "handle", handle);
+ }
+
+ /**
+ * Create a new handle id. The implementation uses the PK of the RDBMS
+ * Handle table.
+ *
+ * @return A new handle id
+ * @exception java.sql.SQLException
+ * If a database error occurs
+ */
+ protected static String createId(int id) throws SQLException
+ {
+ String handlePrefix = getPrefix();
+
+ return handlePrefix + (handlePrefix.endsWith("/") ? "" : "/") + id;
+ }
+
+
+ protected void populateHandleMetadata(Item item, String handle)
+ throws SQLException, IOException, AuthorizeException
+ {
+ String handleref = getCanonicalForm(handle);
+
+ // Add handle as identifier.uri DC value.
+ // First check that identifier doesn't already exist.
+ boolean identifierExists = false;
+ DCValue[] identifiers = item.getDC("identifier", "uri", Item.ANY);
+ for (DCValue identifier : identifiers)
+ {
+ if (handleref.equals(identifier.value))
+ {
+ identifierExists = true;
+ }
+ }
+ if (!identifierExists)
+ {
+ item.addDC("identifier", "uri", null, handleref);
+ }
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/Identifier.java b/dspace-api/src/main/java/org/dspace/identifier/Identifier.java
new file mode 100644
index 0000000000..273c7e2f17
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/Identifier.java
@@ -0,0 +1,18 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public interface Identifier {
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierException.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierException.java
new file mode 100644
index 0000000000..7d65790020
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierException.java
@@ -0,0 +1,33 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+/**
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class IdentifierException extends Exception{
+
+ public IdentifierException() {
+ super();
+ }
+
+ public IdentifierException(String message) {
+ super(message);
+ }
+
+ public IdentifierException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public IdentifierException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierNotFoundException.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierNotFoundException.java
new file mode 100644
index 0000000000..170090d1c4
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierNotFoundException.java
@@ -0,0 +1,34 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class IdentifierNotFoundException extends IdentifierException {
+
+ public IdentifierNotFoundException() {
+ super();
+ }
+
+ public IdentifierNotFoundException(String message) {
+ super(message);
+ }
+
+ public IdentifierNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public IdentifierNotFoundException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierNotResolvableException.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierNotResolvableException.java
new file mode 100644
index 0000000000..a69308dd25
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierNotResolvableException.java
@@ -0,0 +1,34 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class IdentifierNotResolvableException extends IdentifierException {
+
+ public IdentifierNotResolvableException() {
+ super();
+ }
+
+ public IdentifierNotResolvableException(String message) {
+ super(message);
+ }
+
+ public IdentifierNotResolvableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public IdentifierNotResolvableException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierProvider.java
new file mode 100644
index 0000000000..42e74fb447
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierProvider.java
@@ -0,0 +1,58 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+import org.dspace.content.DSpaceObject;
+import org.dspace.core.Context;
+import org.dspace.services.ConfigurationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public abstract class IdentifierProvider {
+
+ protected IdentifierService parentService;
+
+ protected ConfigurationService configurationService;
+
+ @Autowired
+ @Required
+ public void setConfigurationService(ConfigurationService configurationService) {
+ this.configurationService = configurationService;
+ }
+
+ public void setParentService(IdentifierService parentService) {
+ this.parentService = parentService;
+ }
+
+ public abstract boolean supports(Class extends Identifier> identifier);
+
+ public abstract boolean supports(String identifier);
+
+ public abstract String register(Context context, DSpaceObject item) throws IdentifierException;
+
+ public abstract String mint(Context context, DSpaceObject dso) throws IdentifierException;
+
+ public abstract DSpaceObject resolve(Context context, String identifier, String... attributes) throws IdentifierNotFoundException, IdentifierNotResolvableException;;
+
+ public abstract String lookup(Context context, DSpaceObject object) throws IdentifierNotFoundException, IdentifierNotResolvableException;;
+
+ public abstract void delete(Context context, DSpaceObject dso) throws IdentifierException;
+
+ public abstract void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException;
+
+ public abstract void reserve(Context context, DSpaceObject dso, String identifier) throws IdentifierException;
+
+ public abstract void register(Context context, DSpaceObject object, String identifier);
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierService.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierService.java
new file mode 100644
index 0000000000..e9af1449f3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierService.java
@@ -0,0 +1,130 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.DSpaceObject;
+import org.dspace.core.Context;
+
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public interface IdentifierService {
+
+ /**
+ *
+ * @param context
+ * @param dso
+ * @param identifier
+ * @return
+ */
+ String lookup(Context context, DSpaceObject dso, Class extends Identifier> identifier);
+
+ /**
+ *
+ * This will resolve a DSpaceObject based on a provided Identifier. The Service will interrogate the providers in
+ * no particular order and return the first successful result discovered. If no resolution is successful,
+ * the method will return null if no object is found.
+ *
+ * TODO: Verify null is returned.
+ *
+ * @param context
+ * @param identifier
+ * @return
+ * @throws IdentifierNotFoundException
+ * @throws IdentifierNotResolvableException
+ */
+ DSpaceObject resolve(Context context, String identifier) throws IdentifierNotFoundException, IdentifierNotResolvableException;
+
+ /**
+ *
+ * Reserves any identifiers necessary based on the capabilities of all providers in the service.
+ *
+ * @param context
+ * @param dso
+ * @throws org.dspace.authorize.AuthorizeException
+ * @throws java.sql.SQLException
+ * @throws IdentifierException
+ */
+ void reserve(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException;
+
+ /**
+ *
+ * Used to Reserve a Specific Identifier (for example a Handle, hdl:1234.5/6) The provider is responsible for
+ * Detecting and Processing the appropriate identifier, all Providers are interrogated, multiple providers
+ * can process the same identifier.
+ *
+ * @param context
+ * @param dso
+ * @param identifier
+ * @throws org.dspace.authorize.AuthorizeException
+ * @throws java.sql.SQLException
+ * @throws IdentifierException
+ */
+ void reserve(Context context, DSpaceObject dso, String identifier) throws AuthorizeException, SQLException, IdentifierException;
+
+ /**
+ *
+ * @param context
+ * @param dso
+ * @return
+ * @throws org.dspace.authorize.AuthorizeException
+ * @throws java.sql.SQLException
+ * @throws IdentifierException
+ */
+ void register(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException;
+
+ /**
+ *
+ * Used to Register a Specific Identifier (for example a Handle, hdl:1234.5/6) The provider is responsible for
+ * Detecting and Processing the appropriate identifier, all Providers are interrogated, multiple providers
+ * can process the same identifier.
+ *
+ * @param context
+ * @param dso
+ * @param identifier
+ * @return
+ * @throws org.dspace.authorize.AuthorizeException
+ * @throws java.sql.SQLException
+ * @throws IdentifierException
+ */
+ void register(Context context, DSpaceObject dso, String identifier) throws AuthorizeException, SQLException, IdentifierException;
+
+ /**
+ * Delete (Unbind) all identifiers registered for a specific DSpace item. Identifiers are "unbound" across
+ * all providers in no particular order.
+ *
+ * @param context
+ * @param dso
+ * @throws org.dspace.authorize.AuthorizeException
+ * @throws java.sql.SQLException
+ * @throws IdentifierException
+ */
+ void delete(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException;
+
+ /**
+ * Used to Delete a Specific Identifier (for example a Handle, hdl:1234.5/6) The provider is responsible for
+ * Detecting and Processing the appropriate identifier, all Providers are interrogated, multiple providers
+ * can process the same identifier.
+ *
+ * @param context
+ * @param dso
+ * @param identifier
+ * @throws org.dspace.authorize.AuthorizeException
+ * @throws java.sql.SQLException
+ * @throws IdentifierException
+ */
+ void delete(Context context, DSpaceObject dso, String identifier) throws AuthorizeException, SQLException, IdentifierException;
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java
new file mode 100644
index 0000000000..d15414204c
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java
@@ -0,0 +1,165 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+import org.apache.log4j.Logger;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.DSpaceObject;
+import org.dspace.core.Context;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Required;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * The main service class used to reserve, register and resolve identifiers
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class IdentifierServiceImpl implements IdentifierService {
+
+ private List providers;
+
+ /** log4j category */
+ private static Logger log = Logger.getLogger(IdentifierServiceImpl.class);
+
+ @Autowired
+ @Required
+ public void setProviders(List providers)
+ {
+ this.providers = providers;
+
+ for(IdentifierProvider p : providers)
+ {
+ p.setParentService(this);
+ }
+ }
+
+ /**
+ * Reserves identifiers for the item
+ * @param context dspace context
+ * @param dso dspace object
+ */
+ public void reserve(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException {
+ for (IdentifierProvider service : providers)
+ {
+ service.mint(context, dso);
+ }
+ //Update our item
+ dso.update();
+ }
+
+ @Override
+ public void reserve(Context context, DSpaceObject dso, String identifier) throws AuthorizeException, SQLException, IdentifierException {
+ // Next resolve all other services
+ for (IdentifierProvider service : providers)
+ {
+ if(service.supports(identifier))
+ {
+ service.reserve(context, dso, identifier);
+ }
+ }
+ //Update our item
+ dso.update();
+ }
+
+ @Override
+ public void register(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException {
+ //We need to commit our context because one of the providers might require the handle created above
+ // Next resolve all other services
+ for (IdentifierProvider service : providers)
+ {
+ service.register(context, dso);
+ }
+ //Update our item
+ dso.update();
+ }
+
+ @Override
+ public void register(Context context, DSpaceObject object, String identifier) throws AuthorizeException, SQLException, IdentifierException {
+
+ //We need to commit our context because one of the providers might require the handle created above
+ // Next resolve all other services
+ for (IdentifierProvider service : providers)
+ {
+ service.register(context, object, identifier);
+ }
+ //Update our item
+ object.update();
+ }
+
+ @Override
+ public String lookup(Context context, DSpaceObject dso, Class extends Identifier> identifier) {
+ for (IdentifierProvider service : providers)
+ {
+ if(service.supports(identifier))
+ {
+ try{
+ String result = service.lookup(context, dso);
+ if (result != null){
+ return result;
+ }
+ } catch (IdentifierException e) {
+ log.error(e.getMessage(),e);
+ }
+ }
+ }
+ return null;
+ }
+
+ public DSpaceObject resolve(Context context, String identifier) throws IdentifierNotFoundException, IdentifierNotResolvableException{
+ for (IdentifierProvider service : providers)
+ {
+ if(service.supports(identifier))
+ { try
+ {
+ DSpaceObject result = service.resolve(context, identifier);
+ if (result != null)
+ {
+ return result;
+ }
+ } catch (IdentifierException e) {
+ log.error(e.getMessage(),e);
+ }
+ }
+
+ }
+ return null;
+ }
+
+ public void delete(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException {
+ for (IdentifierProvider service : providers)
+ {
+ try
+ {
+ service.delete(context, dso);
+ } catch (IdentifierException e) {
+ log.error(e.getMessage(),e);
+ }
+ }
+ }
+
+ @Override
+ public void delete(Context context, DSpaceObject dso, String identifier) throws AuthorizeException, SQLException, IdentifierException {
+ for (IdentifierProvider service : providers)
+ {
+ try
+ {
+ if(service.supports(identifier))
+ {
+ service.delete(context, dso, identifier);
+ }
+ } catch (IdentifierException e) {
+ log.error(e.getMessage(),e);
+ }
+ }
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProvider.java
new file mode 100644
index 0000000000..ffcbdcc5e5
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProvider.java
@@ -0,0 +1,689 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.identifier;
+
+import org.apache.log4j.Logger;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.*;
+import org.dspace.core.ConfigurationManager;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.core.LogManager;
+import org.dspace.storage.rdbms.DatabaseManager;
+import org.dspace.storage.rdbms.TableRow;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.*;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Date;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+@Component
+public class VersionedHandleIdentifierProvider extends IdentifierProvider {
+ /** log4j category */
+ private static Logger log = Logger.getLogger(VersionedHandleIdentifierProvider.class);
+
+ /** Prefix registered to no one */
+ static final String EXAMPLE_PREFIX = "123456789";
+
+ private static final char DOT = '.';
+
+ private String[] supportedPrefixes = new String[]{"info:hdl", "hdl", "http://"};
+
+ private VersionDAO versionDAO;
+ private VersionHistoryDAO versionHistoryDAO;
+
+ @Override
+ public boolean supports(Class extends Identifier> identifier)
+ {
+ return Handle.class.isAssignableFrom(identifier);
+ }
+
+ public boolean supports(String identifier)
+ {
+ for(String prefix : supportedPrefixes)
+ {
+ if(identifier.startsWith(prefix))
+ {
+ return true;
+ }
+ }
+
+ try {
+ String outOfUrl = retrieveHandleOutOfUrl(identifier);
+ if(outOfUrl != null)
+ {
+ return true;
+ }
+ } catch (SQLException e) {
+ log.error(e.getMessage(), e);
+ }
+
+ return false;
+ }
+
+ public String register(Context context, DSpaceObject dso)
+ {
+ try
+ {
+ String id = mint(context, dso);
+
+ // move canonical to point the latest version
+ if(dso != null && dso.getType() == Constants.ITEM)
+ {
+ Item item = (Item)dso;
+ VersionHistory history = retrieveVersionHistory(context, (Item)dso);
+ if(history!=null)
+ {
+ String canonical = getCanonical(item);
+ // Modify Canonical: 12345/100 will point to the new item
+ TableRow canonicalRecord = findHandleInternal(context, canonical);
+ modifyHandleRecord(context, dso, canonicalRecord, canonical);
+
+ // in case of first version we have to modify the previous metadata to be xxxx.1
+ Version version = history.getVersion(item);
+ Version previous = history.getPrevious(version);
+ if (history.isFirstVersion(previous))
+ {
+ modifyHandleMetadata(previous.getItem(), (canonical + DOT + 1));
+ }
+ // Check if our previous item hasn't got a handle anymore.
+ // This only occurs when a switch has been made from the standard handle identifier provider
+ // to the versioned one, in this case no "versioned handle" is reserved so we need to create one
+ if(previous != null && getHandleInternal(context, Constants.ITEM, previous.getItemID()) == null){
+ makeIdentifierBasedOnHistory(context, previous.getItem(), canonical, history);
+
+ }
+ }
+ populateHandleMetadata(item);
+ }
+
+ return id;
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + (dso != null ? dso.getID() : "")), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + (dso != null ? dso.getID() : ""));
+ }
+ }
+
+ public void register(Context context, DSpaceObject dso, String identifier)
+ {
+ try
+ {
+
+ Item item = (Item) dso;
+
+ // if for this identifier is already present a record in the Handle table and the corresponding item
+ // has an history someone is trying to restore the latest version for the item. When
+ // trying to restore the latest version the identifier in input doesn't have the for 1234/123.latestVersion
+ // it is the canonical 1234/123
+ VersionHistory itemHistory = getHistory(context, identifier);
+ if(!identifier.matches(".*/.*\\.\\d+") && itemHistory!=null){
+
+ int newVersionNumber = itemHistory.getLatestVersion().getVersionNumber()+1;
+ String canonical = identifier;
+ identifier = identifier.concat(".").concat("" + newVersionNumber);
+ restoreItAsVersion(context, dso, identifier, item, canonical, itemHistory);
+ }
+ // if identifier == 1234.5/100.4 reinstate the version 4 in the version table if absent
+ else if(identifier.matches(".*/.*\\.\\d+"))
+ {
+ // if it is a version of an item is needed to put back the record
+ // in the versionitem table
+ String canonical = getCanonical(identifier);
+ DSpaceObject canonicalItem = this.resolve(context, canonical);
+ if(canonicalItem==null){
+ restoreItAsCanonical(context, dso, identifier, item, canonical);
+ }
+ else{
+ VersionHistory history = retrieveVersionHistory(context, (Item)canonicalItem);
+ if(history==null){
+ restoreItAsCanonical(context, dso, identifier, item, canonical);
+ }
+ else
+ {
+ restoreItAsVersion(context, dso, identifier, item, canonical, history);
+
+ }
+ }
+ }
+ else
+ {
+ //A regular handle
+ createNewIdentifier(context, dso, identifier);
+ if(dso instanceof Item)
+ {
+ populateHandleMetadata(item);
+ }
+ }
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID(), e);
+ }
+ }
+
+ private VersionHistory getHistory(Context context, String identifier)
+ {
+ DSpaceObject item = this.resolve(context, identifier);
+ if(item!=null){
+ VersionHistory history = retrieveVersionHistory(context, (Item)item);
+ return history;
+ }
+ return null;
+ }
+
+ private void restoreItAsVersion(Context context, DSpaceObject dso, String identifier, Item item, String canonical, VersionHistory history) throws SQLException, IOException, AuthorizeException
+ {
+ createNewIdentifier(context, dso, identifier);
+ populateHandleMetadata(item);
+
+ int versionNumber = Integer.parseInt(identifier.substring(identifier.lastIndexOf(".") + 1));
+ createVersion(context, history, item, "Restoring from AIP Service", new Date(), versionNumber);
+ Version latest = history.getLatestVersion();
+
+
+ // if restoring the lastest version: needed to move the canonical
+ if(latest.getVersionNumber() < versionNumber){
+ TableRow canonicalRecord = findHandleInternal(context, canonical);
+ modifyHandleRecord(context, dso, canonicalRecord, canonical);
+ }
+ }
+
+ private void restoreItAsCanonical(Context context, DSpaceObject dso, String identifier, Item item, String canonical) throws SQLException, IOException, AuthorizeException
+ {
+ createNewIdentifier(context, dso, identifier);
+ populateHandleMetadata(item);
+
+ int versionNumber = Integer.parseInt(identifier.substring(identifier.lastIndexOf(".")+1));
+ VersionHistory history=versionHistoryDAO.create(context);
+ createVersion(context, history, item, "Restoring from AIP Service", new Date(), versionNumber);
+
+ TableRow canonicalRecord = findHandleInternal(context, canonical);
+ modifyHandleRecord(context, dso, canonicalRecord, canonical);
+
+ }
+
+
+ public void reserve(Context context, DSpaceObject dso, String identifier)
+ {
+ try{
+ TableRow handle = DatabaseManager.create(context, "Handle");
+ modifyHandleRecord(context, dso, handle, identifier);
+ }catch(Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID());
+ }
+ }
+
+
+ /**
+ * Creates a new handle in the database.
+ *
+ * @param context DSpace context
+ * @param dso The DSpaceObject to create a handle for
+ * @return The newly created handle
+ */
+ public String mint(Context context, DSpaceObject dso)
+ {
+ if(dso.getHandle() != null)
+ {
+ return dso.getHandle();
+ }
+
+ try{
+ String handleId = null;
+ VersionHistory history = null;
+ if(dso instanceof Item)
+ {
+ history = retrieveVersionHistory(context, (Item)dso);
+ }
+
+ if(history!=null)
+ {
+ handleId = makeIdentifierBasedOnHistory(context, dso, handleId, history);
+ }else{
+ handleId = createNewIdentifier(context, dso, null);
+ }
+ return handleId;
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
+ throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID());
+ }
+ }
+
+ public DSpaceObject resolve(Context context, String identifier, String... attributes)
+ {
+ // We can do nothing with this, return null
+ try{
+ TableRow dbhandle = findHandleInternal(context, identifier);
+
+ if (dbhandle == null)
+ {
+ //Check for an url
+ identifier = retrieveHandleOutOfUrl(identifier);
+ if(identifier != null)
+ {
+ dbhandle = findHandleInternal(context, identifier);
+ }
+
+ if(dbhandle == null)
+ {
+ return null;
+ }
+ }
+
+ if ((dbhandle.isColumnNull("resource_type_id"))
+ || (dbhandle.isColumnNull("resource_id")))
+ {
+ throw new IllegalStateException("No associated resource type");
+ }
+
+ // What are we looking at here?
+ int handletypeid = dbhandle.getIntColumn("resource_type_id");
+ int resourceID = dbhandle.getIntColumn("resource_id");
+
+ if (handletypeid == Constants.ITEM)
+ {
+ Item item = Item.find(context, resourceID);
+
+ if (log.isDebugEnabled())
+ {
+ log.debug("Resolved handle " + identifier + " to item "
+ + ((item == null) ? (-1) : item.getID()));
+ }
+
+ return item;
+ }
+ else if (handletypeid == Constants.COLLECTION)
+ {
+ Collection collection = Collection.find(context, resourceID);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Resolved handle " + identifier + " to collection "
+ + ((collection == null) ? (-1) : collection.getID()));
+ }
+
+ return collection;
+ }
+ else if (handletypeid == Constants.COMMUNITY)
+ {
+ Community community = Community.find(context, resourceID);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Resolved handle " + identifier + " to community "
+ + ((community == null) ? (-1) : community.getID()));
+ }
+
+ return community;
+ }
+
+
+ }catch (Exception e){
+ log.error(LogManager.getHeader(context, "Error while resolving handle to item", "handle: " + identifier), e);
+ }
+// throw new IllegalStateException("Unsupported Handle Type "
+// + Constants.typeText[handletypeid]);
+ return null;
+ }
+
+ @Override
+ public String lookup(Context context, DSpaceObject dso) throws IdentifierNotFoundException, IdentifierNotResolvableException {
+
+ try
+ {
+ TableRow row = getHandleInternal(context, dso.getType(), dso.getID());
+ if (row == null)
+ {
+ if (dso.getType() == Constants.SITE)
+ {
+ return Site.getSiteHandle();
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ return row.getStringColumn("handle");
+ }
+ }catch(SQLException sqe){
+ throw new IdentifierNotResolvableException(sqe.getMessage(),sqe);
+ }
+ }
+
+ @Override
+ public void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException {
+ delete(context, dso);
+ }
+
+ public void delete(Context context, DSpaceObject dso) throws IdentifierException {
+
+ try {
+ if (dso instanceof Item)
+ {
+ Item item = (Item) dso;
+
+ // If it is the most current version occurs to move the canonical to the previous version
+ VersionHistory history = retrieveVersionHistory(context, item);
+ if(history!=null && history.getLatestVersion().getItem().equals(item) && history.size() > 1)
+ {
+ Item previous = history.getPrevious(history.getLatestVersion()).getItem();
+
+ // Modify Canonical: 12345/100 will point to the new item
+ String canonical = getCanonical(previous);
+ TableRow canonicalRecord = findHandleInternal(context, canonical);
+ modifyHandleRecord(context, previous, canonicalRecord, canonical);
+ }
+ }
+ } catch (Exception e) {
+ log.error(LogManager.getHeader(context, "Error while attempting to register doi", "Item id: " + dso.getID()), e);
+ throw new IdentifierException("Error while moving doi identifier", e);
+ }
+
+
+ }
+
+ public static String retrieveHandleOutOfUrl(String url) throws SQLException
+ {
+ // We can do nothing with this, return null
+ if (!url.contains("/")) return null;
+
+ String[] splitUrl = url.split("/");
+
+ return splitUrl[splitUrl.length - 2] + "/" + splitUrl[splitUrl.length - 1];
+ }
+
+ /**
+ * Get the configured Handle prefix string, or a default
+ * @return configured prefix or "123456789"
+ */
+ public static String getPrefix()
+ {
+ String prefix = ConfigurationManager.getProperty("handle.prefix");
+ if (null == prefix)
+ {
+ prefix = EXAMPLE_PREFIX; // XXX no good way to exit cleanly
+ log.error("handle.prefix is not configured; using " + prefix);
+ }
+ return prefix;
+ }
+
+ protected static String getCanonicalForm(String handle)
+ {
+
+ // Let the admin define a new prefix, if not then we'll use the
+ // CNRI default. This allows the admin to use "hdl:" if they want to or
+ // use a locally branded prefix handle.myuni.edu.
+ String handlePrefix = ConfigurationManager.getProperty("handle.canonical.prefix");
+ if (handlePrefix == null || handlePrefix.length() == 0)
+ {
+ handlePrefix = "http://hdl.handle.net/";
+ }
+
+ return handlePrefix + handle;
+ }
+
+ protected String createNewIdentifier(Context context, DSpaceObject dso, String handleId) throws SQLException {
+ TableRow handle=null;
+ if(handleId != null)
+ {
+ handle = findHandleInternal(context, handleId);
+
+
+ if(handle!=null && !handle.isColumnNull("resource_id"))
+ {
+ //Check if this handle is already linked up to this specified DSpace Object
+ int resourceID = handle.getIntColumn("resource_id");
+ int resourceType = handle.getIntColumn("resource_type_id");
+
+ if(resourceID==dso.getID() && resourceType ==dso.getType())
+ {
+ //This handle already links to this DSpace Object -- so, there's nothing else we need to do
+ return handleId;
+ }
+ else
+ {
+ //handle found in DB table & already in use by another existing resource
+ throw new IllegalStateException("Attempted to create a handle which is already in use: " + handleId);
+ }
+ }
+
+ }
+ else if(handle!=null && !handle.isColumnNull("resource_type_id"))
+ {
+ //If there is a 'resource_type_id' (but 'resource_id' is empty), then the object using
+ // this handle was previously unbound (see unbindHandle() method) -- likely because object was deleted
+ int previousType = handle.getIntColumn("resource_type_id");
+
+ //Since we are restoring an object to a pre-existing handle, double check we are restoring the same *type* of object
+ // (e.g. we will not allow an Item to be restored to a handle previously used by a Collection)
+ if(previousType != dso.getType())
+ {
+ throw new IllegalStateException("Attempted to reuse a handle previously used by a " +
+ Constants.typeText[previousType] + " for a new " +
+ Constants.typeText[dso.getType()]);
+ }
+ }
+
+ if(handle==null){
+ handle = DatabaseManager.create(context, "Handle");
+ handleId = createId(handle.getIntColumn("handle_id"));
+ }
+
+ modifyHandleRecord(context, dso, handle, handleId);
+ return handleId;
+ }
+
+ protected String makeIdentifierBasedOnHistory(Context context, DSpaceObject dso, String handleId, VersionHistory history) throws AuthorizeException, SQLException
+ {
+ Item item = (Item)dso;
+
+ // FIRST time a VERSION is created 2 identifiers will be minted and the canonical will be updated to point to the newer URL:
+ // - id.1-->old URL
+ // - id.2-->new URL
+ Version version = history.getVersion(item);
+ Version previous = history.getPrevious(version);
+ String canonical = getCanonical(previous.getItem());
+ if (history.isFirstVersion(previous))
+ {
+ // add a new Identifier for previous item: 12345/100.1
+ String identifierPreviousItem=canonical + DOT + 1;
+ TableRow handle = DatabaseManager.create(context, "Handle");
+ modifyHandleRecord(context, previous.getItem(), handle, identifierPreviousItem);
+ }
+
+
+ // add a new Identifier for this item: 12345/100.x
+ String idNew = canonical + DOT + version.getVersionNumber();
+ TableRow handle = DatabaseManager.create(context, "Handle");
+ modifyHandleRecord(context, dso, handle, idNew);
+
+ return handleId;
+ }
+
+
+ protected String modifyHandleRecord(Context context, DSpaceObject dso, TableRow handle, String handleId) throws SQLException
+ {
+ handle.setColumn("handle", handleId);
+ handle.setColumn("resource_type_id", dso.getType());
+ handle.setColumn("resource_id", dso.getID());
+ DatabaseManager.update(context, handle);
+
+ if (log.isDebugEnabled())
+ {
+ log.debug("Created new handle for "
+ + Constants.typeText[dso.getType()] + " " + handleId);
+ }
+ return handleId;
+ }
+
+ protected String getCanonical(Item item)
+ {
+ String canonical = item.getHandle();
+ if( canonical.lastIndexOf(DOT)!=-1)
+ {
+ canonical = canonical.substring(0, canonical.lastIndexOf(DOT));
+ }
+
+ return canonical;
+ }
+
+ protected String getCanonical(String identifier)
+ {
+ String canonical = identifier;
+ if( canonical.lastIndexOf(DOT)!=-1)
+ {
+ canonical = canonical.substring(0, canonical.lastIndexOf(DOT));
+ }
+
+ return canonical;
+ }
+
+ /**
+ * Find the database row corresponding to handle.
+ *
+ * @param context DSpace context
+ * @param handle The handle to resolve
+ * @return The database row corresponding to the handle
+ * @exception java.sql.SQLException If a database error occurs
+ */
+ protected static TableRow findHandleInternal(Context context, String handle)
+ throws SQLException {
+ if (handle == null)
+ {
+ throw new IllegalArgumentException("Handle is null");
+ }
+
+ return DatabaseManager.findByUnique(context, "Handle", "handle", handle);
+ }
+
+ /**
+ * Return the handle for an Object, or null if the Object has no handle.
+ *
+ * @param context
+ * DSpace context
+ * @param type
+ * The type of object
+ * @param id
+ * The id of object
+ * @return The handle for object, or null if the object has no handle.
+ * @exception java.sql.SQLException
+ * If a database error occurs
+ */
+ protected static TableRow getHandleInternal(Context context, int type, int id)
+ throws SQLException
+ {
+ String sql = "SELECT * FROM Handle WHERE resource_type_id = ? AND resource_id = ?";
+
+ return DatabaseManager.querySingleTable(context, "Handle", sql, type, id);
+ }
+
+ /**
+ * Create a new handle id. The implementation uses the PK of the RDBMS
+ * Handle table.
+ *
+ * @return A new handle id
+ * @exception java.sql.SQLException
+ * If a database error occurs
+ */
+ protected static String createId(int id) throws SQLException
+ {
+ String handlePrefix = getPrefix();
+
+ return handlePrefix + (handlePrefix.endsWith("/") ? "" : "/") + id;
+ }
+
+
+ protected VersionHistory retrieveVersionHistory(Context c, Item item)
+ {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ return versioningService.findVersionHistory(c, item.getID());
+ }
+
+ protected void populateHandleMetadata(Item item)
+ throws SQLException, IOException, AuthorizeException
+ {
+ String handleref = getCanonicalForm(getCanonical(item));
+
+ // Add handle as identifier.uri DC value.
+ // First check that identifier doesn't already exist.
+ boolean identifierExists = false;
+ DCValue[] identifiers = item.getDC("identifier", "uri", Item.ANY);
+ for (DCValue identifier : identifiers)
+ {
+ if (handleref.equals(identifier.value))
+ {
+ identifierExists = true;
+ }
+ }
+ if (!identifierExists)
+ {
+ item.addDC("identifier", "uri", null, handleref);
+ }
+ }
+
+ protected void modifyHandleMetadata(Item item, String handle)
+ throws SQLException, IOException, AuthorizeException
+ {
+ String handleref = getCanonicalForm(handle);
+ item.clearMetadata("dc", "identifier", "uri", Item.ANY);
+ item.addDC("identifier", "uri", null, handleref);
+ item.update();
+ }
+
+
+ protected VersionImpl createVersion(Context c, VersionHistory vh, Item item, String summary, Date date, int versionNumber) {
+ try {
+ VersionImpl version = versionDAO.create(c);
+
+ // check if an equals versionNumber is already present in the DB (at this point it should never happen).
+ if(vh!=null && vh.getVersions()!=null){
+ for(Version v : vh.getVersions()){
+ if(v.getVersionNumber()==versionNumber){
+ throw new RuntimeException("A Version for this versionNumber is already present. Impossible complete the operation.");
+ }
+ }
+ }
+
+ version.setVersionNumber(versionNumber);
+ version.setVersionDate(date);
+ version.setEperson(item.getSubmitter());
+ version.setItemID(item.getID());
+ version.setSummary(summary);
+ version.setVersionHistory(vh.getVersionHistoryId());
+ versionDAO.update(version);
+ return version;
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ protected int getNextVersionNumer(Version latest){
+ if(latest==null) return 1;
+
+ return latest.getVersionNumber()+1;
+ }
+
+ public void setVersionDAO(VersionDAO versionDAO)
+ {
+ this.versionDAO = versionDAO;
+ }
+
+ public void setVersionHistoryDAO(VersionHistoryDAO versionHistoryDAO)
+ {
+ this.versionHistoryDAO = versionHistoryDAO;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageManager.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageManager.java
index f192e93a4c..53fe086b17 100644
--- a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageManager.java
+++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageManager.java
@@ -637,32 +637,38 @@ public class BitstreamStorageManager
continue; // do not delete registered bitstreams
}
- boolean success = file.delete();
- String message = ("Deleted bitstream " + bid + " (file "
- + file.getAbsolutePath() + ") with result "
- + success);
- if (log.isDebugEnabled())
+ // Since versioning allows for multiple bitstreams, check if the internal identifier isn't used on another place
+ TableRow duplicateBitRow = DatabaseManager.querySingleTable(context, "Bitstream", "SELECT * FROM Bitstream WHERE internal_id = ? AND bitstream_id <> ?", row.getStringColumn("internal_id"), bid);
+ if(duplicateBitRow == null)
{
- log.debug(message);
- }
- if (verbose)
- {
- System.out.println(message);
+ boolean success = file.delete();
+
+ String message = ("Deleted bitstream " + bid + " (file "
+ + file.getAbsolutePath() + ") with result "
+ + success);
+ if (log.isDebugEnabled())
+ {
+ log.debug(message);
+ }
+ if (verbose)
+ {
+ System.out.println(message);
+ }
+
+ // if the file was deleted then
+ // try deleting the parents
+ // Otherwise the cleanup script is set to
+ // leave the db records then the file
+ // and directories have already been deleted
+ // if this is turned off then it still looks like the
+ // file exists
+ if( success )
+ {
+ deleteParents(file);
+ }
}
- // if the file was deleted then
- // try deleting the parents
- // Otherwise the cleanup script is set to
- // leave the db records then the file
- // and directories have already been deleted
- // if this is turned off then it still looks like the
- // file exists
- if( success )
- {
- deleteParents(file);
- }
-
// Make sure to commit our outstanding work every 100
// iterations. Otherwise you risk losing the entire transaction
// if we hit an exception, which isn't useful at all for large
@@ -701,6 +707,22 @@ public class BitstreamStorageManager
}
}
+ /**
+ *
+ * @param context
+ * @param id of the bitstream to clone
+ * @return
+ * @throws SQLException
+ */
+ public static int clone(Context context, int id) throws SQLException
+ {
+ TableRow row = DatabaseManager.find(context, "bitstream", id);
+ row.setColumn("bitstream_id", -1);
+ DatabaseManager.insert(context, row);
+ return row.getIntColumn("bitstream_id");
+ }
+
+
////////////////////////////////////////
// Internal methods
////////////////////////////////////////
diff --git a/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java
new file mode 100644
index 0000000000..0ffdf3d976
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/AbstractVersionProvider.java
@@ -0,0 +1,70 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.*;
+import org.dspace.core.Context;
+import org.dspace.storage.bitstore.BitstreamStorageManager;
+
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public abstract class AbstractVersionProvider {
+
+
+ protected void copyMetadata(Item itemNew, Item nativeItem){
+ DCValue[] md = nativeItem.getMetadata(Item.ANY, Item.ANY, Item.ANY, Item.ANY);
+ for (DCValue aMd : md) {
+ if ((aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("relation") && (aMd.qualifier != null && aMd.qualifier.equals("haspart")))
+ || (aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("relation") && (aMd.qualifier != null && aMd.qualifier.equals("ispartof")))
+ || (aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("identifier"))
+ || (aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("relation") && (aMd.qualifier != null && aMd.qualifier.equals("isversionof")))
+ || (aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("date") && (aMd.qualifier != null && aMd.qualifier.equals("accessioned")))
+ || (aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("date") && (aMd.qualifier != null && aMd.qualifier.equals("available")))
+ || (aMd.schema.equals(MetadataSchema.DC_SCHEMA) && aMd.element.equals("description") && (aMd.qualifier != null && aMd.qualifier.equals("provenance"))))
+ {
+ continue;
+ }
+
+
+ itemNew.addMetadata(aMd.schema, aMd.element, aMd.qualifier, aMd.language, aMd.value);
+ }
+ }
+
+ protected void createBundlesAndAddBitstreams(Context c, Item itemNew, Item nativeItem) throws SQLException, AuthorizeException {
+ for(Bundle nativeBundle : nativeItem.getBundles())
+ {
+ Bundle bundleNew = itemNew.createBundle(nativeBundle.getName());
+
+ for(Bitstream nativeBitstream : nativeBundle.getBitstreams())
+ {
+
+ Bitstream bitstreamNew = createBitstream(c, nativeBitstream);
+ bundleNew.addBitstream(bitstreamNew);
+
+ if(nativeBundle.getPrimaryBitstreamID() == nativeBitstream.getID())
+ {
+ bundleNew.setPrimaryBitstreamID(bitstreamNew.getID());
+ }
+ }
+ }
+ }
+
+
+ protected Bitstream createBitstream(Context context, Bitstream nativeBitstream) throws AuthorizeException, SQLException {
+ int idNew = BitstreamStorageManager.clone(context, nativeBitstream.getID());
+ return Bitstream.find(context, idNew);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java
new file mode 100644
index 0000000000..03e46a5837
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/DefaultItemVersionProvider.java
@@ -0,0 +1,95 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.Item;
+import org.dspace.content.WorkspaceItem;
+import org.dspace.core.Context;
+import org.dspace.identifier.IdentifierException;
+import org.dspace.identifier.IdentifierService;
+import org.dspace.utils.DSpace;
+
+import java.io.IOException;
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class DefaultItemVersionProvider extends AbstractVersionProvider implements ItemVersionProvider
+{
+
+ public Item createNewItemAndAddItInWorkspace(Context context, Item nativeItem) {
+ try
+ {
+ WorkspaceItem workspaceItem = WorkspaceItem.create(context, nativeItem.getOwningCollection(), false);
+ Item itemNew = workspaceItem.getItem();
+ itemNew.update();
+ return itemNew;
+ }catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }catch (AuthorizeException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }catch (IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ public void deleteVersionedItem(Context c, Version versionToDelete, VersionHistory history)
+ {
+ try
+ {
+ // if versionToDelete is the current version we have to reinstate the previous version
+ // and reset canonical
+ if(history.isLastVersion(versionToDelete) && history.size() > 1)
+ {
+ // reset the previous version to archived
+ Item item = history.getPrevious(versionToDelete).getItem();
+ item.setArchived(true);
+ item.update();
+ }
+
+ // assign tombstone to the Identifier and reset canonical to the previous version only if there is a previous version
+ IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class);
+ Item itemToDelete=versionToDelete.getItem();
+ identifierService.delete(c, itemToDelete);
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (AuthorizeException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (IdentifierException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ public Item updateItemState(Context c, Item itemNew, Item previousItem)
+ {
+ try
+ {
+ copyMetadata(itemNew, previousItem);
+ createBundlesAndAddBitstreams(c, itemNew, previousItem);
+ IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class);
+ try
+ {
+ identifierService.reserve(c, itemNew);
+ } catch (IdentifierException e) {
+ throw new RuntimeException("Can't create Identifier!");
+ }
+ itemNew.update();
+ return itemNew;
+ }catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } catch (AuthorizeException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java
new file mode 100644
index 0000000000..865944e38a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/ItemVersionProvider.java
@@ -0,0 +1,24 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public interface ItemVersionProvider {
+ public Item createNewItemAndAddItInWorkspace(Context c, Item item);
+ public void deleteVersionedItem(Context c, Version versionToDelete, VersionHistory history);
+ public Item updateItemState(Context c, Item itemNew, Item previousItem);
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/Version.java b/dspace-api/src/main/java/org/dspace/versioning/Version.java
new file mode 100644
index 0000000000..b09710aea3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/Version.java
@@ -0,0 +1,33 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.eperson.EPerson;
+
+import java.util.Date;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public interface Version
+{
+ public EPerson getEperson();
+ public int getItemID();
+ public Date getVersionDate();
+ public int getVersionNumber();
+ public String getSummary();
+ public int getVersionHistoryID();
+ public int getVersionId();
+ public Item getItem();
+}
+
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionDAO.java b/dspace-api/src/main/java/org/dspace/versioning/VersionDAO.java
new file mode 100644
index 0000000000..253ccd01ea
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersionDAO.java
@@ -0,0 +1,169 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.storage.rdbms.DatabaseManager;
+import org.dspace.storage.rdbms.TableRow;
+import org.dspace.storage.rdbms.TableRowIterator;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersionDAO
+{
+
+ protected final static String TABLE_NAME = "versionitem";
+ protected final static String VERSION_ID = "versionitem_id";
+ protected final static String ITEM_ID = "item_id";
+ protected final static String VERSION_NUMBER = "version_number";
+ protected final static String EPERSON_ID = "eperson_id";
+ protected final static String VERSION_DATE = "version_date";
+ protected final static String VERSION_SUMMARY = "version_summary";
+ protected final static String HISTORY_ID = "versionhistory_id";
+
+
+ public VersionImpl find(Context context, int id) {
+ try
+ {
+ TableRow row = DatabaseManager.findByUnique(context, TABLE_NAME, VERSION_ID, id);
+
+ if (row == null)
+ {
+ return null;
+ }
+
+ return new VersionImpl(context, row);
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+
+ }
+
+ public VersionImpl findByItem(Context c, Item item) {
+ return findByItemId(c, item.getID());
+ }
+
+ public VersionImpl findByItemId(Context context, int itemId) {
+ try {
+ if (itemId == 0 || itemId == -1)
+ {
+ return null;
+ }
+
+ VersionImpl fromCache = (VersionImpl) context.fromCache(VersionImpl.class, itemId);
+ if (fromCache != null)
+ {
+ return fromCache;
+ }
+
+ TableRow row = DatabaseManager.findByUnique(context, TABLE_NAME, ITEM_ID, itemId);
+ if (row == null)
+ {
+ return null;
+ }
+
+ return new VersionImpl(context, row);
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+
+ public List findByVersionHistory(Context context, int versionHistoryId) {
+ TableRowIterator tri = null;
+ try {
+ tri = DatabaseManager.query(context, "SELECT * FROM " + TABLE_NAME + " where " + HISTORY_ID + "=" + versionHistoryId + " order by " + VERSION_NUMBER + " desc");
+
+ List versions = new ArrayList();
+ while (tri.hasNext())
+ {
+ TableRow tr = tri.next();
+
+ VersionImpl fromCache = (VersionImpl) context.fromCache(VersionImpl.class, tr.getIntColumn(VERSION_ID));
+
+ if (fromCache != null)
+ {
+ versions.add(fromCache);
+ }else{
+ versions.add(new VersionImpl(context, tr));
+ }
+ }
+ return versions;
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ } finally {
+ if (tri != null)
+ {
+ tri.close();
+ }
+ }
+
+ }
+
+
+ public VersionImpl create(Context context) {
+ try {
+ TableRow row = DatabaseManager.create(context, TABLE_NAME);
+ VersionImpl v = new VersionImpl(context, row);
+
+ //TODO Do I have to manage the event?
+ //context.addEvent(new Event(Event.CREATE, Constants.VERSION, e.getID(), null));
+
+ return v;
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+
+ public void delete(Context c, int versionID) {
+ try {
+ //TODO Do I have to manage the event?
+ //context.addEvent(new Event(Event.DELETE, Constants.VERSION, getID(), getEmail()));
+
+ // Remove ourself
+ VersionImpl version = find(c, versionID);
+ if(version!=null){
+ //Remove ourself from our cache first !
+ c.removeCached(version, version.getVersionId());
+
+ DatabaseManager.delete(c, version.getMyRow());
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+
+ public void update(VersionImpl version) {
+ try {
+ //TODO Do I have to manage the event?
+ DatabaseManager.update(version.getMyContext(), version.getMyRow());
+
+
+// if (modified)
+// {
+// myContext.addEvent(new Event(Event.MODIFY, Constants.EPERSON, getID(), null));
+// modified = false;
+// }
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionHistory.java b/dspace-api/src/main/java/org/dspace/versioning/VersionHistory.java
new file mode 100644
index 0000000000..3fbf879f92
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersionHistory.java
@@ -0,0 +1,36 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import java.util.List;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public interface VersionHistory {
+
+ public Version getLatestVersion();
+ public Version getFirstVersion();
+ public List getVersions();
+ public int getVersionHistoryId();
+ public Version getPrevious(Version version);
+ public Version getNext(Version version);
+ public boolean hasNext(Version version);
+ public void add(Version version);
+ public Version getVersion(org.dspace.content.Item item);
+ public boolean hasNext(org.dspace.content.Item item);
+ public boolean isFirstVersion(Version version);
+ public boolean isLastVersion(Version version);
+ public void remove(Version version);
+ public boolean isEmpty();
+ public int size();
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryDAO.java b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryDAO.java
new file mode 100644
index 0000000000..11dbff79c3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryDAO.java
@@ -0,0 +1,115 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.core.Context;
+import org.dspace.storage.rdbms.DatabaseManager;
+import org.dspace.storage.rdbms.TableRow;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersionHistoryDAO
+{
+
+ protected final static String TABLE_NAME="versionhistory";
+ protected final static String VERSION_HISTORY_ID = "versionhistory_id";
+
+
+ public VersionHistoryImpl create(Context context)
+ {
+ try {
+ TableRow row = DatabaseManager.create(context, TABLE_NAME);
+ VersionHistoryImpl vh = new VersionHistoryImpl(context, row);
+
+ //TODO Do I have to manage the event?
+ //context.addEvent(new Event(Event.CREATE, Constants.EPERSON, e.getID(), null));
+
+ return vh;
+
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+
+
+ public VersionHistoryImpl find(Context context, int itemID, VersionDAO versionDAO)
+ {
+ try {
+
+ Version version = versionDAO.findByItemId(context, itemID);
+
+ if(version==null)
+ {
+ return null;
+ }
+
+ VersionHistoryImpl fromCache = (VersionHistoryImpl) context.fromCache(VersionHistoryImpl.class, version.getVersionHistoryID());
+ if (fromCache != null)
+ {
+ return fromCache;
+ }
+
+ TableRow row = DatabaseManager.find(context, TABLE_NAME, version.getVersionHistoryID());
+
+ VersionHistoryImpl vh = new VersionHistoryImpl(context, row);
+ List versions= versionDAO.findByVersionHistory(context, vh.getVersionHistoryId());
+ vh.setVersions(versions);
+ return vh;
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+
+ }
+
+ public VersionHistoryImpl findById(Context context, int id, VersionDAO versionDAO)
+ {
+
+ try {
+ TableRow row = DatabaseManager.find(context, TABLE_NAME, id);
+
+ if (row == null) return null;
+
+ VersionHistoryImpl fromCache = (VersionHistoryImpl) context.fromCache(VersionHistoryImpl.class, row.getIntColumn(VERSION_HISTORY_ID));
+
+ if (fromCache != null)
+ {
+ return fromCache;
+ }
+
+ VersionHistoryImpl versionHistoryImpl = new VersionHistoryImpl(context, row);
+
+ List versions= versionDAO.findByVersionHistory(context, versionHistoryImpl.getVersionHistoryId());
+ versionHistoryImpl.setVersions(versions);
+ return versionHistoryImpl;
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+
+ public void delete(Context c, int versionHistoryID, VersionDAO versionDAO)
+ {
+ try {
+ VersionHistoryImpl history = findById(c, versionHistoryID, versionDAO);
+ DatabaseManager.delete(c, history.getMyRow());
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryImpl.java
new file mode 100644
index 0000000000..5bcd0a13dc
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryImpl.java
@@ -0,0 +1,192 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.storage.rdbms.TableRow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersionHistoryImpl implements VersionHistory
+{
+
+ private int versionHistoryId;
+ private Listversions;
+
+ private Context myContext;
+ private TableRow myRow;
+
+
+ protected VersionHistoryImpl(VersionHistoryDAO vhDAO){
+
+ }
+
+
+ protected VersionHistoryImpl(Context c, TableRow row)
+ {
+ myContext = c;
+ myRow = row;
+
+ c.cache(this, row.getIntColumn(VersionHistoryDAO.VERSION_HISTORY_ID));
+ }
+
+ public int getVersionHistoryId() {
+ return myRow.getIntColumn(VersionHistoryDAO.VERSION_HISTORY_ID);
+ }
+
+ // LIST order: descending
+ public Version getPrevious(Version version) {
+ int index = versions.indexOf(version);
+
+ if( (index+1)==versions.size()) return null;
+
+ return versions.get(index+1);
+ }
+
+ // LIST order: descending
+ public Version getNext(Version version)
+ {
+
+ int index = versions.indexOf(version);
+
+ if(index==0)
+ {
+ return null;
+ }
+
+ return versions.get(index-1);
+ }
+
+ public Version getVersion(Item item) {
+ for(Version v : versions)
+ {
+ if(v.getItem().getID()==item.getID())
+ {
+ return v;
+ }
+ }
+ return null;
+ }
+
+ public boolean hasNext(Item item)
+ {
+ Version version = getVersion(item);
+ return hasNext(version);
+ }
+
+ public boolean hasNext(Version version)
+ {
+ return getNext(version)!=null;
+ }
+
+ public List getVersions()
+ {
+ return versions;
+ }
+
+ public void setVersions(List versions)
+ {
+ this.versions = versions;
+ }
+
+ public void add(Version version)
+ {
+ if(versions==null) versions=new ArrayList();
+ versions.add(0, version);
+ }
+
+ public Version getLatestVersion()
+ {
+ if(versions==null || versions.size()==0)
+ {
+ return null;
+ }
+
+ return versions.get(0);
+ }
+
+ public Version getFirstVersion()
+ {
+ if(versions==null || versions.size()==0)
+ {
+ return null;
+ }
+
+ return versions.get(versions.size()-1);
+ }
+
+
+ public boolean isFirstVersion(Version version)
+ {
+ Version first = versions.get(versions.size()-1);
+ return first.equals(version);
+ }
+
+ public boolean isLastVersion(Version version)
+ {
+ Version last = versions.get(0);
+ return last.equals(version);
+ }
+
+ public void remove(Version version)
+ {
+ versions.remove(version);
+ }
+
+ public boolean isEmpty()
+ {
+ return versions.size()==0;
+ }
+
+ public int size()
+ {
+ return versions.size();
+ }
+
+ protected TableRow getMyRow()
+ {
+ return myRow;
+ }
+
+
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+
+ VersionHistoryImpl that = (VersionHistoryImpl) o;
+ return versionHistoryId == that.versionHistoryId;
+
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int hash=7;
+ hash=79*hash+ (this.getVersionHistoryId() ^ (this.getVersionHistoryId() >>> 32));
+ return hash;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersionImpl.java
new file mode 100644
index 0000000000..b66765ab1c
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersionImpl.java
@@ -0,0 +1,168 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+import org.dspace.storage.rdbms.TableRow;
+
+import java.sql.SQLException;
+import java.util.Date;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersionImpl implements Version {
+
+ private int versionId;
+ private EPerson eperson;
+ private int itemID=-1;
+
+ private Date versionDate;
+ private int versionNumber;
+ private String summary;
+ private int versionHistoryID;
+ private Context myContext;
+ private TableRow myRow;
+
+ protected VersionImpl(Context c, TableRow row)
+ {
+ myContext = c;
+ myRow = row;
+
+ c.cache(this, row.getIntColumn(VersionDAO.VERSION_ID));
+ }
+
+
+ public int getVersionId()
+ {
+ return myRow.getIntColumn(VersionDAO.VERSION_ID);
+ }
+
+ protected void setVersionId(int versionId)
+ {
+ this.versionId = versionId;
+ }
+
+ public EPerson getEperson(){
+ try {
+ if (eperson == null)
+ {
+ return EPerson.find(myContext, myRow.getIntColumn(VersionDAO.EPERSON_ID));
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ return eperson;
+ }
+
+ public void setEperson(EPerson ePerson) {
+ this.eperson = ePerson;
+ myRow.setColumn(VersionDAO.EPERSON_ID, ePerson.getID());
+ }
+
+ public int getItemID() {
+ return myRow.getIntColumn(VersionDAO.ITEM_ID);
+ }
+
+
+ public Item getItem(){
+ try{
+ if(getItemID()==-1)
+ {
+ return null;
+ }
+
+ return Item.find(myContext, getItemID());
+
+ }catch(SQLException e){
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ public void setItemID(int itemID)
+ {
+ this.itemID = itemID;
+ myRow.setColumn(VersionDAO.ITEM_ID, itemID);
+ }
+
+ public Date getVersionDate() {
+ return myRow.getDateColumn(VersionDAO.VERSION_DATE);
+ }
+
+ public void setVersionDate(Date versionDate) {
+ this.versionDate = versionDate;
+ myRow.setColumn(VersionDAO.VERSION_DATE, versionDate);
+ }
+
+ public int getVersionNumber() {
+ return myRow.getIntColumn(VersionDAO.VERSION_NUMBER);
+ }
+
+ public void setVersionNumber(int versionNumber) {
+ this.versionNumber = versionNumber;
+ myRow.setColumn(VersionDAO.VERSION_NUMBER, versionNumber);
+ }
+
+ public String getSummary() {
+ return myRow.getStringColumn(VersionDAO.VERSION_SUMMARY);
+ }
+
+ public void setSummary(String summary) {
+ this.summary = summary;
+ myRow.setColumn(VersionDAO.VERSION_SUMMARY, summary);
+ }
+
+
+ public int getVersionHistoryID() {
+ return myRow.getIntColumn(VersionDAO.HISTORY_ID);
+ }
+
+ public void setVersionHistory(int versionHistoryID) {
+ this.versionHistoryID = versionHistoryID;
+ myRow.setColumn(VersionDAO.HISTORY_ID, versionHistoryID);
+ }
+
+
+ public Context getMyContext(){
+ return myContext;
+ }
+
+ protected TableRow getMyRow(){
+ return myRow;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+
+ VersionImpl version = (VersionImpl) o;
+
+ return getVersionId() == version.getVersionId();
+
+ }
+
+ @Override
+ public int hashCode() {
+ int hash=7;
+ hash=79*hash+(int) (this.getVersionId() ^ (this.getVersionId() >>> 32));
+ return hash;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java
new file mode 100644
index 0000000000..a4b4714117
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningConsumer.java
@@ -0,0 +1,86 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.event.Consumer;
+import org.dspace.event.Event;
+import org.dspace.utils.DSpace;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersioningConsumer implements Consumer {
+
+ private static Set- itemsToProcess;
+
+ public void initialize() throws Exception {}
+
+ public void finish(Context ctx) throws Exception {}
+
+ public void consume(Context ctx, Event event) throws Exception {
+ if(itemsToProcess == null){
+ itemsToProcess = new HashSet
- ();
+ }
+
+ int st = event.getSubjectType();
+ int et = event.getEventType();
+
+ if(st == Constants.ITEM && et == Event.INSTALL){
+ Item item = (Item) event.getSubject(ctx);
+ if (item != null && item.isArchived()) {
+ VersionHistory history = retrieveVersionHistory(ctx, item);
+ if (history != null) {
+ Version latest = history.getLatestVersion();
+ Version previous = history.getPrevious(latest);
+ if(previous != null){
+ Item previousItem = previous.getItem();
+ if(previousItem != null){
+ itemsToProcess.add(previousItem);
+ //Fire a new modify event for our previous item
+ //Due to the need to reindex the item in the search & browse index we need to fire a new event
+ ctx.addEvent(new Event(Event.MODIFY, previousItem.getType(), previousItem.getID(), null));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void end(Context ctx) throws Exception {
+ if(itemsToProcess != null){
+ for(Item item : itemsToProcess){
+ ctx.turnOffAuthorisationSystem();
+ try {
+ item.setArchived(false);
+ item.update();
+ } finally {
+ ctx.restoreAuthSystemState();
+ }
+ }
+ ctx.getDBConnection().commit();
+ }
+
+ itemsToProcess = null;
+ }
+
+
+ private static org.dspace.versioning.VersionHistory retrieveVersionHistory(Context c, Item item) {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ return versioningService.findVersionHistory(c, item.getID());
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningService.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningService.java
new file mode 100644
index 0000000000..8719a24456
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningService.java
@@ -0,0 +1,41 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public interface VersioningService {
+
+ Version createNewVersion(Context c, int itemId);
+
+ Version createNewVersion(Context c, int itemId, String summary);
+
+ void removeVersion(Context c, int versionID);
+
+ void removeVersion(Context c, Item item);
+
+ Version getVersion(Context c, int versionID);
+
+ Version restoreVersion(Context c, int versionID);
+
+ Version restoreVersion(Context c, int versionID, String summary);
+
+ VersionHistory findVersionHistory(Context c, int itemId);
+
+ Version updateVersion(Context c, int itemId, String summary);
+
+ Version getVersion(Context c, Item item);
+}
diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java
new file mode 100644
index 0000000000..af2ecb896e
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java
@@ -0,0 +1,188 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.versioning;
+
+import org.dspace.content.Collection;
+import org.dspace.content.DCDate;
+import org.dspace.content.DCValue;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.springframework.beans.factory.annotation.Required;
+
+import java.sql.SQLException;
+import java.util.Date;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersioningServiceImpl implements VersioningService{
+
+ private VersionHistoryDAO versionHistoryDAO;
+ private VersionDAO versionDAO;
+ private DefaultItemVersionProvider provider;
+
+
+ /** Service Methods */
+ public Version createNewVersion(Context c, int itemId){
+ return createNewVersion(c, itemId, null);
+ }
+
+ public Version createNewVersion(Context c, int itemId, String summary) {
+ try{
+ Item item = Item.find(c, itemId);
+ VersionHistory vh = versionHistoryDAO.find(c, item.getID(), versionDAO);
+ if(vh==null)
+ {
+ // first time: create 2 versions, .1(old version) and .2(new version)
+ vh=versionHistoryDAO.create(c);
+
+ // get dc:date.accessioned to be set as first version date...
+ DCValue[] values = item.getMetadata("dc", "date", "accessioned", Item.ANY);
+ Date versionDate = new Date();
+ if(values!=null && values.length > 0){
+ String date = values[0].value;
+ versionDate = new DCDate(date).toDate();
+ }
+ createVersion(c, vh, item, "", versionDate);
+ }
+ // Create new Item
+ Item itemNew = provider.createNewItemAndAddItInWorkspace(c, item);
+
+ // create new version
+ Version version = createVersion(c, vh, itemNew, summary, new Date());
+
+ // Complete any update of the Item and new Identifier generation that needs to happen
+ provider.updateItemState(c, itemNew, item);
+
+ return version;
+ }catch (Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ public void removeVersion(Context c, int versionID) {
+ Version version = versionDAO.find(c, versionID);
+ if(version!=null){
+ removeVersion(c, version);
+ }
+ }
+
+ public void removeVersion(Context c, Item item) {
+ Version version = versionDAO.findByItem(c, item);
+ if(version!=null){
+ removeVersion(c, version);
+ }
+ }
+
+ protected void removeVersion(Context c, Version version) {
+ try{
+ VersionHistory history = versionHistoryDAO.findById(c, version.getVersionHistoryID(), versionDAO);
+ provider.deleteVersionedItem(c, version, history);
+ versionDAO.delete(c, version.getVersionId());
+
+ history.remove(version);
+
+ if(history.isEmpty()){
+ versionHistoryDAO.delete(c, version.getVersionHistoryID(), versionDAO);
+ }
+ //Delete the item linked to the version
+ Item item = version.getItem();
+ Collection[] collections = item.getCollections();
+
+ // Remove item from all the collections it's in (so our item is also deleted)
+ for (Collection collection : collections)
+ {
+ collection.removeItem(item);
+ }
+ }catch (Exception e) {
+ c.abort();
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ public Version getVersion(Context c, int versionID) {
+ return versionDAO.find(c, versionID);
+ }
+
+
+ public Version restoreVersion(Context c, int versionID){
+ return restoreVersion(c, versionID, null);
+ }
+
+ public Version restoreVersion(Context c, int versionID, String summary) {
+ return null;
+ }
+
+ public VersionHistory findVersionHistory(Context c, int itemId){
+ return versionHistoryDAO.find(c, itemId, versionDAO);
+ }
+
+ public Version updateVersion(Context c, int itemId, String summary) {
+ VersionImpl version = versionDAO.findByItemId(c, itemId);
+ version.setSummary(summary);
+ versionDAO.update(version);
+ return version;
+ }
+
+ public Version getVersion(Context c, Item item){
+ return versionDAO.findByItemId(c, item.getID());
+ }
+
+// **** PROTECTED METHODS!!
+
+ protected VersionImpl createVersion(Context c, VersionHistory vh, Item item, String summary, Date date) {
+ try {
+ VersionImpl version = versionDAO.create(c);
+
+ version.setVersionNumber(getNextVersionNumer(vh.getLatestVersion()));
+ version.setVersionDate(date);
+ version.setEperson(item.getSubmitter());
+ version.setItemID(item.getID());
+ version.setSummary(summary);
+ version.setVersionHistory(vh.getVersionHistoryId());
+ versionDAO.update(version);
+ vh.add(version);
+ return version;
+ } catch (SQLException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ protected int getNextVersionNumer(Version latest){
+ if(latest==null) return 1;
+
+ return latest.getVersionNumber()+1;
+ }
+
+
+
+ public VersionHistoryDAO getVersionHistoryDAO() {
+ return versionHistoryDAO;
+ }
+
+ public void setVersionHistoryDAO(VersionHistoryDAO versionHistoryDAO) {
+ this.versionHistoryDAO = versionHistoryDAO;
+ }
+
+ public VersionDAO getVersionDAO() {
+ return versionDAO;
+ }
+
+ public void setVersionDAO(VersionDAO versionDAO) {
+ this.versionDAO = versionDAO;
+ }
+
+ @Required
+ public void setProvider(DefaultItemVersionProvider provider) {
+ this.provider = provider;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/workflow/WorkflowItem.java b/dspace-api/src/main/java/org/dspace/workflow/WorkflowItem.java
index cd04744bce..f348dc4e10 100644
--- a/dspace-api/src/main/java/org/dspace/workflow/WorkflowItem.java
+++ b/dspace-api/src/main/java/org/dspace/workflow/WorkflowItem.java
@@ -355,7 +355,7 @@ public class WorkflowItem implements InProgressSubmission
/**
* Update the workflow item, including the unarchived item.
*/
- public void update() throws SQLException, IOException, AuthorizeException
+ public void update() throws SQLException, AuthorizeException
{
// FIXME check auth
log.info(LogManager.getHeader(ourContext, "update_workflow_item",
diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/XmlWorkflowItem.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/XmlWorkflowItem.java
index 40ec28566f..7bd4231c2d 100644
--- a/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/XmlWorkflowItem.java
+++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/storedcomponents/XmlWorkflowItem.java
@@ -63,7 +63,7 @@ public class XmlWorkflowItem implements InProgressSubmission {
*/
// private ArrayList activeSteps;
- XmlWorkflowItem(Context context, TableRow row) throws SQLException {
+ XmlWorkflowItem(Context context, TableRow row) throws SQLException, AuthorizeException, IOException {
ourContext = context;
wfRow = row;
// activeSteps = new ArrayList();
@@ -360,35 +360,36 @@ public class XmlWorkflowItem implements InProgressSubmission {
/**
* Check to see if a particular item is currently under Workflow.
- * If so, its XmlWorkflowItem is returned. If not, null is returned
+ * If so, its WorkflowItem is returned. If not, null is returned
*
* @param context
* the context object
- * @param i
+ * @param item
* the item
*
- * @return XmlWorkflow item corresponding to the item, or null
+ * @return workflow item corresponding to the item, or null
*/
- public static XmlWorkflowItem findByItem(Context context, Item i)
- throws SQLException
- {
- // Look for the unique workflowitem entry where 'item_id' references this item
- TableRow row = DatabaseManager.findByUnique(context, "cwf_workflowitem", "item_id", i.getID());
+ public static XmlWorkflowItem findByItem(Context context, Item item) throws SQLException {
+ TableRow row = DatabaseManager.findByUnique(context, "cwf_workflowitem", "item_id", item.getID());
- if (row == null)
- {
- return null;
- }
- else
- {
- return new XmlWorkflowItem(context, row);
+ XmlWorkflowItem wi = null;
+ if(row != null){
+ // Check the cache
+ wi = (XmlWorkflowItem) context.fromCache(XmlWorkflowItem.class, row.getIntColumn("workflowitem_id"));
+
+ // not in cache? turn row into workflowitem
+ if (wi == null)
+ {
+ wi = new XmlWorkflowItem(context, row);
+ }
}
+ return wi;
}
/**
* Update the workflow item, including the unarchived item.
*/
- public void update() throws SQLException, IOException, AuthorizeException {
+ public void update() throws SQLException, AuthorizeException {
// FIXME check auth
log.info(LogManager.getHeader(ourContext, "update_workflow_item",
"workflowitem_id=" + getID()));
diff --git a/dspace-api/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-core-services.xml
new file mode 100644
index 0000000000..1e0fa082f1
--- /dev/null
+++ b/dspace-api/src/main/resources/spring/spring-dspace-core-services.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java b/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java
index caf32fdfb1..d53abfcad5 100644
--- a/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java
+++ b/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java
@@ -10,11 +10,8 @@ package org.dspace;
import static org.junit.Assert.fail;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
-import java.nio.channels.FileChannel;
import java.sql.SQLException;
import java.util.Properties;
import java.util.TimeZone;
@@ -38,6 +35,8 @@ import org.dspace.core.ConfigurationManager;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.search.DSIndexer;
+import org.dspace.servicemanager.DSpaceKernelImpl;
+import org.dspace.servicemanager.DSpaceKernelInit;
import org.dspace.storage.rdbms.MockDatabaseManager;
import org.junit.After;
import org.junit.AfterClass;
@@ -78,6 +77,7 @@ public class AbstractUnitTest
*/
protected static EPerson eperson;
+ protected static DSpaceKernelImpl kernelImpl;
/**
* This method will be run before the first test as per @BeforeClass. It will
@@ -111,6 +111,13 @@ public class AbstractUnitTest
//load the test configuration file
ConfigurationManager.loadConfig(null);
+ // Initialise the service manager kernel
+ kernelImpl = DSpaceKernelInit.getKernel(null);
+ if (!kernelImpl.isRunning())
+ {
+ kernelImpl.start(ConfigurationManager.getProperty("dspace.dir"));
+ }
+
// Load the default registries. This assumes the temporary
// filesystem is working and the in-memory DB in place.
Context ctx = new Context();
diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java
new file mode 100644
index 0000000000..45c4cd1bb6
--- /dev/null
+++ b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java
@@ -0,0 +1,139 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.content;
+
+import org.apache.log4j.Logger;
+import org.dspace.AbstractUnitTest;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.handle.HandleManager;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.Version;
+import org.dspace.versioning.VersionHistory;
+import org.dspace.versioning.VersioningService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.sql.SQLException;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+
+/**
+ * Unit Tests for versioning
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersioningTest extends AbstractUnitTest {
+
+ private static final Logger log = Logger.getLogger(VersioningTest.class);
+
+ private Item originalItem;
+ private Item versionedItem;
+ private String summary = "Unit test version";
+ private DSpace dspace = new DSpace();
+ private VersioningService versioningService = dspace.getSingletonService(VersioningService.class);
+
+
+ /**
+ * This method will be run before every test as per @Before. It will
+ * initialize resources required for the tests.
+ *
+ * Other methods can be annotated with @Before here or in subclasses
+ * but no execution order is guaranteed
+ */
+ @Before
+ @Override
+ public void init()
+ {
+ super.init();
+ try
+ {
+ context.turnOffAuthorisationSystem();
+ Collection col = Collection.create(context);
+ WorkspaceItem is = WorkspaceItem.create(context, col, false);
+
+ originalItem = InstallItem.installItem(context, is);
+
+ Version version = versioningService.createNewVersion(context, originalItem.getID(), summary);
+ WorkspaceItem wsi = WorkspaceItem.findByItem(context, version.getItem());
+
+ versionedItem = InstallItem.installItem(context, wsi);
+ context.restoreAuthSystemState();
+ context.commit();
+ }
+ catch (AuthorizeException ex)
+ {
+ log.error("Authorization Error in init", ex);
+ fail("Authorization Error in init");
+ }
+ catch (SQLException ex)
+ {
+ log.error("SQL Error in init", ex);
+ fail("SQL Error in init");
+ } catch (IOException ex) {
+ log.error("IO Error in init", ex);
+ fail("IO Error in init");
+ }
+
+ }
+
+ /**
+ * This method will be run after every test as per @After. It will
+ * clean resources initialized by the @Before methods.
+ *
+ * Other methods can be annotated with @After here or in subclasses
+ * but no execution order is guaranteed
+ */
+ @After
+ @Override
+ public void destroy()
+ {
+ super.destroy();
+ }
+
+
+ @Test
+ public void testVersionFind(){
+ VersionHistory versionHistory = versioningService.findVersionHistory(context, originalItem.getID());
+ assertThat("testFindVersionHistory", versionHistory, notNullValue());
+ Version version = versionHistory.getVersion(versionedItem);
+ assertThat("testFindVersion", version, notNullValue());
+ }
+
+ /**
+ * Test of installItem method, of class InstallItem.
+ */
+ @Test
+ public void testVersionSummary() throws Exception
+ {
+ //Start by creating a new item !
+ VersionHistory versionHistory = versioningService.findVersionHistory(context, originalItem.getID());
+ Version version = versionHistory.getVersion(versionedItem);
+ assertThat("Test_version_summary", summary, equalTo(version.getSummary()));
+ }
+
+ @Test
+ public void testVersionHandle() throws Exception {
+ assertThat("Test_version_handle", versionedItem.getHandle(), notNullValue());
+ }
+
+ @Test
+ public void testVersionDelete() throws Exception {
+ context.turnOffAuthorisationSystem();
+ versioningService.removeVersion(context, versionedItem);
+ assertThat("Test_version_delete", Item.find(context, versionedItem.getID()), nullValue());
+ assertThat("Test_version_handle_delete", HandleManager.resolveToObject(context, versionedItem.getHandle()), nullValue());
+ context.restoreAuthSystemState();
+ }
+}
diff --git a/dspace-api/src/test/resources/database_schema.sql b/dspace-api/src/test/resources/database_schema.sql
index 70babe41ca..b959e219c8 100644
--- a/dspace-api/src/test/resources/database_schema.sql
+++ b/dspace-api/src/test/resources/database_schema.sql
@@ -116,6 +116,8 @@ CREATE SEQUENCE group2group_seq;
CREATE SEQUENCE group2groupcache_seq;
CREATE SEQUENCE harvested_collection_seq;
CREATE SEQUENCE harvested_item_seq;
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
-------------------------------------------------------
-- BitstreamFormatRegistry table
@@ -787,6 +789,24 @@ CREATE INDEX harvested_item_fk_idx ON harvested_item(item_id);
+CREATE TABLE versionhistory
+(
+ versionhistory_id INTEGER NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE versionitem
+(
+ versionitem_id INTEGER NOT NULL PRIMARY KEY,
+ item_id INTEGER REFERENCES Item(item_id),
+ version_number INTEGER,
+ eperson_id INTEGER REFERENCES EPerson(eperson_id),
+ version_date TIMESTAMP,
+ version_summary VARCHAR(255),
+ versionhistory_id INTEGER REFERENCES VersionHistory(versionhistory_id)
+);
+
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
diff --git a/dspace-xmlui/dspace-xmlui-api/pom.xml b/dspace-xmlui/dspace-xmlui-api/pom.xml
index 243e9e7c08..419a9c055b 100644
--- a/dspace-xmlui/dspace-xmlui-api/pom.xml
+++ b/dspace-xmlui/dspace-xmlui-api/pom.xml
@@ -82,6 +82,13 @@
gson
2.2.1
+
+
+ org.springframework
+ spring-webmvc
+ 3.0.5.RELEASE
+
+
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/DeleteVersionsConfirm.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/DeleteVersionsConfirm.java
new file mode 100644
index 0000000000..77caa21f1b
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/DeleteVersionsConfirm.java
@@ -0,0 +1,144 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.apache.commons.lang.StringUtils;
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.*;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.DCDate;
+import org.dspace.content.DCValue;
+import org.dspace.content.MetadataSchema;
+import org.dspace.eperson.EPerson;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.Version;
+import org.dspace.versioning.VersioningService;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class DeleteVersionsConfirm extends AbstractDSpaceTransformer {
+
+ /** Language strings */
+ private static final Message T_dspace_home = message("xmlui.general.dspace_home");
+
+ private static final Message T_title = message("xmlui.aspect.versioning.DeleteVersionsConfirm.title");
+ private static final Message T_trail = message("xmlui.aspect.versioning.DeleteVersionsConfirm.trail");
+ private static final Message T_head1 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.head1");
+ private static final Message T_para1 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.para1");
+ private static final Message T_para2 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.para2");
+ private static final Message T_column1 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.column1");
+ private static final Message T_column2 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.column2");
+ private static final Message T_column3 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.column3");
+ private static final Message T_column4 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.column4");
+ private static final Message T_column5 = message("xmlui.aspect.versioning.DeleteVersionsConfirm.column5");
+
+
+ private static final Message T_submit_delete = message("xmlui.general.delete");
+ private static final Message T_submit_cancel = message("xmlui.general.cancel");
+
+
+ public void addPageMeta(PageMeta pageMeta) throws WingException {
+ pageMeta.addMetadata("title").addContent(T_title);
+
+ pageMeta.addTrailLink(contextPath + "/", T_dspace_home);
+ //pageMeta.addTrailLink(contextPath + "/admin/item",T_item_trail);
+ pageMeta.addTrail().addContent(T_trail);
+ }
+
+ public void addBody(Body body) throws WingException, AuthorizeException {
+ Division main = createMainDivision(body);
+
+ createTable(main);
+
+ addButtons(main);
+
+ main.addHidden("versioning-continue").setValue(knot.getId());
+ }
+
+
+ private Division createMainDivision(Body body) throws WingException {
+ Division main = body.addInteractiveDivision("versions-confirm-delete", contextPath+"/item/versionhistory", Division.METHOD_POST, "delete version");
+ main.setHead(T_head1);
+ Para helpPara = main.addPara();
+ helpPara.addContent(T_para1);
+ helpPara.addHighlight("bold").addContent(T_para2);
+ return main;
+ }
+
+
+ private void createTable(Division main) throws WingException {
+ // Get all our parameters
+ String idsString = parameters.getParameter("versionIDs", null);
+
+ Table table = main.addTable("versions-confirm-delete", 1, 1);
+
+ Row header = table.addRow(Row.ROLE_HEADER);
+ header.addCellContent(T_column1);
+ header.addCellContent(T_column2);
+ header.addCellContent(T_column3);
+ header.addCellContent(T_column4);
+ header.addCellContent(T_column5);
+
+
+ for (String id : idsString.split(","))
+ {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ Version version = null;
+
+ if(StringUtils.isNotBlank(id))
+ {
+ version = versioningService.getVersion(context, Integer.parseInt(id));
+ }
+
+ if(version!=null)
+ {
+ Row row = table.addRow();
+ row.addCell().addContent(version.getVersionNumber());
+ addItemIdentifier(row.addCell(), version.getItem());
+
+ EPerson editor = version.getEperson();
+ row.addCell().addXref("mailto:" + editor.getEmail(), editor.getFullName());
+ row.addCell().addContent(new DCDate(version.getVersionDate()).toString());
+ row.addCell().addContent(version.getSummary());
+ }
+ }
+
+ }
+
+ private void addButtons(Division main) throws WingException {
+ Para buttons = main.addPara();
+ buttons.addButton("submit_confirm").setValue(T_submit_delete);
+ buttons.addButton("submit_cancel").setValue(T_submit_cancel);
+ }
+
+ private void addItemIdentifier(Cell cell, org.dspace.content.Item item) throws WingException {
+ String itemHandle = item.getHandle();
+
+ DCValue[] identifiers = item.getMetadata(MetadataSchema.DC_SCHEMA, "identifier", null, org.dspace.content.Item.ANY);
+ String itemIdentifier=null;
+ if(identifiers!=null && identifiers.length > 0)
+ {
+ itemIdentifier = identifiers[0].value;
+ }
+
+ if(itemIdentifier!=null)
+ {
+ cell.addXref(contextPath + "/resource/" + itemIdentifier, itemIdentifier);
+ }else{
+ cell.addXref(contextPath + "/handle/" + itemHandle, itemHandle);
+ }
+ }
+}
+
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/Navigation.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/Navigation.java
new file mode 100644
index 0000000000..c25ac399ac
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/Navigation.java
@@ -0,0 +1,220 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.apache.cocoon.caching.CacheableProcessingComponent;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.util.HashUtil;
+import org.apache.excalibur.source.SourceValidity;
+import org.apache.excalibur.source.impl.validity.NOPValidity;
+import org.dspace.app.util.Util;
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.utils.DSpaceValidity;
+import org.dspace.app.xmlui.utils.HandleUtil;
+import org.dspace.app.xmlui.utils.UIException;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.List;
+import org.dspace.app.xmlui.wing.element.Options;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.AuthorizeManager;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.core.Constants;
+import org.dspace.eperson.Group;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.VersioningService;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.sql.SQLException;
+
+
+/**
+ *
+ * Navigation for Versioning of Items.
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class Navigation extends AbstractDSpaceTransformer implements CacheableProcessingComponent
+{
+ private static final Message T_context_head = message("xmlui.administrative.Navigation.context_head");
+ private static final Message T_context_create_version= message("xmlui.aspect.versioning.VersioningNavigation.context_create_version");
+ private static final Message T_context_show_version_history= message("xmlui.aspect.versioning.VersioningNavigation.context_show_version_history");
+
+ /** Cached validity object */
+ private SourceValidity validity;
+
+ /**
+ * Generate the unique cache key.
+ *
+ * @return The generated key hashes the src
+ */
+ public Serializable getKey()
+ {
+ Request request = ObjectModelHelper.getRequest(objectModel);
+
+ // Special case, don't cache anything if the user is logging
+ // in. The problem occures because of timming, this cache key
+ // is generated before we know whether the operation has
+ // succeded or failed. So we don't know whether to cache this
+ // under the user's specific cache or under the anonymous user.
+ if (request.getParameter("login_email") != null ||
+ request.getParameter("login_password") != null ||
+ request.getParameter("login_realm") != null )
+ {
+ return "0";
+ }
+
+ String key;
+ if (context.getCurrentUser() != null)
+ {
+ key = context.getCurrentUser().getEmail();
+ }
+ else
+ key = "anonymous";
+
+ return HashUtil.hash(key);
+ }
+
+ /**
+ * Generate the validity object.
+ *
+ * @return The generated validity object or
null
if the
+ * component is currently not cacheable.
+ */
+ public SourceValidity getValidity()
+ {
+ if (this.validity == null)
+ {
+ // Only use the DSpaceValidity object is someone is logged in.
+ if (context.getCurrentUser() != null)
+ {
+ try {
+ DSpaceValidity validity = new DSpaceValidity();
+
+ validity.add(eperson);
+
+ Group[] groups = Group.allMemberGroups(context, eperson);
+ for (Group group : groups)
+ {
+ validity.add(group);
+ }
+
+ DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
+ if(dso != null)
+ {
+ validity.add(dso);
+ }
+
+ this.validity = validity.complete();
+ }
+ catch (SQLException sqle)
+ {
+ // Just ignore it and return invalid.
+ }
+ }
+ else
+ {
+ this.validity = NOPValidity.SHARED_INSTANCE;
+ }
+ }
+ return this.validity;
+ }
+
+ public void addOptions(Options options) throws SAXException, WingException,
+ UIException, SQLException, IOException, AuthorizeException {
+ /* Create skeleton menu structure to ensure consistent order between aspects,
+ * even if they are never used
+ */
+ options.addList("browse");
+ options.addList("account");
+
+ List context = options.addList("context");
+
+
+ // Context Administrative options for Versioning
+ DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
+
+
+ if(dso==null)
+ {
+ // case: internal-item http://localhost:8100/internal-item?itemID=3085
+ // case: admin http://localhost:8100/admin/item?itemID=3340
+ // retrieve the object from the DB
+ dso = getItemById();
+ }
+
+ if (dso != null && dso.getType() == Constants.ITEM)
+ {
+ Item item = (Item) dso;
+ if(AuthorizeManager.isAdmin(this.context, item.getOwningCollection()))
+ {
+ boolean headAdded=false;
+ if(isLatest(item) && item.isArchived())
+ {
+ context.setHead(T_context_head);
+ headAdded=true;
+ context.addItem().addXref(contextPath+"/item/version?itemID="+item.getID(), T_context_create_version);
+ }
+
+ if(hasVersionHistory(item))
+ {
+ if(!headAdded)
+ {
+ context.setHead(T_context_head);
+ }
+ context.addItem().addXref(contextPath+"/item/versionhistory?itemID="+item.getID(), T_context_show_version_history);
+ }
+ }
+ }
+ }
+
+
+ private Item getItemById() throws SQLException
+ {
+ Request request = ObjectModelHelper.getRequest(objectModel);
+ Item item = null;
+ int itemId = Util.getIntParameter(request, "itemID");
+ if (itemId != -1)
+ {
+ item = Item.find(this.context, itemId);
+ }
+ return item;
+ }
+
+ /**
+ * recycle
+ */
+ public void recycle()
+ {
+ this.validity = null;
+ super.recycle();
+ }
+
+ private boolean isLatest(Item item)
+ {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ org.dspace.versioning.VersionHistory history = versioningService.findVersionHistory(context, item.getID());
+ return (history==null || history.getLatestVersion().getItem().getID() == item.getID());
+ }
+
+
+ private boolean hasVersionHistory(Item item)
+ {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ org.dspace.versioning.VersionHistory history = versioningService.findVersionHistory(context, item.getID());
+ return (history!=null);
+ }
+
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/RestoreVersionForm.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/RestoreVersionForm.java
new file mode 100644
index 0000000000..545f023561
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/RestoreVersionForm.java
@@ -0,0 +1,109 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.*;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.DCDate;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.Version;
+import org.dspace.versioning.VersioningService;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class RestoreVersionForm extends AbstractDSpaceTransformer
+{
+ /** Language strings */
+ private static final Message T_dspace_home = message("xmlui.general.dspace_home");
+ private static final Message T_item_trail = message("xmlui.administrative.item.general.item_trail");
+
+ private static final Message T_title = message("xmlui.aspect.versioning.RestoreVersionForm.title");
+ private static final Message T_trail = message("xmlui.aspect.versioning.RestoreVersionForm.trail");
+ private static final Message T_head1 = message("xmlui.aspect.versioning.RestoreVersionForm.head1");
+ private static final Message T_para1 = message("xmlui.aspect.versioning.RestoreVersionForm.para1");
+ private static final Message T_column1 = message("xmlui.aspect.versioning.RestoreVersionForm.column1");
+ private static final Message T_column2 = message("xmlui.aspect.versioning.RestoreVersionForm.column2");
+ private static final Message T_column3 = message("xmlui.aspect.versioning.RestoreVersionForm.column3");
+ private static final Message T_column4 = message("xmlui.aspect.versioning.RestoreVersionForm.column4");
+
+ private static final Message T_submit_restore = message("xmlui.aspect.versioning.RestoreVersionForm.restore");
+ private static final Message T_submit_cancel = message("xmlui.general.cancel");
+
+
+ public void addPageMeta(PageMeta pageMeta) throws WingException
+ {
+ pageMeta.addMetadata("title").addContent(T_title);
+
+ pageMeta.addTrailLink(contextPath + "/", T_dspace_home);
+ pageMeta.addTrailLink(contextPath + "/admin/item",T_item_trail);
+ pageMeta.addTrail().addContent(T_trail);
+ }
+
+ public void addBody(Body body) throws WingException, AuthorizeException
+ {
+ Division main = createMainDivision(body);
+ createTable(main);
+ addButtons(main);
+ main.addHidden("versioning-continue").setValue(knot.getId());
+ }
+
+
+ private Division createMainDivision(Body body) throws WingException
+ {
+ Division main = body.addInteractiveDivision("restore-version", contextPath+"/item/versionhistory", Division.METHOD_POST, "restore version");
+ main.setHead(T_head1);
+ main.addPara(T_para1);
+ return main;
+ }
+
+
+ private void createTable(Division main) throws WingException
+ {
+ // Get all our parameters
+ String id = parameters.getParameter("versionID", null);
+
+ Table table = main.addTable("version", 1, 1);
+
+ Row header = table.addRow(Row.ROLE_HEADER);
+ header.addCellContent(T_column1);
+ header.addCellContent(T_column2);
+ header.addCellContent(T_column3);
+ header.addCellContent(T_column4);
+
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ Version version = versioningService.getVersion(context, Integer.parseInt(id));
+
+ Row row = table.addRow();
+ row.addCell().addContent(version.getVersionNumber());
+ row.addCell().addContent(version.getEperson().getEmail());
+ row.addCell().addContent(new DCDate(version.getVersionDate()).toString());
+ row.addCell().addContent(version.getSummary());
+
+
+ List fields = main.addList("fields", List.TYPE_FORM);
+ Composite addComposite = fields.addItem().addComposite("summary");
+ addComposite.setLabel(T_column4);
+ addComposite.addTextArea("summary");
+ }
+
+ private void addButtons(Division main) throws WingException
+ {
+ Para buttons = main.addPara();
+ buttons.addButton("submit_restore").setValue(T_submit_restore);
+ buttons.addButton("submit_cancel").setValue(T_submit_cancel);
+ }
+}
+
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionHistoryForm.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionHistoryForm.java
new file mode 100644
index 0000000000..53e2d9e9c6
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionHistoryForm.java
@@ -0,0 +1,253 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.utils.HandleUtil;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.*;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.AuthorizeManager;
+import org.dspace.content.*;
+import org.dspace.content.Item;
+import org.dspace.core.ConfigurationManager;
+import org.dspace.eperson.EPerson;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.Version;
+import org.dspace.versioning.VersionHistory;
+import org.dspace.versioning.VersioningService;
+import org.dspace.workflow.WorkflowItem;
+import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
+
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersionHistoryForm extends AbstractDSpaceTransformer {
+ /** Language strings */
+ private static final Message T_dspace_home = message("xmlui.general.dspace_home");
+
+ private static final Message T_head2 = message("xmlui.aspect.versioning.VersionHistoryForm.head2");
+ private static final Message T_column1 = message("xmlui.aspect.versioning.VersionHistoryForm.column1");
+ private static final Message T_column2 = message("xmlui.aspect.versioning.VersionHistoryForm.column2");
+ private static final Message T_column3 = message("xmlui.aspect.versioning.VersionHistoryForm.column3");
+ private static final Message T_column4 = message("xmlui.aspect.versioning.VersionHistoryForm.column4");
+ private static final Message T_column5 = message("xmlui.aspect.versioning.VersionHistoryForm.column5");
+ private static final Message T_column6 = message("xmlui.aspect.versioning.VersionHistoryForm.column6");
+ private static final Message T_title = message("xmlui.aspect.versioning.VersionHistoryForm.title");
+ private static final Message T_trail = message("xmlui.aspect.versioning.VersionHistoryForm.trail");
+ private static final Message T_submit_update = message("xmlui.aspect.versioning.VersionHistoryForm.update");
+ private static final Message T_submit_cancel = message("xmlui.aspect.versioning.VersionHistoryForm.return");
+ private static final Message T_submit_delete = message("xmlui.aspect.versioning.VersionHistoryForm.delete");
+ private static final Message T_legend = message("xmlui.aspect.versioning.VersionHistoryForm.legend");
+
+
+ public void addPageMeta(PageMeta pageMeta) throws WingException, SQLException
+ {
+ pageMeta.addMetadata("title").addContent(T_title);
+ pageMeta.addTrailLink(contextPath + "/", T_dspace_home);
+
+ Item item = getItem();
+ if(item != null)
+ {
+ HandleUtil.buildHandleTrail(item, pageMeta, contextPath);
+ pageMeta.addTrailLink(contextPath + "/handle/" + item.getHandle(), item.getName());
+ }
+ pageMeta.addTrail().addContent(T_trail);
+ }
+
+
+ public void addBody(Body body) throws WingException, SQLException, AuthorizeException
+ {
+ boolean isItemView=parameters.getParameterAsInteger("itemID",-1) == -1;
+ Item item = getItem();
+
+ if(item==null || !AuthorizeManager.isAdmin(this.context, item.getOwningCollection()))
+ {
+ if(isItemView)
+ {
+ //Check if only administrators can view the item history
+ if(new DSpace().getConfigurationService().getPropertyAsType("versioning.item.history.view.admin", false))
+ {
+ return;
+ }
+ }else{
+ //Only admins can delete versions
+ throw new AuthorizeException();
+ }
+ }
+
+
+
+ VersionHistory versionHistory = retrieveVersionHistory(item);
+ if(versionHistory!=null)
+ {
+ Division main = createMain(body);
+ createTable(main, versionHistory, isItemView, item);
+
+ if(!isItemView)
+ {
+ addButtons(main, versionHistory);
+ main.addHidden("versioning-continue").setValue(knot.getId());
+ }
+
+ Para note = main.addPara();
+ note.addContent(T_legend);
+ }
+ }
+
+
+ private Item getItem() throws WingException
+ {
+ try
+ {
+ if(parameters.getParameterAsInteger("itemID",-1) == -1)
+ {
+ DSpaceObject dso;
+ dso = HandleUtil.obtainHandle(objectModel);
+ if (!(dso instanceof Item))
+ {
+ return null;
+ }
+ return (Item) dso;
+ }else{
+ return Item.find(context, parameters.getParameterAsInteger("itemID", -1));
+ }
+ } catch (SQLException e) {
+ throw new WingException(e);
+ }
+
+
+ }
+
+ private VersionHistory retrieveVersionHistory(Item item) throws WingException
+ {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ return versioningService.findVersionHistory(context, item.getID());
+ }
+
+
+ private Division createMain(Body body) throws WingException
+ {
+ Division main = body.addInteractiveDivision("view-verion-history", contextPath+"/item/versionhistory", Division.METHOD_POST, "view version history");
+ main.setHead(T_head2);
+ return main;
+ }
+
+ private void createTable(Division main, VersionHistory history, boolean isItemView, Item item) throws WingException, SQLException
+ {
+ Table table = main.addTable("versionhistory", 1, 1);
+
+ Row header = table.addRow(Row.ROLE_HEADER);
+ if(!isItemView)
+ {
+ header.addCell().addContent("");
+ }
+ header.addCell().addContent(T_column1);
+ header.addCell().addContent(T_column2);
+ header.addCell().addContent(T_column3);
+ header.addCell().addContent(T_column4);
+ header.addCell().addContent(T_column5);
+
+ if(!isItemView)
+ {
+ header.addCell().addContent(T_column6);
+ }
+
+ if(history != null)
+ {
+ for(Version version : history.getVersions())
+ {
+
+ //Skip items currently in submission
+ if(isItemInSubmission(version.getItem()))
+ {
+ continue;
+ }
+
+ Row row = table.addRow(null, Row.ROLE_DATA,"metadata-value");
+ if(!isItemView)
+ {
+ CheckBox remove = row.addCell().addCheckBox("remove");
+ remove.setLabel("remove");
+ remove.addOption(version.getVersionId());
+ }
+
+ row.addCell().addContent(version.getVersionNumber());
+ addItemIdentifier(row.addCell(), item, version);
+
+ EPerson editor = version.getEperson();
+ row.addCell().addXref("mailto:" + editor.getEmail(), editor.getFullName());
+ row.addCell().addContent(new DCDate(version.getVersionDate()).toString());
+ row.addCell().addContent(version.getSummary());
+
+
+ if(!isItemView)
+ {
+ row.addCell().addXref(contextPath + "/item/versionhistory?versioning-continue="+knot.getId()+"&versionID="+version.getVersionId() +"&itemID="+ version.getItem().getID() + "&submit_update", T_submit_update);
+ }
+ }
+ }
+ }
+
+
+ private boolean isItemInSubmission(Item item) throws SQLException
+ {
+ WorkspaceItem workspaceItem = WorkspaceItem.findByItem(context, item);
+ InProgressSubmission workflowItem;
+ if(ConfigurationManager.getProperty("workflow", "workflow.framework").equals("xmlworkflow"))
+ {
+ workflowItem = XmlWorkflowItem.findByItem(context, item);
+ }else{
+ workflowItem = WorkflowItem.findByItem(context, item);
+ }
+
+ return workspaceItem != null || workflowItem != null;
+ }
+
+ private void addItemIdentifier(Cell cell, Item item, Version version) throws WingException
+ {
+ String itemHandle = version.getItem().getHandle();
+
+ DCValue[] identifiers = version.getItem().getMetadata(MetadataSchema.DC_SCHEMA, "identifier", null, Item.ANY);
+ String itemIdentifier=null;
+ if(identifiers!=null && identifiers.length > 0)
+ {
+ itemIdentifier = identifiers[0].value;
+ }
+
+ if(itemIdentifier!=null)
+ {
+ cell.addXref(contextPath + "/resource/" + itemIdentifier, itemIdentifier);
+ }else{
+ cell.addXref(contextPath + "/handle/" + itemHandle, itemHandle);
+ }
+
+ if(item.getID()==version.getItemID())
+ {
+ cell.addContent("*");
+ }
+ }
+
+ private void addButtons(Division main, VersionHistory history) throws WingException {
+ Para actions = main.addPara();
+
+ if(history!=null && history.getVersions().size() > 0)
+ {
+ actions.addButton("submit_delete").setValue(T_submit_delete);
+ }
+
+ actions.addButton("submit_cancel").setValue(T_submit_cancel);
+ }
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionItemForm.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionItemForm.java
new file mode 100644
index 0000000000..5e8084fb87
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionItemForm.java
@@ -0,0 +1,120 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.apache.avalon.framework.parameters.ParameterException;
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.utils.HandleUtil;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.*;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.AuthorizeManager;
+import org.dspace.content.Item;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.VersioningService;
+
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+// Versioning
+public class VersionItemForm extends AbstractDSpaceTransformer {
+
+ /** Language strings */
+ private static final Message T_dspace_home = message("xmlui.general.dspace_home");
+ private static final Message T_submit_cancel = message("xmlui.general.cancel");
+
+ private static final Message T_title = message("xmlui.aspect.versioning.VersionItemForm.title");
+ private static final Message T_trail = message("xmlui.aspect.versioning.VersionItemForm.trail");
+ private static final Message T_head1 = message("xmlui.aspect.versioning.VersionItemForm.head1");
+ private static final Message T_submit_version= message("xmlui.aspect.versioning.VersionItemForm.submit_version");
+ private static final Message T_submit_update_version= message("xmlui.aspect.versioning.VersionItemForm.submit_update_version");
+ private static final Message T_summary = message("xmlui.aspect.versioning.VersionItemForm.summary");
+
+
+ public void addPageMeta(PageMeta pageMeta) throws WingException, SQLException {
+ pageMeta.addMetadata("title").addContent(T_title);
+ pageMeta.addTrailLink(contextPath + "/", T_dspace_home);
+ //pageMeta.addTrailLink(contextPath+"/admin/item", T_item_trail);
+ Item item = getItem();
+ if(item != null)
+ {
+ HandleUtil.buildHandleTrail(item, pageMeta, contextPath);
+ pageMeta.addTrailLink(contextPath + "/handle/" + item.getHandle(), item.getName());
+ }
+ pageMeta.addTrail().addContent(T_trail);
+ }
+
+ public void addBody(Body body) throws WingException, SQLException, AuthorizeException {
+
+ // Get our parameters and state
+ Item item = getItem();
+
+ //Only (collection) admins should be able to create a new version
+ if(!AuthorizeManager.isAdmin(context, item.getOwningCollection())){
+ throw new AuthorizeException();
+ }
+
+ String summary;
+ try {
+ summary = parameters.getParameter("summary");
+ } catch (ParameterException e) {
+ throw new RuntimeException(e);
+ }
+
+ // DIVISION: Main
+ Division main = body.addInteractiveDivision("version-item", contextPath+"/item/version", Division.METHOD_POST, "primary administrative version");
+ main.setHead(T_head1.parameterize(item.getHandle()));
+
+ // Fields
+ List fields = main.addList("fields", List.TYPE_FORM);
+ Composite addComposite = fields.addItem().addComposite("summary");
+ addComposite.setLabel(T_summary);
+ TextArea addValue = addComposite.addTextArea("summary");
+ if(summary!=null)
+ {
+ addValue.setValue(summary);
+ }
+
+
+ // Buttons
+ Para actions = main.addPara();
+
+ org.dspace.versioning.VersionHistory history = retrieveVersionHistory(item);
+ if(history!=null && history.hasNext(item))
+ {
+ actions.addButton("submit_update_version").setValue(T_submit_update_version);
+ }
+ else
+ {
+ actions.addButton("submit_version").setValue(T_submit_version);
+ }
+
+ actions.addButton("submit_cancel").setValue(T_submit_cancel);
+
+ main.addHidden("versioning-continue").setValue(knot.getId());
+ }
+
+ private Item getItem() throws SQLException {
+ int itemID = parameters.getParameterAsInteger("itemID",-1);
+ return Item.find(context, itemID);
+ }
+
+
+ private org.dspace.versioning.VersionHistory retrieveVersionHistory(Item item){
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ return versioningService.findVersionHistory(context, item.getID());
+ }
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionManager.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionManager.java
new file mode 100644
index 0000000000..e36f03f8a9
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionManager.java
@@ -0,0 +1,179 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.dspace.app.xmlui.aspect.administrative.FlowResult;
+import org.dspace.app.xmlui.utils.UIException;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.AuthorizeManager;
+import org.dspace.content.Item;
+import org.dspace.content.WorkspaceItem;
+import org.dspace.core.Context;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.Version;
+import org.dspace.versioning.VersionHistory;
+import org.dspace.versioning.VersioningService;
+
+import java.io.IOException;
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+public class VersionManager {
+
+ private static final Message T_version_created = new Message("default", "The new version has been created.");
+ private static final Message T_version_delete = new Message("default", "The selected version(s) have been deleted.");
+ private static final Message T_version_updated = new Message("default", "The version has been updated.");
+ private static final Message T_version_restored = new Message("default", "The version has been restored.");
+
+
+ /**
+ * Create a new version of the specified item
+ *
+ * @param context The DSpace context
+ * @param itemID The id of the to-be-versioned item
+ * @return A result object
+ */
+ // Versioning
+ public static FlowResult processCreateNewVersion(Context context, int itemID, String summary) throws SQLException, AuthorizeException, IOException {
+ FlowResult result = new FlowResult();
+ try {
+ result.setContinue(false);
+
+ Item item = Item.find(context, itemID);
+
+ if (AuthorizeManager.isAdmin(context, item) || item.canEdit()) {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ Version version = versioningService.createNewVersion(context, itemID, summary);
+ WorkspaceItem wsi = WorkspaceItem.findByItem(context, version.getItem());
+
+ context.commit();
+
+ result.setParameter("wsid", wsi.getID());
+ result.setOutcome(true);
+ result.setContinue(true);
+ result.setMessage(T_version_created);
+ result.setParameter("summary", summary);
+ }
+ } catch (Exception ex) {
+ context.abort();
+ throw new RuntimeException(ex);
+ }
+ return result;
+ }
+
+ /**
+ * Modify latest version
+ *
+ * @param context The DSpace context
+ * @param itemID The id of the to-be-versioned item
+ * @return A result object
+ */
+ // Versioning
+ public static FlowResult processUpdateVersion(Context context, int itemID, String summary) throws SQLException, AuthorizeException, IOException {
+
+ FlowResult result = new FlowResult();
+ try {
+ result.setContinue(false);
+
+ Item item = Item.find(context, itemID);
+
+ if (AuthorizeManager.isAdmin(context, item)) {
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ versioningService.updateVersion(context, itemID, summary);
+
+ context.commit();
+
+ result.setOutcome(true);
+ result.setContinue(true);
+ result.setMessage(T_version_updated);
+ result.setParameter("summary", summary);
+ }
+ } catch (Exception ex) {
+ context.abort();
+ throw new RuntimeException(ex);
+ }
+ return result;
+ }
+
+
+ /**
+ * Restore a version
+ *
+ * @param versionID id of the version to restore
+ * @param context The DSpace context
+ * @return A result object
+ */
+ // Versioning
+ public static FlowResult processRestoreVersion(Context context, int versionID, String summary) throws SQLException, AuthorizeException, IOException {
+ FlowResult result = new FlowResult();
+ try {
+ result.setContinue(false);
+
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ versioningService.restoreVersion(context, versionID, summary);
+
+ context.commit();
+
+ result.setOutcome(true);
+ result.setContinue(true);
+ result.setMessage(T_version_restored);
+ } catch (Exception ex) {
+ context.abort();
+ throw new RuntimeException(ex);
+ }
+ return result;
+ }
+
+
+ /**
+ * Delete version(s)
+ *
+ * @param context The DSpace context
+ * @param versionIDs list of versionIDs to delete
+ * @return A result object
+ */
+ // Versioning
+ public static FlowResult processDeleteVersions(Context context, int itemId, String[] versionIDs) throws SQLException, AuthorizeException, IOException, UIException {
+ FlowResult result = new FlowResult();
+ try {
+ result.setContinue(false);
+
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+
+ VersionHistory versionHistory = versioningService.findVersionHistory(context, itemId);
+
+ for (String id : versionIDs) {
+ versioningService.removeVersion(context, Integer.parseInt(id));
+ }
+ context.commit();
+
+ //Retrieve the latest version of our history (IF any is even present)
+ Version latestVersion = versionHistory.getLatestVersion();
+ if(latestVersion == null){
+ result.setParameter("itemID", null);
+ }else{
+ result.setParameter("itemID", latestVersion.getItemID());
+ }
+ result.setContinue(true);
+ result.setOutcome(true);
+ result.setMessage(T_version_delete);
+
+ } catch (Exception ex) {
+ context.abort();
+ throw new RuntimeException(ex);
+ }
+ return result;
+ }
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionNoticeTransformer.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionNoticeTransformer.java
new file mode 100644
index 0000000000..e9e315725a
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionNoticeTransformer.java
@@ -0,0 +1,125 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.apache.cocoon.ProcessingException;
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.utils.HandleUtil;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.Body;
+import org.dspace.app.xmlui.wing.element.Division;
+import org.dspace.app.xmlui.wing.element.Para;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.AuthorizeManager;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.handle.HandleManager;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.Version;
+import org.dspace.versioning.VersionHistory;
+import org.dspace.versioning.VersioningService;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Adds a notice to item page in the following conditions
+ * A new version of an item is available
+ * If the person is an admin an message will also be shown if the item has a new version in the workflow
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ *
+ */
+public class VersionNoticeTransformer extends AbstractDSpaceTransformer {
+
+ private static final Message T_new_version_head = message("xmlui.aspect.versioning.VersionNoticeTransformer.notice.new_version_head");
+ private static final Message T_new_version_help = message("xmlui.aspect.versioning.VersionNoticeTransformer.notice.new_version_help");
+ private static final Message T_workflow_version_head = message("xmlui.aspect.versioning.VersionNoticeTransformer.notice.workflow_version_head");
+ private static final Message T_workflow_version_help = message("xmlui.aspect.versioning.VersionNoticeTransformer.notice.workflow_version_help");
+
+ @Override
+ public void addBody(Body body) throws SAXException, WingException, SQLException, IOException, AuthorizeException, ProcessingException {
+ DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
+ if (!(dso instanceof Item))
+ {
+ return;
+ }
+
+ Item item = (Item) dso;
+
+ if(item.isWithdrawn())
+ {
+ return;
+ }
+
+ //Always add a placeholder in which the item information can be added !
+ Division mainDivision = body.addDivision("item-view","primary");
+ String title = item.getName();
+ if(title != null)
+ {
+ mainDivision.setHead(title);
+ }else{
+ mainDivision.setHead(item.getHandle());
+ }
+
+
+ //Check if we have a history for the item
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ VersionHistory history = versioningService.findVersionHistory(context, item.getID());
+
+ if(history != null){
+ Version latestVersion = retrieveLatestVersion(history, item);
+ if(latestVersion != null && latestVersion.getItemID() != item.getID())
+ {
+ //We have a newer version
+ Item latestVersionItem = latestVersion.getItem();
+ if(latestVersionItem.isArchived())
+ {
+ //Available, add a link for the user alerting him that a new version is available
+ addVersionNotice(mainDivision, latestVersionItem, T_new_version_head, T_new_version_help, true);
+ }else{
+ //We might be dealing with a workflow/workspace item
+ addVersionNotice(mainDivision, latestVersionItem, T_workflow_version_head, T_workflow_version_help, false);
+ }
+ }
+
+
+ }
+ }
+
+ private Version retrieveLatestVersion(VersionHistory history, Item item) throws SQLException {
+ //Attempt to retrieve the latest version
+ List allVersions = history.getVersions();
+ for (Version version : allVersions) {
+ if (version.getItem().isArchived() || AuthorizeManager.isAdmin(context, item.getOwningCollection()))
+ {
+ return version;
+ }
+ }
+ return null;
+ }
+
+ protected void addVersionNotice(Division division, Item item, Message head, Message content, boolean addItemUrl) throws WingException, SQLException
+ {
+ Division noticeDiv = division.addDivision("general-message", "version-notice notice neutral");
+ noticeDiv.setHead(head);
+
+ Para para = noticeDiv.addPara();
+ para.addContent(content);
+ if(addItemUrl)
+ {
+ String url = HandleManager.resolveToURL(context, item.getHandle());
+ para.addXref(url, url);
+ }
+ }
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionUpdateForm.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionUpdateForm.java
new file mode 100644
index 0000000000..2e3c51e4de
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/versioning/VersionUpdateForm.java
@@ -0,0 +1,80 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.xmlui.aspect.versioning;
+
+import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
+import org.dspace.app.xmlui.wing.Message;
+import org.dspace.app.xmlui.wing.WingException;
+import org.dspace.app.xmlui.wing.element.*;
+import org.dspace.content.Item;
+import org.dspace.utils.DSpace;
+import org.dspace.versioning.VersioningService;
+
+import java.sql.SQLException;
+
+/**
+ *
+ *
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+// Versioning
+public class VersionUpdateForm extends AbstractDSpaceTransformer {
+
+ /** Language strings */
+ private static final Message T_dspace_home = message("xmlui.general.dspace_home");
+ private static final Message T_submit_cancel = message("xmlui.general.cancel");
+
+ private static final Message T_title = message("xmlui.aspect.versioning.VersionUpdateForm.title");
+ private static final Message T_trail = message("xmlui.aspect.versioning.VersionUpdateForm.trail");
+ private static final Message T_head1 = message("xmlui.aspect.versioning.VersionUpdateForm.head1");
+ private static final Message T_submit_version= message("xmlui.aspect.versioning.VersionUpdateForm.submit_version");
+ private static final Message T_submit_update_version= message("xmlui.aspect.versioning.VersionUpdateForm.submit_update_version");
+ private static final Message T_summary = message("xmlui.aspect.versioning.VersionUpdateForm.summary");
+
+
+ public void addPageMeta(PageMeta pageMeta) throws WingException {
+ pageMeta.addMetadata("title").addContent(T_title);
+ pageMeta.addTrailLink(contextPath + "/", T_dspace_home);
+ pageMeta.addTrail().addContent(T_trail);
+ }
+
+ public void addBody(Body body) throws WingException, SQLException{
+ int versionID = parameters.getParameterAsInteger("versionID",-1);
+ org.dspace.versioning.Version version = getVersion(versionID);
+
+ Item item = version.getItem();
+
+ // DIVISION: Main
+ Division main = body.addInteractiveDivision("version-item", contextPath+"/item/versionhistory", Division.METHOD_POST, "primary administrative version");
+ main.setHead(T_head1.parameterize(item.getHandle()));
+
+ // Fields
+ List fields = main.addList("fields", List.TYPE_FORM);
+ Composite addComposite = fields.addItem().addComposite("summary");
+ addComposite.setLabel(T_summary);
+ TextArea addValue = addComposite.addTextArea("summary");
+ addValue.setValue(version.getSummary());
+
+
+ // Buttons
+ Para actions = main.addPara();
+
+ actions.addButton("submit_update").setValue(T_submit_update_version);
+ actions.addButton("submit_cancel").setValue(T_submit_cancel);
+ main.addHidden("versioning-continue").setValue(knot.getId());
+ }
+
+
+ private org.dspace.versioning.Version getVersion(int versionID){
+ VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class);
+ return versioningService.getVersion(context, versionID);
+ }
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/BibTexView.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/BibTexView.java
new file mode 100644
index 0000000000..9fca7fa1d4
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/BibTexView.java
@@ -0,0 +1,216 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.springmvc;
+
+
+import org.dspace.content.DCValue;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.servlet.View;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+public class BibTexView implements View {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BibTexView.class);
+ private static final String EOL = "\r\n";
+
+ private String resourceIdentifier=null;
+
+ public String getContentType() {
+
+ return "text/plain;charset=utf-8";
+ }
+
+ public BibTexView(String resourceIdentifier)
+ {
+ this.resourceIdentifier = resourceIdentifier;
+ }
+
+ public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ DSpaceObject item = (DSpaceObject)request.getAttribute(ResourceIdentifierController.DSPACE_OBJECT);
+
+ Context context = new Context();
+ context.turnOffAuthorisationSystem();
+
+ String fileName = getFileName(item);
+
+ write(response, getBibTex((Item) item, resourceIdentifier), fileName);
+
+ OutputStream aOutputStream = response.getOutputStream();
+ aOutputStream.close();
+
+ }
+
+ private void write(HttpServletResponse aResponse, String aContent, String aFileName) throws IOException {
+ aResponse.setContentType("text/plain;charset=utf-8");
+ aResponse.setContentLength(aContent.length());
+ aResponse.setHeader("Content-Disposition", "attachment; filename=\""
+ + aFileName + "\"");
+
+ // It's all over but the writing...
+ PrintWriter writer = aResponse.getWriter();
+ writer.print(aContent);
+ writer.close();
+ }
+
+
+ private String getFileName(DSpaceObject item)
+ {
+ String fileName = resourceIdentifier;
+ if(resourceIdentifier.lastIndexOf("/") !=-1)
+ {
+ fileName = resourceIdentifier.replaceAll("/", "_") + ".bib";
+ }
+
+ return fileName;
+ }
+
+
+ private String getBibTex(Item item, String resourceIdentifier) {
+ // No standardized format for data so using 'misc' for now
+ StringBuilder builder = new StringBuilder("@misc{");
+
+ String[] authors = getAuthors(item);
+ String year = getYear(item);
+ String title = getMetadataValue(item, "dc.title");
+
+ builder.append(resourceIdentifier).append(',').append(EOL);
+
+ if (title != null) {
+ builder.append(" title = {").append(title).append("},");
+ builder.append(EOL);
+ }
+
+ if (authors.length > 0) {
+ builder.append(" author = {");
+
+ // Bibtex needs the comma... do we want full names here?
+ for (int index = 0; index < authors.length; index++) {
+ if (index + 1 >= authors.length) { // last one
+ builder.append(authors[index].replace(" ", ", "));
+ }
+ else if (index + 1 < authors.length) { // not last one
+ builder.append(authors[index].replace(" ", ", "));
+ builder.append(" and ");
+ }
+ }
+
+ builder.append("},").append(EOL);
+ }
+
+ if (year != null) {
+ builder.append(" year = {").append(year).append("},").append(EOL);
+ }
+
+ return builder.append("}").append(EOL).toString();
+ }
+
+ private String getMetadataValue(Item item, String metadatafield)
+ {
+ for (DCValue value : item.getMetadata(metadatafield))
+ {
+ return value.value;
+ }
+ return null;
+ }
+
+
+ private String[] getAuthors(Item aItem)
+ {
+ ArrayList authors = new ArrayList();
+
+ authors.addAll(getAuthors(aItem.getMetadata("dc.contributor.author")));
+ authors.addAll(getAuthors(aItem.getMetadata("dc.creator")));
+ authors.addAll(getAuthors(aItem.getMetadata("dc.contributor")));
+
+ return authors.toArray(new String[authors.size()]);
+ }
+
+ private String getYear(Item aItem)
+ {
+ for (DCValue date : aItem.getMetadata("dc.date.issued"))
+ {
+ return date.value.substring(0, 4);
+ }
+
+ return null;
+ }
+
+ private List getAuthors(DCValue[] aMetadata)
+ {
+ ArrayList authors = new ArrayList();
+ StringTokenizer tokenizer;
+
+ for (DCValue metadata : aMetadata)
+ {
+ StringBuilder builder = new StringBuilder();
+
+ if (metadata.value.indexOf(",") != -1)
+ {
+ String[] parts = metadata.value.split(",");
+
+ if (parts.length > 1)
+ {
+ tokenizer = new StringTokenizer(parts[1], ". ");
+ builder.append(parts[0]).append(" ");
+
+ while (tokenizer.hasMoreTokens())
+ {
+ builder.append(tokenizer.nextToken().charAt(0));
+ }
+ }
+ else
+ {
+ builder.append(metadata.value);
+ }
+
+ authors.add(builder.toString());
+ }
+ // Now the minority case (as we've cleaned up data and input method)
+ else
+ {
+ String[] parts = metadata.value.split("\\s+|\\.");
+ String name = parts[parts.length - 1].replace("\\s+|\\.", "");
+
+ builder.append(name).append(" ");
+
+ for (int index = 0; index < parts.length - 1; index++)
+ {
+ if (parts[index].length() > 0)
+ {
+ name = parts[index].replace("\\s+|\\.", "");
+ builder.append(name.charAt(0));
+ }
+ }
+ }
+ }
+ return authors;
+ }
+
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/CocoonForwardController.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/CocoonForwardController.java
new file mode 100644
index 0000000000..fde953c14f
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/CocoonForwardController.java
@@ -0,0 +1,41 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+package org.dspace.springmvc;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.View;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.sql.SQLException;
+import java.util.*;
+
+
+/**
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+@Controller(value = "cocoonForwardController")
+public class CocoonForwardController {
+
+ private static final Logger log = LoggerFactory.getLogger(CocoonForwardController.class);
+
+ @RequestMapping
+ public ModelAndView forwardRequest(HttpServletRequest request, HttpServletResponse response) throws SQLException {
+ log.debug("CocoonForwardController!!!!!");
+ return new ModelAndView(new CocoonView());
+ }
+
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/CocoonView.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/CocoonView.java
new file mode 100644
index 0000000000..d0bd906806
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/CocoonView.java
@@ -0,0 +1,136 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+package org.dspace.springmvc;
+
+import org.apache.cocoon.servletservice.DynamicProxyRequestHandler;
+import org.springframework.context.ApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+import org.springframework.web.servlet.View;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+
+public class CocoonView implements View
+{
+ /**
+ * The startup date of the Spring application context used to setup the {@link #blockServletCollector}.
+ */
+ long applicationContextStartDate;
+ /**
+ * The servlet collector bean
+ */
+ Map blockServletCollector;
+
+ public CocoonView()
+ {
+ }
+
+ void getInterfaces(Set interfaces, Class clazz)
+ {
+ Class[] clazzInterfaces = clazz.getInterfaces();
+ for (int i = 0; i < clazzInterfaces.length; i++)
+ {
+ //add all interfaces extended by this interface or directly
+ //implemented by this class
+ getInterfaces(interfaces, clazzInterfaces[i]);
+ }
+
+ // the superclazz is null if class is instanceof Object, is
+ // an interface, a primitive type or void
+ Class superclazz = clazz.getSuperclass();
+ if (superclazz != null)
+ {
+ //add all interfaces of the superclass to the list
+ getInterfaces(interfaces, superclazz);
+ }
+
+ interfaces.addAll(Arrays.asList(clazzInterfaces));
+ }
+
+ public String getContentType()
+ {
+ return null;
+ }
+
+ public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception
+ {
+
+ final Map mountableServlets = getBlockServletMap(request);
+
+ String path = request.getPathInfo();
+
+ if (path == null)
+ {
+ path = "";
+ }
+
+ // find the servlet which mount path is the longest prefix of the path info
+ int index = path.length();
+ Servlet servlet = null;
+ while (servlet == null && index != -1)
+ {
+ path = path.substring(0, index);
+ servlet = (Servlet) mountableServlets.get(path);
+ index = path.lastIndexOf('/');
+ }
+ //case when servlet is mounted at "/" must be handled separately
+ servlet = servlet == null ? (Servlet) mountableServlets.get("/") : servlet;
+ if (servlet == null)
+ {
+ String message = "No block for " + request.getPathInfo();
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, message);
+
+ return;
+ }
+
+ // Create a dynamic proxy class that overwrites the getServletPath and
+ // getPathInfo methods to provide reasonable values in the called servlet
+ // the dynamic proxy implements all interfaces of the original request
+ HttpServletRequest prequest = (HttpServletRequest) Proxy.newProxyInstance(
+ request.getClass().getClassLoader(),
+ getInterfaces(request.getClass()),
+ new DynamicProxyRequestHandler(request, path));
+
+ servlet.service(prequest, response);
+
+ }
+
+ Class[] getInterfaces(final Class clazz)
+ {
+ Set interfaces = new LinkedHashSet();
+ getInterfaces(interfaces, clazz);
+ return (Class[]) interfaces.toArray(new Class[interfaces.size()]);
+ }
+
+ public Map getBlockServletMap(HttpServletRequest request)
+ {
+ ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getSession(true).getServletContext());
+
+ if (this.blockServletCollector == null || applicationContext.getStartupDate() != this.applicationContextStartDate)
+ {
+ this.applicationContextStartDate = applicationContext.getStartupDate();
+ this.blockServletCollector = (Map) applicationContext.getBean("org.apache.cocoon.servletservice.spring.BlockServletMap");
+ }
+
+ return blockServletCollector;
+ }
+}
\ No newline at end of file
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/ResourceIdentifierController.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/ResourceIdentifierController.java
new file mode 100644
index 0000000000..29dd000f73
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/ResourceIdentifierController.java
@@ -0,0 +1,219 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+package org.dspace.springmvc;
+
+import org.dspace.app.xmlui.utils.ContextUtil;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+
+import org.dspace.core.Context;
+import org.dspace.identifier.IdentifierNotFoundException;
+import org.dspace.identifier.IdentifierNotResolvableException;
+import org.dspace.identifier.IdentifierService;
+import org.dspace.utils.DSpace;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.servlet.ModelAndView;
+
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.sql.SQLException;
+
+
+/**
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+@Controller
+@RequestMapping(value={"/handle","/resource"})
+
+public class ResourceIdentifierController {
+
+ public static final String DSPACE_OBJECT = "dspace.object";
+ private static final String RESOURCE = "/resource";
+ private static final String METS = "mets";
+ private static final String DRI = "DRI";
+
+ private static final int STATUS_OK=200;
+ private static final int STATUS_FORBIDDEN=400;
+
+ @RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD},value={"/{prefix:.*}"})
+ public String processHandle(HttpServletRequest request, @PathVariable String prefix) {
+ //String resourceIdentifier=null;
+ try {
+
+ //String requestUri = request.getRequestURI().toString();
+
+ //resourceIdentifier = requestUri.substring(requestUri.indexOf(RESOURCE) + RESOURCE.length() + 1);
+
+ Context context = ContextUtil.obtainContext(request);
+
+ IdentifierService dis = new DSpace().getSingletonService(IdentifierService.class);
+
+ if (dis == null)
+ throw new RuntimeException("Cannot instantiate IdentifierService. Problem with spring configuration!");
+
+ DSpaceObject dso = dis.resolve(context, prefix);
+
+ if (dso == null) throw new RuntimeException("Cannot find Item!");
+
+ request.setAttribute(DSPACE_OBJECT, dso);
+
+ /** TODO: This is a temporary solution until we can adjust cocoon to not have a /handle URI */
+ return "forward:/handle/" + dso.getHandle();
+
+ } catch (SQLException e) {
+ return "forward:/error";
+
+ } catch (IdentifierNotResolvableException e) {
+ return "forward:/tombstone";
+
+ } catch (IdentifierNotFoundException e) {
+ request.setAttribute("identifier", prefix);
+ return "forward:/identifier-not-found";
+ }
+ }
+
+ @RequestMapping("/**/mets.xml")
+ public String processMETSHandle(HttpServletRequest request) {
+ try {
+
+ String requestUri = request.getRequestURI().toString();
+
+ String resourceIdentifier = requestUri.substring(requestUri.indexOf(RESOURCE) + RESOURCE.length() + 1);
+ resourceIdentifier = resourceIdentifier.substring(0, resourceIdentifier.indexOf(METS) - 1);
+
+ Context context = ContextUtil.obtainContext(request);
+
+ IdentifierService dis = new DSpace().getSingletonService(IdentifierService.class);
+
+ DSpaceObject dso = dis.resolve(context, resourceIdentifier);
+
+ if (dso == null) return null;
+
+ request.setAttribute(DSPACE_OBJECT, dso);
+
+ return "forward:/metadata/handle/" + dso.getHandle() + "/mets.xml";
+
+ } catch (SQLException e) {
+ return "forward:/error";
+ } catch (IdentifierNotResolvableException e) {
+ return "forward:/tombstone";
+
+ } catch (IdentifierNotFoundException e) {
+ return "forward:/identifier-not-found";
+ }
+ }
+
+ @RequestMapping("/**/DRI")
+ public String processDRIHandle(HttpServletRequest request) {
+ try {
+
+ String requestUri = request.getRequestURI().toString();
+
+ String resourceIdentifier = requestUri.substring(requestUri.indexOf(RESOURCE) + RESOURCE.length() + 1);
+ resourceIdentifier = resourceIdentifier.substring(0, resourceIdentifier.indexOf(DRI) - 1);
+
+ Context context = ContextUtil.obtainContext(request);
+
+ IdentifierService dis = new DSpace().getSingletonService(IdentifierService.class);
+
+ DSpaceObject dso = dis.resolve(context, resourceIdentifier);
+
+ if (dso == null) return null;
+
+ request.setAttribute(DSPACE_OBJECT, dso);
+
+ return "forward:/DRI/handle/" + dso.getHandle();
+ } catch (SQLException e) {
+ return "forward:/error";
+
+ } catch (IdentifierNotResolvableException e) {
+ return "forward:/tombstone";
+
+ } catch (IdentifierNotFoundException e) {
+ return "forward:/identifier-not-found";
+ }
+ }
+
+
+ @RequestMapping("/{prefix}/{suffix}/citation/ris")
+ public ModelAndView genRisRepresentation(@PathVariable String prefix, @PathVariable String suffix, HttpServletRequest request, HttpServletResponse response) {
+ String resourceIdentifier = prefix + "/" + suffix;
+ request.setAttribute(DSPACE_OBJECT, getDSO(request, resourceIdentifier));
+ return new ModelAndView(new RisView(resourceIdentifier));
+ }
+
+ @RequestMapping("/{prefix}/{suffix}/citation/bib")
+ public ModelAndView genBibTexRepresentation(@PathVariable String prefix, @PathVariable String suffix, HttpServletRequest request, HttpServletResponse response) {
+ String resourceIdentifier = prefix + "/" + suffix;
+ request.setAttribute(DSPACE_OBJECT, getDSO(request, resourceIdentifier) );
+ return new ModelAndView(new BibTexView(resourceIdentifier));
+ }
+
+
+ private DSpaceObject getDSO(HttpServletRequest request, String resourceIdentifier) {
+ DSpaceObject dso=null;
+ IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class);
+ Context context =null;
+ try {
+ context = new Context();
+ context.turnOffAuthorisationSystem();
+ dso = identifierService.resolve(context, resourceIdentifier);
+ if(dso==null) throw new RuntimeException("Invalid DOI! " + resourceIdentifier);
+
+ return dso;
+ }catch (IdentifierNotFoundException e) {
+ throw new RuntimeException(e);
+
+ } catch (IdentifierNotResolvableException e) {
+ throw new RuntimeException(e);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+
+ }
+ }
+
+
+ private int validate(String resourceID, HttpServletRequest request){
+ String token = request.getParameter("token");
+
+ if(token==null || "".equals(token)) return STATUS_FORBIDDEN;
+
+ if(resourceID==null || "".equals(resourceID)) return STATUS_FORBIDDEN;
+
+ // try to resolve DOI
+ DSpaceObject dso=null;
+ IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class);
+ Context context =null;
+ try {
+ context = new Context();
+ context.turnOffAuthorisationSystem();
+ dso = identifierService.resolve(context, resourceID);
+ request.setAttribute(DSPACE_OBJECT, dso);
+
+ if(!(dso instanceof Item)) return STATUS_FORBIDDEN;
+
+ return STATUS_OK;
+
+ }catch (SQLException e) {
+ return STATUS_FORBIDDEN;
+ }catch (IdentifierNotFoundException e) {
+ return STATUS_FORBIDDEN;
+
+ } catch (IdentifierNotResolvableException e) {
+ return STATUS_FORBIDDEN;
+ }
+ }
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/RisView.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/RisView.java
new file mode 100644
index 0000000000..291dce863e
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/springmvc/RisView.java
@@ -0,0 +1,272 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+package org.dspace.springmvc;
+
+
+import org.dspace.content.DCValue;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.identifier.IdentifierService;
+import org.dspace.utils.DSpace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.servlet.View;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Fabio Bolognesi (fabio at atmire dot com)
+ * @author Mark Diggory (markd at atmire dot com)
+ * @author Ben Bosman (ben at atmire dot com)
+ */
+
+public class RisView implements View {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RisView.class);
+ private static final String EOL = "\r\n";
+
+ private String resourceIdentifier=null;
+
+ public RisView(String resourceIdentifier)
+ {
+ this.resourceIdentifier = resourceIdentifier;
+ }
+
+ public String getContentType()
+ {
+
+ return "text/plain;charset=utf-8";
+ }
+
+ public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ DSpaceObject item = (DSpaceObject)request.getAttribute(ResourceIdentifierController.DSPACE_OBJECT);
+
+ Context context = new Context();
+ context.turnOffAuthorisationSystem();
+
+ String fileName = getFileName(item);
+
+ write(response, getRIS((Item) item, resourceIdentifier), fileName);
+ OutputStream aOutputStream = response.getOutputStream();
+ aOutputStream.close();
+ }
+
+ private String getFileName(DSpaceObject item)
+ {
+ String fileName = resourceIdentifier;
+ if(resourceIdentifier.indexOf("/") !=-1)
+ {
+ fileName = resourceIdentifier.replaceAll("/", "_") + ".ris";
+ }
+ return fileName;
+ }
+
+ private void write(HttpServletResponse aResponse, String aContent, String aFileName) throws IOException
+ {
+ aResponse.setContentType("text/plain;charset=utf-8");
+ aResponse.setContentLength(aContent.length());
+ aResponse.setHeader("Content-Disposition", "attachment; filename=\"" + aFileName + "\"");
+
+ // It's all over but the writing...
+ PrintWriter writer = aResponse.getWriter();
+ writer.print(aContent);
+ writer.close();
+ }
+
+
+ private String getRIS(Item item, String resourceIdentifier)
+ {
+ StringBuilder builder = new StringBuilder("TY - DATA").append(EOL);
+
+ String[] dateParts = getDate(item);
+ String title = getMetadataValue(item, "dc.title");
+ String description = getMetadataValue(item, "dc.description");
+
+ String[] keywords = getKeywords(item);
+
+
+ if (resourceIdentifier != null)
+ {
+ builder.append("ID - ").append(resourceIdentifier).append(EOL);
+ }
+
+ if (title != null)
+ {
+ builder.append("T1 - ").append(title).append(EOL);
+ }
+
+ for (String author : getAuthors(item)) {
+ builder.append("AU - ").append(author).append(EOL);
+ }
+
+ // Date for data package
+ if (dateParts.length > 0)
+ {
+ int count = 3;
+
+ builder.append("Y1 - ");
+
+ if (dateParts.length < 3)
+ {
+ count = dateParts.length;
+ }
+
+ for (int index = 0; index < count; index++) {
+ builder.append(dateParts[index]).append("/");
+ }
+
+ for (; count < 3; count++)
+ {
+ builder.append('/');
+ }
+
+ builder.append(EOL);
+ }
+
+ if (description != null)
+ {
+ builder.append("N2 - ").append(description).append(EOL);
+ }
+
+ for (String keyword : keywords)
+ {
+ builder.append("KW - ").append(keyword).append(EOL);
+ }
+
+ return builder.append("ER - ").append(EOL).toString();
+ }
+
+
+ private String[] getAuthors(Item aItem)
+ {
+ ArrayList authors = new ArrayList();
+
+ authors.addAll(getAuthors(aItem.getMetadata("dc.contributor.author")));
+ authors.addAll(getAuthors(aItem.getMetadata("dc.creator")));
+ authors.addAll(getAuthors(aItem.getMetadata("dc.contributor")));
+
+ return authors.toArray(new String[authors.size()]);
+ }
+
+ private String[] getKeywords(Item aItem)
+ {
+ ArrayList keywordList = new ArrayList();
+
+ for (DCValue keyword : aItem.getMetadata("dc.subject"))
+ {
+ if (keyword.value.length() < 255)
+ {
+ keywordList.add(keyword.value);
+ }
+ }
+
+ for (DCValue keyword : aItem.getMetadata("dwc.ScientificName"))
+ {
+ if (keyword.value.length() < 255)
+ {
+ keywordList.add(keyword.value);
+ }
+ }
+
+ return keywordList.toArray(new String[keywordList.size()]);
+ }
+
+ private String[] getDate(Item item)
+ {
+ StringTokenizer tokenizer;
+
+ for (DCValue date : item.getMetadata("dc.date.issued"))
+ {
+ tokenizer = new StringTokenizer(date.value, "-/ T");
+ String[] dateParts = new String[tokenizer.countTokens()];
+
+ for (int index = 0; index < dateParts.length; index++)
+ {
+ dateParts[index] = tokenizer.nextToken();
+ }
+
+ return dateParts;
+ }
+
+ return new String[0];
+ }
+
+ private String getMetadataValue(Item item, String metadatafield)
+ {
+ for (DCValue value : item.getMetadata(metadatafield))
+ {
+ return value.value;
+ }
+ return null;
+ }
+
+ private List getAuthors(DCValue[] aMetadata)
+ {
+ ArrayList authors = new ArrayList();
+ StringTokenizer tokenizer;
+
+ for (DCValue metadata : aMetadata)
+ {
+ StringBuilder builder = new StringBuilder();
+
+ if (metadata.value.indexOf(",") != -1)
+ {
+ String[] parts = metadata.value.split(",");
+
+ if (parts.length > 1)
+ {
+ tokenizer = new StringTokenizer(parts[1], ". ");
+ builder.append(parts[0]).append(" ");
+
+ while (tokenizer.hasMoreTokens())
+ {
+ builder.append(tokenizer.nextToken().charAt(0));
+ }
+ }
+ else
+ {
+ builder.append(metadata.value);
+ }
+
+ authors.add(builder.toString());
+ }
+ // Now the minority case (as we've cleaned up data and input method)
+ else
+ {
+ String[] parts = metadata.value.split("\\s+|\\.");
+ String name = parts[parts.length - 1].replace("\\s+|\\.", "");
+
+ builder.append(name).append(" ");
+
+ for (int index = 0; index < parts.length - 1; index++)
+ {
+ if (parts[index].length() > 0)
+ {
+ name = parts[index].replace("\\s+|\\.", "");
+ builder.append(name.charAt(0));
+ }
+ }
+ }
+ }
+ return authors;
+ }
+
+}
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Versioning/sitemap.xmap b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Versioning/sitemap.xmap
new file mode 100644
index 0000000000..cd3227379a
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Versioning/sitemap.xmap
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Versioning/versioning.js b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Versioning/versioning.js
new file mode 100644
index 0000000000..0967f68f49
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Versioning/versioning.js
@@ -0,0 +1,359 @@
+/*
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+importClass(Packages.java.lang.Class);
+importClass(Packages.java.lang.ClassLoader);
+
+importClass(Packages.org.dspace.app.xmlui.utils.FlowscriptUtils);
+
+importClass(Packages.org.dspace.app.xmlui.aspect.administrative.FlowResult);
+
+importClass(Packages.org.apache.cocoon.environment.http.HttpEnvironment);
+importClass(Packages.org.apache.cocoon.servlet.multipart.Part);
+importClass(Packages.org.dspace.content.Item);
+
+importClass(Packages.org.dspace.handle.HandleManager);
+importClass(Packages.org.dspace.core.Constants);
+importClass(Packages.org.dspace.authorize.AuthorizeManager);
+importClass(Packages.org.dspace.license.CreativeCommons);
+
+importClass(Packages.org.dspace.app.xmlui.utils.ContextUtil);
+importClass(Packages.org.dspace.app.xmlui.cocoon.HttpServletRequestCocoonWrapper);
+importClass(Packages.org.dspace.app.xmlui.aspect.versioning.VersionManager);
+
+importClass(Packages.org.dspace.submit.AbstractProcessingStep);
+
+
+
+/* Global variable which stores a comma-separated list of all fields
+ * which errored out during processing of the last step.
+ */
+var ERROR_FIELDS = null;
+
+/**
+ * Simple access method to access the current cocoon object model.
+ */
+function getObjectModel()
+{
+ return FlowscriptUtils.getObjectModel(cocoon);
+}
+
+/**
+ * Return the DSpace context for this request since each HTTP request generates
+ * a new context this object should never be stored and instead allways accessed
+ * through this method so you are ensured that it is the correct one.
+ */
+function getDSContext()
+{
+ return ContextUtil.obtainContext(getObjectModel());
+}
+
+
+/**
+ * Return the HTTP Request object for this request
+ */
+function getHttpRequest()
+{
+ //return getObjectModel().get(HttpEnvironment.HTTP_REQUEST_OBJECT)
+
+ // Cocoon's request object handles form encoding, thus if the users enters
+ // non-ascii characters such as those found in foriegn languages they will
+ // come through corruped if they are not obtained through the cocoon request
+ // object. However, since the dspace-api is built to accept only HttpServletRequest
+ // a wrapper class HttpServletRequestCocoonWrapper has bee built to translate
+ // the cocoon request back into a servlet request. This is not a fully complete
+ // translation as some methods are unimplemeted. But it is enough for our
+ // purposes here.
+ return new HttpServletRequestCocoonWrapper(getObjectModel());
+}
+
+/**
+ * Return the HTTP Response object for the response
+ * (used for compatibility with DSpace configurable submission system)
+ */
+function getHttpResponse()
+{
+ return getObjectModel().get(HttpEnvironment.HTTP_RESPONSE_OBJECT);
+}
+
+
+
+/**
+ * Start editing an individual item.
+ */
+function startCreateNewVersionItem(){
+ var itemID = cocoon.request.get("itemID");
+
+ assertEditItem(itemID);
+
+ var result= new FlowResult();
+ do{
+ result = doCreateNewVersion(itemID, result);
+ }while(result!=null);
+
+ var item = Item.find(getDSContext(),itemID);
+
+ //Send us back to the item page if we cancel !
+ cocoon.redirectTo(cocoon.request.getContextPath() + "/handle/" + item.getHandle(), true);
+ getDSContext().complete();
+ item = null;
+ cocoon.exit();
+}
+
+/*
+ * Move this item to another collection
+ */
+function doCreateNewVersion(itemID, result){
+ assertEditItem(itemID);
+ do {
+ sendPageAndWait("item/version/create",{"itemID":itemID, "summary":result.getParameter("summary")}, result);
+
+ if (cocoon.request.get("submit_cancel")){
+ return null;
+ }
+ else if (cocoon.request.get("submit_version")){
+ var summary = cocoon.request.get("summary");
+ assertEditItem(itemID);
+ result = VersionManager.processCreateNewVersion(getDSContext(),itemID, summary);
+
+ var wsid = result.getParameter("wsid");
+ cocoon.redirectTo(cocoon.request.getContextPath()+"/submit?workspaceID=" + wsid,true);
+ cocoon.exit();
+ }
+ else if (cocoon.request.get("submit_update_version")){
+ var summary = cocoon.request.get("summary");
+ assertEditItem(itemID);
+ result = VersionManager.processUpdateVersion(getDSContext(),itemID, summary);
+ }
+ } while (result == null || !result.getContinue());
+
+ return result;
+}
+
+/**
+ * Start editing an individual item.
+ */
+function startVersionHistoryItem(){
+ var itemID = cocoon.request.get("itemID");
+
+ assertEditItem(itemID);
+
+ var result= new FlowResult();
+ do{
+ result=doVersionHistoryItem(itemID, result);
+ }while(result!=null);
+}
+
+function doVersionHistoryItem(itemID, result){
+ //var result;
+ do {
+ sendPageAndWait("item/versionhistory/show",{"itemID":itemID},result);
+ assertEditItem(itemID);
+ result = null;
+
+ if (cocoon.request.get("submit_cancel")){
+ //Pressed the cancel button, redirect us to the item page
+ var item = Item.find(getDSContext(),itemID);
+
+ cocoon.redirectTo(cocoon.request.getContextPath()+"/handle/"+item.getHandle(),true);
+ getDSContext().complete();
+ item = null;
+ cocoon.exit();
+ }
+ else if (cocoon.request.get("submit_delete") && cocoon.request.get("remove")){
+ var versionIDs = cocoon.request.getParameterValues("remove");
+ result = doDeleteVersions(itemID, versionIDs);
+ if(result != null){
+ if(result.getParameter("itemID") == null){
+ // We have removed everything, redirect us to the home page !
+ cocoon.redirectTo(cocoon.request.getContextPath(), true);
+ getDSContext().complete();
+ cocoon.exit();
+ }else{
+ //Perhaps we have a new item (if we deleted the current version)
+ itemID = result.getParameter("itemID");
+ }
+ }
+
+ }
+ else if (cocoon.request.get("submit_restore") && cocoon.request.get("versionID")){
+ var versionID = cocoon.request.get("versionID");
+ itemID = cocoon.request.get("itemID");
+
+ result = doRestoreVersion(itemID, versionID);
+ }
+ else if (cocoon.request.get("submit_update") && cocoon.request.get("versionID")){
+ var versionID = cocoon.request.get("versionID");
+ itemID = cocoon.request.get("itemID");
+ result = doUpdateVersion(itemID, versionID);
+ }
+ } while (true)
+}
+
+/**
+ * Confirm and delete the given version(s)
+ */
+function doDeleteVersions(itemID, versionIDs){
+
+ sendPageAndWait("item/versionhistory/delete",{"itemID":itemID,"versionIDs":versionIDs.join(',')});
+
+ if (cocoon.request.get("submit_cancel")){
+ return null;
+ }
+ else if (cocoon.request.get("submit_confirm")){
+ return VersionManager.processDeleteVersions(getDSContext(), itemID, versionIDs);
+ }
+ return null;
+}
+
+
+/**
+ * Restore the given version
+ */
+function doRestoreVersion(itemID, versionID){
+ var result;
+ do {
+ sendPageAndWait("item/versionhistory/restore", {"itemID":itemID,"versionID":versionID}, result);
+ result = null;
+ if (cocoon.request.get("submit_cancel"))
+ return null;
+
+ else if (cocoon.request.get("submit_restore")){
+ var summary = cocoon.request.get("summary");
+ result = VersionManager.processRestoreVersion(getDSContext(),versionID, summary);
+ }
+
+
+ } while (result == null || ! result.getContinue())
+ return result;
+}
+
+
+/**
+ * Update the given version
+ */
+function doUpdateVersion(itemID, versionID){
+ var result;
+ do {
+ sendPageAndWait("item/versionhistory/update", {"itemID":itemID,"versionID":versionID}, result);
+ result = null;
+ if (cocoon.request.get("submit_cancel")){
+ return null;
+ }
+ else if (cocoon.request.get("submit_update")){
+ var summary = cocoon.request.get("summary");
+ result = VersionManager.processUpdateVersion(getDSContext(),itemID, summary);
+ }
+
+
+ } while (result == null || ! result.getContinue())
+ return result;
+}
+
+
+/**
+ * Assert that the currently authenticated eperson can edit this item, if they can
+ * not then this method will never return.
+ */
+function assertEditItem(itemID) {
+
+ if ( ! canEditItem(itemID)) {
+ sendPage("admin/not-authorized");
+ cocoon.exit();
+ }
+}
+
+/**
+ * Return weather the currently authenticated eperson can edit the identified item.
+ */
+function canEditItem(itemID)
+{
+ // Navigation already deals with loading the right operation. return always true.
+ return true;
+}
+
+
+
+
+function sendPageAndWait(uri,bizData,result)
+{
+ if (bizData == null)
+ bizData = {};
+
+ if (result != null)
+ {
+ var outcome = result.getOutcome();
+ var header = result.getHeader();
+ var message = result.getMessage();
+ var characters = result.getCharacters();
+
+ if (message != null || characters != null)
+ {
+ bizData["notice"] = "true";
+ bizData["outcome"] = outcome;
+ bizData["header"] = header;
+ bizData["message"] = message;
+ bizData["characters"] = characters;
+ }
+
+ var errors = result.getErrorString();
+ if (errors != null)
+ {
+ bizData["errors"] = errors;
+ }
+ }
+
+ // just to remember where we came from.
+ bizData["flow"] = "true";
+ cocoon.sendPageAndWait(uri,bizData);
+}
+
+/**
+ * Send the given page and DO NOT wait for the flow to be continued. Excution will
+ * proccede as normal. This method will preform two usefull actions: set the flow
+ * parameter & add result information.
+ *
+ * The flow parameter is used by the sitemap to seperate requests comming from a
+ * flow script from just normal urls.
+ *
+ * The result object could potentialy contain a notice message and a list of
+ * errors. If either of these are present then they are added to the sitemap's
+ * parameters.
+ */
+function sendPage(uri,bizData,result)
+{
+ if (bizData == null)
+ bizData = {};
+
+ if (result != null)
+ {
+ var outcome = result.getOutcome();
+ var header = result.getHeader();
+ var message = result.getMessage();
+ var characters = result.getCharacters();
+
+ if (message != null || characters != null)
+ {
+ bizData["notice"] = "true";
+ bizData["outcome"] = outcome;
+ bizData["header"] = header;
+ bizData["message"] = message;
+ bizData["characters"] = characters;
+ }
+
+ var errors = result.getErrorString();
+ if (errors != null)
+ {
+ bizData["errors"] = errors;
+ }
+ }
+
+ // just to remember where we came from.
+ bizData["flow"] = "true";
+ cocoon.sendPage(uri,bizData);
+}
diff --git a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/spring-servlet.xml b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/spring-servlet.xml
new file mode 100644
index 0000000000..05b25064bb
--- /dev/null
+++ b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/spring-servlet.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/web.xml b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/web.xml
index 5032b86757..d667283ab0 100644
--- a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/web.xml
+++ b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/WEB-INF/web.xml
@@ -127,17 +127,17 @@
-->
CocoonMultipartFilter
- Cocoon
+ spring
DSpaceCocoonServletFilter
- Cocoon
+ spring
SetCharacterEncoding
- Cocoon
+ spring
-
+
+
+
+
+
+
+ spring
+ org.springframework.web.servlet.DispatcherServlet
1
@@ -209,7 +217,7 @@
to change this parameter.
-->
- Cocoon
+ spring
/*
@@ -218,7 +226,7 @@
by '/' mapping, but must be overriden explicitly.
-->
- Cocoon
+ spring
*.jsp
- Cocoon
+ spring
*.html
diff --git a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/i18n/messages.xml b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/i18n/messages.xml
index bb95595a2a..b5fde035d5 100644
--- a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/i18n/messages.xml
+++ b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/i18n/messages.xml
@@ -2163,4 +2163,68 @@
Items in Google Scholar
Download
+
+
+ Create version of this item
+ Show version history
+
+ Create Version
+ Version
+ Create new version of item: {0}
+ Version
+ Update Version
+ Reason for creating new version
+
+ Update Version
+ Version
+ Update version of item: {0}
+ Version
+ Update Version
+ Summary
+
+
+ Confirm Deletion
+ Confirm deletion
+ Confirm Deletion(s)
+ Are you sure you want to delete these versions.
+ PLEASE NOTE: That by deleting these versions, the associated items will no longer be accessible.
+ Version
+ Item
+ Editor
+ Date
+ Summary
+
+
+ Restore Version
+ Restore Version
+ Restore Version
+ Are you sure you want to restore this version:
+ Version
+ Editor
+ Date
+ Summary
+ Restore
+
+ Version History
+ Version history
+ Version History
+ Version
+ Item
+ Editor
+ Date
+ Summary
+ Actions
+ Restore
+ Update
+ *Selected version
+ Delete Versions
+ Return
+ (collection administrators only)
+
+ Notice
+ This is not the latest version of this item. The latest version can be found at:
+ Notice
+ A more recent version of this item is in the Workflow.
+
+
diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg
index 1c223d97cc..daac5145f6 100644
--- a/dspace/config/dspace.cfg
+++ b/dspace/config/dspace.cfg
@@ -631,9 +631,9 @@ event.dispatcher.default.class = org.dspace.event.BasicDispatcher
#
# uncomment below and comment out original property to enable discovery indexing
-# event.dispatcher.default.consumers = search, browse, discovery, eperson, harvester
+# event.dispatcher.default.consumers = versioning, search, browse, discovery, eperson, harvester
#
-event.dispatcher.default.consumers = search, browse, eperson, harvester
+event.dispatcher.default.consumers = versioning, search, browse, eperson, harvester
# The noindex dispatcher will not create search or browse indexes (useful for batch item imports)
event.dispatcher.noindex.class = org.dspace.event.BasicDispatcher
@@ -663,6 +663,10 @@ event.consumer.harvester.filters = Item+Delete
#event.consumer.test.class = org.dspace.event.TestConsumer
#event.consumer.test.filters = All+All
+# consumer to maintain versions
+event.consumer.versioning.class = org.dspace.versioning.VersioningConsumer
+event.consumer.versioning.filters = Item+Install
+
# ...set to true to enable testConsumer messages to standard output
#testConsumer.verbose = true
diff --git a/dspace/config/modules/versioning.cfg b/dspace/config/modules/versioning.cfg
new file mode 100644
index 0000000000..2c51ac2d9e
--- /dev/null
+++ b/dspace/config/modules/versioning.cfg
@@ -0,0 +1,10 @@
+#---------------------------------------------------#
+#------------ VERSIONING CONFIGURATIONS ------------#
+#---------------------------------------------------#
+# These configs are used by the versioning system #
+#---------------------------------------------------#
+
+# Control if the history overview of an item should only be shown to administrators
+# If enabled only the administrators for the item will be able to view the versioning history
+# If disabled anyone with READ permissions on the item will be able to view the versioning history
+item.history.view.admin=false
diff --git a/dspace/config/spring/api/identifier-service.xml b/dspace/config/spring/api/identifier-service.xml
new file mode 100644
index 0000000000..3e8f7b8d77
--- /dev/null
+++ b/dspace/config/spring/api/identifier-service.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dspace/config/spring/api/versioning-service.xml b/dspace/config/spring/api/versioning-service.xml
new file mode 100644
index 0000000000..8c3c109323
--- /dev/null
+++ b/dspace/config/spring/api/versioning-service.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/dspace/config/xmlui.xconf b/dspace/config/xmlui.xconf
index f1b895b2b8..ea40b3e9d3 100644
--- a/dspace/config/xmlui.xconf
+++ b/dspace/config/xmlui.xconf
@@ -64,6 +64,7 @@
BrowseArtifacts, SearchArtifacts
-->
+
diff --git a/dspace/etc/h2/database_schema.sql b/dspace/etc/h2/database_schema.sql
index 7865ba7cc6..5d54f9cdc8 100644
--- a/dspace/etc/h2/database_schema.sql
+++ b/dspace/etc/h2/database_schema.sql
@@ -788,6 +788,25 @@ CREATE TABLE harvested_item
CREATE INDEX harvested_item_fk_idx ON harvested_item(item_id);
+CREATE TABLE versionhistory
+(
+ versionhistory_id INTEGER NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE versionitem
+(
+ versionitem_id INTEGER NOT NULL PRIMARY KEY,
+ item_id INTEGER REFERENCES Item(item_id),
+ version_number INTEGER,
+ eperson_id INTEGER REFERENCES EPerson(eperson_id),
+ version_date TIMESTAMP,
+ version_summary VARCHAR(255),
+ versionhistory_id INTEGER REFERENCES VersionHistory(versionhistory_id)
+);
+
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
+
diff --git a/dspace/etc/oracle/database_schema.sql b/dspace/etc/oracle/database_schema.sql
index cb338d2693..10cac830e0 100644
--- a/dspace/etc/oracle/database_schema.sql
+++ b/dspace/etc/oracle/database_schema.sql
@@ -70,6 +70,8 @@ CREATE SEQUENCE group2group_seq;
CREATE SEQUENCE group2groupcache_seq;
CREATE SEQUENCE harvested_collection_seq;
CREATE SEQUENCE harvested_item_seq;
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
-------------------------------------------------------
-- BitstreamFormatRegistry table
@@ -739,3 +741,19 @@ CREATE TABLE harvested_item
);
CREATE INDEX harvested_item_fk_idx ON harvested_item(item_id);
+
+CREATE TABLE versionhistory
+(
+ versionhistory_id INTEGER NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE versionitem
+(
+ versionitem_id INTEGER NOT NULL PRIMARY KEY,
+ item_id INTEGER REFERENCES Item(item_id),
+ version_number INTEGER,
+ eperson_id INTEGER REFERENCES EPerson(eperson_id),
+ version_date TIMESTAMP,
+ version_summary VARCHAR2(255),
+ versionhistory_id INTEGER REFERENCES VersionHistory(versionhistory_id)
+);
diff --git a/dspace/etc/oracle/database_schema_18-3.sql b/dspace/etc/oracle/database_schema_18-3.sql
index 3f72fd2596..f5db0a04a1 100644
--- a/dspace/etc/oracle/database_schema_18-3.sql
+++ b/dspace/etc/oracle/database_schema_18-3.sql
@@ -30,3 +30,22 @@ ALTER TABLE resourcepolicy
ALTER TABLE item ADD discoverable NUMBER(1);
+
+CREATE TABLE versionhistory
+(
+ versionhistory_id INTEGER NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE versionitem
+(
+ versionitem_id INTEGER NOT NULL PRIMARY KEY,
+ item_id INTEGER REFERENCES Item(item_id),
+ version_number INTEGER,
+ eperson_id INTEGER REFERENCES EPerson(eperson_id),
+ version_date TIMESTAMP,
+ version_summary VARCHAR2(255),
+ versionhistory_id INTEGER REFERENCES VersionHistory(versionhistory_id)
+);
+
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
diff --git a/dspace/etc/postgres/database_schema.sql b/dspace/etc/postgres/database_schema.sql
index cb73d60049..d93ae9566f 100644
--- a/dspace/etc/postgres/database_schema.sql
+++ b/dspace/etc/postgres/database_schema.sql
@@ -107,6 +107,8 @@ CREATE SEQUENCE group2group_seq;
CREATE SEQUENCE group2groupcache_seq;
CREATE SEQUENCE harvested_collection_seq;
CREATE SEQUENCE harvested_item_seq;
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
-------------------------------------------------------
-- BitstreamFormatRegistry table
@@ -780,6 +782,24 @@ CREATE INDEX harvested_item_fk_idx ON harvested_item(item_id);
+CREATE TABLE versionhistory
+(
+ versionhistory_id INTEGER NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE versionitem
+(
+ versionitem_id INTEGER NOT NULL PRIMARY KEY,
+ item_id INTEGER REFERENCES Item(item_id),
+ version_number INTEGER,
+ eperson_id INTEGER REFERENCES EPerson(eperson_id),
+ version_date TIMESTAMP,
+ version_summary VARCHAR(255),
+ versionhistory_id INTEGER REFERENCES VersionHistory(versionhistory_id)
+);
+
+
+
diff --git a/dspace/etc/postgres/database_schema_18-3.sql b/dspace/etc/postgres/database_schema_18-3.sql
index b80f78b1ba..a85d0ac7b7 100644
--- a/dspace/etc/postgres/database_schema_18-3.sql
+++ b/dspace/etc/postgres/database_schema_18-3.sql
@@ -31,8 +31,32 @@ ALTER TABLE resourcepolicy ADD rpdescription VARCHAR(100);
ALTER TABLE item ADD discoverable BOOLEAN;
+
update item set discoverable=true;
+-------------------------------------------
+-- Item Level Versioning Tables
+-------------------------------------------
+
+CREATE TABLE versionhistory
+(
+ versionhistory_id INTEGER NOT NULL PRIMARY KEY
+);
+
+CREATE TABLE versionitem
+(
+ versionitem_id INTEGER NOT NULL PRIMARY KEY,
+ item_id INTEGER REFERENCES Item(item_id),
+ version_number INTEGER,
+ eperson_id INTEGER REFERENCES EPerson(eperson_id),
+ version_date TIMESTAMP,
+ version_summary VARCHAR(255),
+ versionhistory_id INTEGER REFERENCES VersionHistory(versionhistory_id)
+);
+
+CREATE SEQUENCE versionitem_seq;
+CREATE SEQUENCE versionhistory_seq;
+
-------------------------------------------
-- New columns and longer hash for salted password hashing DS-861 --