diff --git a/community-module/pom.xml b/community-module/pom.xml
index f22cdf4..359c4f3 100644
--- a/community-module/pom.xml
+++ b/community-module/pom.xml
@@ -17,7 +17,7 @@
5.2.0
- 23.3.0
+ 7.0.0
diff --git a/community-module/src/main/java/com/inteligr8/alfresco/asie/cache/MultiValueCache.java b/community-module/src/main/java/com/inteligr8/alfresco/asie/cache/MultiValueCache.java
new file mode 100644
index 0000000..d6d820c
--- /dev/null
+++ b/community-module/src/main/java/com/inteligr8/alfresco/asie/cache/MultiValueCache.java
@@ -0,0 +1,67 @@
+package com.inteligr8.alfresco.asie.cache;
+
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Collection;
+
+import org.alfresco.repo.cache.SimpleCache;
+
+public class MultiValueCache> implements SimpleCache {
+
+ private SimpleCache cache;
+ private Class> collectionType;
+
+ public MultiValueCache(SimpleCache cache, Class> collectionType) {
+ this.cache = cache;
+ this.collectionType = collectionType;
+ }
+
+ @SuppressWarnings("unchecked")
+ public boolean add(K key, V value) {
+ C c = this.cache.get(key);
+ if (c != null)
+ return c.add(value);
+
+ try {
+ Constructor> constructor = this.collectionType.getConstructor();
+ c = (C) constructor.newInstance();
+ this.cache.put(key, c);
+ return c.add(value);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
+ throw new UnsupportedOperationException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void clear() {
+ this.cache.clear();
+ }
+
+ @Override
+ public C get(K key) {
+ return this.cache.get(key);
+ }
+
+ @Override
+ public boolean contains(K key) {
+ C c = this.cache.get(key);
+ return c == null ? false : !c.isEmpty();
+ }
+
+ @Override
+ public Collection getKeys() {
+ return this.cache.getKeys();
+ }
+
+ @Override
+ public void put(K key, C value) {
+ this.cache.put(key, value);
+ }
+
+ @Override
+ public void remove(K key) {
+ this.cache.remove(key);
+ }
+
+}
diff --git a/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/CmisQueryInspector.java b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/CmisQueryInspector.java
new file mode 100644
index 0000000..7b0486b
--- /dev/null
+++ b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/CmisQueryInspector.java
@@ -0,0 +1,47 @@
+package com.inteligr8.alfresco.asie.compute;
+
+import java.util.Set;
+
+import org.alfresco.repo.search.impl.parsers.CMISLexer;
+import org.alfresco.repo.search.impl.parsers.CMISParser;
+import org.alfresco.service.cmr.search.SearchParameters.Operator;
+import org.alfresco.service.cmr.search.SearchService;
+import org.alfresco.service.namespace.QName;
+import org.antlr.runtime.ANTLRStringStream;
+import org.antlr.runtime.CharStream;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.RecognitionException;
+import org.antlr.runtime.tree.CommonTree;
+import org.antlr.runtime.tree.Tree;
+import org.apache.commons.collections4.SetUtils;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CmisQueryInspector implements QueryInspector {
+
+ private Set supportedLanguages = SetUtils.unmodifiableSet(
+ SearchService.LANGUAGE_CMIS_ALFRESCO,
+ SearchService.LANGUAGE_CMIS_STRICT,
+ SearchService.LANGUAGE_INDEX_CMIS,
+ SearchService.LANGUAGE_SOLR_CMIS);
+
+ @Override
+ public Set getSupportedLanguages() {
+ return this.supportedLanguages;
+ }
+
+ @Override
+ public QueryValue findRequiredProperty(String cmisQuery, Operator defaultOperator, QName property) throws RecognitionException {
+ Tree tree = this.parseCmis(cmisQuery, defaultOperator);
+ }
+
+ protected Tree parseCmis(String cmisQuery, Operator defaultOperator) throws RecognitionException {
+ CharStream cs = new ANTLRStringStream(cmisQuery);
+ CMISLexer lexer = new CMISLexer(cs);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ CMISParser parser = new CMISParser(tokens);
+ CommonTree tree = (CommonTree) parser.query().getTree();
+ return tree;
+ }
+
+}
diff --git a/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/FtsQueryInspector.java b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/FtsQueryInspector.java
new file mode 100644
index 0000000..cb972ba
--- /dev/null
+++ b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/FtsQueryInspector.java
@@ -0,0 +1,290 @@
+package com.inteligr8.alfresco.asie.compute;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.alfresco.repo.search.impl.parsers.FTSLexer;
+import org.alfresco.repo.search.impl.parsers.FTSParser;
+import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
+import org.alfresco.service.cmr.repository.AssociationRef;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.search.SearchParameters.Operator;
+import org.alfresco.service.cmr.search.SearchService;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.antlr.runtime.ANTLRStringStream;
+import org.antlr.runtime.CharStream;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.RecognitionException;
+import org.antlr.runtime.tree.CommonTree;
+import org.antlr.runtime.tree.Tree;
+import org.apache.commons.collections4.SetUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class FtsQueryInspector implements QueryInspector {
+
+ private final Logger logger = LoggerFactory.getLogger(FtsQueryInspector.class);
+
+ private final Set supportedLanguages = SetUtils.unmodifiableSet(
+ SearchService.LANGUAGE_FTS_ALFRESCO,
+ SearchService.LANGUAGE_INDEX_FTS_ALFRESCO,
+ SearchService.LANGUAGE_SOLR_FTS_ALFRESCO,
+ SearchService.LANGUAGE_LUCENE);
+
+ @Autowired
+ private NamespaceService namespaceService;
+
+ @Override
+ public Set getSupportedLanguages() {
+ return this.supportedLanguages;
+ }
+
+ @Override
+ public List findRequiredPropertyValues(String ftsQuery, Operator defaultOperator, QName property, DataTypeDefinition dataTypeDef) throws RecognitionException {
+ Tree tree = this.parseFts(ftsQuery, defaultOperator);
+ tree = this.bypassSingleTermDisjunctions(tree);
+ if (tree == null)
+ return null;
+
+ Collection trees = this.extractRequiredTerms(tree);
+ this.logger.trace("Found {} required terms in query: {}", trees.size(), ftsQuery);
+ this.filterPropertyTerms(trees, property);
+ this.logger.trace("Found {} required terms for property {} in query: {}", trees.size(), property, ftsQuery);
+ this.filterOutFuzzyTerms(trees);
+ this.logger.trace("Found {} required definitive terms for property {} in query: {}", trees.size(), property, ftsQuery);
+
+ List values = new ArrayList<>(trees.size());
+ for (Tree t : trees)
+ values.add(this.extractValue(t, dataTypeDef));
+ return values;
+ }
+
+ protected Tree parseFts(String ftsQuery, Operator defaultOperator) throws RecognitionException {
+ CharStream cs = new ANTLRStringStream(ftsQuery);
+ FTSLexer lexer = new FTSLexer(cs);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ FTSParser parser = new FTSParser(tokens);
+ parser.setDefaultFieldConjunction(defaultOperator.equals(Operator.AND));
+ parser.setMode(defaultOperator.equals(Operator.AND) ? FTSParser.Mode.DEFAULT_CONJUNCTION : FTSParser.Mode.DEFAULT_DISJUNCTION);
+ CommonTree tree = (CommonTree) parser.ftsQuery().getTree();
+ return tree;
+ }
+
+ protected Tree bypassSingleTermDisjunctions(Tree tree) {
+ while ("DISJUNCTION".equals(tree.getText()) && tree.getChildCount() == 1)
+ tree = tree.getChild(0);
+ if ("DISJUNCTION".equals(tree.getText()))
+ return null;
+ return tree;
+ }
+
+ protected Collection extractRequiredTerms(Tree tree) {
+ while ("DISJUNCTION".equals(tree.getText()) && tree.getChildCount() == 1)
+ tree = tree.getChild(0);
+
+ List terms = new LinkedList<>();
+
+ switch (tree.getText()) {
+ case "DISJUNCTION":
+ break;
+ case "CONJUNCTION":
+ for (int c = 0; c < tree.getChildCount(); c++) {
+ Collection subtrees = this.extractRequiredTerms(tree.getChild(c));
+ if (subtrees == null || subtrees.isEmpty())
+ continue;
+ terms.addAll(subtrees);
+ }
+ break;
+ case "DEFAULT":
+ terms.add(tree);
+ break;
+ default:
+ this.logger.warn("Unexpected/unsupported tree: {}", tree.getText());
+ }
+
+ return terms;
+ }
+
+ protected Collection filterPropertyTerms(Collection trees, QName property) {
+ if (trees.isEmpty())
+ return trees;
+
+ Set prefixes = new HashSet<>(this.namespaceService.getPrefixes(property.getNamespaceURI()));
+ if (prefixes.isEmpty()) {
+ this.logger.warn("Unexpected/unsupported namespace: {}", property.getNamespaceURI());
+ trees.clear();
+ return trees;
+ }
+
+ Iterator i = trees.iterator();
+
+ while (i.hasNext()) {
+ Tree tree = i.next();
+
+ if ("DEFAULT".equals(tree.getText()))
+ tree = tree.getChild(0);
+
+ int skip = -1;
+ switch (tree.getText()) {
+ case "TERM":
+ case "PHRASE":
+ case "EXACT_TERM":
+ case "EXACT_PHRASE":
+ skip = 1; // skip the value child
+ break;
+ case "RANGE":
+ skip = 4; // skip the inclusive, start, end, inclusive children
+ break;
+ default:
+ }
+
+ if (skip >= 0) {
+ Tree fieldRef = tree.getChild(skip);
+ if (!"FIELD_REF".equals(fieldRef.getText())) {
+ this.logger.warn("Unexpected/unsupported tree: {}", tree.getText());
+ } else if (!fieldRef.getChild(0).getText().equals(property.getLocalName())) {
+ this.logger.trace("Found but ignoring property: {}", fieldRef.getChild(0).getText());
+ } else {
+ Tree prefix = fieldRef.getChild(1);
+ if (!"PREFIX".equals(prefix.getText())) {
+ this.logger.warn("Unexpected/unsupported tree: {}", tree.getText());
+ } else if (!prefixes.contains(prefix.getChild(0).getText())) {
+ this.logger.trace("Found but ignoring property: {}:{}", prefix.getChild(0).getText(), property.getLocalName());
+ } else {
+ // this will skip the remove()
+ continue;
+ }
+ }
+ }
+
+ i.remove();
+ }
+
+ return trees;
+ }
+
+ protected Collection filterOutFuzzyTerms(Collection trees) {
+ if (trees.isEmpty())
+ return trees;
+
+ Iterator i = trees.iterator();
+
+ while (i.hasNext()) {
+ Tree tree = i.next();
+
+ if ("DEFAULT".equals(tree.getText()))
+ tree = tree.getChild(0);
+
+ switch (tree.getText()) {
+ case "EXACT_TERM":
+ case "EXACT_PHRASE":
+ case "RANGE":
+ break;
+ default:
+ i.remove();
+ }
+ }
+
+ return trees;
+ }
+
+ protected QueryValue extractValue(Tree tree, DataTypeDefinition dataTypeDef) {
+ if ("DEFAULT".equals(tree.getText()))
+ tree = tree.getChild(0);
+
+ switch (tree.getText()) {
+ case "RANGE":
+ return this.extractRangeValue(tree, dataTypeDef);
+ default:
+ }
+
+ String value = this.unquote(tree.getChild(0).getText());
+
+ switch (dataTypeDef.getName().getLocalName()) {
+ case "boolean":
+ return new QuerySingleValue(Boolean.parseBoolean(value));
+ case "double":
+ return new QuerySingleValue(Double.parseDouble(value));
+ case "float":
+ return new QuerySingleValue(Float.parseFloat(value));
+ case "int":
+ return new QuerySingleValue(Integer.parseInt(value));
+ case "long":
+ return new QuerySingleValue(Long.parseLong(value));
+ case "date":
+ return new QuerySingleValue(this.evaluateAsDate(value));
+ case "datetime":
+ return new QuerySingleValue(this.evaluateAsDateTime(value));
+ case "period":
+ return new QuerySingleValue(Period.parse(value));
+ case "qname":
+ return new QuerySingleValue(QName.createQName(value, this.namespaceService));
+ case "noderef":
+ return new QuerySingleValue(new NodeRef(value));
+ case "childassocref":
+ return new QuerySingleValue(new ChildAssociationRef(value));
+ case "assocref":
+ return new QuerySingleValue(new AssociationRef(value));
+ case "locale":
+ return new QuerySingleValue(new Locale(value));
+ default:
+ return new QuerySingleValue(value);
+ }
+ }
+
+ protected QueryRangeValue> extractRangeValue(Tree tree, DataTypeDefinition dataTypeDef) {
+ boolean includeStart = "INCLUSIVE".equals(tree.getChild(0).getText());
+ String start = this.unquote(tree.getChild(1).getText());
+ String end = this.unquote(tree.getChild(2).getText());
+ boolean includeEnd = "INCLUSIVE".equals(tree.getChild(3).getText());
+
+ switch (dataTypeDef.getName().getLocalName()) {
+ case "double":
+ return new QueryRangeValue(includeStart, Double.parseDouble(start), includeEnd, Double.parseDouble(end));
+ case "float":
+ return new QueryRangeValue(includeStart, Float.parseFloat(start), includeEnd, Float.parseFloat(end));
+ case "int":
+ return new QueryRangeValue(includeStart, Integer.parseInt(start), includeEnd, Integer.parseInt(end));
+ case "long":
+ return new QueryRangeValue(includeStart, Long.parseLong(start), includeEnd, Long.parseLong(end));
+ case "date":
+ return new QueryRangeValue(includeStart, this.evaluateAsDate(start), includeEnd, this.evaluateAsDate(end));
+ case "datetime":
+ return new QueryRangeValue(includeStart, this.evaluateAsDateTime(start), includeEnd, this.evaluateAsDateTime(end));
+ default:
+ throw new UnsupportedOperationException("The data type does not make sense for range evaluation: " + dataTypeDef.getName());
+ }
+ }
+
+ protected LocalDate evaluateAsDate(String str) {
+ if ("now".equalsIgnoreCase(str)) return LocalDate.now();
+ else return LocalDate.parse(str);
+ }
+
+ protected LocalDateTime evaluateAsDateTime(String str) {
+ if ("now".equalsIgnoreCase(str)) return LocalDateTime.now();
+ else return LocalDateTime.parse(str);
+ }
+
+ protected String unquote(String str) {
+ if (str.length() < 2) return str;
+ else if (str.charAt(0) == '\'' && str.charAt(str.length()-1) == '\'') return str.substring(1, str.length()-1);
+ else if (str.charAt(0) == '\"' && str.charAt(str.length()-1) == '\"') return str.substring(1, str.length()-1);
+ else return str;
+ }
+
+}
diff --git a/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/QueryInspector.java b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/QueryInspector.java
new file mode 100644
index 0000000..6a1348d
--- /dev/null
+++ b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/QueryInspector.java
@@ -0,0 +1,74 @@
+package com.inteligr8.alfresco.asie.compute;
+
+import java.util.List;
+import java.util.Set;
+
+import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
+import org.alfresco.service.cmr.search.SearchParameters.Operator;
+import org.alfresco.service.namespace.QName;
+import org.antlr.runtime.RecognitionException;
+
+public interface QueryInspector {
+
+ Set getSupportedLanguages();
+
+ List findRequiredPropertyValues(String query, Operator defaultOperator, QName property, DataTypeDefinition dataTypeDef) throws RecognitionException;
+
+
+
+ public interface QueryValue {
+
+ }
+
+ public class QuerySingleValue implements QueryValue {
+
+ private T value;
+
+ public QuerySingleValue(T value) {
+ this.value = value;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return this.value.toString();
+ }
+
+ }
+
+ public class QueryRangeValue implements QueryValue {
+
+ private boolean includeStart;
+ private T start;
+ private boolean includeEnd;
+ private T end;
+
+ public QueryRangeValue(boolean includeStart, T start, boolean includeEnd, T end) {
+ this.includeStart = includeStart;
+ this.start = start;
+ this.includeEnd = includeEnd;
+ this.end = end;
+ }
+
+ public boolean isIncludeStart() {
+ return includeStart;
+ }
+
+ public boolean isIncludeEnd() {
+ return includeEnd;
+ }
+
+ public T getStart() {
+ return start;
+ }
+
+ public T getEnd() {
+ return end;
+ }
+
+ }
+
+}
diff --git a/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/QueryInspectorFactory.java b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/QueryInspectorFactory.java
new file mode 100644
index 0000000..bad416d
--- /dev/null
+++ b/community-module/src/main/java/com/inteligr8/alfresco/asie/compute/QueryInspectorFactory.java
@@ -0,0 +1,31 @@
+package com.inteligr8.alfresco.asie.compute;
+
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class QueryInspectorFactory implements InitializingBean {
+
+ @Autowired
+ private List inspectors;
+
+ private Map languageInspectorMap;
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ for (QueryInspector inspector : this.inspectors) {
+ for (String language : inspector.getSupportedLanguages())
+ this.languageInspectorMap.put(language, inspector);
+ }
+ }
+
+ public QueryInspector selectQueryInspector(SearchParameters searchParams) {
+ return this.languageInspectorMap.get(searchParams.getLanguage());
+ }
+
+}
diff --git a/community-module/src/main/java/com/inteligr8/alfresco/asie/service/SolrShardRegistry.java b/community-module/src/main/java/com/inteligr8/alfresco/asie/service/SolrShardRegistry.java
index 9a96726..c982712 100644
--- a/community-module/src/main/java/com/inteligr8/alfresco/asie/service/SolrShardRegistry.java
+++ b/community-module/src/main/java/com/inteligr8/alfresco/asie/service/SolrShardRegistry.java
@@ -8,8 +8,8 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.OptionalInt;
import java.util.Map.Entry;
+import java.util.OptionalInt;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -21,34 +21,15 @@ import org.alfresco.repo.index.shard.ShardInstance;
import org.alfresco.repo.index.shard.ShardMethodEnum;
import org.alfresco.repo.index.shard.ShardRegistry;
import org.alfresco.repo.index.shard.ShardState;
-import org.alfresco.repo.search.impl.QueryParserUtils;
-import org.alfresco.repo.search.impl.parsers.AlfrescoFunctionEvaluationContext;
-import org.alfresco.repo.search.impl.parsers.CMISLexer;
-import org.alfresco.repo.search.impl.parsers.FTSLexer;
-import org.alfresco.repo.search.impl.parsers.FTSParser;
-import org.alfresco.repo.search.impl.parsers.FTSQueryParser;
-import org.alfresco.repo.search.impl.querymodel.Conjunction;
-import org.alfresco.repo.search.impl.querymodel.Constraint;
-import org.alfresco.repo.search.impl.querymodel.Disjunction;
-import org.alfresco.repo.search.impl.querymodel.FunctionalConstraint;
-import org.alfresco.repo.search.impl.querymodel.QueryEngine;
-import org.alfresco.repo.search.impl.querymodel.QueryModelFactory;
-import org.alfresco.repo.search.impl.querymodel.QueryOptions;
-import org.alfresco.repo.search.impl.querymodel.QueryOptions.Connective;
-import org.alfresco.repo.search.impl.querymodel.impl.BaseConstraint;
-import org.alfresco.repo.search.impl.querymodel.impl.lucene.LuceneQueryBuilderComponent;
+import org.alfresco.repo.lock.JobLockService;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.alfresco.service.cmr.attributes.AttributeService.AttributeQueryCallback;
+import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.search.SearchParameters;
-import org.alfresco.service.cmr.search.SearchParameters.Operator;
-import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
-import org.antlr.runtime.ANTLRStringStream;
-import org.antlr.runtime.CharStream;
-import org.antlr.runtime.CommonTokenStream;
-import org.antlr.runtime.tree.CommonTree;
+import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -59,6 +40,12 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean;
import org.springframework.stereotype.Component;
import com.inteligr8.alfresco.asie.Constants;
+import com.inteligr8.alfresco.asie.cache.MultiValueCache;
+import com.inteligr8.alfresco.asie.compute.QueryInspector;
+import com.inteligr8.alfresco.asie.compute.QueryInspector.QueryRangeValue;
+import com.inteligr8.alfresco.asie.compute.QueryInspector.QuerySingleValue;
+import com.inteligr8.alfresco.asie.compute.QueryInspector.QueryValue;
+import com.inteligr8.alfresco.asie.compute.QueryInspectorFactory;
import com.inteligr8.alfresco.asie.model.Node;
import com.inteligr8.alfresco.asie.model.ShardSet;
@@ -68,6 +55,8 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Pattern coreShardPattern = Pattern.compile("(.+)-[0-9]+");
+ private final QName shardLock = QName.createQName(Constants.NAMESPACE_ASIE, "shardLock");
+
@Autowired
private ShardStateService sss;
@@ -79,21 +68,30 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
private NamespaceService namespaceService;
@Autowired
- @Qualifier(Constants.BEAN_SHARD_STATE_CACHE)
- private SimpleCache onlineShardCache;
+ private DictionaryService dictionaryService;
+
+ @Autowired
+ private QueryInspectorFactory queryInspectorFactory;
+
+ @Autowired
+ private JobLockService jobLockService;
+
+ @Autowired
+ @Qualifier(Constants.BEAN_FLOC_SHARD_NODE_CACHE)
+ private SimpleCache>> flocShardNodeCache;
+
+ @Autowired
+ @Qualifier(Constants.BEAN_ONLINE_SHARD_STATE_CACHE)
+ private SimpleCache onlineNodeShardStateCache;
@Autowired
@Qualifier(Constants.BEAN_OFFILINE_SHARD_STATE_CACHE)
- private SimpleCache offlineShardCache;
+ private SimpleCache offlineNodeShardStateCache;
@Autowired
@Qualifier(Constants.BEAN_CORE_EXPLICIT_CACHE)
private SimpleCache coreExplicitIdCache;
- @Autowired
- @Qualifier(Constants.BEAN_FLOC_CACHE)
- private SimpleCache flocCache;
-
@Value("${inteligr8.asie.registerUnknownShardOffline}")
private boolean registerOffline;
@@ -105,64 +103,99 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
@Override
protected void onBootstrap(ApplicationEvent event) {
- this.attrService.getAttributes(new AttributeQueryCallback() {
- @Override
- public boolean handleAttribute(Long id, Serializable value, Serializable[] keys) {
- switch ((String) keys[2]) {
- case Constants.ATTR_STATE:
- ShardState shardNodeState = (ShardState) value;
- ShardInstance shardNode = shardNodeState.getShardInstance();
- cacheShard(shardNode, shardNodeState, (String) keys[1]);
-
- if (ShardMethodEnum.EXPLICIT_ID.toString().equals(shardNodeState.getPropertyBag().get("shard.method"))) {
- String coreName = shardNode.getShard().getFloc().getPropertyBag().get("coreName");
- if (coreName != null && !coreExplicitIdCache.contains(coreName)) {
- String property = shardNodeState.getPropertyBag().get("shard.key");
- QName propertyQname = QName.createQName(property, namespaceService);
-
- logger.debug("Mapping core to explicit ID: {} => {}", coreName, propertyQname);
- coreExplicitIdCache.put(coreName, propertyQname);
- }
- }
-
- return true;
- default:
- return true;
-
+ String lock = this.jobLockService.getLock(this.shardLock, 2500L, 500L, 10);
+ try {
+ this.attrService.getAttributes(new AttributeQueryCallback() {
+ @Override
+ public boolean handleAttribute(Long id, Serializable value, Serializable[] keys) {
+ switch ((String) keys[2]) {
+ case Constants.ATTR_STATE:
+ ShardState shardNodeState = (ShardState) value;
+ ShardInstance shardNode = shardNodeState.getShardInstance();
+ cacheShard(shardNode, shardNodeState, (String) keys[1]);
+ return true;
+ default:
+ return true;
+
+ }
}
- }
- }, Constants.ATTR_ASIE_NODES);
+ }, Constants.ATTR_ASIE_NODE_SHARD);
+ } finally {
+ this.jobLockService.releaseLock(lock, this.shardLock);
+ }
}
@Override
protected void onShutdown(ApplicationEvent event) {
}
-
- protected void cacheShard(ShardInstance shardNode, ShardState shardNodeState, String nodeId) {
- SimpleCache shardCache = this.onlineShardCache;
- ShardState cachedShardNodeState = this.onlineShardCache.get(shardNode);
+
+ /**
+ * This is private because it must be wrapped in a cluster-safe lock
+ */
+ private void cacheShard(ShardInstance shardNode, ShardState shardNodeState, String nodeShardId) {
+ ShardInstance detachedShardNode = this.detach(shardNode);
+
+ SimpleCache shardCache = this.onlineNodeShardStateCache;
+ ShardState cachedShardNodeState = this.onlineNodeShardStateCache.get(detachedShardNode);
if (cachedShardNodeState == null) {
- cachedShardNodeState = this.offlineShardCache.get(shardNode);
- shardCache = this.offlineShardCache;
+ cachedShardNodeState = this.offlineNodeShardStateCache.get(detachedShardNode);
+ shardCache = this.offlineNodeShardStateCache;
}
+
+ Shard shard = shardNode.getShard();
+ this.putPutAdd(this.flocShardNodeCache, shard.getFloc(), shard.getInstance(), detachedShardNode);
if (cachedShardNodeState == null) {
- Boolean online = (Boolean) this.attrService.getAttribute(Constants.ATTR_ASIE_NODES, nodeId, Constants.ATTR_ONLINE);
+ Boolean online = (Boolean) this.attrService.getAttribute(Constants.ATTR_ASIE_NODE_SHARD, nodeShardId, Constants.ATTR_ONLINE);
if (online != null) {
if (online.booleanValue()) {
- this.onlineShardCache.put(shardNode, cachedShardNodeState);
+ this.onlineNodeShardStateCache.put(detachedShardNode, cachedShardNodeState);
} else {
- this.offlineShardCache.put(shardNode, cachedShardNodeState);
+ this.offlineNodeShardStateCache.put(detachedShardNode, cachedShardNodeState);
}
} else {
if (this.registerOffline) {
- this.offlineShardCache.put(shardNode, cachedShardNodeState);
+ this.offlineNodeShardStateCache.put(detachedShardNode, cachedShardNodeState);
} else {
- this.onlineShardCache.put(shardNode, cachedShardNodeState);
+ this.onlineNodeShardStateCache.put(detachedShardNode, cachedShardNodeState);
}
}
} else if (cachedShardNodeState.getLastIndexedTxId() < shardNodeState.getLastIndexedTxId()) {
- shardCache.put(shardNode, shardNodeState);
+ // update the cached state if the state's last indexes transaction is later
+ shardCache.put(shardNode, this.detach(shardNodeState));
+ }
+
+ switch (shardNode.getShard().getFloc().getShardMethod()) {
+ case EXPLICIT_ID:
+ cacheExplicitShard(shardNode, shardNodeState);
+ break;
+ default:
+ }
+ }
+
+ private void cacheExplicitShard(ShardInstance shardNode, ShardState shardNodeState) {
+ String coreName = shardNode.getShard().getFloc().getPropertyBag().get("coreName");
+ if (coreName != null && !this.coreExplicitIdCache.contains(coreName)) {
+ String property = shardNodeState.getPropertyBag().get("shard.key");
+ QName propertyQName = QName.createQName(property, this.namespaceService);
+
+ this.logger.debug("Mapping core to explicit ID: {} => {}", coreName, propertyQName);
+ this.coreExplicitIdCache.put(coreName, propertyQName);
+ }
+ }
+
+ @Override
+ public void registerShardState(ShardState shardNodeState) {
+ ShardInstance shardNode = shardNodeState.getShardInstance();
+ Node node = new Node(shardNode);
+ this.fixFlocPropertyBag(shardNodeState);
+
+ String lock = this.jobLockService.getLock(this.shardLock, 2500L, 500L, 10);
+ try {
+ this.cacheShard(shardNode, shardNodeState, node.getId());
+ this.persistShards();
+ } finally {
+ this.jobLockService.releaseLock(lock, this.shardLock);
}
}
@@ -182,25 +215,50 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
}
protected String extractCoreName(String coreShardName) {
- Matcher matcher = coreShardPattern.matcher(coreShardName);
+ Matcher matcher = this.coreShardPattern.matcher(coreShardName);
if (!matcher.matches())
return null;
return matcher.group(1);
}
- @Override
- public void registerShardState(ShardState shardNodeState) {
- ShardInstance shardNode = shardNodeState.getShardInstance();
- Node node = new Node(shardNode);
- this.fixFlocPropertyBag(shardNodeState);
- this.cacheShard(shardNode, shardNodeState, node.getId());
+ /**
+ * This is private because it must be wrapped in a cluster-safe lock
+ */
+ private void persistShards() {
+ long onlineExpired = System.currentTimeMillis() - this.offlineIdleShardInSeconds * 1000L;
+ long offlineExpired = System.currentTimeMillis() - this.forgetOfflineShardInSeconds * 1000L;
+
+ for (ShardInstance shardNode : this.onlineNodeShardStateCache.getKeys()) {
+ String nodeShardId = new Node(shardNode).getId() + ";" + shardNode.getShard().getInstance();
+ ShardState shardNodeState = this.onlineNodeShardStateCache.get(shardNode);
+ if (shardNodeState.getLastUpdated() < onlineExpired) {
+ this.logger.warn("Taking shard offline: {}", shardNode);
+ this.onlineNodeShardStateCache.remove(shardNode);
+ this.offlineNodeShardStateCache.put(shardNode, shardNodeState);
+ } else {
+ this.attrService.setAttribute(shardNodeState, Constants.ATTR_ASIE_NODE_SHARD, nodeShardId, Constants.ATTR_STATE);
+ this.attrService.setAttribute(Boolean.TRUE, Constants.ATTR_ASIE_NODE_SHARD, nodeShardId, Constants.ATTR_ONLINE);
+ }
+ }
+
+ for (ShardInstance shardNode : this.offlineNodeShardStateCache.getKeys()) {
+ String nodeShardId = new Node(shardNode).getId() + ";" + shardNode.getShard().getInstance();
+ ShardState shardNodeState = this.offlineNodeShardStateCache.get(shardNode);
+ if (shardNodeState.getLastUpdated() < offlineExpired) {
+ this.logger.info("Forgetting about already offline shard: {}", shardNode);
+ this.offlineNodeShardStateCache.remove(shardNode);
+ } else {
+ this.attrService.setAttribute(shardNodeState, Constants.ATTR_ASIE_NODE_SHARD, nodeShardId, Constants.ATTR_STATE);
+ this.attrService.setAttribute(Boolean.FALSE, Constants.ATTR_ASIE_NODE_SHARD, nodeShardId, Constants.ATTR_ONLINE);
+ }
+ }
}
@Override
public Map>> getFlocs() {
Map>> flocs = new HashMap<>();
- for (ShardInstance shardNode : this.onlineShardCache.getKeys()) {
+ for (ShardInstance shardNode : this.onlineNodeShardStateCache.getKeys()) {
Floc floc = shardNode.getShard().getFloc();
Map> shards = flocs.get(floc);
@@ -211,7 +269,7 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
if (shardNodeStates == null)
shards.put(shardNode.getShard(), shardNodeStates = new HashSet<>());
- ShardState shardNodeState = this.onlineShardCache.get(shardNode);
+ ShardState shardNodeState = this.onlineNodeShardStateCache.get(shardNode);
if (shardNodeState != null) // in case it was removed during the looping (very rare)
shardNodeStates.add(shardNodeState);
}
@@ -229,20 +287,20 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
long onlineExpired = System.currentTimeMillis() - this.offlineIdleShardInSeconds * 1000L;
long offlineExpired = System.currentTimeMillis() - this.forgetOfflineShardInSeconds * 1000L;
- for (ShardInstance shardNode : this.onlineShardCache.getKeys()) {
- ShardState shardNodeState = this.onlineShardCache.get(shardNode);
+ for (ShardInstance shardNode : this.onlineNodeShardStateCache.getKeys()) {
+ ShardState shardNodeState = this.onlineNodeShardStateCache.get(shardNode);
if (shardNodeState.getLastUpdated() < onlineExpired) {
this.logger.warn("Taking shard offline: {}", shardNode);
- this.onlineShardCache.remove(shardNode);
- this.offlineShardCache.put(shardNode, shardNodeState);
+ this.onlineNodeShardStateCache.remove(shardNode);
+ this.offlineNodeShardStateCache.put(shardNode, shardNodeState);
}
}
- for (ShardInstance shardNode : this.offlineShardCache.getKeys()) {
- ShardState shardNodeState = this.offlineShardCache.get(shardNode);
+ for (ShardInstance shardNode : this.offlineNodeShardStateCache.getKeys()) {
+ ShardState shardNodeState = this.offlineNodeShardStateCache.get(shardNode);
if (shardNodeState.getLastUpdated() < offlineExpired) {
this.logger.info("Forgetting about already offline shard: {}", shardNode);
- this.offlineShardCache.remove(shardNode);
+ this.offlineNodeShardStateCache.remove(shardNode);
}
}
}
@@ -256,7 +314,7 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
public Set getShardInstanceList(String coreName) {
Set shardIds = new HashSet<>();
- for (ShardInstance shardNode : this.onlineShardCache.getKeys()) {
+ for (ShardInstance shardNode : this.onlineNodeShardStateCache.getKeys()) {
shardIds.add(shardNode.getShard().getInstance());
}
@@ -270,94 +328,94 @@ public class SolrShardRegistry extends AbstractLifecycleBean implements ShardReg
@Override
public List getIndexSlice(SearchParameters searchParameters) {
- for (Floc floc : this.flocCache.getKeys()) {
- Set shardIds = new HashSet<>();
+ if (searchParameters.getQuery() == null)
+ return Collections.emptyList();
+
+ List bestShards = null;
+
+ for (Floc floc : this.flocShardMultiCache.getKeys()) {
+ List shards = new LinkedList<>();
switch (floc.getShardMethod()) {
case EXPLICIT_ID:
String property = floc.getPropertyBag().get("shard.key");
- // check filters and other parameters
- if (searchParameters.getQuery() != null) {
- SearchTerm term = this.extractPropertySearchTeam(searchParameters, property);
- if (term != null && term.operator.equals("=")) {
- try {
- shardIds.add(Integer.parseInt(term.value));
- } catch (NumberFormatException nfe) {
- // skip
- }
+ QName propertyQName = QName.createQName(property, this.namespaceService);
+ DataTypeDefinition dtdef = this.dictionaryService.getProperty(propertyQName).getDataType();
+
+ QueryInspector inspector = this.queryInspectorFactory.selectQueryInspector(searchParameters);
+ if (inspector == null)
+ continue;
+
+ Set shardIds = new HashSet<>();
+ List values = inspector.findRequiredPropertyValues(searchParameters.getQuery(), searchParameters.getDefaultOperator(), propertyQName, dtdef);
+ for (QueryValue value : values) {
+ if (value instanceof QuerySingleValue>) {
+ @SuppressWarnings("unchecked")
+ Number num = ((QuerySingleValue extends Number>) value).getValue();
+ shardIds.add(num.intValue());
+ } else if (value instanceof QueryRangeValue>) {
+ @SuppressWarnings("unchecked")
+ QueryRangeValue extends Number> num = (QueryRangeValue extends Number>) value;
+ int start = num.getStart().intValue();
+ if (!num.isIncludeStart())
+ start++;
+ int end = num.getStart().intValue();
+ if (!num.isIncludeEnd())
+ end--;
+ for (int shardId = start; shardId <= end; shardId++)
+ shardIds.add(shardId);
}
}
+
+ // shardIds to shardInstances
break;
+ default:
+ // make no determination
}
+
+ if (!shards.isEmpty() && (bestShards == null || shards.size() < bestShards.size()))
+ bestShards = shards;
}
- searchParameters.get
- // TODO Auto-generated method stub
- return null;
+
+ return bestShards;
}
- private SearchTerm extractPropertySearchTeam(SearchParameters searchParameters, String property) {
- switch (searchParameters.getLanguage()) {
- case SearchService.LANGUAGE_CMIS_ALFRESCO:
- case SearchService.LANGUAGE_CMIS_STRICT:
- case SearchService.LANGUAGE_INDEX_CMIS:
- case SearchService.LANGUAGE_SOLR_CMIS:
- return this.extractCmisPropertySearchTerm(searchParameters, property, "=");
- case SearchService.LANGUAGE_FTS_ALFRESCO:
- case SearchService.LANGUAGE_INDEX_ALFRESCO:
- case SearchService.LANGUAGE_INDEX_FTS_ALFRESCO:
- case SearchService.LANGUAGE_LUCENE:
- case SearchService.LANGUAGE_SOLR_ALFRESCO:
- case SearchService.LANGUAGE_SOLR_FTS_ALFRESCO:
- return this.extractFtsPropertySearchTerm(searchParameters, "=@" + property);
- default:
- return null;
- }
+ protected List getIndexSlice() {
+
}
- @Autowired
- private QueryEngine queryEngine;
-
- @Autowired
- private DictionaryService dictionaryService;
-
- private SearchTerm extractFtsPropertySearchTerm(SearchParameters searchParameters, String field) {
- // TODO include filter and other possible constraints
-
- if (searchParameters.getQuery() == null)
- return null;
-
- CharStream cs = new ANTLRStringStream(searchParameters.getQuery());
- FTSLexer lexer = new FTSLexer(cs);
- CommonTokenStream tokens = new CommonTokenStream(lexer);
- FTSParser parser = new FTSParser(tokens);
- parser.setDefaultFieldConjunction(searchParameters.getDefaultFTSOperator().equals(Operator.AND));
- parser.setMode(searchParameters.getDefaultFTSOperator().equals(Operator.AND) ? FTSParser.Mode.DEFAULT_CONJUNCTION : FTSParser.Mode.DEFAULT_DISJUNCTION);
- CommonTree ftsNode = (CommonTree) parser.ftsQuery().getTree();
+ private ShardInstance detach(ShardInstance shardNode) {
+ ShardInstance detachedShardNode = new ShardInstance();
+ detachedShardNode.setHostName(shardNode.getHostName());
+ detachedShardNode.setPort(shardNode.getPort());
+ detachedShardNode.setBaseUrl(shardNode.getBaseUrl());
+ return detachedShardNode;
}
- private SearchTerm extractCmisPropertySearchTerm(SearchParameters searchParameters, String field, String operator) {
- // TODO include filter and other possible constraints
-
- if (searchParameters.getQuery() == null)
- return null;
-
- CharStream cs = new ANTLRStringStream(searchParameters.getQuery());
- CMISLexer lexer = new CMISLexer();
- CommonTokenStream tokens = new CommonTokenStream(lexer);
- FTSParser parser = new FTSParser(tokens);
- parser.setDefaultFieldConjunction(searchParameters.getDefaultFTSOperator().equals(Operator.AND));
- parser.setMode(searchParameters.getDefaultFTSOperator().equals(Operator.AND) ? FTSParser.Mode.DEFAULT_CONJUNCTION : FTSParser.Mode.DEFAULT_DISJUNCTION);
- CommonTree ftsNode = (CommonTree) parser.ftsQuery().getTree();
+ private ShardState detach(ShardState shardState) {
+ ShardState detachedShardState = new ShardState();
+ detachedShardState.setLastIndexedChangeSetCommitTime(shardState.getLastIndexedChangeSetCommitTime());
+ detachedShardState.setLastIndexedChangeSetId(shardState.getLastIndexedChangeSetId());
+ detachedShardState.setLastIndexedTxCommitTime(shardState.getLastIndexedTxCommitTime());
+ detachedShardState.setLastIndexedTxId(shardState.getLastIndexedTxId());
+ detachedShardState.setLastUpdated(shardState.getLastUpdated());
+ detachedShardState.setMaster(shardState.isMaster());
+ detachedShardState.setPropertyBag(shardState.getPropertyBag());
+ return detachedShardState;
}
+ private boolean putPutAdd(SimpleCache>> cache, K1 cacheKey, K2 mapKey, V mapValue) {
+ Map> map = cache.get(cacheKey);
+ if (map == null)
+ map = new HashMap<>();
+ return this.putAdd(map, mapKey, mapValue);
+ }
-
- private class SearchTerm {
-
- private String field;
- private String operator;
- private String value;
-
+ private boolean putAdd(Map> map, K key, V value) {
+ Set set = map.get(key);
+ if (set == null)
+ set = new HashSet<>();
+ return set.add(value);
}
}
diff --git a/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/alfresco-global.properties b/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/alfresco-global.properties
index ef89240..b3963b8 100644
--- a/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/alfresco-global.properties
+++ b/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/alfresco-global.properties
@@ -4,10 +4,22 @@ inteligr8.asie.idleShardExpirationInSeconds=${}
+# Overrides of alfresco-repository.jar/alfresco/caches.properties
+cache.shardStateSharedCache.tx.maxItems=16384
+cache.shardStateSharedCache.tx.statsEnabled=${caches.tx.statsEnabled}
+cache.shardStateSharedCache.maxItems=16384
+cache.shardStateSharedCache.timeToLiveSeconds=1800
+cache.shardStateSharedCache.maxIdleSeconds=0
+cache.shardStateSharedCache.cluster.type=fully-distributed
+cache.shardStateSharedCache.backup-count=1
+cache.shardStateSharedCache.eviction-policy=LRU
+cache.shardStateSharedCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
+cache.shardStateSharedCache.readBackupData=false
+
# maxItems needs to be greater than total shards, including HA instances
-cache.offlineShardStateSharedCache.tx.maxItems=1024
+cache.offlineShardStateSharedCache.tx.maxItems=16384
cache.offlineShardStateSharedCache.tx.statsEnabled=${caches.tx.statsEnabled}
-cache.offlineShardStateSharedCache.maxItems=1024
+cache.offlineShardStateSharedCache.maxItems=16384
cache.offlineShardStateSharedCache.timeToLiveSeconds=1800
cache.offlineShardStateSharedCache.maxIdleSeconds=0
cache.offlineShardStateSharedCache.cluster.type=fully-distributed
@@ -16,9 +28,9 @@ cache.offlineShardStateSharedCache.eviction-policy=LRU
cache.offlineShardStateSharedCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.offlineShardStateSharedCache.readBackupData=false
-cache.coreExplicitIdSharedCache.tx.maxItems=1024
+cache.coreExplicitIdSharedCache.tx.maxItems=16384
cache.coreExplicitIdSharedCache.tx.statsEnabled=${caches.tx.statsEnabled}
-cache.coreExplicitIdSharedCache.maxItems=1024
+cache.coreExplicitIdSharedCache.maxItems=16384
cache.coreExplicitIdSharedCache.timeToLiveSeconds=1800
cache.coreExplicitIdSharedCache.maxIdleSeconds=0
cache.coreExplicitIdSharedCache.cluster.type=fully-distributed
diff --git a/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/log4j2.properties b/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/log4j2.properties
index 6c345f1..e69de29 100644
--- a/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/log4j2.properties
+++ b/community-module/src/main/resources/alfresco/module/com_inteligr8_alfresco_asie-community-platform-module/log4j2.properties
@@ -1,3 +0,0 @@
-
-logger.inteligr8-asie.name=com.inteligr8.alfresco.asie
-logger.inteligr8-asie.level=INFO
diff --git a/community-module/src/test/java/com/inteligr8/alfresco/asie/QueryConstraintUnitTest.java b/community-module/src/test/java/com/inteligr8/alfresco/asie/QueryConstraintUnitTest.java
new file mode 100644
index 0000000..1e05d44
--- /dev/null
+++ b/community-module/src/test/java/com/inteligr8/alfresco/asie/QueryConstraintUnitTest.java
@@ -0,0 +1,146 @@
+package com.inteligr8.alfresco.asie;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.repo.search.impl.parsers.FTSLexer;
+import org.alfresco.repo.search.impl.parsers.FTSParser;
+import org.alfresco.service.cmr.search.SearchParameters.Operator;
+import org.antlr.runtime.ANTLRStringStream;
+import org.antlr.runtime.CharStream;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.RecognitionException;
+import org.antlr.runtime.tree.CommonTree;
+import org.antlr.runtime.tree.Tree;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class QueryConstraintUnitTest {
+
+ private static final ObjectMapper om = new ObjectMapper();
+
+ @BeforeClass
+ public static void init() {
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(Tree.class, new TreeSerializer());
+ om.registerModule(module);
+ }
+
+ @Test
+ public void testSingleExactTerm() throws RecognitionException, JsonProcessingException {
+ Tree tree = this.parseFts("=@cm:title:test", Operator.AND);
+ tree = this.validateChildren(tree, "DISJUNCTION");
+ tree = this.validateChildren(tree, "CONJUNCTION");
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "EXACT_TERM", "test");
+ tree = this.validateChildren(tree, "FIELD_REF", "title");
+ this.validate(tree, "PREFIX", "cm");
+ }
+
+ @Test
+ public void testSingleFuzzyTerm() throws RecognitionException, JsonProcessingException {
+ Tree tree = this.parseFts("@cm:title:test", Operator.AND);
+ tree = this.validateChildren(tree, "DISJUNCTION");
+ tree = this.validateChildren(tree, "CONJUNCTION");
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "TERM", "test");
+ tree = this.validateChildren(tree, "FIELD_REF", "title");
+ this.validate(tree, "PREFIX", "cm");
+ }
+
+ @Test
+ public void testSingleFuzzyString() throws RecognitionException, JsonProcessingException {
+ Tree tree = this.parseFts("@cm:title:'testing'", Operator.AND);
+ tree = this.validateChildren(tree, "DISJUNCTION");
+ tree = this.validateChildren(tree, "CONJUNCTION");
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "PHRASE", "'testing'");
+ tree = this.validateChildren(tree, "FIELD_REF", "title");
+ this.validate(tree, "PREFIX", "cm");
+ }
+
+ @Test
+ public void testSingleFuzzyStringDoubleQuotes() throws RecognitionException, JsonProcessingException {
+ Tree tree = this.parseFts("cm:title:\"testing\"", Operator.AND);
+ tree = this.validateChildren(tree, "DISJUNCTION");
+ tree = this.validateChildren(tree, "CONJUNCTION");
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "PHRASE", "\"testing\"");
+ tree = this.validateChildren(tree, "FIELD_REF", "title");
+ this.validate(tree, "PREFIX", "cm");
+ }
+
+ @Test
+ public void testSingleRange() throws RecognitionException, JsonProcessingException {
+ Tree tree = this.parseFts("@cm:created:[NOW TO '2025-01-01T00:00:00'>", Operator.AND);
+ tree = this.validateChildren(tree, "DISJUNCTION");
+ tree = this.validateChildren(tree, "CONJUNCTION");
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "RANGE", "INCLUSIVE", "NOW", "'2025-01-01T00:00:00'", "EXCLUSIVE");
+ tree = this.validateChildren(tree, "FIELD_REF", "created");
+ this.validate(tree, "PREFIX", "cm");
+ }
+
+ @Test
+ public void testTwoTerms() throws RecognitionException, JsonProcessingException {
+ Tree tree = this.parseFts("=@cm:title:test1 AND @cm:author:test2", Operator.AND);
+ tree = this.validateChildren(tree, "DISJUNCTION");
+ List trees = this.validateChildren(tree, "CONJUNCTION", 2);
+
+ tree = trees.get(0);
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "EXACT_TERM", "test1");
+ tree = this.validateChildren(tree, "FIELD_REF", "title");
+ this.validate(tree, "PREFIX", "cm");
+
+ tree = trees.get(1);
+ tree = this.validateChildren(tree, "DEFAULT");
+ tree = this.validateChildren(tree, "TERM", "test2");
+ tree = this.validateChildren(tree, "FIELD_REF", "author");
+ this.validate(tree, "PREFIX", "cm");
+ }
+
+ protected void validate(Tree tree, String text, String... extraValues) {
+ Assert.assertNotNull(tree);
+ Assert.assertEquals(text, tree.getText());
+ Assert.assertEquals(extraValues.length, tree.getChildCount());
+ for (int c = 0; c < extraValues.length; c++)
+ Assert.assertEquals(extraValues[c], tree.getChild(c).getText());
+ }
+
+ protected Tree validateChildren(Tree tree, String text, String... extraValues) {
+ Assert.assertNotNull(tree);
+ Assert.assertEquals(text, tree.getText());
+ Assert.assertEquals(extraValues.length + 1, tree.getChildCount());
+ for (int c = 0; c < extraValues.length; c++)
+ Assert.assertEquals(extraValues[c], tree.getChild(c).getText());
+ return tree.getChild(extraValues.length);
+ }
+
+ protected List validateChildren(Tree tree, String text, int count) {
+ Assert.assertNotNull(tree);
+ Assert.assertEquals(text, tree.getText());
+ Assert.assertEquals(count, tree.getChildCount());
+ List children = new ArrayList<>();
+ for (int c = 0; c < tree.getChildCount(); c++)
+ children.add(tree.getChild(c));
+ return children;
+ }
+
+ protected Tree parseFts(String ftsQuery, Operator defaultOperator) throws RecognitionException, JsonProcessingException {
+ CharStream cs = new ANTLRStringStream(ftsQuery);
+ FTSLexer lexer = new FTSLexer(cs);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ FTSParser parser = new FTSParser(tokens);
+ parser.setDefaultFieldConjunction(defaultOperator.equals(Operator.AND));
+ parser.setMode(defaultOperator.equals(Operator.AND) ? FTSParser.Mode.DEFAULT_CONJUNCTION : FTSParser.Mode.DEFAULT_DISJUNCTION);
+ CommonTree tree = (CommonTree) parser.ftsQuery().getTree();
+ return tree;
+ }
+
+}
diff --git a/community-module/src/test/java/com/inteligr8/alfresco/asie/TreeSerializer.java b/community-module/src/test/java/com/inteligr8/alfresco/asie/TreeSerializer.java
new file mode 100644
index 0000000..b9da03a
--- /dev/null
+++ b/community-module/src/test/java/com/inteligr8/alfresco/asie/TreeSerializer.java
@@ -0,0 +1,44 @@
+package com.inteligr8.alfresco.asie;
+
+import java.io.IOException;
+
+import org.antlr.runtime.tree.Tree;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class TreeSerializer extends StdSerializer {
+
+ private static final long serialVersionUID = -2714782538361726878L;
+
+ public TreeSerializer() {
+ super(Tree.class);
+ }
+
+ public TreeSerializer(Class type) {
+ super(type);
+ }
+
+ public TreeSerializer(JavaType type) {
+ super(type);
+ }
+
+ @Override
+ public void serialize(Tree value, JsonGenerator gen, SerializerProvider provider) throws IOException {
+ gen.writeStartObject();
+ if (value.getText() != null)
+ gen.writeStringField("text", value.getText());
+
+ if (value.getChildCount() > 0) {
+ gen.writeArrayFieldStart("children");
+ for (int c = 0; c < value.getChildCount(); c++)
+ gen.writeObject(value.getChild(c));
+ gen.writeEndArray();
+ }
+
+ gen.writeEndObject();
+ }
+
+}