All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cxf.jaxrs.ext.search.fiql.FiqlParser Maven / Gradle / Ivy

There is a newer version: 3.0.0-milestone2
Show newest version
/**
 * 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.cxf.jaxrs.ext.search.fiql;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;

import org.apache.cxf.jaxrs.ext.search.AndSearchCondition;
import org.apache.cxf.jaxrs.ext.search.Beanspector;
import org.apache.cxf.jaxrs.ext.search.Beanspector.TypeInfo;
import org.apache.cxf.jaxrs.ext.search.ConditionType;
import org.apache.cxf.jaxrs.ext.search.OrSearchCondition;
import org.apache.cxf.jaxrs.ext.search.PropertyNotFoundException;
import org.apache.cxf.jaxrs.ext.search.SearchBean;
import org.apache.cxf.jaxrs.ext.search.SearchCondition;
import org.apache.cxf.jaxrs.ext.search.SearchConditionParser;
import org.apache.cxf.jaxrs.ext.search.SearchParseException;
import org.apache.cxf.jaxrs.ext.search.SearchUtils;
import org.apache.cxf.jaxrs.ext.search.SimpleSearchCondition;
import org.apache.cxf.jaxrs.ext.search.collections.CollectionCheck;
import org.apache.cxf.jaxrs.ext.search.collections.CollectionCheckInfo;
import org.apache.cxf.jaxrs.utils.InjectionUtils;
import org.apache.cxf.message.MessageUtils;


/**
 * Parses FIQL expression to
 * construct {@link SearchCondition} structure. Since this class operates on Java type T, not on XML
 * structures "selectors" part of specification is not applicable; instead selectors describes getters of type
 * T used as search condition type (see {@link SimpleSearchCondition#isMet(Object)} for details.
 * 
 * @param  type of search condition.
 */
public class FiqlParser implements SearchConditionParser {

    public static final String OR = ",";
    public static final String AND = ";";

    public static final String GT = "=gt=";
    public static final String GE = "=ge=";
    public static final String LT = "=lt=";
    public static final String LE = "=le=";
    public static final String EQ = "==";
    public static final String NEQ = "!=";
    
    public static final Map CONDITION_MAP;
        
    public static final String EXTENSION_COUNT = "count";
    
    private static final String EXTENSION_COUNT_OPEN = EXTENSION_COUNT + "(";
    
    private static final Map OPERATORS_MAP;
    private static final Pattern COMPARATORS_PATTERN; 
    
    static {
        // operatorsMap
        OPERATORS_MAP = new HashMap();
        OPERATORS_MAP.put(GT, ConditionType.GREATER_THAN);
        OPERATORS_MAP.put(GE, ConditionType.GREATER_OR_EQUALS);
        OPERATORS_MAP.put(LT, ConditionType.LESS_THAN);
        OPERATORS_MAP.put(LE, ConditionType.LESS_OR_EQUALS);
        OPERATORS_MAP.put(EQ, ConditionType.EQUALS);
        OPERATORS_MAP.put(NEQ, ConditionType.NOT_EQUALS);
        
        CONDITION_MAP = new HashMap();
        CONDITION_MAP.put(ConditionType.GREATER_THAN, GT);
        CONDITION_MAP.put(ConditionType.GREATER_OR_EQUALS, GE);
        CONDITION_MAP.put(ConditionType.LESS_THAN, LT);
        CONDITION_MAP.put(ConditionType.LESS_OR_EQUALS, LE);
        CONDITION_MAP.put(ConditionType.EQUALS, EQ);
        CONDITION_MAP.put(ConditionType.NOT_EQUALS, NEQ);
        
        
        // pattern
        String comparators = GT + "|" + GE + "|" + LT + "|" + LE + "|" + EQ + "|" + NEQ;
        String s1 = "[\\p{ASCII}]+(" + comparators + ")";
        COMPARATORS_PATTERN = Pattern.compile(s1);
    }

    private Beanspector beanspector;
    private Class conditionClass;
    private Map contextProperties;
    private Map beanPropertiesMap;
    /**
     * Creates FIQL parser.
     * 
     * @param tclass - class of T used to create condition objects in built syntax tree. Class T must have
     *            accessible no-arg constructor and complementary setters to these used in FIQL expressions.
     */
    public FiqlParser(Class tclass) {
        this(tclass, Collections.emptyMap());
    }
    
    /**
     * Creates FIQL parser.
     * 
     * @param tclass - class of T used to create condition objects in built syntax tree. Class T must have
     *            accessible no-arg constructor and complementary setters to these used in FIQL expressions.
     * @param contextProperties            
     */
    public FiqlParser(Class tclass, Map contextProperties) {
        this(tclass, contextProperties, null);
    }
    
