diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 743dd9f936..47af0037e1 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -106,13 +106,35 @@ + + + + maven-antrun-plugin + + + process-test-resources + + + + + + + run + + + + + maven-surefire-plugin ${project.build.directory}/testing/dspace ${basedir}/src/test/data/dspaceFolder - ${project.build.directory}/testing/dspace.cfg.woven + ${project.build.directory}/testing/dspace/config/dspace.cfg ${project.build.directory}/testing/dspace/etc/h2/database_schema.sql true diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseConsumer.java b/dspace-api/src/main/java/org/dspace/browse/BrowseConsumer.java index 2f54dc8915..ab1a495d6e 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseConsumer.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseConsumer.java @@ -73,7 +73,7 @@ public class BrowseConsumer implements Consumer // If an Item is created or its metadata is modified.. case Constants.ITEM: - if (et == Event.MODIFY_METADATA || et == Event.CREATE) + if (et == Event.MODIFY_METADATA || et == Event.CREATE || et == Event.MODIFY) { Item subj = (Item)event.getSubject(ctx); if (subj != null) diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseItem.java b/dspace-api/src/main/java/org/dspace/browse/BrowseItem.java index 83ee679756..f965054755 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseItem.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseItem.java @@ -8,6 +8,7 @@ package org.dspace.browse; import org.apache.log4j.Logger; +import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeManager; import org.dspace.content.*; import org.dspace.core.Constants; @@ -384,6 +385,12 @@ public class BrowseItem extends DSpaceObject } } + @Override + public void update() throws SQLException, AuthorizeException + { + + } + @Override public void updateLastModified() { diff --git a/dspace-api/src/main/java/org/dspace/browse/IndexBrowse.java b/dspace-api/src/main/java/org/dspace/browse/IndexBrowse.java index 16a2115e4a..8fecf998bb 100644 --- a/dspace-api/src/main/java/org/dspace/browse/IndexBrowse.java +++ b/dspace-api/src/main/java/org/dspace/browse/IndexBrowse.java @@ -321,7 +321,7 @@ public class IndexBrowse { indexItem(new ItemMetadataProxy(item), addingNewItem); } - else if (item.isWithdrawn()) + else if (item.isWithdrawn() || !item.isArchived()) { indexItem(new ItemMetadataProxy(item), false); } diff --git a/dspace-api/src/main/java/org/dspace/content/Collection.java b/dspace-api/src/main/java/org/dspace/content/Collection.java index 760a925ab8..f9c350ccf4 100644 --- a/dspace-api/src/main/java/org/dspace/content/Collection.java +++ b/dspace-api/src/main/java/org/dspace/content/Collection.java @@ -974,7 +974,7 @@ public class Collection extends DSpaceObject * @throws IOException * @throws AuthorizeException */ - public void update() throws SQLException, IOException, AuthorizeException + public void update() throws SQLException, AuthorizeException { // Check authorisation canEdit(true); diff --git a/dspace-api/src/main/java/org/dspace/content/Community.java b/dspace-api/src/main/java/org/dspace/content/Community.java index b9b43f3a0a..3c377f2b6e 100644 --- a/dspace-api/src/main/java/org/dspace/content/Community.java +++ b/dspace-api/src/main/java/org/dspace/content/Community.java @@ -509,7 +509,7 @@ public class Community extends DSpaceObject /** * Update the community metadata (including logo) to the database. */ - public void update() throws SQLException, IOException, AuthorizeException + public void update() throws SQLException, AuthorizeException { // Check authorisation canEdit(); diff --git a/dspace-api/src/main/java/org/dspace/content/DSpaceObject.java b/dspace-api/src/main/java/org/dspace/content/DSpaceObject.java index 21406f7531..4265885d61 100644 --- a/dspace-api/src/main/java/org/dspace/content/DSpaceObject.java +++ b/dspace-api/src/main/java/org/dspace/content/DSpaceObject.java @@ -7,13 +7,14 @@ */ package org.dspace.content; +import java.sql.SQLException; + +import org.dspace.authorize.AuthorizeException; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; -import java.sql.SQLException; - /** * Abstract base class for DSpace objects */ @@ -171,5 +172,7 @@ public abstract class DSpaceObject return null; } + public abstract void update() throws SQLException, AuthorizeException; + public abstract void updateLastModified(); } diff --git a/dspace-api/src/main/java/org/dspace/content/InProgressSubmission.java b/dspace-api/src/main/java/org/dspace/content/InProgressSubmission.java index efe4ff9156..75dcf6d6fd 100644 --- a/dspace-api/src/main/java/org/dspace/content/InProgressSubmission.java +++ b/dspace-api/src/main/java/org/dspace/content/InProgressSubmission.java @@ -37,7 +37,7 @@ public interface InProgressSubmission /** * Update the submission, including the unarchived item. */ - void update() throws SQLException, IOException, AuthorizeException; + void update() throws SQLException, AuthorizeException; /** * Get the incomplete item object diff --git a/dspace-api/src/main/java/org/dspace/content/InstallItem.java b/dspace-api/src/main/java/org/dspace/content/InstallItem.java index 5caaccbd76..44adf66cdf 100644 --- a/dspace-api/src/main/java/org/dspace/content/InstallItem.java +++ b/dspace-api/src/main/java/org/dspace/content/InstallItem.java @@ -15,7 +15,9 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.embargo.EmbargoManager; import org.dspace.event.Event; -import org.dspace.handle.HandleManager; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.IdentifierService; +import org.dspace.utils.DSpace; /** * Support to install an Item in the archive. @@ -58,21 +60,18 @@ public class InstallItem IOException, AuthorizeException { Item item = is.getItem(); - String handle; - - // if no previous handle supplied, create one - if (suppliedHandle == null) - { - // create a new handle for this item - handle = HandleManager.createHandle(c, item); - } - else - { - // assign the supplied handle to this item - handle = HandleManager.createHandle(c, item, suppliedHandle); + IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class); + try { + if(suppliedHandle == null) + { + identifierService.register(c, item); + }else{ + identifierService.register(c, item, suppliedHandle); + } + } catch (IdentifierException e) { + throw new RuntimeException("Can't create an Identifier!"); } - populateHandleMetadata(item, handle); populateMetadata(c, item); @@ -102,21 +101,17 @@ public class InstallItem throws SQLException, IOException, AuthorizeException { Item item = is.getItem(); - String handle; - // if no handle supplied - if (suppliedHandle == null) - { - // create a new handle for this item - handle = HandleManager.createHandle(c, item); - //only populate handle metadata for new handles - // (existing handles should already be in the metadata -- as it was restored by ingest process) - populateHandleMetadata(item, handle); - } - else - { - // assign the supplied handle to this item - handle = HandleManager.createHandle(c, item, suppliedHandle); + IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class); + try { + if(suppliedHandle == null) + { + identifierService.register(c, item); + }else{ + identifierService.register(c, item, suppliedHandle); + } + } catch (IdentifierException e) { + throw new RuntimeException("Can't create an Identifier!"); } // Even though we are restoring an item it may not have the proper dates. So let's @@ -146,28 +141,6 @@ public class InstallItem return finishItem(c, item, is); } - private static void populateHandleMetadata(Item item, String handle) - throws SQLException, IOException, AuthorizeException - { - String handleref = HandleManager.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); - } - } - private static void populateMetadata(Context c, Item item) throws SQLException, IOException, AuthorizeException diff --git a/dspace-api/src/main/java/org/dspace/content/Item.java b/dspace-api/src/main/java/org/dspace/content/Item.java index 50386856de..94fa7ce9d4 100644 --- a/dspace-api/src/main/java/org/dspace/content/Item.java +++ b/dspace-api/src/main/java/org/dspace/content/Item.java @@ -38,9 +38,13 @@ import org.dspace.event.Event; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.handle.HandleManager; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.IdentifierService; import org.dspace.storage.rdbms.DatabaseManager; import org.dspace.storage.rdbms.TableRow; import org.dspace.storage.rdbms.TableRowIterator; +import org.dspace.utils.DSpace; +import org.dspace.versioning.VersioningService; /** * Class representing an item in DSpace. @@ -2039,10 +2043,30 @@ public class Item extends DSpaceObject // Remove any Handle HandleManager.unbindHandle(ourContext, this); + // remove version attached to the item + removeVersion(); + + // Finally remove item row DatabaseManager.delete(ourContext, itemRow); } + private void removeVersion() throws AuthorizeException, SQLException + { + VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class); + if(versioningService.getVersion(ourContext, this)!=null) + { + versioningService.removeVersion(ourContext, this); + }else{ + IdentifierService identifierService = new DSpace().getSingletonService(IdentifierService.class); + try { + identifierService.delete(ourContext, this); + } catch (IdentifierException e) { + throw new RuntimeException(e); + } + } + } + /** * Remove item and all its sub-structure from the context cache. * Useful in batch processes where a single context has a long, diff --git a/dspace-api/src/main/java/org/dspace/content/Site.java b/dspace-api/src/main/java/org/dspace/content/Site.java index c739471ed6..aaaa6821aa 100644 --- a/dspace-api/src/main/java/org/dspace/content/Site.java +++ b/dspace-api/src/main/java/org/dspace/content/Site.java @@ -96,7 +96,7 @@ public class Site extends DSpaceObject } public void update() - throws SQLException, AuthorizeException, IOException + throws SQLException, AuthorizeException { } diff --git a/dspace-api/src/main/java/org/dspace/content/WorkspaceItem.java b/dspace-api/src/main/java/org/dspace/content/WorkspaceItem.java index caed6e991f..4900ef71d3 100644 --- a/dspace-api/src/main/java/org/dspace/content/WorkspaceItem.java +++ b/dspace-api/src/main/java/org/dspace/content/WorkspaceItem.java @@ -506,7 +506,7 @@ public class WorkspaceItem implements InProgressSubmission /** * Update the workspace item, including the unarchived item. */ - public void update() throws SQLException, AuthorizeException, IOException + public void update() throws SQLException, AuthorizeException { // Authorisation is checked by the item.update() method below diff --git a/dspace-api/src/main/java/org/dspace/core/Context.java b/dspace-api/src/main/java/org/dspace/core/Context.java index 8167803e30..15023c3351 100644 --- a/dspace-api/src/main/java/org/dspace/core/Context.java +++ b/dspace-api/src/main/java/org/dspace/core/Context.java @@ -9,13 +9,7 @@ package org.dspace.core; import java.sql.Connection; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.EmptyStackException; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Stack; +import java.util.*; import org.apache.log4j.Logger; import org.dspace.eperson.EPerson; @@ -24,6 +18,7 @@ import org.dspace.event.Dispatcher; import org.dspace.event.Event; import org.dspace.event.EventManager; import org.dspace.storage.rdbms.DatabaseManager; +import org.springframework.util.CollectionUtils; /** * Class representing the context of a particular DSpace operation. This stores @@ -77,7 +72,7 @@ public class Context private List specialGroups; /** Content events */ - private List events = null; + private LinkedList events = null; /** Event dispatcher name */ private String dispName = null; @@ -375,7 +370,7 @@ public class Context { if (events == null) { - events = new ArrayList(); + events = new LinkedList(); } events.add(event); @@ -385,17 +380,32 @@ public class Context * Get the current event list. If there is a separate list of events from * already-committed operations combine that with current list. * - * TODO WARNING: events uses an ArrayList, a class not ready for concurrency. - * Read http://download.oracle.com/javase/6/docs/api/java/util/Collections.html#synchronizedList%28java.util.List%29 - * on how to properly synchronize the class when calling this method - * * @return List of all available events. */ - public List getEvents() + public LinkedList getEvents() { return events; } + public boolean hasEvents() + { + return !CollectionUtils.isEmpty(events); + } + + /** + * Retrieves the first element in the events list & removes it from the list of events once retrieved + * @return The first event of the list or null if the list is empty + */ + public Event pollEvent() + { + if(hasEvents()) + { + return events.poll(); + }else{ + return null; + } + } + /** * Close the context, without committing any of the changes performed using * this context. The database connection is freed. No exception is thrown if diff --git a/dspace-api/src/main/java/org/dspace/event/BasicDispatcher.java b/dspace-api/src/main/java/org/dspace/event/BasicDispatcher.java index bb40cb72c6..a33bd03356 100644 --- a/dspace-api/src/main/java/org/dspace/event/BasicDispatcher.java +++ b/dspace-api/src/main/java/org/dspace/event/BasicDispatcher.java @@ -7,9 +7,7 @@ */ package org.dspace.event; -import java.util.Collections; import java.util.Iterator; -import java.util.List; import org.apache.log4j.Logger; import org.dspace.core.Context; @@ -72,9 +70,8 @@ public class BasicDispatcher extends Dispatcher { if (!consumers.isEmpty()) { - List 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 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 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 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 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 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 --