package nl.ipo.cds.etl.filtering; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import nl.idgis.commons.jobexecutor.Job; import nl.idgis.commons.jobexecutor.JobLogger.LogLevel; import nl.ipo.cds.domain.AttributeExpression; import nl.ipo.cds.domain.AttributeType; import nl.ipo.cds.domain.DatasetFilter; import nl.ipo.cds.domain.FeatureType; import nl.ipo.cds.domain.FeatureTypeAttribute; import nl.ipo.cds.domain.FilterExpression; import nl.ipo.cds.domain.OperatorExpression; import nl.ipo.cds.domain.OperatorExpression.OperatorType; import nl.ipo.cds.domain.ValueExpression; import nl.ipo.cds.domain.ValueExpression.ValueType; import nl.ipo.cds.etl.log.EventLogger; public class FilterExpressionValidator { public enum MessageKey { DATASET_FILTER_TECHNICAL_ERROR, // Technical errors. DATASET_FILTER_MISSING, // There is no dataset filter (filter is null). DATASET_FILTER_NO_DATASET, // The dataset filter has no dataset. DATASET_FILTER_NO_ROOT_EXPRESSION, // The dataset filter has no root expression. DATASET_FILTER_LOGICAL_OPERATOR_WITHOUT_INPUTS, // Logical operator must have at least one input. DATASET_FILTER_LOGICAL_OPERATOR_MISSING_INPUT, // Logical operator has a missing (null) input. DATASET_FILTER_LOGICAL_OPERATOR_WRONG_TYPE, // Logical operator input is not of type OperatorExpression. DATASET_FILTER_OPERATOR_NO_ATTRIBUTE,// Comparison operator must have an attribute. DATASET_FILTER_OPERATOR_NO_VALUE, // Comparison operator has no value. DATASET_FILTER_OPERATOR_INCOMPATIBLE_TYPES, // Incompatible types in comparison operator. DATASET_FILTER_OPERATOR_INPUT_COUNT, // Operator must have exactly two inputs. DATASET_FILTER_OPERATOR_MISSING_ATTRIBUTE // An attribute is missing from the feature type. } private class Logger { private int messageCount = 0; private final Job job; public Logger (final Job job) { this.job = job; } public void report (final MessageKey messageKey, final String ... parameters) { final List<String> params = new ArrayList<String> (); final Map<String, Object> context = new HashMap<String, Object> (); context.put ("featureTypeName", featureType.getName ().getLocalPart ()); context.put ("featureTypeNamespace", featureType.getName ().getNamespace ()); logger.logEvent (job, messageKey, LogLevel.ERROR, context, params.toArray (new String[params.size ()])); ++ messageCount; } public int getMessageCount () { return messageCount; } } private final FeatureType featureType; private final EventLogger<MessageKey> logger; public FilterExpressionValidator (final FeatureType featureType, final EventLogger<MessageKey> logger) { if (featureType == null) { throw new NullPointerException ("featureType cannot be null"); } if (logger == null) { throw new NullPointerException ("logger cannot be null"); } this.featureType = featureType; this.logger = logger; } public boolean isValid (final Job job, final DatasetFilter filter) { final Logger log = new Logger (job); validateDatasetFilter (filter, log); return log.getMessageCount () == 0; } private void validateDatasetFilter (final DatasetFilter filter, final Logger log) { if (filter == null) { log.report (MessageKey.DATASET_FILTER_MISSING); return; } // Filter must have a dataset: if (filter.getDataset () == null) { log.report (MessageKey.DATASET_FILTER_NO_DATASET); } // Filter must have a root expression: final FilterExpression rootExpression = filter.getRootExpression (); if (rootExpression == null) { log.report (MessageKey.DATASET_FILTER_NO_ROOT_EXPRESSION); return; } // Root expression must be of type OperatorExpression: if (rootExpression == null || !(rootExpression instanceof OperatorExpression)) { log.report (MessageKey.DATASET_FILTER_TECHNICAL_ERROR, "The root expression must be of type OperatorExpression"); return; } validateRootOperation ((OperatorExpression)rootExpression, log); } private void validateRootOperation (final OperatorExpression rootExpression, final Logger log) { validateOperatorExpression (rootExpression, log); } private void validateOperatorExpression (final OperatorExpression expression, final Logger log) { // Expression must have an operator: final OperatorType operatorType = expression.getOperatorType (); if (operatorType == null) { log.report (MessageKey.DATASET_FILTER_TECHNICAL_ERROR, "An operator expression must have an operator type."); return; } // Operator specific validation: switch (operatorType) { // Logical operators: case AND: case OR: validateLogicalOperator (expression, log); break; // Comparison operators: case EQUALS: case GREATER_THAN: case GREATER_THAN_EQUAL: case LESS_THAN: case LESS_THAN_EQUAL: case NOT_EQUALS: validateComparisonOperator (expression, log); break; // In-operator: case IN: validateInOperator (expression, log); break; // Like-operator: case LIKE: validateLikeOperator (expression, log); break; case NOT_NULL: validateNotNullOperator (expression, log); break; default: log.report (MessageKey.DATASET_FILTER_TECHNICAL_ERROR, String.format ("Unknown operator type `%s`", operatorType)); break; } } private void validateLogicalOperator (final OperatorExpression expression, final Logger log) { final List<FilterExpression> inputs = expression.getInputs (); // The "and" operator must have inputs, the "or" operator is allowed without inputs (to allow empty filters): if (inputs == null || inputs.size () == 0) { if (expression.getOperatorType () != OperatorType.OR) { log.report (MessageKey.DATASET_FILTER_LOGICAL_OPERATOR_WITHOUT_INPUTS); } return; } // Validate inputs: for (final FilterExpression input: inputs) { // Input must exist: if (input == null) { log.report (MessageKey.DATASET_FILTER_LOGICAL_OPERATOR_MISSING_INPUT); continue; } // Input must be of type OperatorExpression: if (input == null || !(input instanceof OperatorExpression)) { log.report (MessageKey.DATASET_FILTER_LOGICAL_OPERATOR_WRONG_TYPE); continue; } validateOperatorExpression ((OperatorExpression)input, log); } } private void validateComparisonOperator (final OperatorExpression expression, final Logger log) { if (!validateOperatorInputs (expression, log)) { return; } final List<FilterExpression> inputs = expression.getInputs (); final AttributeExpression attributeExpression = (AttributeExpression)inputs.get (0); final FeatureTypeAttribute attribute = findAttribute (attributeExpression.getAttributeName (), attributeExpression.getAttributeType ()); // The attribute and value must have matching types: if (!compareAttributeType (attribute, ((ValueExpression)inputs.get (1)).getValueType ())) { log.report (MessageKey.DATASET_FILTER_OPERATOR_INCOMPATIBLE_TYPES); } } private void validateInOperator (final OperatorExpression expression, final Logger log) { if (!validateOperatorInputs (expression, log)) { return; } final List<FilterExpression> inputs = expression.getInputs (); final ValueExpression valueExpression = (ValueExpression)inputs.get (1); if (valueExpression.getValueType () != ValueType.STRING) { log.report (MessageKey.DATASET_FILTER_OPERATOR_INCOMPATIBLE_TYPES); } } private void validateLikeOperator (final OperatorExpression expression, final Logger log) { if (!validateOperatorInputs (expression, log)) { return; } final List<FilterExpression> inputs = expression.getInputs (); final AttributeExpression attributeExpression = (AttributeExpression)inputs.get (0); final FeatureTypeAttribute attribute = findAttribute (attributeExpression.getAttributeName (), attributeExpression.getAttributeType ()); final ValueExpression valueExpression = (ValueExpression)inputs.get (1); // The attribute and value must have matching types: if (!compareAttributeType (attribute, ((ValueExpression)inputs.get (1)).getValueType ()) || valueExpression.getValueType() != ValueType.STRING) { log.report (MessageKey.DATASET_FILTER_OPERATOR_INCOMPATIBLE_TYPES); } } private boolean validateNotNullOperator (final OperatorExpression expression, final Logger log) { final List<FilterExpression> inputs = expression.getInputs (); // A not null operator must have one input: if (inputs == null || inputs.size () < 1 || inputs.size () > 2) { log.report (MessageKey.DATASET_FILTER_OPERATOR_INPUT_COUNT); return false; } // First input must be an instance of AttributeExpression: if (inputs.get (0) == null || !(inputs.get (0) instanceof AttributeExpression) || ((AttributeExpression)inputs.get (0)).getAttributeName () == null || ((AttributeExpression)inputs.get (0)).getAttributeType () == null) { log.report (MessageKey.DATASET_FILTER_OPERATOR_NO_ATTRIBUTE); return false; } // The attribute must exist: final AttributeExpression attributeExpression = (AttributeExpression)inputs.get (0); final FeatureTypeAttribute attribute = findAttribute (attributeExpression.getAttributeName (), attributeExpression.getAttributeType ()); if (attribute == null) { log.report (MessageKey.DATASET_FILTER_OPERATOR_MISSING_ATTRIBUTE, attributeExpression.getAttributeName ()); return false; } return true; } private boolean validateOperatorInputs (final OperatorExpression expression, final Logger log) { final List<FilterExpression> inputs = expression.getInputs (); // A comparison operator must have exactly two inputs: if (inputs == null || inputs.size () != 2) { log.report (MessageKey.DATASET_FILTER_OPERATOR_INPUT_COUNT); return false; } // First input must be an instance of AttributeExpression: boolean invalidInputs = false; if (inputs.get (0) == null || !(inputs.get (0) instanceof AttributeExpression) || ((AttributeExpression)inputs.get (0)).getAttributeName () == null || ((AttributeExpression)inputs.get (0)).getAttributeType () == null) { log.report (MessageKey.DATASET_FILTER_OPERATOR_NO_ATTRIBUTE); invalidInputs = true; } // Second input must be an instance of ValueExpression: if (inputs.get (1) == null || !(inputs.get (1) instanceof ValueExpression) || ((ValueExpression)inputs.get (1)).getStringValue () == null || ((ValueExpression)inputs.get (1)).getValueType () == null) { log.report (MessageKey.DATASET_FILTER_OPERATOR_NO_VALUE); invalidInputs = true; } if (invalidInputs) { return false; } // The attribute must exist: final AttributeExpression attributeExpression = (AttributeExpression)inputs.get (0); final FeatureTypeAttribute attribute = findAttribute (attributeExpression.getAttributeName (), attributeExpression.getAttributeType ()); if (attribute == null) { log.report (MessageKey.DATASET_FILTER_OPERATOR_MISSING_ATTRIBUTE, attributeExpression.getAttributeName ()); } return true; } private boolean compareAttributeType (final FeatureTypeAttribute attribute, final ValueType valueType) { if (attribute == null) { return false; } final AttributeType at = attribute.getType (); switch (valueType) { case DATE: return at == AttributeType.DATE; case DATE_TIME: return at == AttributeType.DATE_TIME; case DOUBLE: return at == AttributeType.DECIMAL || at == AttributeType.DOUBLE || at == AttributeType.FLOAT || at == AttributeType.GEOMETRY; case INTEGER: return at == AttributeType.INTEGER || at == AttributeType.GEOMETRY; case STRING: return at == AttributeType.STRING; case TIME: return at == AttributeType.TIME; default: return false; } } private FeatureTypeAttribute findAttribute (final String attributePath, final AttributeType attributeType) { final String attributeName = stripPath (attributePath); for (final FeatureTypeAttribute attr: featureType.getAttributes ()) { if (attr.getName ().getLocalPart ().equals (attributeName) && attr.getType ().equals (attributeType)) { return attr; } } return null; } private final String stripPath (final String attributePath) { if (attributePath == null) { return null; } final int offset = attributePath.indexOf ('/'); if (offset >= 0) { return attributePath.substring (0, offset); } return attributePath; } }