Index: Packager.java =================================================================== --- Packager.java (.../dspace/trunk/dspace-api/src/main/java/org/dspace/app/packager) (revision 5257) +++ Packager.java (.../sandbox/aip-external-1_6-prototype/dspace-api/src/main/java/org/dspace/app/packager) (revision 5257) @@ -38,22 +38,24 @@ package org.dspace.app.packager; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; +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.content.Collection; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; -import org.dspace.content.InstallItem; -import org.dspace.content.WorkspaceItem; +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; @@ -61,8 +63,6 @@ import org.dspace.core.PluginManager; import org.dspace.eperson.EPerson; import org.dspace.handle.HandleManager; -import org.dspace.workflow.WorkflowManager; -import org.dspace.workflow.WorkflowItem; /** * Command-line interface to the Packager plugin. @@ -79,13 +79,15 @@ * (Add the -h option to get the command to show its own help) * *
- * 1. To submit a SIP: - * java org.dspace.app.packager.Packager + * 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} - * -c {collection-handle} [ -c {collection} ...] - * -o {name}={value} [ -o {name}={value} ..] - * [-w] + * -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 @@ -94,14 +96,37 @@ * 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} * - * 2. To write out a DIP: - * java org.dspace.content.packager.Packager + * 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 {item-handle} - * -o {name}={value} [ -o {name}={value} ..] + * -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. @@ -113,10 +138,16 @@ * 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) { @@ -128,8 +159,8 @@ public static void main(String[] argv) throws Exception { Options options = new Options(); - options.addOption("c", "collection", true, - "destination collection(s) Handle (repeatable)"); + 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 @@ -138,27 +169,34 @@ "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("i", "item", true, "Handle of item to disseminate."); + 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"); + 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[] collections = null; - boolean useWorkflow = true; - String packageType = null; - boolean submit = true; - String itemHandle = 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(); @@ -176,21 +214,38 @@ 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')) - useWorkflow = false; + 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('c')) - collections = line.getOptionValues('c'); + if (line.hasOption('p')) + parents = line.getOptionValues('p'); if (line.hasOption('t')) - packageType = line.getOptionValue('t'); + myPackager.packageType = line.getOptionValue('t'); if (line.hasOption('i')) - itemHandle = line.getOptionValue('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')) - submit = false; + myPackager.submit = false; if (line.hasOption('o')) { String popt[] = line.getOptionValues('o'); @@ -209,8 +264,8 @@ } // Sanity checks on arg list: required args - if (sourceFile == null || eperson == null || packageType == null - || (submit && collections == null)) + // 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"); @@ -228,102 +283,425 @@ usageError("Error, eperson cannot be found: " + eperson); context.setCurrentUser(myEPerson); - if (submit) + + //If we are in REPLACE mode + if(pkgParams.replaceModeEnabled()) { - // make sure we have an input file - InputStream source = (sourceFile.equals("-")) ? System.in - : new FileInputStream(sourceFile); - PackageIngester sip = (PackageIngester) PluginManager - .getNamedPlugin(PackageIngester.class, packageType); + .getNamedPlugin(PackageIngester.class, myPackager.packageType); if (sip == null) - usageError("Error, Unknown package type: " + packageType); + usageError("Error, Unknown package type: " + myPackager.packageType); - // find collections - Collection[] mycollections = null; + DSpaceObject objToReplace = null; - System.out.println("Destination collections:"); + //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 +"\""); + } - // validate each collection arg to see if it's a real collection - mycollections = new Collection[collections.length]; - for (int i = 0; i < collections.length; i++) + String choiceString = null; + if(myPackager.userInteractionEnabled) { - // sanity check: did handle resolve, and to a collection? - DSpaceObject dso = HandleManager.resolveToObject(context, - collections[i]); - if (dso == null) - throw new IllegalArgumentException( - "Bad collection list -- " - + "Cannot resolve collection handle \"" - + collections[i] + "\""); - else if (dso.getType() != Constants.COLLECTION) - throw new IllegalArgumentException( - "Bad collection list -- " + "Object at handle \"" - + collections[i] - + "\" is not a collection!"); - mycollections[i] = (Collection) dso; - System.out.println((i == 0 ? " Owning " : " ") - + " Collection: " - + mycollections[i].getMetadata("name")); + 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"; + } - try + if (choiceString.equalsIgnoreCase("y")) { - WorkspaceItem wi = sip.ingest(context, mycollections[0], - source, pkgParams, null); - if (useWorkflow) + System.out.println("Beginning replacement process..."); + + try { - String handle = null; + //replace the object from the source file + myPackager.replace(context, sip, pkgParams, sourceFile, objToReplace); - // Check if workflow completes immediately, and - // return Handle if so. - WorkflowItem wfi = WorkflowManager.startWithoutNotify(context, wi); - - if (wfi.getState() == WorkflowManager.WFSTATE_ARCHIVE) - { - Item ni = wfi.getItem(); - handle = HandleManager.findHandle(context, ni); - } - if (handle == null) - System.out.println("Created Workflow item, ID=" - + String.valueOf(wfi.getID())); - else - System.out.println("Created and installed item, handle="+handle); + //commit all changes & exit successfully + context.complete(); + System.exit(0); } - else + catch (Exception e) { - InstallItem.installItem(context, wi); - System.out.println("Created and installed item, handle=" - + HandleManager.findHandle(context, wi.getItem())); + // 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(); - e.printStackTrace(); System.out.println(e); System.exit(1); } - } + }// else, if DISSEMINATE mode else { - OutputStream dest = (sourceFile.equals("-")) ? (OutputStream) System.out - : (OutputStream) (new FileOutputStream(sourceFile)); - + //retrieve specified package disseminator PackageDisseminator dip = (PackageDisseminator) PluginManager - .getNamedPlugin(PackageDisseminator.class, packageType); + .getNamedPlugin(PackageDisseminator.class, myPackager.packageType); if (dip == null) - usageError("Error, Unknown package type: " + packageType); + usageError("Error, Unknown package type: " + myPackager.packageType); - DSpaceObject dso = HandleManager.resolveToObject(context, - itemHandle); + DSpaceObject dso = HandleManager.resolveToObject(context, identifier); if (dso == null) - throw new IllegalArgumentException("Bad Item handle -- " - + "Cannot resolve handle \"" + itemHandle); - dip.disseminate(context, dso, pkgParams, dest); + 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. + *+ * 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
dsoResults = sip.ingestAll(context, parent, pkgFile, pkgParams, null); + + if(dsoResults!=null) + { + //Report total objects created + System.out.println("\nCREATED a total of " + dsoResults.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(DSpaceObject result : dsoResults) + { + if(pkgParams.restoreModeEnabled()) + System.out.println("RESTORED DSpace " + Constants.typeText[result.getType()] + + " [ hdl=" + result.getHandle() + " ] "); + else + System.out.println("CREATED new DSpace " + Constants.typeText[result.getType()] + + " [ hdl=" + result.getHandle() + " ] "); + } + } + + } + } + 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() + " ] "); + else + System.out.println("CREATED new DSpace " + Constants.typeText[dso.getType()] + + " [ hdl=" + dso.getHandle() + " ] "); + } + } + 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 + * @param identifier identifier of main DSpace object to disseminate + * @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 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 dsoResults = sip.replaceAll(context, objToReplace, pkgFile, pkgParams); + + if(dsoResults!=null) + { + //Report total objects replaced + System.out.println("\nREPLACED a total of " + dsoResults.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(DSpaceObject result : dsoResults) + { + System.out.println("REPLACED new DSpace " + Constants.typeText[result.getType()] + + " [ hdl=" + result.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() + " ] "); + } + } + }