package org.orienteer.bpm.camunda.handler;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.wicket.Application;
import org.apache.wicket.Session;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.core.util.lang.PropertyResolver;
import org.apache.wicket.core.util.lang.PropertyResolverConverter;
import org.apache.wicket.core.util.lang.PropertyResolver.IGetAndSet;
import org.apache.wicket.util.string.Strings;
import org.camunda.bpm.engine.ProcessEngineException;
import org.camunda.bpm.engine.impl.db.DbEntity;
import org.camunda.bpm.engine.impl.db.HasDbRevision;
import org.camunda.bpm.engine.impl.db.entitymanager.operation.DbBulkOperation;
import org.orienteer.bpm.camunda.OPersistenceSession;
import org.orienteer.core.OrienteerWebApplication;
import org.orienteer.core.util.OSchemaHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import com.github.raymanrt.orientqb.query.Clause;
import com.github.raymanrt.orientqb.query.Operator;
import com.github.raymanrt.orientqb.query.Parameter;
import com.github.raymanrt.orientqb.query.Query;
import com.github.raymanrt.orientqb.query.core.AbstractQuery;
import com.gitub.raymanrt.orientqb.delete.Delete;
import com.google.common.base.Converter;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Lists;
import com.google.common.reflect.Reflection;
import com.google.common.reflect.TypeToken;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.hook.ORecordHook.RESULT;
import com.orientechnologies.orient.core.hook.ORecordHook.TYPE;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OCommandSQL;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
import static com.github.raymanrt.orientqb.query.Clause.*;
/**
* Abstract implementation of {@link IEntityHandler}
* @param <T> type of {@link DbEntity} to be handled by this instance
*/
public abstract class AbstractEntityHandler<T extends DbEntity> implements IEntityHandler<T> {
private static final PropertyResolverConverter PROPERTY_RESOLVER_CONVERTEER =
new PropertyResolverConverter(OrienteerWebApplication.lookupApplication()
.getConverterLocator(), Locale.getDefault());
protected final Logger logger = LoggerFactory.getLogger(getClass());
private TypeToken<T> type = new TypeToken<T>(getClass()) {};
private final String schemaClass;
private final String pkField;
protected Map<String, String> mappingFromEntityToDoc;
protected Map<String, String> mappingFromDocToEntity;
/**
* Additional map to customize mapping especially from Query to doc queries
*/
protected Map<String, String> mappingFromQueryToDoc = new HashMap<>();
/**
* Converters for changing value. Applied in both ways
*/
protected Map<String, Converter<Object, Object>> mappingConvertors = new HashMap<>();
private Map<String, Method> statementMethodsMapping = new HashMap<>();
public AbstractEntityHandler(String schemaClass) {
this(schemaClass, "id");
}
public AbstractEntityHandler(String schemaClass, String pkField) {
this.schemaClass = schemaClass;
this.pkField = pkField;
for(Method method:getClass().getMethods()) {
Statement statement = method.getAnnotation(Statement.class);
if(statement!=null) {
String st = statement.value();
if(Strings.isEmpty(st)) st = method.getName();
statementMethodsMapping.put(st, method);
}
}
}
@Override
public Class<T> getEntityClass() {
return (Class<T>) type.getRawType();
}
@Override
public String getSchemaClass() {
return schemaClass;
}
@Override
public String getPkField() {
return pkField;
}
@Override
public void create(T entity, OPersistenceSession session) {
ODocument doc = mapToODocument(entity, null, session);
session.getDatabase().save(doc);
session.cacheODocument(doc);
}
@Override
public T read(String id, OPersistenceSession session) {
ODocument doc = readAsDocument(id, session);
return doc==null?null:mapToEntity(doc, null, session);
}
@Override
public ODocument readAsDocument(String id, OPersistenceSession session) {
String oid = (String) convertValueFromEntity("id", id);
OIdentifiable oIdentifiable = session.lookupOIdentifiableForIdInCache(oid);
if(oIdentifiable!=null) return oIdentifiable.getRecord();
else {
ODatabaseDocument db = session.getDatabase();
List<ODocument> ret = db.query(new OSQLSynchQuery<>("select from "+getSchemaClass()+" where "+getPkField()+" = ?", 1), oid);
return ret==null || ret.isEmpty()? null : ret.get(0);
}
}
@Override
public void update(T entity, OPersistenceSession session) {
ODocument doc = readAsDocument(entity.getId(), session);
mapToODocument(entity, doc, session);
session.getDatabase().save(doc);
}
@Override
public void delete(T entity, OPersistenceSession session) {
ODatabaseDocument db = session.getDatabase();
String id = entity.getId();
String oid = (String) convertValueFromEntity("id", id);
OIdentifiable oIdentifiable = session.lookupOIdentifiableForIdInCache(oid);
if(oIdentifiable!=null) {
db.delete(oIdentifiable.getIdentity());
} else {
db.command(new OCommandSQL("delete from "+getSchemaClass()+" where "+getPkField()+" = ?")).execute(oid);
}
}
protected void checkMapping(OPersistenceSession session) {
if(mappingFromDocToEntity==null || mappingFromEntityToDoc==null){
if(session!=null) initMapping(session);
}
}
protected IGetAndSet getGetAndSetter(Class<?> clazz, String property) {
return PropertyResolver.getLocator().get(clazz, property);
}
protected void initMapping(OPersistenceSession session) {
mappingFromDocToEntity = new HashMap<>();
mappingFromEntityToDoc = new HashMap<>();
OClass oClass = session.getClass(getSchemaClass());
Class<T> entityClass = getEntityClass();
for(OProperty property : oClass.properties()) {
String propertyName = property.getName();
String beanPropertyName = propertyName;
boolean isLink = property.getType().isLink();
if(isLink) beanPropertyName+="Id";
IGetAndSet getAndSet = getGetAndSetter(entityClass, beanPropertyName);
if(getAndSet!=null) {
if(isLink) {
IEntityHandler<?> targetHandler = HandlersManager.get().getHandlerBySchemaClass(property.getLinkedClass().getName());
if(targetHandler==null || targetHandler.getPkField()==null) continue;
propertyName+="."+targetHandler.getPkField();
}
if(getAndSet.getSetter()!=null) mappingFromDocToEntity.put(propertyName, beanPropertyName);
if(getAndSet.getGetter()!=null) mappingFromEntityToDoc.put(beanPropertyName, propertyName);
}
}
}
protected Object convertValueToEntity(String entityFieldName, Object value) {
Converter<Object, Object> converter = mappingConvertors.get(entityFieldName);
return converter==null?value:converter.reverse().convert(value);
}
protected Object convertValueFromEntity(String entityFieldName, Object value) {
Converter<Object, Object> converter = mappingConvertors.get(entityFieldName);
return converter==null?value:converter.convert(value);
}
@Override
public T mapToEntity(ODocument doc, T entity, OPersistenceSession session) {
checkMapping(session);
try {
if(hasNeedInCache() && session!=null) {
entity = (T)session.lookupEntityInCache((String)doc.field(getPkField()));
if(entity!=null) return entity;
}
if(entity==null) {
entity = getEntityClass().newInstance();
}
for(Map.Entry<String, String> mapToEntity : mappingFromDocToEntity.entrySet()) {
Object valueToSet = doc.field(mapToEntity.getKey());
valueToSet = convertValueToEntity(mapToEntity.getValue(), valueToSet);
if(valueToSet!=null) PropertyResolver.setValue(mapToEntity.getValue(), entity, valueToSet, PROPERTY_RESOLVER_CONVERTEER);
else {
IGetAndSet getAndSet = getGetAndSetter(entity.getClass(), mapToEntity.getValue());
if(!getAndSet.getTargetClass().isPrimitive()) getAndSet.setValue(entity, null, PROPERTY_RESOLVER_CONVERTEER);
}
}
if(entity instanceof HasDbRevision) {
((HasDbRevision)entity).setRevision(doc.getVersion());
}
if(session!=null) session.fireEntityLoaded(doc, entity, hasNeedInCache());
return entity;
} catch (Exception e) {
logger.error("There shouldn't be this exception in case of predefined mapping", e);
throw new IllegalStateException("There shouldn't be this exception in case of predefined mapping", e);
}
}
@Override
public boolean hasNeedInCache() {
return false;
}
@Override
public ODocument mapToODocument(T entity, ODocument doc, OPersistenceSession session) {
checkMapping(session);
if(doc==null) {
doc = new ODocument(getSchemaClass());
}
for(Map.Entry<String, String> mapToDoc : mappingFromEntityToDoc.entrySet()) {
Object value = PropertyResolver.getValue(mapToDoc.getKey(), entity);
String docField = mapToDoc.getValue();
int refIndex = docField.indexOf('.');
if(refIndex>=0 || !Objects.equal(value, doc.field(docField))) {
if(refIndex>=0) {
String refPkField = docField.substring(refIndex+1);
docField = docField.substring(0, refIndex);
OProperty refProperty = doc.getSchemaClass().getProperty(docField);
IEntityHandler<?> refHandler = refProperty!=null
? HandlersManager.get().getHandlerBySchemaClass(refProperty.getLinkedClass())
: null;
if(refHandler==null || !Objects.equal(refPkField, refHandler.getPkField())) {
logger.error("Mapping for entity field '"+mapToDoc.getKey()+"' is wrongly set to '"+mapToDoc.getValue()+"'");
continue;
}
if(value!=null)
{
String referToId = value.toString();
OIdentifiable referTo = session.lookupOIdentifiableForIdInCache(referToId);
if(referTo==null) {
referTo = refHandler.readAsDocument(referToId, session);
}
if(!Objects.equal(doc.field(docField), referTo)) doc.field(docField, referTo);
} else
{
doc.field(docField, (Object) null);
}
} else {
doc.field(docField, convertValueFromEntity(mapToDoc.getKey(), value));
}
}
}
return doc;
}
@Override
public void applySchema(OSchemaHelper helper) {
helper.oClass(schemaClass, BPM_ENTITY_CLASS);
}
@Override
public void applyRelationships(OSchemaHelper helper) {
helper.oClass(schemaClass, BPM_ENTITY_CLASS);
}
@Override
public boolean supportsStatement(String statement) {
return statementMethodsMapping.containsKey(statement);
}
protected <T> T invokeStatement(String statement, Object... args) {
Method method = statementMethodsMapping.get(statement);
try {
return (T) method.invoke(this, args);
} catch (Exception e) {
throw new IllegalStateException("With good defined handler we should not be here. Method: "+method, e);
}
}
@Override
public List<T> selectList(String statement, Object parameter, OPersistenceSession session) {
return invokeStatement(statement, session, parameter);
}
@Override
public T selectOne(String statement, Object parameter, OPersistenceSession session) {
return invokeStatement(statement, session, parameter);
}
@Override
public void lock(String statement, Object parameter, OPersistenceSession session) {
invokeStatement(statement, session, parameter);
}
@Override
public void deleteBulk(DbBulkOperation operation, OPersistenceSession session) {
invokeStatement(operation.getStatement(), session, operation.getParameter());
}
@Override
public void updateBulk(DbBulkOperation operation, OPersistenceSession session) {
invokeStatement(operation.getStatement(), session, operation.getParameter());
}
protected T querySingle(OPersistenceSession session, String sql, Object... args) {
ODatabaseDocument db = session.getDatabase();
List<ODocument> ret = db.query(new OSQLSynchQuery<>(sql, 1), args);
return ret==null || ret.isEmpty()?null:mapToEntity(ret.get(0), null, session);
}
protected List<T> queryList(final OPersistenceSession session, String sql, Object... args) {
ODatabaseDocument db = session.getDatabase();
List<ODocument> ret = db.query(new OSQLSynchQuery<>(sql), args);
if(ret==null) return Collections.emptyList();
return new ArrayList<T>(Lists.transform(ret, new Function<ODocument, T>() {
@Override
public T apply(ODocument input) {
return mapToEntity(input, null, session);
}
}));
}
protected void command(OPersistenceSession session, String sql, Object... args) {
ODatabaseDocument db = session.getDatabase();
db.command(new OCommandSQL(sql)).execute(args);
}
protected List<T> query(final OPersistenceSession session, org.camunda.bpm.engine.query.Query<?, ? super T> query, String... ignoreFileds) {
return query(session, query, null, ignoreFileds);
}
protected List<T> query(final OPersistenceSession session, org.camunda.bpm.engine.query.Query<?, ? super T> query, Function<Query, Query> queryManger, String... ignoreFileds) {
try {
OClass schemaClass = session.getClass(getSchemaClass());
Query q = new Query().from(getSchemaClass());
List<Object> args = new ArrayList<>();
enrichWhereByBean(session, q, schemaClass, query, args, Arrays.asList(ignoreFileds));
if(queryManger!=null) q = queryManger.apply(q);
return queryList(session, q.toString(), args.toArray());
} catch (Exception e) {
throw new ProcessEngineException("Problems with read method of "+query.getClass().getName(), e);
}
}
protected List<T> query(final OPersistenceSession session, Map<String, ?> query, String... ignoreFileds) {
return query(session, query, null, ignoreFileds);
}
protected List<T> query(final OPersistenceSession session, Map<String, ?> query, Function<Query, Query> queryManger, String... ignoreFileds) {
OClass schemaClass = session.getClass(getSchemaClass());
Query q = new Query().from(getSchemaClass());
List<Object> args = new ArrayList<>();
enrichWhereByMap(session, q, schemaClass, query, args, Arrays.asList(ignoreFileds));
if(queryManger!=null) q = queryManger.apply(q);
return queryList(session, q.toString(), args.toArray());
}
protected void delete(final OPersistenceSession session, org.camunda.bpm.engine.query.Query<?, ? super T> query, String... ignoreFileds) {
delete(session, query, null, ignoreFileds);
}
protected void delete(final OPersistenceSession session, org.camunda.bpm.engine.query.Query<?, ? super T> query, Function<Query, Query> queryManger, String... ignoreFileds) {
try {
OClass schemaClass = session.getClass(getSchemaClass());
Query q = new Query().from(getSchemaClass());
List<Object> args = new ArrayList<>();
enrichWhereByBean(session, q, schemaClass, query, args, Arrays.asList(ignoreFileds));
if(queryManger!=null) q = queryManger.apply(q);
command(session, q.toString(), args.toArray());
} catch (Exception e) {
throw new ProcessEngineException("Problems with read method of "+query.getClass().getName(), e);
}
}
protected void delete(final OPersistenceSession session, Map<String, ?> query, String... ignoreFileds) {
delete(session, query, null, ignoreFileds);
}
protected void delete(final OPersistenceSession session, Map<String, ?> query, Function<Query, Query> queryManger, String... ignoreFileds) {
OClass schemaClass = session.getClass(getSchemaClass());
Query q = new Query().from(getSchemaClass());
List<Object> args = new ArrayList<>();
enrichWhereByMap(session, q, schemaClass, query, args, Arrays.asList(ignoreFileds));
if(queryManger!=null) q = queryManger.apply(q);
command(session, q.toString(), args.toArray());
}
protected void enrichWhereByBean(OPersistenceSession session, AbstractQuery q, OClass schemaClass, Object query, List<Object> args, List<String> ignore)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
checkMapping(session);
for(PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(query.getClass())) {
if(pd.getReadMethod()!=null
&& ( mappingFromEntityToDoc.containsKey(pd.getName())
|| mappingFromQueryToDoc.containsKey(pd.getName()))
&& (ignore==null || !ignore.contains(pd.getName()))) {
String docMapping = mappingFromEntityToDoc.get(pd.getName());
if(docMapping==null) docMapping = mappingFromQueryToDoc.get(pd.getName());
Object value = pd.getReadMethod().invoke(query);
if(value!=null) {
where(q, clause(docMapping, Operator.EQ, Parameter.PARAMETER));
args.add(convertValueFromEntity(pd.getName(), value));
}
}
}
}
protected void enrichWhereByMap(OPersistenceSession session, AbstractQuery q, OClass schemaClass, Map<String, ?> query, List<Object> args, List<String> ignore) {
checkMapping(session);
for(Map.Entry<String, ?> entry : query.entrySet()) {
if((mappingFromEntityToDoc.containsKey(entry.getKey()) || mappingFromEntityToDoc.containsKey(entry.getKey()))
&& (ignore==null || !ignore.contains(entry.getKey()))) {
String docMapping = mappingFromEntityToDoc.get(entry.getKey());
if(docMapping==null) docMapping = mappingFromQueryToDoc.get(entry.getKey());
Object value = entry.getValue();
if(value!=null) {
where(q, clause(docMapping, Operator.EQ, Parameter.PARAMETER));
args.add(convertValueFromEntity(entry.getKey(), value));
}
}
}
}
protected AbstractQuery where(AbstractQuery q, Clause clause) {
if(q instanceof Query)((Query)q).where(clause);
else if(q instanceof Delete)((Delete)q).where(clause);
return q;
}
@Override
public RESULT onTrigger(ODatabaseDocument db, ODocument doc, TYPE iType) {
return RESULT.RECORD_NOT_CHANGED;
}
}