/*
* Copyright (c) 2009-2012 Clark & Parsia, LLC. <http://www.clarkparsia.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.clarkparsia.empire.impl;
import com.clarkparsia.empire.ds.DataSourceException;
import com.clarkparsia.empire.ds.MutableDataSource;
import com.clarkparsia.empire.ds.SupportsNamedGraphs;
import com.clarkparsia.empire.SupportsRdfId;
import com.clarkparsia.empire.ds.SupportsTransactions;
import com.clarkparsia.empire.ds.DataSourceUtil;
import com.clarkparsia.empire.ds.QueryException;
import com.clarkparsia.empire.ds.impl.TransactionalDataSource;
import com.clarkparsia.empire.Empire;
import com.clarkparsia.empire.EmpireException;
import com.clarkparsia.empire.EmpireGenerated;
import com.clarkparsia.empire.EmpireOptions;
import com.clarkparsia.empire.annotation.InvalidRdfException;
import com.clarkparsia.empire.annotation.RdfGenerator;
import com.clarkparsia.empire.annotation.RdfsClass;
import com.clarkparsia.empire.annotation.AnnotationChecker;
import com.complexible.common.openrdf.model.Graphs;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.openrdf.model.Graph;
import org.openrdf.model.impl.GraphImpl;
import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.Entity;
import javax.persistence.PrePersist;
import javax.persistence.EntityListeners;
import javax.persistence.PostPersist;
import javax.persistence.PostRemove;
import javax.persistence.PostUpdate;
import javax.persistence.PostLoad;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.AccessibleObject;
import java.util.Map;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.HashSet;
import java.util.Collections;
import java.util.WeakHashMap;
import java.util.Set;
import java.net.URI;
import static com.clarkparsia.empire.util.BeanReflectUtil.getAnnotatedFields;
import static com.clarkparsia.empire.util.BeanReflectUtil.getAnnotatedGetters;
import static com.clarkparsia.empire.util.BeanReflectUtil.asSetter;
import static com.clarkparsia.empire.util.BeanReflectUtil.safeGet;
import static com.clarkparsia.empire.util.BeanReflectUtil.safeSet;
import static com.clarkparsia.empire.util.BeanReflectUtil.hasAnnotation;
import static com.clarkparsia.empire.util.BeanReflectUtil.getAnnotatedMethods;
import com.clarkparsia.empire.util.EmpireUtil;
import com.clarkparsia.empire.util.BeanReflectUtil;
import com.google.common.base.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>Implementation of the JPA {@link EntityManager} interface to support the persistence model over
* an RDF database.</p>
*
* @author Michael Grove
* @since 0.1
* @version 0.7
*
* @see EntityManager
* @see com.clarkparsia.empire.ds.DataSource
*/
public final class EntityManagerImpl implements EntityManager {
/**
* The logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(EntityManagerImpl.class.getName());
/**
* Whether or not this EntityManagerImpl is open
*/
private boolean mIsOpen = false;
/**
* The underlying data source
*/
private MutableDataSource mDataSource;
/**
* The current transaction
*/
private EntityTransaction mTransaction;
/**
* The Entity Listeners for our managed entities.
*/
private Map<Object, Collection<Object>> mManagedEntityListeners = new WeakHashMap<Object, Collection<Object>>();
/**
* The current collapsed view of a DataSourceOperation which is a merged set of adds & removes to the DataSource.
* Used during the canonical EntityManager operations such as merge, persist, remove
*/
private DataSourceOperation mOp;
/**
* The list of things which are ready to be cascaded. They are tracked in this list to help prevent infinite loops
*/
private Collection<Object> mCascadePending = new HashSet<Object>();
/**
* Create a new EntityManagerImpl
* @param theSource the underlying RDF datasource used for persistence operations
*/
public EntityManagerImpl(MutableDataSource theSource) {
// TODO: sparql for everything, just convert serql into sparql
// TODO: work like JPA/hibernate -- if something does not have a @Transient on it, convert it. we'll just need to coin a URI in those cases
// TODO: add an @RdfsLabel annotation that will use the value of a property as the label during annotation
// TODO: support for owl/rdfs annotations not mappable to JPA annotations such as min/max cardinality and others.
mIsOpen = true;
mDataSource = theSource;
}
/**
* @inheritDoc
*/
public void flush() {
assertOpen();
// we'll do nothing here since our default implementation doesn't queue up changes, they're made
// as soon as remove/persist are called
}
/**
* @inheritDoc
*/
public void setFlushMode(final FlushModeType theFlushModeType) {
assertOpen();
if (theFlushModeType != FlushModeType.AUTO) {
throw new IllegalArgumentException("Commit style flush mode not supported");
}
}
/**
* @inheritDoc
*/
public FlushModeType getFlushMode() {
assertOpen();
return FlushModeType.AUTO;
}
/**
* @inheritDoc
*/
public void lock(final Object theObj, final LockModeType theLockModeType) {
throw new PersistenceException("Lock is not supported.");
}
/**
* @inheritDoc
*/
public void refresh(Object theObj) {
assertStateOk(theObj);
assertContains(theObj);
Object aDbObj = find(theObj.getClass(), EmpireUtil.asSupportsRdfId(theObj).getRdfId());
Collection<AccessibleObject> aAccessors = new HashSet<AccessibleObject>();
aAccessors.addAll(getAnnotatedFields(aDbObj.getClass()));
aAccessors.addAll(getAnnotatedGetters(aDbObj.getClass(), true));
if (theObj instanceof EmpireGenerated) {
((EmpireGenerated)theObj).setAllTriples(((EmpireGenerated)aDbObj).getAllTriples());
((EmpireGenerated)theObj).setInstanceTriples(((EmpireGenerated)aDbObj).getInstanceTriples());
}
try {
for (AccessibleObject aAccess : aAccessors) {
Object aValue = safeGet(aAccess, aDbObj);
AccessibleObject aSetter = asSetter(aDbObj.getClass(), aAccess);
safeSet(aSetter, theObj, aValue);
}
}
catch (InvocationTargetException e) {
throw new PersistenceException(e);
}
}
/**
* @inheritDoc
*/
public void clear() {
assertOpen();
cleanState();
}
/**
* @inheritDoc
*/
public boolean contains(final Object theObj) {
assertStateOk(theObj);
try {
return DataSourceUtil.exists(getDataSource(), theObj);
}
catch (DataSourceException e) {
throw new PersistenceException(e);
}
}
/**
* @inheritDoc
*/
public Query createQuery(final String theQueryString) {
return getDataSource().getQueryFactory().createQuery(theQueryString);
}
/**
* @inheritDoc
*/
public Query createNamedQuery(final String theName) {
return getDataSource().getQueryFactory().createNamedQuery(theName);
}
/**
* @inheritDoc
*/
public Query createNativeQuery(final String theQueryString) {
return getDataSource().getQueryFactory().createNativeQuery(theQueryString);
}
/**
* @inheritDoc
*/
public Query createNativeQuery(final String theQueryString, final Class theResultClass) {
return getDataSource().getQueryFactory().createNativeQuery(theQueryString, theResultClass);
}
/**
* @inheritDoc
*/
public Query createNativeQuery(final String theQueryString, final String theResultSetMapping) {
return getDataSource().getQueryFactory().createNativeQuery(theQueryString, theResultSetMapping);
}
/**
* @inheritDoc
*/
public void joinTransaction() {
assertOpen();
// TODO: maybe do something? I don't really understand what this method is supposed to do. from the javadoc
// the intent is not clear. i need to do a little more reading on this. for now, lets make it fail
// like a user would expect, but not do anything. that's not ideal, but we'll eventually sort this out.
}
/**
* @inheritDoc
*/
public Object getDelegate() {
return mDataSource;
}
/**
* @inheritDoc
*/
public void close() {
if (!isOpen()) {
throw new IllegalStateException("EntityManager is already closed.");
}
getDataSource().disconnect();
mIsOpen = false;
cleanState();
}
/**
* Clean up the current state of the EntityManager, release attached entities and the like.
*/
private void cleanState() {
mManagedEntityListeners.clear();
}
/**
* @inheritDoc
*/
public boolean isOpen() {
return mIsOpen;
}
/**
* @inheritDoc
*/
public EntityTransaction getTransaction() {
if (mTransaction == null) {
mTransaction = new DataSourceEntityTransaction(asSupportsTransactions());
}
return mTransaction;
}
/**
* @inheritDoc
*/
public void persist(final Object theObj) {
assertStateOk(theObj);
try {
assertNotContains(theObj);
}
catch (Throwable e) {
throw new EntityExistsException(e);
}
try {
prePersist(theObj);
boolean isTopOperation = (mOp == null);
DataSourceOperation aOp = new DataSourceOperation();
Graph aData = RdfGenerator.asRdf(theObj);
if (doesSupportNamedGraphs() && EmpireUtil.hasNamedGraphSpecified(theObj)) {
aOp.add(EmpireUtil.getNamedGraph(theObj), aData);
}
else {
aOp.add(aData);
}
joinCurrentDataSourceOperation(aOp);
cascadeOperation(theObj, new IsPersistCascade(), new MergeCascade());
finishCurrentDataSourceOperation(isTopOperation);
postPersist(theObj);
}
catch (InvalidRdfException ex) {
throw new IllegalStateException(ex);
}
catch (DataSourceException ex) {
throw new PersistenceException(ex);
}
}
private MutableDataSource getDataSource() {
return (MutableDataSource) getDelegate();
}
private void finishCurrentDataSourceOperation(boolean theIsTop) throws DataSourceException {
if (theIsTop) {
mCascadePending.clear();
mOp.execute();
mOp = null;
}
}
/**
* @inheritDoc
*/
@SuppressWarnings("unchecked")
public <T> T merge(final T theT) {
assertStateOk(theT);
Graph aExistingData = null;
if (theT instanceof EmpireGenerated) {
aExistingData = ((EmpireGenerated) theT).getInstanceTriples();
}
if (aExistingData == null || aExistingData.isEmpty()) {
// it looks like this bean instance does not have instance triples set properly (for some reason)
// if we assume that aExistingData is empty, then no triples will be removed in the try/catch section below,
// which can lead to duplicate triples
// while in ideal world, this situation should not occur, below is an attempt to alleviate the case (i.e.,
// find out what the instance triples actually are)
try {
if (theT instanceof EmpireGenerated) {
// if bean has been generated by Empire, then we can try to read its copy from the database, and use the triples from that copy
Object aDbObj = find(((EmpireGenerated) theT).getInterfaceClass(), EmpireUtil.asSupportsRdfId(theT).getRdfId());
if (aDbObj != null) {
aExistingData = ((EmpireGenerated) aDbObj).getInstanceTriples();
}
else {
aExistingData = new GraphImpl();
}
}
else {
// as a fall back, we can perform a describe to find all related triples for the individual;
// unfortunately, describe returns more information (in fact, it probably gives us more what getAllTriples() would return
// rather than getInstanceTriples()), but there is not much else we can do
aExistingData = assertContainsAndDescribe(theT);
}
}
catch (IllegalArgumentException e) {
// when everything else fails, just assume that existing data was indeed empty ...
aExistingData = new GraphImpl();
}
}
try {
preUpdate(theT);
Graph aData = RdfGenerator.asRdf(theT);
boolean isTopOperation = (mOp == null);
DataSourceOperation aOp = new DataSourceOperation();
if (doesSupportNamedGraphs() && EmpireUtil.hasNamedGraphSpecified(theT)) {
java.net.URI aGraphURI = EmpireUtil.getNamedGraph(theT);
aOp.remove(aGraphURI, aExistingData);
aOp.add(aGraphURI, aData);
}
else {
aOp.remove(aExistingData);
aOp.add(aData);
}
joinCurrentDataSourceOperation(aOp);
// cascade the merge
cascadeOperation(theT, new IsMergeCascade(), new MergeCascade());
finishCurrentDataSourceOperation(isTopOperation);
postUpdate(theT);
return theT;
}
catch (DataSourceException ex) {
throw new PersistenceException(ex);
}
catch (InvalidRdfException ex) {
throw new IllegalStateException(ex);
}
}
private void joinCurrentDataSourceOperation(final DataSourceOperation theOp) {
if (mOp == null) {
mOp = theOp;
}
else {
mOp.merge(theOp);
}
}
private <T> void cascadeOperation(T theT, CascadeTest theCascadeTest, CascadeAction theAction) {
// if we've already cascaded this, move on to the next thing, we don't want infinite loops
if (mCascadePending.contains(theT)) {
return;
}
else {
mCascadePending.add(theT);
}
Collection<AccessibleObject> aAccessors = new HashSet<AccessibleObject>();
aAccessors.addAll(getAnnotatedFields(theT.getClass()));
aAccessors.addAll(getAnnotatedGetters(theT.getClass(), true));
for (AccessibleObject aObj : aAccessors) {
if (theCascadeTest.apply(aObj)) {
try {
Object aAccessorValue = BeanReflectUtil.safeGet(aObj, theT);
if (aAccessorValue == null) {
continue;
}
theAction.apply(aAccessorValue);
}
catch (Exception e) {
throw new PersistenceException(e);
}
}
}
}
private class MergeCascade extends CascadeAction {
public void cascade(Object theValue) {
// is it the correct JPA behavior to persist a value when it does not exist during a cascaded
// merge? or should that be a PersistenceException just like any normal merge for an un-managed
// object?
if (AnnotationChecker.isValid(theValue.getClass())) {
if (contains(theValue)) {
merge(theValue);
}
else {
persist(theValue);
}
}
}
}
private class RemoveCascade extends CascadeAction {
public void cascade(Object theValue) {
if (AnnotationChecker.isValid(theValue.getClass())) {
if (contains(theValue)) {
remove(theValue);
}
}
}
}
private class IsMergeCascade extends CascadeTest {
public boolean apply(final AccessibleObject theValue) {
return BeanReflectUtil.isMergeCascade(theValue);
}
}
private class IsRemoveCascade extends CascadeTest {
public boolean apply(final AccessibleObject theValue) {
return BeanReflectUtil.isRemoveCascade(theValue);
}
}
private class IsPersistCascade extends CascadeTest {
public boolean apply(final AccessibleObject theValue) {
return BeanReflectUtil.isPersistCascade(theValue);
}
}
private abstract class CascadeTest implements Predicate<AccessibleObject> {
}
private abstract class CascadeAction implements Predicate<Object> {
public abstract void cascade(Object theObj);
public final boolean apply(Object theObj) {
// is it an error if you specify a cascade type for something that cannot be
// cascaded? such as strings, or a non Entity instance?
if (Collection.class.isAssignableFrom(theObj.getClass())) {
for (Object aValue : (Collection) theObj) {
cascade(aValue);
}
}
else {
cascade(theObj);
}
return true;
}
}
/**
* @inheritDoc
*/
public void remove(final Object theObj) {
assertStateOk(theObj);
Graph aData = assertContainsAndDescribe(theObj);
try {
preRemove(theObj);
boolean isTopOperation = (mOp == null);
DataSourceOperation aOp = new DataSourceOperation();
// we were transforming the current object to RDF and deleting that, but i dont think that's the intended
// behavior. you want to delete everything about the object in the database, not the properties specifically
// on the thing being deleted -- there's an obvious case where there could be a delta between them and you
// don't delete everything. so we'll do a describe on the object and delete everything we know about it
// i.e. everything where its in the subject position.
//Graph aData = RdfGenerator.asRdf(theObj);
//Graph aData = DataSourceUtil.describe(getDataSource(), theObj);
if (doesSupportNamedGraphs() && EmpireUtil.hasNamedGraphSpecified(theObj)) {
aOp.remove(EmpireUtil.getNamedGraph(theObj), aData);
}
else {
aOp.remove(aData);
}
joinCurrentDataSourceOperation(aOp);
cascadeOperation(theObj, new IsRemoveCascade(), new RemoveCascade());
finishCurrentDataSourceOperation(isTopOperation);
postRemove(theObj);
}
catch (DataSourceException ex) {
throw new PersistenceException(ex);
}
}
/**
* @inheritDoc
*/
public <T> T find(final Class<T> theClass, final Object theObj) {
assertOpen();
try {
AnnotationChecker.assertValid(theClass);
}
catch (EmpireException e) {
throw new IllegalArgumentException(e);
}
try {
if (DataSourceUtil.exists(getDataSource(), EmpireUtil.asPrimaryKey(theObj))) {
T aT = RdfGenerator.fromRdf(theClass, EmpireUtil.asPrimaryKey(theObj), getDataSource());
postLoad(aT);
return aT;
}
else {
return null;
}
}
catch (InvalidRdfException e) {
throw new IllegalArgumentException("Type is not valid, or object with key is not a valid Rdf Entity.", e);
}
catch (DataSourceException e) {
throw new PersistenceException(e);
}
}
/**
* @inheritDoc
*/
public <T> T getReference(final Class<T> theClass, final Object theObj) {
assertOpen();
T aObj = find(theClass, theObj);
if (aObj == null) {
throw new EntityNotFoundException("Cannot find Entity with primary key: " + theObj);
}
return aObj;
}
/**
* Enforce that the object exists in the database
* @param theObj the object that should exist
* @throws IllegalArgumentException thrown if the object does not exist in the database
*/
private void assertContains(Object theObj) {
if (!contains(theObj)) {
throw new IllegalArgumentException("Entity does not exist: " + theObj);
}
}
/**
* Performs the same checks as assertContains, but returns the Graph describing the resource as a result so you
* do not need to perform a subsequent call to the database to get the data that is checked during containment
* checks in the first place, thus saving a query to the database.
* @param theObj the object that should exist.
* @return The graph describing the resource
* @throws IllegalArgumentException thrown if the object does not exist in the database
*/
private Graph assertContainsAndDescribe(Object theObj) {
assertStateOk(theObj);
try {
Graph aGraph = DataSourceUtil.describe(getDataSource(), theObj);
if (aGraph.isEmpty()) {
throw new IllegalArgumentException("Entity does not exist: " + theObj);
}
return aGraph;
}
catch (QueryException e) {
throw new PersistenceException(e);
}
}
/**
* Enforce that the object does not exist in the database
* @param theObj the object that should not exist
* @throws IllegalArgumentException thrown if the object already exists in the database
*/
private void assertNotContains(Object theObj) {
if (contains(theObj)) {
throw new IllegalArgumentException("Entity already exists: " + theObj);
}
}
/**
* Assert that the state of the EntityManager is ok; that it is open, and the specified object is a valid Rdf entity.
* @param theObj the object to check
* @throws IllegalStateException if the EntityManager is closed
* @throws IllegalArgumentException thrown if the value is not a valid Rdf Entity
*/
private void assertStateOk(Object theObj) {
assertOpen();
assertSupported(theObj);
}
/**
* Enforce that the EntityManager is open
* @throws IllegalStateException thrown if the EntityManager is closed or not yet open
*/
private void assertOpen() {
if (!isOpen()) {
throw new IllegalStateException("Cannot perform operation, EntityManager is not open");
}
}
/**
* Assert that the object can be supported by this EntityManager, that is it a valid Rdf entity
* @param theObj the object to validate
* @throws IllegalArgumentException thrown if the object is not a valid Rdf entity.
*/
private void assertSupported(final Object theObj) {
Preconditions.checkArgument(theObj != null, "null objects are not supported");
Preconditions.checkArgument(theObj instanceof SupportsRdfId, "Persistent RDF objects must implement the SupportsRdfId interface.");
assertEntity(theObj);
assertRdfClass(theObj);
try {
AnnotationChecker.assertValid(theObj.getClass());
}
catch (EmpireException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Enforce that the object has the {@link Entity} annotation
* @param theObj the instance
* @throws IllegalArgumentException if the instances does not have the Entity Annotation
* @see Entity
*/
private void assertEntity(final Object theObj) {
if (EmpireOptions.ENFORCE_ENTITY_ANNOTATION) {
assertHasAnnotation(theObj, Entity.class);
}
}
/**
* Enforce that the object has the {@link com.clarkparsia.empire.annotation.RdfsClass} annotation
* @param theObj the instance
* @throws IllegalArgumentException if the instances does not have the RdfClass annotation
* @see com.clarkparsia.empire.annotation.RdfsClass
*/
private void assertRdfClass(final Object theObj) {
assertHasAnnotation(theObj, RdfsClass.class);
}
/**
* Verify that the instance has the specified annotation
* @param theObj the instance
* @param theAnnotation the annotation the instance is required to have
* @throws IllegalArgumentException thrown if the instance does not have the required annotation
*/
private void assertHasAnnotation(final Object theObj, final Class<? extends Annotation> theAnnotation) {
if (!hasAnnotation(theObj.getClass(), theAnnotation)) {
throw new IllegalArgumentException("Object (" + theObj.getClass() + ") is not an " + theAnnotation.getSimpleName());
}
}
/**
* Returns whether or not the data source supports operations on named sub-graphs
* @return true if it does, false otherwise. Returning true indicates calls to {@link #asSupportsNamedGraphs()}
* will return successfully without a ClassCastException
*/
private boolean doesSupportNamedGraphs() {
return getDataSource() instanceof SupportsNamedGraphs;
}
/**
* Returns a reference to an object (the data source) which can perform operations on named sub-graphs
* @return the data source as a {@link com.clarkparsia.empire.ds.SupportsNamedGraphs}
* @throws ClassCastException thrown if the data source does not implements SupportsNamedGraphs
*/
private SupportsNamedGraphs asSupportsNamedGraphs() {
return (SupportsNamedGraphs) getDataSource();
}
/**
* Returns a reference to an object (the DataSource) which supports Transactions. Transaction support is
* provided by either the DataSource's native transaction support, or via our naive
* {@link TransactionalDataSource transactional wrapper}.
* @return a source which supports transactions
*/
private SupportsTransactions asSupportsTransactions() {
if (mDataSource instanceof SupportsTransactions) {
return (SupportsTransactions) mDataSource;
}
else {
// it doesnt support transactions natively, so we'll wrap it in our naive transaction support.
return new TransactionalDataSource(mDataSource);
}
}
/**
* Fire the PostPersist lifecycle event for the Entity
* @param theObj the Entity to fire the event for
*/
private void postPersist(final Object theObj) {
handleLifecycleCallback(theObj, PostPersist.class);
}
/**
* Fire the PostRemove lifecycle event for the Entity
* @param theObj the Entity to fire the event for
*/
private void postRemove(final Object theObj) {
handleLifecycleCallback(theObj, PostRemove.class);
}
/**
* Fire the PostLoad lifecycle event for the Entity
* @param theObj the Entity to fire the event for
*/
private void postLoad(final Object theObj) {
handleLifecycleCallback(theObj, PostLoad.class);
}
/**
* Fire the PreRemove lifecycle event for the Entity
* @param theObj the Entity to fire the event for
*/
private void preRemove(final Object theObj) {
handleLifecycleCallback(theObj, PreRemove.class);
}
/**
* Fire the PreUpdate lifecycle event for the Entity
* @param theObj the Entity to fire the event for
*/
private void preUpdate(final Object theObj) {
handleLifecycleCallback(theObj, PreUpdate.class);
}
/**
* Fire the PostUpdate lifecycle event for the Entity
* @param theObj the Entity to fire the event for
*/
private void postUpdate(final Object theObj) {
handleLifecycleCallback(theObj, PostUpdate.class);
}
/**
* Fire the PrePersist lifecycle event for the Entity
* @param theObj the entity to fire the event for
*/
private void prePersist(final Object theObj) {
handleLifecycleCallback(theObj, PrePersist.class);
}
/**
* Handle the dispatching of the specified lifecycle event
* @param theObj the object involved in the event
* @param theLifecycleAnnotation the annotation denoting the event, such as {@link PrePersist}, {@link PostLoad}, etc.
*/
private void handleLifecycleCallback(final Object theObj, final Class<? extends Annotation> theLifecycleAnnotation) {
if (theObj == null) {
return;
}
Collection<Method> aMethods = getAnnotatedMethods(theObj.getClass(), theLifecycleAnnotation);
// Entity methods take no arguments...
try {
for (Method aMethod : aMethods) {
aMethod.invoke(theObj);
}
}
catch (Exception e) {
LOGGER.error("There was an error during entity lifecycle notification for annotation: " +
theLifecycleAnnotation + " on object: " + theObj +".", e);
throw new PersistenceException(e.getCause());
}
for (Object aListener : getEntityListeners(theObj)) {
Collection<Method> aListenerMethods = getAnnotatedMethods(aListener.getClass(), theLifecycleAnnotation);
// EntityListeners methods take a single arguement, the entity
try {
for (Method aListenerMethod : aListenerMethods) {
aListenerMethod.invoke(aListener, theObj);
}
}
catch (Exception e) {
LOGGER.error("There was an error during lifecycle notification for annotation: " +
theLifecycleAnnotation + " on object: " + theObj + ".", e);
throw new PersistenceException(e.getCause());
}
}
}
/**
* Get or create the list of EntityListeners for an object. If a list is created, it will be kept around and
* re-used for later persistence operations.
* @param theObj the object to get EntityLIsteners for
* @return the list of EntityListeners for the object, or null if they do not exist
*/
private Collection<Object> getEntityListeners(final Object theObj) {
Collection<Object> aListeners = mManagedEntityListeners.get(theObj);
if (aListeners == null) {
EntityListeners aEntityListeners = BeanReflectUtil.getAnnotation(theObj.getClass(), EntityListeners.class);
if (aEntityListeners != null) {
// if there are entity listeners, lets create them
aListeners = new LinkedHashSet<Object>();
for (Class<?> aClass : aEntityListeners.value()) {
try {
aListeners.add(Empire.get().instance(aClass));
}
catch (Exception e) {
LOGGER.error("There was an error instantiating an EntityListener. ", e);
}
}
mManagedEntityListeners.put(theObj, aListeners);
}
else {
aListeners = Collections.emptyList();
}
}
return aListeners;
}
/**
* Class which encapsulates a set of adds & removes to a DataSource. Used to process a set of changes in a single
* operation, well, two operations. Remove and then Add. Also will verify that all objects that should have been
* added/removed from the KB have been added or removed.
* @author Michael Grove
* @since 0.7
* @version 0.7
*/
protected class DataSourceOperation {
// HashMap's used here rather than the more generic Map interface because we allow null keys (no specified
// named graph) which HashMap allows, while generically Map makes no guarantees about this, so we're explicit here.
private final Map<java.net.URI, Graph> mAdd;
private final Map<java.net.URI, Graph> mRemove;
private final Set<Object> mVerifyAdd = Sets.newHashSet();
private final Set<Object> mVerifyRemove = Sets.newHashSet();
/**
* Create a new DataSourceOperation
*/
DataSourceOperation() {
mAdd = Maps.newHashMap();
mRemove = Maps.newHashMap();
}
/**
* Execute this operation. Removes & adds all the specified data in as few database calls as possible. Then
* verifies the results of the operations
* @throws DataSourceException if there is an error while performing the add/remove operations
* @throws PersistenceException if any objects were failed to be added or removed from the database
*/
public void execute() throws DataSourceException {
// TODO: should this be in its own transaction? or join the current one?
if (getDataSource() instanceof SupportsTransactions) {
((SupportsTransactions)getDataSource()).begin();
}
try {
for (URI aGraphURI : mRemove.keySet()) {
if (doesSupportNamedGraphs() && aGraphURI != null) {
asSupportsNamedGraphs().remove(aGraphURI, mRemove.get(aGraphURI));
}
else {
getDataSource().remove(mRemove.get(aGraphURI));
}
}
for (URI aGraphURI : mAdd.keySet()) {
if (doesSupportNamedGraphs() && aGraphURI != null) {
asSupportsNamedGraphs().add(aGraphURI, mAdd.get(aGraphURI));
}
else {
getDataSource().add(mAdd.get(aGraphURI));
}
}
if (getDataSource() instanceof SupportsTransactions) {
((SupportsTransactions)getDataSource()).commit();
}
verify();
}
catch (DataSourceException e) {
if (getDataSource() instanceof SupportsTransactions) {
((SupportsTransactions)getDataSource()).rollback();
}
}
}
/**
* Add the specified object to the list of objects that should be removed from the database when this operation
* is executed.
* @param theObj the object that should be revmoed from the database when the operation is executed
*/
public void verifyRemove(final Object theObj) {
mVerifyRemove.add(theObj);
}
/**
* Add the specified object to the list of objects that should be added to the database when this operation
* is executed.
* @param theObj the object that should be added to the database when the operation is executed
*/
public void verifyAdd(final Object theObj) {
mVerifyAdd.add(theObj);
}
/**
* Verify that all the objects to be added/removed were completed successfully.
* @throws PersistenceException if an add or remove failed for any reason
*/
private void verify() {
for (Object aObj : mVerifyRemove) {
if (contains(aObj)) {
throw new PersistenceException("Remove failed for object: " + aObj.getClass() + " -> " + EmpireUtil.asSupportsRdfId(aObj).getRdfId());
}
}
for (Object aObj : mVerifyAdd) {
if (!contains(aObj)) {
throw new PersistenceException("Addition failed for object: " + aObj.getClass() + " -> " + EmpireUtil.asSupportsRdfId(aObj).getRdfId());
}
}
}
/**
* Add this graph to the set of data to be added when this operation is executed
* @param theGraph the graph to be added
*/
public void add(final Graph theGraph) {
add(null, theGraph);
}
/**
* Add this graph to the set of data to be added, to the specified named graph, when this operation is executed
* @param theGraphURI the named graph the data should be added to
* @param theGraph the data to add
*/
public void add(final java.net.URI theGraphURI, final Graph theGraph) {
Graph aGraph = mAdd.get(theGraphURI);
if (aGraph == null) {
aGraph = Graphs.newGraph();
}
aGraph.addAll(theGraph);
mAdd.put(theGraphURI, aGraph);
}
/**
* Add this graph to the set of data to be removed when this operation is executed
* @param theGraph the graph to be removed
*/
public void remove(final Graph theGraph) {
remove(null, theGraph);
}
/**
* Add this graph to the set of data to be removed, from the specified named graph, when this operation is executed
* @param theGraphURI the named graph the data should be removed from
* @param theGraph the data to remove
*/
public void remove(final java.net.URI theGraphURI, final Graph theGraph) {
Graph aGraph = mRemove.get(theGraphURI);
if (aGraph == null) {
aGraph = Graphs.newGraph();
}
aGraph.addAll(theGraph);
mRemove.put(theGraphURI, aGraph);
}
/**
* Merge the operation with this one. This will merge all the changes being tracked into a single operation.
* @param theOp the operation to merge
*/
public void merge(final DataSourceOperation theOp) {
for (Map.Entry<URI, Graph> aEntry : theOp.mRemove.entrySet()) {
remove(aEntry.getKey(), aEntry.getValue());
}
for (Map.Entry<URI, Graph> aEntry : theOp.mAdd.entrySet()) {
add(aEntry.getKey(), aEntry.getValue());
}
mVerifyAdd.addAll(theOp.mVerifyAdd);
mVerifyRemove.addAll(theOp.mVerifyRemove);
}
}
}