org.openmrs.module.reporting.query.evaluator.CompositionQueryEvaluator.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.reporting.query.evaluator.CompositionQueryEvaluator.java

Source

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.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://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.module.reporting.query.evaluator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.OpenmrsObject;
import org.openmrs.module.reporting.common.BooleanOperator;
import org.openmrs.module.reporting.definition.evaluator.DefinitionEvaluator;
import org.openmrs.module.reporting.evaluation.EvaluationContext;
import org.openmrs.module.reporting.evaluation.EvaluationException;
import org.openmrs.module.reporting.evaluation.MissingDependencyException;
import org.openmrs.module.reporting.evaluation.parameter.Mapped;
import org.openmrs.module.reporting.query.CompositionQuery;
import org.openmrs.module.reporting.query.IdSet;
import org.openmrs.module.reporting.query.Query;
import org.openmrs.module.reporting.query.QueryUtil;

import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import java.util.Stack;

/**
 * Evaluates a CompositionQuery and produces an IdSet
 */
public abstract class CompositionQueryEvaluator<Q extends Query<T>, T extends OpenmrsObject>
        implements DefinitionEvaluator<Q> {

    protected Log log = LogFactory.getLog(getClass());

    public static final List<String> AND_WORDS = Arrays.asList("and", "intersection", "*");
    public static final List<String> OR_WORDS = Arrays.asList("or", "union", "+");
    public static final List<String> NOT_WORDS = Arrays.asList("not", "!");
    public static final List<Character> OPEN_PARENTHESES_WORDS = Arrays.asList('(', '[', '{');
    public static final List<Character> CLOSE_PARENTHESES_WORDS = Arrays.asList(')', ']', '}');
    public static final List<Character> CHARACTER_WORDS = Arrays.asList('+', '!', '(', '[', '{', ')', ']', '}');
    public static final List<Class<?>> SUPPORTED_TYPES = Arrays.asList(Integer.class, BooleanOperator.class,
            Query.class, List.class);

    /**
     * Default Constructor
     */
    public CompositionQueryEvaluator() {
    }

    /**
     * Implementation classes need to override this method to provide the necessary functionality to evaluate a Query
     */
    protected abstract IdSet<T> evaluateQuery(Mapped<Q> query, EvaluationContext context)
            throws EvaluationException;

    /**
     * Implementation classes need to override this method to return the necessary definition that can be evaluated to all ids of that type
     */
    protected abstract Q getAllIdQuery();

    protected IdSet<T> evaluateToIdSet(Q compositionQuery, EvaluationContext context) throws EvaluationException {
        CompositionQuery<Q, T> composition = (CompositionQuery<Q, T>) compositionQuery;
        try {
            List<Object> tokens = parseIntoTokens(composition.getCompositionString());
            return evaluateTokens(tokens, composition, context);
        } catch (MissingDependencyException ex) {
            String name = composition.getName() != null ? composition.getName()
                    : composition.getCompositionString();
            throw new EvaluationException(
                    "sub-query '" + ex.getPropertyThatFailed() + "' of composition '" + name + "'", ex);
        }
    }

    /**
     * Recursively traverse the List<Object> phrase to produce a (possibly nested) CompositionQuery
     * If another List<Object> is found in the list, recursively evaluate it in place
     * If anything in this list is a key into searches, replace it with the relevant filter from searches
     * @throws EvaluationException
     */
    @SuppressWarnings("unchecked")
    protected IdSet<T> evaluateTokens(List<Object> tokens, CompositionQuery<Q, T> composition,
            EvaluationContext context) throws EvaluationException {

        log.debug("Evaluating: " + tokens + " for searches: " + composition.getSearches());
        List<Object> use = new ArrayList<Object>();
        for (Object o : tokens) {
            log.debug("Checking token: " + o);
            if (o instanceof List) {
                log.debug("This is a list, evaluate it as a group...");
                IdSet<T> result = evaluateTokens((List<Object>) o, composition, context);
                log.debug(o + " evaluated to: " + result.getSize());
                use.add(result);
            } else if (o instanceof String || o instanceof Integer) {
                log.debug("This refers to a Search, try to find it...");
                Mapped<Q> mappedQuery = composition.getSearches().get(o.toString());
                if (mappedQuery == null || mappedQuery.getParameterizable() == null) {
                    throw new MissingDependencyException(o.toString());
                }
                log.debug("Found search: " + mappedQuery);
                IdSet<T> result;
                try {
                    result = evaluateQuery(mappedQuery, context);
                } catch (Exception ex) {
                    throw new EvaluationException(o.toString(), ex);
                }
                log.debug("This evaluated to: " + result.getSize());
                use.add(result);
            } else {
                log.debug("This refers to an operator: " + o);
                use.add(o);
            }
        }
        log.debug("Converted tokens to IdSets and Operators: " + use);

        log.debug("Inverting all [..., NOT, IdSet, ...] combinations");
        boolean invertTheNext = false;
        for (ListIterator<Object> i = use.listIterator(); i.hasNext();) {
            Object o = i.next();
            log.debug("Looking at element: " + o);
            if (o instanceof BooleanOperator) {
                if (o == BooleanOperator.NOT) {
                    i.remove();
                    invertTheNext = !invertTheNext;
                    log.debug("This is a NOT, so removing it and invert the next = " + invertTheNext);
                } else {
                    if (invertTheNext) {
                        throw new RuntimeException(
                                "Invalid expression string, cannot have a NOT followed by an AND");
                    }
                }
            } else {
                if (invertTheNext) {
                    log.debug("Need to invert this...");
                    if (o instanceof IdSet) {
                        IdSet<T> toInvert = (IdSet<T>) o;
                        IdSet<T> superSet = evaluateQuery(Mapped.noMappings(getAllIdQuery()), context);
                        log.debug("Originally an IdSet of size " + toInvert.getSize());
                        log.debug("With a superSet for context of size " + superSet.getSize());
                        IdSet<T> invertedQuery = QueryUtil.subtract(superSet, toInvert);
                        log.debug("Makes a new IdSet is of size " + invertedQuery.getSize());
                        i.set(invertedQuery);
                    } else {
                        throw new RuntimeException(
                                "There is no method implemented for inverting a " + o.getClass());
                    }
                    invertTheNext = false;
                }
            }
        }
        log.debug("NOT conversion complete.  Now have: " + use);

        log.debug("Iterating across all Queries and Operators...");
        IdSet<T> ret = null;
        BooleanOperator operator = BooleanOperator.AND;
        for (Object o : use) {
            if (o instanceof BooleanOperator) {
                operator = (BooleanOperator) o;
                log.debug("New operator: " + operator);
            } else if (o instanceof IdSet) {
                IdSet<T> c = (IdSet<T>) o;
                log.debug("Found IdSet: " + c.getSize());
                if (ret == null) {
                    ret = c;
                    log.debug("Setting this as starting IdSet for return.");
                } else {
                    if (operator == BooleanOperator.AND) {
                        ret = QueryUtil.intersect(ret, c);
                        log.debug("AND this in to get: " + ret.getSize());
                    } else if (operator == BooleanOperator.OR) {
                        ret = QueryUtil.union(ret, c);
                        log.debug("OR this in to get: " + ret.getSize());
                    } else {
                        throw new RuntimeException("Unable to handle BooleanOperator: " + operator);
                    }
                }
            } else {
                throw new RuntimeException(
                        "Can only handle IdSet and Operators.  Unable to handle class: " + o.getClass());
            }
        }
        log.debug("Done.  Returning: " + (ret == null ? null : ret.getSize()));
        return ret;
    }

    /**
     * Elements in this list can be: an Integer, indicating a 1-based index into a search history a
     * BooleanOperator (AND, OR, NOT) a Query, another List of the same form, which indicates a parenthetical expression
     */
    public List<Object> parseIntoTokens(String expression) throws EvaluationException {

        List<Object> tokens = new ArrayList<Object>();
        try {
            StreamTokenizer st = new StreamTokenizer(new StringReader(expression));
            for (Character c : CHARACTER_WORDS) {
                st.ordinaryChar(c);
            }
            while (st.nextToken() != StreamTokenizer.TT_EOF) {
                if (st.ttype == StreamTokenizer.TT_NUMBER) {
                    Integer thisInt = (int) st.nval;
                    if (thisInt < 1) {
                        throw new IllegalArgumentException("Invalid number < 1 found");
                    }
                    tokens.add(thisInt);
                } else if (OPEN_PARENTHESES_WORDS.contains(Character.valueOf((char) st.ttype))) {
                    tokens.add("(");
                } else if (CLOSE_PARENTHESES_WORDS.contains(Character.valueOf((char) st.ttype))) {
                    tokens.add(")");
                } else if (st.ttype == StreamTokenizer.TT_WORD) {
                    tokens.add(st.sval);
                }
            }
            return parseIntoTokens(tokens);
        } catch (Exception e) {
            throw new EvaluationException("Unable to parse expression <" + expression + "> into tokens", e);
        }
    }

    /**
     * Parses the passed tokens into another list of tokens, handling parenthesis
     */
    protected List<Object> parseIntoTokens(List<Object> tokens) throws EvaluationException {
        List<Object> currentLine = new ArrayList<Object>();
        Stack<List<Object>> stack = new Stack<List<Object>>();
        try {
            for (Object token : tokens) {
                if (token instanceof String) {
                    String s = (String) token;
                    String lower = s.toLowerCase();
                    if (AND_WORDS.contains(lower)) {
                        currentLine.add(BooleanOperator.AND);
                    } else if (OR_WORDS.contains(lower)) {
                        currentLine.add(BooleanOperator.OR);
                    } else if (NOT_WORDS.contains(lower)) {
                        currentLine.add(BooleanOperator.NOT);
                    } else {
                        if (s.length() == 1) {
                            char c = s.charAt(0);
                            if (OPEN_PARENTHESES_WORDS.contains(c)) {
                                stack.push(currentLine);
                                currentLine = new ArrayList<Object>();
                            } else if (CLOSE_PARENTHESES_WORDS.contains(c)) {
                                List<Object> l = stack.pop();
                                l.add(currentLine);
                                currentLine = l;
                            } else {
                                currentLine.add(s);
                            }
                        } else {
                            currentLine.add(s);
                        }
                    }
                } else if (SUPPORTED_TYPES.contains(token.getClass())) {
                    currentLine.add(token);
                } else {
                    throw new IllegalArgumentException("Unknown class in token list: " + token.getClass());
                }
            }
        } catch (Exception e) {
            throw new EvaluationException("Unable to parse tokens into tokens", e);
        }
        return currentLine;
    }
}