/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.nifi.hl7.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.tree.Tree;
import org.apache.nifi.hl7.model.HL7Message;
import org.apache.nifi.hl7.query.evaluator.BooleanEvaluator;
import org.apache.nifi.hl7.query.evaluator.Evaluator;
import org.apache.nifi.hl7.query.evaluator.IntegerEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.EqualsEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.GreaterThanEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.GreaterThanOrEqualEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.IsNullEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.LessThanEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.LessThanOrEqualEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.NotEqualsEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.NotEvaluator;
import org.apache.nifi.hl7.query.evaluator.comparison.NotNullEvaluator;
import org.apache.nifi.hl7.query.evaluator.literal.IntegerLiteralEvaluator;
import org.apache.nifi.hl7.query.evaluator.literal.StringLiteralEvaluator;
import org.apache.nifi.hl7.query.evaluator.logic.AndEvaluator;
import org.apache.nifi.hl7.query.evaluator.logic.OrEvaluator;
import org.apache.nifi.hl7.query.evaluator.message.DeclaredReferenceEvaluator;
import org.apache.nifi.hl7.query.evaluator.message.DotEvaluator;
import org.apache.nifi.hl7.query.evaluator.message.MessageEvaluator;
import org.apache.nifi.hl7.query.evaluator.message.SegmentEvaluator;
import org.apache.nifi.hl7.query.exception.HL7QueryParsingException;
import org.apache.nifi.hl7.query.result.MissedResult;
import org.apache.nifi.hl7.query.result.StandardQueryResult;
import org.apache.nifi.hl7.query.antlr.HL7QueryLexer;
import org.apache.nifi.hl7.query.antlr.HL7QueryParser;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.AND;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.DECLARE;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.DOT;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.EQUALS;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.GE;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.GT;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.IDENTIFIER;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.IS_NULL;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.LE;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.LT;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.MESSAGE;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.NOT;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.NOT_EQUALS;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.NOT_NULL;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.NUMBER;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.OR;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.REQUIRED;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.SEGMENT_NAME;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.SELECT;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.STRING_LITERAL;
import static org.apache.nifi.hl7.query.antlr.HL7QueryParser.WHERE;
public class HL7Query {
private final Tree tree;
private final String query;
private final Set<Declaration> declarations = new HashSet<>();
private final List<Selection> selections;
private final BooleanEvaluator whereEvaluator;
private HL7Query(final Tree tree, final String query) {
this.tree = tree;
this.query = query;
List<Selection> select = null;
BooleanEvaluator where = null;
for (int i = 0; i < tree.getChildCount(); i++) {
final Tree child = tree.getChild(i);
switch (child.getType()) {
case DECLARE:
processDeclare(child);
break;
case SELECT:
select = processSelect(child);
break;
case WHERE:
where = processWhere(child);
break;
default:
throw new HL7QueryParsingException("Found unexpected clause at root level: " + tree.getText());
}
}
this.whereEvaluator = where;
this.selections = select;
}
private void processDeclare(final Tree declare) {
for (int i = 0; i < declare.getChildCount(); i++) {
final Tree declarationTree = declare.getChild(i);
final String identifier = declarationTree.getChild(0).getText();
final Tree requiredOrOptionalTree = declarationTree.getChild(1);
final boolean required = requiredOrOptionalTree.getType() == REQUIRED;
final String segmentName = declarationTree.getChild(2).getText();
final Declaration declaration = new Declaration() {
@Override
public String getAlias() {
return identifier;
}
@Override
public boolean isRequired() {
return required;
}
@Override
public Object getDeclaredValue(final HL7Message message) {
if (message == null) {
return null;
}
return message.getSegments(segmentName);
}
};
declarations.add(declaration);
}
}
private List<Selection> processSelect(final Tree select) {
final List<Selection> selections = new ArrayList<>();
for (int i = 0; i < select.getChildCount(); i++) {
final Tree selectable = select.getChild(i);
final String alias = getSelectedName(selectable);
final Evaluator<?> selectionEvaluator = buildReferenceEvaluator(selectable);
final Selection selection = new Selection(selectionEvaluator, alias);
selections.add(selection);
}
return selections;
}
private String getSelectedName(final Tree selectable) {
if (selectable.getChildCount() == 0) {
return selectable.getText();
} else if (selectable.getType() == DOT) {
return getSelectedName(selectable.getChild(0)) + "." + getSelectedName(selectable.getChild(1));
} else {
return selectable.getChild(selectable.getChildCount() - 1).getText();
}
}
private BooleanEvaluator processWhere(final Tree where) {
return buildBooleanEvaluator(where.getChild(0));
}
private Evaluator<?> buildReferenceEvaluator(final Tree tree) {
switch (tree.getType()) {
case MESSAGE:
return new MessageEvaluator();
case SEGMENT_NAME:
return new SegmentEvaluator(new StringLiteralEvaluator(tree.getText()));
case IDENTIFIER:
return new DeclaredReferenceEvaluator(new StringLiteralEvaluator(tree.getText()));
case DOT:
final Tree firstChild = tree.getChild(0);
final Tree secondChild = tree.getChild(1);
return new DotEvaluator(buildReferenceEvaluator(firstChild), buildIntegerEvaluator(secondChild));
case STRING_LITERAL:
return new StringLiteralEvaluator(tree.getText());
case NUMBER:
return new IntegerLiteralEvaluator(Integer.parseInt(tree.getText()));
default:
throw new HL7QueryParsingException("Failed to build evaluator for " + tree.getText());
}
}
private IntegerEvaluator buildIntegerEvaluator(final Tree tree) {
switch (tree.getType()) {
case NUMBER:
return new IntegerLiteralEvaluator(Integer.parseInt(tree.getText()));
default:
throw new HL7QueryParsingException("Failed to build Integer Evaluator for " + tree.getText());
}
}
private BooleanEvaluator buildBooleanEvaluator(final Tree tree) {
// TODO: add Date comparisons
// LT/GT/GE/GE should allow for dates based on Field's Type
// BETWEEN
// DATE('2015/01/01')
// DATE('2015/01/01 12:00:00')
// DATE('24 HOURS AGO')
// DATE('YESTERDAY')
switch (tree.getType()) {
case EQUALS:
return new EqualsEvaluator(buildReferenceEvaluator(tree.getChild(0)), buildReferenceEvaluator(tree.getChild(1)));
case NOT_EQUALS:
return new NotEqualsEvaluator(buildReferenceEvaluator(tree.getChild(0)), buildReferenceEvaluator(tree.getChild(1)));
case GT:
return new GreaterThanEvaluator(buildReferenceEvaluator(tree.getChild(0)), buildReferenceEvaluator(tree.getChild(1)));
case LT:
return new LessThanEvaluator(buildReferenceEvaluator(tree.getChild(0)), buildReferenceEvaluator(tree.getChild(1)));
case GE:
return new GreaterThanOrEqualEvaluator(buildReferenceEvaluator(tree.getChild(0)), buildReferenceEvaluator(tree.getChild(1)));
case LE:
return new LessThanOrEqualEvaluator(buildReferenceEvaluator(tree.getChild(0)), buildReferenceEvaluator(tree.getChild(1)));
case NOT:
return new NotEvaluator(buildBooleanEvaluator(tree.getChild(0)));
case AND:
return new AndEvaluator(buildBooleanEvaluator(tree.getChild(0)), buildBooleanEvaluator(tree.getChild(1)));
case OR:
return new OrEvaluator(buildBooleanEvaluator(tree.getChild(0)), buildBooleanEvaluator(tree.getChild(1)));
case IS_NULL:
return new IsNullEvaluator(buildReferenceEvaluator(tree.getChild(0)));
case NOT_NULL:
return new NotNullEvaluator(buildReferenceEvaluator(tree.getChild(0)));
default:
throw new HL7QueryParsingException("Cannot build boolean evaluator for '" + tree.getText() + "'");
}
}
Tree getTree() {
return tree;
}
public String getQuery() {
return query;
}
@Override
public String toString() {
return "HL7Query[" + query + "]";
}
public static HL7Query compile(final String query) {
try {
final CommonTokenStream lexerTokenStream = createTokenStream(query);
final HL7QueryParser parser = new HL7QueryParser(lexerTokenStream);
final Tree tree = (Tree) parser.query().getTree();
return new HL7Query(tree, query);
} catch (final HL7QueryParsingException e) {
throw e;
} catch (final Exception e) {
throw new HL7QueryParsingException(e);
}
}
private static CommonTokenStream createTokenStream(final String expression) throws HL7QueryParsingException {
final CharStream input = new ANTLRStringStream(expression);
final HL7QueryLexer lexer = new HL7QueryLexer(input);
return new CommonTokenStream(lexer);
}
public List<Class<?>> getReturnTypes() {
final List<Class<?>> returnTypes = new ArrayList<>();
for (final Selection selection : selections) {
returnTypes.add(selection.getEvaluator().getType());
}
return returnTypes;
}
@SuppressWarnings("unchecked")
public QueryResult evaluate(final HL7Message message) {
int totalIterations = 1;
final LinkedHashMap<String, List<Object>> possibleValueMap = new LinkedHashMap<>();
for (final Declaration declaration : declarations) {
final Object value = declaration.getDeclaredValue(message);
if (value == null && declaration.isRequired()) {
return new MissedResult(selections);
}
final List<Object> possibleValues;
if (value instanceof List) {
possibleValues = (List<Object>) value;
} else if (value instanceof Collection) {
possibleValues = new ArrayList<Object>((Collection<Object>) value);
} else {
possibleValues = new ArrayList<>(1);
possibleValues.add(value);
}
if (possibleValues.isEmpty()) {
return new MissedResult(selections);
}
possibleValueMap.put(declaration.getAlias(), possibleValues);
totalIterations *= possibleValues.size();
}
final Set<Map<String, Object>> resultSet = new HashSet<>();
for (int i = 0; i < totalIterations; i++) {
final Map<String, Object> aliasValues = assignAliases(possibleValueMap, i);
aliasValues.put(Evaluator.MESSAGE_KEY, message);
if (whereEvaluator == null || Boolean.TRUE.equals(whereEvaluator.evaluate(aliasValues))) {
final Map<String, Object> resultMap = new HashMap<>();
for (final Selection selection : selections) {
final Object value = selection.getEvaluator().evaluate(aliasValues);
resultMap.put(selection.getName(), value);
}
resultSet.add(resultMap);
}
}
return new StandardQueryResult(selections, resultSet);
}
/**
* assigns one of the possible values to each alias, based on which iteration this is.
* require LinkedHashMap just to be very clear and explicit that the order of the Map MUST be guaranteed
* between multiple invocations of this method.
* package protected for testing visibility
*/
static Map<String, Object> assignAliases(final LinkedHashMap<String, List<Object>> possibleValues, final int iteration) {
final Map<String, Object> aliasMap = new HashMap<>();
int divisor = 1;
for (final Map.Entry<String, List<Object>> entry : possibleValues.entrySet()) {
final String alias = entry.getKey();
final List<Object> validValues = entry.getValue();
final int idx = (iteration / divisor) % validValues.size();
final Object obj = validValues.get(idx);
aliasMap.put(alias, obj);
divisor *= validValues.size();
}
return aliasMap;
}
public String toTreeString() {
final StringBuilder sb = new StringBuilder();
toTreeString(tree, sb, 0);
return sb.toString();
}
private void toTreeString(final Tree tree, final StringBuilder sb, final int indentLevel) {
final String nodeName = tree.getText();
for (int i = 0; i < indentLevel; i++) {
sb.append(" ");
}
sb.append(nodeName);
sb.append("\n");
for (int i = 0; i < tree.getChildCount(); i++) {
final Tree child = tree.getChild(i);
toTreeString(child, sb, indentLevel + 2);
}
}
}