/*
* Copyright 2012 Adaptrex, LLC
*
* Licensed 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 com.adaptrex.core.persistence.jpa;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.adaptrex.core.ext.ExtConfig;
import com.adaptrex.core.ext.ExtTypeFormatter;
import com.adaptrex.core.ext.Filter;
import com.adaptrex.core.ext.ModelInstance;
import com.adaptrex.core.ext.Sorter;
import com.adaptrex.core.persistence.api.AdaptrexEntityType;
import com.adaptrex.core.persistence.api.BaseStoreData;
import com.adaptrex.core.persistence.api.AdaptrexStoreData;
public class JPAStoreData extends BaseStoreData implements AdaptrexStoreData {
private ExtConfig extConfig;
private static Logger log = Logger.getLogger(JPAStoreData.class);
public JPAStoreData(ExtConfig extConfig) {
this.extConfig = extConfig;
}
@SuppressWarnings("unchecked")
@Override
public List<Map<String, Object>> getData() throws Exception {
List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();
Class<?> clazz = extConfig.getEntityClass();
JPAPersistenceManager jpa = (JPAPersistenceManager) extConfig.getORMPersistenceManager();
EntityManager em = jpa.getEntityManager();
AdaptrexEntityType adaptrexEntity = jpa.getAdaptrexEntity(clazz.getSimpleName());
/*
* If we have a where clause, fall back to JPQL
*/
if (this.getWhere() != null && !this.getWhere().isEmpty()) {
return getWhereFallback(jpa, em, clazz, data);
}
try {
/*
* Get the criteria builder
*/
CriteriaBuilder builder = em.getCriteriaBuilder();
/*
* Create the CriteriaQuery and it's Root
*/
CriteriaQuery<?> query = builder.createQuery(clazz);
Root<?> root = query.from(clazz);
/*
* Create the list of predicates to hold our individual filters
*/
List<Predicate> predicates = new ArrayList<Predicate>();
for (Filter filter : this.getFilters()) {
/*
* Get the value of the filter
*/
Object val = filter.getValue();
if (val == null || String.valueOf(val).isEmpty()) continue;
/*
* Get the property to filter
*/
String key = filter.getProperty();
String propertyName = key;
String extFieldType = null;
Path<String> currentNode = (Path<String>) root;
if (key.contains(".")) {
propertyName = StringUtils.substringAfterLast(key, ".");
key = StringUtils.substringBeforeLast(key, ".");
String[] propParts = StringUtils.split(key, ".");
for (String propPart : propParts) {
currentNode = currentNode.get(propPart);
}
extFieldType = jpa.getAdaptrexEntity(currentNode.getJavaType().getSimpleName())
.getField(propertyName).getFieldDefinition().getType();
} else {
extFieldType = adaptrexEntity.getField(propertyName).getFieldDefinition().getType();
}
/*
* Get an expression representing the entity property we're filtering
*/
Expression<?> path = null;
try {
path = currentNode.get(propertyName);
} catch (Exception e) {
log.warn("Adaptrex Store Error: Filter: Couldn't find property for " + filter.getProperty());
continue;
}
/*
* String filter
*
* Should be consistent with ExtJS filtering
* By default, the settings are: exactMatch:false, caseSensitive:false, anyMatch:false
*/
if (extFieldType.equals(ExtTypeFormatter.STRING)) {
String stringVal = String.valueOf(val);
/*
* If we're not doing a case sensitive filter, normalize both
* the path and the value we're comparing
*/
if (!filter.getCaseSensitive()) {
path = builder.lower((Expression<String>) path);
val = stringVal.toLowerCase();
}
/*
* If we've got an "or" get the parts
*/
String[] stringOrParts = stringVal.contains("||")
? StringUtils.split(stringVal, "||")
: null;
/*
* If exactMatch = true, we need to check for absolute equality
*/
if (filter.getExactMatch()) {
if (stringOrParts != null) {
List<Predicate> orParts = new ArrayList<Predicate>();
for (String orPart : stringOrParts) {
orParts.add(builder.equal(path, orPart));
}
predicates.add(builder.or(orParts.toArray(new Predicate[orParts.size()])));
} else {
predicates.add(builder.equal(path, stringVal));
}
/*
* If anyMatch = true, check for a match anywhere in the string
*/
} else if (filter.getAnyMatch()) {
if (stringOrParts != null) {
List<Predicate> orParts = new ArrayList<Predicate>();
for (String orPart : stringOrParts) {
orParts.add(builder.like((Expression<String>) path, "%" + orPart + "%"));
}
predicates.add(builder.or(orParts.toArray(new Predicate[orParts.size()])));
} else {
predicates.add(builder.like((Expression<String>) path, "%" + stringVal + "%"));
}
/*
* Otherwise match just the beginning of the string
*/
} else {
if (stringOrParts != null) {
List<Predicate> orParts = new ArrayList<Predicate>();
for (String orPart : stringOrParts) {
orParts.add(builder.like((Expression<String>) path, orPart + "%"));
}
predicates.add(builder.or(orParts.toArray(new Predicate[orParts.size()])));
} else {
predicates.add(builder.like((Expression<String>) path, stringVal + "%"));
}
}
/*
* Boolean filter
*/
} else if (extFieldType.equals(ExtTypeFormatter.BOOLEAN)) {
predicates.add(builder.equal(path, String.valueOf(val).toLowerCase().equals("true")));
/*
* Integer filter
*
* Inline stores allow flexibility not directly available in Ext. Since we
* control the component tags, we can test for various additional conditions.
* Since this is server side only, local filtering should not also be applied
*/
} else if (extFieldType.equals(ExtTypeFormatter.INT)) {
Expression<Integer> numPath = (Expression<Integer>) path;
Integer numVal = null;
try {
numVal = (val instanceof Integer)
? (Integer) val
: Integer.valueOf(String.valueOf(val));
} catch (Exception e) {
log.warn("Adaptrex Store Error: Filter: Couldn't parse integer: " + val);
continue;
}
/*
* Filter based on various conditions
*/
Integer t = filter.getFilterType();
if (t == Filter.EQUAL) {
predicates.add(builder.equal(numPath, numVal));
} else if (t == Filter.NOT_EQUAL) {
predicates.add(builder.notEqual(numPath, numVal));
} else if (t == Filter.LESS_THAN) {
predicates.add(builder.lt(numPath, numVal));
} else if (t == Filter.LESS_THAN_EQUAL) {
predicates.add(builder.le(numPath, numVal));
} else if (t == Filter.GREATER_THAN) {
predicates.add(builder.gt(numPath, numVal));
} else if (t == Filter.GREATER_THAN_EQUAL) {
predicates.add(builder.ge(numPath, numVal));
}
/*
* Float filter
*
* Similar to integer filter in that we can test for additional conditions
* not avaialable in standard Ext filtering. Do not rely on these filters
* to work as local filters. Also, since we can't currently test for float
* equality, we should only be checking for less than or greater than at
* this time.
*
* In the future, we may add additional filter settings which would allow
* us to test for float equality up to a certain precision.
*/
} else if (extFieldType.equals(ExtTypeFormatter.FLOAT)) {
Expression<Double> numPath = (Expression<Double>) path;
Double numVal = null;
try {
numVal = (val instanceof Double)
? (Double) val
: Double.valueOf(String.valueOf(val));
} catch (Exception e) {
log.warn("Adaptrex Store Error: Filter: Couldn't parse float: " + val);
continue;
}
Integer t = filter.getFilterType();
if (t == Filter.EQUAL) {
log.warn("Adaptrex Store Error: Filter: Float equality not implemented: " + val);
continue;
} else if (t == Filter.NOT_EQUAL) {
log.warn("Adaptrex Store Error: Filter: Float inequality not implemented: " + val);
continue;
} else if (t == Filter.LESS_THAN || t == Filter.LESS_THAN_EQUAL) {
predicates.add(builder.le(numPath, numVal));
} else if (t == Filter.GREATER_THAN || t == Filter.GREATER_THAN_EQUAL) {
predicates.add(builder.ge(numPath, numVal));
}
/*
* Date filter
*
* Also simililar to integer. We can test for several date comparison
* conditions not available in standard Ext filtering.
*/
} else if (extFieldType.equals(ExtTypeFormatter.DATE)) {
Expression<Date> datePath = (Expression<Date>) path;
Date dateVal = (Date) ExtTypeFormatter.parse(val, Date.class);
Integer t = filter.getFilterType();
if (t == Filter.EQUAL) {
predicates.add(builder.equal(datePath, dateVal));
} else if (t == Filter.NOT_EQUAL) {
predicates.add(builder.notEqual(datePath, dateVal));
} else if (t == Filter.LESS_THAN) {
predicates.add(builder.lessThan(datePath, dateVal));
} else if (t == Filter.LESS_THAN_EQUAL) {
predicates.add(builder.lessThanOrEqualTo(datePath, dateVal));
} else if (t == Filter.GREATER_THAN) {
predicates.add(builder.greaterThan(datePath, dateVal));
} else if (t == Filter.GREATER_THAN_EQUAL) {
predicates.add(builder.greaterThanOrEqualTo(datePath, dateVal));
}
}
}
/*
* Package all the filters into a final predicate
*/
Predicate finalPredicate = builder.and((Predicate[]) predicates.toArray((new Predicate[predicates.size()])));
query.where(finalPredicate);
/*
* Add sorters to the query
*/
List<String> spentSort = new ArrayList<String>();
List<Order> order = new ArrayList<Order>();
this.getGroupers().addAll(this.getSorters());
for (Sorter sorter : this.getGroupers()) {
String prop = sorter.getProperty();
if (spentSort.contains(prop)) continue;
spentSort.add(prop);
String[] propParts = StringUtils.split(prop, ".");
Path<String> currentNode = (Path<String>) root;
for (String propPart : propParts) {
currentNode = currentNode.get(propPart);
}
String d = sorter.getDirection();
boolean isDesc = d != null && d.toLowerCase().equals("desc");
order.add(isDesc ? builder.desc(currentNode) : builder.asc(currentNode));
}
query.orderBy(order);
/*
* Create a typed query from the criteria query, set start/limit and process results
*/
TypedQuery<?> typedQuery = em.createQuery(query);
if (this.getLimit() != null) typedQuery.setMaxResults(this.getLimit());
if (this.getStart() != null) typedQuery.setFirstResult(this.getStart());
for (Object entity : typedQuery.getResultList()) {
data.add(new ModelInstance(extConfig, entity).getData());
}
/*
* Create a CriteriaQuery used to get the total count of records in the resultset
*/
CriteriaQuery<Long> queryCount = builder.createQuery(Long.class);
queryCount.select(builder.count(queryCount.from(clazz)));
queryCount.where(finalPredicate);
this.setTotalCount(em.createQuery(queryCount).getSingleResult().longValue());
} catch (Exception e) {
throw new Exception(e);
} finally {
em.close();
}
return data;
}
private List<Map<String, Object>> getWhereFallback(
JPAPersistenceManager jpa, EntityManager em, Class<?> clazz, List<Map<String, Object>> data) throws Exception {
String className = clazz.getSimpleName();
String alias = className.toLowerCase().substring(0, 1);
/*
* Start the JPQL
*/
StringBuffer jpql = new StringBuffer();
jpql.append("SELECT " + alias + " FROM " + className + " " + alias + " ");
jpql.append("WHERE " + this.getWhere());
StringBuffer jpqlCount = new StringBuffer();
jpqlCount.append("SELECT COUNT (" + alias + ") FROM " + className + " " + alias + " ");
jpqlCount.append("WHERE " + this.getWhere());
/*
* ExtJS sends sorting parameters along with grouping parameters
* for the same grouper. Unsure if this behavior makes sense or
* may change in the future. Instead, we should filter out the
* duplicate sort.
*/
List<String> spentSort = new ArrayList<String>();
String sortClause = "";
this.getGroupers().addAll(this.getSorters());
if (this.getGroupers().size() > 0) {
for (Sorter sorter : this.getGroupers()) {
String prop = sorter.getProperty();
if (spentSort.contains(prop)) continue;
spentSort.add(prop);
if (!sortClause.isEmpty()) sortClause += ",";
sortClause += alias + "." + prop + " " + sorter.getDirection();
}
}
if (!sortClause.isEmpty()) {
jpql.append(" ORDER BY " + sortClause);
}
/*
* Create the JPQL typed query
*/
Query query = em.createQuery(jpql.toString(), clazz);
Query queryCount = em.createQuery(jpqlCount.toString(), Long.class);
/*
* Add any named parameters
*/
Map<String,Object> params = this.getParams();
for (String key : params.keySet()) {
Class<?> paramType = query.getParameter(key).getParameterType();
Object paramObj = ExtTypeFormatter.parse(params.get(key), paramType);
query.setParameter(key, paramObj);
queryCount.setParameter(key, paramObj);
}
if (this.getLimit() != null) query.setMaxResults(this.getLimit());
if (this.getStart() != null) query.setFirstResult(this.getStart().intValue());
for (Object entity : query.getResultList()) {
data.add(new ModelInstance(extConfig, entity).getData());
}
/*
* Get the initial list before applying limits
*/
this.setTotalCount((Long) queryCount.getSingleResult());
return data;
}
}