diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml
index 1a7326fa75..329255b438 100644
--- a/dspace-api/pom.xml
+++ b/dspace-api/pom.xml
@@ -757,6 +757,13 @@
json
20180130
+
+
+ org.xmlunit
+ xmlunit-matchers
+ 2.6.2
+ test
+
diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java
index f39803c2b5..2e71a6e3fb 100644
--- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java
+++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java
@@ -7,12 +7,16 @@
*/
package org.dspace.administer;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -29,6 +33,9 @@ import org.apache.xpath.XPathAPI;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Community;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataSchema;
+import org.dspace.content.MetadataValue;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.CommunityService;
@@ -36,6 +43,7 @@ import org.dspace.core.Context;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.jdom.Element;
+import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -47,18 +55,17 @@ import org.xml.sax.SAXException;
* an XML file.
*
* The XML file structure needs to be:
- *
- * {@code
+ *
{@code
*
- *
- * ....
- * ...
- *
- * ....
- *
- *
+ *
+ * ....
+ * ...
+ *
+ * ....
+ *
+ *
*
- * }
+ * }
*
* It can be arbitrarily deep, and supports all the metadata elements
* that make up the community and collection metadata. See the system
@@ -68,26 +75,31 @@ import org.xml.sax.SAXException;
*/
public class StructBuilder {
- /**
- * The output XML document which will contain updated information about the
- * imported structure.
+ /** Name of the root element for the document to be imported. */
+ static final String INPUT_ROOT = "import_structure";
+
+ /*
+ * Name of the root element for the document produced by importing.
+ * Community and collection elements are annotated with their identifiers.
*/
- private static final org.jdom.Document xmlOutput
- = new org.jdom.Document(new Element("imported_structure"));
+ static final String RESULT_ROOT = "imported_structure";
/**
- * A hash table to hold metadata for the collection being worked on.
+ * A table to hold metadata for the collection being worked on.
*/
private static final Map collectionMap = new HashMap<>();
/**
- * A hash table to hold metadata for the community being worked on.
+ * A table to hold metadata for the community being worked on.
*/
private static final Map communityMap = new HashMap<>();
- protected static CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService();
- protected static CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService();
- protected static EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
+ protected static CommunityService communityService
+ = ContentServiceFactory.getInstance().getCommunityService();
+ protected static CollectionService collectionService
+ = ContentServiceFactory.getInstance().getCollectionService();
+ protected static EPersonService ePersonService
+ = EPersonServiceFactory.getInstance().getEPersonService();
/**
* Default constructor
@@ -96,30 +108,38 @@ public class StructBuilder {
/**
* Main method to be run from the command line to import a structure into
- * DSpace
+ * DSpacee or export existing structure to a file.The command is of the form:
*
- * This is of the form:
+ * {@code StructBuilder -f [XML source] -e [administrator email] -o [output file]}
*
- * {@code StructBuilder -f [xml source] -e [administrator email] -o [output file]}
+ *
to import, or
*
- * The output file will contain exactly the same as the source xml document, but
- * with the handle for each imported item added as an attribute.
+ *
{@code StructBuilder -x -e [administrator email] -o [output file]}
*
- * @param argv the command line arguments given
+ * to export. The output will contain exactly the same as the source XML
+ * document, but with the Handle for each imported item added as an attribute.
+ *
+ *
+ * @param argv command line arguments.
* @throws ParserConfigurationException passed through.
* @throws SQLException passed through.
+ * @throws FileNotFoundException if input or output could not be opened.
+ * @throws TransformerException if the input document is invalid.
*/
public static void main(String[] argv)
- throws ParserConfigurationException, SQLException {
+ throws ParserConfigurationException, SQLException,
+ FileNotFoundException, IOException, TransformerException {
+ // Parse the command line.
CommandLineParser parser = new DefaultParser();
Options options = new Options();
- options.addOption("h", "help", false, "help");
+ options.addOption("h", "help", false, "Print this help message.");
options.addOption("?", "help");
- options.addOption("f", "file", true, "input structure document");
- options.addOption("e", "eperson", true, "eperson");
- options.addOption("o", "output", true, "output structure document");
+ options.addOption("f", "file", true, "File of new structure information.");
+ options.addOption("e", "eperson", true, "User who is manipulating the repository's structure.");
+ options.addOption("o", "output", true, "File to receive the structure map.");
+ options.addOption("x", "export", false, "Export the current structure as XML.");
CommandLine line = null;
try {
@@ -130,32 +150,49 @@ public class StructBuilder {
System.exit(1);
}
+ // If the user asked for help, give it and exit.
if (line.hasOption('h') || line.hasOption('?')) {
- usage(options);
+ giveHelp(options);
System.exit(0);
}
- String file = null;
+ // Otherwise, analyze the command.
+ // Must be import or export.
+ if (!(line.hasOption('f') || line.hasOption('x'))) {
+ giveHelp(options);
+ System.exit(1);
+ }
+
+ String input = null;
String eperson = null;
String output = null;
if (line.hasOption('f')) {
- file = line.getOptionValue('f');
+ input = line.getOptionValue('f');
}
if (line.hasOption('e')) {
eperson = line.getOptionValue('e');
+ } else { // EPerson is required
+ usage(options);
+ System.exit(1);
}
if (line.hasOption('o')) {
output = line.getOptionValue('o');
- }
-
- if (output == null || eperson == null || file == null) {
+ } else { // output is required
usage(options);
System.exit(1);
}
+ // Open the output stream.
+ OutputStream outputStream;
+ if ("-".equals(output)) {
+ outputStream = System.out;
+ } else {
+ outputStream = new FileOutputStream(output);
+ }
+
// create a context
Context context = new Context();
@@ -167,10 +204,40 @@ public class StructBuilder {
System.exit(1);
}
+ // Export? Import?
+ if (line.hasOption('x')) { // export
+ exportStructure(context, outputStream);
+ } else { // Must be import
+ InputStream inputStream;
+ if ("-".equals(input)) {
+ inputStream = System.in;
+ } else {
+ inputStream = new FileInputStream(input);
+ }
+ importStructure(context, inputStream, outputStream);
+ }
+ System.exit(0);
+ }
+
+ /**
+ * Import new Community/Collection structure.
+ *
+ * @param context
+ * @param input XML which describes the new communities and collections.
+ * @param output input, annotated with the new objects' identifiers.
+ * @throws IOException
+ * @throws ParserConfigurationException
+ * @throws SAXException
+ * @throws TransformerException
+ * @throws SQLException
+ */
+ static void importStructure(Context context, InputStream input, OutputStream output)
+ throws IOException, ParserConfigurationException, SQLException, TransformerException {
+
// load the XML
Document document = null;
try {
- document = loadXML(file);
+ document = loadXML(input);
} catch (IOException ex) {
System.err.format("The input document could not be read: %s%n", ex.getMessage());
System.exit(1);
@@ -180,7 +247,7 @@ public class StructBuilder {
}
// run the preliminary validation, to be sure that the the XML document
- // is properly structured
+ // is properly structured.
try {
validate(document);
} catch (TransformerException ex) {
@@ -188,6 +255,12 @@ public class StructBuilder {
System.exit(1);
}
+ // Check for 'identifier' attributes -- possibly output by this class.
+ NodeList identifierNodes = XPathAPI.selectNodeList(document, "//*[@identifier]");
+ if (identifierNodes.getLength() > 0) {
+ System.err.println("The input document has 'identifier' attributes, which will be ignored.");
+ }
+
// load the mappings into the member variable hashmaps
communityMap.put("name", "name");
communityMap.put("description", "short_description");
@@ -219,33 +292,152 @@ public class StructBuilder {
}
// generate the output
- Element root = xmlOutput.getRootElement();
+ final Element root = new Element(RESULT_ROOT);
+
for (Element element : elements) {
root.addContent(element);
}
- // finally write the string into the output file
- try (BufferedWriter out = new BufferedWriter(new FileWriter(output));) {
- out.write(new XMLOutputter().outputString(xmlOutput));
+ // finally write the string into the output file.
+ final org.jdom.Document xmlOutput = new org.jdom.Document(root);
+ try {
+ new XMLOutputter().output(xmlOutput, output);
} catch (IOException e) {
- System.out.println("Unable to write to output file " + output);
+ System.out.printf("Unable to write to output file %s: %s%n",
+ output, e.getMessage());
System.exit(1);
}
context.complete();
}
+ /**
+ * Add a single community, and its children, to the Document.
+ *
+ * @param community
+ * @return a fragment representing this Community.
+ */
+ private static Element exportACommunity(Community community) {
+ // Export this Community.
+ Element element = new Element("community");
+ element.setAttribute("identifier", community.getHandle());
+ element.addContent(new Element("name").setText(community.getName()));
+ element.addContent(new Element("description")
+ .setText(communityService.getMetadataFirstValue(community,
+ MetadataSchema.DC_SCHEMA, "description", "abstract", Item.ANY)));
+ element.addContent(new Element("intro")
+ .setText(communityService.getMetadataFirstValue(community,
+ MetadataSchema.DC_SCHEMA, "description", null, Item.ANY)));
+ element.addContent(new Element("copyright")
+ .setText(communityService.getMetadataFirstValue(community,
+ MetadataSchema.DC_SCHEMA, "rights", null, Item.ANY)));
+ element.addContent(new Element("sidebar")
+ .setText(communityService.getMetadataFirstValue(community,
+ MetadataSchema.DC_SCHEMA, "description", "tableofcontents", Item.ANY)));
+
+ // Export this Community's Community children.
+ for (Community subCommunity : community.getSubcommunities()) {
+ element.addContent(exportACommunity(subCommunity));
+ }
+
+ // Export this Community's Collection children.
+ for (Collection collection : community.getCollections()) {
+ element.addContent(exportACollection(collection));
+ }
+
+ return element;
+ }
+
+ /**
+ * Add a single Collection to the Document.
+ *
+ * @param collection
+ * @return a fragment representing this Collection.
+ */
+ private static Element exportACollection(Collection collection) {
+ // Export this Collection.
+ Element element = new Element("collection");
+ element.setAttribute("identifier", collection.getHandle());
+ element.addContent(new Element("name").setText(collection.getName()));
+ element.addContent(new Element("description")
+ .setText(collectionService.getMetadataFirstValue(collection,
+ MetadataSchema.DC_SCHEMA, "description", "abstract", Item.ANY)));
+ element.addContent(new Element("intro")
+ .setText(collectionService.getMetadataFirstValue(collection,
+ MetadataSchema.DC_SCHEMA, "description", null, Item.ANY)));
+ element.addContent(new Element("copyright")
+ .setText(collectionService.getMetadataFirstValue(collection,
+ MetadataSchema.DC_SCHEMA, "rights", null, Item.ANY)));
+ element.addContent(new Element("sidebar")
+ .setText(collectionService.getMetadataFirstValue(collection,
+ MetadataSchema.DC_SCHEMA, "description", "tableofcontents", Item.ANY)));
+ element.addContent(new Element("license")
+ .setText(collectionService.getMetadataFirstValue(collection,
+ MetadataSchema.DC_SCHEMA, "rights", "license", Item.ANY)));
+ // Provenance is special: multivalued
+ for (MetadataValue value : collectionService.getMetadata(collection,
+ MetadataSchema.DC_SCHEMA, "provenance", null, Item.ANY)) {
+ element.addContent(new Element("provenance")
+ .setText(value.getValue()));
+ }
+
+ return element;
+ }
+
+ /**
+ * Write out the existing Community/Collection structure.
+ */
+ static void exportStructure(Context context, OutputStream output) {
+ // Build a document from the Community/Collection hierarchy.
+ Element rootElement = new Element(INPUT_ROOT); // To be read by importStructure, perhaps
+
+ List communities = null;
+ try {
+ communities = communityService.findAllTop(context);
+ } catch (SQLException ex) {
+ System.out.printf("Unable to get the list of top-level communities: %s%n",
+ ex.getMessage());
+ System.exit(1);
+ }
+
+ for (Community community : communities) {
+ rootElement.addContent(exportACommunity(community));
+ }
+
+ // Now write the structure out.
+ org.jdom.Document xmlOutput = new org.jdom.Document(rootElement);
+ try {
+ XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
+ outputter.output(xmlOutput, output);
+ } catch (IOException e) {
+ System.out.printf("Unable to write to output file %s: %s%n",
+ output, e.getMessage());
+ System.exit(1);
+ }
+ }
+
/**
* Output the usage information.
*/
private static void usage(Options options) {
HelpFormatter helper = new HelpFormatter();
- helper.printHelp("java StructBuilder -f -o -e ",
- "Load community/collection structure from a file.",
+ helper.printUsage(new PrintWriter(System.out), 80/* FIXME Magic */,
+ "structure-builder", options);
+ }
+
+ /**
+ * Help the user more.
+ */
+ private static void giveHelp(Options options) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("struct-builder",
+ "Import or export Community/Collection structure.",
options,
- "Communities will be created from the top level,"
- + " and a map of communities to handles will be returned"
- + " in the output file.");
+ "When importing (-f), communities will be created from the "
+ + "top level, and a map of communities to handles will "
+ + "be returned in the output file. When exporting (-x),"
+ + "the current structure will be written to the map file.",
+ true);
}
/**
@@ -267,7 +459,7 @@ public class StructBuilder {
if (first.getLength() == 0) {
err.append("-There are no top level communities in the source document.");
System.out.println(err.toString());
- System.exit(0);
+ System.exit(1);
}
String errs = validateCommunities(first, 1);
@@ -278,7 +470,7 @@ public class StructBuilder {
if (trip) {
System.out.println(err.toString());
- System.exit(0);
+ System.exit(1);
}
}
@@ -367,17 +559,17 @@ public class StructBuilder {
}
/**
- * Load in the XML from file.
+ * Load the XML document from input.
*
- * @param filename the filename to load from
- * @return the DOM representation of the XML file
+ * @param input the filename to load from.
+ * @return the DOM representation of the XML input.
*/
- private static org.w3c.dom.Document loadXML(String filename)
+ private static org.w3c.dom.Document loadXML(InputStream input)
throws IOException, ParserConfigurationException, SAXException {
DocumentBuilder builder = DocumentBuilderFactory.newInstance()
.newDocumentBuilder();
- org.w3c.dom.Document document = builder.parse(new File(filename));
+ org.w3c.dom.Document document = builder.parse(input);
return document;
}
@@ -388,7 +580,7 @@ public class StructBuilder {
* @param node the node from which we want to extract the string value
* @return the string value of the node
*/
- public static String getStringValue(Node node) {
+ private static String getStringValue(Node node) {
String value = node.getNodeValue();
if (node.hasChildNodes()) {
diff --git a/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java b/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java
new file mode 100644
index 0000000000..1136008c06
--- /dev/null
+++ b/dspace-api/src/test/java/org/dspace/administer/StructBuilderIT.java
@@ -0,0 +1,334 @@
+/**
+ * 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.administer;
+
+import static org.junit.Assert.assertFalse;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.stream.StreamSource;
+
+import org.dspace.AbstractIntegrationTest;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.Collection;
+import org.dspace.content.Community;
+import org.dspace.content.MetadataSchema;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.CollectionService;
+import org.dspace.content.service.CommunityService;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+import org.xmlunit.builder.DiffBuilder;
+import org.xmlunit.diff.Comparison;
+import org.xmlunit.diff.ComparisonFormatter;
+import org.xmlunit.diff.DefaultComparisonFormatter;
+import org.xmlunit.diff.Diff;
+import org.xmlunit.diff.Difference;
+import org.xmlunit.util.Predicate;
+
+/**
+ * Tests of {@link StructBuilder}.
+ *
+ * @author Mark H. Wood
+ */
+public class StructBuilderIT
+ extends AbstractIntegrationTest {
+ private static final Logger log = LoggerFactory.getLogger(StructBuilderIT.class);
+
+ private static final CommunityService communityService
+ = ContentServiceFactory.getInstance().getCommunityService();
+ private static final CollectionService collectionService
+ = ContentServiceFactory.getInstance().getCollectionService();
+
+ public StructBuilderIT() {
+ }
+
+ @BeforeClass
+ public static void setUpClass() {
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ }
+
+ /**
+ * Ensure that there is no left-over structure to confuse a test.
+ *
+ * @throws SQLException passed through.
+ * @throws AuthorizeException passed through.
+ * @throws IOException passed through.
+ */
+ @Before
+ public void setUp() throws SQLException, AuthorizeException, IOException {
+ // Clear out all communities and collections.
+ context.turnOffAuthorisationSystem();
+ for (Community community : communityService.findAllTop(context)) {
+ deleteSubCommunities(community);
+ communityService.delete(context, community);
+ }
+ context.restoreAuthSystemState();
+ }
+
+ @After
+ public void tearDown() {
+ }
+
+ /** Test structure document. */
+ private static final String IMPORT_DOCUMENT =
+ "\n" +
+ "\n" +
+ " \n" +
+ " Top Community 0 \n" +
+ " " +
+ " \n" +
+ " Sub Community 0.0 \n" +
+ " " +
+ " \n" +
+ " Collection 0.0.0 \n" +
+ " " +
+ " \n" +
+ " \n" +
+ " \n" +
+ " Collection 0.1 \n" +
+ " " +
+ " \n" +
+ " \n" +
+ " \n";
+
+ private static final String EXPORT_DOCUMENT =
+ "\n" +
+ "\n" +
+ " \n" +
+ " Top Community 0 " +
+ " " +
+ " \n" +
+ " Collection 0.0 " +
+ " " +
+ " \n" +
+ " \n" +
+ " \n";
+
+ /**
+ * Test of main method, of class StructBuilder.
+ * @throws java.lang.Exception
+/*
+ @Test
+ public void testMain()
+ throws Exception {
+ System.out.println("main");
+ String[] argv = null;
+ StructBuilder.main(argv);
+ // TODO review the generated test code and remove the default call to fail.
+ fail("The test case is a prototype.");
+ }
+*/
+
+ /**
+ * Test of importStructure method, of class StructBuilder.
+ *
+ * @throws java.lang.Exception passed through.
+ */
+ @Test
+ public void testImportStructure()
+ throws Exception {
+ System.out.println("importStructure");
+
+ // Run the method under test and collect its output.
+ ByteArrayOutputStream outputDocument
+ = new ByteArrayOutputStream(IMPORT_DOCUMENT.length() * 2 * 2);
+ byte[] inputBytes = IMPORT_DOCUMENT.getBytes(StandardCharsets.UTF_8);
+ context.turnOffAuthorisationSystem();
+ try (InputStream input = new ByteArrayInputStream(inputBytes);) {
+ StructBuilder.importStructure(context, input, outputDocument);
+ } catch (IOException | SQLException
+ | ParserConfigurationException | TransformerException ex) {
+ System.err.println(ex.getMessage());
+ System.exit(1);
+ } finally {
+ context.restoreAuthSystemState();
+ }
+
+ // Compare import's output with its input.
+ // N.B. here we rely on StructBuilder to emit communities and
+ // collections in the same order as the input document. If that changes,
+ // we will need a smarter NodeMatcher, probably based on children.
+ Source output = new StreamSource(
+ new ByteArrayInputStream(outputDocument.toByteArray()));
+ Source reference = new StreamSource(
+ new ByteArrayInputStream(
+ IMPORT_DOCUMENT.getBytes(StandardCharsets.UTF_8)));
+ Diff myDiff = DiffBuilder.compare(reference).withTest(output)
+ .normalizeWhitespace()
+// .withNodeFilter(new MyNodeFilter())
+ .withAttributeFilter((Attr attr) ->
+ !attr.getName().equals("identifier"))
+ .checkForIdentical()
+ .build();
+
+ // Was there a difference?
+ // Always output differences -- one is expected.
+ ComparisonFormatter formatter = new DefaultComparisonFormatter();
+ for (Difference difference : myDiff.getDifferences()) {
+ System.err.println(difference.toString(formatter));
+ }
+ // Test for *significant* differences.
+ assertFalse("Output does not match input.", isDifferent(myDiff));
+
+ // TODO spot-check some objects.
+ }
+
+ /**
+ * Test of exportStructure method, of class StructBuilder.
+ * @throws ParserConfigurationException passed through.
+ * @throws org.xml.sax.SAXException passed through.
+ * @throws java.io.IOException passed through.
+ * @throws java.sql.SQLException passed through.
+ * @throws org.dspace.authorize.AuthorizeException passed through.
+ */
+ @Test
+ public void testExportStructure()
+ throws ParserConfigurationException, SAXException, IOException,
+ SQLException, AuthorizeException {
+ // Create some structure to test.
+ context.turnOffAuthorisationSystem();
+ Community community0 = communityService.create(null, context);
+ communityService.setMetadataSingleValue(context, community0,
+ MetadataSchema.DC_SCHEMA, "title", null,
+ null, "Top Community 0");
+ Collection collection0_0 = collectionService.create(context, community0);
+ collectionService.setMetadataSingleValue(context, collection0_0,
+ MetadataSchema.DC_SCHEMA, "title", null,
+ null, "Collection 0.0");
+
+ // Export the current structure.
+ System.out.println("exportStructure");
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ StructBuilder.exportStructure(context, outputStream);
+
+ context.restoreAuthSystemState();
+
+ // Compare the output to the expected output.
+ Source output = new StreamSource(
+ new ByteArrayInputStream(outputStream.toByteArray()));
+ Source reference = new StreamSource(
+ new ByteArrayInputStream(
+ EXPORT_DOCUMENT.getBytes(StandardCharsets.UTF_8)));
+ Diff myDiff = DiffBuilder.compare(reference).withTest(output)
+ .normalizeWhitespace()
+// .withNodeFilter(new MyNodeFilter())
+ .withAttributeFilter((Attr attr) ->
+ !attr.getName().equals("identifier"))
+ .checkForIdentical()
+ .build();
+
+ // Was there a difference?
+ // Always output differences -- one is expected.
+ ComparisonFormatter formatter = new DefaultComparisonFormatter();
+ for (Difference difference : myDiff.getDifferences()) {
+ System.err.println(difference.toString(formatter));
+ }
+ // Test for *significant* differences.
+ assertFalse("Output does not match input.", myDiff.hasDifferences());
+ }
+
+ /**
+ * Delete all child communities and collections of a given community.
+ * All descendant collections must be empty of Items.
+ *
+ * @param c the Community to be pruned of all descendants.
+ * @throws SQLException passed through.
+ * @throws AuthorizeException passed through.
+ * @throws IOException passed through.
+ */
+ private void deleteSubCommunities(Community c)
+ throws SQLException, AuthorizeException, IOException {
+ for (Community subCommunity : c.getSubcommunities()) {
+ deleteSubCommunities(subCommunity);
+ communityService.delete(context, subCommunity);
+ }
+ for (Collection collection : c.getCollections()) {
+ collectionService.delete(context, collection);
+ }
+ }
+
+ /**
+ * Test that the documents are not different, except that their root
+ * elements have specific different names.
+ *
+ * @param diff
+ * @return true if these are otherwise-identical "import_structure" and
+ * "imported_structure" documents.
+ */
+ private boolean isDifferent(Diff diff) {
+ Iterator diffIterator = diff.getDifferences().iterator();
+
+ // There must be at least one difference.
+ if (!diffIterator.hasNext()) {
+ log.error("Not enough differences.");
+ return true;
+ }
+
+ // The difference must be that the root nodes are named "import_structure"
+ // and "imported_structure".
+ Comparison comparison = diffIterator.next().getComparison();
+ Node controlNode = comparison.getControlDetails().getTarget();
+ Node testNode = comparison.getTestDetails().getTarget();
+ if (!controlNode.getNodeName().equals("import_structure")
+ || !testNode.getNodeName().equals("imported_structure")) {
+ log.error("controlNode name: {}", controlNode.getNodeName());
+ log.error("test node name: {}", testNode.getNodeName());
+ return true;
+ }
+ if ((controlNode.getParentNode().getNodeType() != Node.DOCUMENT_NODE)
+ || (testNode.getParentNode().getNodeType() != Node.DOCUMENT_NODE)) {
+ log.error("control node's parent type is {}", controlNode.getParentNode().getNodeType());
+ log.error("test node's parent type is {}", testNode.getParentNode().getNodeType());
+ return true;
+ }
+
+ // There must be at most one difference.
+ return diffIterator.hasNext();
+ }
+
+ /**
+ * Reject uninteresting nodes.
+ */
+ private static class MyNodeFilter implements Predicate {
+ private static final List dontCare = Arrays.asList(
+ "description",
+ "intro",
+ "copyright",
+ "sidebar",
+ "license",
+ "provenance");
+
+ @Override
+ public boolean test(Node node) {
+ String type = node.getLocalName();
+ return ! dontCare.contains(type);
+ }
+ }
+}
diff --git a/dspace-api/src/test/java/org/dspace/administer/package-info.java b/dspace-api/src/test/java/org/dspace/administer/package-info.java
new file mode 100644
index 0000000000..e8e1bf68f0
--- /dev/null
+++ b/dspace-api/src/test/java/org/dspace/administer/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * 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.administer;
+
+/**
+ * Test administrative tools.
+ */