/************************************************************************
* Copyright (c) 2015-2016 IoT-Solutions e.U.
*
* 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 iot.jcypher.domainquery.internal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import iot.jcypher.domainquery.AbstractDomainQuery;
import iot.jcypher.domainquery.GDomainQuery;
import iot.jcypher.domainquery.InternalAccess;
import iot.jcypher.domainquery.api.APIAccess;
import iot.jcypher.domainquery.api.DomainObjectMatch;
import iot.jcypher.domainquery.internal.RecordedQuery.Invocation;
import iot.jcypher.domainquery.internal.RecordedQuery.Statement;
import iot.jcypher.query.values.MathFunctions;
import iot.jcypher.query.values.ValueAccess;
import iot.jcypher.query.values.ValueElement;
public class QueryRecorder {
public static final String QUERY_ID = "q";
private static ThreadLocal<QueriesPerThread> queriesPerThread =
new ThreadLocal<QueriesPerThread>();
public static ThreadLocal<Boolean> blockRecording =
new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return Boolean.FALSE;
}
};
/**
* invocations on domainQuery (q)
* but stacked within e.g. a SELECT_FROM(...).ELEMENTS(...) statement.
* Must not be directly added to the root RecordedQuery
* but must be encapsulated in a sub statement
* @param on
* @param method
* @param result
* @param params
*/
public static void recordStackedInvocation(Object on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
recordInvocation(false, true, false, on, method, result, params);
}
/**
* invocations on domainQuery (q)
* but stacked within e.g. a SELECT_FROM(...).ELEMENTS(...) statement.
* Must not be directly added to the root RecordedQuery
* but must be encapsulated in a sub statement
* @param on
* @param method
* @param result
* @param params
*/
public static void recordStackedAssignment(Object on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
recordInvocation(true, true, false, on, method, result, params);
}
public static void recordInvocation(Object on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
recordInvocation(false, false, false, on, method, result, params);
}
public static void recordInvocationNoConcat(Object on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
recordInvocation(false, false, true, on, method, result, params);
}
public static void recordAssignment(Object on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
recordInvocation(true, false, false, on, method, result, params);
}
private static void recordInvocation(boolean assign, boolean subRoot, boolean noConcat, Object on, String method,
Object result, Object... params) {
// subRoot true means invocations on domainQuery (q)
// but stacked within e.g. a SELECT_FROM(...).ELEMENTS(...) statement.
// Must not be directly added to the root RecordedQuery
// but must be encapsulated in a sub statement
QueriesPerThread qpt = getCreateQueriesPerThread();
RecQueryHolder rqh = null;
if (noConcat) {
rqh = createRecQueryHolder();
} else {
if (on instanceof AbstractDomainQuery)
rqh = getRecQueryHolder((AbstractDomainQuery)on);
else {
rqh = qpt.getHolderRef(on);
if (rqh == null)
rqh = createRecQueryHolder();
}
}
if (subRoot && rqh.root) {
// the parameters have already been adopted to the root
rqh.stackLastNStatements(assign, params == null ? 0 : params.length, on, method, result);
} else {
List<Statement> parameters = new ArrayList<Statement>(params.length);
for (int i = 0; i < params.length; i++) {
Object param = params[i];
if (param instanceof Literal)
parameters.add(rqh.recordedQuery.literal(((Literal)param).value));
else if (param instanceof PlaceHolder) {
Object val = ((PlaceHolder)param).value;
if (val instanceof DomainObjectMatch<?>) {
String oid = rqh.object2IdMap.get(val);
parameters.add(rqh.recordedQuery.doMatchRef(oid));
} else {
RecQueryHolder trqh = qpt.getHolderRef(val);
if (trqh != null) {
List<Statement> adopted = adoptStatements(trqh, rqh);
parameters.addAll(adopted);
qpt.removeHolderRef(val);
} else { // assume it is a literal (or a parameter)
parameters.add(rqh.recordedQuery.literal(val));
}
}
} else if (param instanceof Reference) {
Object val = ((Reference)param).value;
parameters.add(rqh.recordedQuery.reference(val, rqh.getNextRefId()));
}
}
rqh.recordInvocation(assign, on, method, result, parameters);
qpt.putHolderRef(result, rqh);
if (rqh.root) {
qpt.removeFromQuery2HolderMap(rqh, null, false, null); // don't remove root
}
}
return;
}
private static List<Statement> adoptStatements(RecQueryHolder from,
RecQueryHolder to) {
List<Statement> params = from.recordedQuery.getStatements();
for (Statement s : params) {
adoptStatement(from, to, s);
}
return params;
}
private static void adoptStatement(RecQueryHolder from,
RecQueryHolder to, Statement s) {
if (s instanceof Invocation) {
String or = ((RecordedQuery.Invocation)s).getOnObjectRef();
Object o = from.id2ObjectMap.get(or);
String id = to.object2IdMap.get(o);
if (id == null) {
id = to.getNextId();
to.object2IdMap.put(o, id);
to.id2ObjectMap.put(id, o);
}
((RecordedQuery.Invocation) s).setOnObjectRef(id);
or = ((RecordedQuery.Invocation)s).getReturnObjectRef();
o = from.id2ObjectMap.get(or);
id = to.object2IdMap.get(o);
if (id == null) {
id = to.getNextId();
to.object2IdMap.put(o, id);
to.id2ObjectMap.put(id, o);
}
((RecordedQuery.Invocation) s).setReturnObjectRef(id);
QueriesPerThread qpt = getCreateQueriesPerThread();
List<Statement> params = ((RecordedQuery.Invocation) s).getParams();
for (Statement stmt : params) {
RecQueryHolder nextFrom = qpt.getHolderForQuery(stmt.getRecordedQuery());
if (nextFrom != null)
adoptStatement(nextFrom, to, stmt);
}
}
to.addAdopted(from);
}
public static void recordInvocationConditional(ValueElement on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
QueriesPerThread qpt = queriesPerThread.get();
if (qpt != null && !qpt.isEmpty()) {
Object dom = ValueAccess.getAnyHint(on, APIAccess.hintKey_dom);
if (dom != null)
recordInvocation(on, method, result, params);
}
}
public static void recordInvocationConditional(MathFunctions on, String method, Object result, Object... params) {
if (blockRecording.get())
return;
QueriesPerThread qpt = queriesPerThread.get();
if (qpt != null && !qpt.isEmpty()) {
Object dom = ValueAccess.getAnyHint(ValueAccess.getArgument(on), APIAccess.hintKey_dom);
if (dom != null)
recordInvocation(on, method, result, params);
}
}
public static void recordInvocationReplace(DomainObjectMatch<?> on, Object toReplace,
String methodName) {
if (blockRecording.get())
return;
QueriesPerThread qpt = getCreateQueriesPerThread();
RecQueryHolder trqh = qpt.getHolderRef(toReplace);
RecQueryHolder rqh = qpt.getHolderRef(on);
if (trqh != null) {
List<Statement> stmts = trqh.recordedQuery.getStatements();
for (Statement stmt : stmts) {
if (stmt instanceof Invocation) {
String onId = trqh.object2IdMap.get(on);
if (onId == null) {
onId = trqh.getNextId();
trqh.object2IdMap.put(on, onId);
trqh.id2ObjectMap.put(onId, on);
}
((RecordedQuery.Invocation)stmt).setOnObjectRef(onId);
((RecordedQuery.Invocation)stmt).setMethod(methodName);
}
}
//qpt.removeHolderRef(toReplace);
if (rqh == null)
qpt.putHolderRef(on, trqh);
else
rqh.addReplaced(trqh);
}
}
public static void recordCreateQuery(AbstractDomainQuery query) {
if (blockRecording.get())
return;
RecordedQuery rq = new RecordedQuery(query instanceof GDomainQuery);
RecQueryHolder rqh = new RecQueryHolder(rq);
rqh.root = true;
getCreateQueriesPerThread().put(query, rqh);
}
public static void queryCompleted(AbstractDomainQuery query) {
if (blockRecording.get())
return;
QueryExecutor qe = InternalAccess.getQueryExecutor((AbstractDomainQuery) query);
boolean done = qe.queryCreationCompleted(false);
if (!done) {
QueriesPerThread qpt = queriesPerThread.get();
if (qpt != null) {
qpt.queryCompleted(query);
}
}
}
public static RecordedQuery getRecordedQuery(AbstractDomainQuery query) {
if (blockRecording.get())
return null;
RecQueryHolder rqh = getRecQueryHolder(query);
if (rqh != null)
return rqh.recordedQuery;
return null;
}
public static Literal literal(Object value) {
return new Literal(value);
}
public static PlaceHolder placeHolder(Object value) {
return new PlaceHolder(value);
}
public static Reference reference(Object value) {
return new Reference(value);
}
private static RecQueryHolder getRecQueryHolder(AbstractDomainQuery on) {
QueriesPerThread qpt = getCreateQueriesPerThread();
return qpt.get(on);
}
private static RecQueryHolder createRecQueryHolder() {
RecQueryHolder rqh = new RecQueryHolder(new RecordedQuery(false));
return rqh;
}
public static QueriesPerThread getCreateQueriesPerThread() {
QueriesPerThread qpt = queriesPerThread.get();
if (qpt == null) {
qpt = new QueriesPerThread();
queriesPerThread.set(qpt);
}
return qpt;
}
public static QueriesPerThread getQueriesPerThread() {
return queriesPerThread.get();
}
/*******************************/
public static class QueriesPerThread {
private Map<AbstractDomainQuery, RecQueryHolder> queries;
private Map<Object, RecQueryHolder> recHolderRefs;
private Map<RecordedQuery, RecQueryHolder> query2HolderMap;
private QueriesPerThread() {
super();
this.queries = new HashMap<AbstractDomainQuery, RecQueryHolder>();
this.recHolderRefs = new IdentityHashMap<Object, RecQueryHolder>();
this.query2HolderMap = new HashMap<RecordedQuery, RecQueryHolder>();
}
private void put(AbstractDomainQuery key, RecQueryHolder value) {
this.queries.put(key, value);
}
private RecQueryHolder get(AbstractDomainQuery key) {
RecQueryHolder ret = this.queries.get(key);
return ret;
}
private void putHolderRef(Object key, RecQueryHolder value) {
this.recHolderRefs.put(key, value);
this.query2HolderMap.put(value.recordedQuery, value);
}
private RecQueryHolder getHolderRef(Object key) {
RecQueryHolder ret = this.recHolderRefs.get(key);
return ret;
}
private RecQueryHolder getHolderForQuery(RecordedQuery key) {
RecQueryHolder ret = this.query2HolderMap.get(key);
return ret;
}
private RecQueryHolder removeHolderRef(Object key) {
RecQueryHolder ret = this.recHolderRefs.remove(key);
//this.query2HolderMap.remove(ret.recordedQuery);
return ret;
}
private void removeFromQuery2HolderMap(RecQueryHolder rqh, RecQueryHolder par,
boolean removeRoot, Set<RecQueryHolder> recursionSet) {
if (recursionSet == null)
recursionSet = new HashSet<RecQueryHolder>();
recursionSet.add(rqh);
if (!rqh.root || removeRoot) {
this.query2HolderMap.remove(rqh.recordedQuery);
if (par != null)
par.adopted.remove(rqh);
}
ArrayList<RecQueryHolder> adopted = new ArrayList<RecQueryHolder>();
adopted.addAll(rqh.adopted);
for (RecQueryHolder qh : adopted) {
if (!recursionSet.contains(qh))
this.removeFromQuery2HolderMap(qh, rqh, removeRoot, recursionSet);
}
}
private boolean isEmpty() {
return this.queries.isEmpty();
}
/**
* For testing purposes
* @return
*/
public boolean isCleared() {
if (Settings.TEST_MODE) {
System.gc();
System.runFinalization();
}
return this.queries.isEmpty() &&
this.recHolderRefs.isEmpty() &&
this.query2HolderMap.isEmpty();
}
public void queryCompleted(AbstractDomainQuery query) {
RecQueryHolder rqh = this.queries.remove(query);
if (rqh != null) {
this.removeFromQuery2HolderMap(rqh, null, true, null); // also remove root
List<Object> toRemove = new ArrayList<Object>();
Iterator<Entry<Object, RecQueryHolder>> it = this.recHolderRefs.entrySet().iterator();
while(it.hasNext()) {
Entry<Object, RecQueryHolder> e = it.next();
if (e.getValue() == rqh)
toRemove.add(e.getKey());
else {
if (rqh.inReplaced(e.getValue()))
toRemove.add(e.getKey());
}
}
for (Object o : toRemove)
this.recHolderRefs.remove(o);
}
}
/**
* answer a map of DomainObjectMatch(es) to recorded query ids.
* <br/>Note: this destroys the original object2IdMap and may be called
* <br/>only after query recording has been completed.
* @param q
* @return
*/
public Map<Object, String> getDOM2IdMap(AbstractDomainQuery q) {
RecQueryHolder rqh = this.get(q);
if (rqh != null) {
List<Object> remove = new ArrayList<Object>();
Iterator<Object> it = rqh.object2IdMap.keySet().iterator();
while(it.hasNext()) {
Object o = it.next();
if (!(o instanceof DomainObjectMatch<?>))
remove.add(o);
}
for(Object o : remove)
rqh.object2IdMap.remove(o);
return rqh.object2IdMap;
}
return null;
}
}
/*******************************/
private static class RecQueryHolder {
private static final String idPrefix = "obj";
private static final String refIdPrefix = "ref_";
private boolean root;
private RecordedQuery recordedQuery;
private Map<Object, String> object2IdMap;
private Map<String, Object> id2ObjectMap;
private List<RecQueryHolder> adopted;
private List<RecQueryHolder> replaced;
private int lastId;
private int lastRefId;
private RecQueryHolder(RecordedQuery recordedQuery) {
super();
this.recordedQuery = recordedQuery;
this.object2IdMap = new HashMap<Object, String>();
this.id2ObjectMap = new HashMap<String, Object>();
this.adopted = new ArrayList<RecQueryHolder>();
this.replaced = new ArrayList<RecQueryHolder>();
this.lastId = -1;
this.lastRefId = -1;
this.root = false;
}
private void recordInvocation(boolean assign, Object on, String method, Object result, List<Statement> parameters) {
String[] ids = getIds(on, result);
if (assign)
this.recordedQuery.addAssignment(ids[0], method, ids[1], parameters);
else
this.recordedQuery.addInvocation(ids[0], method, ids[1], parameters);
}
private String getRefId(Object ref) {
String refId = this.object2IdMap.get(ref);
if (refId == null) {
refId = getNextRefId();
this.object2IdMap.put(ref, refId);
this.id2ObjectMap.put(refId, ref);
}
return refId;
}
/**
* @param on
* @param result
* @return [onId, resId]
*/
private String[] getIds(Object on, Object result) {
String onId = this.object2IdMap.get(on);
if (onId == null) {
if (on instanceof AbstractDomainQuery)
onId = QUERY_ID;
else
onId = getNextId();
this.object2IdMap.put(on, onId);
this.id2ObjectMap.put(onId, on);
}
String resId = this.object2IdMap.get(result);
if (resId == null) {
resId = getNextId();
this.object2IdMap.put(result, resId);
this.id2ObjectMap.put(resId, result);
}
return new String[]{onId, resId};
}
private String getNextId() {
this.lastId++;
return idPrefix.concat(String.valueOf(this.lastId));
}
private String getNextRefId() {
this.lastRefId++;
return refIdPrefix.concat(String.valueOf(this.lastRefId));
}
private void addAdopted(RecQueryHolder qh) {
if (!this.adopted.contains(qh))
this.adopted.add(qh);
}
private void addReplaced(RecQueryHolder qh) {
if (!this.replaced.contains(qh))
this.replaced.add(qh);
}
private boolean inReplaced(RecQueryHolder rqh) {
int idx = this.replaced.indexOf(rqh);
return idx >= 0;
}
private void stackLastNStatements(boolean assign, int n, Object on, String method, Object result) {
List<Statement> stmts = new ArrayList<Statement>(n);
if (n > 0) {
int addOffs = 0;
Statement stmt = null;
Statement following = null;
List<Statement> qstmts = this.recordedQuery.getStatements();
int offs = qstmts.size() - 1;
for (int i = 0; i < n +addOffs; i++) {
stmt = qstmts.remove(offs - i);
stmts.add(0, stmt);
// adjustment for concatenated statements
if (following instanceof Invocation && stmt instanceof Invocation) {
if (((Invocation)following).getOnObjectRef().equals(
((Invocation)stmt).getReturnObjectRef()))
addOffs++;
}
following = stmt;
}
// now follow the concatenations of the first stacked statement
offs = qstmts.size() - 1;
while (offs >= 0) {
Statement prev = qstmts.get(offs);
boolean goOn = false;
if (prev instanceof Invocation && stmt instanceof Invocation) {
if (((Invocation)stmt).getOnObjectRef().equals(
((Invocation)prev).getReturnObjectRef())) {
qstmts.remove(offs);
stmts.add(0, prev);
stmt = prev;
offs--;
goOn = true;
}
}
if (!goOn)
break;
}
}
String[] ids = getIds(on, result);
if (assign)
this.recordedQuery.addAssignment(ids[0], method, ids[1], stmts);
else
this.recordedQuery.addInvocation(ids[0], method, ids[1], stmts);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.recordedQuery.toString());
return sb.toString();
}
}
/*********************************/
public static class Literal {
private Object value;
public Literal(Object value) {
super();
this.value = value;
}
}
/*********************************/
public static class PlaceHolder {
private Object value;
public PlaceHolder(Object value) {
super();
this.value = value;
}
}
/*********************************/
public static class Reference {
private Object value;
public Reference(Object value) {
super();
this.value = value;
}
}
}