package io.twasyl.jstackfx.search; import io.twasyl.jstackfx.search.exceptions.ConversionException; import io.twasyl.jstackfx.search.exceptions.UnparsableQueryException; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.StringJoiner; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Class defining a query that can be made using the search bar. * * @author Thierry Wasylczenko * @since JStackFX 1.1 */ public class Query<T> { private static Logger LOGGER = Logger.getLogger(Query.class.getName()); protected static Pattern EXPRESSION_PATTERN; protected static String FIELD_NAME_GROUP = "fieldName"; protected static String FIELD_VALUE_GROUP = "fieldValue"; protected static String COMPARATOR_GROUP = "comparator"; protected static String OPERAND_GROUP = "operand"; protected static Map<String, Class> FIELD_TYPE_CACHE = new HashMap<>(); static { final String comparators = Arrays.stream(Comparator.values()) .map(Comparator::getRegexExpression) .collect(Collectors.joining("|", "(?<" + COMPARATOR_GROUP + ">", ")")); final String operands = Arrays.stream(Operand.values()) .map(Operand::getRegexExpression) .collect(Collectors.joining("|", "(?<" + OPERAND_GROUP + ">", ")")); final StringBuilder expression = new StringBuilder("(?<").append(FIELD_NAME_GROUP).append(">[^\\s]+)") .append("\\s*").append(comparators) .append("\\s*(?<").append(FIELD_VALUE_GROUP).append(">[^\\s]+)") .append("\\s*").append(operands).append("?"); EXPRESSION_PATTERN = Pattern.compile(expression.toString()); } protected Class<T> clazz; protected FieldExpressionQueue<T> expressions; protected String rawQuery; private Query(final Class<T> clazz) { this.clazz = clazz; this.expressions = new FieldExpressionQueue<>(); } public static <T> Query<T> create(final Class<T> clazz) { if (clazz == null) throw new NullPointerException("The class can not be null"); final Query<T> query = new Query(clazz); return query; } public String getRawQuery() { return rawQuery; } public boolean parse(final String query) throws UnparsableQueryException { this.rawQuery = query; final Matcher matcher = EXPRESSION_PATTERN.matcher(query); this.expressions.clear(); boolean parsingSuccessful = true; while (matcher.find() && parsingSuccessful) { final String field = matcher.group(FIELD_NAME_GROUP); final Comparator comparator = Comparator.fromRegex(matcher.group(COMPARATOR_GROUP)); final Comparable value; try { value = convertFieldValue(field, matcher.group(FIELD_VALUE_GROUP)); this.expressions.put(FieldExpression.Builder.create(this.clazz) .onField(field) .using(comparator) .withWalue(value) .build()); final String operandString = matcher.group(OPERAND_GROUP); if (operandString != null) { final Operand operand = Operand.fromRegex(operandString); if (operand == Operand.AND) { this.expressions.and(); } else if (operand == Operand.OR) { this.expressions.or(); } } } catch (ConversionException e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.WARNING, "Error parsing the query", e); } parsingSuccessful = false; } } return parsingSuccessful; } public boolean match(final T object) { return this.expressions.match(object); } protected Comparable convertFieldValue(final String fieldName, final String fieldValue) throws ConversionException { if (!FIELD_TYPE_CACHE.containsKey(fieldName)) { FIELD_TYPE_CACHE.put(fieldName, determineFieldClass(fieldName)); } final Class aClass = FIELD_TYPE_CACHE.get(fieldName); try { if (aClass.isEnum()) { return Enum.valueOf(aClass, fieldValue.toUpperCase()); } else if (aClass.equals(byte.class) || aClass.equals(Byte.class)) { return Byte.parseByte(fieldValue); } else if (aClass.equals(int.class) || aClass.equals(Integer.class)) { return Integer.parseInt(fieldValue); } else if (aClass.equals(long.class) || aClass.equals(Long.class)) { return Long.parseLong(fieldValue); } else if (aClass.equals(short.class) || aClass.equals(Short.class)) { return Short.parseShort(fieldValue); } else if (aClass.equals(float.class) || aClass.equals(Float.class)) { return Float.parseFloat(fieldValue); } else if (aClass.equals(double.class) || aClass.equals(Double.class)) { return Double.parseDouble(fieldValue); } else if (aClass.equals(boolean.class) || aClass.equals(Boolean.class)) { return Boolean.parseBoolean(fieldValue); } else if (aClass.equals(char.class) || aClass.equals(Character.class)) { if (fieldValue.length() == 1) { return fieldValue.charAt(0); } else { throw new ConversionException("Field value [" + fieldValue + "] is not a character"); } } else if (aClass.equals(String.class)) { return fieldValue; } else { throw new ConversionException("Type [" + aClass.getName() + "] is not supported in search"); } } catch (ConversionException e) { throw e; } catch (Exception e) { throw new ConversionException("Can not convert value [" + fieldValue + "] to class [" + aClass.getName() + "]", e); } } protected Class determineFieldClass(final String fieldName) { try { final BeanInfo beanInfo = Introspector.getBeanInfo(this.clazz); final PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); final PropertyDescriptor fieldDescriptor = Arrays.stream(descriptors).filter(descriptor -> descriptor.getName().equals(fieldName)).findFirst().orElse(null); return fieldDescriptor.getReadMethod().getReturnType(); } catch (IntrospectionException e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.WARNING, "Error determining the field [" + fieldName + "] class", e); } return null; } } }