/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Jspresso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.model.persistence.mongo.criterion;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.jspresso.framework.application.action.AbstractActionContextAware;
import org.jspresso.framework.model.component.IPropertyTranslation;
import org.jspresso.framework.model.component.IQueryComponent;
import org.jspresso.framework.model.component.query.ComparableQueryStructure;
import org.jspresso.framework.model.component.query.EnumQueryStructure;
import org.jspresso.framework.model.component.query.EnumValueQueryStructure;
import org.jspresso.framework.model.descriptor.ICollectionPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IComponentDescriptor;
import org.jspresso.framework.model.descriptor.IEnumerationPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IReferencePropertyDescriptor;
import org.jspresso.framework.model.descriptor.IStringPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.AbstractComponentDescriptor;
import org.jspresso.framework.model.descriptor.query.ComparableQueryStructureDescriptor;
import org.jspresso.framework.model.entity.EntityHelper;
import org.jspresso.framework.model.entity.IEntity;
import org.jspresso.framework.util.bean.PropertyHelper;
import org.jspresso.framework.util.collection.ESort;
import org.jspresso.framework.view.descriptor.basic.PropertyViewDescriptorHelper;
/**
* Default implementation of a query factory.
*
* @author Vincent Vandenschrick
*/
public class DefaultQueryFactory extends AbstractActionContextAware implements IQueryFactory {
private static final Logger LOG = LoggerFactory.getLogger(DefaultQueryFactory.class);
private static final String SPECIAL_CHARS = "${}*^";
private boolean triStateBooleanSupported;
/**
* Constructs a new {@code DefaultQueryFactory} instance.
*/
public DefaultQueryFactory() {
triStateBooleanSupported = false;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("ConstantConditions")
@Override
public void completeQueryWithOrdering(Query query, IQueryComponent queryComponent, Map<String, Object> context) {
// complete sorting properties
if (queryComponent.getOrderingProperties() != null) {
List<Sort.Order> sortOrders = new ArrayList<>();
for (Map.Entry<String, ESort> orderingProperty : queryComponent.getOrderingProperties().entrySet()) {
String propertyName = orderingProperty.getKey();
String[] propElts = propertyName.split("\\.");
boolean sortable = true;
if (propElts.length > 1) {
IComponentDescriptor<?> currentCompDesc = queryComponent.getQueryDescriptor();
int i = 0;
for (; sortable && i < propElts.length - 1; i++) {
IReferencePropertyDescriptor<?> refPropDescriptor = ((IReferencePropertyDescriptor<?>) currentCompDesc
.getPropertyDescriptor(propElts[i]));
if (refPropDescriptor != null) {
sortable = sortable && isSortable(refPropDescriptor);
if (EntityHelper.isInlineComponentReference(refPropDescriptor)) {
break;
}
currentCompDesc = refPropDescriptor.getReferencedDescriptor();
} else {
LOG.error("Ordering property {} not found on {}", propElts[i],
currentCompDesc.getComponentContract().getName());
sortable = false;
}
}
if (sortable) {
StringBuilder name = new StringBuilder();
for (int j = i; sortable && j < propElts.length; j++) {
IPropertyDescriptor propDescriptor = currentCompDesc.getPropertyDescriptor(propElts[j]);
sortable = sortable && isSortable(propDescriptor);
if (j < propElts.length - 1) {
currentCompDesc = ((IReferencePropertyDescriptor<?>) propDescriptor).getReferencedDescriptor();
}
if (j > i) {
name.append(".");
}
name.append(propElts[j]);
}
if (sortable) {
propertyName = name.toString();
}
}
} else {
IPropertyDescriptor propertyDescriptor = queryComponent.getQueryDescriptor().getPropertyDescriptor(
propertyName);
if (propertyDescriptor != null) {
sortable = isSortable(propertyDescriptor);
} else {
LOG.error("Ordering property {} not found on {}", propertyName,
queryComponent.getQueryDescriptor().getComponentContract().getName());
sortable = false;
}
}
if (sortable) {
Sort.Order order;
switch (orderingProperty.getValue()) {
case DESCENDING:
order = new Sort.Order(Sort.Direction.DESC, PropertyHelper.toJavaBeanPropertyName(propertyName));
break;
case ASCENDING:
default:
order = new Sort.Order(Sort.Direction.ASC, PropertyHelper.toJavaBeanPropertyName(propertyName));
}
sortOrders.add(order);
}
}
query.with(new Sort(sortOrders));
}
}
private boolean isSortable(IPropertyDescriptor propertyDescriptor) {
return propertyDescriptor != null && (!propertyDescriptor.isComputed()
|| propertyDescriptor.getPersistenceFormula() != null);
}
/**
* {@inheritDoc}
*/
@Override
public Query createQuery(IQueryComponent queryComponent, Map<String, Object> context) {
Query query = new Query();
boolean abort = completeQuery(query, null, queryComponent, context);
if (abort) {
return null;
}
return query;
}
@SuppressWarnings("ConstantConditions")
private boolean completeQuery(Query query, String path, IQueryComponent aQueryComponent,
Map<String, Object> context) {
boolean abort = false;
IComponentDescriptor<?> componentDescriptor = aQueryComponent.getQueryDescriptor();
if (aQueryComponent instanceof ComparableQueryStructure) {
completeQuery(query, createComparableQueryStructureRestriction(path, (ComparableQueryStructure) aQueryComponent,
componentDescriptor, aQueryComponent, context));
} else {
String translationsPath = AbstractComponentDescriptor.getComponentTranslationsDescriptorTemplate().getName();
for (Map.Entry<String, Object> property : aQueryComponent.entrySet()) {
String propertyName = property.getKey();
Object propertyValue = property.getValue();
IPropertyDescriptor propertyDescriptor = componentDescriptor.getPropertyDescriptor(propertyName);
if (propertyDescriptor != null) {
boolean isEntityRef = false;
if (componentDescriptor.isEntity() && aQueryComponent.containsKey(IEntity.ID)) {
isEntityRef = true;
}
if ((!PropertyViewDescriptorHelper.isComputed(componentDescriptor, propertyName) || (
propertyDescriptor instanceof IStringPropertyDescriptor
&& ((IStringPropertyDescriptor) propertyDescriptor).isTranslatable())) && (!isEntityRef || IEntity.ID
.equals(propertyName))) {
String prefixedProperty;
if (path != null) {
prefixedProperty = path + "." + propertyName;
} else {
prefixedProperty = propertyName;
}
if (propertyValue instanceof IEntity) {
if (!((IEntity) propertyValue).isPersistent()) {
abort = true;
} else {
completeQuery(query, where(prefixedProperty).is(propertyValue));
}
} else if (propertyValue instanceof Boolean && (isTriStateBooleanSupported() || (Boolean) propertyValue)) {
completeQuery(query, where(prefixedProperty).is(propertyValue));
} else if (IEntity.ID.equalsIgnoreCase(propertyName)) {
completeQuery(query,
createIdRestriction(propertyDescriptor, prefixedProperty, propertyValue, componentDescriptor,
aQueryComponent, context));
} else if (propertyValue instanceof String) {
completeQueryWithTranslations(query, translationsPath, translationsPath, property, propertyDescriptor,
prefixedProperty, getBackendController(context).getLocale(), componentDescriptor, aQueryComponent,
context);
} else if (propertyValue instanceof Number || propertyValue instanceof Date) {
completeQuery(query, where(prefixedProperty).is(propertyValue));
} else if (propertyValue instanceof EnumQueryStructure) {
completeQuery(query,
createEnumQueryStructureRestriction(prefixedProperty, ((EnumQueryStructure) propertyValue)));
} else if (propertyValue instanceof IQueryComponent) {
IQueryComponent joinedComponent = ((IQueryComponent) propertyValue);
if (!isQueryComponentEmpty(joinedComponent, propertyDescriptor)) {
if (joinedComponent.isInlineComponent()/* || path != null */) {
// the joined component is an inline component so we must use
// dot nested properties. Same applies if we are in a nested
// path i.e. already on an inline component.
abort = abort || completeQuery(query, prefixedProperty, (IQueryComponent) propertyValue, context);
} else {
// the joined component is an entity so we must use
// nested query; unless the autoComplete property
// is a special char.
boolean digDeeper = true;
String autoCompleteProperty = joinedComponent.getQueryDescriptor().getAutoCompleteProperty();
if (autoCompleteProperty != null) {
String val = (String) joinedComponent.get(autoCompleteProperty);
if (val != null) {
boolean negate = false;
if (val.startsWith(IQueryComponent.NOT_VAL)) {
val = val.substring(1);
negate = true;
}
if (IQueryComponent.NULL_VAL.equals(val)) {
Criteria crit;
if (negate) {
crit = where(prefixedProperty).ne(null);
} else {
crit = where(prefixedProperty).is(null);
}
completeQuery(query, crit);
// there might be other restrictions
// digDeeper = false;
}
}
}
if (digDeeper) {
abort = abort || completeQuery(query, prefixedProperty, joinedComponent, context);
}
}
}
} else if (propertyValue != null) {
// Unknown property type. Assume equals.
completeQuery(query, where(prefixedProperty).is(propertyValue));
}
}
}
}
}
return abort;
}
/**
* Complete with criteria.
*
* @param currentQuery
* the current query
* @param criteria
* the criteria
*/
protected void completeQuery(Query currentQuery, Criteria criteria) {
if (criteria != null) {
currentQuery.addCriteria(criteria);
}
}
/**
* Complements a query by processing an enumeration query structure.
*
* @param path
* the path to the comparable property.
* @param enumQueryStructure
* the collection of checked / unchecked enumeration values.
* @return the created criteria or null if no criteria necessary.
*/
protected Criteria createEnumQueryStructureRestriction(String path, EnumQueryStructure enumQueryStructure) {
Set<String> inListValues = new HashSet<>();
boolean nullAllowed = false;
for (EnumValueQueryStructure inListValue : enumQueryStructure.getSelectedEnumerationValues()) {
if (inListValue.getValue() == null || "".equals(inListValue.getValue())) {
nullAllowed = true;
} else {
inListValues.add(inListValue.getValue());
}
}
if (!inListValues.isEmpty()) {
List<Criteria> disjunctions = new ArrayList<>();
disjunctions.add(where(path).in(inListValues));
if (nullAllowed) {
disjunctions.add(where(path).is(null));
}
if (disjunctions.size() == 1) {
return disjunctions.get(0);
}
return where(path).orOperator(disjunctions.toArray(new Criteria[disjunctions.size()]));
}
return null;
}
/**
* Creates an id based restriction.
*
* @param propertyDescriptor
* the id property descriptor.
* @param prefixedProperty
* the full path of the property.
* @param propertyValue
* the string property value.
* @param componentDescriptor
* the component descriptor
* @param queryComponent
* the query component
* @param context
* the context
* @return the created criteria or null if no criteria necessary.
*/
@SuppressWarnings("unchecked")
protected Criteria createIdRestriction(IPropertyDescriptor propertyDescriptor, String prefixedProperty,
Object propertyValue, IComponentDescriptor<?> componentDescriptor,
IQueryComponent queryComponent, Map<String, Object> context) {
String joinedProperty = prefixedProperty.replace('.' + IEntity.ID, "");
if (propertyValue instanceof Collection<?>) {
return Criteria.where(joinedProperty).in((Collection<?>) propertyValue);
} else if (propertyValue instanceof String) {
return createStringRestriction(propertyDescriptor, joinedProperty, (String) propertyValue, componentDescriptor,
queryComponent, context);
} else {
return where(joinedProperty).is(propertyValue);
}
}
/**
* Complete query with translations.
*
* @param currentQuery
* the current query
* @param translationsPath
* the translations path
* @param translationsAlias
* the translations alias
* @param property
* the property
* @param propertyDescriptor
* the property descriptor
* @param prefixedProperty
* the prefixed property
* @param locale
* the locale
* @param componentDescriptor
* the component descriptor
* @param queryComponent
* the query component
* @param context
* the context
*/
@SuppressWarnings("unchecked")
protected void completeQueryWithTranslations(Query currentQuery, String translationsPath, String translationsAlias,
Map.Entry<String, Object> property,
IPropertyDescriptor propertyDescriptor, String prefixedProperty,
Locale locale, IComponentDescriptor<?> componentDescriptor,
IQueryComponent queryComponent, Map<String, Object> context) {
String propertyValue = (String) property.getValue();
propertyValue = sanitizeStringValue(propertyValue);
if (propertyDescriptor instanceof IStringPropertyDescriptor && ((IStringPropertyDescriptor) propertyDescriptor)
.isTranslatable()) {
String nlsOrRawValue = null;
String barePropertyName = property.getKey();
if (property.getKey().endsWith(IComponentDescriptor.NLS_SUFFIX)) {
barePropertyName = barePropertyName.substring(0,
barePropertyName.length() - IComponentDescriptor.NLS_SUFFIX.length());
} else {
nlsOrRawValue = propertyValue;
}
if (propertyValue != null) {
List<Criteria> translationRestriction = new ArrayList<>();
translationRestriction.add(createStringRestriction(
((ICollectionPropertyDescriptor<IPropertyTranslation>) componentDescriptor.getPropertyDescriptor(
translationsPath)).getCollectionDescriptor().getElementDescriptor()
.getPropertyDescriptor(IPropertyTranslation.TRANSLATED_VALUE),
translationsAlias + "." + IPropertyTranslation.TRANSLATED_VALUE, propertyValue, componentDescriptor,
queryComponent, context));
String languagePath = translationsAlias + "." + IPropertyTranslation.LANGUAGE;
translationRestriction.add(where(languagePath).is(locale.getLanguage()));
translationRestriction.add(
where(translationsAlias + "." + IPropertyTranslation.PROPERTY_NAME).is(barePropertyName));
List<Criteria> disjunction = new ArrayList<>();
disjunction.add(
new Criteria().andOperator(translationRestriction.toArray(new Criteria[translationRestriction.size()])));
if (nlsOrRawValue != null) {
List<Criteria> rawValueRestriction = new ArrayList<>();
// No SQL exists equivalent in Mongo...
// rawValueRestriction.add(new Criteria().orOperator(where(translationsPath).is(null), where(languagePath)
// .is(locale.getLanguage())));
String rawPropertyName = barePropertyName + IComponentDescriptor.RAW_SUFFIX;
rawValueRestriction.add(
createStringRestriction(componentDescriptor.getPropertyDescriptor(rawPropertyName), rawPropertyName,
nlsOrRawValue, componentDescriptor, queryComponent, context));
if (rawValueRestriction.size() == 1) {
disjunction.add(rawValueRestriction.get(0));
} else {
disjunction.add(
new Criteria().andOperator(rawValueRestriction.toArray(new Criteria[rawValueRestriction.size()])));
}
}
currentQuery.addCriteria(new Criteria().orOperator(disjunction.toArray(new Criteria[disjunction.size()])));
}
} else {
completeQuery(currentQuery,
createStringRestriction(propertyDescriptor, prefixedProperty, propertyValue, componentDescriptor,
queryComponent, context));
}
}
private String sanitizeStringValue(String propertyValue) {
String sanitizedValue = propertyValue;
if (propertyValue != null) {
for (char specialChar : SPECIAL_CHARS.toCharArray()) {
String toReplace = "\\" + String.valueOf(specialChar);
sanitizedValue = sanitizedValue.replaceAll(toReplace, Matcher.quoteReplacement(toReplace));
}
}
return sanitizedValue;
}
/**
* Creates a string based restriction.
*
* @param propertyDescriptor
* the property descriptor.
* @param prefixedProperty
* the full path of the property.
* @param propertyValue
* the string property value.
* @param componentDescriptor
* the component descriptor
* @param queryComponent
* the query component
* @param context
* the context
* @return the created criteria or null if no criteria necessary.
*/
protected Criteria createStringRestriction(IPropertyDescriptor propertyDescriptor, String prefixedProperty,
String propertyValue, IComponentDescriptor<?> componentDescriptor,
IQueryComponent queryComponent, Map<String, Object> context) {
List<Criteria> disjunctions = new ArrayList<>();
if (propertyValue.length() > 0) {
String[] stringDisjunctions = propertyValue.split(IQueryComponent.DISJUNCT);
for (String stringDisjunction : stringDisjunctions) {
List<Criteria> conjunctions = new ArrayList<>();
String[] stringConjunctions = stringDisjunction.split(IQueryComponent.CONJUNCT);
for (String stringConjunction : stringConjunctions) {
String val = stringConjunction;
if (val.length() > 0) {
Criteria crit;
boolean negate = false;
if (val.startsWith(IQueryComponent.NOT_VAL)) {
val = val.substring(1);
negate = true;
}
if (IQueryComponent.NULL_VAL.equals(val)) {
if (negate) {
crit = where(prefixedProperty).ne(null);
} else {
crit = where(prefixedProperty).is(null);
}
} else {
if (IEntity.ID.equals(propertyDescriptor.getName())
|| propertyDescriptor instanceof IEnumerationPropertyDescriptor) {
if (negate) {
crit = where(prefixedProperty).ne(val);
} else {
crit = where(prefixedProperty).is(val);
}
} else {
crit = createLikeRestriction(propertyDescriptor, prefixedProperty, val, negate, componentDescriptor,
queryComponent, context);
}
}
conjunctions.add(crit);
}
}
int conjunctionCount = conjunctions.size();
if (conjunctionCount == 1) {
disjunctions.add(conjunctions.get(0));
} else {
disjunctions.add(new Criteria().andOperator(conjunctions.toArray(new Criteria[conjunctionCount])));
}
}
}
int disjunctionCount = disjunctions.size();
if (disjunctionCount == 1) {
return disjunctions.get(0);
}
return new Criteria().orOperator(disjunctions.toArray(new Criteria[disjunctionCount]));
}
/**
* Creates a like restriction.
*
* @param propertyDescriptor
* the property descriptor.
* @param prefixedProperty
* the complete property path.
* @param propertyValue
* the value to create the like restriction for
* @param componentDescriptor
* the component descriptor
* @param queryComponent
* the query component
* @param context
* the context
* @return the created criteria or null if no criteria necessary.
*/
@SuppressWarnings("unused")
protected Criteria createLikeRestriction(IPropertyDescriptor propertyDescriptor, String prefixedProperty,
String propertyValue, boolean negate,
IComponentDescriptor<?> componentDescriptor, IQueryComponent queryComponent,
Map<String, Object> context) {
String regex = propertyValue;
if (propertyDescriptor instanceof IStringPropertyDescriptor && ((IStringPropertyDescriptor) propertyDescriptor)
.isUpperCase()) {
regex = regex.toUpperCase();
}
if (!regex.startsWith("%")) {
regex = "^" + regex; // make sure that the index is used
}
regex = regex.replaceAll("%", ".*");
if (!regex.endsWith(".*")) {
regex += ".*";
}
Criteria criteria = where(prefixedProperty);
if (negate) {
criteria = criteria.not();
}
return criteria.regex(regex);
}
/**
* Creates a criteria by processing a comparable query structure.
*
* @param path
* the path to the comparable property.
* @param queryStructure
* the comparable query structure.
* @param componentDescriptor
* the component descriptor
* @param queryComponent
* the query component
* @param context
* the context
* @return the created criteria or null if no criteria necessary.
*/
@SuppressWarnings("unused")
protected Criteria createComparableQueryStructureRestriction(String path, ComparableQueryStructure queryStructure,
IComponentDescriptor<?> componentDescriptor,
IQueryComponent queryComponent,
Map<String, Object> context) {
Criteria queryStructureRestriction = where(path);
if (queryStructure.isRestricting()) {
String comparator = queryStructure.getComparator();
Object infValue = queryStructure.getInfValue();
Object supValue = queryStructure.getSupValue();
Object compareValue = infValue;
if (compareValue == null) {
compareValue = supValue;
}
switch (comparator) {
case ComparableQueryStructureDescriptor.EQ:
queryStructureRestriction.is(compareValue);
break;
case ComparableQueryStructureDescriptor.GT:
queryStructureRestriction.gt(compareValue);
break;
case ComparableQueryStructureDescriptor.GE:
queryStructureRestriction.gte(compareValue);
break;
case ComparableQueryStructureDescriptor.LT:
queryStructureRestriction.lt(compareValue);
break;
case ComparableQueryStructureDescriptor.LE:
queryStructureRestriction.lte(compareValue);
break;
case ComparableQueryStructureDescriptor.NU:
queryStructureRestriction.is(null);
break;
case ComparableQueryStructureDescriptor.NN:
queryStructureRestriction.ne(null);
break;
case ComparableQueryStructureDescriptor.BE:
if (infValue != null && supValue != null) {
queryStructureRestriction.gte(infValue).andOperator(where(path).lte(supValue));
} else if (infValue != null) {
queryStructureRestriction.gte(infValue);
} else {
queryStructureRestriction.lte(supValue);
}
break;
default:
break;
}
}
return queryStructureRestriction;
}
/**
* Whether a query component must be considered empty, thus not generating any
* restriction.
*
* @param queryComponent
* the query component to test.
* @param holdingPropertyDescriptor
* the holding property descriptor or null if none.
* @return true, if the query component does not generate any restriction.
*/
@SuppressWarnings("UnusedParameters")
protected boolean isQueryComponentEmpty(IQueryComponent queryComponent,
IPropertyDescriptor holdingPropertyDescriptor) {
return !queryComponent.isRestricting();
}
/**
* Gets the triStateBooleanSupported.
*
* @return the triStateBooleanSupported.
*/
public boolean isTriStateBooleanSupported() {
return triStateBooleanSupported;
}
/**
* Sets the triStateBooleanSupported.
*
* @param triStateBooleanSupported
* the triStateBooleanSupported to set.
*/
public void setTriStateBooleanSupported(boolean triStateBooleanSupported) {
this.triStateBooleanSupported = triStateBooleanSupported;
}
}