001    /*
002     * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003     * Copyright (C) 2011 NightLabs Consulting GmbH
004     *
005     * This program is free software: you can redistribute it and/or modify
006     * it under the terms of the GNU Affero General Public License as
007     * published by the Free Software Foundation, either version 3 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU Affero General Public License for more details.
014     *
015     * You should have received a copy of the GNU Affero General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    package org.cumulus4j.store.query.eval;
019    
020    import java.util.Collection;
021    import java.util.Collections;
022    import java.util.HashMap;
023    import java.util.HashSet;
024    import java.util.Map;
025    import java.util.Set;
026    
027    import org.cumulus4j.store.model.ClassMeta;
028    import org.cumulus4j.store.model.DataEntry;
029    import org.cumulus4j.store.model.FieldMeta;
030    import org.cumulus4j.store.model.FieldMetaRole;
031    import org.cumulus4j.store.model.IndexEntry;
032    import org.cumulus4j.store.model.IndexEntryFactory;
033    import org.cumulus4j.store.model.IndexEntryObjectRelationHelper;
034    import org.cumulus4j.store.model.IndexValue;
035    import org.cumulus4j.store.model.ObjectContainer;
036    import org.cumulus4j.store.query.QueryEvaluator;
037    import org.cumulus4j.store.query.QueryHelper;
038    import org.cumulus4j.store.query.method.MethodEvaluator;
039    import org.datanucleus.ClassLoaderResolver;
040    import org.datanucleus.metadata.AbstractMemberMetaData;
041    import org.datanucleus.plugin.ConfigurationElement;
042    import org.datanucleus.query.QueryUtils;
043    import org.datanucleus.query.expression.Expression;
044    import org.datanucleus.query.expression.Expression.Operator;
045    import org.datanucleus.query.expression.InvokeExpression;
046    import org.datanucleus.query.expression.Literal;
047    import org.datanucleus.query.expression.ParameterExpression;
048    import org.datanucleus.query.expression.PrimaryExpression;
049    import org.datanucleus.query.expression.VariableExpression;
050    import org.datanucleus.store.StoreManager;
051    import org.slf4j.Logger;
052    import org.slf4j.LoggerFactory;
053    
054    /**
055     * Series of helper methods for processing expressions.
056     */
057    public class ExpressionHelper
058    {
059            static Map<String, MethodEvaluator> evaluatorByMethod = new HashMap<String, MethodEvaluator>();
060    
061            /**
062             * Accessor for the evaluator object for use of method xxx(...) of class Yyy in queries.
063             * @param storeMgr Store Manager
064             * @param clr ClassLoader resolver
065             * @param clsName The class on which to invoke the method
066             * @param method The method to call on the class
067             * @return The MethodEvaluator
068             */
069            public static MethodEvaluator getMethodEvaluatorForMethodOfClass(StoreManager storeMgr, ClassLoaderResolver clr,
070                            String clsName, String method) {
071    
072                    String key = clsName + "." + method;
073    
074                    // Check if it is cached
075                    if (evaluatorByMethod.containsKey(key)) {
076                            return evaluatorByMethod.get(key);
077                    }
078    
079                    ConfigurationElement elem =
080                            storeMgr.getNucleusContext().getPluginManager().getConfigurationElementForExtension(
081                                            "org.cumulus4j.store.query_method", new String[]{"class", "method"}, new String[]{clsName, method});
082                    if (elem == null) {
083                            throw new UnsupportedOperationException("Invocation of method \""+method+"\" on object of type \""+clsName+"\" is not supported");
084                    }
085    
086                    String evaluatorClassName = elem.getAttribute("evaluator");
087                    Class<?> evaluatorCls = clr.classForName(evaluatorClassName);
088                    MethodEvaluator eval = null;
089                    try {
090                            eval = (MethodEvaluator) evaluatorCls.newInstance();
091                    } catch (Exception e) {
092                            throw new UnsupportedOperationException("Attempt to instantiate an evaluator for "+key + " threw an exception", e);
093                    }
094    
095                    // Cache  the method for later use
096                    evaluatorByMethod.put(key, eval);
097    
098                    return eval;
099            }
100    
101            /**
102             * Method to evaluate the arguments for passing in to a method invocation.
103             * @param queryEval The QueryEvaluator
104             * @param expr The invoke expression
105             * @return The argument(s)
106             */
107            public static Object[] getEvaluatedInvokeArguments(QueryEvaluator queryEval, InvokeExpression expr) {
108                    Object[] invokeArgs = new Object[expr.getArguments().size()];
109    
110                    int i=0;
111                    for (Expression argExpr : expr.getArguments()) {
112                            if (argExpr instanceof Literal)
113                                    invokeArgs[i++] = ((Literal)argExpr).getLiteral();
114                            else if (argExpr instanceof ParameterExpression)
115                                    invokeArgs[i++] = QueryUtils.getValueForParameterExpression(queryEval.getParameterValues(),
116                                                    (ParameterExpression)argExpr);
117                            else
118                                    throw new UnsupportedOperationException("NYI");
119                    }
120                    return invokeArgs;
121            }
122    
123            /**
124             * Method to evaluate the argument for passing in to a method invocation.
125             * @param queryEval The QueryEvaluator
126             * @param expr The invoke expression
127             * @return The argument
128             */
129            public static Object getEvaluatedInvokeArgument(QueryEvaluator queryEval, InvokeExpression expr) {
130                    if (expr.getArguments().size() != 1) {
131                            throw new UnsupportedOperationException("Invalid number of arguments to " + expr.getOperation());
132                    }
133    
134                    Object argExpr = expr.getArguments().get(0);
135                    if (argExpr instanceof Literal)
136                            return ((Literal)argExpr).getLiteral();
137                    else if (argExpr instanceof ParameterExpression)
138                            return QueryUtils.getValueForParameterExpression(queryEval.getParameterValues(), (ParameterExpression)argExpr);
139                    else
140                            throw new UnsupportedOperationException("NYI");
141            }
142    
143            private static abstract class AbstractContainsResolver extends PrimaryExpressionResolver
144            {
145                    protected FieldMetaRole role;
146                    protected boolean negate;
147    
148                    protected Set<Long> negateIfNecessary(FieldMeta fieldMeta, Set<Long> positiveResult)
149                    {
150                            if (!negate) {
151                                    return positiveResult;
152                            }
153    
154                            Class<?> candidateClass = executionContext.getClassLoaderResolver().classForName(fieldMeta.getClassMeta().getClassName());
155                            Set<ClassMeta> candidateClassMetas = QueryHelper.getCandidateClassMetas(queryEvaluator.getStoreManager(),
156                                            executionContext, candidateClass, true);
157                            Set<Long> allDataEntryIDs = queryEvaluator.getAllDataEntryIDsForCandidateClasses(candidateClassMetas);
158    
159                            Set<Long> negativeResult = new HashSet<Long>(allDataEntryIDs.size() - positiveResult.size());
160                            for (Long dataEntryID : allDataEntryIDs) {
161                                    if (!positiveResult.contains(dataEntryID))
162                                            negativeResult.add(dataEntryID);
163                            }
164                            return negativeResult;
165                    }
166    
167                    public AbstractContainsResolver(
168                                    QueryEvaluator queryEvaluator, PrimaryExpression primaryExpression,
169                                    FieldMetaRole role, boolean negate
170                    )
171                    {
172                            super(queryEvaluator, primaryExpression);
173                            this.role = role;
174                            this.negate = negate;
175    
176                            if (role != FieldMetaRole.collectionElement && role != FieldMetaRole.mapKey && role != FieldMetaRole.mapValue)
177                                    throw new IllegalArgumentException("role == " + role);
178                    }
179    
180                    @Override
181                    protected final Set<Long> queryEnd(FieldMeta fieldMeta) {
182                            AbstractMemberMetaData mmd = fieldMeta.getDataNucleusMemberMetaData(executionContext);
183                            FieldMeta subFieldMeta = fieldMeta.getSubFieldMeta(role);
184    
185                            boolean argumentIsPersistent;
186                            Class<?> argumentType;
187                            switch (role) {
188                                    case collectionElement:
189                                            argumentIsPersistent = mmd.getCollection().elementIsPersistent();
190                                            argumentType = executionContext.getClassLoaderResolver().classForName(mmd.getCollection().getElementType());
191                                            break;
192                                    case mapKey:
193                                            argumentIsPersistent = mmd.getMap().keyIsPersistent();
194                                            argumentType = executionContext.getClassLoaderResolver().classForName(mmd.getMap().getKeyType());
195                                            break;
196                                    case mapValue:
197                                            argumentIsPersistent = mmd.getMap().valueIsPersistent();
198                                            argumentType = executionContext.getClassLoaderResolver().classForName(mmd.getMap().getValueType());
199                                            break;
200                                    default:
201                                            throw new IllegalStateException("Unknown role: " + role);
202                            }
203    
204                            return _queryEnd(fieldMeta, mmd, subFieldMeta, argumentIsPersistent, argumentType);
205                    }
206    
207                    protected abstract Set<Long> _queryEnd(FieldMeta fieldMeta, AbstractMemberMetaData mmd, FieldMeta subFieldMeta, boolean argumentIsPersistent, Class<?> argumentType
208                    );
209            }
210    
211            /**
212             * Resolve {@link Collection#contains(Object)} with the argument being a query variable.
213             *
214             * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
215             */
216            public static class ContainsVariableResolver extends AbstractContainsResolver
217            {
218                    private VariableExpression variableExpr;
219    
220                    public ContainsVariableResolver(
221                                    QueryEvaluator queryEvaluator, PrimaryExpression primaryExpression,
222                                    FieldMetaRole role, VariableExpression variableExpr, boolean negate
223                    )
224                    {
225                            super(queryEvaluator, primaryExpression, role, negate);
226                            this.variableExpr = variableExpr;
227    
228                            if (variableExpr == null)
229                                    throw new IllegalArgumentException("variableExpr == null");
230    
231                            if (variableExpr.getSymbol() == null)
232                                    throw new IllegalArgumentException("variableExpr.getSymbol() == null");
233                    }
234    
235                    @Override
236                    public Set<Long> _queryEnd(FieldMeta fieldMeta, AbstractMemberMetaData mmd, FieldMeta subFieldMeta,
237                                    boolean argumentIsPersistent, Class<?> argumentType)
238                    {
239                            if (argumentIsPersistent || subFieldMeta.getMappedByFieldMeta(executionContext) != null) {
240                                    AbstractExpressionEvaluator<?> eval = queryEvaluator.getExpressionEvaluator();
241    
242                                    Collection<Long> valueDataEntryIDs = eval.queryResultDataEntryIDs(
243                                                    new ResultDescriptor(variableExpr.getSymbol(), argumentType, subFieldMeta.getMappedByFieldMeta(executionContext))
244                                    );
245                                    if (valueDataEntryIDs == null)
246                                            return null;
247    
248                                    Set<Long> result = new HashSet<Long>();
249                                    if (mmd.getMappedBy() != null) {
250                                            for (Long valueDataEntryID : valueDataEntryIDs) {
251                                                    DataEntry valueDataEntry = DataEntry.getDataEntry(queryEvaluator.getPersistenceManagerForData(), valueDataEntryID);
252                                                    ObjectContainer constantObjectContainer = queryEvaluator.getEncryptionHandler().decryptDataEntry(cryptoContext, valueDataEntry);
253                                                    Object value = constantObjectContainer.getValue(
254                                                                    fieldMeta.getMappedByFieldMeta(executionContext).getFieldID()
255                                                    );
256                                                    Long mappedByDataEntryID = (Long) value;
257                                                    if (mappedByDataEntryID != null)
258                                                            result.add(mappedByDataEntryID);
259                                            }
260                                    }
261                                    else {
262                                            for (Long valueDataEntryID : valueDataEntryIDs) {
263                                                    IndexEntry indexEntry =
264                                                            IndexEntryObjectRelationHelper.getIndexEntry(queryEvaluator.getPersistenceManagerForIndex(), subFieldMeta, valueDataEntryID);
265                                                    if (indexEntry != null) {
266                                                            IndexValue indexValue = queryEvaluator.getEncryptionHandler().decryptIndexEntry(cryptoContext, indexEntry);
267                                                            result.addAll(indexValue.getDataEntryIDs());
268                                                    }
269                                            }
270                                    }
271                                    return negateIfNecessary(fieldMeta, result);
272                            }
273                            else {
274                                    AbstractExpressionEvaluator<?> eval = queryEvaluator.getExpressionEvaluator();
275                                    Set<Long> result = eval.queryResultDataEntryIDs(new ResultDescriptor(variableExpr.getSymbol(), argumentType, subFieldMeta));
276                                    return negateIfNecessary(fieldMeta, result);
277                            }
278                    }
279            }
280    
281            /**
282             * Resolve {@link Collection#contains(Object)} with the argument being a concrete value (a 'constant').
283             * This concrete value is either a query parameter or a literal - i.e. no variable.
284             *
285             * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
286             */
287            public static class ContainsConstantResolver extends AbstractContainsResolver
288            {
289                    private static Logger logger = LoggerFactory.getLogger(ContainsConstantResolver.class);
290                    private Object constant;
291    
292                    public ContainsConstantResolver(
293                                    QueryEvaluator queryEvaluator, PrimaryExpression primaryExpression,
294                                    FieldMetaRole role, Object constant, boolean negate
295                    )
296                    {
297                            super(queryEvaluator, primaryExpression, role, negate);
298                            this.constant = constant;
299                    }
300    
301                    private static Set<Long> emptyDataEntryIDs = Collections.emptySet();
302    
303                    @Override
304                    public Set<Long> _queryEnd(FieldMeta fieldMeta, AbstractMemberMetaData mmd, FieldMeta subFieldMeta,
305                                    boolean argumentIsPersistent, Class<?> argumentType)
306                    {
307                            if (constant != null && !argumentType.isInstance(constant)) {
308                                    logger.debug(
309                                                    "_queryEnd: constant {} is of type {} but field {} is of type {} and thus constant cannot be contained. Returning empty set!",
310                                                    new Object[] {
311                                                                    constant, constant.getClass().getName(), fieldMeta, argumentType.getClass().getName()
312                                                    }
313                                    );
314                                    return negateIfNecessary(fieldMeta, emptyDataEntryIDs);
315                            }
316    
317                            if (argumentIsPersistent) {
318                                    Long constantDataEntryID = null;
319                                    if (constant != null) {
320                                            ClassMeta constantClassMeta = queryEvaluator.getStoreManager().getClassMeta(executionContext, constant.getClass());
321                                            Object constantID = executionContext.getApiAdapter().getIdForObject(constant);
322                                            if (constantID == null)
323                                                    throw new IllegalStateException("The ApiAdapter returned null as object-ID for: " + constant);
324    
325                                            if (mmd.getMappedBy() != null) {
326                                                    DataEntry constantDataEntry = DataEntry.getDataEntry(queryEvaluator.getPersistenceManagerForData(),
327                                                                    constantClassMeta, constantID.toString());
328                                                    ObjectContainer constantObjectContainer = queryEvaluator.getEncryptionHandler().decryptDataEntry(cryptoContext, constantDataEntry);
329                                                    Object value = constantObjectContainer.getValue(
330                                                                    fieldMeta.getMappedByFieldMeta(executionContext).getFieldID()
331                                                    );
332    
333                                                    Long mappedByDataEntryID = (Long) value;
334                                                    if (mappedByDataEntryID == null)
335                                                            return negateIfNecessary(fieldMeta, emptyDataEntryIDs);
336                                                    else
337                                                            return negateIfNecessary(fieldMeta, Collections.singleton(mappedByDataEntryID));
338                                            }
339    
340                                            constantDataEntryID = DataEntry.getDataEntryID(queryEvaluator.getPersistenceManagerForData(), constantClassMeta,
341                                                            constantID.toString());
342                                    }
343                                    IndexEntry indexEntry = IndexEntryObjectRelationHelper.getIndexEntry(queryEvaluator.getPersistenceManagerForIndex(),
344                                                    subFieldMeta, constantDataEntryID);
345                                    if (indexEntry == null)
346                                            return negateIfNecessary(fieldMeta, emptyDataEntryIDs);
347    
348                                    IndexValue indexValue = queryEvaluator.getEncryptionHandler().decryptIndexEntry(cryptoContext, indexEntry);
349                                    return negateIfNecessary(fieldMeta, indexValue.getDataEntryIDs());
350                            }
351                            else if (subFieldMeta.getMappedByFieldMeta(executionContext) != null) {
352                                    FieldMeta oppositeFieldMeta = subFieldMeta.getMappedByFieldMeta(executionContext);
353                                    IndexEntryFactory indexEntryFactory =
354                                            queryEvaluator.getStoreManager().getIndexFactoryRegistry().getIndexEntryFactory(executionContext, oppositeFieldMeta, true);
355                                    IndexEntry indexEntry = indexEntryFactory == null ? null :
356                                            indexEntryFactory.getIndexEntry(queryEvaluator.getPersistenceManagerForIndex(), oppositeFieldMeta, constant);
357                                    if (indexEntry == null)
358                                            return negateIfNecessary(fieldMeta, emptyDataEntryIDs);
359    
360                                    IndexValue indexValue = queryEvaluator.getEncryptionHandler().decryptIndexEntry(cryptoContext, indexEntry);
361                                    Set<Long> result = new HashSet<Long>(indexValue.getDataEntryIDs().size());
362                                    for (Long elementDataEntryID : indexValue.getDataEntryIDs()) {
363                                            DataEntry elementDataEntry = DataEntry.getDataEntry(queryEvaluator.getPersistenceManagerForData(), elementDataEntryID);
364                                            ObjectContainer elementObjectContainer = queryEvaluator.getEncryptionHandler().decryptDataEntry(cryptoContext, elementDataEntry);
365                                            Object value = elementObjectContainer.getValue(
366                                                            fieldMeta.getMappedByFieldMeta(executionContext).getFieldID()
367                                            );
368    
369                                            Long mappedByDataEntryID = (Long) value;
370                                            if (mappedByDataEntryID != null)
371                                                    result.add(mappedByDataEntryID);
372                                    }
373                                    return negateIfNecessary(fieldMeta, result);
374                            }
375                            else {
376                                    IndexEntryFactory indexEntryFactory = queryEvaluator.getStoreManager().getIndexFactoryRegistry().getIndexEntryFactory(executionContext, subFieldMeta, true);
377                                    IndexEntry indexEntry = indexEntryFactory == null ? null :
378                                            indexEntryFactory.getIndexEntry(queryEvaluator.getPersistenceManagerForIndex(), subFieldMeta, constant);
379                                    if (indexEntry == null)
380                                            return negateIfNecessary(fieldMeta, emptyDataEntryIDs);
381    
382                                    IndexValue indexValue = queryEvaluator.getEncryptionHandler().decryptIndexEntry(cryptoContext, indexEntry);
383                                    return negateIfNecessary(fieldMeta, indexValue.getDataEntryIDs());
384                            }
385                    }
386            }
387    
388            public static String getOperatorAsJDOQLSymbol(Operator operator, boolean negate)
389            {
390                    if (Expression.OP_EQ == operator)
391                            return negate ? "!=" : "==";
392                    if (Expression.OP_NOTEQ == operator)
393                            return negate ? "==" : "!=";
394                    if (Expression.OP_LT == operator)
395                            return negate ? ">=" : "<";
396                    if (Expression.OP_LTEQ == operator)
397                            return negate ? ">"  : "<=";
398                    if (Expression.OP_GT == operator)
399                            return negate ? "<=" : ">";
400                    if (Expression.OP_GTEQ == operator)
401                            return negate ? "<"  : ">=";
402    
403                    throw new UnsupportedOperationException("NYI");
404            }
405    }