/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package fedora.server.storage.translation;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import org.jrdf.graph.URIReference;
import org.junit.Test;
import fedora.server.errors.ObjectIntegrityException;
import fedora.server.errors.StreamIOException;
import fedora.server.storage.types.AuditRecord;
import fedora.server.storage.types.BasicDigitalObject;
import fedora.server.storage.types.Datastream;
import fedora.server.storage.types.DatastreamXMLMetadata;
import fedora.server.storage.types.DigitalObject;
import fedora.server.storage.types.Disseminator;
import static fedora.common.Models.FEDORA_OBJECT_3_0;
import static fedora.common.Models.SERVICE_DEFINITION_3_0;
import static fedora.common.Models.SERVICE_DEPLOYMENT_3_0;
import static fedora.server.storage.translation.DOTranslationUtility.DESERIALIZE_INSTANCE;
import static fedora.server.storage.translation.DOTranslationUtility.SERIALIZE_STORAGE_INTERNAL;
/**
* Common unit tests and utility methods for XML-based deserializers.
*
* @author Chris Wilper
*/
@SuppressWarnings("deprecation")
public abstract class TestXMLDODeserializer
extends TranslationTest {
/** The deserializer to test. */
protected final DODeserializer m_deserializer;
/** The associated (separately unit-tested) serializer. */
protected final DOSerializer m_serializer;
TestXMLDODeserializer(DODeserializer deserializer, DOSerializer serializer) {
m_deserializer = deserializer;
m_serializer = serializer;
}
//---
// Tests
//---
@Test
public void testDeserializeSimpleDataObject() {
doSimpleTest(FEDORA_OBJECT_3_0);
}
@Test
public void testDeserializeSimpleSDepObject() {
doSimpleTest(SERVICE_DEPLOYMENT_3_0);
}
@Test
public void testDeserializeSimpleSDefObject() {
doSimpleTest(SERVICE_DEFINITION_3_0);
}
@Test
public void testTwoInlineDatastreams() {
DigitalObject obj = createTestObject(FEDORA_OBJECT_3_0);
final String dsID1 = "DS1";
DatastreamXMLMetadata ds1 = createXDatastream(dsID1);
final String dsID2 = "DS2";
DatastreamXMLMetadata ds2 = createXDatastream(dsID2);
obj.addDatastreamVersion(ds1, true);
obj.addDatastreamVersion(ds2, true);
DigitalObject result = doDeserializeOrFail(obj);
int numDatastreams = 0;
Iterator<String> iter = result.datastreamIdIterator();
while (iter.hasNext()) {
iter.next();
numDatastreams++;
}
/* 3 datastreams: ds1, ds2, rels-ext */
assertEquals(3, numDatastreams);
assertTrue(result.datastreams(dsID1).iterator().hasNext());
assertTrue(result.datastreams(dsID2).iterator().hasNext());
}
/**
* Tests for deterministic inline-XML content between generations. Addresses
* bug #1771136: inlineXML would increase in size between copy generations
* due to added whitespace.
*
* @throws Exception
*/
@Test
public void testInlineXMLCopyIntegrity() throws Exception {
DigitalObject original = createTestObject(FEDORA_OBJECT_3_0);
final String dsID1 = "DS1";
/* Populate the object with a test datastream and serialize */
DatastreamXMLMetadata ds1 = createXDatastream(dsID1);
original.addDatastreamVersion(ds1, true);
DigitalObject copy = translatedCopy(original);
DigitalObject copyOfCopy = translatedCopy(copy);
DatastreamXMLMetadata ds1copy =
(DatastreamXMLMetadata) copy.datastreams(dsID1).iterator()
.next();
DatastreamXMLMetadata ds1copyOfCopy =
(DatastreamXMLMetadata) copyOfCopy.datastreams(dsID1)
.iterator().next();
assertEquals("Length of XML datastream copies is not deterministic!",
ds1copy.xmlContent.length,
ds1copyOfCopy.xmlContent.length);
}
@Test
public void testAuditDatastream() throws Exception {
AuditRecord record = new AuditRecord();
record.action = "modifyDatastreamByReference";
record.componentID = "DRAWING-ICON";
record.date = new Date(0L);
record.id = "AUDREC1";
record.justification = "malice";
record.processType = "Fedora API-M";
record.responsibility = "fedoraAdmin";
DigitalObject original = createTestObject(FEDORA_OBJECT_3_0);
original.getAuditRecords().add(record);
// serialize to file
File temp = File.createTempFile("audit", ".xml");
OutputStream out = new FileOutputStream(temp);
m_serializer.serialize(original, out, "utf-8", DOTranslationUtility.SERIALIZE_EXPORT_PUBLIC);
out.close();
// deserialize
DigitalObject candidate = new BasicDigitalObject();
InputStream in = new FileInputStream(temp);
m_deserializer.deserialize(in, candidate, "utf-8", DOTranslationUtility.DESERIALIZE_INSTANCE);
List<AuditRecord> a1 = original.getAuditRecords();
List<AuditRecord> a2 = candidate.getAuditRecords();
assertEquals(a1.size(), a2.size());
for (int i = 0; i < a1.size(); i++) {
assertEquals(a1.get(i).action, a2.get(i).action);
assertEquals(a1.get(i).componentID, a2.get(i).componentID);
assertEquals(a1.get(i).date, a2.get(i).date);
assertEquals(a1.get(i).id, a2.get(i).id);
assertEquals(a1.get(i).justification, a2.get(i).justification);
assertEquals(a1.get(i).processType, a2.get(i).processType);
assertEquals(a1.get(i).responsibility, a2.get(i).responsibility);
}
temp.delete();
}
/** Tests the serializers/deserializers when faced with null object property values.
* <p>
* Currently, this test assures that null iproperty values are handled consistently
* among serializers and deserializers. The expected behaviour is a bit un-intuitive,
* but represents the "status quo" that satisfies existing server code:
* <dl>
* <dt>CreatedDate, LastModifiedDate, External properties</dt>
* <dd>Null value should be interpreted as null</dd>
* <dt>Label, OwnerId</dt>
* <dd>Null value should be interpreted as an empty string ("")</dd>
* <dt>State</dt>
* <dd>Null value should be interpreted as "Active"</dd>
* </dl>
* </p>
*/
@Test
public void testNullObjectProperties() {
final String EXT_PROP = "http://example.org/test";
DigitalObject input = createTestObject(FEDORA_OBJECT_3_0);
input.setCreateDate(null);
input.setLastModDate(null);
input.setLabel(null);
input.setOwnerId(null);
input.setState(null);
input.setExtProperty(EXT_PROP, null);
DigitalObject obj = doDeserializeOrFail(input);
assertNull("Create date should be null", obj.getCreateDate());
assertNull("LastMod date should be null", obj.getLastModDate());
assertEquals("Null label should be interpreted as empty string", "", obj.getLabel());
assertEquals("Null ownerid should be interpreted as empty string", "", obj.getOwnerId());
assertEquals("Null state should be interpreted as active", "A", obj.getState());
assertNull("Ext property should be null", obj.getExtProperty(EXT_PROP));
}
/** Tests the serializers/deserializers when faced with empty ("") object property values.
* <p>
* Currently, this test assures that empty string property values are handled consistently
* among serializers and deserializers. The expected behaviour is as follows:
* <dl>
* <dt>Label, Ownerid, External properties</dt>
* <dd>Empty string value should be interpreted the empty string ("")</dd>
* <dt>State</dt>
* <dd>Empty string values should be interpreted as "Active"</dd>
* </dl>
* </p>
*/
@Test
public void testEmptyObjectProperties() {
final String EXT_PROP_SUPPORTED = "http://example.org/ext-supported";
final String EXT_PROP = "http://example.org/test";
DigitalObject input = createTestObject(FEDORA_OBJECT_3_0);
input.setLabel("");
input.setOwnerId("");
//input.setState("");
input.setExtProperty(EXT_PROP_SUPPORTED, "true");
input.setExtProperty(EXT_PROP, "");
DigitalObject obj = doDeserializeOrFail(input);
assertEquals("Empty label should remain empty", "", obj.getLabel());
assertEquals("Empty Ownerid should remain empty", "", obj.getOwnerId());
assertEquals("Empty State should be interpreted as active", "A", obj.getState());
/* Some formats (METS) don't support ext. properties */
if ("true".equals(obj.getExtProperty(EXT_PROP_SUPPORTED))) {
assertEquals("Empty Ext property should remain empty", "", obj.getExtProperty(EXT_PROP));
}
}
@Test
public void testFedoraLocalServerSubstitution() {
DigitalObject o = createTestObject(SERVICE_DEPLOYMENT_3_0);
DatastreamXMLMetadata ds1 = createXDatastream("WSDL");
ds1.xmlContent = "<test>http://local.fedora.server/</test>".getBytes();
o.addDatastreamVersion(ds1, false);
DigitalObject processed = doDeserializeOrFail(o);
DatastreamXMLMetadata ds1proc =
(DatastreamXMLMetadata) processed.datastreams("WSDL")
.iterator().next();
Iterator<String> ids = processed.datastreamIdIterator();
String content = new String(ds1proc.xmlContent);
assertFalse(content.contains("local.fedora.server"));
assertTrue(content.contains("http"));
}
/**
* Copies of an object by deserializing and re-serializing. In theory, there
* should be no difference between copy generations..
*
* @param original
* Object to copy
* @return Copy formed by serializing and de-serializing the original.
* @throws UnsupportedEncodingException
* @throws ObjectIntegrityException
* @throws StreamIOException
*/
private DigitalObject translatedCopy(DigitalObject original)
throws UnsupportedEncodingException, ObjectIntegrityException,
StreamIOException {
DigitalObject copy = new BasicDigitalObject();
ByteArrayOutputStream out = new ByteArrayOutputStream();
m_serializer.serialize(original,
out,
"UTF-8",
SERIALIZE_STORAGE_INTERNAL);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
m_deserializer.deserialize(in,
copy,
"UTF-8",
SERIALIZE_STORAGE_INTERNAL);
return copy;
}
//---
// Instance helpers
//---
protected void doSimpleTest(URIReference... models) {
DigitalObject input = createTestObject(models);
DigitalObject obj = doDeserializeOrFail(input);
for (URIReference model : models) {
assertTrue("Did not detect that object had model " + model, obj
.hasContentModel(model));
}
assertEquals(TEST_PID, obj.getPid());
}
protected DigitalObject doDeserializeOrFail(DigitalObject obj) {
DigitalObject result = null;
try {
result = doDeserialize(obj);
} catch (ObjectIntegrityException e) {
e.printStackTrace();
fail("Deserializer threw ObjectIntegrityException");
} catch (StreamIOException e) {
e.printStackTrace();
fail("Deserializer threw StreamIOException");
}
return result;
}
@SuppressWarnings("unchecked")
protected DigitalObject doDeserialize(DigitalObject obj)
throws ObjectIntegrityException, StreamIOException {
/*
* Make RELS-EXT the last datastream, just to make things trickiest for
* deserializers (i.e. if serializers need to know relationships before
* RELS-EXT has been parsed..)
*/
try {
Field dsField =
BasicDigitalObject.class.getDeclaredField("m_datastreams");
dsField.setAccessible(true);
LinkedHashMap<String, List<Datastream>> nativelyOrdered =
(LinkedHashMap<String, List<Datastream>>) dsField.get(obj);
LinkedHashMap<String, List<Datastream>> speciallyOrdered =
new LinkedHashMap<String, List<Datastream>>();
Iterator<String> di = obj.datastreamIdIterator();
/* Just copy everything EXCEPT rels ext */
while (di.hasNext()) {
String id = di.next();
if (!id.equals("RELS-EXT")) {
speciallyOrdered.put(id, nativelyOrdered.get(id));
}
}
/* Put RELS-EXT last, if defined */
List<Datastream> rels = nativelyOrdered.get("RELS-EXT");
if (rels != null) {
speciallyOrdered.put("RELS-EXT", rels);
}
dsField.set(obj, speciallyOrdered);
} catch (Exception e) {
throw new RuntimeException(e);
}
return doDeserialize(getStream(obj));
}
protected DigitalObject doDeserialize(InputStream in)
throws ObjectIntegrityException, StreamIOException {
BasicDigitalObject obj = new BasicDigitalObject();
try {
m_deserializer.deserialize(in, obj, "UTF-8", DESERIALIZE_INSTANCE);
} catch (UnsupportedEncodingException wontHappen) {
fail("Deserializer doesn't support UTF-8?!");
}
return obj;
}
// use the associated serializer to create a stream for the object, or fail
protected InputStream getStream(DigitalObject obj) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
m_serializer.serialize(obj,
out,
"UTF-8",
SERIALIZE_STORAGE_INTERNAL);
} catch (Exception e) {
e.printStackTrace();
fail("Failed to serialize test object for deserialization test");
}
return new ByteArrayInputStream(out.toByteArray());
}
protected void doTestTwoDisseminators() {
DigitalObject obj = createTestObject(FEDORA_OBJECT_3_0);
final String dissID1 = "DISS1";
Disseminator diss1 = createDisseminator(dissID1, 1);
final String dissID2 = "DISS2";
Disseminator diss2 = createDisseminator(dissID2, 1);
obj.disseminators(dissID1).add(diss1);
obj.disseminators(dissID2).add(diss2);
DigitalObject result = doDeserializeOrFail(obj);
int numDisseminators = 0;
Iterator<String> iter = result.disseminatorIdIterator();
while (iter.hasNext()) {
iter.next();
numDisseminators++;
}
assertEquals(2, numDisseminators);
assertEquals(1, result.disseminators(dissID1).size());
assertEquals(1, result.disseminators(dissID2).size());
}
}