| /******************************************************************************* |
| * Copyright (c) 2011-2018 The University of York, Aston University. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the Eclipse |
| * Public License, v. 2.0 are satisfied: GNU General Public License, version 3. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0 |
| * |
| * Contributors: |
| * Konstantinos Barmpis - initial API and implementation |
| * Antonio Garcia-Dominguez - improved error reporting |
| ******************************************************************************/ |
| package org.eclipse.hawk.epsilon.emc.optimisation; |
| |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Set; |
| |
| import org.eclipse.epsilon.eol.dom.AndOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.EqualsOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.Expression; |
| import org.eclipse.epsilon.eol.dom.GreaterEqualOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.GreaterThanOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.ImpliesOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.LessEqualOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.LessThanOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.NameExpression; |
| import org.eclipse.epsilon.eol.dom.NotEqualsOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.NotOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.OperatorExpression; |
| import org.eclipse.epsilon.eol.dom.OrOperatorExpression; |
| import org.eclipse.epsilon.eol.dom.PropertyCallExpression; |
| import org.eclipse.epsilon.eol.dom.XorOperatorExpression; |
| import org.eclipse.epsilon.eol.exceptions.EolRuntimeException; |
| import org.eclipse.epsilon.eol.execute.context.IEolContext; |
| import org.eclipse.epsilon.eol.execute.context.Variable; |
| import org.eclipse.epsilon.eol.execute.operations.declarative.SelectOperation; |
| import org.eclipse.hawk.core.IModelIndexer; |
| import org.eclipse.hawk.core.graph.IGraphDatabase; |
| import org.eclipse.hawk.core.graph.IGraphIterable; |
| import org.eclipse.hawk.core.graph.IGraphNode; |
| import org.eclipse.hawk.core.graph.IGraphNodeIndex; |
| import org.eclipse.hawk.core.graph.IGraphTransaction; |
| import org.eclipse.hawk.core.util.Utils; |
| import org.eclipse.hawk.epsilon.emc.AbstractHawkModel; |
| import org.eclipse.hawk.epsilon.emc.EOLQueryEngine; |
| import org.eclipse.hawk.epsilon.emc.wrappers.GraphNodeWrapper; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| public class OptimisableCollectionSelectOperation extends SelectOperation { |
| |
| // XXX can also index subsets of X.all as the context is kept. but this may |
| // be counter-productive as you may end up doing a retain all on a list of a |
| // couple of elements into an indexed result of millions |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(OptimisableCollectionSelectOperation.class); |
| |
| protected EOLQueryEngine model; |
| private IEolContext context; |
| private boolean returnOnFirstMatch; |
| private Variable iterator; |
| |
| IGraphNode metaclass; |
| IGraphDatabase graph = null; |
| |
| @Override |
| public Object execute(Object target, Variable iterator, Expression ast, IEolContext context, |
| boolean returnOnFirstMatch) throws EolRuntimeException { |
| |
| try { |
| |
| this.context = context; |
| // cannot guarantee correctness if returnOnFirstMatch is used |
| this.returnOnFirstMatch = false; |
| this.iterator = iterator; |
| model = (EOLQueryEngine) ((OptimisableCollection) target).getModel(); |
| |
| graph = model.getBackend(); |
| try (IGraphTransaction ignored = graph.beginTransaction()) { |
| metaclass = ((OptimisableCollection) target).type.getNode(); |
| ignored.success(); |
| } |
| |
| return decomposeAST(target, ast); |
| } catch (Exception e) { |
| throw new EolRuntimeException("select(...) failed: " + e.getMessage(), ast); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected Collection<Object> decomposeAST(Object target, Expression ast) throws Exception { |
| |
| if (ast instanceof AndOperatorExpression) { |
| return and(target, (AndOperatorExpression) ast); |
| } else if (ast instanceof OrOperatorExpression) { |
| return or(target, (OrOperatorExpression) ast); |
| } else if (ast instanceof XorOperatorExpression) { |
| return xor(target, (XorOperatorExpression) ast); |
| // ( a or b ) and ( not(a and b) ) |
| // a != b |
| } else if (ast instanceof ImpliesOperatorExpression) { |
| return implies(target, (ImpliesOperatorExpression) ast); |
| // not(a) or b |
| // not( a and not(b) ) |
| } else if (ast instanceof NotOperatorExpression) { |
| return not(target, (NotOperatorExpression) ast); |
| } else if (isOptimisable(ast)) { |
| return optimisedExecution(target, ast); |
| } else { |
| // System.err.println("giving to super: "+ast.toStringTree()); |
| Object ret = super.execute(target, iterator, (Expression) ast, context, returnOnFirstMatch); |
| // System.err.println("super returns: "+ret.getClass()); |
| return (Collection<Object>) ret; |
| |
| } |
| |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Collection<Object> implies(Object target, ImpliesOperatorExpression ast) throws Exception { |
| |
| final Expression lOperand = ast.getFirstOperand(); |
| final Expression rOperand = ast.getSecondOperand(); |
| final boolean lOptimisable = isOptimisable(lOperand); |
| final boolean rOptimisable = isOptimisable(rOperand); |
| |
| final Set<Object> filter = new HashSet<Object>(); |
| filter.addAll((Collection<Object>) target); |
| |
| if (lOptimisable && rOptimisable) { |
| // not(a) or b |
| Collection<Object> aa = optimisedExecution(target, lOperand); |
| Collection<Object> bb = optimisedExecution(target, rOperand); |
| |
| filter.removeAll(aa); |
| filter.addAll(bb); |
| return filter; |
| } |
| else if (lOptimisable) { |
| // not( a and not(b) ) |
| final Collection<Object> lhsResult = optimisedExecution(target, lOperand); |
| lhsResult.removeAll((Collection<Object>) decomposeAST(lhsResult, rOperand)); |
| filter.removeAll(lhsResult); |
| return filter; |
| } |
| else if (rOptimisable) { |
| // not( not(b) and a ) |
| final Collection<Object> rhsResult = optimisedExecution(target, rOperand); |
| final Collection<Object> notb = new HashSet<Object>((Collection<Object>) target); |
| notb.removeAll(rhsResult); |
| |
| filter.removeAll(decomposeAST(notb, lOperand)); |
| return filter; |
| } |
| else { |
| // not(a) or b |
| final Collection<Object> aa = decomposeAST(target, lOperand); |
| final Collection<Object> bb = decomposeAST(target, rOperand); |
| filter.removeAll(aa); |
| filter.addAll(bb); |
| return filter; |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Collection<Object> not(Object target, NotOperatorExpression ast) throws Exception { |
| final Set<Object> filter = new HashSet<Object>((Collection<Object>) target); |
| |
| final Expression operand = ast.getFirstOperand(); |
| if (isOptimisable(operand)) { |
| filter.removeAll(optimisedExecution(target, operand)); |
| } |
| else { |
| filter.removeAll(decomposeAST(target, operand)); |
| } |
| |
| return filter; |
| } |
| |
| private Collection<Object> xor(Object target, XorOperatorExpression ast) throws Exception { |
| Collection<Object> filter = new HashSet<Object>(); |
| Collection<Object> a = null; |
| Collection<Object> b = null; |
| |
| final Expression lOperand = ast.getFirstOperand(); |
| final Expression rOperand = ast.getSecondOperand(); |
| if (isOptimisable(lOperand) && isOptimisable(rOperand)) { |
| a = optimisedExecution(target, lOperand); |
| b = optimisedExecution(target, rOperand); |
| } else { |
| a = decomposeAST(target, lOperand); |
| b = decomposeAST(target, rOperand); |
| } |
| |
| // a or b |
| filter.addAll(a); |
| filter.addAll(b); |
| |
| // intersection of a and b |
| a.retainAll(b); |
| |
| // a xor b |
| filter.removeAll(a); |
| |
| return filter; |
| } |
| |
| private Collection<Object> or(Object target, OrOperatorExpression ast) throws Exception { |
| // System.err.println("anding: |
| // "+lOperand.toStringTree()+"\n"+rOperand.toStringTree()); |
| |
| final Expression lOperand = ast.getFirstOperand(); |
| final Expression rOperand = ast.getSecondOperand(); |
| final Collection<Object> filter = new HashSet<Object>(); |
| |
| if (isOptimisable(lOperand) && (isOptimisable(rOperand))) { |
| filter.addAll(optimisedExecution(target, lOperand)); |
| filter.addAll(optimisedExecution(target, rOperand)); |
| } else { |
| filter.addAll(decomposeAST(target, lOperand)); |
| filter.addAll(decomposeAST(target, rOperand)); |
| } |
| |
| return filter; |
| } |
| |
| private Collection<Object> and(Object target, AndOperatorExpression ast) throws Exception { |
| |
| // System.err.println("anding: |
| // "+lOperand.toStringTree()+"\n"+rOperand.toStringTree()); |
| |
| final Expression lOperand = ast.getFirstOperand(); |
| final Expression rOperand = ast.getSecondOperand(); |
| final boolean lOptimisable = isOptimisable(lOperand); |
| final boolean rOptimisable = isOptimisable(rOperand); |
| |
| final Collection<Object> filter = new HashSet<>(); |
| if (lOptimisable && rOptimisable) { |
| filter.addAll(optimisedExecution(optimisedExecution(target, lOperand), rOperand)); |
| } else if (lOptimisable) { |
| filter.addAll(decomposeAST(optimisedExecution(target, lOperand), rOperand)); |
| } else if (rOptimisable) { |
| filter.addAll(decomposeAST(optimisedExecution(target, rOperand), lOperand)); |
| } else { |
| filter.addAll(decomposeAST(decomposeAST(target, lOperand), rOperand)); |
| // modifiedlist = (Collection<?>) |
| // super.execute(modifiedlist,iterator, ast, context, |
| // returnOnFirstMatch); |
| } |
| |
| return filter; |
| |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Collection<Object> optimisedExecution(Object target, Expression ast) throws EolRuntimeException { |
| |
| // NOTE: this assumes that isOptimisable(ast) returned true |
| final OperatorExpression opExp = (OperatorExpression) ast; |
| final PropertyCallExpression lOperand = (PropertyCallExpression) opExp.getFirstOperand(); |
| final String attributename = lOperand.getName(); |
| final String variableName = ((NameExpression) lOperand.getTargetExpression()).getName(); |
| |
| final Expression valueAST = opExp.getSecondOperand(); |
| Object attributevalue = null; |
| try { |
| attributevalue = context.getExecutorFactory().execute(valueAST, context); |
| } catch (Exception e) { |
| // if the rhs is invalid or tries to use the iterator of the select |
| // (which is outside its scope) -- default to epsilon's select |
| LOGGER.warn("Warning: the RHS of the expression:\n{}" |
| + "\ncannot be evaluated using database indexing,\nas the iterator variable of the current select operation ({}) " |
| + "is not used in this process.\nDefaulting to Epsilon's select", ast, iterator.getName()); |
| } |
| |
| String indexname; |
| if (attributevalue != null && (indexname = isIndexed(attributename)) != null) { |
| if (!(attributevalue instanceof Collection<?>)) { |
| attributevalue = AbstractHawkModel.toPrimitive(attributevalue); |
| } else { |
| Collection<?> cRet = (Collection<?>) attributevalue; |
| Object[] aRet = new Object[cRet.size()]; |
| int count = 0; |
| for (Iterator<?> it = cRet.iterator(); it.hasNext();) { |
| aRet[count] = AbstractHawkModel.toPrimitive(it.next()); |
| count++; |
| } |
| // flatten to allow comparison to index value (which cannot be |
| // multi-valued) |
| attributevalue = Arrays.toString(aRet); |
| } |
| |
| final Set<Object> result = new HashSet<Object>((Collection<Object>) target); |
| System.err.println(String.format("indexed ast found: %s.%s %s %s (type: %s)", |
| variableName, attributename, ast.getClass().getName(), |
| new Utils().toString(attributevalue), |
| attributevalue.getClass().getName())); |
| |
| final Set<Object> filter = new HashSet<Object>(); |
| |
| // use index to query |
| try (IGraphTransaction ignored = graph.beginTransaction()) { |
| |
| IGraphNodeIndex index = graph.getOrCreateNodeIndex(indexname); |
| |
| // |
| IGraphIterable<? extends IGraphNode> hits = null; |
| |
| if (ast instanceof EqualsOperatorExpression && !(ast instanceof NotEqualsOperatorExpression)) { |
| if (attributevalue instanceof Integer) |
| hits = index.query(attributename, (int) attributevalue, (int) attributevalue, true, true); |
| else if (attributevalue instanceof Long) |
| hits = index.query(attributename, (long) attributevalue, (long) attributevalue, true, true); |
| else if (attributevalue instanceof Double) |
| hits = index.query(attributename, (double) attributevalue, (double) attributevalue, true, true); |
| else |
| hits = index.get(attributename, attributevalue); |
| } else if (ast instanceof GreaterEqualOperatorExpression) { |
| if (attributevalue instanceof Integer) |
| hits = index.query(attributename, (int) attributevalue, Integer.MAX_VALUE, true, true); |
| else if (attributevalue instanceof Long) |
| hits = index.query(attributename, (long) attributevalue, Long.MAX_VALUE, true, true); |
| else if (attributevalue instanceof Double) |
| hits = index.query(attributename, (double) attributevalue, Double.MAX_VALUE, true, true); |
| else |
| throw new EolRuntimeException( |
| ">= used with a non numeric value (" + attributevalue.getClass() + ")"); |
| } else if (ast instanceof GreaterThanOperatorExpression) { |
| if (attributevalue instanceof Integer) |
| hits = index.query(attributename, (int) attributevalue, Integer.MAX_VALUE, false, true); |
| else if (attributevalue instanceof Long) |
| hits = index.query(attributename, (long) attributevalue, Long.MAX_VALUE, false, true); |
| else if (attributevalue instanceof Double) |
| hits = index.query(attributename, (double) attributevalue, Double.MAX_VALUE, false, true); |
| else |
| throw new EolRuntimeException( |
| "> used with a non numeric value (" + attributevalue.getClass() + ")"); |
| } else if (ast instanceof LessEqualOperatorExpression) { |
| if (attributevalue instanceof Integer) |
| hits = index.query(attributename, Integer.MIN_VALUE, (int) attributevalue, true, true); |
| else if (attributevalue instanceof Long) |
| hits = index.query(attributename, Long.MIN_VALUE, (long) attributevalue, true, true); |
| else if (attributevalue instanceof Double) |
| hits = index.query(attributename, Double.MIN_VALUE, (double) attributevalue, true, true); |
| else |
| throw new EolRuntimeException( |
| "<= used with a non numeric value (" + attributevalue.getClass() + ")"); |
| } else if (ast instanceof LessThanOperatorExpression) { |
| if (attributevalue instanceof Integer) |
| hits = index.query(attributename, Integer.MIN_VALUE, (int) attributevalue, true, false); |
| else if (attributevalue instanceof Long) |
| hits = index.query(attributename, Long.MIN_VALUE, (long) attributevalue, true, false); |
| else if (attributevalue instanceof Double) |
| hits = index.query(attributename, Double.MIN_VALUE, (double) attributevalue, true, false); |
| else |
| throw new EolRuntimeException( |
| "< used with a non numeric value (" + attributevalue.getClass() + ")"); |
| } |
| |
| // modifiedlist.clear(); |
| |
| for (IGraphNode hit : hits) { |
| |
| filter.add(new GraphNodeWrapper(hit, model)); |
| |
| // ((OptimisableCollection) modifiedlist) |
| // .add(new NeoIdWrapperDebuggable(graph, hit |
| // .getId(), model)); |
| |
| } |
| |
| ignored.success(); |
| |
| } catch (Exception e) { |
| throw new EolRuntimeException("select optimisation failed: " + e.getMessage(), ast); |
| } |
| |
| result.retainAll(filter); |
| return result; |
| // System.err.println(Arrays.toString(ret.toArray())); |
| // return ret; |
| |
| } else { |
| |
| // System.err.println("giving to super: "+ast.toStringTree()); |
| |
| return (Collection<Object>) super.execute(target, iterator, (Expression) ast, context, returnOnFirstMatch); |
| |
| // System.err.println("super returns: "+rett.getClass()); |
| // return rett; |
| |
| } |
| |
| } |
| |
| private boolean isOptimisable(Expression ast) { |
| try { |
| if (!(ast instanceof OperatorExpression)) { |
| return false; |
| } |
| |
| // LEFT - we should have iterator.property |
| // L1. Check for a property call expression |
| final OperatorExpression opExp = (OperatorExpression) ast; |
| final Expression rawLOperand = opExp.getFirstOperand(); |
| if (!(rawLOperand instanceof PropertyCallExpression)) { |
| return false; |
| } |
| final PropertyCallExpression lOperand = (PropertyCallExpression) rawLOperand; |
| |
| // L2. Check that we're using the iterator |
| final Expression rawTargetExpression = lOperand.getTargetExpression(); |
| if (!(lOperand.getTargetExpression() instanceof NameExpression)) { |
| return false; |
| } |
| final NameExpression nameExpression = (NameExpression) rawTargetExpression; |
| if (!iterator.getName().equals(nameExpression.getName())) { |
| return false; |
| } |
| |
| // MIDDLE - we should be using a comparison operator (but not "!="). |
| // Antonio: checking with Dimitris on subtype relationships in =/!=, </<= and >/>= |
| return ast instanceof EqualsOperatorExpression && !(ast instanceof NotEqualsOperatorExpression) |
| || ast instanceof GreaterThanOperatorExpression |
| || ast instanceof LessThanOperatorExpression |
| || ast instanceof NotEqualsOperatorExpression |
| || ast instanceof GreaterEqualOperatorExpression |
| || ast instanceof LessEqualOperatorExpression |
| ; |
| |
| } catch (Exception e) { |
| return false; |
| } |
| } |
| |
| private String isIndexed(String attributename) { |
| String result = null; |
| |
| try (IGraphTransaction ignored = graph.beginTransaction()) { |
| String indexname = metaclass.getOutgoingWithType("epackage").iterator().next().getEndNode() |
| .getProperty(IModelIndexer.IDENTIFIER_PROPERTY) + "##" |
| + metaclass.getProperty(IModelIndexer.IDENTIFIER_PROPERTY) + "##" + attributename; |
| |
| // if (indexManager == null) indexManager = graph.index(); |
| // System.err.println(indexname); |
| // System.err.println(graph.getNodeIndexNames()); |
| |
| if (graph.nodeIndexExists(indexname)) |
| result = indexname; |
| |
| ignored.success(); |
| } catch (Exception e) { |
| LOGGER.warn("Suppressed exception from isIndexed", e); |
| } |
| |
| return result; |
| } |
| } |