From 63ed82cef932009bad5c56a0ce0e23aa13193dae Mon Sep 17 00:00:00 2001 From: Jim Blake Date: Fri, 18 Jul 2014 17:04:58 -0400 Subject: [PATCH] VIVO-823 Create some tests for the VitroModelFactory Make changes as determined by the tests, to BulkUpdatingOntModel as well. Add debug statements to ModelSynchronizer. --- .../webapp/dao/jena/ModelSynchronizer.java | 114 ++- .../adapters/BulkUpdatingOntModel.java | 28 +- .../adapters/VitroModelFactory.java | 61 +- .../mannlib/vitro/testing/RecordingProxy.java | 116 +++ .../adapters/VitroModelFactoryTest.java | 722 ++++++++++++++++++ 5 files changed, 982 insertions(+), 59 deletions(-) create mode 100644 webapp/test/edu/cornell/mannlib/vitro/testing/RecordingProxy.java create mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactoryTest.java diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ModelSynchronizer.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ModelSynchronizer.java index f4fcb14d4..4c3077656 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ModelSynchronizer.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/ModelSynchronizer.java @@ -3,79 +3,115 @@ package edu.cornell.mannlib.vitro.webapp.dao.jena; import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.ModelChangedListener; import com.hp.hpl.jena.rdf.model.Statement; import com.hp.hpl.jena.rdf.model.StmtIterator; +import com.hp.hpl.jena.rdf.model.impl.StmtIteratorImpl; import edu.cornell.mannlib.vitro.webapp.dao.jena.event.CloseEvent; /** - * Simple change listener to keep a model (the 'synchronizee') in synch with the model with which it is registered. + * Simple change listener to keep a model (the 'synchronizee') in synch with the + * model with which it is registered. + * * @author bjl23 - * + * */ public class ModelSynchronizer implements ModelChangedListener { + private static final Log log = LogFactory.getLog(ModelSynchronizer.class); private Model m; - - public ModelSynchronizer (Model synchronizee) { + private String hash; + + public ModelSynchronizer(Model synchronizee, String name) { this.m = synchronizee; - } - - public void addedStatement(Statement arg0) { - m.add(arg0); + this.hash = Integer.toHexString(this.hashCode()); + log.debug(String.format("create: %s, wraps %s(%s) as %s", hash, this.m + .getClass().getName(), Integer.toHexString(this.m.hashCode()), + name)); } - public void addedStatements(Statement[] arg0) { - m.add(arg0); + @Override + public void addedStatement(Statement s) { + log.debug(hash + " addedStatement" + s); + m.add(s); } - - public void addedStatements(List arg0) { - m.add(arg0); + @Override + public void addedStatements(Statement[] statements) { + log.debug(hash + " addedStatements: " + statements.length); + m.add(statements); } - - public void addedStatements(StmtIterator arg0) { - m.add(arg0); + @Override + public void addedStatements(List statements) { + log.debug(hash + " addedStatements: " + statements.size()); + m.add(statements); } - - public void addedStatements(Model arg0) { - m.add(arg0); + @Override + public void addedStatements(StmtIterator statements) { + if (log.isDebugEnabled()) { + Set set = statements.toSet(); + log.debug(hash + " addedStatements: " + set.size()); + m.add(new StmtIteratorImpl(set.iterator())); + } else { + m.add(new StmtIteratorImpl(statements)); + } } - - public void notifyEvent(Model arg0, Object arg1) { - if ( arg1 instanceof CloseEvent ) { + @Override + public void addedStatements(Model model) { + log.debug(hash + " addedStatements: " + model.size()); + m.add(model); + } + + @Override + public void notifyEvent(Model model, Object event) { + if (event instanceof CloseEvent) { m.close(); } } - - public void removedStatement(Statement arg0) { - m.remove(arg0); + @Override + public void removedStatement(Statement s) { + log.debug(hash + " removedStatement" + s); + m.remove(s); } - - public void removedStatements(Statement[] arg0) { - m.remove(arg0); - } - - public void removedStatements(List arg0) { - m.remove(arg0); + @Override + public void removedStatements(Statement[] statements) { + log.debug(hash + " removedStatements: " + statements.length); + m.remove(statements); } - - public void removedStatements(StmtIterator arg0) { - m.remove(arg0); + @Override + public void removedStatements(List statements) { + log.debug(hash + " removedStatements: " + statements.size()); + m.remove(statements); } - - public void removedStatements(Model arg0) { - m.remove(arg0); + @Override + public void removedStatements(StmtIterator statements) { + if (log.isDebugEnabled()) { + Set set = statements.toSet(); + log.debug(hash + " removedStatements: " + set.size()); + m.remove(new StmtIteratorImpl(set.iterator())); + } else { + m.remove(new StmtIteratorImpl(statements)); + } } - + + @Override + public void removedStatements(Model model) { + log.debug(hash + " removedStatements: " + model.size()); + m.remove(model); + } + } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/BulkUpdatingOntModel.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/BulkUpdatingOntModel.java index 81c8ae10e..388df3c8f 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/BulkUpdatingOntModel.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/BulkUpdatingOntModel.java @@ -9,8 +9,14 @@ import java.net.URL; import java.util.Iterator; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import com.hp.hpl.jena.graph.BulkUpdateHandler; +import com.hp.hpl.jena.graph.Graph; import com.hp.hpl.jena.graph.Triple; +import com.hp.hpl.jena.graph.impl.GraphWithPerform; +import com.hp.hpl.jena.graph.impl.WrappedBulkUpdateHandler; import com.hp.hpl.jena.ontology.OntModel; import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.ModelFactory; @@ -27,13 +33,31 @@ import com.hp.hpl.jena.util.iterator.Map1; * BulkUpdateHandler. */ public class BulkUpdatingOntModel extends AbstractOntModelDecorator { + private static final Log log = LogFactory + .getLog(BulkUpdatingOntModel.class); + private static final RDFReaderF readerFactory = new RDFReaderFImpl(); private final BulkUpdateHandler buh; - public BulkUpdatingOntModel(OntModel inner, BulkUpdateHandler buh) { + public BulkUpdatingOntModel(OntModel inner) { super(inner); - this.buh = buh; + this.buh = inner.getGraph().getBulkUpdateHandler(); + } + + @SuppressWarnings("deprecation") + private static BulkUpdateHandler getWrappedBulkUpdateHandler(Graph graph) { + if (graph instanceof GraphWithPerform) { + return new WrappedBulkUpdateHandler((GraphWithPerform) graph, + graph.getBulkUpdateHandler()); + } else { + try { + throw new IllegalStateException(); + } catch (IllegalStateException e) { + log.warn("Graph is not an instance of GraphWithPerform", e); + } + return graph.getBulkUpdateHandler(); + } } @SuppressWarnings("deprecation") diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactory.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactory.java index 39ffdf18c..81e1acb46 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactory.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactory.java @@ -4,16 +4,25 @@ package edu.cornell.mannlib.vitro.webapp.rdfservice.adapters; import static com.hp.hpl.jena.ontology.OntModelSpec.OWL_MEM; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import com.hp.hpl.jena.graph.BulkUpdateHandler; import com.hp.hpl.jena.graph.Graph; +import com.hp.hpl.jena.graph.compose.Union; +import com.hp.hpl.jena.graph.impl.WrappedBulkUpdateHandler; import com.hp.hpl.jena.ontology.OntModel; +import com.hp.hpl.jena.ontology.impl.OntModelImpl; import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.impl.ModelCom; /** * Make models that will do proper bulk updates. */ public class VitroModelFactory { + private static final Log log = LogFactory.getLog(VitroModelFactory.class); + public static Model createModel() { return ModelFactory.createDefaultModel(); } @@ -21,38 +30,54 @@ public class VitroModelFactory { public static OntModel createOntologyModel() { return ModelFactory.createOntologyModel(OWL_MEM); } - - public static OntModel createOntologyModel(Model model) { - @SuppressWarnings("deprecation") - BulkUpdateHandler buh = model.getGraph().getBulkUpdateHandler(); - OntModel ontModel = ModelFactory.createOntologyModel(OWL_MEM, model); - return new BulkUpdatingOntModel(ontModel, buh); + public static OntModel createOntologyModel(Model model) { + Graph graph = model.getGraph(); + Model bareModel = new ModelCom(graph); + OntModel ontModel = new OntModelImpl(OWL_MEM, bareModel); + return new BulkUpdatingOntModel(ontModel); } - public static Model createUnion(Model baseModel, Model otherModel) { - @SuppressWarnings("deprecation") - BulkUpdateHandler buh = baseModel.getGraph().getBulkUpdateHandler(); + public static Model createUnion(Model baseModel, Model plusModel) { + Graph baseGraph = baseModel.getGraph(); + Graph plusGraph = plusModel.getGraph(); + BulkUpdatingUnion unionGraph = new BulkUpdatingUnion(baseGraph, + plusGraph); - Model unionModel = ModelFactory.createUnion(baseModel, otherModel); + BulkUpdateHandler buh = getBulkUpdateHandler(unionGraph); + Model unionModel = ModelFactory.createModelForGraph(unionGraph); return new BulkUpdatingModel(unionModel, buh); } - public static OntModel createUnion(OntModel baseModel, OntModel otherModel) { - @SuppressWarnings("deprecation") - BulkUpdateHandler buh = baseModel.getGraph().getBulkUpdateHandler(); + public static OntModel createUnion(OntModel baseModel, OntModel plusModel) { + Graph baseGraph = baseModel.getGraph(); + Graph plusGraph = plusModel.getGraph(); + BulkUpdatingUnion unionGraph = new BulkUpdatingUnion(baseGraph, + plusGraph); - Model unionModel = createUnion((Model) baseModel, (Model) otherModel); + Model unionModel = ModelFactory.createModelForGraph(unionGraph); OntModel unionOntModel = ModelFactory.createOntologyModel(OWL_MEM, unionModel); - return new BulkUpdatingOntModel(unionOntModel, buh); + return new BulkUpdatingOntModel(unionOntModel); } public static Model createModelForGraph(Graph g) { - @SuppressWarnings("deprecation") - BulkUpdateHandler buh = g.getBulkUpdateHandler(); - + BulkUpdateHandler buh = getBulkUpdateHandler(g); return new BulkUpdatingModel(ModelFactory.createModelForGraph(g), buh); } + private static class BulkUpdatingUnion extends Union { + @SuppressWarnings("deprecation") + public BulkUpdatingUnion(Graph L, Graph R) { + super(L, R); + this.bulkHandler = new WrappedBulkUpdateHandler(this, + L.getBulkUpdateHandler()); + } + + } + + @SuppressWarnings("deprecation") + private static BulkUpdateHandler getBulkUpdateHandler(Graph graph) { + return graph.getBulkUpdateHandler(); + } } diff --git a/webapp/test/edu/cornell/mannlib/vitro/testing/RecordingProxy.java b/webapp/test/edu/cornell/mannlib/vitro/testing/RecordingProxy.java new file mode 100644 index 000000000..8698c493b --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/testing/RecordingProxy.java @@ -0,0 +1,116 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.testing; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * The create() method creates a dynamic Proxy that wraps your inner object, and + * implements your interfaze. + * + * It also implements the MethodCallRecorder interface (although you will need + * to cast it), so you can find out what methods were called on the proxy. + */ +public class RecordingProxy { + public static T create(T inner, Class interfaze) { + RecordingInvocationHandler handler = new RecordingInvocationHandler( + inner); + + ClassLoader classLoader = interfaze.getClassLoader(); + Class[] interfaces = new Class[] { interfaze, + MethodCallRecorder.class }; + return interfaze.cast(Proxy.newProxyInstance(classLoader, interfaces, + handler)); + } + + /** + * The "add-on" interface that allows us to ask what methods were called on + * the proxy since it was created, or since it was reset. + */ + public interface MethodCallRecorder { + List getMethodCalls(); + + List getMethodCallNames(); + + void resetMethodCalls(); + } + + public static class MethodCall { + /** a convenience method to get just the names of the methods called. */ + public static Object justNames(List methodCalls) { + List names = new ArrayList<>(); + for (MethodCall methodCall : methodCalls) { + names.add(methodCall.getName()); + } + return names; + } + + private final String name; + private final List argList; + + public MethodCall(String name, Object[] args) { + this.name = name; + if (args == null) { + this.argList = Collections.emptyList(); + } else { + this.argList = Collections.unmodifiableList(new ArrayList<>( + Arrays.asList(args))); + } + } + + public String getName() { + return name; + } + + public List getArgList() { + return argList; + } + + } + + public static class RecordingInvocationHandler implements InvocationHandler { + private final Object inner; + private final List methodCalls = new ArrayList<>(); + + RecordingInvocationHandler(Object inner) { + this.inner = inner; + } + + List getMethodCalls() { + return new ArrayList<>(methodCalls); + } + + void reset() { + methodCalls.clear(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + switch (method.getName()) { + case "getMethodCalls": + return new ArrayList(methodCalls); + case "getMethodCallNames": + return MethodCall.justNames(methodCalls); + case "resetMethodCalls": + methodCalls.clear(); + return null; + case "equals": + if (args == null) return false; + if (args.length == 0) return false; + return args[0].equals(inner); + default: + methodCalls.add(new MethodCall(method.getName(), args)); + return method.invoke(inner, args); + } + } + + } + +} \ No newline at end of file diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactoryTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactoryTest.java new file mode 100644 index 000000000..3b4edc231 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/rdfservice/adapters/VitroModelFactoryTest.java @@ -0,0 +1,722 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.rdfservice.adapters; + +import static com.hp.hpl.jena.ontology.OntModelSpec.OWL_MEM; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import com.hp.hpl.jena.graph.BulkUpdateHandler; +import com.hp.hpl.jena.graph.impl.GraphWithPerform; +import com.hp.hpl.jena.graph.impl.SimpleBulkUpdateHandler; +import com.hp.hpl.jena.mem.GraphMem; +import com.hp.hpl.jena.ontology.OntModel; +import com.hp.hpl.jena.rdf.listeners.StatementListener; +import com.hp.hpl.jena.rdf.model.Literal; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.ModelChangedListener; +import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.ResourceFactory; +import com.hp.hpl.jena.rdf.model.Statement; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.testing.RecordingProxy; +import edu.cornell.mannlib.vitro.testing.RecordingProxy.MethodCall; +import edu.cornell.mannlib.vitro.testing.RecordingProxy.MethodCallRecorder; + +/** + * Test that the VitroModelFactory is doing what we want, with regard to bulk + * updates. + * + * With the switch to Jena 2.10, bulk update operations are deprecated, but + * still supported, to a large extent. A Graph still has a bulk updater which + * can be called for bulk operations (like adding multiple statements). However, + * the default Model won't call the bulk updater of its Graph, and neither will + * the default OntModel. + * + * VitroModelFactory creates Models and OntModels that do call the bulk updaters + * of their respective graphs. + * + * --------------- + * + * These tests show which methods are called on which objects (graph, model, + * listener) for both simple operations (add a statement) and bulk operations + * (add multiple statements). + * + * The tests of the default ModelFactory aren't necessary. They do add + * confidence to the testing mechanism, and provide a contrast with the + * VitroModelFactory. + * + * The tests of simple operations may or may not add value. Probably good to + * keep them. + * + * ---------------- + * + * Who knows how we will deal with this in the next Jena upgrade, when + * presumably the bulk updaters will be removed completely. + */ +public class VitroModelFactoryTest extends AbstractTestClass { + private static final Statement SINGLE_STATEMENT = stmt( + resource("http://subject"), property("http://add"), + literal("object")); + private static final Statement[] MULTIPLE_STATEMENTS = { + stmt(resource("http://subject"), property("http://add"), + literal("first")), + stmt(resource("http://subject"), property("http://add"), + literal("second")) }; + + private static final String[] BORING_METHOD_NAMES = { "getPrefixMapping", + "getEventManager", "getBulkUpdateHandler", "find", "getGraph" }; + + // ---------------------------------------------------------------------- + // createModelForGraph() + // ---------------------------------------------------------------------- + + /** + * A ModelGroup has a talkative graph, with a talkative bulkUpdater, wrapped + * by a model that has a talkative listener attached. + * + * But what kind of model? + */ + private static abstract class ModelGroup extends TestObjectGrouping { + final GraphWithPerform g; + final BulkUpdateHandler bu; + final ModelChangedListener l; + final Model m; + + protected ModelGroup() { + MyGraphMem rawGraph = new MyGraphMem(); + this.g = wrapGraph(rawGraph); + this.bu = makeBulkUpdater(this.g, rawGraph); + this.l = makeListener(); + + this.m = wrapModel(makeModel(this.g)); + this.m.register(this.l); + + reset(g); + reset(bu); + reset(l); + reset(m); + } + + protected abstract Model makeModel(GraphWithPerform g); + } + + /** A ModelGroup with a default-style model. */ + private static class DefaultModelGroup extends ModelGroup { + @Override + protected Model makeModel(GraphWithPerform g) { + return ModelFactory.createModelForGraph(g); + } + } + + /** A ModelGroup with a Vitro-style model. */ + private static class VitroModelGroup extends ModelGroup { + @Override + protected Model makeModel(GraphWithPerform g) { + return VitroModelFactory.createModelForGraph(g); + } + } + + private ModelGroup mg; + + @Test + public void addOneToModel() { + mg = new DefaultModelGroup(); + mg.m.add(SINGLE_STATEMENT); + new MethodCalls().add(mg.g, "add").add(mg.bu) + .add(mg.l, "addedStatement").test(); + } + + @Test + public void addOneToVitroModel() { + mg = new VitroModelGroup(); + mg.m.add(SINGLE_STATEMENT); + new MethodCalls().add(mg.g, "add").add(mg.bu) + .add(mg.l, "addedStatement").test(); + } + + @Test + public void addMultipleToModel() { + mg = new DefaultModelGroup(); + mg.m.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(mg.g, "performAdd", "performAdd").add(mg.bu) + .add(mg.l, "addedStatements").test(); + } + + @Test + public void addMultipleToVitroModel() { + mg = new VitroModelGroup(); + mg.m.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(mg.g, "performAdd", "performAdd") + .add(mg.bu, "add").add(mg.l, "addedStatements").test(); + } + + // ---------------------------------------------------------------------- + // createOntologyModel() + // ---------------------------------------------------------------------- + + private OntModelGroup omg; + + /** + * An OntModelGroup is like a ModelGroup, but the model is wrapped in an + * OntModel that has its own talkative listener. + * + * But what kind of Model, and what kind of OntModel? + */ + private static abstract class OntModelGroup extends ModelGroup { + final ModelChangedListener ol; + final OntModel om; + + protected OntModelGroup() { + this.ol = makeListener(); + this.om = wrapOntModel(makeOntModel(this.m)); + this.om.register(this.ol); + } + + protected abstract OntModel makeOntModel(Model m); + + } + + /** + * An OntModelGroup with a default-style OntModel and a default-style Model. + */ + private static class DefaultOntModelGroup extends OntModelGroup { + @Override + protected OntModel makeOntModel(Model m) { + return ModelFactory.createOntologyModel(OWL_MEM, m); + + } + + @Override + protected Model makeModel(GraphWithPerform g) { + return ModelFactory.createModelForGraph(g); + } + } + + /** + * An OntModelGroup with a Vitro-style OntModel and a Vitro-style Model. + */ + private static class VitroOntModelGroup extends OntModelGroup { + @Override + protected OntModel makeOntModel(Model m) { + return VitroModelFactory.createOntologyModel(m); + + } + + @Override + protected Model makeModel(GraphWithPerform g) { + return VitroModelFactory.createModelForGraph(g); + } + } + + @Test + public void addOneToOntModel() { + omg = new DefaultOntModelGroup(); + omg.om.add(SINGLE_STATEMENT); + new MethodCalls().add(omg.g, "add").add(omg.bu) + .add(omg.l, "addedStatement").add(omg.ol, "addedStatement") + .test(); + } + + @Test + public void addOneToVitroOntModel() { + omg = new VitroOntModelGroup(); + omg.om.add(SINGLE_STATEMENT); + new MethodCalls().add(omg.g, "add").add(omg.bu) + .add(omg.l, "addedStatement").add(omg.ol, "addedStatement") + .test(); + } + + @Test + public void addMultipleToOntModel() { + omg = new DefaultOntModelGroup(); + omg.om.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(omg.g, "add", "add").add(omg.bu) + .add(omg.l, "addedStatement", "addedStatement") + .add(omg.ol, "addedStatements").test(); + } + + @Test + public void addMultipleToVitroOntModel() { + omg = new VitroOntModelGroup(); + omg.om.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(omg.g, "performAdd", "performAdd") + .add(omg.bu, "add").add(omg.l, "addedStatements") + .add(omg.ol, "addedStatements").test(); + } + + // ---------------------------------------------------------------------- + // createUnion(Model, Model) + // ---------------------------------------------------------------------- + + /** + * A UnionModelGroup is two ModelGroups, joined into a union that has its + * own talkative listener. + * + * But what kind of ModelGroup, and what kind of union? + */ + private abstract static class UnionModelGroup extends TestObjectGrouping { + final ModelGroup base; + final ModelGroup plus; + final Model m; + final ModelChangedListener l; + + protected UnionModelGroup() { + this.base = makeModelGroup(); + this.plus = makeModelGroup(); + this.m = wrapModel(makeUnion(this.base.m, this.plus.m)); + + this.l = makeListener(); + this.m.register(this.l); + + } + + protected abstract ModelGroup makeModelGroup(); + + protected abstract Model makeUnion(Model baseModel, Model plusModel); + } + + /** + * A UnionModelGroup with default-style Models and a default-style union. + */ + private static class DefaultUnionModelGroup extends UnionModelGroup { + @Override + protected ModelGroup makeModelGroup() { + return new DefaultModelGroup(); + } + + @Override + protected Model makeUnion(Model baseModel, Model plusModel) { + return ModelFactory.createUnion(baseModel, plusModel); + } + } + + /** + * A UnionModelGroup with Vitro-style Models and a Vitro-style union. + */ + private static class VitroUnionModelGroup extends UnionModelGroup { + @Override + protected ModelGroup makeModelGroup() { + return new VitroModelGroup(); + } + + @Override + protected Model makeUnion(Model baseModel, Model plusModel) { + return VitroModelFactory.createUnion(baseModel, plusModel); + } + } + + private UnionModelGroup umg; + + @Test + public void addOneToUnion() { + umg = new DefaultUnionModelGroup(); + umg.m.add(SINGLE_STATEMENT); + new MethodCalls().add(umg.base.g, "add").add(umg.base.bu) + .add(umg.base.l, "addedStatement").add(umg.plus.g) + .add(umg.plus.bu).add(umg.plus.l).add(umg.l, "addedStatement") + .test(); + } + + @Test + public void addOneToVitroUnion() { + umg = new VitroUnionModelGroup(); + umg.m.add(SINGLE_STATEMENT); + new MethodCalls().add(umg.base.g, "add").add(umg.base.bu) + .add(umg.base.l, "addedStatement").add(umg.plus.g) + .add(umg.plus.bu).add(umg.plus.l).add(umg.l, "addedStatement") + .test(); + } + + @Test + public void addMultipleToUnion() { + umg = new DefaultUnionModelGroup(); + umg.m.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(umg.base.g, "add", "add").add(umg.base.bu) + .add(umg.base.l, "addedStatement", "addedStatement") + .add(umg.plus.g).add(umg.plus.bu).add(umg.plus.l) + .add(umg.l, "addedStatements").test(); + } + + @Test + public void addMultipleToVitroUnion() { + umg = new VitroUnionModelGroup(); + umg.m.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(umg.base.g, "performAdd", "performAdd") + .add(umg.base.bu, "add").add(umg.base.l, "addedStatements") + .add(umg.plus.g).add(umg.plus.bu).add(umg.plus.l) + .add(umg.l, "addedStatements").test(); + } + + // ---------------------------------------------------------------------- + // createUnion(OntModel, OntModel) + // ---------------------------------------------------------------------- + + /** + * A UnionOntModelGroup is two OntModelGroups, joined into a union that has + * its own talkative listener. + * + * But what kind of OntModelGroup, and what kind of union? + */ + private abstract static class UnionOntModelGroup extends TestObjectGrouping { + final OntModelGroup base; + final OntModelGroup plus; + final OntModel om; + final ModelChangedListener l; + + protected UnionOntModelGroup() { + this.base = makeOntModelGroup(); + this.plus = makeOntModelGroup(); + this.om = wrapOntModel(makeOntUnion(this.base.om, this.plus.om)); + + this.l = makeListener(); + this.om.register(this.l); + + } + + protected abstract OntModelGroup makeOntModelGroup(); + + protected abstract OntModel makeOntUnion(OntModel baseModel, + OntModel plusModel); + } + + /** + * A UnionOntModelGroup with default-style OntModels and a default-style + * union. + */ + private static class DefaultUnionOntModelGroup extends UnionOntModelGroup { + @Override + protected OntModelGroup makeOntModelGroup() { + return new DefaultOntModelGroup(); + } + + @Override + protected OntModel makeOntUnion(OntModel baseModel, OntModel plusModel) { + return ModelFactory.createOntologyModel(OWL_MEM, + ModelFactory.createUnion(baseModel, plusModel)); + } + } + + /** + * A UnionOntModelGroup with Vitro-style OntModels and a Vitro-style union. + */ + private static class VitroUnionOntModelGroup extends UnionOntModelGroup { + @Override + protected OntModelGroup makeOntModelGroup() { + return new VitroOntModelGroup(); + } + + @Override + protected OntModel makeOntUnion(OntModel baseModel, OntModel plusModel) { + return VitroModelFactory.createUnion(baseModel, plusModel); + } + } + + private UnionOntModelGroup uomg; + + @Test + public void addOneToOntUnion() { + uomg = new DefaultUnionOntModelGroup(); + uomg.om.add(SINGLE_STATEMENT); + new MethodCalls().add(uomg.base.g, "add").add(uomg.base.bu) + .add(uomg.base.l, "addedStatement").add(uomg.plus.g) + .add(uomg.plus.bu).add(uomg.plus.l) + .add(uomg.l, "addedStatement").test(); + } + + @Test + public void addOneToVitroOntUnion() { + uomg = new VitroUnionOntModelGroup(); + uomg.om.add(SINGLE_STATEMENT); + new MethodCalls().add(uomg.base.g, "add").add(uomg.base.bu) + .add(uomg.base.l, "addedStatement").add(uomg.plus.g) + .add(uomg.plus.bu).add(uomg.plus.l) + .add(uomg.l, "addedStatement").test(); + } + + @Test + public void addMultipleToOntUnion() { + uomg = new DefaultUnionOntModelGroup(); + uomg.om.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(uomg.base.g, "add", "add").add(uomg.base.bu) + .add(uomg.base.l, "addedStatement", "addedStatement") + .add(uomg.plus.g).add(uomg.plus.bu).add(uomg.plus.l) + .add(uomg.l, "addedStatements").test(); + } + + @Test + public void addMultipleToVitroOntUnion() { + uomg = new VitroUnionOntModelGroup(); + uomg.om.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(uomg.base.g, "performAdd", "performAdd") + .add(uomg.base.bu, "add").add(uomg.base.l, "addedStatements") + .add(uomg.plus.g).add(uomg.plus.bu).add(uomg.plus.l) + .add(uomg.l, "addedStatements").test(); + } + + // ---------------------------------------------------------------------- + // OntModel of Union of Models + // + // This shouldn't hold any surprises, should it? + // ---------------------------------------------------------------------- + + /** + * A OntModelUnionModelGroup is a UnionModelGroup wrapped by an OntModel + * with a listener. + * + * But what kind of UnionModelGroup, and what kind of OntModel? + */ + private abstract static class OntModelUnionModelGroup extends + TestObjectGrouping { + final UnionModelGroup union; + final OntModel om; + final ModelChangedListener ol; + + protected OntModelUnionModelGroup() { + this.union = makeUnionModelGroup(); + this.om = wrapOntModel(makeOntModel(union.m)); + + this.ol = makeListener(); + this.om.register(this.ol); + reset(om); + } + + protected abstract UnionModelGroup makeUnionModelGroup(); + + protected abstract OntModel makeOntModel(Model m); + } + + /** + * A OntModelUnionModelGroup with default-style UnionModelGroup and a + * default-style OntModel. + */ + private static class DefaultOntModelUnionModelGroup extends + OntModelUnionModelGroup { + @Override + protected UnionModelGroup makeUnionModelGroup() { + return new DefaultUnionModelGroup(); + } + + @Override + protected OntModel makeOntModel(Model m) { + return ModelFactory.createOntologyModel(OWL_MEM, m); + } + } + + /** + * A OntModelUnionModelGroup with Vitro-style UnionModelGroup and a + * Vitro-style OntModel. + */ + private static class VitroOntModelUnionModelGroup extends + OntModelUnionModelGroup { + @Override + protected UnionModelGroup makeUnionModelGroup() { + return new VitroUnionModelGroup(); + } + + @Override + protected OntModel makeOntModel(Model m) { + return VitroModelFactory.createOntologyModel(m); + } + } + + private OntModelUnionModelGroup omumg; + + @Test + public void addOneToOntModeledUnionModel() { + omumg = new DefaultOntModelUnionModelGroup(); + omumg.om.add(SINGLE_STATEMENT); + new MethodCalls().add(omumg.om, "add").add(omumg.ol, "addedStatement") + .add(omumg.union.base.g, "add").add(omumg.union.base.bu) + .add(omumg.union.base.m) + .add(omumg.union.base.l, "addedStatement") + .add(omumg.union.plus.g).add(omumg.union.plus.bu) + .add(omumg.union.plus.m).add(omumg.union.plus.l).test(); + } + + @Test + public void addOneToVitroOntModeledUnionModel() { + omumg = new VitroOntModelUnionModelGroup(); + omumg.om.add(SINGLE_STATEMENT); + new MethodCalls().add(omumg.om, "add").add(omumg.ol, "addedStatement") + .add(omumg.union.base.g, "add").add(omumg.union.base.bu) + .add(omumg.union.base.m) + .add(omumg.union.base.l, "addedStatement") + .add(omumg.union.plus.g).add(omumg.union.plus.bu) + .add(omumg.union.plus.m).add(omumg.union.plus.l).test(); + } + + @Test + public void addMultipleToOntModeledUnionModel() { + omumg = new DefaultOntModelUnionModelGroup(); + omumg.om.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(omumg.om, "add").add(omumg.ol, "addedStatements") + .add(omumg.union.base.g, "add", "add").add(omumg.union.base.bu) + .add(omumg.union.base.m) + .add(omumg.union.base.l, "addedStatement", "addedStatement") + .add(omumg.union.plus.g).add(omumg.union.plus.bu) + .add(omumg.union.plus.m).add(omumg.union.plus.l).test(); + } + + @Test + public void addMultipleToVitroOntModeledUnionModel() { + omumg = new VitroOntModelUnionModelGroup(); + omumg.om.add(MULTIPLE_STATEMENTS); + new MethodCalls().add(omumg.om, "add").add(omumg.ol, "addedStatements") + .add(omumg.union.base.g, "performAdd", "performAdd") + .add(omumg.union.base.bu, "add").add(omumg.union.base.m) + .add(omumg.union.base.l, "addedStatements") + .add(omumg.union.plus.g).add(omumg.union.plus.bu) + .add(omumg.union.plus.m).add(omumg.union.plus.l).test(); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private static Statement stmt(Resource subject, Property predicate, + RDFNode object) { + return ResourceFactory.createStatement(subject, predicate, object); + } + + private static Resource resource(String uri) { + return ResourceFactory.createResource(uri); + } + + private static Property property(String uri) { + return ResourceFactory.createProperty(uri); + } + + private static Literal literal(String value) { + return ResourceFactory.createPlainLiteral(value); + } + + /** Just for debugging */ + private void dumpMethodCalls(String message, Object proxy) { + System.out.println(message + " method calls:"); + for (MethodCall call : ((MethodCallRecorder) proxy).getMethodCalls()) { + String formatted = " " + call.getName(); + for (Object arg : call.getArgList()) { + formatted += " " + arg.getClass(); + } + System.out.println(formatted); + } + + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + /** + * The latest Graph classes allow you to get their BulkUpdateHandler, but + * won't allow you to set it. + */ + private static class MyGraphMem extends GraphMem { + public void setBulkUpdateHandler(BulkUpdateHandler bulkHandler) { + this.bulkHandler = bulkHandler; + } + } + + /** + * A collection of "CallNames", each of which holds a list of expected + * calls, a recording proxy from which we can get the actual calls, and a + * method to compare them. + */ + private static class MethodCalls { + private final List list = new ArrayList<>(); + + public MethodCalls add(Object proxy, String... names) { + list.add(new CallNames((MethodCallRecorder) proxy, names)); + return this; + } + + /** + * Create a string that represents all of the expected method calls. + * Create a string that represents all of the interesting actual calls. + * Compare the strings. + */ + private void test() { + try (StringWriter expectSw = new StringWriter(); + PrintWriter expectWriter = new PrintWriter(expectSw, true); + StringWriter actualSw = new StringWriter(); + PrintWriter actualWriter = new PrintWriter(actualSw, true);) { + for (CallNames calls : list) { + expectWriter.println(Arrays.asList(calls.names)); + actualWriter.println(filterMethodNames(calls.proxy + .getMethodCallNames())); + } + assertEquals(expectSw.toString(), actualSw.toString()); + } catch (IOException e) { + fail(e.toString()); + } + } + + private List filterMethodNames(List raw) { + List filtered = new ArrayList<>(raw); + filtered.removeAll(Arrays.asList(BORING_METHOD_NAMES)); + return filtered; + } + + private static class CallNames { + private final MethodCallRecorder proxy; + private final String[] names; + + public CallNames(MethodCallRecorder proxy, String[] names) { + this.proxy = proxy; + this.names = names; + } + + } + + } + + /** + * Some utility methods for creating a group of test objects. + */ + private static abstract class TestObjectGrouping { + protected GraphWithPerform wrapGraph(MyGraphMem raw) { + return RecordingProxy.create(raw, GraphWithPerform.class); + } + + protected BulkUpdateHandler makeBulkUpdater(GraphWithPerform g, + MyGraphMem raw) { + SimpleBulkUpdateHandler rawBu = new SimpleBulkUpdateHandler(g); + BulkUpdateHandler bu = RecordingProxy.create(rawBu, + BulkUpdateHandler.class); + raw.setBulkUpdateHandler(bu); + return bu; + } + + protected static ModelChangedListener makeListener() { + return RecordingProxy.create(new StatementListener(), + ModelChangedListener.class); + } + + protected Model wrapModel(Model m) { + return RecordingProxy.create(m, Model.class); + } + + protected OntModel wrapOntModel(OntModel om) { + return RecordingProxy.create(om, OntModel.class); + } + + protected T reset(T proxy) { + ((MethodCallRecorder) proxy).resetMethodCalls(); + return proxy; + } + } + +}