--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2014, 2015 Association for Decentralized Information Management\r
+ * in Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ * Semantum Oy - initial API and implementation\r
+ * Semantum Oy - improvements for Simantics issue #6053\r
+ *******************************************************************************/\r
+package org.simantics.db.indexing;\r
+\r
+import java.io.Reader;\r
+import java.util.HashMap;\r
+import java.util.Map;\r
+import java.util.concurrent.atomic.AtomicReference;\r
+\r
+import org.apache.lucene.analysis.Analyzer;\r
+import org.apache.lucene.analysis.Tokenizer;\r
+import org.apache.lucene.analysis.core.KeywordAnalyzer;\r
+import org.apache.lucene.analysis.core.WhitespaceAnalyzer;\r
+import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;\r
+import org.apache.lucene.analysis.util.CharTokenizer;\r
+import org.apache.lucene.queryparser.classic.ParseException;\r
+import org.apache.lucene.queryparser.classic.QueryParser;\r
+import org.apache.lucene.search.NumericRangeQuery;\r
+import org.apache.lucene.search.Query;\r
+import org.apache.lucene.util.AttributeFactory;\r
+import org.apache.lucene.util.Version;\r
+import org.simantics.databoard.util.ObjectUtils;\r
+import org.simantics.utils.datastructures.Pair;\r
+\r
+public class Queries {\r
+\r
+ static final class LowerCaseWhitespaceTokenizer extends CharTokenizer {\r
+ /**\r
+ * Construct a new WhitespaceTokenizer. * @param matchVersion Lucene version\r
+ * to match See {@link <a href="#version">above</a>}\r
+ * \r
+ * @param in\r
+ * the input to split up into tokens\r
+ */\r
+ public LowerCaseWhitespaceTokenizer(Version matchVersion, Reader in) {\r
+ super(matchVersion, in);\r
+ }\r
+\r
+ /**\r
+ * Construct a new WhitespaceTokenizer using a given\r
+ * {@link org.apache.lucene.util.AttributeSource.AttributeFactory}.\r
+ *\r
+ * @param\r
+ * matchVersion Lucene version to match See\r
+ * {@link <a href="#version">above</a>}\r
+ * @param factory\r
+ * the attribute factory to use for this {@link Tokenizer}\r
+ * @param in\r
+ * the input to split up into tokens\r
+ */\r
+ public LowerCaseWhitespaceTokenizer(Version matchVersion, AttributeFactory factory, Reader in) {\r
+ super(matchVersion, factory, in);\r
+ }\r
+\r
+ @Override\r
+ protected int normalize(int c) {\r
+ return Character.toLowerCase(c);\r
+ }\r
+\r
+ protected boolean isTokenChar(int c) {\r
+ return !Character.isWhitespace(c);\r
+ }\r
+ }\r
+\r
+ static final class LowerCaseWhitespaceAnalyzer extends Analyzer {\r
+\r
+ private final Version matchVersion;\r
+\r
+ /**\r
+ * Creates a new {@link WhitespaceAnalyzer}\r
+ * @param matchVersion Lucene version to match See {@link <a href="#version">above</a>}\r
+ */\r
+ public LowerCaseWhitespaceAnalyzer(Version matchVersion) {\r
+ this.matchVersion = matchVersion;\r
+ }\r
+\r
+ @Override\r
+ protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {\r
+ return new TokenStreamComponents(new LowerCaseWhitespaceTokenizer(matchVersion, reader));\r
+ }\r
+ }\r
+\r
+ private static AtomicReference<Pair<Query, String>> queryCache = new AtomicReference<>();\r
+\r
+ final static PerFieldAnalyzerWrapper analyzer = createAnalyzer();\r
+ \r
+ static PerFieldAnalyzerWrapper createAnalyzer() {\r
+ \r
+ Map<String,Analyzer> analyzerPerField = new HashMap<>();\r
+ analyzerPerField.put("Model", new KeywordAnalyzer());\r
+ analyzerPerField.put("Parent", new KeywordAnalyzer());\r
+ analyzerPerField.put("Resource", new KeywordAnalyzer());\r
+ analyzerPerField.put("GUID", new KeywordAnalyzer());\r
+ \r
+ PerFieldAnalyzerWrapper analyzer = new PerFieldAnalyzerWrapper(new LowerCaseWhitespaceAnalyzer(Version.LUCENE_4_9), analyzerPerField);\r
+ return analyzer;\r
+ \r
+ }\r
+\r
+ static PerFieldAnalyzerWrapper getAnalyzer() {\r
+ return analyzer;\r
+ }\r
+\r
+ static Query parse(String search, IndexSchema schema) throws ParseException {\r
+ Pair<Query, String> cachedQuery = queryCache.get();\r
+ if (cachedQuery != null && search.equals(cachedQuery.second))\r
+ return cachedQuery.first;\r
+\r
+ //System.err.println("parse " + search + " (cached=" + (cachedQuery != null ? cachedQuery.second : "null") + ")" );\r
+ CustomQueryParser parser = new CustomQueryParser(Version.LUCENE_4_9, "Name", getAnalyzer(), schema);\r
+ Query query = parser.parse(search);\r
+\r
+ queryCache.set(Pair.make(query, search));\r
+ return query;\r
+ }\r
+\r
+\r
+ public static class CustomQueryParser extends QueryParser {\r
+\r
+ protected final IndexSchema schema;\r
+\r
+ public CustomQueryParser(Version version, String field, Analyzer analyzer, IndexSchema schema) {\r
+ super(version, field, analyzer);\r
+ this.schema = schema;\r
+ setAllowLeadingWildcard(true);\r
+ }\r
+\r
+ @Override\r
+ protected Query getRangeQuery(\r
+ String field,\r
+ String part1,\r
+ String part2,\r
+ boolean startInclusive,\r
+ boolean endInclusive) throws ParseException\r
+ {\r
+ IndexSchema.Type type = schema.typeMap.get(field);\r
+ if (IndexSchema.NUMERIC_TYPES.contains(type)) {\r
+ boolean equalParts = ObjectUtils.objectEquals(part1, part2);\r
+ try {\r
+ switch (type) {\r
+ case INT: {\r
+ Integer min = part1 != null ? ( Integer.valueOf(part1)) : null;\r
+ Integer max = part2 != null ? (equalParts ? min : Integer.valueOf(part2)) : null;\r
+ return NumericRangeQuery.newIntRange(field, min, max, startInclusive, endInclusive);\r
+ }\r
+ case LONG: {\r
+ Long min = part1 != null ? ( Long.valueOf(part1)) : null;\r
+ Long max = part2 != null ? (equalParts ? min : Long.valueOf(part2)) : null;\r
+ return NumericRangeQuery.newLongRange(field, min, max, startInclusive, endInclusive);\r
+ }\r
+ case FLOAT: {\r
+ Float min = part1 != null ? ( Float.valueOf(part1)) : null;\r
+ Float max = part2 != null ? (equalParts ? min : Float.valueOf(part2)) : null;\r
+ return NumericRangeQuery.newFloatRange(field, min, max, startInclusive, endInclusive);\r
+ }\r
+ case DOUBLE: {\r
+ Double min = part1 != null ? ( Double.valueOf(part1)) : null;\r
+ Double max = part2 != null ? (equalParts ? min : Double.valueOf(part2)) : null;\r
+ return NumericRangeQuery.newDoubleRange(field, min, max, startInclusive, endInclusive);\r
+ }\r
+ default:\r
+ throw new ParseException("Unrecognized numeric field type '" + type + "' for field '" + field + "'"); \r
+ }\r
+ } catch (NumberFormatException e) {\r
+ throw new ParseException(e.getMessage()); \r
+ }\r
+ }\r
+ return super.getRangeQuery(field, part1, part2, startInclusive, endInclusive); \r
+ } \r
+\r
+ }\r
+\r
+}\r