/*
* Copyright 2015 herd contributors
*
* 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 org.finra.herd.dao.impl;
import java.sql.Timestamp;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.BaseJpaDao;
import org.finra.herd.model.dto.ConfigurationValue;
import org.finra.herd.model.jpa.ConfigurationEntity;
/**
* A generic JPA DAO that can be used directly or as a base class for more specialized DAO's.
*/
@Repository
public class BaseJpaDaoImpl implements BaseJpaDao
{
@PersistenceContext
protected EntityManager entityManager;
@Autowired
private ConfigurationHelper configurationHelper;
@Override
public <T> void delete(T entity)
{
Validate.notNull(entity);
entityManager.remove(entity);
// Flush to persist so we ensure that data integrity violations, etc. are thrown here rather than at the time of transaction commit.
entityManager.flush();
}
@Override
public <T> void detach(T entity)
{
Validate.notNull(entity);
entityManager.detach(entity);
}
@Override
public <T> List<T> findAll(Class<T> entityClass)
{
Validate.notNull(entityClass);
return query("select type from " + StringUtils.unqualify(entityClass.getName()) + " type");
}
@Override
public <T> T findById(Class<T> entityClass, Object entityId)
{
Validate.notNull(entityClass);
Validate.notNull(entityId);
return entityManager.find(entityClass, entityId);
}
@Override
public <T> List<T> findByNamedProperties(Class<T> entityClass, Map<String, ?> params)
{
Validate.notNull(entityClass);
Validate.notEmpty(params);
StringBuilder queryStringBuilder = new StringBuilder("select type from " + StringUtils.unqualify(entityClass.getName()) + " type where ");
Iterator<String> iterator = params.keySet().iterator();
int index = 0;
while (iterator.hasNext())
{
String propertyName = iterator.next();
if (index > 0)
{
queryStringBuilder.append(" and ");
}
queryStringBuilder.append("(type.").append(propertyName).append(" = :").append(propertyName).append(')');
index++;
}
return queryByNamedParams(queryStringBuilder.toString(), params);
}
@Override
public <T> T findUniqueByNamedProperties(Class<T> entityClass, Map<String, ?> params)
{
Validate.notNull(entityClass);
Validate.notEmpty(params);
List<T> resultList = findByNamedProperties(entityClass, params);
Validate.isTrue(resultList.size() < 2,
"Found more than one persistent instance of type " + StringUtils.unqualify(entityClass.getName() + " with parameters " + params.toString()));
return resultList.size() == 1 ? resultList.get(0) : null;
}
@Override
public Timestamp getCurrentTimestamp()
{
// Create the criteria builder and the criteria.
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Timestamp> criteria = builder.createQuery(Timestamp.class);
// Add the clauses for the query.
criteria.select(builder.currentTimestamp()).from(ConfigurationEntity.class);
return entityManager.createQuery(criteria).getSingleResult();
}
@Override
public EntityManager getEntityManager()
{
return entityManager;
}
@SuppressWarnings({"unchecked"})
@Override
public <T> List<T> query(String queryString)
{
Validate.notEmpty(queryString);
return entityManager.createQuery(queryString).getResultList();
}
@Override
public <T> List<T> queryByNamedParams(String queryString, Map<String, ?> params)
{
Validate.notEmpty(queryString);
Validate.notEmpty(params);
return executeQueryWithNamedParams(entityManager.createQuery(queryString), params);
}
@Override
public <T> T save(T entity)
{
Validate.notNull(entity);
entityManager.persist(entity);
return entity;
}
@Override
public <T> T saveAndRefresh(T entity)
{
// Save the entity.
save(entity);
// Flush (i.e. persist) the entity and re-load it to retrieve the create/update date that was populated by the database.
entityManager.flush();
entityManager.refresh(entity);
// Return the persisted entity.
return entity;
}
/**
* Executes query, validates if result list contains no more than record and returns the query result.
*
* @param <T> The type of the root entity class
* @param criteria the criteria select query to be executed
* @param message the exception message to use if the query returns fails
*
* @return the query result or null if 0 records were selected
*/
protected <T> T executeSingleResultQuery(CriteriaQuery<T> criteria, String message)
{
List<T> resultList = entityManager.createQuery(criteria).getResultList();
// Validate that the query returned no more than one record.
Validate.isTrue(resultList.size() < 2, message);
return resultList.size() == 1 ? resultList.get(0) : null;
}
/**
* Gets an "in" clause predicate for a list of values. This will take care of breaking the list of values into a group of sub-lists where each sub-list is
* placed in a separate "in" clause and all "in" clauses are "or"ed together. The size of each sub-list is obtained through an environment configuration.
*
* @param builder the criteria builder.
* @param path the path to the field that is being filtered.
* @param values the list of values to place in the in clause.
* @param <T> the type referenced by the path.
*
* @return the predicate for the in clause.
*/
protected <T> Predicate getPredicateForInClause(CriteriaBuilder builder, Path<T> path, List<T> values)
{
// Get the chunk size from the environment and use a default as necessary.
int inClauseChunkSize = configurationHelper.getProperty(ConfigurationValue.DB_IN_CLAUSE_CHUNK_SIZE, Integer.class);
// Initializes the returned predicate and the value list size.
Predicate predicate = null;
int listSize = values.size();
// Loop through each chunk of values until we have reached the end of the values.
for (int i = 0; i < listSize; i += inClauseChunkSize)
{
// Get a sub-list for the current chunk of data.
List<T> valuesSubList = values.subList(i, (listSize > (i + inClauseChunkSize) ? (i + inClauseChunkSize) : listSize));
// Get an updated predicate which will be the "in" clause of the sub-list on the first loop or the "in" clause of the sub-list "or"ed with the\
// previous sub-list "in" clause.
predicate = (predicate == null ? path.in(valuesSubList) : builder.or(predicate, path.in(valuesSubList)));
}
// Return the "in" clause predicate.
return predicate;
}
/**
* Executes a query with named parameters and returns the result list.
*
* @param query the query to execute.
* @param params the named parameters.
*
* @return the list of results.
*/
@SuppressWarnings({"unchecked"})
private <T> List<T> executeQueryWithNamedParams(Query query, Map<String, ?> params)
{
Validate.notNull(query);
Validate.notNull(params);
for (Map.Entry<String, ?> entry : params.entrySet())
{
query.setParameter(entry.getKey(), entry.getValue());
}
return query.getResultList();
}
}