/* * Bundle.java * * Version: $Revision: 3705 $ * * Date: $Date: 2009-04-11 18:02:24 +0100 (Sat, 11 Apr 2009) $ * * Copyright (c) 2002-2005, Hewlett-Packard Company and Massachusetts * Institute of Technology. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the Hewlett-Packard Company nor the name of the * Massachusetts Institute of Technology nor the names of their * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package org.dspace.content; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import org.apache.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeManager; import org.dspace.authorize.ResourcePolicy; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogManager; import org.dspace.event.Event; import org.dspace.storage.rdbms.DatabaseManager; import org.dspace.storage.rdbms.TableRow; import org.dspace.storage.rdbms.TableRowIterator; /** * Class representing bundles of bitstreams stored in the DSpace system * <P> * The corresponding Bitstream objects are loaded into memory. At present, there * is no metadata associated with bundles - they are simple containers. Thus, * the <code>update</code> method doesn't do much yet. Creating, adding or * removing bitstreams has instant effect in the database. * * @author Robert Tansley * @version $Revision: 3705 $ */ public class Bundle extends DSpaceObject { /** log4j logger */ private static Logger log = Logger.getLogger(Bundle.class); /** Our context */ private Context ourContext; /** The table row corresponding to this bundle */ private TableRow bundleRow; /** The bitstreams in this bundle */ private List<Bitstream> bitstreams; /** Flag set when data is modified, for events */ private boolean modified; /** Flag set when metadata is modified, for events */ private boolean modifiedMetadata; /** * Construct a bundle object with the given table row * * @param context * the context this object exists in * @param row * the corresponding row in the table */ Bundle(Context context, TableRow row) throws SQLException { ourContext = context; bundleRow = row; bitstreams = new ArrayList<Bitstream>(); // Get bitstreams TableRowIterator tri = DatabaseManager.queryTable( ourContext, "bitstream", "SELECT bitstream.* FROM bitstream, bundle2bitstream WHERE " + "bundle2bitstream.bitstream_id=bitstream.bitstream_id AND " + "bundle2bitstream.bundle_id= ? ", bundleRow.getIntColumn("bundle_id")); try { while (tri.hasNext()) { TableRow r = (TableRow) tri.next(); // First check the cache Bitstream fromCache = (Bitstream) context.fromCache( Bitstream.class, r.getIntColumn("bitstream_id")); if (fromCache != null) { bitstreams.add(fromCache); } else { bitstreams.add(new Bitstream(ourContext, r)); } } } finally { // close the TableRowIterator to free up resources if (tri != null) tri.close(); } // Cache ourselves context.cache(this, row.getIntColumn("bundle_id")); modified = modifiedMetadata = false; } /** * Get a bundle from the database. The bundle and bitstream metadata are all * loaded into memory. * * @param context * DSpace context object * @param id * ID of the bundle * * @return the bundle, or null if the ID is invalid. */ public static Bundle find(Context context, int id) throws SQLException { // First check the cache Bundle fromCache = (Bundle) context.fromCache(Bundle.class, id); if (fromCache != null) { return fromCache; } TableRow row = DatabaseManager.find(context, "bundle", id); if (row == null) { if (log.isDebugEnabled()) { log.debug(LogManager.getHeader(context, "find_bundle", "not_found,bundle_id=" + id)); } return null; } else { if (log.isDebugEnabled()) { log.debug(LogManager.getHeader(context, "find_bundle", "bundle_id=" + id)); } return new Bundle(context, row); } } /** * Create a new bundle, with a new ID. This method is not public, since * bundles need to be created within the context of an item. For this * reason, authorisation is also not checked; that is the responsibility of * the caller. * * @param context * DSpace context object * * @return the newly created bundle */ static Bundle create(Context context) throws SQLException { // Create a table row TableRow row = DatabaseManager.create(context, "bundle"); log.info(LogManager.getHeader(context, "create_bundle", "bundle_id=" + row.getIntColumn("bundle_id"))); context.addEvent(new Event(Event.CREATE, Constants.BUNDLE, row.getIntColumn("bundle_id"), null)); return new Bundle(context, row); } /** * Get the internal identifier of this bundle * * @return the internal identifier */ public int getID() { return bundleRow.getIntColumn("bundle_id"); } /** * Get the name of the bundle * * @return name of the bundle (ORIGINAL, TEXT, THUMBNAIL) or NULL if not set */ public String getName() { return bundleRow.getStringColumn("name"); } /** * Set the name of the bundle * * @param name * string name of the bundle (ORIGINAL, TEXT, THUMBNAIL) are the * values currently used */ public void setName(String name) { bundleRow.setColumn("name", name); modifiedMetadata = true; } /** * Get the primary bitstream ID of the bundle * * @return primary bitstream ID or -1 if not set */ public int getPrimaryBitstreamID() { return bundleRow.getIntColumn("primary_bitstream_id"); } /** * Set the primary bitstream ID of the bundle * * @param bitstreamID * int ID of primary bitstream (e.g. index html file) */ public void setPrimaryBitstreamID(int bitstreamID) { bundleRow.setColumn("primary_bitstream_id", bitstreamID); modified = true; } /** * Unset the primary bitstream ID of the bundle */ public void unsetPrimaryBitstreamID() { bundleRow.setColumnNull("primary_bitstream_id"); } public String getHandle() { // No Handles for bundles return null; } /** * @param name * name of the bitstream you're looking for * * @return the bitstream or null if not found */ public Bitstream getBitstreamByName(String name) { Bitstream target = null; Iterator i = bitstreams.iterator(); while (i.hasNext()) { Bitstream b = (Bitstream) i.next(); if (name.equals(b.getName())) { target = b; break; } } return target; } /** * Get the bitstreams in this bundle * * @return the bitstreams */ public Bitstream[] getBitstreams() { Bitstream[] bitstreamArray = new Bitstream[bitstreams.size()]; bitstreamArray = (Bitstream[]) bitstreams.toArray(bitstreamArray); return bitstreamArray; } /** * Get the items this bundle appears in * * @return array of <code>Item</code> s this bundle appears in */ public Item[] getItems() throws SQLException { List<Item> items = new ArrayList<Item>(); // Get items TableRowIterator tri = DatabaseManager.queryTable( ourContext, "item", "SELECT item.* FROM item, item2bundle WHERE " + "item2bundle.item_id=item.item_id AND " + "item2bundle.bundle_id= ? ", bundleRow.getIntColumn("bundle_id")); try { while (tri.hasNext()) { TableRow r = (TableRow) tri.next(); // Used cached copy if there is one Item fromCache = (Item) ourContext.fromCache(Item.class, r .getIntColumn("item_id")); if (fromCache != null) { items.add(fromCache); } else { items.add(new Item(ourContext, r)); } } } finally { // close the TableRowIterator to free up resources if (tri != null) tri.close(); } Item[] itemArray = new Item[items.size()]; itemArray = (Item[]) items.toArray(itemArray); return itemArray; } /** * Create a new bitstream in this bundle. * * @param is * the stream to read the new bitstream from * * @return the newly created bitstream */ public Bitstream createBitstream(InputStream is) throws AuthorizeException, IOException, SQLException { Boolean overideAuthorization = false; Item[] items = getItems(); if (items.length == 1) // only one parent item for this bundle { if (items[0].canEdit()) // user attempting to create bitstream is authorized to edit parent item { overideAuthorization = true; } } if (overideAuthorization) { ourContext.turnOffAuthorisationSystem(); AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD); ourContext.restoreAuthSystemState(); } else { AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD); } Bitstream b = Bitstream.create(ourContext, is); // FIXME: Set permissions for bitstream addBitstream(b); return b; } /** * Create a new bitstream in this bundle. This method is for registering * bitstreams. * * @param assetstore corresponds to an assetstore in dspace.cfg * @param bitstreamPath the path and filename relative to the assetstore * @return the newly created bitstream * @throws IOException * @throws SQLException */ public Bitstream registerBitstream(int assetstore, String bitstreamPath) throws AuthorizeException, IOException, SQLException { // check authorisation AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD); Bitstream b = Bitstream.register(ourContext, assetstore, bitstreamPath); // FIXME: Set permissions for bitstream addBitstream(b); return b; } /** * Add an existing bitstream to this bundle * * @param b * the bitstream to add */ public void addBitstream(Bitstream b) throws SQLException, AuthorizeException { Boolean overideAuthorization = false; Item[] items = getItems(); if (items.length == 1) // only one parent Item for this Bundle { if (items[0].canEdit()) // user attempting to add Bitstream is authorized to edit parent Item { overideAuthorization = true; } } if (overideAuthorization) { ourContext.turnOffAuthorisationSystem(); AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD); ourContext.restoreAuthSystemState(); } else { AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD); } log.info(LogManager.getHeader(ourContext, "add_bitstream", "bundle_id=" + getID() + ",bitstream_id=" + b.getID())); // First check that the bitstream isn't already in the list for (int i = 0; i < bitstreams.size(); i++) { Bitstream existing = (Bitstream) bitstreams.get(i); if (b.getID() == existing.getID()) { // Bitstream is already there; no change return; } } // Add the bitstream object bitstreams.add(b); ourContext.addEvent(new Event(Event.ADD, Constants.BUNDLE, getID(), Constants.BITSTREAM, b.getID(), String.valueOf(b.getSequenceID()))); // copy authorization policies from bundle to bitstream // FIXME: multiple inclusion is affected by this... AuthorizeManager.inheritPolicies(ourContext, this, b); // Add the mapping row to the database TableRow mappingRow = DatabaseManager.create(ourContext, "bundle2bitstream"); mappingRow.setColumn("bundle_id", getID()); mappingRow.setColumn("bitstream_id", b.getID()); DatabaseManager.update(ourContext, mappingRow); } /** * Remove a bitstream from this bundle - the bitstream is only deleted if * this was the last reference to it * <p> * If the bitstream in question is the primary bitstream recorded for the * bundle the primary bitstream field is unset in order to free the * bitstream from the foreign key constraint so that the * <code>cleanup</code> process can run normally. * * @param b * the bitstream to remove */ public void removeBitstream(Bitstream b) throws AuthorizeException, SQLException, IOException { Boolean overideAuthorization = false; Bundle[] bundleArr = b.getBundles(); if (bundleArr.length == 1) // only one parent Bundle for this Bitstream { Item[] items = bundleArr[0].getItems(); if (items.length == 1) // only one parent Item for this Bundle { if (items[0].canEdit()) // user attempting to update Bitstream is authorized to edit parent Item { overideAuthorization = true; } } } if (overideAuthorization) { ourContext.turnOffAuthorisationSystem(); AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE); ourContext.restoreAuthSystemState(); } else { // Check authorisation AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE); } log.info(LogManager.getHeader(ourContext, "remove_bitstream", "bundle_id=" + getID() + ",bitstream_id=" + b.getID())); // Check for and remove any thumbnail and full text Bitsreams associated to this Bitsream /* * Determine which Bundles that this Bitstream is a member of. (Should * only be one Bundle for Jorum) */ for (Bundle bitsreamBundle : bundleArr) { /* * Determine which Items that this Bundle is a member of. (Should * only be one Item for Jorum) */ Item[] itemArr = bitsreamBundle.getItems(); for (Item item : itemArr) { /* * Determine which Bundles belong to this Item. */ Bundle[] allItemBundles = item.getBundles(); for (Bundle itemBundle : allItemBundles) { /* * Check the bundle for Bitstreams that match the passed * Bitstream name with either '.txt' or '.jpg' appended to * the end. If a Bitstream is found then remove it */ Bitstream foundBitstream = null; removeGeneratedBitstream(b, item, itemBundle, "TEXT", ".txt"); removeGeneratedBitstream(b, item, itemBundle, "THUMBNAIL", ".jpg"); } } } // Remove from internal list of bitstreams ListIterator li = bitstreams.listIterator(); while (li.hasNext()) { Bitstream existing = (Bitstream) li.next(); if (b.getID() == existing.getID()) { // We've found the bitstream to remove li.remove(); // In the event that the bitstream to remove is actually // the primary bitstream, be sure to unset the primary // bitstream. if (b.getID() == getPrimaryBitstreamID()) { unsetPrimaryBitstreamID(); } } } ourContext.addEvent(new Event(Event.REMOVE, Constants.BUNDLE, getID(), Constants.BITSTREAM, b.getID(), String.valueOf(b.getSequenceID()))); // Delete the mapping row DatabaseManager.updateQuery(ourContext, "DELETE FROM bundle2bitstream WHERE bundle_id= ? "+ "AND bitstream_id= ? ", getID(), b.getID()); // If the bitstream is orphaned, it's removed TableRowIterator tri = DatabaseManager.query(ourContext, "SELECT * FROM bundle2bitstream WHERE bitstream_id= ? ", b.getID()); try { if (!tri.hasNext()) { // The bitstream is an orphan, delete it b.delete(); } } finally { // close the TableRowIterator to free up resources if (tri != null) tri.close(); } } private void removeGeneratedBitstream(Bitstream b, Item item, Bundle itemBundle, String bundleName, String bitstreamSuffix) throws AuthorizeException, SQLException, IOException { Bitstream foundBitstream; if (itemBundle.getName().equals(bundleName)) { try { foundBitstream = itemBundle.getBitstreamByName(b.getName() + bitstreamSuffix); itemBundle.removeBitstream(foundBitstream); if (itemBundle.getBitstreams().length == 0) { item.removeBundle(itemBundle); item.update(); } } catch (NullPointerException e) { log.info(LogManager.getHeader(ourContext, "remove_bitstream", "No " + bundleName + " Bitstream associated with this Bitstream.")); } } } /** * Update the bundle metadata */ public void update() throws SQLException, AuthorizeException { // Check authorisation //AuthorizeManager.authorizeAction(ourContext, this, Constants.WRITE); log.info(LogManager.getHeader(ourContext, "update_bundle", "bundle_id=" + getID())); if (modified) { ourContext.addEvent(new Event(Event.MODIFY, Constants.BUNDLE, getID(), null)); modified = false; } if (modifiedMetadata) { ourContext.addEvent(new Event(Event.MODIFY_METADATA, Constants.BUNDLE, getID(), null)); modifiedMetadata = false; } DatabaseManager.update(ourContext, bundleRow); } /** * Delete the bundle. Bitstreams contained by the bundle are removed first; * this may result in their deletion, if deleting this bundle leaves them as * orphans. */ // GWaller 11/9/09 Changed accessor to public to allow method to be called form Jorum code public void delete() throws SQLException, AuthorizeException, IOException { log.info(LogManager.getHeader(ourContext, "delete_bundle", "bundle_id=" + getID())); ourContext.addEvent(new Event(Event.DELETE, Constants.BUNDLE, getID(), getName())); // Remove from cache ourContext.removeCached(this, getID()); // Remove bitstreams Bitstream[] bs = getBitstreams(); for (int i = 0; i < bs.length; i++) { removeBitstream(bs[i]); } // remove our authorization policies AuthorizeManager.removeAllPolicies(ourContext, this); // Remove ourself DatabaseManager.delete(ourContext, bundleRow); } /** * return type found in Constants */ public int getType() { return Constants.BUNDLE; } /** * remove all policies on the bundle and its contents, and replace them with * the DEFAULT_BITSTREAM_READ policies belonging to the collection. * * @param c * Collection * @throws java.sql.SQLException * if an SQL error or if no default policies found. It's a bit * draconian, but default policies must be enforced. * @throws AuthorizeException */ public void inheritCollectionDefaultPolicies(Collection c) throws java.sql.SQLException, AuthorizeException { List<ResourcePolicy> policies = AuthorizeManager.getPoliciesActionFilter(ourContext, c, Constants.DEFAULT_BITSTREAM_READ); // change the action to just READ // just don't call update on the resourcepolicies!!! Iterator<ResourcePolicy> i = policies.iterator(); if (!i.hasNext()) { throw new java.sql.SQLException("Collection " + c.getID() + " has no default bitstream READ policies"); } while (i.hasNext()) { ResourcePolicy rp = (ResourcePolicy) i.next(); rp.setAction(Constants.READ); } replaceAllBitstreamPolicies(policies); } /** * remove all of the policies for the bundle and bitstream contents and replace * them with a new list of policies * * @param newpolicies - * this will be all of the new policies for the bundle and * bitstream contents * @throws SQLException * @throws AuthorizeException */ public void replaceAllBitstreamPolicies(List<ResourcePolicy> newpolicies) throws SQLException, AuthorizeException { if (bitstreams != null && bitstreams.size() > 0) { for (Bitstream bs : bitstreams) { // change bitstream policies AuthorizeManager.removeAllPolicies(ourContext, bs); AuthorizeManager.addPolicies(ourContext, newpolicies, bs); } } // change bundle policies AuthorizeManager.removeAllPolicies(ourContext, this); AuthorizeManager.addPolicies(ourContext, newpolicies, this); } // GWaller 19/11/09 Getter for the context stored in this item public Context getContext(){ return this.ourContext; } }