diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java index c9e0a8b42a..ad2b2154a7 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java +++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java @@ -41,13 +41,34 @@ package org.dspace.app.itemexport; import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.mail.BodyPart; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; @@ -57,13 +78,17 @@ import org.apache.commons.cli.PosixParser; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.content.DCValue; +import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.ItemIterator; import org.dspace.content.MetadataSchema; +import org.dspace.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.Utils; +import org.dspace.eperson.EPerson; import org.dspace.handle.HandleManager; /** @@ -81,459 +106,1077 @@ import org.dspace.handle.HandleManager; * issues -doesn't handle special characters in metadata (needs to turn &'s into * &, etc.) *

- * Modified by David Little, UCSD Libraries 12/21/04 to - * allow the registration of files (bitstreams) into DSpace. + * Modified by David Little, UCSD Libraries 12/21/04 to allow the registration + * of files (bitstreams) into DSpace. + * + * @author David Little + * @author Jay Paz */ -public class ItemExport -{ - private static final int SUBDIR_LIMIT = 0; - - /* - * - */ - public static void main(String[] argv) throws Exception - { - // create an options object and populate it - CommandLineParser parser = new PosixParser(); - - Options options = new Options(); - - options.addOption("t", "type", true, "type: COLLECTION or ITEM"); - options.addOption("i", "id", true, "ID or handle of thing to export"); - options.addOption("d", "dest", true, - "destination where you want items to go"); - options.addOption("n", "number", true, - "sequence number to begin exporting items with"); - options.addOption("h", "help", false, "help"); - - CommandLine line = parser.parse(options, argv); - - String typeString = null; - String destDirName = null; - String myIDString = null; - int seqStart = -1; - int myType = -1; - - Item myItem = null; - Collection mycollection = null; - - if (line.hasOption('h')) - { - HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("ItemExport\n", options); - System.out - .println("\nfull collection: ItemExport -t COLLECTION -i ID -d dest -n number"); - System.out - .println("singleitem: ItemExport -t ITEM -i ID -d dest -n number"); - - System.exit(0); - } - - if (line.hasOption('t')) // type - { - typeString = line.getOptionValue('t'); - - if (typeString.equals("ITEM")) - { - myType = Constants.ITEM; - } - else if (typeString.equals("COLLECTION")) - { - myType = Constants.COLLECTION; - } - } - - if (line.hasOption('i')) // id - { - myIDString = line.getOptionValue('i'); - } - - if (line.hasOption('d')) // dest - { - destDirName = line.getOptionValue('d'); - } - - if (line.hasOption('n')) // number - { - seqStart = Integer.parseInt(line.getOptionValue('n')); - } - - // now validate the args - if (myType == -1) - { - System.out - .println("type must be either COLLECTION or ITEM (-h for help)"); - System.exit(1); - } - - if (destDirName == null) - { - System.out - .println("destination directory must be set (-h for help)"); - System.exit(1); - } - - if (seqStart == -1) - { - System.out - .println("sequence start number must be set (-h for help)"); - System.exit(1); - } - - if (myIDString == null) - { - System.out - .println("ID must be set to either a database ID or a handle (-h for help)"); - System.exit(1); - } - - Context c = new Context(); - c.setIgnoreAuthorization(true); - - if (myType == Constants.ITEM) - { - // first, is myIDString a handle? - if (myIDString.indexOf('/') != -1) - { - myItem = (Item) HandleManager.resolveToObject(c, myIDString); - - if ((myItem == null) || (myItem.getType() != Constants.ITEM)) - { - myItem = null; - } - } - else - { - myItem = Item.find(c, Integer.parseInt(myIDString)); - } - - if (myItem == null) - { - System.out - .println("Error, item cannot be found: " + myIDString); - } - } - else - { - if (myIDString.indexOf('/') != -1) - { - // has a / must be a handle - mycollection = (Collection) HandleManager.resolveToObject(c, - myIDString); - - // ensure it's a collection - if ((mycollection == null) - || (mycollection.getType() != Constants.COLLECTION)) - { - mycollection = null; - } - } - else if (myIDString != null) - { - mycollection = Collection.find(c, Integer.parseInt(myIDString)); - } - - if (mycollection == null) - { - System.out.println("Error, collection cannot be found: " - + myIDString); - System.exit(1); - } - } - - if (myItem != null) - { - // it's only a single item - exportItem(c, myItem, destDirName, seqStart); - } - else - { - System.out.println("Exporting from collection: " + myIDString); - - // it's a collection, so do a bunch of items - ItemIterator i = mycollection.getItems(); - - exportItem(c, i, destDirName, seqStart); - } - - c.complete(); - } - - private static void exportItem(Context c, ItemIterator i, - String destDirName, int seqStart) throws Exception - { - int mySequenceNumber = seqStart; - int counter = SUBDIR_LIMIT - 1; - int subDirSuffix = 0; - String fullPath = destDirName; - String subdir = ""; - File dir; - - if (SUBDIR_LIMIT > 0) - { - dir = new File(destDirName); - if (!dir.isDirectory()) - { - throw new IOException(destDirName + " is not a directory."); - } - } - - System.out.println("Beginning export"); - - while (i.hasNext()) - { - if (SUBDIR_LIMIT > 0 && ++counter == SUBDIR_LIMIT) - { - subdir = new Integer(subDirSuffix++).toString(); - fullPath = destDirName + dir.separatorChar + subdir; - counter = 0; - - if (!new File(fullPath).mkdirs()) - { - throw new IOException("Error, can't make dir " + fullPath); - } - } - - System.out.println("Exporting item to " + mySequenceNumber); - exportItem(c, i.next(), fullPath, mySequenceNumber); - mySequenceNumber++; - } - } - - private static void exportItem(Context c, Item myItem, String destDirName, - int seqStart) throws Exception - { - File destDir = new File(destDirName); - - if (destDir.exists()) - { - // now create a subdirectory - File itemDir = new File(destDir + "/" + seqStart); - - System.out.println("Exporting Item " + myItem.getID() + " to " - + itemDir); - - if (itemDir.exists()) - { - throw new Exception("Directory " + destDir + "/" + seqStart - + " already exists!"); - } - - if (itemDir.mkdir()) - { - // make it this far, now start exporting - writeMetadata(c, myItem, itemDir); - writeBitstreams(c, myItem, itemDir); - writeHandle(c, myItem, itemDir); - } - else - { - throw new Exception("Error, can't make dir " + itemDir); - } - } - else - { - throw new Exception("Error, directory " + destDirName - + " doesn't exist!"); - } - } - - /** - * Discover the different schemas in use and output a seperate metadata - * XML file for each schema. - * - * @param c - * @param i - * @param destDir - * @throws Exception - */ - private static void writeMetadata(Context c, Item i, File destDir) - throws Exception - { - // Build a list of schemas for the item - HashMap map = new HashMap(); - DCValue[] dcorevalues = i.getMetadata(Item.ANY, Item.ANY, Item.ANY, Item.ANY); - for (int ii = 0; ii < dcorevalues.length; ii++) - { - map.put(dcorevalues[ii].schema, null); - } - - // Save each of the schemas into it's own metadata file - Iterator iterator = map.keySet().iterator(); - while (iterator.hasNext()) - { - String schema = (String) iterator.next(); - writeMetadata(c, schema, i, destDir); - } - } - - // output the item's dublin core into the item directory - private static void writeMetadata(Context c, String schema, Item i, File destDir) - throws Exception - { - String filename; - if (schema.equals(MetadataSchema.DC_SCHEMA)) { - filename = "dublin_core.xml"; - } else { - filename = "metadata_" + schema + ".xml"; - } - - File outFile = new File(destDir, filename); - - System.out.println("Attempting to create file " + outFile); - - if (outFile.createNewFile()) - { - BufferedOutputStream out = new BufferedOutputStream( - new FileOutputStream(outFile)); - - DCValue[] dcorevalues = i.getMetadata(schema, Item.ANY, Item.ANY, Item.ANY); - - // XML preamble - byte[] utf8 = "\n" - .getBytes("UTF-8"); - out.write(utf8, 0, utf8.length); - - String dcTag = "\n"; - utf8 = dcTag.getBytes("UTF-8"); - out.write(utf8, 0, utf8.length); - - for (int j = 0; j < dcorevalues.length; j++) - { - DCValue dcv = dcorevalues[j]; - String qualifier = dcv.qualifier; - - if (qualifier == null) - { - qualifier = "none"; - } - - utf8 = (" " - + Utils.addEntities(dcv.value) + "\n").getBytes("UTF-8"); - - out.write(utf8, 0, utf8.length); - } - - utf8 = "\n".getBytes("UTF-8"); - out.write(utf8, 0, utf8.length); - - out.close(); - } - else - { - throw new Exception("Cannot create dublin_core.xml in " + destDir); - } - } - - // create the file 'handle' which contains the handle assigned to the item - private static void writeHandle(Context c, Item i, File destDir) - throws Exception - { - String filename = "handle"; - - File outFile = new File(destDir, filename); - - if (outFile.createNewFile()) - { - PrintWriter out = new PrintWriter(new FileWriter(outFile)); - - out.println(i.getHandle()); - - // close the contents file - out.close(); - } - else - { - throw new Exception("Cannot create file " + filename + " in " - + destDir); - } - } - - /** - * Create both the bitstreams and the contents file. Any bitstreams that - * were originally registered will be marked in the contents file as such. - * However, the export directory will contain actual copies of the content - * files being exported. - * - * @param c the DSpace context - * @param i the item being exported - * @param destDir the item's export directory - * @throws Exception if there is any problem writing to the export - * directory - */ - private static void writeBitstreams(Context c, Item i, File destDir) - throws Exception - { - File outFile = new File(destDir, "contents"); - - if (outFile.createNewFile()) - { - PrintWriter out = new PrintWriter(new FileWriter(outFile)); - - Bundle[] bundles = i.getBundles(); - - for (int j = 0; j < bundles.length; j++) - { - // bundles can have multiple bitstreams now... - Bitstream[] bitstreams = bundles[j].getBitstreams(); - - String bundleName = bundles[j].getName(); - - for (int k = 0; k < bitstreams.length; k++) - { - Bitstream b = bitstreams[k]; - - String myName = b.getName(); - String oldName = myName; - int myPrefix = 1; // only used with name conflict - - InputStream is = b.retrieve(); - - boolean isDone = false; // done when bitstream is finally - // written - - while (!isDone) - { - File fout = new File(destDir, myName); - - if (fout.createNewFile()) - { - FileOutputStream fos = new FileOutputStream(fout); - Utils.bufferedCopy(is, fos); - // close streams - is.close(); - fos.close(); - - // write the manifest file entry - if (b.isRegisteredBitstream()) { - out.println("-r -s " + b.getStoreNumber() - + " -f " + myName - + "\tbundle:" + bundleName); - } else { - out.println(myName + "\tbundle:" + bundleName); - } - - isDone = true; - } - else - { - myName = myPrefix + "_" + oldName; // keep appending - // numbers to the - // filename until - // unique - myPrefix++; - } - } - } - } - - // close the contents file - out.close(); - } - else - { - throw new Exception("Cannot create contents in " + destDir); - } - } +public class ItemExport { + private static final int SUBDIR_LIMIT = 0; + + /** + * used for export download + */ + public static final String COMPRESSED_EXPORT_MIME_TYPE = "application/zip"; + + /* + * + */ + public static void main(String[] argv) throws Exception { + // create an options object and populate it + CommandLineParser parser = new PosixParser(); + + Options options = new Options(); + + options.addOption("t", "type", true, "type: COLLECTION or ITEM"); + options.addOption("i", "id", true, "ID or handle of thing to export"); + options.addOption("d", "dest", true, + "destination where you want items to go"); + options.addOption("n", "number", true, + "sequence number to begin exporting items with"); + options.addOption("h", "help", false, "help"); + + CommandLine line = parser.parse(options, argv); + + String typeString = null; + String destDirName = null; + String myIDString = null; + int seqStart = -1; + int myType = -1; + + Item myItem = null; + Collection mycollection = null; + + if (line.hasOption('h')) { + HelpFormatter myhelp = new HelpFormatter(); + myhelp.printHelp("ItemExport\n", options); + System.out + .println("\nfull collection: ItemExport -t COLLECTION -i ID -d dest -n number"); + System.out + .println("singleitem: ItemExport -t ITEM -i ID -d dest -n number"); + + System.exit(0); + } + + if (line.hasOption('t')) // type + { + typeString = line.getOptionValue('t'); + + if (typeString.equals("ITEM")) { + myType = Constants.ITEM; + } else if (typeString.equals("COLLECTION")) { + myType = Constants.COLLECTION; + } + } + + if (line.hasOption('i')) // id + { + myIDString = line.getOptionValue('i'); + } + + if (line.hasOption('d')) // dest + { + destDirName = line.getOptionValue('d'); + } + + if (line.hasOption('n')) // number + { + seqStart = Integer.parseInt(line.getOptionValue('n')); + } + + // now validate the args + if (myType == -1) { + System.out + .println("type must be either COLLECTION or ITEM (-h for help)"); + System.exit(1); + } + + if (destDirName == null) { + System.out + .println("destination directory must be set (-h for help)"); + System.exit(1); + } + + if (seqStart == -1) { + System.out + .println("sequence start number must be set (-h for help)"); + System.exit(1); + } + + if (myIDString == null) { + System.out + .println("ID must be set to either a database ID or a handle (-h for help)"); + System.exit(1); + } + + Context c = new Context(); + c.setIgnoreAuthorization(true); + + if (myType == Constants.ITEM) { + // first, is myIDString a handle? + if (myIDString.indexOf('/') != -1) { + myItem = (Item) HandleManager.resolveToObject(c, myIDString); + + if ((myItem == null) || (myItem.getType() != Constants.ITEM)) { + myItem = null; + } + } else { + myItem = Item.find(c, Integer.parseInt(myIDString)); + } + + if (myItem == null) { + System.out + .println("Error, item cannot be found: " + myIDString); + } + } else { + if (myIDString.indexOf('/') != -1) { + // has a / must be a handle + mycollection = (Collection) HandleManager.resolveToObject(c, + myIDString); + + // ensure it's a collection + if ((mycollection == null) + || (mycollection.getType() != Constants.COLLECTION)) { + mycollection = null; + } + } else if (myIDString != null) { + mycollection = Collection.find(c, Integer.parseInt(myIDString)); + } + + if (mycollection == null) { + System.out.println("Error, collection cannot be found: " + + myIDString); + System.exit(1); + } + } + + if (myItem != null) { + // it's only a single item + exportItem(c, myItem, destDirName, seqStart); + } else { + System.out.println("Exporting from collection: " + myIDString); + + // it's a collection, so do a bunch of items + ItemIterator i = mycollection.getItems(); + + exportItem(c, i, destDirName, seqStart); + } + + c.complete(); + } + + private static void exportItem(Context c, ItemIterator i, + String destDirName, int seqStart) throws Exception { + int mySequenceNumber = seqStart; + int counter = SUBDIR_LIMIT - 1; + int subDirSuffix = 0; + String fullPath = destDirName; + String subdir = ""; + File dir; + + if (SUBDIR_LIMIT > 0) { + dir = new File(destDirName); + if (!dir.isDirectory()) { + throw new IOException(destDirName + " is not a directory."); + } + } + + System.out.println("Beginning export"); + + while (i.hasNext()) { + if (SUBDIR_LIMIT > 0 && ++counter == SUBDIR_LIMIT) { + subdir = new Integer(subDirSuffix++).toString(); + fullPath = destDirName + File.separatorChar + subdir; + counter = 0; + + if (!new File(fullPath).mkdirs()) { + throw new IOException("Error, can't make dir " + fullPath); + } + } + + System.out.println("Exporting item to " + mySequenceNumber); + exportItem(c, i.next(), fullPath, mySequenceNumber); + mySequenceNumber++; + } + } + + private static void exportItem(Context c, Item myItem, String destDirName, + int seqStart) throws Exception { + File destDir = new File(destDirName); + + if (destDir.exists()) { + // now create a subdirectory + File itemDir = new File(destDir + "/" + seqStart); + + System.out.println("Exporting Item " + myItem.getID() + " to " + + itemDir); + + if (itemDir.exists()) { + throw new Exception("Directory " + destDir + "/" + seqStart + + " already exists!"); + } + + if (itemDir.mkdir()) { + // make it this far, now start exporting + writeMetadata(c, myItem, itemDir); + writeBitstreams(c, myItem, itemDir); + writeHandle(c, myItem, itemDir); + } else { + throw new Exception("Error, can't make dir " + itemDir); + } + } else { + throw new Exception("Error, directory " + destDirName + + " doesn't exist!"); + } + } + + /** + * Discover the different schemas in use and output a seperate metadata XML + * file for each schema. + * + * @param c + * @param i + * @param destDir + * @throws Exception + */ + private static void writeMetadata(Context c, Item i, File destDir) + throws Exception { + // Build a list of schemas for the item + HashMap map = new HashMap(); + DCValue[] dcorevalues = i.getMetadata(Item.ANY, Item.ANY, Item.ANY, + Item.ANY); + for (int ii = 0; ii < dcorevalues.length; ii++) { + map.put(dcorevalues[ii].schema, null); + } + + // Save each of the schemas into it's own metadata file + Iterator iterator = map.keySet().iterator(); + while (iterator.hasNext()) { + String schema = (String) iterator.next(); + writeMetadata(c, schema, i, destDir); + } + } + + // output the item's dublin core into the item directory + private static void writeMetadata(Context c, String schema, Item i, + File destDir) throws Exception { + String filename; + if (schema.equals(MetadataSchema.DC_SCHEMA)) { + filename = "dublin_core.xml"; + } else { + filename = "metadata_" + schema + ".xml"; + } + + File outFile = new File(destDir, filename); + + System.out.println("Attempting to create file " + outFile); + + if (outFile.createNewFile()) { + BufferedOutputStream out = new BufferedOutputStream( + new FileOutputStream(outFile)); + + DCValue[] dcorevalues = i.getMetadata(schema, Item.ANY, Item.ANY, + Item.ANY); + + // XML preamble + byte[] utf8 = "\n" + .getBytes("UTF-8"); + out.write(utf8, 0, utf8.length); + + String dcTag = "\n"; + utf8 = dcTag.getBytes("UTF-8"); + out.write(utf8, 0, utf8.length); + + for (int j = 0; j < dcorevalues.length; j++) { + DCValue dcv = dcorevalues[j]; + String qualifier = dcv.qualifier; + + if (qualifier == null) { + qualifier = "none"; + } + + utf8 = (" " + + Utils.addEntities(dcv.value) + "\n") + .getBytes("UTF-8"); + + out.write(utf8, 0, utf8.length); + } + + utf8 = "\n".getBytes("UTF-8"); + out.write(utf8, 0, utf8.length); + + out.close(); + } else { + throw new Exception("Cannot create dublin_core.xml in " + destDir); + } + } + + // create the file 'handle' which contains the handle assigned to the item + private static void writeHandle(Context c, Item i, File destDir) + throws Exception { + if (i.getHandle() == null) { + return; + } + String filename = "handle"; + + File outFile = new File(destDir, filename); + + if (outFile.createNewFile()) { + PrintWriter out = new PrintWriter(new FileWriter(outFile)); + + out.println(i.getHandle()); + + // close the contents file + out.close(); + } else { + throw new Exception("Cannot create file " + filename + " in " + + destDir); + } + } + + /** + * Create both the bitstreams and the contents file. Any bitstreams that + * were originally registered will be marked in the contents file as such. + * However, the export directory will contain actual copies of the content + * files being exported. + * + * @param c + * the DSpace context + * @param i + * the item being exported + * @param destDir + * the item's export directory + * @throws Exception + * if there is any problem writing to the export directory + */ + private static void writeBitstreams(Context c, Item i, File destDir) + throws Exception { + File outFile = new File(destDir, "contents"); + + if (outFile.createNewFile()) { + PrintWriter out = new PrintWriter(new FileWriter(outFile)); + + Bundle[] bundles = i.getBundles(); + + for (int j = 0; j < bundles.length; j++) { + // bundles can have multiple bitstreams now... + Bitstream[] bitstreams = bundles[j].getBitstreams(); + + String bundleName = bundles[j].getName(); + + for (int k = 0; k < bitstreams.length; k++) { + Bitstream b = bitstreams[k]; + + String myName = b.getName(); + String oldName = myName; + int myPrefix = 1; // only used with name conflict + + InputStream is = b.retrieve(); + + boolean isDone = false; // done when bitstream is finally + // written + + while (!isDone) { + File fout = new File(destDir, myName); + + if (fout.createNewFile()) { + FileOutputStream fos = new FileOutputStream(fout); + Utils.bufferedCopy(is, fos); + // close streams + is.close(); + fos.close(); + + // write the manifest file entry + if (b.isRegisteredBitstream()) { + out.println("-r -s " + b.getStoreNumber() + + " -f " + myName + "\tbundle:" + + bundleName); + } else { + out.println(myName + "\tbundle:" + bundleName); + } + + isDone = true; + } else { + myName = myPrefix + "_" + oldName; // keep + // appending + // numbers to the + // filename until + // unique + myPrefix++; + } + } + } + } + + // close the contents file + out.close(); + } else { + throw new Exception("Cannot create contents in " + destDir); + } + } + + /** + * Convenience methot to create export a single Community, Collection, or + * Item + * + * @param dso - + * the dspace object to export + * @param context - + * the dspace context + * @throws Exception + */ + public static void createDownloadableExport(DSpaceObject dso, + Context context) throws Exception { + EPerson eperson = context.getCurrentUser(); + ArrayList list = new ArrayList(1); + list.add(dso); + processDownloadableExport(list, context, eperson == null ? null + : eperson.getEmail()); + } + + /** + * Convenience method to export a List of dspace objects (Community, + * Collection or Item) + * + * @param dsObjects - + * List containing dspace objects + * @param context - + * the dspace context + * @throws Exception + */ + public static void createDownloadableExport(List dsObjects, + Context context) throws Exception { + EPerson eperson = context.getCurrentUser(); + processDownloadableExport(dsObjects, context, eperson == null ? null + : eperson.getEmail()); + } + + /** + * Convenience methot to create export a single Community, Collection, or + * Item + * + * @param dso - + * the dspace object to export + * @param context - + * the dspace context + * @param additionalEmail - + * cc email to use + * @throws Exception + */ + public static void createDownloadableExport(DSpaceObject dso, + Context context, String additionalEmail) throws Exception { + ArrayList list = new ArrayList(1); + list.add(dso); + processDownloadableExport(list, context, additionalEmail); + } + + /** + * Convenience method to export a List of dspace objects (Community, + * Collection or Item) + * + * @param dsObjects - + * List containing dspace objects + * @param context - + * the dspace context + * @param additionalEmail - + * cc email to use + * @throws Exception + */ + public static void createDownloadableExport(List dsObjects, + Context context, String additionalEmail) throws Exception { + processDownloadableExport(dsObjects, context, additionalEmail); + } + + /** + * Does the work creating a List with all the Items in the Community or + * Collection It then kicks off a new Thread to export the items, zip the + * export directory and send confirmation email + * + * @param dsObjects - + * List of dspace objects to process + * @param context - + * the dspace context + * @param additionalEmail - + * email address to cc in addition the the current user email + * @throws Exception + */ + private static void processDownloadableExport(List dsObjects, + Context context, final String additionalEmail) throws Exception { + final EPerson eperson = context.getCurrentUser(); + + // before we create a new export archive lets delete the 'expired' + // archives + deleteOldExportArchives(eperson.getID()); + + // keep track of the commulative size of all bitstreams in each of the + // items + // it will be checked against the config file entry + float size = 0; + final ArrayList items = new ArrayList(); + for (DSpaceObject dso : dsObjects) { + if (dso.getType() == Constants.COMMUNITY) { + Community community = (Community) dso; + // get all the collections in the community + Collection[] collections = community.getCollections(); + for (Collection collection : collections) { + // get all the items in each collection + ItemIterator iitems = collection.getItems(); + while (iitems.hasNext()) { + Item item = iitems.next(); + // get all the bundles in the item + Bundle[] bundles = item.getBundles(); + for (Bundle bundle : bundles) { + // get all the bitstreams in each bundle + Bitstream[] bitstreams = bundle.getBitstreams(); + for (Bitstream bit : bitstreams) { + // add up the size + size += bit.getSize(); + } + } + items.add(item.getID()); + } + } + } else if (dso.getType() == Constants.COLLECTION) { + Collection collection = (Collection) dso; + // get all the items in the collection + ItemIterator iitems = collection.getItems(); + while (iitems.hasNext()) { + Item item = iitems.next(); + // get all thebundles in the item + Bundle[] bundles = item.getBundles(); + for (Bundle bundle : bundles) { + // get all the bitstreams in the bundle + Bitstream[] bitstreams = bundle.getBitstreams(); + for (Bitstream bit : bitstreams) { + // add up the size + size += bit.getSize(); + } + } + items.add(item.getID()); + } + } else if (dso.getType() == Constants.ITEM) { + Item item = (Item) dso; + // get all the bundles in the item + Bundle[] bundles = item.getBundles(); + for (Bundle bundle : bundles) { + // get all the bitstreams in the bundle + Bitstream[] bitstreams = bundle.getBitstreams(); + for (Bitstream bit : bitstreams) { + // add up the size + size += bit.getSize(); + } + } + items.add(item.getID()); + } else { + // nothing to do just ignore this type of DSPaceObject + } + } + + // check the size of all the bitstreams against the configuration file + // entry if it exists + String megaBytes = ConfigurationManager + .getProperty("org.dspace.app.itemexport.max.size"); + if (megaBytes != null) { + float maxSize = 0; + try { + maxSize = Float.parseFloat(megaBytes); + } catch (Exception e) { + // ignore...configuration entry may not be present + } + + if (maxSize > 0) { + if (maxSize < (size / 1048576.00)) { // a megabyte + throw new Exception( + "The overall size of this export is too large. Please contact your administrator for more information."); + } + } + } + + // if we have any items to process then kick off annonymous thread + if (items.size() > 0) { + Thread go = new Thread() { + public void run() { + Context context; + try { + // create a new dspace context + context = new Context(); + // ignore auths + context.setIgnoreAuthorization(true); + ItemIterator iitems = new ItemIterator(context, items); + + String fileName = assembleFileName(eperson, new Date()); + String workDir = getExportWorkDirectory() + + System.getProperty("file.separator") + + fileName; + String downloadDir = getExportDownloadDirectory(eperson + .getID()); + + File wkDir = new File(workDir); + if (!wkDir.exists()) { + wkDir.mkdirs(); + } + + File dnDir = new File(downloadDir); + if (!dnDir.exists()) { + dnDir.mkdirs(); + } + + // export the items using normal export method + exportItem(context, iitems, workDir, 1); + // now zip up the export directory created above + zip(workDir, downloadDir + + System.getProperty("file.separator") + + fileName + ".zip"); + // email message letting user know the file is ready for + // download + emailSuccessMessage(eperson.getEmail(), + ConfigurationManager + .getProperty("mail.from.address"), + additionalEmail, fileName + ".zip"); + // return to enforcing auths + context.setIgnoreAuthorization(false); + } catch (Exception e1) { + try { + emailErrorMessage(eperson.getEmail(), + ConfigurationManager + .getProperty("mail.from.address"), + additionalEmail, e1.getMessage()); + } catch (Exception e) { + // wont throw here + } + throw new RuntimeException(e1); + } + } + + }; + + go.isDaemon(); + go.start(); + } + } + + /** + * Create a file name based on the date and eperson + * + * @param eperson - + * eperson who requested export and will be able to download it + * @param date - + * the date the export process was created + * @return String representing the file name in the form of + * 'export_yyy_MMM_dd_count_epersonID' + * @throws Exception + */ + private static String assembleFileName(EPerson eperson, Date date) + throws Exception { + // to format the date + SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MMM_dd"); + String downloadDir = getExportDownloadDirectory(eperson.getID()); + // used to avoid name collision + int count = 1; + boolean exists = true; + String fileName = null; + while (exists) { + fileName = "export_" + sdf.format(date) + "_" + count + "_" + + eperson.getID(); + exists = new File(downloadDir + + System.getProperty("file.separator") + fileName + ".zip") + .exists(); + count++; + } + return fileName; + } + + /** + * Use config file entry for org.dspace.app.itemexport.download.dir and id + * of the eperson to create a download directory name + * + * @param ePersonID - + * id of the eperson who requested export archive + * @return String representing a directory in the form of + * org.dspace.app.itemexport.download.dir/epersonID + * @throws Exception + */ + private static String getExportDownloadDirectory(int ePersonID) + throws Exception { + String downloadDir = ConfigurationManager + .getProperty("org.dspace.app.itemexport.download.dir"); + if (downloadDir == null) { + throw new Exception( + "A dspace.cfg entry for 'org.dspace.app.itemexport.download.dir' does not exist."); + } + + return downloadDir + System.getProperty("file.separator") + ePersonID; + + } + + /** + * Returns config file entry for org.dspace.app.itemexport.work.dir + * + * @return String representing config file entry for + * org.dspace.app.itemexport.work.dir + * @throws Exception + */ + private static String getExportWorkDirectory() throws Exception { + String exportDir = ConfigurationManager + .getProperty("org.dspace.app.itemexport.work.dir"); + if (exportDir == null) { + throw new Exception( + "A dspace.cfg entry for 'org.dspace.app.itemexport.work.dir' does not exist."); + } + return exportDir; + } + + /** + * Used to read the export archived. Inteded for download. + * + * @param fileName + * the name of the file to download + * @param eperson + * the eperson requesting the download + * @return an input stream of the file to be downloaded + * @throws Exception + */ + public static InputStream getExportDownloadInputStream(String fileName, + EPerson eperson) throws Exception { + File file = new File(getExportDownloadDirectory(eperson.getID()) + + System.getProperty("file.separator") + fileName); + if (file.exists()) { + return new FileInputStream(file); + } else + return null; + } + + /** + * Get the file size of the export archive represented by the file name + * + * @param fileName + * name of the file to get the size + * @param eperson + * the eperson requesting file + * @return + * @throws Exception + */ + public static long getExportFileSize(String fileName) throws Exception { + String strID = fileName.substring(fileName.lastIndexOf('_') + 1, + fileName.lastIndexOf('.')); + File file = new File( + getExportDownloadDirectory(Integer.parseInt(strID)) + + System.getProperty("file.separator") + fileName); + if (!file.exists() || !file.isFile()) { + throw new FileNotFoundException("The file " + getExportDownloadDirectory(Integer.parseInt(strID)) + + System.getProperty("file.separator") + fileName + + " does not exist."); + } + + return file.length(); + } + + public static long getExportFileLastModified(String fileName) throws Exception { + String strID = fileName.substring(fileName.lastIndexOf('_') + 1, + fileName.lastIndexOf('.')); + File file = new File( + getExportDownloadDirectory(Integer.parseInt(strID)) + + System.getProperty("file.separator") + fileName); + if (!file.exists() || !file.isFile()) { + throw new FileNotFoundException("The file " + getExportDownloadDirectory(Integer.parseInt(strID)) + + System.getProperty("file.separator") + fileName + + " does not exist."); + } + + return file.lastModified(); + } + + /** + * The file name of the export archive contains the eperson id of the person + * who created it When requested for download this method can check if the + * person requesting it is the same one that created it + * + * @param context + * dspace context + * @param fileName + * the file name to check auths for + * @return true if it is the same person false otherwise + */ + public static boolean canDownload(Context context, String fileName) { + EPerson eperson = context.getCurrentUser(); + if (eperson == null) { + return false; + } + String strID = fileName.substring(fileName.lastIndexOf('_') + 1, + fileName.lastIndexOf('.')); + try { + if (Integer.parseInt(strID) == eperson.getID()) { + return true; + } + } catch (Exception e) { + return false; + } + return false; + } + + /** + * Reads the download directory for the eperson to see if any export + * archives are available + * + * @param eperson + * @return a list of file names representing export archives that have been + * processed + * @throws Exception + */ + public static List getExportsAvailable(EPerson eperson) + throws Exception { + File downloadDir = new File(getExportDownloadDirectory(eperson.getID())); + if (!downloadDir.exists() || !downloadDir.isDirectory()) { + return null; + } + + List fileNames = new ArrayList(); + + for (String fileName : downloadDir.list()) { + if (fileName.contains("export") && fileName.endsWith(".zip")) { + fileNames.add(fileName); + } + } + + if (fileNames.size() > 0) { + return fileNames; + } + + return null; + } + + /** + * A clean up method that is ran before a new export archive is created. It + * uses the config file entry 'org.dspace.app.itemexport.life.span.hours' to + * determine if the current exports are too old and need pruging + * + * @param epersonID - + * the id of the eperson to clean up + * @throws Exception + */ + private static void deleteOldExportArchives(int epersonID) throws Exception { + int hours = ConfigurationManager + .getIntProperty("org.dspace.app.itemexport.life.span.hours"); + Calendar now = Calendar.getInstance(); + now.setTime(new Date()); + now.add(Calendar.HOUR, (-hours)); + File downloadDir = new File(getExportDownloadDirectory(epersonID)); + if (downloadDir.exists()) { + File[] files = downloadDir.listFiles(); + for (File file : files) { + if (file.lastModified() < now.getTimeInMillis()) { + file.delete(); + } + } + } + + } + + /** + * Since the archive is created in a new thread we are unable to communicate + * with calling method about success or failure. We accomplis this + * communication with email instead. Send a success email once the export + * archive is complete and ready for download + * + * @param toMail - + * email to send message to + * @param fromMail - + * email for the from field + * @param ccMail - + * carbon copy email + * @param fileName - + * the file name to be downloaded. It is added to the url in the + * email + * @throws MessagingException + */ + private static void emailSuccessMessage(String toMail, String fromMail, + String ccMail, String fileName) throws MessagingException { + StringBuffer content = new StringBuffer(); + content + .append("The item export you requested from the repositry is now ready for download."); + content.append(System.getProperty("line.separator")); + content.append(System.getProperty("line.separator")); + content + .append("You may download the compressed file using the following web address:"); + content.append(System.getProperty("line.separator")); + content.append(ConfigurationManager.getProperty("dspace.url")); + content.append("/exportdownload/"); + content.append(fileName); + content.append(System.getProperty("line.separator")); + content.append(System.getProperty("line.separator")); + content.append("Tis file will remain available for at least "); + content.append(ConfigurationManager + .getProperty("org.dspace.app.itemexport.life.span.hours")); + content.append(" hours."); + content.append(System.getProperty("line.separator")); + content.append(System.getProperty("line.separator")); + content.append("Thank you"); + + sendMessage(toMail, fromMail, ccMail, + "Item export requested is ready for download", content); + } + + /** + * Since the archive is created in a new thread we are unable to communicate + * with calling method about success or failure. We accomplis this + * communication with email instead. Send an error email if the export + * archive fails + * + * @param toMail - + * email to send message to + * @param fromMail - + * email for the from field + * @param ccMail - + * carbon copy email + * @param fileName - + * the file name to be downloaded. It is added to the url in the + * email + * @throws MessagingException + */ + private static void emailErrorMessage(String toMail, String fromMail, + String ccMail, String error) throws MessagingException { + StringBuffer content = new StringBuffer(); + content.append("The item export you requested was not completed."); + content.append(System.getProperty("line.separator")); + content.append(System.getProperty("line.separator")); + content + .append("For more infrmation you may contact your system administrator."); + content.append(System.getProperty("line.separator")); + content.append(System.getProperty("line.separator")); + content.append("Error message received: "); + content.append(error); + content.append(System.getProperty("line.separator")); + content.append(System.getProperty("line.separator")); + content.append("Thank you"); + + sendMessage(toMail, fromMail, ccMail, + "Item export requested was not completed", content); + } + + private static void sendMessage(String toMail, String fromMail, + String ccMail, String subject, StringBuffer content) + throws MessagingException { + try { + if (toMail == null || !toMail.contains("@")) { + return; + } + + // Get the mail configuration properties + String server = ConfigurationManager.getProperty("mail.server"); + + // Set up properties for mail session + Properties props = System.getProperties(); + props.put("mail.smtp.host", server); + + // Get session + Session session = Session.getDefaultInstance(props, null); + + MimeMessage msg = new MimeMessage(session); + Multipart multipart = new MimeMultipart(); + + // create the first part of the email + BodyPart messageBodyPart = new MimeBodyPart(); + messageBodyPart.setText(content.toString()); + + multipart.addBodyPart(messageBodyPart); + msg.setContent(multipart); + msg.setFrom(new InternetAddress(fromMail)); + msg.addRecipient(Message.RecipientType.TO, new InternetAddress( + toMail)); + if (ccMail != null && ccMail.contains("@")) { + msg.addRecipient(Message.RecipientType.CC, new InternetAddress( + ccMail)); + } + msg.setSentDate(new Date()); + msg.setSubject(subject); + Transport.send(msg); + } catch (MessagingException e) { + e.printStackTrace(); + throw e; + } + } + + private static void zip(String strSource, String target) throws Exception { + ZipOutputStream cpZipOutputStream = null; + String tempFileName = target + "_tmp"; + try { + File cpFile = new File(strSource); + if (!cpFile.isFile() && !cpFile.isDirectory()) { + return; + } + File targetFile = new File(tempFileName); + if (!targetFile.exists()) { + targetFile.createNewFile(); + } + FileOutputStream fos = new FileOutputStream(tempFileName); + cpZipOutputStream = new ZipOutputStream(fos); + cpZipOutputStream.setLevel(9); + zipFiles(cpFile, strSource, tempFileName, cpZipOutputStream); + cpZipOutputStream.finish(); + cpZipOutputStream.close(); + deleteDirectory(cpFile); + targetFile.renameTo(new File(target)); + } catch (Exception e) { + throw e; + } + } + + private static void zipFiles(File cpFile, String strSource, + String strTarget, ZipOutputStream cpZipOutputStream) + throws Exception { + int byteCount; + final int DATA_BLOCK_SIZE = 2048; + FileInputStream cpFileInputStream; + if (cpFile.isDirectory()) { + File[] fList = cpFile.listFiles(); + for (int i = 0; i < fList.length; i++) { + zipFiles(fList[i], strSource, strTarget, cpZipOutputStream); + } + } else { + try { + if (cpFile.getAbsolutePath().equalsIgnoreCase(strTarget)) { + return; + } + String strAbsPath = cpFile.getPath(); + String strZipEntryName = strAbsPath.substring(strSource + .length() + 1, strAbsPath.length()); + + // byte[] b = new byte[ (int)(cpFile.length()) ]; + + cpFileInputStream = new FileInputStream(cpFile); + ZipEntry cpZipEntry = new ZipEntry(strZipEntryName); + cpZipOutputStream.putNextEntry(cpZipEntry); + + byte[] b = new byte[DATA_BLOCK_SIZE]; + while ((byteCount = cpFileInputStream.read(b, 0, + DATA_BLOCK_SIZE)) != -1) { + cpZipOutputStream.write(b, 0, byteCount); + } + + // cpZipOutputStream.write(b, 0, (int)cpFile.length()); + cpZipOutputStream.closeEntry(); + } catch (Exception e) { + throw e; + } + } + } + + private static boolean deleteDirectory(File path) { + if (path.exists()) { + File[] files = path.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteDirectory(files[i]); + } else { + files[i].delete(); + } + } + } + + boolean pathDeleted = path.delete(); + return (pathDeleted); + } + } diff --git a/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/ItemExportArchiveServlet.java b/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/ItemExportArchiveServlet.java new file mode 100644 index 0000000000..15ff3d9de2 --- /dev/null +++ b/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/ItemExportArchiveServlet.java @@ -0,0 +1,158 @@ +/* + * ItemExportArchiveServlet.java + * + * Version: $Revision: 2073 $ + * + * Date: $Date: 2007-07-19 11:45:10 -0500 (Thu, 19 Jul 2007) $ + * + * 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.app.webui.servlet; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.Logger; +import org.dspace.app.itemexport.ItemExport; +import org.dspace.app.webui.util.JSPManager; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.ConfigurationManager; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.LogManager; +import org.dspace.core.Utils; + +/** + * Servlet for retrieving item export archives. The bits are simply piped to the + * user. If there is an If-Modified-Since header, only a 304 + * status code is returned if the containing item has not been modified since + * that date. + *

+ * /exportdownload/filename + * + * @author Jay Paz + */ +public class ItemExportArchiveServlet extends DSpaceServlet { + /** log4j category */ + private static Logger log = Logger + .getLogger(ItemExportArchiveServlet.class); + + /** + * Threshold on export size size before content-disposition will be set. + */ + private int threshold; + + @Override + public void init(ServletConfig arg0) throws ServletException { + + super.init(arg0); + threshold = ConfigurationManager + .getIntProperty("webui.content_disposition_threshold"); + } + + @Override + protected void doDSGet(Context context, HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException, + SQLException, AuthorizeException { + String filename = null; + + filename = request.getPathInfo().substring( + request.getPathInfo().lastIndexOf('/')+1); + System.out.println(filename); + + if (ItemExport.canDownload(context, filename)) { + try { + InputStream exportStream = ItemExport + .getExportDownloadInputStream(filename, context + .getCurrentUser()); + + if (exportStream == null || filename == null) { + // No bitstream found or filename was wrong -- ID invalid + log.info(LogManager.getHeader(context, "invalid_id", + "path=" + filename)); + JSPManager.showInvalidIDError(request, response, filename, + Constants.BITSTREAM); + + return; + } + + log.info(LogManager.getHeader(context, + "download_export_archive", "filename=" + filename)); + + // Modification date + // TODO: Currently the date of the item, since we don't have + // dates + // for files + long lastModified = ItemExport + .getExportFileLastModified(filename); + response.setDateHeader("Last-Modified", lastModified); + + // Check for if-modified-since header + long modSince = request.getDateHeader("If-Modified-Since"); + + if (modSince != -1 && lastModified < modSince) { + // Item has not been modified since requested date, + // hence bitstream has not; return 304 + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + + // Set the response MIME type + response.setContentType(ItemExport.COMPRESSED_EXPORT_MIME_TYPE); + + // Response length + long size = ItemExport.getExportFileSize(filename); + response.setHeader("Content-Length", String.valueOf(size)); + + response.setHeader("Content-Disposition", + "attachment;filename=" + filename); + + Utils.bufferedCopy(exportStream, response.getOutputStream()); + exportStream.close(); + response.getOutputStream().flush(); + } catch (Exception e) { + throw new ServletException(e); + } + } else { + throw new AuthorizeException( + "You are not authorized to download this Export Archive."); + } + } + +} diff --git a/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/MyDSpaceServlet.java b/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/MyDSpaceServlet.java index 2fae1e4146..2f6e1e026d 100644 --- a/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/MyDSpaceServlet.java +++ b/dspace-jspui/dspace-jspui-api/src/main/java/org/dspace/app/webui/servlet/MyDSpaceServlet.java @@ -50,11 +50,13 @@ import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.dspace.app.webui.util.JSPManager; +import org.dspace.app.itemexport.ItemExport; import org.dspace.app.util.SubmissionConfigReader; import org.dspace.app.util.SubmissionConfig; import org.dspace.app.webui.util.UIUtil; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.ItemIterator; import org.dspace.content.SupervisedItem; @@ -73,6 +75,7 @@ import org.dspace.workflow.WorkflowManager; * Servlet for constructing the components of the "My DSpace" page * * @author Robert Tansley + * @author Jay Paz * @version $Id$ */ public class MyDSpaceServlet extends DSpaceServlet @@ -94,6 +97,9 @@ public class MyDSpaceServlet extends DSpaceServlet /** The "reason for rejection" page */ public static final int REJECT_REASON_PAGE = 4; + + /** The "request export archive for download" page */ + public static final int REQUEST_EXPORT_ARCHIVE = 5; protected void doDSGet(Context context, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException, @@ -135,6 +141,10 @@ public class MyDSpaceServlet extends DSpaceServlet case REJECT_REASON_PAGE: processRejectReason(context, request, response); + break; + case REQUEST_EXPORT_ARCHIVE: + processExportArchive(context, request, response); + break; default: @@ -576,6 +586,99 @@ public class MyDSpaceServlet extends DSpaceServlet } } + private void processExportArchive(Context context, + HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ + + if (request.getParameter("item_id") != null) { + Item item = null; + try { + item = Item.find(context, Integer.parseInt(request + .getParameter("item_id"))); + } catch (Exception e) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } + + if (item == null) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } else { + try { + ItemExport.createDownloadableExport(item, context); + } catch (Exception e) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } + } + + // success + JSPManager.showJSP(request, response, "/mydspace/task-complete.jsp"); + } else if (request.getParameter("collection_id") != null) { + Collection col = null; + try { + col = Collection.find(context, Integer.parseInt(request + .getParameter("collection_id"))); + } catch (Exception e) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } + + if (col == null) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } else { + try { + ItemExport.createDownloadableExport(col, context); + } catch (Exception e) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } + } + JSPManager.showJSP(request, response, "/mydspace/task-complete.jsp"); + } else if (request.getParameter("community_id") != null) { + Community com = null; + try { + com = Community.find(context, Integer.parseInt(request + .getParameter("community_id"))); + } catch (Exception e) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } + + if (com == null) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } else { + try { + org.dspace.app.itemexport.ItemExport.createDownloadableExport(com, context); + } catch (Exception e) { + log.warn(LogManager.getHeader(context, "integrity_error", UIUtil + .getRequestLogInfo(request))); + JSPManager.showIntegrityError(request, response); + return; + } + } + JSPManager.showJSP(request, response, "/mydspace/task-complete.jsp"); + } + + + } // **************************************************************** // **************************************************************** // METHODS FOR SHOWING FORMS @@ -623,7 +726,16 @@ public class MyDSpaceServlet extends DSpaceServlet SupervisedItem[] supervisedItems = SupervisedItem.findbyEPerson( context, currentUser); - + // export archives available for download + List exportArchives = null; + try{ + exportArchives = ItemExport.getExportsAvailable(currentUser); + } + catch (Exception e) { + // nothing to do they just have no export archives available for download + } + + // Set attributes request.setAttribute("mydspace.user", currentUser); request.setAttribute("workspace.items", workspaceItems); @@ -633,6 +745,7 @@ public class MyDSpaceServlet extends DSpaceServlet request.setAttribute("group.memberships", memberships); request.setAttribute("display.groupmemberships", new Boolean(displayMemberships)); request.setAttribute("supervised.items", supervisedItems); + request.setAttribute("export.archives", exportArchives); // Forward to main mydspace page JSPManager.showJSP(request, response, "/mydspace/main.jsp"); diff --git a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/WEB-INF/web.xml b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/WEB-INF/web.xml index a1dff39e94..f6f9e9c08b 100644 --- a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/WEB-INF/web.xml +++ b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/WEB-INF/web.xml @@ -164,6 +164,11 @@ org.dspace.app.webui.servlet.BitstreamServlet + + exportdownload + org.dspace.app.webui.servlet.ItemExportArchiveServlet + + browse org.dspace.app.webui.servlet.BrowserServlet @@ -413,6 +418,11 @@ /bitstream/* + + exportdownload + /exportdownload/* + + browse /browse diff --git a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/collection-home.jsp b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/collection-home.jsp index 4e71344cf1..8d03a08572 100644 --- a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/collection-home.jsp +++ b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/collection-home.jsp @@ -126,6 +126,7 @@ ItemCounter ic = new ItemCounter(UIUtil.obtainContext(request)); %> +<%@page import="org.dspace.app.webui.servlet.MyDSpaceServlet"%> @@ -288,6 +289,17 @@ +<% } %> +<% if( editor_button || admin_button) { %> + + + <% } %>
+
+ + + " /> +
+
diff --git a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/community-home.jsp b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/community-home.jsp index 9f9c1b0a9b..87ef7a580f 100644 --- a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/community-home.jsp +++ b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/community-home.jsp @@ -103,6 +103,7 @@ ItemCounter ic = new ItemCounter(UIUtil.obtainContext(request)); %> +<%@page import="org.dspace.app.webui.servlet.MyDSpaceServlet"%> @@ -333,6 +334,17 @@ <% } %> + <% if( editor_button ) { %> + + + + <% } %> + <% } %>
+
+ + + " /> +
+
"> diff --git a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/display-item.jsp b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/display-item.jsp index 902f29015b..22b3d6f2cd 100644 --- a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/display-item.jsp +++ b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/display-item.jsp @@ -111,6 +111,7 @@ } %> +<%@page import="org.dspace.app.webui.servlet.MyDSpaceServlet"%> <% @@ -136,6 +137,14 @@ " /> +
+ + + <%----%> + " /> +
+
diff --git a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/mydspace/main.jsp b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/mydspace/main.jsp index be3795ba58..2b0418cb08 100644 --- a/dspace-jspui/dspace-jspui-webapp/src/main/webapp/mydspace/main.jsp +++ b/dspace-jspui/dspace-jspui-webapp/src/main/webapp/mydspace/main.jsp @@ -73,6 +73,7 @@ <%@ page import="org.dspace.eperson.Group" %> <%@ page import="org.dspace.workflow.WorkflowItem" %> <%@ page import="org.dspace.workflow.WorkflowManager" %> +<%@ page import="java.util.List" %> <% EPerson user = (EPerson) request.getAttribute("mydspace.user"); @@ -95,6 +96,8 @@ SupervisedItem[] supervisedItems = (SupervisedItem[]) request.getAttribute("supervised.items"); + List exportsAvailable = (List)request.getAttribute("export.archives"); + // Is the logged in user an admin Boolean displayMembership = (Boolean)request.getAttribute("display.groupmemberships"); boolean displayGroupMembership = (displayMembership == null ? false : displayMembership.booleanValue()); @@ -438,4 +441,13 @@ <% } %> + + <%if(exportsAvailable!=null && exportsAvailable.size()>0){ %> +

+
    + <%for(String fileName:exportsAvailable){%> +
  1. " title="<%= fileName %>"><%=fileName%>
  2. + <% } %> +
+ <%} %>
diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/ItemExport.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/ItemExport.java new file mode 100644 index 0000000000..a517617d74 --- /dev/null +++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/ItemExport.java @@ -0,0 +1,323 @@ +/* + * ItemExport.java + * + * Version: $Revision: 1.3 $ + * + * Date: $Date: 2006/07/13 23:20:54 $ + * + * Copyright (c) 2002, 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.app.xmlui.aspect.administrative; + +import java.io.IOException; +import java.io.Serializable; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Map; + +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.caching.CacheableProcessingComponent; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.environment.Request; +import org.apache.cocoon.environment.Response; +import org.apache.cocoon.environment.SourceResolver; +import org.apache.cocoon.util.HashUtil; +import org.apache.excalibur.source.SourceValidity; +import org.apache.excalibur.source.impl.validity.NOPValidity; +import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer; +import org.dspace.app.xmlui.utils.DSpaceValidity; +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.Body; +import org.dspace.app.xmlui.wing.element.Division; +import org.dspace.app.xmlui.wing.element.List; +import org.dspace.app.xmlui.wing.element.PageMeta; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.eperson.Group; +import org.xml.sax.SAXException; + +/** + * + * Create the ability to view currently available export archives for download. + * + * @author Jay Paz + */ +public class ItemExport extends AbstractDSpaceTransformer implements + CacheableProcessingComponent { + private final static Message T_dspace_home = message("xmlui.general.dspace_home"); + + private static final Message T_main_head = message("xmlui.administrative.ItemExport.head"); + + private static final Message T_export_bad_item_id = message("xmlui.administrative.ItemExport.item.id.error"); + + private static final Message T_export_bad_col_id = message("xmlui.administrative.ItemExport.collection.id.error"); + + private static final Message T_export_bad_community_id = message("xmlui.administrative.ItemExport.community.id.error"); + + private static final Message T_export_item_not_found = message("xmlui.administrative.ItemExport.item.not.found"); + + private static final Message T_export_col_not_found = message("xmlui.administrative.ItemExport.collection.not.found"); + + private static final Message T_export_community_not_found = message("xmlui.administrative.ItemExport.community.not.found"); + + private static final Message T_item_export_success = message("xmlui.administrative.ItemExport.item.success"); + + private static final Message T_col_export_success = message("xmlui.administrative.ItemExport.collection.success"); + + private static final Message T_community_export_success = message("xmlui.administrative.ItemExport.community.success"); + + private static final Message T_avail_head = message("xmlui.administrative.ItemExport.available.head"); + + /** The Cocoon request */ + Request request; + + /** The Cocoon response */ + Response response; + + java.util.List errors; + + java.util.List availableExports; + + Message message; + + /** Cached validity object */ + private SourceValidity validity; + + @Override + public void setup(SourceResolver resolver, Map objectModel, String src, + Parameters parameters) throws ProcessingException, SAXException, + IOException { + super.setup(resolver, objectModel, src, parameters); + this.objectModel = objectModel; + this.request = ObjectModelHelper.getRequest(objectModel); + this.response = ObjectModelHelper.getResponse(objectModel); + try { + availableExports = org.dspace.app.itemexport.ItemExport + .getExportsAvailable(context.getCurrentUser()); + } catch (Exception e) { + // nothing to do + } + errors = new ArrayList(); + if (request.getParameter("itemID") != null) { + Item item = null; + try { + item = Item.find(context, Integer.parseInt(request + .getParameter("itemID"))); + } catch (Exception e) { + errors.add(T_export_bad_item_id); + } + + if (item == null) { + errors.add(T_export_item_not_found); + } else { + try { + org.dspace.app.itemexport.ItemExport + .createDownloadableExport(item, context); + } catch (Exception e) { + errors.add(message(e.getMessage())); + } + } + if (errors.size() <= 0) + message = T_item_export_success; + } else if (request.getParameter("collectionID") != null) { + Collection col = null; + try { + col = Collection.find(context, Integer.parseInt(request + .getParameter("collectionID"))); + } catch (Exception e) { + errors.add(T_export_bad_col_id); + } + + if (col == null) { + errors.add(T_export_col_not_found); + } else { + try { + org.dspace.app.itemexport.ItemExport + .createDownloadableExport(col, context); + } catch (Exception e) { + errors.add(message(e.getMessage())); + } + } + if (errors.size() <= 0) + message = T_col_export_success; + } else if (request.getParameter("communityID") != null) { + Community com = null; + try { + com = Community.find(context, Integer.parseInt(request + .getParameter("communityID"))); + } catch (Exception e) { + errors.add(T_export_bad_community_id); + } + + if (com == null) { + errors.add(T_export_community_not_found); + } else { + try { + org.dspace.app.itemexport.ItemExport + .createDownloadableExport(com, context); + } catch (Exception e) { + errors.add(message(e.getMessage())); + } + } + if (errors.size() <= 0) + message = T_community_export_success; + } + } + + /** + * 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(); + if (availableExports != null && availableExports.size() > 0) { + for (String fileName : availableExports) { + key += ":" + fileName; + } + } + + if (request.getQueryString() != null) { + key += request.getQueryString(); + } + } 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); + } + + this.validity = validity.complete(); + } catch (SQLException sqle) { + // Just ignore it and return invalid. + } + } else { + this.validity = NOPValidity.SHARED_INSTANCE; + } + } + return this.validity; + } + + /** + * Add Page metadata. + */ + public void addPageMeta(PageMeta pageMeta) throws SAXException, + WingException, UIException, SQLException, IOException, + AuthorizeException { + pageMeta.addMetadata("title").addContent(T_main_head); + + pageMeta.addTrailLink(contextPath + "/", T_dspace_home); + pageMeta.addTrail().addContent(T_main_head); + } + + public void addBody(Body body) throws SAXException, WingException, + UIException, SQLException, IOException, AuthorizeException { + Division main = body.addDivision("export_main"); + main.setHead(T_main_head); + + if (message != null) { + main.addDivision("success", "success").addPara(message); + } + + if (errors.size() > 0) { + Division errors = main.addDivision("export-errors", "error"); + for (Message error : this.errors) { + errors.addPara(error); + } + } + + if (availableExports != null && availableExports.size() > 0) { + Division avail = main.addDivision("available-exports", + "available-exports"); + avail.setHead(T_avail_head); + + List fileList = avail.addList("available-files", List.TYPE_ORDERED); + for (String fileName : availableExports) { + fileList.addItem().addXref( + this.contextPath + "/exportdownload/" + fileName, + fileName); + } + } + } + + /** + * recycle + */ + public void recycle() { + this.validity = null; + this.errors = null; + this.message = null; + this.availableExports = null; + super.recycle(); + } + +} diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/Navigation.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/Navigation.java index c3d1471e1d..a77b4a106b 100644 --- a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/Navigation.java +++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/aspect/administrative/Navigation.java @@ -42,13 +42,18 @@ package org.dspace.app.xmlui.aspect.administrative; import java.io.IOException; import java.io.Serializable; import java.sql.SQLException; +import java.util.Map; +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.cocoon.ProcessingException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.environment.ObjectModelHelper; import org.apache.cocoon.environment.Request; +import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.util.HashUtil; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.NOPValidity; +import org.dspace.app.itemexport.ItemExport; import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer; import org.dspace.app.xmlui.utils.DSpaceValidity; import org.dspace.app.xmlui.utils.HandleUtil; @@ -75,6 +80,7 @@ import org.xml.sax.SAXException; * @author Scott Phillips * @author Afonso Araujo Neto (internationalization) * @author Alexey Maslov + * @author Jay Paz */ public class Navigation extends AbstractDSpaceTransformer implements CacheableProcessingComponent { @@ -101,12 +107,18 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr private static final Message T_statistics = message("xmlui.administrative.Navigation.statistics"); - + private static final Message T_context_export_item = message("xmlui.administrative.Navigation.context_export_item"); + private static final Message T_context_export_collection = message("xmlui.administrative.Navigation.context_export_collection"); + private static final Message T_context_export_community = message("xmlui.administrative.Navigation.context_export_community"); + private static final Message T_account_export = message("xmlui.administrative.Navigation.account_export"); /** Cached validity object */ private SourceValidity validity; + /** exports available for download */ + java.util.List availableExports = null; + /** * Generate the unique cache key. * @@ -128,9 +140,16 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr return "0"; } - String key; + String key; if (context.getCurrentUser() != null) - key = context.getCurrentUser().getEmail(); + { + key = context.getCurrentUser().getEmail(); + if(availableExports!=null && availableExports.size()>0){ + for(String fileName:availableExports){ + key+= ":"+fileName; + } + } + } else key = "anonymous"; @@ -176,6 +195,20 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr return this.validity; } + + public void setup(SourceResolver resolver, Map objectModel, String src, Parameters parameters) throws ProcessingException, SAXException, IOException { + super.setup(resolver, objectModel, src, parameters); + try{ + availableExports = ItemExport.getExportsAvailable(context.getCurrentUser()); + } + catch (Exception e) { + // nothing to do + } + } + + + + public void addOptions(Options options) throws SAXException, WingException, UIException, SQLException, IOException, AuthorizeException { @@ -183,10 +216,16 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr * even if they are never used */ options.addList("browse"); - options.addList("account"); + List account = options.addList("account"); List context = options.addList("context"); List admin = options.addList("administrative"); + // My Account options + if(availableExports!=null && availableExports.size()>0){ + account.addItem().addXref(contextPath+"/admin/export", T_account_export); + } + + // Context Administrative options DSpaceObject dso = HandleUtil.obtainHandle(objectModel); if (dso instanceof Item) @@ -197,6 +236,7 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr { context.setHead(T_context_head); context.addItem().addXref(contextPath+"/admin/item?itemID="+item.getID(), T_context_edit_item); + context.addItem().addXref(contextPath+"/admin/export?itemID="+item.getID(), T_context_export_item ); } } else if (dso instanceof Collection) @@ -209,7 +249,8 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr { context.setHead(T_context_head); context.addItemXref(contextPath+"/admin/collection?collectionID=" + collection.getID(), T_context_edit_collection); - context.addItemXref(contextPath+"/admin/mapper?collectionID="+collection.getID(), T_context_item_mapper); + context.addItemXref(contextPath+"/admin/mapper?collectionID="+collection.getID(), T_context_item_mapper); + context.addItem().addXref(contextPath+"/admin/export?collectionID="+collection.getID(), T_context_export_collection ); } } else if (dso instanceof Community) @@ -220,7 +261,8 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr if (community.canEditBoolean()) { context.setHead(T_context_head); - context.addItemXref(contextPath+"/admin/community?communityID=" + community.getID(), T_context_edit_community); + context.addItemXref(contextPath+"/admin/community?communityID=" + community.getID(), T_context_edit_community); + context.addItem().addXref(contextPath+"/admin/export?communityID="+community.getID(), T_context_export_community ); } // can they add to this community? diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/DSpaceCocoonServlet.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/DSpaceCocoonServlet.java index 0016fda01c..b9d117a1d9 100644 --- a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/DSpaceCocoonServlet.java +++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/DSpaceCocoonServlet.java @@ -223,7 +223,13 @@ public class DSpaceCocoonServlet extends CocoonServlet finally { // Ensure that the current context is removed from ThreadLocal - ContextMap.removeCurrentContext(); + try { + ContextMap.removeCurrentContext(); + } + catch (NoSuchMethodError nsme) + { + // just ignore this, it means we're using an out of date logging library. + } } } diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/ItemExportDownloadReader.java b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/ItemExportDownloadReader.java new file mode 100644 index 0000000000..7e94be81ff --- /dev/null +++ b/dspace-xmlui/dspace-xmlui-api/src/main/java/org/dspace/app/xmlui/cocoon/ItemExportDownloadReader.java @@ -0,0 +1,299 @@ +/* + * ItemExportDownlaodReader.java + * + * Version: $Revision: 1.5 $ + * + * Date: $Date: 2006/08/08 20:59:54 $ + * + * Copyright (c) 2002, 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.app.xmlui.cocoon; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.avalon.excalibur.pool.Recyclable; +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.environment.Request; +import org.apache.cocoon.environment.Response; +import org.apache.cocoon.environment.SourceResolver; +import org.apache.cocoon.environment.http.HttpEnvironment; +import org.apache.cocoon.environment.http.HttpResponse; +import org.apache.cocoon.reading.AbstractReader; +import org.apache.cocoon.util.ByteRange; +import org.dspace.app.itemexport.ItemExport; +import org.dspace.app.xmlui.utils.AuthenticationUtil; +import org.dspace.app.xmlui.utils.ContextUtil; +import org.dspace.core.Context; +import org.xml.sax.SAXException; + +/** + * @author Jay Paz + */ + +public class ItemExportDownloadReader extends AbstractReader implements Recyclable +{ + + /** + * Messages to be sent when the user is not authorized to view + * a particular bitstream. They will be redirected to the login + * where this message will be displayed. + */ + private final static String AUTH_REQUIRED_HEADER = "xmlui.ItemExportDownloadReader.auth_header"; + private final static String AUTH_REQUIRED_MESSAGE = "xmlui.ItemExportDownloadReader.auth_message"; + + /** + * How big of a buffer should we use when reading from the bitstream before + * writting to the HTTP response? + */ + protected static final int BUFFER_SIZE = 8192; + + /** + * When should a download expire in milliseconds. This should be set to + * some low value just to prevent someone hiting DSpace repeatily from + * killing the server. Note: 60000 milliseconds are in a second. + * + * Format: minutes * seconds * milliseconds + */ + protected static final int expires = 60 * 60 * 60000; + + /** The Cocoon response */ + protected Response response; + + /** The Cocoon request */ + protected Request request; + + /** The bitstream file */ + protected InputStream compressedExportInputStream; + + /** The compressed export's reported size */ + protected long compressedExportSize; + + protected String compressedExportName; + /** + * Set up the export reader. + * + * See the class description for information on configuration options. + */ + public void setup(SourceResolver resolver, Map objectModel, String src, + Parameters par) throws ProcessingException, SAXException, + IOException + { + super.setup(resolver, objectModel, src, par); + + try + { + this.request = ObjectModelHelper.getRequest(objectModel); + this.response = ObjectModelHelper.getResponse(objectModel); + Context context = ContextUtil.obtainContext(objectModel); + + // Get our parameters that identify the bitstream + String fileName = par.getParameter("fileName", null); + + + // Is there a User logged in and does the user have access to read it? + if (!ItemExport.canDownload(context, fileName)) + { + if(this.request.getSession().getAttribute("dspace.current.user.id")!=null){ + // A user is logged in, but they are not authorized to read this bitstream, + // instead of asking them to login again we'll point them to a friendly error + // message that tells them the bitstream is restricted. + String redictURL = request.getContextPath() + "/restricted-resource?name=" + fileName; + + HttpServletResponse httpResponse = (HttpServletResponse) + objectModel.get(HttpEnvironment.HTTP_RESPONSE_OBJECT); + httpResponse.sendRedirect(redictURL); + return; + } + else{ + + // The user does not have read access to this bitstream. Inturrupt this current request + // and then forward them to the login page so that they can be authenticated. Once that is + // successfull they will request will be resumed. + AuthenticationUtil.interruptRequest(objectModel, AUTH_REQUIRED_HEADER, AUTH_REQUIRED_MESSAGE, null); + + // Redirect + String redictURL = request.getContextPath() + "/login"; + + HttpServletResponse httpResponse = (HttpServletResponse) + objectModel.get(HttpEnvironment.HTTP_RESPONSE_OBJECT); + httpResponse.sendRedirect(redictURL); + return; + } + } + + + // Success, bitstream found and the user has access to read it. + // Store these for later retreval: + this.compressedExportInputStream = ItemExport.getExportDownloadInputStream(fileName, context.getCurrentUser()); + this.compressedExportSize = ItemExport.getExportFileSize(fileName); + this.compressedExportName = fileName; + } + catch (Exception e) + { + throw new ProcessingException("Unable to read bitstream.",e); + } + } + + + /** + * Write the actual data out to the response. + * + * Some implementation notes, + * + * 1) We set a short expires time just in the hopes of preventing someone + * from overloading the server by clicking reload a bunch of times. I + * realize that this is nowhere near 100% effective but it may help in some + * cases and shouldn't hurt anything. + * + */ + public void generate() throws IOException, SAXException, + ProcessingException + { + if (this.compressedExportInputStream == null) + return; + + byte[] buffer = new byte[BUFFER_SIZE]; + int length = -1; + + response.setDateHeader("Expires", System.currentTimeMillis() + + expires); + response.setHeader("Content-disposition","attachement; filename=" + this.compressedExportName ); + // Turn off partial downloads, they cause problems + // and are only rarely used. Specifically some windows pdf + // viewers are incapable of handling this request. By + // uncommenting the following two lines you will turn this feature back on. + // response.setHeader("Accept-Ranges", "bytes"); + // String ranges = request.getHeader("Range"); + String ranges = null; + + + ByteRange byteRange = null; + if (ranges != null) + { + try + { + ranges = ranges.substring(ranges.indexOf('=') + 1); + byteRange = new ByteRange(ranges); + } + catch (NumberFormatException e) + { + byteRange = null; + if (response instanceof HttpResponse) + { + // Respond with status 416 (Request range not + // satisfiable) + ((HttpResponse) response).setStatus(416); + } + } + } + + if (byteRange != null) + { + String entityLength; + String entityRange; + if (this.compressedExportSize != -1) + { + entityLength = "" + this.compressedExportSize; + entityRange = byteRange.intersection( + new ByteRange(0, this.compressedExportSize)).toString(); + } + else + { + entityLength = "*"; + entityRange = byteRange.toString(); + } + + response.setHeader("Content-Range", entityRange + "/" + + entityLength); + if (response instanceof HttpResponse) + { + // Response with status 206 (Partial content) + ((HttpResponse) response).setStatus(206); + } + + int pos = 0; + int posEnd; + while ((length = this.compressedExportInputStream.read(buffer)) > -1) + { + posEnd = pos + length - 1; + ByteRange intersection = byteRange + .intersection(new ByteRange(pos, posEnd)); + if (intersection != null) + { + out.write(buffer, (int) intersection.getStart() + - pos, (int) intersection.length()); + } + pos += length; + } + } + else + { + response.setHeader("Content-Length", String + .valueOf(this.compressedExportSize)); + + while ((length = this.compressedExportInputStream.read(buffer)) > -1) + { + out.write(buffer, 0, length); + } + out.flush(); + } + } + + /** + * Returns the mime-type of the bitstream. + */ + public String getMimeType() + { + return ItemExport.COMPRESSED_EXPORT_MIME_TYPE; + } + + /** + * Recycle + */ + public void recycle() { + this.response = null; + this.request = null; + this.compressedExportInputStream = null; + this.compressedExportSize = 0; + + } + + +} diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Administrative/sitemap.xmap b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Administrative/sitemap.xmap index ae23dd227f..6ea3471041 100644 --- a/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Administrative/sitemap.xmap +++ b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/Administrative/sitemap.xmap @@ -105,6 +105,7 @@ to administer DSpace. + @@ -777,6 +778,10 @@ to administer DSpace. + + + + @@ -796,6 +801,10 @@ to administer DSpace. + + + + diff --git a/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/ArtifactBrowser/sitemap.xmap b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/ArtifactBrowser/sitemap.xmap index 056a10c02a..8afcebb8a8 100644 --- a/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/ArtifactBrowser/sitemap.xmap +++ b/dspace-xmlui/dspace-xmlui-api/src/main/resources/aspects/ArtifactBrowser/sitemap.xmap @@ -161,6 +161,14 @@ and searching the repository. + + + + + + + + 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 d40b237b72..e9f878ad2a 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 @@ -320,7 +320,9 @@ The file is restricted The file you are attempting to access is a restricted file and requires credentials to view. Please login below to access the file. - + + This export archive is restricted. + The export archive you are attempting to access is a restricted resource and requires credentials to view. Please login below to access the export archive. Administrative @@ -811,6 +816,24 @@ Control Panel Statistics + + My Exports + + + + Export Archive + The item id provided is invalid. + The collection id provided is invalid. + The community id provided is invalid + The item requested was not found. + The collection requested was not found. + The community requested was not found. + The item was exported successfully. You should receive an e-mail when the archive is ready for download. You can also use the 'My Exports' link to view a list of your available archives. + The collection was exported successfully. You should receive an e-mail when the archive is ready for download. You can also use the 'My Exports' link to view a list of your available archives. + The community was exported successfully. You should receive an e-mail when the archive is ready for download. You can also use the 'My Exports' link to view a list of your available archives. + Available export archives for download: + + Manage E-people diff --git a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/sitemap.xmap b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/sitemap.xmap index 1d924f5bdd..f9911f9269 100644 --- a/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/sitemap.xmap +++ b/dspace-xmlui/dspace-xmlui-webapp/src/main/webapp/sitemap.xmap @@ -158,6 +158,7 @@ 3600000 + @@ -261,6 +262,16 @@ + + + + + + + + diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 2fe98edafb..8a341b6f9a 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -578,6 +578,27 @@ checker.retention.default=10y checker.retention.CHECKSUM_MATCH=8w +### Item export and download settings ### +# The directory where the exports will be done and compressed +org.dspace.app.itemexport.work.dir = ${dspace.dir}/exports + +# The directory where the compressed files will reside and be read by the downloader +org.dspace.app.itemexport.download.dir = ${dspace.dir}/exports/download + +# The length og time in hours each archive should live for. When new archives are +# created this entry is used to delete old ones +org.dspace.app.itemexport.life.span.hours = 48 + +# The maximum size in Megabytes the export should be. This is enforced before the +# compression. Each bitstream's size in each item being exported is added up, if their +# cummulative sizes are more than this entry the export is not kicked off +org.dspace.app.itemexport.max.size = 200 + + + + + + #---------------------------------------------------------------# #--------------JSPUI & XMLUI CONFIGURATIONS---------------------# #---------------------------------------------------------------# diff --git a/dspace/config/dstat.map b/dspace/config/dstat.map index 82776a69dc..140049ee13 100644 --- a/dspace/config/dstat.map +++ b/dspace/config/dstat.map @@ -99,4 +99,5 @@ remove_subcommunity=Sub Community Removed show_feedback_form=Feedback Form Displayed create_dc_type=New Dublin Core Type Created remove_template_item=Item Template Removed -withdraw_item=Item Withdrawn \ No newline at end of file +withdraw_item=Item Withdrawn +download_export_archive = Download Export Archive \ No newline at end of file diff --git a/dspace/modules/jspui/src/main/resources/Messages.properties b/dspace/modules/jspui/src/main/resources/Messages.properties index 803922f4bf..3c6e86f70e 100644 --- a/dspace/modules/jspui/src/main/resources/Messages.properties +++ b/dspace/modules/jspui/src/main/resources/Messages.properties @@ -598,6 +598,7 @@ jsp.mydspace.main.heading3 = Tasks in the P jsp.mydspace.main.heading4 = Unfinished Submissions jsp.mydspace.main.heading5 = Submissions In Workflow Process jsp.mydspace.main.heading6 = Authorization Groups I'm a Member Of +jsp.mydspace.main.heading7 = Export Archives Available for Download jsp.mydspace.main.item = Item jsp.mydspace.main.link = See Your Subscriptions jsp.mydspace.main.perform.button = Perform This Task @@ -656,6 +657,7 @@ jsp.mydspace.subscriptions.title = Your Subscript jsp.mydspace.subscriptions.unsub.button = Unsubscribe jsp.mydspace.task-complete.text1 = The task is complete, and notification has been sent to the appropriate people. jsp.mydspace.task-complete.title = Thank You +jsp.mydspace.main.export.archive.title = Download Export Archive {0} jsp.register.already-registered.info1 = Our records show that you've already registered with DSpace and have an active account with us. jsp.register.already-registered.info2 = You can set a new password if you've forgotten it. jsp.register.already-registered.info4 = If you're having trouble logging in, please contact us.