/* * 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 de.unioninvestment.eai.portal.portlet.crud.domain.model; import com.google.common.base.Strings; import com.vaadin.data.util.converter.Converter.ConversionException; import de.unioninvestment.eai.portal.portlet.crud.config.CompoundSearchConfig; import de.unioninvestment.eai.portal.portlet.crud.config.CompoundSearchDetailsConfig; import de.unioninvestment.eai.portal.portlet.crud.domain.events.CompoundQueryChangedEvent; import de.unioninvestment.eai.portal.portlet.crud.domain.events.CompoundQueryChangedEventHandler; import de.unioninvestment.eai.portal.portlet.crud.domain.exception.BusinessException; import de.unioninvestment.eai.portal.portlet.crud.domain.model.TableColumn.Searchable; import de.unioninvestment.eai.portal.portlet.crud.domain.model.filter.*; import de.unioninvestment.eai.portal.portlet.crud.domain.model.filter.Filter; import de.unioninvestment.eai.portal.portlet.crud.domain.search.AsIsAnalyzer; import de.unioninvestment.eai.portal.portlet.crud.domain.search.SearchableTablesFinder; import de.unioninvestment.eai.portal.support.vaadin.context.Context; import de.unioninvestment.eai.portal.support.vaadin.date.DateUtils; import de.unioninvestment.eai.portal.support.vaadin.date.GermanDateFormats; import de.unioninvestment.eai.portal.support.vaadin.mvp.EventRouter; import de.unioninvestment.eai.portal.support.vaadin.support.NumberFormatter; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.*; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.util.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; import static java.util.Arrays.asList; import static java.util.Collections.sort; /** * Repräsentation der Compound-Suche. Konvertiert eine Suche in Lucene-Syntax in * Vaadin-Containerfilter. * * @author cmj */ @SuppressWarnings("serial") public class CompoundSearch extends Panel { private static final Logger LOGGER = LoggerFactory .getLogger(CompoundSearch.class); SearchableTablesFinder searchableTablesFinder = new SearchableTablesFinder(); private CompoundSearchConfig config; private GermanDateFormats dateFormats = new GermanDateFormats(); protected EventRouter<CompoundQueryChangedEventHandler, CompoundQueryChangedEvent> eventRouter = new EventRouter<CompoundQueryChangedEventHandler, CompoundQueryChangedEvent>(); private TableColumns searchableColumns; public CompoundSearch(CompoundSearchConfig config) { super(notNullDetails(config)); this.config = config; } private static CompoundSearchDetailsConfig notNullDetails( CompoundSearchConfig config) { return config.getDetails() != null ? config.getDetails() : new CompoundSearchDetailsConfig(); } public String getId() { return config.getId(); } /** * @return searchable Columns sorted by name */ public TableColumns getSearchableColumns() { if (searchableColumns == null) { final HashMap<String, TableColumn> unorderedColumns = new HashMap<String, TableColumn>(); for (Table table : getTables()) { if (table.getColumns() != null) { for (TableColumn column : table.getColumns()) { if (column.getSearchable() == Searchable.DEFAULT || !unorderedColumns.containsKey(column .getName())) { unorderedColumns.put(column.getName(), column); } } } } ArrayList<TableColumn> listOfColumns = new ArrayList<TableColumn>( unorderedColumns.values()); sort(listOfColumns, new Comparator<TableColumn>() { @Override public int compare(TableColumn o1, TableColumn o2) { if (!o1.getSearchable().equals(o2.getSearchable())) { return o1.getSearchable().equals(Searchable.DEFAULT) ? -1 : 1; } else { return o1.getName().compareTo(o2.getName()); } } }); searchableColumns = new TableColumns(listOfColumns); } return searchableColumns; } Filter getFiltersForTable(Table table, Query query) { if (query instanceof BooleanQuery) { return convertBooleanQuery(table, (BooleanQuery) query); } else if (query instanceof TermQuery) { return convertTermQuery(table, (TermQuery) query); } else if (query instanceof PrefixQuery) { return convertPrefixQuery(table, (PrefixQuery) query); } else if (query instanceof WildcardQuery) { return convertWildcardQuery(table, (WildcardQuery) query); } else if (query instanceof TermRangeQuery) { return convertTermRangeQuery(table, (TermRangeQuery) query); } throw new BusinessException( "portlet.crud.error.compoundsearch.unsupportedQuerySyntax", query.toString()); } private Filter convertWildcardQuery(Table table, WildcardQuery query) { Term wildcard = query.getTerm(); String columnName = caseCorrectedFieldName(wildcard.field()); if (table.getContainer().getType(columnName) == null) { return null; } return new Wildcard(columnName, wildcard.text(), false); } private Filter convertPrefixQuery(Table table, PrefixQuery query) { Term prefix = query.getPrefix(); String columnName = caseCorrectedFieldName(prefix.field()); if (table.getContainer().getType(columnName) == null) { return null; } return new StartsWith(columnName, prefix.text(), false); } private Filter convertTermRangeQuery(Table table, TermRangeQuery termRangeQuery) { String columnName = caseCorrectedFieldName(termRangeQuery.getField()); Class<?> columnType = table.getContainer().getType(columnName); if (columnType == null) { return null; } String lowerText = termRangeQuery.getLowerTerm().utf8ToString(); String upperText = termRangeQuery.getUpperTerm().utf8ToString(); Filter lowerFilter; Filter upperFilter; if (Number.class.isAssignableFrom(columnType)) { Number lowerNumber = convertTextToNumber(table, columnName, columnType, lowerText); lowerFilter = new Greater(columnName, lowerNumber, termRangeQuery.includesLower()); Number upperNumber = convertTextToNumber(table, columnName, columnType, upperText); upperFilter = new Less(columnName, upperNumber, termRangeQuery.includesUpper()); } else if (Date.class.isAssignableFrom(columnType)) { Date lowerDate = convertTextToDate(columnName, lowerText, dateFormats, !termRangeQuery.includesLower()); lowerFilter = new Greater(columnName, DateUtils.adjustDateType( lowerDate, columnType), true); Date upperDate = convertTextToDate(columnName, upperText, dateFormats, termRangeQuery.includesUpper()); upperFilter = new Less(columnName, DateUtils.adjustDateType( upperDate, columnType), false); } else { /* String */ lowerFilter = new Greater(columnName, lowerText, termRangeQuery.includesLower()); upperFilter = new Less(columnName, upperText, termRangeQuery.includesUpper()); } return new All(asList(lowerFilter, upperFilter)); } private Date convertTextToDate(String columnName, String text, GermanDateFormats formats, boolean returnEndDate) { try { String datePattern = formats.find(text); Date date = new SimpleDateFormat(datePattern, Locale.GERMANY) .parse(text); if (returnEndDate) { int resolution = DateUtils.getResolution(datePattern); date = DateUtils.getEndDate(date, resolution); } return date; } catch (ParseException e) { throw new BusinessException( "portlet.crud.error.compoundsearch.dateConversionFailed", columnName, text, e.getMessage()); } } private Filter convertTermQuery(Table table, TermQuery query) { Term term = query.getTerm(); String columnName = caseCorrectedFieldName(term.field()); Class<?> columnType = table.getContainer().getType(columnName); if (columnType == null) { return null; } String text = term.text(); boolean selection = getSearchableColumns().isSelection(columnName); if (selection) { text = getFieldOptionKey(columnName, text); if (text == null) { throw new BusinessException( "portlet.crud.error.compoundsearch.invalidSelection", columnName, term.text()); } } if (Number.class.isAssignableFrom(columnType)) { Number numberValue = convertTextToNumber(table, columnName, columnType, text); return new Equal(columnName, numberValue); } else if (Date.class.isAssignableFrom(columnType)) { Date lowerDate = convertTextToDate(columnName, text, dateFormats, false); Date upperDate = convertTextToDate(columnName, text, dateFormats, true); Filter lowerFilter = new Greater(columnName, DateUtils.adjustDateType(lowerDate, columnType), true); Filter upperFilter = new Less(columnName, DateUtils.adjustDateType( upperDate, columnType), false); return new All(asList(lowerFilter, upperFilter)); } else { if (selection) { return new Equal(columnName, text); } if (text.equals("*")) { return new Not(asList((Filter) new IsNull(columnName))); } else if (hasWildcards(text)) { return new Wildcard(columnName, text, false); } else { return new StartsWith(columnName, text, false); } } } private String caseCorrectedFieldName(String fieldName) { Map<String, String> mapping = getSearchableColumns() .getLowerCaseColumnNamesMapping(); String realFieldName = mapping.get(fieldName.toLowerCase()); return realFieldName != null ? realFieldName : fieldName; } private String getFieldOptionKey(String columnName, String title) { return getSearchableColumns().getDropdownSelections(columnName).getKey( title, null); } private Number convertTextToNumber(Table table, String columnName, Class<?> columnType, String text) { try { NumberFormatter numberFormatter = new NumberFormatter( (NumberFormat) table.getContainer().getFormat(columnName)); Locale locale = Context.getLocale(); @SuppressWarnings("unchecked") Number numberValue = numberFormatter.convertToModel(text, (Class<? extends Number>) columnType, locale); return numberValue; } catch (ConversionException e) { throw new BusinessException( "portlet.crud.error.compoundsearch.numberConversionFailed", columnName, text, e.getMessage()); } } private Filter convertBooleanQuery(Table table, BooleanQuery query) { BooleanQuery booleanQuery = (BooleanQuery) query; if (isBooleanMixed(booleanQuery)) { throw new BusinessException( "portlet.crud.error.compoundsearch.mixedBooleansProhibited", booleanQuery); } else if (isBooleanAND(booleanQuery)) { List<Filter> subList = convertBooleanClauses(table, booleanQuery, false); if (subList.size() == 0) { return null; } else if (subList.size() == 1) { return subList.get(0); } else { return new All(subList); } } else { List<Filter> subList = convertBooleanClauses(table, booleanQuery, true); if (subList.size() == 0) { return null; } else if (subList.size() == 1) { return subList.get(0); } else { return new Any(subList); } } } private boolean isBooleanMixed(BooleanQuery booleanQuery) { boolean hasMustClause = false; boolean hasShouldClause = false; // boolean hasMustNotClause = false; for (BooleanClause clause : booleanQuery.getClauses()) { if (clause.getOccur() == Occur.MUST) { hasMustClause = true; } else if (clause.getOccur() == Occur.MUST_NOT) { // hasMustNotClause = true; } else if (clause.getOccur() == Occur.SHOULD) { hasShouldClause = true; } else { throw new UnsupportedOperationException("Unknown:" + clause.getOccur()); } } return hasMustClause && hasShouldClause; } private boolean isMustClause(BooleanClause clause) { boolean isMustClause = (clause.getOccur() == Occur.MUST || clause .getOccur() == Occur.MUST_NOT); return isMustClause; } private boolean isBooleanAND(BooleanQuery booleanQuery) { for (BooleanClause clause : booleanQuery.getClauses()) { if (!isMustClause(clause)) { return false; } } return true; } private boolean hasWildcards(String text) { return text.contains("*") || text.contains("?"); } private List<Filter> convertBooleanClauses(Table table, BooleanQuery booleanQuery, boolean ignorePartialErrors) { List<Filter> subList = new ArrayList<Filter>( booleanQuery.getClauses().length); List<RuntimeException> errors = new LinkedList<RuntimeException>(); for (BooleanClause clause : booleanQuery.clauses()) { try { Filter subFilter = getFiltersForTable(table, clause.getQuery()); if (subFilter != null) { if (clause.isProhibited()) { subFilter = new Not(asList(subFilter)); } subList.add(subFilter); } } catch (RuntimeException e) { errors.add(e); } } if (ignorePartialErrors) { if (errors.size() < booleanQuery.clauses().size()) { for (RuntimeException e : errors) { LOGGER.info("Ignoring error in 'OR' clause: " + e.getMessage()); } return subList; } } if (errors.size() > 0) { throw errors.get(0); } return subList; } /** * @return all tables that have column definitions */ private List<Table> getTables() { List<Table> searchableTables = searchableTablesFinder .findSearchableTables(this, config.getTables()); removeTablesWithoutColumnDefinition(searchableTables); return searchableTables; } private void removeTablesWithoutColumnDefinition( List<Table> searchableTables) { for (Iterator<Table> it = searchableTables.iterator(); it.hasNext();) { if (it.next().getColumns() == null) { it.remove(); } } } /** * Search according to the query string on all matching tables. * * @param queryString */ public void search(String queryString) { Map<Table, Filter> filtersMap = prepareQuery(queryString); applyFiltersToTables(filtersMap); fireQueryChangedEvent(queryString); } private void applyFiltersToTables(Map<Table, Filter> filtersMap) { for (Entry<Table, Filter> entry : filtersMap.entrySet()) { List<Filter> filters = entry.getValue() != null ? asList(entry .getValue()) : Collections.<Filter> emptyList(); entry.getKey().getContainer().replaceFilters(filters, false, true); } } public void addQueryChangedEventHandler( CompoundQueryChangedEventHandler handler) { eventRouter.addHandler(handler); } public void removeQueryChangedEventHandler( CompoundQueryChangedEventHandler handler) { eventRouter.removeHandler(handler); } void fireQueryChangedEvent(String queryString) { eventRouter.fireEvent(new CompoundQueryChangedEvent(this, queryString)); } public boolean isValidQuery(String queryString) { try { prepareQuery(queryString); return true; } catch (Exception e) { return false; } } private Map<Table, Filter> prepareQuery(String queryString) { Query query = parseQuery(queryString); return createTableFiltersMap(query); } private Map<Table, Filter> createTableFiltersMap(Query query) { Map<Table, Filter> results = new HashMap<Table, Filter>(); for (Table table : getTables()) { if (query == null) { results.put(table, null); } else { Filter filters = getFiltersForTable(table, query); results.put(table, filters); } } return results; } private Query parseQuery(String queryString) { if (Strings.isNullOrEmpty(queryString)) { return null; } Collection<String> defaultFields = getSearchableColumns() .getDefaultSearchablePrefixes().values(); String[] defaultFieldsArray = defaultFields .toArray(new String[defaultFields.size()]); QueryParser luceneParser = new MultiFieldQueryParser(Version.LUCENE_46, defaultFieldsArray, new AsIsAnalyzer()); try { return luceneParser.parse(queryString); } catch (org.apache.lucene.queryparser.classic.ParseException e) { throw new BusinessException( "portlet.crud.error.compoundsearch.invalidQuery", queryString); } } public boolean isDefault(String name) { TableColumn column = getSearchableColumns().get(name); return column != null && column.getSearchable() == Searchable.DEFAULT; } }