package org.orienteer.core.hook;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.apache.wicket.util.string.Strings;
import org.orienteer.core.CustomAttribute;
import com.google.common.base.Function;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.hook.ODocumentHookAbstract;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.schema.OSchema;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
/**
* {@link ODocumentHookAbstract} for automatic calculation of some properties.
* Properties to be automatically calculated should be marked by {@link CustomAttribute}.CALCULABLE
* Logic for calculation should be stored in {@link CustomAttribute}.CALC_SCRIPT
*/
public class CalculablePropertiesHook extends ODocumentHookAbstract
{
private final static Pattern FULL_QUERY_PATTERN = Pattern.compile("^\\s*(select|traverse)", Pattern.CASE_INSENSITIVE);
private Map<String, Integer> schemaVersions = new ConcurrentHashMap<String, Integer>();
private Table<String, String, List<String>> calcProperties = HashBasedTable.create();
public CalculablePropertiesHook(ODatabaseDocument database) {
super(database);
}
@Override
public DISTRIBUTED_EXECUTION_MODE getDistributedExecutionMode() {
return DISTRIBUTED_EXECUTION_MODE.SOURCE_NODE;
}
@SuppressWarnings("deprecation")
private List<String> getCalcProperties(ODocument iDocument)
{
ODatabaseDocument db = iDocument.getDatabase();
OClass oClass = iDocument.getSchemaClass();
if(db==null || oClass==null) return null;
OSchema schema = db.getMetadata().getSchema();
int schemaVersion = schema.getVersion();
Integer prevSchemaVersion = schemaVersions.get(db.getURL());
if(!Objects.equals(prevSchemaVersion, schemaVersion))
{
//Clear and fullfill cache
calcProperties.row(db.getURL()).clear();
for(OClass clazz: schema.getClasses())
{
List<String> calcProperties=null;
for(OProperty property: clazz.properties())
{
if(CustomAttribute.CALCULABLE.getValue(property, false))
{
if(calcProperties==null) calcProperties = new ArrayList<String>();
calcProperties.add(property.getName());
}
}
if(calcProperties!=null) this.calcProperties.put(db.getURL(), clazz.getName(), calcProperties);
}
//Update schemaVersion
schemaVersions.put(db.getURL(), schemaVersion);
}
return calcProperties.get(db.getURL(), oClass.getName());
}
@Override
public RESULT onRecordBeforeCreate(ODocument iDocument) {
return onRecordBeforeUpdate(iDocument);
}
@Override
public RESULT onRecordBeforeUpdate(ODocument iDocument) {
List<String> calcProperties = getCalcProperties(iDocument);
if(calcProperties!=null && calcProperties.size()>0)
{
boolean wasChanged=false;
String[] fieldNames = iDocument.fieldNames();
for (String field : fieldNames)
{
if(calcProperties.contains(field))
{
boolean tracking = iDocument.isTrackingChanges();
if(tracking) iDocument.undo(field);
// iDocument.removeField(field);
wasChanged = true;
}
}
return wasChanged?RESULT.RECORD_CHANGED:RESULT.RECORD_NOT_CHANGED;
}
return RESULT.RECORD_NOT_CHANGED;
}
/*
* Temporal commenting out! It should be fixed in OrientDB. Issue #4158
* @Override
public void onRecordAfterCreate(ODocument iDocument) {
onRecordAfterRead(iDocument);
}*/
@Override
public void onRecordAfterUpdate(ODocument iDocument) {
onRecordAfterRead(iDocument);
}
@Override
public void onRecordAfterRead(ODocument iDocument) {
super.onRecordAfterRead(iDocument);
OClass oClass = iDocument.getSchemaClass();
if(oClass!=null)
{
List<String> calcProperties = getCalcProperties(iDocument);
if(calcProperties!=null && calcProperties.size()>0)
{
for (String calcProperty :calcProperties) {
//Force calculation. Required for work around issue in OrientDB
//if(iDocument.field(calcProperty)!=null) continue;
final OProperty property = oClass.getProperty(calcProperty);
String script = CustomAttribute.CALC_SCRIPT.getValue(property);
if(!Strings.isEmpty(script))
{
List<ODocument> calculated;
if(FULL_QUERY_PATTERN.matcher(script).find())
{
calculated = iDocument.getDatabase().query(new OSQLSynchQuery<Object>(script), iDocument);
}
else
{
script = "select "+script+" as value from "+iDocument.getIdentity();
calculated = iDocument.getDatabase().query(new OSQLSynchQuery<Object>(script));
}
if(calculated!=null && calculated.size()>0)
{
OType type = property.getType();
Object value;
if(type.isMultiValue())
{
final OType linkedType = property.getLinkedType();
value = linkedType==null
?calculated
:Lists.transform(calculated, new Function<ODocument, Object>() {
@Override
public Object apply(ODocument input) {
return OType.convert(input.field("value"), linkedType.getDefaultJavaType());
}
});
}
else
{
value = calculated.get(0).field("value");
}
value = OType.convert(value, type.getDefaultJavaType());
iDocument.field(calcProperty, value);
}
}
}
}
}
}
}