    /**
     * Creates FIQL parser.
     * 
     * @param tclass - class of T used to create condition objects in built syntax tree. Class T must have
     *            accessible no-arg constructor and complementary setters to these used in FIQL expressions.
     * @param contextProperties            
     */
    public FiqlParser(Class tclass, 
                      Map contextProperties,
                      Map beanProperties) {
        beanspector = SearchBean.class.isAssignableFrom(tclass) 
            ? null : new Beanspector(tclass);
        conditionClass = tclass;
        this.contextProperties = contextProperties == null 
            ? Collections.emptyMap() : contextProperties;
        this.beanPropertiesMap = beanProperties;
    }
    
    /**
     * Parses expression and builds search filter. Names used in FIQL expression are names of getters/setters
     * in type T.
     * 

* Example: * *

     * class Condition {
     *   public String getFoo() {...}
     *   public void setFoo(String foo) {...}
     *   public int getBar() {...}
     *   public void setBar(int bar) {...}
     * }
     * 
     * FiqlParser<Condition> parser = new FiqlParser<Condition>(Condition.class);
     * parser.parse("foo==mystery*;bar=ge=10");
     * 
* * @param fiqlExpression expression of filter. * @return tree of {@link SearchCondition} objects representing runtime search structure. * @throws SearchParseException when expression does not follow FIQL grammar */ public SearchCondition parse(String fiqlExpression) throws SearchParseException { ASTNode ast = parseAndsOrsBrackets(fiqlExpression); // System.out.println(ast); return ast.build(); } private ASTNode parseAndsOrsBrackets(String expr) throws SearchParseException { List subexpressions = new ArrayList(); List operators = new ArrayList(); int level = 0; int lastIdx = 0; int idx = 0; for (idx = 0; idx < expr.length(); idx++) { char c = expr.charAt(idx); if (c == '(') { level++; } else if (c == ')') { level--; if (level < 0) { throw new SearchParseException(String.format("Unexpected closing bracket at position %d", idx)); } } String cs = Character.toString(c); boolean isOperator = AND.equals(cs) || OR.equals(cs); if (level == 0 && isOperator) { String s1 = expr.substring(lastIdx, idx); String s2 = expr.substring(idx, idx + 1); subexpressions.add(s1); operators.add(s2); lastIdx = idx + 1; } boolean isEnd = idx == expr.length() - 1; if (isEnd) { String s1 = expr.substring(lastIdx, idx + 1); subexpressions.add(s1); operators.add(null); lastIdx = idx + 1; } } if (level != 0) { throw new SearchParseException(String .format("Unmatched opening and closing brackets in expression: %s", expr)); } if (operators.get(operators.size() - 1) != null) { String op = operators.get(operators.size() - 1); String ex = subexpressions.get(subexpressions.size() - 1); throw new SearchParseException("Dangling operator at the end of expression: ..." + ex + op); } // looking for adjacent ANDs then group them into ORs // Note: in case not ANDs is found (e.g only ORs) every single subexpression is // treated as "single item group of ANDs" int from = 0; int to = 0; SubExpression ors = new SubExpression(OR); while (to < operators.size()) { while (to < operators.size() && AND.equals(operators.get(to))) { to++; } SubExpression ands = new SubExpression(AND); for (; from <= to; from++) { String subex = subexpressions.get(from); ASTNode node = null; if (subex.startsWith("(")) { node = parseAndsOrsBrackets(subex.substring(1, subex.length() - 1)); } else { node = parseComparison(subex); } if (node != null) { ands.add(node); } } to = from; if (ands.getSubnodes().size() == 1) { ors.add(ands.getSubnodes().get(0)); } else { ors.add(ands); } } if (ors.getSubnodes().size() == 1) { return ors.getSubnodes().get(0); } else { return ors; } } private Comparison parseComparison(String expr) throws SearchParseException { Matcher m = COMPARATORS_PATTERN.matcher(expr); if (m.find()) { String propertyName = expr.substring(0, m.start(1)); String operator = m.group(1); String value = expr.substring(m.end(1)); if ("".equals(value)) { throw new SearchParseException("Not a comparison expression: " + expr); } String name = unwrapSetter(propertyName); String beanPropertyName = beanPropertiesMap == null ? null : beanPropertiesMap.get(name); if (beanPropertyName != null) { name = beanPropertyName; } TypeInfoObject castedValue = parseType(propertyName, name, value); if (castedValue != null) { return new Comparison(name, operator, castedValue); } else if (MessageUtils.isTrue(contextProperties.get(SearchUtils.LAX_PROPERTY_MATCH))) { return null; } else { throw new PropertyNotFoundException(name, value); } } else { throw new SearchParseException("Not a comparison expression: " + expr); } } private TypeInfoObject parseType(String originalName, String setter, String value) throws SearchParseException { String name = getSetter(setter); try { TypeInfo typeInfo = beanspector != null ? beanspector.getAccessorTypeInfo(name) : new TypeInfo(String.class, String.class); Object object = parseType(originalName, null, null, setter, typeInfo, value); return new TypeInfoObject(object, typeInfo); } catch (Exception e) { return null; } } private Object parseType(String originalPropName, Object ownerBean, Object lastCastedValue, String setter, TypeInfo typeInfo, String value) throws SearchParseException { Class valueType = typeInfo.getTypeClass(); boolean isCollection = InjectionUtils.isSupportedCollectionOrArray(valueType); Class actualType = isCollection ? InjectionUtils.getActualType(typeInfo.getGenericType()) : valueType; int index = setter.indexOf("."); if (index == -1) { Object castedValue = value; if (Date.class.isAssignableFrom(valueType)) { castedValue = convertToDate(value); } else { boolean isPrimitive = InjectionUtils.isPrimitive(valueType); boolean isPrimitiveOrEnum = isPrimitive || valueType.isEnum(); if (ownerBean == null || isPrimitiveOrEnum) { try { CollectionCheck collCheck = getCollectionCheck(originalPropName, isCollection, actualType); if (collCheck == null) { castedValue = InjectionUtils.convertStringToPrimitive(value, actualType); } if (collCheck == null && isCollection) { castedValue = getCollectionSingleton(valueType, castedValue); } else if (isCollection) { typeInfo.setCollectionCheckInfo(new CollectionCheckInfo(collCheck, castedValue)); castedValue = getEmptyCollection(valueType); } } catch (Exception e) { throw new SearchParseException("Cannot convert String value \"" + value + "\" to a value of class " + valueType.getName(), e); } } else { Class classType = isCollection ? valueType : value.getClass(); try { Method setterM = valueType.getMethod("set" + getMethodNameSuffix(setter), new Class[]{classType}); Object objectValue = !isCollection ? value : getCollectionSingleton(valueType, value); setterM.invoke(ownerBean, new Object[]{objectValue}); castedValue = objectValue; } catch (Throwable ex) { throw new SearchParseException("Cannot convert String value \"" + value + "\" to a value of class " + valueType.getName(), ex); } } } if (lastCastedValue != null) { castedValue = lastCastedValue; } return castedValue; } else { String[] names = setter.split("\\."); try { String nextPart = getMethodNameSuffix(names[1]); Method getterM = actualType.getMethod("get" + nextPart, new Class[]{}); Class returnType = getterM.getReturnType(); boolean returnCollection = InjectionUtils.isSupportedCollectionOrArray(returnType); Class actualReturnType = !returnCollection ? returnType : InjectionUtils.getActualType(getterM.getGenericReturnType()); boolean isPrimitive = InjectionUtils.isPrimitive(returnType) || returnType.isEnum(); boolean lastTry = names.length == 2 && (isPrimitive || returnType == Date.class || returnCollection); Object valueObject = lastTry && ownerBean != null ? ownerBean : actualType.newInstance(); Object nextObject; if (lastTry) { if (!returnCollection) { nextObject = isPrimitive ? InjectionUtils.convertStringToPrimitive(value, returnType) : convertToDate(value); } else { CollectionCheck collCheck = getCollectionCheck(originalPropName, true, actualReturnType); if (collCheck == null) { nextObject = getCollectionSingleton(valueType, value); } else { typeInfo.setCollectionCheckInfo(new CollectionCheckInfo(collCheck, value)); nextObject = getEmptyCollection(valueType); } } } else { nextObject = returnType.newInstance(); } Method setterM = actualType.getMethod("set" + nextPart, new Class[]{returnType}); setterM.invoke(valueObject, new Object[]{nextObject}); lastCastedValue = lastCastedValue == null ? valueObject : lastCastedValue; if (lastTry) { return isCollection ? getCollectionSingleton(valueType, lastCastedValue) : lastCastedValue; } TypeInfo nextTypeInfo = new TypeInfo(nextObject.getClass(), getterM.getGenericReturnType()); return parseType(originalPropName, nextObject, lastCastedValue, setter.substring(index + 1), nextTypeInfo, value); } catch (Throwable e) { throw new SearchParseException("Cannot convert String value \"" + value + "\" to a value of class " + valueType.getName(), e); } } } private CollectionCheck getCollectionCheck(String propName, boolean isCollection, Class actualCls) { if (isCollection) { if (InjectionUtils.isPrimitive(actualCls)) { if (propName.startsWith(EXTENSION_COUNT_OPEN)) { return CollectionCheck.SIZE; } } else { return CollectionCheck.SIZE; } } return null; } private Object getCollectionSingleton(Class collectionCls, Object value) { if (Set.class.isAssignableFrom(collectionCls)) { return Collections.singleton(value); } else { return Collections.singletonList(value); } } private Object getEmptyCollection(Class collectionCls) { if (Set.class.isAssignableFrom(collectionCls)) { return Collections.emptySet(); } else { return Collections.emptyList(); } } private Object convertToDate(String value) throws SearchParseException { try { DateFormat df = SearchUtils.getDateFormat(contextProperties); String dateValue = value; if (SearchUtils.isTimeZoneSupported(contextProperties, Boolean.FALSE)) { // zone in XML is "+01:00" in Java is "+0100"; stripping semicolon int idx = value.lastIndexOf(':'); if (idx != -1) { dateValue = value.substring(0, idx) + value.substring(idx + 1); } } return df.parse(dateValue); } catch (ParseException e) { // is that duration? try { Date now = new Date(); DatatypeFactory.newInstance().newDuration(value).addTo(now); return now; } catch (DatatypeConfigurationException e1) { throw new SearchParseException(e1); } catch (IllegalArgumentException e1) { throw new SearchParseException("Can parse " + value + " neither as date nor duration", e); } } } private String getSetter(String setter) { int index = setter.indexOf("."); if (index != -1) { return setter.substring(0, index).toLowerCase(); } else { return setter; } } private String unwrapSetter(String setter) { if (setter.startsWith(EXTENSION_COUNT_OPEN) && setter.endsWith(")")) { return setter.substring(EXTENSION_COUNT_OPEN.length(), setter.length() - 1); } else { return setter; } } private String getMethodNameSuffix(String name) { if (name.length() == 1) { return name.toUpperCase(); } else { return Character.toUpperCase(name.charAt(0)) + name.substring(1); } } // node of abstract syntax tree private interface ASTNode { SearchCondition build() throws SearchParseException; } private class SubExpression implements ASTNode { private String operator; private List> subnodes = new ArrayList>(); public SubExpression(String operator) { this.operator = operator; } public void add(ASTNode node) { subnodes.add(node); } public List> getSubnodes() { return Collections.unmodifiableList(subnodes); } @Override public String toString() { String s = operator.equals(AND) ? "AND" : "OR"; s += ":["; for (int i = 0; i < subnodes.size(); i++) { s += subnodes.get(i); if (i < subnodes.size() - 1) { s += ", "; } } s += "]"; return s; } public SearchCondition build() throws SearchParseException { List> scNodes = new ArrayList>(); for (ASTNode node : subnodes) { scNodes.add(node.build()); } if (OR.equals(operator)) { return new OrSearchCondition(scNodes); } else { return new AndSearchCondition(scNodes); } } } private class Comparison implements ASTNode { private String name; private String operator; private TypeInfoObject tvalue; public Comparison(String name, String operator, TypeInfoObject value) { this.name = name; this.operator = operator; this.tvalue = value; } @Override public String toString() { return name + " " + operator + " " + tvalue.getObject() + " (" + tvalue.getObject().getClass().getSimpleName() + ")"; } public SearchCondition build() throws SearchParseException { String templateName = getSetter(name); T cond = createTemplate(templateName); ConditionType ct = OPERATORS_MAP.get(operator); if (isPrimitive(cond)) { return new SimpleSearchCondition(ct, cond); } else { return new SimpleSearchCondition(Collections.singletonMap(templateName, ct), Collections.singletonMap(templateName, name), Collections.singletonMap(templateName, tvalue.getTypeInfo()), cond); } } private boolean isPrimitive(T pojo) { return pojo.getClass().getName().startsWith("java.lang"); } @SuppressWarnings("unchecked") private T createTemplate(String setter) throws SearchParseException { try { if (beanspector != null) { beanspector.instantiate().setValue(setter, tvalue.getObject()); return beanspector.getBean(); } else { SearchBean bean = (SearchBean)conditionClass.newInstance(); bean.set(setter, tvalue.getObject().toString()); return (T)bean; } } catch (Throwable e) { throw new SearchParseException(e); } } } static class TypeInfoObject { private Object object; private TypeInfo typeInfo; public TypeInfoObject(Object object, TypeInfo typeInfo) { this.object = object; this.typeInfo = typeInfo; } public TypeInfo getTypeInfo() { return typeInfo; } public Object getObject() { return object; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy