/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.packager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.DSpaceObject;
import org.dspace.content.crosswalk.CrosswalkException;
import org.dspace.content.packager.PackageDisseminator;
import org.dspace.content.packager.PackageException;
import org.dspace.content.packager.PackageParameters;
import org.dspace.content.packager.PackageIngester;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.PluginManager;
import org.dspace.eperson.EPerson;
import org.dspace.handle.HandleManager;
/**
* Command-line interface to the Packager plugin.
* <p>
* This class ONLY exists to provide a CLI for the packager plugins. It does not
* "manage" the plugins and it is not called from within DSpace, but the name
* follows a DSpace convention.
* <p>
* It can invoke one of the Submission (SIP) packagers to create a new DSpace
* Item out of a package, or a Dissemination (DIP) packager to write an Item out
* as a package.
* <p>
* Usage is as follows:<br>
* (Add the -h option to get the command to show its own help)
*
* <pre>
* 1. To submit a SIP (submissions tend to create a *new* object, with a new handle. If you want to restore an object, see -r option below)
* dspace packager
* -e {ePerson}
* -t {PackagerType}
* -p {parent-handle} [ -p {parent2} ...]
* [-o {name}={value} [ -o {name}={value} ..]]
* [-a] --- also recursively ingest all child packages of the initial package
* (child pkgs must be referenced from parent pkg)
* [-w] --- skip Workflow
* {package-filename}
*
* {PackagerType} must match one of the aliases of the chosen Packager
* plugin.
*
* The "-w" option circumvents Workflow, and is optional. The "-o"
* option, which may be repeated, passes options to the packager
* (e.g. "metadataOnly" to a DIP packager).
*
* 2. To restore an AIP (similar to submit mode, but attempts to restore with the handles/parents specified in AIP):
* dspace packager
* -r --- restores a object from a package info, including the specified handle (will throw an error if handle is already in use)
* -e {ePerson}
* -t {PackagerType}
* [-o {name}={value} [ -o {name}={value} ..]]
* [-a] --- also recursively restore all child packages of the initial package
* (child pkgs must be referenced from parent pkg)
* [-k] --- Skip over errors where objects already exist and Keep Existing objects by default.
* Use with -r to only restore objects which do not already exist. By default, -r will throw an error
* and rollback all changes when an object is found that already exists.
* [-f] --- Force a restore (even if object already exists).
* Use with -r to replace an existing object with one from a package (essentially a delete and restore).
* By default, -r will throw an error and rollback all changes when an object is found that already exists.
* [-i {identifier-handle-of-object}] -- Optional when -f is specified. When replacing an object, you can specify the
* object to replace if it cannot be easily determined from the package itself.
* {package-filename}
*
* Restoring is very similar to submitting, except that you are recreating pre-existing objects. So, in a restore, the object(s) are
* being recreated based on the details in the AIP. This means that the object is recreated with the same handle and same parent/children
* objects. Not all {PackagerTypes} may support a "restore".
*
* 3. To write out a DIP:
* dspace packager
* -d
* -e {ePerson}
* -t {PackagerType}
* -i {identifier-handle-of-object}
* [-a] --- also recursively disseminate all child objects of this object
* [-o {name}={value} [ -o {name}={value} ..]]
* {package-filename}
*
* The "-d" switch chooses a Dissemination packager, and is required.
* The "-o" option, which may be repeated, passes options to the packager
* (e.g. "metadataOnly" to a DIP packager).
* </pre>
*
* Note that {package-filename} may be "-" for standard input or standard
* output, respectively.
*
* @author Larry Stone
* @author Tim Donohue
* @version $Revision$
*/
public class Packager
{
/* Various private global settings/options */
private String packageType = null;
private boolean submit = true;
private boolean userInteractionEnabled = true;
// die from illegal command line
private static void usageError(String msg)
{
System.out.println(msg);
System.out.println(" (run with -h flag for details)");
System.exit(1);
}
public static void main(String[] argv) throws Exception
{
Options options = new Options();
options.addOption("p", "parent", true,
"Handle(s) of parent Community or Collection into which to ingest object (repeatable)");
options.addOption("e", "eperson", true,
"email address of eperson doing importing");
options
.addOption(
"w",
"install",
false,
"disable workflow; install immediately without going through collection's workflow");
options.addOption("r", "restore", false, "ingest in \"restore\" mode. Restores a missing object based on the contents in a package.");
options.addOption("k", "keep-existing", false, "if an object is found to already exist during a restore (-r), then keep the existing object and continue processing. Can only be used with '-r'. This avoids object-exists errors which are thrown by -r by default.");
options.addOption("f", "force-replace", false, "if an object is found to already exist during a restore (-r), then remove it and replace it with the contents of the package. Can only be used with '-r'. This REPLACES the object(s) in the repository with the contents from the package(s).");
options.addOption("t", "type", true, "package type or MIMEtype");
options
.addOption("o", "option", true,
"Packager option to pass to plugin, \"name=value\" (repeatable)");
options.addOption("d", "disseminate", false,
"Disseminate package (output); default is to submit.");
options.addOption("s", "submit", false,
"Submission package (Input); this is the default. ");
options.addOption("i", "identifier", true, "Handle of object to disseminate.");
options.addOption("a", "all", false, "also recursively ingest/disseminate any child packages, e.g. all Items within a Collection (not all packagers may support this option!)");
options.addOption("h", "help", false, "help (you may also specify '-h -t [type]' for additional help with a specific type of packager)");
options.addOption("u", "no-user-interaction", false, "Skips over all user interaction (i.e. [y/n] question prompts) within this script. This flag can be used if you want to save (pipe) a report of all changes to a file, and therefore need to bypass all user interaction.");
CommandLineParser parser = new PosixParser();
CommandLine line = parser.parse(options, argv);
String sourceFile = null;
String eperson = null;
String[] parents = null;
String identifier = null;
PackageParameters pkgParams = new PackageParameters();
//initialize a new packager -- we'll add all our current params as settings
Packager myPackager = new Packager();
if (line.hasOption('h'))
{
HelpFormatter myhelp = new HelpFormatter();
myhelp.printHelp("Packager [options] package-file|-\n",
options);
//If user specified a type, also print out the SIP and DIP options
// that are specific to that type of packager
if (line.hasOption('t'))
{
System.out.println("\n--------------------------------------------------------------");
System.out.println("Additional options for the " + line.getOptionValue('t') + " packager:");
System.out.println("--------------------------------------------------------------");
System.out.println("(These options may be specified using --option as described above)");
PackageIngester sip = (PackageIngester) PluginManager
.getNamedPlugin(PackageIngester.class, line.getOptionValue('t'));
if (sip != null)
{
System.out.println("\n\n" + line.getOptionValue('t') + " Submission (SIP) plugin options:\n");
System.out.println(sip.getParameterHelp());
}
else
{
System.out.println("\nNo valid Submission plugin found for " + line.getOptionValue('t') + " type.");
}
PackageDisseminator dip = (PackageDisseminator) PluginManager
.getNamedPlugin(PackageDisseminator.class, line.getOptionValue('t'));
if (dip != null)
{
System.out.println("\n\n" + line.getOptionValue('t') + " Dissemination (DIP) plugin options:\n");
System.out.println(dip.getParameterHelp());
}
else
{
System.out.println("\nNo valid Dissemination plugin found for " + line.getOptionValue('t') + " type.");
}
}
else //otherwise, display list of valid packager types
{
System.out.println("\nAvailable Submission Package (SIP) types:");
String pn[] = PluginManager
.getAllPluginNames(PackageIngester.class);
for (int i = 0; i < pn.length; ++i)
{
System.out.println(" " + pn[i]);
}
System.out
.println("\nAvailable Dissemination Package (DIP) types:");
pn = PluginManager.getAllPluginNames(PackageDisseminator.class);
for (int i = 0; i < pn.length; ++i)
{
System.out.println(" " + pn[i]);
}
}
System.exit(0);
}
//look for flag to disable all user interaction
if(line.hasOption('u'))
{
myPackager.userInteractionEnabled = false;
}
if (line.hasOption('w'))
{
pkgParams.setWorkflowEnabled(false);
}
if (line.hasOption('r'))
{
pkgParams.setRestoreModeEnabled(true);
}
//keep-existing is only valid in restoreMode (-r) -- otherwise ignore -k option.
if (line.hasOption('k') && pkgParams.restoreModeEnabled())
{
pkgParams.setKeepExistingModeEnabled(true);
}
//force-replace is only valid in restoreMode (-r) -- otherwise ignore -f option.
if (line.hasOption('f') && pkgParams.restoreModeEnabled())
{
pkgParams.setReplaceModeEnabled(true);
}
if (line.hasOption('e'))
{
eperson = line.getOptionValue('e');
}
if (line.hasOption('p'))
{
parents = line.getOptionValues('p');
}
if (line.hasOption('t'))
{
myPackager.packageType = line.getOptionValue('t');
}
if (line.hasOption('i'))
{
identifier = line.getOptionValue('i');
}
if (line.hasOption('a'))
{
//enable 'recursiveMode' param to packager implementations, in case it helps with packaging or ingestion process
pkgParams.setRecursiveModeEnabled(true);
}
String files[] = line.getArgs();
if (files.length > 0)
{
sourceFile = files[0];
}
if (line.hasOption('d'))
{
myPackager.submit = false;
}
if (line.hasOption('o'))
{
String popt[] = line.getOptionValues('o');
for (int i = 0; i < popt.length; ++i)
{
String pair[] = popt[i].split("\\=", 2);
if (pair.length == 2)
{
pkgParams.addProperty(pair[0].trim(), pair[1].trim());
}
else if (pair.length == 1)
{
pkgParams.addProperty(pair[0].trim(), "");
}
else
{
System.err
.println("Warning: Illegal package option format: \""
+ popt[i] + "\"");
}
}
}
// Sanity checks on arg list: required args
// REQUIRED: sourceFile, ePerson (-e), packageType (-t)
if (sourceFile == null || eperson == null || myPackager.packageType == null)
{
System.err.println("Error - missing a REQUIRED argument or option.\n");
HelpFormatter myhelp = new HelpFormatter();
myhelp.printHelp("PackageManager [options] package-file|-\n", options);
System.exit(0);
}
// find the EPerson, assign to context
Context context = new Context();
EPerson myEPerson = null;
myEPerson = EPerson.findByEmail(context, eperson);
if (myEPerson == null)
{
usageError("Error, eperson cannot be found: " + eperson);
}
context.setCurrentUser(myEPerson);
//If we are in REPLACE mode
if(pkgParams.replaceModeEnabled())
{
PackageIngester sip = (PackageIngester) PluginManager
.getNamedPlugin(PackageIngester.class, myPackager.packageType);
if (sip == null)
{
usageError("Error, Unknown package type: " + myPackager.packageType);
}
DSpaceObject objToReplace = null;
//if a specific identifier was specified, make sure it is valid
if(identifier!=null && identifier.length()>0)
{
objToReplace = HandleManager.resolveToObject(context, identifier);
if (objToReplace == null)
{
throw new IllegalArgumentException("Bad identifier/handle -- "
+ "Cannot resolve handle \"" + identifier + "\"");
}
}
String choiceString = null;
if(myPackager.userInteractionEnabled)
{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
System.out.println("\n\nWARNING -- You are running the packager in REPLACE mode.");
System.out.println("\nREPLACE mode may be potentially dangerous as it will automatically remove and replace contents within DSpace.");
System.out.println("We highly recommend backing up all your DSpace contents (files & database) before continuing.");
System.out.print("\nWould you like to continue? [y/n]: ");
choiceString = input.readLine();
}
else
{
//user interaction disabled -- default answer to 'yes', otherwise script won't continue
choiceString = "y";
}
if (choiceString.equalsIgnoreCase("y"))
{
System.out.println("Beginning replacement process...");
try
{
//replace the object from the source file
myPackager.replace(context, sip, pkgParams, sourceFile, objToReplace);
//commit all changes & exit successfully
context.complete();
System.exit(0);
}
catch (Exception e)
{
// abort all operations
e.printStackTrace();
context.abort();
System.out.println(e);
System.exit(1);
}
}
}
//else if normal SUBMIT mode (or basic RESTORE mode -- which is a special type of submission)
else if (myPackager.submit || pkgParams.restoreModeEnabled())
{
PackageIngester sip = (PackageIngester) PluginManager
.getNamedPlugin(PackageIngester.class, myPackager.packageType);
if (sip == null)
{
usageError("Error, Unknown package type: " + myPackager.packageType);
}
// validate each parent arg (if any)
DSpaceObject parentObjs[] = null;
if(parents!=null)
{
System.out.println("Destination parents:");
parentObjs = new DSpaceObject[parents.length];
for (int i = 0; i < parents.length; i++)
{
// sanity check: did handle resolve?
parentObjs[i] = HandleManager.resolveToObject(context,
parents[i]);
if (parentObjs[i] == null)
{
throw new IllegalArgumentException(
"Bad parent list -- "
+ "Cannot resolve parent handle \""
+ parents[i] + "\"");
}
System.out.println((i == 0 ? "Owner: " : "Parent: ")
+ parentObjs[i].getHandle());
}
}
try
{
//ingest the object from the source file
myPackager.ingest(context, sip, pkgParams, sourceFile, parentObjs);
//commit all changes & exit successfully
context.complete();
System.exit(0);
}
catch (Exception e)
{
// abort all operations
e.printStackTrace();
context.abort();
System.out.println(e);
System.exit(1);
}
}// else, if DISSEMINATE mode
else
{
//retrieve specified package disseminator
PackageDisseminator dip = (PackageDisseminator) PluginManager
.getNamedPlugin(PackageDisseminator.class, myPackager.packageType);
if (dip == null)
{
usageError("Error, Unknown package type: " + myPackager.packageType);
}
DSpaceObject dso = HandleManager.resolveToObject(context, identifier);
if (dso == null)
{
throw new IllegalArgumentException("Bad identifier/handle -- "
+ "Cannot resolve handle \"" + identifier + "\"");
}
//disseminate the requested object
myPackager.disseminate(context, dip, dso, pkgParams, sourceFile);
}
System.exit(0);
}
/**
* Ingest one or more DSpace objects from package(s) based on the
* options passed to the 'packager' script. This method is called
* for both 'submit' (-s) and 'restore' (-r) modes.
* <p>
* Please note that replace (-r -f) mode calls the replace() method instead.
*
* @param context DSpace Context
* @param sip PackageIngester which will actually ingest the package
* @param pkgParams Parameters to pass to individual packager instances
* @param sourceFile location of the source package to ingest
* @param parentObjs Parent DSpace object(s) to attach new object to
* @throws IOException
* @throws SQLException
* @throws FileNotFoundException
* @throws AuthorizeException
* @throws CrosswalkException
* @throws PackageException
*/
protected void ingest(Context context, PackageIngester sip, PackageParameters pkgParams, String sourceFile, DSpaceObject parentObjs[])
throws IOException, SQLException, FileNotFoundException, AuthorizeException, CrosswalkException, PackageException
{
// make sure we have an input file
File pkgFile = new File(sourceFile);
if(!pkgFile.exists())
{
System.out.println("\nERROR: Package located at " + sourceFile + " does not exist!");
System.exit(1);
}
System.out.println("\nIngesting package located at " + sourceFile);
//find first parent (if specified) -- this will be the "owner" of the object
DSpaceObject parent = null;
if(parentObjs!=null && parentObjs.length>0)
{
parent = parentObjs[0];
}
//NOTE: at this point, Parent may be null -- in which case it is up to the PackageIngester
// to either determine the Parent (from package contents) or throw an error.
//If we are doing a recursive ingest, call ingestAll()
if(pkgParams.recursiveModeEnabled())
{
System.out.println("\nAlso ingesting all referenced packages (recursive mode)..");
System.out.println("This may take a while, please check your logs for ongoing status while we process each package.");
//ingest first package & recursively ingest anything else that package references (child packages, etc)
List<String> hdlResults = sip.ingestAll(context, parent, pkgFile, pkgParams, null);
if(hdlResults!=null)
{
//Report total objects created
System.out.println("\nCREATED a total of " + hdlResults.size() + " DSpace Objects.");
String choiceString = null;
//Ask if user wants full list printed to command line, as this may be rather long.
if(this.userInteractionEnabled)
{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
System.out.print("\nWould you like to view a list of all objects that were created? [y/n]: ");
choiceString = input.readLine();
}
else
{
// user interaction disabled -- default answer to 'yes', as
// we want to provide user with as detailed a report as possible.
choiceString = "y";
}
// Provide detailed report if user answered 'yes'
if (choiceString.equalsIgnoreCase("y"))
{
System.out.println("\n\n");
for(String result : hdlResults)
{
DSpaceObject dso = HandleManager.resolveToObject(context, result);
if(dso!=null)
{
if(pkgParams.restoreModeEnabled())
{
System.out.println("RESTORED DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + ", dbID=" + dso.getID() + " ] ");
}
else
{
System.out.println("CREATED new DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + ", dbID=" + dso.getID() + " ] ");
}
}
}
}
}
}
else
{
//otherwise, just one package to ingest
try
{
DSpaceObject dso = sip.ingest(context, parent, pkgFile, pkgParams, null);
if(dso!=null)
{
if(pkgParams.restoreModeEnabled())
{
System.out.println("RESTORED DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + ", dbID=" + dso.getID() + " ] ");
}
else
{
System.out.println("CREATED new DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + ", dbID=" + dso.getID() + " ] ");
}
}
}
catch(IllegalStateException ie)
{
// NOTE: if we encounter an IllegalStateException, this means the
// handle is already in use and this object already exists.
//if we are skipping over (i.e. keeping) existing objects
if(pkgParams.keepExistingModeEnabled())
{
System.out.println("\nSKIPPED processing package '" + pkgFile + "', as an Object already exists with this handle.");
}
else // Pass this exception on -- which essentially causes a full rollback of all changes (this is the default)
{
throw ie;
}
}
}
}
/**
* Disseminate one or more DSpace objects into package(s) based on the
* options passed to the 'packager' script
*
* @param context DSpace context
* @param dip PackageDisseminator which will actually create the package
* @param dso DSpace Object to disseminate as a package
* @param pkgParams Parameters to pass to individual packager instances
* @param outputFile File where final package should be saved
* @throws IOException
* @throws SQLException
* @throws FileNotFoundException
* @throws AuthorizeException
* @throws CrosswalkException
* @throws PackageException
*/
protected void disseminate(Context context, PackageDisseminator dip,
DSpaceObject dso, PackageParameters pkgParams,
String outputFile)
throws IOException, SQLException, FileNotFoundException, AuthorizeException, CrosswalkException, PackageException
{
// initialize output file
File pkgFile = new File(outputFile);
System.out.println("\nDisseminating DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + " ] to " + outputFile);
//If we are doing a recursive dissemination of this object & all its child objects, call disseminateAll()
if(pkgParams.recursiveModeEnabled())
{
System.out.println("\nAlso disseminating all child objects (recursive mode)..");
System.out.println("This may take a while, please check your logs for ongoing status while we process each package.");
//disseminate initial object & recursively disseminate all child objects as well
List<File> fileResults = dip.disseminateAll(context, dso, pkgParams, pkgFile);
if(fileResults!=null)
{
//Report total files created
System.out.println("\nCREATED a total of " + fileResults.size() + " dissemination package files.");
String choiceString = null;
//Ask if user wants full list printed to command line, as this may be rather long.
if(this.userInteractionEnabled)
{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
System.out.print("\nWould you like to view a list of all files that were created? [y/n]: ");
choiceString = input.readLine();
}
else
{
// user interaction disabled -- default answer to 'yes', as
// we want to provide user with as detailed a report as possible.
choiceString = "y";
}
// Provide detailed report if user answered 'yes'
if (choiceString.equalsIgnoreCase("y"))
{
System.out.println("\n\n");
for(File result : fileResults)
{
System.out.println("CREATED package file: " + result.getCanonicalPath());
}
}
}
}
else
{
//otherwise, just disseminate a single object to a single package file
dip.disseminate(context, dso, pkgParams, pkgFile);
if(pkgFile!=null && pkgFile.exists())
{
System.out.println("\nCREATED package file: " + pkgFile.getCanonicalPath());
}
}
}
/**
* Replace an one or more existing DSpace objects with the contents of
* specified package(s) based on the options passed to the 'packager' script.
* This method is only called for full replaces ('-r -f' options specified)
*
* @param context DSpace Context
* @param sip PackageIngester which will actually replace the object with the package
* @param pkgParams Parameters to pass to individual packager instances
* @param sourceFile location of the source package to ingest as the replacement
* @param objToReplace DSpace object to replace (may be null if it will be specified in the package itself)
* @throws IOException
* @throws SQLException
* @throws FileNotFoundException
* @throws AuthorizeException
* @throws CrosswalkException
* @throws PackageException
*/
protected void replace(Context context, PackageIngester sip, PackageParameters pkgParams, String sourceFile, DSpaceObject objToReplace)
throws IOException, SQLException, FileNotFoundException, AuthorizeException, CrosswalkException, PackageException
{
// make sure we have an input file
File pkgFile = new File(sourceFile);
if(!pkgFile.exists())
{
System.out.println("\nPackage located at " + sourceFile + " does not exist!");
System.exit(1);
}
System.out.println("\nReplacing DSpace object(s) with package located at " + sourceFile);
if(objToReplace!=null)
{
System.out.println("Will replace existing DSpace " + Constants.typeText[objToReplace.getType()] +
" [ hdl=" + objToReplace.getHandle() + " ]");
}
// NOTE: At this point, objToReplace may be null. If it is null, it is up to the PackageIngester
// to determine which Object needs to be replaced (based on the handle specified in the pkg, etc.)
//If we are doing a recursive replace, call replaceAll()
if(pkgParams.recursiveModeEnabled())
{
//ingest first object using package & recursively replace anything else that package references (child objects, etc)
List<String> hdlResults = sip.replaceAll(context, objToReplace, pkgFile, pkgParams);
if(hdlResults!=null)
{
//Report total objects replaced
System.out.println("\nREPLACED a total of " + hdlResults.size() + " DSpace Objects.");
String choiceString = null;
//Ask if user wants full list printed to command line, as this may be rather long.
if(this.userInteractionEnabled)
{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
System.out.print("\nWould you like to view a list of all objects that were replaced? [y/n]: ");
choiceString = input.readLine();
}
else
{
// user interaction disabled -- default answer to 'yes', as
// we want to provide user with as detailed a report as possible.
choiceString = "y";
}
// Provide detailed report if user answered 'yes'
if (choiceString.equalsIgnoreCase("y"))
{
System.out.println("\n\n");
for(String result : hdlResults)
{
DSpaceObject dso = HandleManager.resolveToObject(context, result);
if(dso!=null)
{
System.out.println("REPLACED DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + " ] ");
}
}
}
}
}
else
{
//otherwise, just one object to replace
DSpaceObject dso = sip.replace(context, objToReplace, pkgFile, pkgParams);
if(dso!=null)
{
System.out.println("REPLACED DSpace " + Constants.typeText[dso.getType()] +
" [ hdl=" + dso.getHandle() + " ] ");
}
}
}
}