org.statefulj.persistence.mongo.MongoPersister Maven / Gradle / Ivy
/***
*
* Copyright 2014 Andrew Hall
*
* 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 org.statefulj.persistence.mongo;
import java.lang.reflect.Field;
import java.util.Calendar;
import java.util.List;
import java.util.Random;
import javax.persistence.EmbeddedId;
import org.bson.types.ObjectId;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import static org.statefulj.common.utils.ReflectionUtils.*;
import org.statefulj.fsm.Persister;
import org.statefulj.fsm.StaleStateException;
import org.statefulj.fsm.model.State;
import org.statefulj.persistence.common.AbstractPersister;
import org.statefulj.persistence.mongo.model.StateDocument;
import com.mongodb.DBObject;
public class MongoPersister
extends AbstractPersister
implements
Persister,
ApplicationListener,
BeanDefinitionRegistryPostProcessor,
ApplicationContextAware {
final static FindAndModifyOptions RETURN_NEW = FindAndModifyOptions.options().returnNew(true);
private ApplicationContext appContext;
private String repoId;
private MongoTemplate mongoTemplate;
private String templateId;
/**
* Instantiate the MongoPersister with a specified template. The State field
* on the Entity will be determined by inspection of Entity for the @State annotation
*
* @param states List of the States
* @param start The Start State
* @param clazz The Managed Entity class
* @param mongoTemplate MongoTemplate to use to persist the Managed Entity
*/
public MongoPersister(
List> states,
State start,
Class clazz,
MongoTemplate mongoTemplate) {
super(states, null, start, clazz);
this.mongoTemplate = mongoTemplate;
}
/**
* Instantiate the MongoPersister with a specified template. The MongoPersister
* will use the stateFieldName to determine the State field.
*
* @param states List of the StatesC
* @param stateFieldName The name of the State Field
* @param start The Start State
* @param clazz The Managed Entity class
* @param mongoTemplate MongoTemplate to use to persist the Managed Entity
*/
public MongoPersister(
List> states,
String stateFieldName,
State start,
Class clazz,
MongoTemplate mongoTemplate) {
super(states, stateFieldName, start, clazz);
this.mongoTemplate = mongoTemplate;
}
/**
* Instantiate the MongoPersister with the id of the MongoRepository bean for
* the Managed Entity. The State field on the Entity will be
* determined by inspection of Entity for the @State annotation
*
* @param states List of the States
* @param start The Start State
* @param clazz The Managed Entity class
* @param repoId Bean Id of the Managed Entity's MongoRepository
*/
public MongoPersister(
List> states,
State start,
Class clazz,
String repoId) {
this(states, null, start,clazz, repoId);
}
/**
* Instantiate the MongoPersister with the id of the MongoRepository bean for
* the Managed Entity. The MongoPersister will use the stateFieldName to
* determine the State field.
*
* @param states List of States
* @param stateFieldName The name of the State Field
* @param start The Start State
* @param clazz The Managed Entity class
* @param repoId Bean Id of the Managed Entity's MongoRepository
*/
public MongoPersister(
List> states,
String stateFieldName,
State start,
Class clazz,
String repoId) {
super(states, stateFieldName, start, clazz);
this.repoId = repoId;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.appContext = applicationContext;
}
/**
* Set the current State. This method will ensure that the state in the db matches the expected current state.
* If not, it will throw a StateStateException
*
* @param stateful
* @param current
* @param next
* @throws StaleStateException
*/
public void setCurrent(T stateful, State current, State next) throws StaleStateException {
try {
// Has this Entity been persisted to Mongo?
//
StateDocumentImpl stateDoc = this.getStateDocument(stateful);
if (stateDoc != null && stateDoc.isPersisted()) {
// Update state in the DB
//
updateStateInDB(stateful, current, next, stateDoc);
} else {
// The Entity hasn't been persisted to Mongo - so it exists only
// this Application memory. So, serialize the qualified update to prevent
// concurrency conflicts
//
updateInMemory(stateful, stateDoc, current.getName(), next.getName());
}
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/* (non-Javadoc)
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
this.mongoTemplate = (MongoTemplate)appContext.getBean(this.templateId);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(
BeanDefinitionRegistry registry) throws BeansException {
if (this.mongoTemplate == null) {
if (this.repoId != null) {
// Fetch the MongoTemplate Bean Id
//
BeanDefinition repo = registry.getBeanDefinition(this.repoId);
this.templateId = ((BeanReference)repo.getPropertyValues().get("mongoOperations")).getBeanName();
}
// Check to make sure we have a reference to the MongoTemplate
//
if (this.templateId == null) {
throw new RuntimeException("Unable to obtain a reference to a MongoTemplate");
}
}
// Add in CascadeSupport
//
BeanDefinition mongoCascadeSupportBean = BeanDefinitionBuilder
.genericBeanDefinition(MongoCascadeSupport.class)
.getBeanDefinition();
ConstructorArgumentValues args = mongoCascadeSupportBean.getConstructorArgumentValues();
args.addIndexedArgumentValue(0, this);
registry.registerBeanDefinition(Long.toString((new Random()).nextLong()), mongoCascadeSupportBean);
}
@Override
protected boolean validStateField(Field stateField) {
return stateField.getType().equals(StateDocument.class);
}
@Override
protected Field findIdField(Class> clazz) {
Field idField = getReferencedField(this.getClazz(), Id.class);
if (idField == null) {
idField = getReferencedField(this.getClazz(), javax.persistence.Id.class);
if (idField == null) {
idField = getReferencedField(this.getClazz(), EmbeddedId.class);
}
}
return idField;
}
@Override
protected Class> getStateFieldType() {
return StateDocumentImpl.class;
}
protected Query buildQuery(StateDocumentImpl state, State current) {
return Query.query(new Criteria("_id").is(state.getId()).and("state").is(current.getName()));
}
protected Update buildUpdate(State current, State next) {
Update update = new Update();
update.set("prevState", current.getName());
update.set("state", next.getName());
update.set("updated", Calendar.getInstance().getTime());
return update;
}
protected String getState(T stateful) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
StateDocumentImpl stateDoc = this.getStateDocument(stateful);
return (stateDoc != null) ? stateDoc.getState() : getStart().getName();
}
protected void setState(T stateful, String state) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
StateDocumentImpl stateDoc = this.getStateDocument(stateful);
if (stateDoc == null) {
stateDoc = createStateDocument(stateful);
}
stateDoc.setPrevState(stateDoc.getState());
stateDoc.setState(state);
stateDoc.setUpdated(Calendar.getInstance().getTime());
}
protected StateDocumentImpl getStateDocument(T stateful) throws IllegalArgumentException, IllegalAccessException {
Object stateDoc = getStateField().get(stateful);
if (stateDoc instanceof LazyLoadingProxy) {
stateDoc = ((LazyLoadingProxy)stateDoc).getTarget();
}
return (StateDocumentImpl)stateDoc;
}
protected StateDocumentImpl createStateDocument(T stateful) throws IllegalArgumentException, IllegalAccessException, SecurityException, NoSuchFieldException {
StateDocumentImpl stateDoc = new StateDocumentImpl();
stateDoc.setPersisted(false);
stateDoc.setId(new ObjectId().toHexString());
stateDoc.setState(getStart().getName());
stateDoc.setManagedCollection(this.mongoTemplate.getCollectionName(stateful.getClass()));
stateDoc.setManagedField(this.getStateField().getName());
setStateDocument(stateful, stateDoc);
return stateDoc;
}
protected void setStateDocument(T stateful, StateDocument stateDoc) throws IllegalArgumentException, IllegalAccessException {
getStateField().set(stateful, stateDoc);
}
protected void updateInMemory(T stateful, StateDocumentImpl stateDoc, String current, String next) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException, StaleStateException {
synchronized(stateful) {
if (stateDoc == null) {
stateDoc = createStateDocument(stateful);
}
if (stateDoc.getState().equals(current)) {
setState(stateful, next);
} else {
throwStaleState(current, next);
}
}
}
protected void throwStaleState(String current, String next) throws StaleStateException {
String err = String.format(
"Unable to update state, entity.state=%s, db.state=%s",
current,
next);
throw new StaleStateException(err);
}
protected StateDocumentImpl updateStateDoc(Query query, Update update) {
return (StateDocumentImpl)this.mongoTemplate.findAndModify(query, update, RETURN_NEW, StateDocumentImpl.class);
}
protected StateDocumentImpl findStateDoc(String id) {
return (StateDocumentImpl)this.mongoTemplate.findById(id, StateDocumentImpl.class);
}
@SuppressWarnings("unchecked")
/***
* Cascade the Save to the StateDocument
*
* @param obj
* @param dbo
*/
void onAfterSave(Object stateful, DBObject dbo) {
// Is the Class being saved the managed class?
//
if (stateful.getClass().equals(getClazz())) {
try {
boolean updateStateful = false;
StateDocumentImpl stateDoc = this.getStateDocument((T)stateful);
// If the StatefulDocument doesn't have an associated StateDocument, then
// we need to create a new StateDocument - save the StateDocument and save the
// Stateful Document again so that they both valid DBRef objects
//
if (stateDoc == null) {
stateDoc = createStateDocument((T)stateful);
stateDoc.setUpdated(Calendar.getInstance().getTime());
updateStateful = true;
}
if (!stateDoc.isPersisted()) {
stateDoc.setManagedId(this.getId((T)stateful));
this.mongoTemplate.save(stateDoc);
stateDoc.setPersisted(true);
if (updateStateful) {
this.mongoTemplate.save(stateful);
}
}
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
void onAfterDelete(Class> stateful, DBObject obj) {
if (stateful.equals(getClazz())) {
StateDocumentImpl stateDoc;
Criteria criteria = new Criteria("managedId").is(obj.get(this.getIdField().getName())).
and("managedCollection").is(this.mongoTemplate.getCollectionName(getClazz())).
and("managedField").is(this.getStateField().getName());
stateDoc = this.mongoTemplate.findOne(new Query(criteria), StateDocumentImpl.class);
if (stateDoc != null) {
this.mongoTemplate.remove(stateDoc);
}
}
}
/**
* @param stateful
* @param current
* @param next
* @param stateDoc
* @throws IllegalAccessException
* @throws StaleStateException
*/
private void updateStateInDB(T stateful, State current, State next,
StateDocumentImpl stateDoc) throws IllegalAccessException,
StaleStateException {
// Entity is in the database - perform qualified update based off
// the current State value
//
Query query = buildQuery(stateDoc, current);
Update update = buildUpdate(current, next);
// Update state in DB
//
StateDocumentImpl updatedDoc = updateStateDoc(query, update);
if (updatedDoc != null) {
// Success, update in memory
//
setStateDocument(stateful, updatedDoc);
} else {
// If we aren't able to update - it's most likely that we are out of sync.
// So, fetch the latest value and update the Stateful object. Then throw a RetryException
// This will cause the event to be reprocessed by the FSM
//
updatedDoc = findStateDoc(stateDoc.getId());
if (updatedDoc != null) {
String currentState = stateDoc.getState();
setStateDocument(stateful, updatedDoc);
throwStaleState(currentState, updatedDoc.getState());
} else {
throw new RuntimeException("Unable to find StateDocument with id=" + stateDoc.getId());
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy