/* 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.test.api;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.custommonkey.xmlunit.NamespaceContext;
import org.custommonkey.xmlunit.SimpleNamespaceContext;
import org.custommonkey.xmlunit.XMLUnit;
import org.openrdf.rio.ntriples.NTriplesParser;
import org.trippi.TripleIterator;
import org.trippi.io.RIOTripleIterator;
import fedora.client.FedoraClient;
import fedora.common.Constants;
import fedora.common.Models;
import fedora.common.PID;
import fedora.server.management.FedoraAPIM;
import fedora.server.types.gen.RelationshipTuple;
import fedora.test.FedoraServerTestCase;
/**
* Tests for the various relationship API-M methods. Tests assume a running
* instance of the Fedora server with Resource Index enabled.
*
* @author Edwin Shin
*/
public class TestRelationships
extends FedoraServerTestCase
implements Constants {
private FedoraAPIM apim;
private static final String RISEARCH_QUERY =
"/risearch?type=triples&lang=spo&format=NTriples&stream=on&"
+ "flush=true&query=";
private static byte[] DEMO_888_FOXML;
private static byte[] DEMO_777_FOXML;
private static String MULTIBYTE_UTF8;
// FIXME: once the raw pid form of subject in the relationship methods is no longer in use, remove 0 and 4 below
// demo:777 contains no rels-ext/rels-int datastream, demo:888 contains both
// subject identifiers for the following scenarios (add/purge mostly)
// 0: demo:777, subject is the digital object, as a pid
// 1: demo:777, subject is the digital object, as a uri
// 2: demo:777, subject is Datastream DS1, as a uri
// 3: demo:777, subject is Datastream DS2, as a uri
// 4: demo:888, subject is the digital object, as a pid
// 5: demo:888, subject is the digital object, as a uri
// 6: demo:888, subject is Datastream DS1, as a uri
// 7: demo:888, subject is Datastream DS2, as a uri
// test subject identifiers for the above
private final String subject[] = {
"demo:777", // deprecated
"info:fedora/demo:777",
"info:fedora/demo:777/DS1",
"info:fedora/demo:777/DS2",
"demo:888", // deprecated
"info:fedora/demo:888",
"info:fedora/demo:888/DS1",
"info:fedora/demo:888/DS2"};
static {
// Test FOXML object with RELS-EXT and RELS-INT datastream
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sb.append("<foxml:digitalObject VERSION=\"1.1\" PID=\"demo:888\" xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"info:fedora/fedora-system:def/foxml# http://www.fedora.info/definitions/1/0/foxml1-1.xsd\">");
sb.append(" <foxml:objectProperties>");
sb.append(" <foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\"A\"/>");
sb.append(" </foxml:objectProperties>");
sb.append(" <foxml:datastream ID=\"RELS-EXT\" CONTROL_GROUP=\"M\" STATE=\"A\">");
sb.append(" <foxml:datastreamVersion FORMAT_URI=\"info:fedora/fedora-system:FedoraRELSExt-1.0\" ID=\"RELS-EXT.0\" MIMETYPE=\"application/rdf+xml\" LABEL=\"RDF Statements about this object\">");
sb.append(" <foxml:xmlContent>");
sb.append(" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
+ " xmlns:fedora-model=\"info:fedora/fedora-system:def/model#\">");
sb.append(" <rdf:Description rdf:about=\"info:fedora/demo:888\">");
sb.append(" <fedora-model:hasModel rdf:resource=\"info:fedora/demo:UVA_STD_IMAGE_1\"/>");
sb.append(" <fedora-model:hasModel rdf:resource=\"" + Models.FEDORA_OBJECT_CURRENT.uri + "\"/>");
sb.append(" </rdf:Description>");
sb.append(" </rdf:RDF>");
sb.append(" </foxml:xmlContent>");
sb.append(" </foxml:datastreamVersion>");
sb.append(" </foxml:datastream>");
sb.append(" <foxml:datastream ID=\"RELS-INT\" CONTROL_GROUP=\"M\" STATE=\"A\">");
sb.append(" <foxml:datastreamVersion FORMAT_URI=\"info:fedora/fedora-system:FedoraRELSInt-1.0\" ID=\"RELS-INT.0\" MIMETYPE=\"application/rdf+xml\" LABEL=\"RDF Statements about datastreams in this object\">");
sb.append(" <foxml:xmlContent>");
sb.append(" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
+ " xmlns:myns=\"http://www.example.org/testns#\">");
sb.append(" <rdf:Description rdf:about=\"info:fedora/demo:888/DS1\">");
sb.append(" <myns:test1 rdf:resource=\"info:fedora/demo:UVA_STD_IMAGE_1\"/>");
sb.append(" </rdf:Description>");
sb.append(" <rdf:Description rdf:about=\"info:fedora/demo:888/DS3\">");
sb.append(" <myns:test2 rdf:resource=\"info:fedora/demo:11223344\"/>");
sb.append(" </rdf:Description>");
sb.append(" </rdf:RDF>");
sb.append(" </foxml:xmlContent>");
sb.append(" </foxml:datastreamVersion>");
sb.append(" </foxml:datastream>");
sb.append("</foxml:digitalObject>");
try {
DEMO_888_FOXML = sb.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
}
// Test FOXML object with no RELS-EXT (or RELS-INT) datastream
sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sb.append("<foxml:digitalObject VERSION=\"1.1\" PID=\"demo:777\" xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"info:fedora/fedora-system:def/foxml# http://www.fedora.info/definitions/1/0/foxml1-1.xsd\">");
sb.append(" <foxml:objectProperties>");
sb.append(" <foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\"A\"/>");
sb.append(" </foxml:objectProperties>");
sb.append("</foxml:digitalObject>");
try {
DEMO_777_FOXML = sb.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
}
try {
// UTF-8 string with multibyte characters (for literal object tests)
// construct explicitly from bytes to avoid any encoding issues with this source file (which *should* be utf-8)
// (or could use: MULTIBYTE_UTF8 = "“Α ¿”";
MULTIBYTE_UTF8 = new String( new byte[] {
(byte)0xE2, (byte)0x80, (byte)0x9C, // left double quotes “
(byte)0xCE, (byte)0x91, // capital alpha Α
(byte)0x20, // space
(byte)0xC2, (byte)0xBF, // inverted question mark ¿
(byte)0xE2, (byte)0x80, (byte)0x9D // right double quotes ”
}, "UTF-8");
} catch (UnsupportedEncodingException uee) {
}
}
public static Test suite() {
TestSuite suite = new TestSuite("TestRelationships TestSuite");
suite.addTestSuite(TestRelationships.class);
return suite;
}
@Override
public void setUp() throws Exception {
apim = getFedoraClient().getAPIM();
Map<String, String> nsMap = new HashMap<String, String>();
nsMap.put("oai_dc", "http://www.openarchives.org/OAI/2.0/oai_dc/");
nsMap.put("dc", "http://purl.org/dc/elements/1.1/");
nsMap.put("foxml", "info:fedora/fedora-system:def/foxml#");
NamespaceContext ctx = new SimpleNamespaceContext(nsMap);
XMLUnit.setXpathNamespaceContext(ctx);
apim.ingest(DEMO_888_FOXML, FOXML1_1.uri, "ingesting new foxml object");
apim.ingest(DEMO_777_FOXML, FOXML1_1.uri, "ingesting new foxml object");
}
@Override
public void tearDown() throws Exception {
apim.purgeObject("demo:777", "", false);
apim.purgeObject("demo:888", "", false);
XMLUnit.setXpathNamespaceContext(SimpleNamespaceContext.EMPTY_CONTEXT);
}
public void testAddRelationship() throws Exception {
String p, o;
int relNum = 0; // used to form unique relationships... addRelationship needs unique predicate x object combinatinos
for (String s : subject) {
p = "urn:bar" + relNum++;
o = "urn:baz";
addRelationship(s, p, o, false, null);
// plain literal
o = "quux";
addRelationship(s, p, o, true, null);
// datatyped literal
o = "1970-01-01T00:00:00Z";
addRelationship(s, p, o, true, Constants.RDF_XSD.DATE_TIME.uri);
// utf-8 literal with multibyte sequences
o = MULTIBYTE_UTF8;
addRelationship(s, p, o, true, null);
}
}
public void testValidation() {
String p, o;
int relNum = 0; // used to form unique relationships or objects/object literals... addRelationship needs unique predicate x object combinations
for (String s : subject) {
p = "http://purl.org/dc/elements/1.1/title";
o = "A Dictionary of Maqiao" + relNum++;
// DC rels only invalid for RELS-EXT...
if (!s.endsWith("DS1") && !s.endsWith("DS2")) {
try {
apim.addRelationship(s, p, o, true, null);
fail("Adding Dublin Core relationship should have failed - " + s);
} catch (RemoteException e) {
}
}
p = "info:fedora/fedora-system:def/model#foo";
try {
apim.addRelationship(s, p, o, true, null);
fail("Adding Fedora Model relationship should have failed - " + s);
} catch (RemoteException e) {
}
p = "urn:bar" + relNum;
// invalid dateTime literal
o = "2009-10-05T16:02:26+0100";
try {
apim.addRelationship(s, p, o, true, Constants.RDF_XSD.DATE_TIME.uri);
fail("Adding invalid date/time literal in relationship should have failed - " + s);
} catch (RemoteException e) {
}
}
}
public void testBadSubjectURI() {
String s, p, o;
// subject is a valid info:fedora/ uri for an object, but object does not exist
s = "info:fedora/does:notexist";
p = "urn:foo";
o = "urn:bar";
try {
apim.addRelationship(s, p, o, false, null);
fail("Adding relationship with subject as a Fedora DO that does not exist should have failed");
} catch (RemoteException e) {
}
// subject is a valid info:fedora/ uri for a datastream, but object does not exist
s = "info:fedora/does:notexist/DS1";
p = "urn:foo";
o = "urn:baz";
try {
apim.addRelationship(s, p, o, false, null);
fail("Adding relationship with subject as a Fedora object datastream where DO does not exist should have failed");
} catch (RemoteException e) {
}
// subject is a valid uri, but not in info:fedora/ scheme
s = "http://www.example.org/test";
p = "urn:foo";
o = "urn:quux";
try {
apim.addRelationship(s, p, o, false, null);
fail("Adding relationship with subject uri not in the info:fedora scheme should have failed");
} catch (RemoteException e) {
}
// Valid PID & datastream ID, but invalid subject URI
// should be: info:fedora/demo:888/DS1
s = "demo:888/DS1";
p = "urn:foo";
o = "urn:quux";
try {
apim.addRelationship(s, p, o, false, null);
fail("Adding relationship with invalid short URI should have failed");
} catch (RemoteException e) {
}
}
public void testGetRelationships() throws Exception {
String p, o;
int relNum = 0; // used to form unique relationships or objects/object literals... addRelationship needs unique predicate x object combinations
for (String s : subject) {
p = "urn:bar" + relNum++;
o = "urn:baz";
getRelationship(s, p, o, false, null);
p = "urn:title" + relNum;
o = "asdf";
getRelationship(s, p, o, true, null);
p = "urn:temperature" + relNum;
o = "98.6";
getRelationship(s, p, o, true, Constants.RDF_XSD.FLOAT.uri);
// utf-8 literal with multibyte sequences
p = "urn:utf8literal" + relNum;
o = MULTIBYTE_UTF8;
getRelationship(s, p, o, true, null);
}
}
public void testGetAllRelationships() throws Exception {
// subject uri and pid
RelationshipTuple[] tuples = apim.getRelationships("demo:777", null);
assertEquals(1, tuples.length);
}
public void testBasicCModelRelationships() throws Exception {
// just the uri form for subject, pid form has got a hammering above
for (String pid : new String[] { "info:fedora/demo:777", "info:fedora/demo:888" }) {
checkExistsViaGetRelationships(pid,
Constants.MODEL.HAS_MODEL.uri,
Models.FEDORA_OBJECT_CURRENT.uri);
}
}
public void testPurgeRelationships() throws Exception {
String p, o;
int relNum = 0; // used to form unique relationships or objects/object literals... addRelationship needs unique predicate x object combinations
for (String s : subject) {
p = "urn:p" + relNum++;
o = "urn:o";
purgeRelationship(s, p, o, false, null);
p = "urn:title" + relNum;
o = "asdf";//"三国演义"; // test unicode
purgeRelationship(s, p, o, true, null);
p = "urn:temperature" + relNum;
o = "98.6";
purgeRelationship(s, p, o, true, Constants.RDF_XSD.FLOAT.uri);
// utf-8 literal with multibyte sequences
p = "urn:utf8literal" + relNum;
o = MULTIBYTE_UTF8;
purgeRelationship(s, p, o, true, null);
assertFalse("Purging non-existant relation should have failed", apim
.purgeRelationship(s, "urn:asdf", "867-5309", true, null));
}
}
private void checkExistsViaGetRelationships(String subject,
String predicate,
String object) throws Exception {
boolean found = false;
for (RelationshipTuple tuple : apim.getRelationships(subject, predicate)) {
if (tuple.getSubject().equals(subjectAsURI(subject))
&& tuple.getPredicate().equals(predicate)
&& tuple.getObject().equals(object)) {
found = true;
}
}
assertTrue("Relationship not found via getRelationships (subject=" + subject
+ ", predicate=" + predicate + ", object=" + object, found);
}
// note: queries resource index by predicate and object, and then checks subject is ok
// so make sure if testing across multiple objects that predicate x object combinations are unique
private void addRelationship(String subject,
String predicate,
String object,
boolean isLiteral,
String datatype) throws Exception {
assertTrue(apim.addRelationship(subject,
predicate,
object,
isLiteral,
datatype));
assertFalse("Adding duplicate relationship should return false", apim
.addRelationship(subject, predicate, object, isLiteral, datatype));
// check resource index
String query = "";
if (isLiteral) {
if (datatype != null) {
query =
String.format("* <%s> '%s'^^%s",
predicate,
object,
datatype);
} else {
query = String.format("* <%s> '%s'", predicate, object);
}
} else {
query = String.format("* <%s> <%s>", predicate, object);
}
TripleIterator triples = queryRI(query);
try {
assertTrue("Relationship not found in RI (query = " + query + ")", triples.hasNext());
while (triples.hasNext()) {
assertEquals(triples.next().getSubject().stringValue(),
subjectAsURI(subject));
}
} finally {
triples.close();
}
}
// FIXME: remove once pid no longer allowed as subject in relationships methods
// check if subject is a uri or just a pid, if a pid then return the uri form
private String subjectAsURI(String subj) {
// already a uri?
if (subj.startsWith(Constants.FEDORA.uri))
return subj;
// no, convert to uri
return PID.toURI(subj);
}
private void getRelationship(String subject,
String predicate,
String object,
boolean isLiteral,
String datatype) throws Exception {
addRelationship(subject, predicate, object, isLiteral, datatype);
RelationshipTuple[] tuples = apim.getRelationships(subject, predicate);
assertNotNull(tuples);
assertEquals(1, tuples.length);
assertEquals(subjectAsURI(subject), tuples[0].getSubject());
assertEquals(predicate, tuples[0].getPredicate());
assertEquals(object, tuples[0].getObject());
assertEquals(isLiteral, tuples[0].isIsLiteral());
assertEquals(datatype, tuples[0].getDatatype());
}
private void purgeRelationship(String subject,
String predicate,
String object,
boolean isLiteral,
String datatype) throws Exception {
addRelationship(subject, predicate, object, isLiteral, datatype);
assertTrue(apim.purgeRelationship(subject,
predicate,
object,
isLiteral,
datatype));
}
private TripleIterator queryRI(String query) throws Exception {
FedoraClient client = getFedoraClient();
InputStream results =
client.get(RISEARCH_QUERY + URLEncoder.encode(query, "UTF-8"), true);
return new RIOTripleIterator(results,
new NTriplesParser(),
"info/fedora");
}
public static void main(String[] args) {
junit.textui.TestRunner.run(TestRelationships.class);
}
}