CRUDServiceImpl.java
/*
* Copyright 2020 Global Crop Diversity Trust
*
* 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.gringlobal.service.impl;
import java.lang.reflect.ParameterizedType;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.filerepository.service.RepositoryService;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.api.v1.MultiOp;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.model.AuditedModel;
import org.gringlobal.model.DateVersionEntityId;
import org.gringlobal.model.DateVersionEntityId.EntityIdAndModifiedDate;
import org.gringlobal.model.LazyLoading;
import org.gringlobal.service.CRUDService;
import org.gringlobal.service.JasperReportService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
/**
* The basic FilteredCRUDServiceImpl.
*
* @param <T> the model type
* @param <R> the repository type
*/
@Transactional(readOnly = true)
@Slf4j
public abstract class CRUDServiceImpl<T extends EmptyModel, R extends JpaRepository<T, Long>> implements InitializingBean, CRUDService<T> {
/** The repository. */
@Autowired
protected R repository;
@Autowired
protected JPAQueryFactory jpaQueryFactory;
@Autowired
protected RepositoryService repositoryService;
@Autowired
protected JasperReportService jasperReportService;
@PersistenceContext
protected EntityManager entityManager;
protected Set<String> idSortParams = new HashSet<>();
@Override
public void afterPropertiesSet() throws Exception {
// JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
// this.builder = new PathBuilderFactory().create(domainClass);
idSortParams = getIdSortParams();
}
protected Set<String> getIdSortParams() {
var modelClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
Set<String> idParamNames = new HashSet<>();
ReflectionUtils.doWithFields(modelClass, field -> idParamNames.add(field.getName()), field -> field.getAnnotation(Id.class) != null);
return idParamNames;
}
// Until .lazyLoad() was in an interface
protected T _lazyLoad(T entity) {
if (entity == null) {
return entity;
}
if (entity instanceof LazyLoading<?>) {
((LazyLoading<?>) entity).lazyLoad();
return entity;
} else {
// throw new RuntimeException("Class " + entity.getClass() + " does not implement LazyLoading<> interface. You must provide an implementation");
return entity;
}
}
/**
* Save the new entity
*
* @param source incoming data
* @return the saved entity
*/
@Override
@Transactional
public abstract T create(T source);
/**
* The default MultiOp implementation calls {@link #create(EmptyModel))} for each insert.
* If any one insert fails, the transaction is marked as roll-back only and cannot be used for further writes.
*/
@Override
@Transactional
public MultiOp<T> create(List<T> inserts) {
var result = new MultiOp<T>();
result.success = new ArrayList<T>(inserts.size());
for (T one : inserts) {
result.success.add(this.create(one));
}
return result;
}
/**
* Fetch entity by id, don't lazy load
*
* @param id the id
* @return the entity
*/
@Override
public T get(long id) {
return repository.findById(id).orElseThrow(() -> new NotFoundElement("No record with id=" + id));
}
/**
* Fetch entity by EntityIdAndModifiedDate, check version
*
* @param id the EntityIdAndModifiedDate
* @return the entity
*/
@Override
public T get(EntityIdAndModifiedDate id) {
if (id.modifiedDate == null || id.id == null) {
throw new InvalidApiUsageException("Entity id and modifiedDate must be provided.");
}
T current = get(id.id);
DateVersionEntityId audited = (DateVersionEntityId) current;
// check that target has the same modifiedDate as source
if (audited.getModifiedDate() != null && !audited.getModifiedDate().equals(id.modifiedDate)) {
log.warn("modifiedDate mismatch got={} want={}", id.modifiedDate, audited.getModifiedDate());
throw new ObjectOptimisticLockingFailureException(current.getClass(), id.id);
}
return current;
}
/**
* Fetch entity by id, check version
*
* @param id the id
* @param modifiedDate the modifiedDate
* @return the entity
*/
@Override
public T get(long id, Instant modifiedDate) {
return get(new EntityIdAndModifiedDate(id, modifiedDate));
}
/**
* Re-get the entity based on identifiers in the source.
*
* @param source the source entity
* @return the reloaded entity, lazy-loading included
*/
public T get(T source) {
if (source.getId() == null) {
throw new InvalidApiUsageException("Entity id must be provided.");
}
T current;
if (source instanceof AuditedModel) {
current = get(source.getId(), ((AuditedModel) source).getModifiedDate());
} else {
current = get(source.getId());
}
return current;
}
/**
* Load entity by id and lazy load.
*
* @param id the id
* @return the entity, lazy-loading included
*/
@Override
public T load(long id) {
T entity = get(id);
return _lazyLoad(entity);
}
/**
* Re-load the entity based on identifiers in the source.
*
* @param source the source entity
* @return the reloaded entity, lazy-loading included
*/
@Override
public final T reload(T source) {
return _lazyLoad(get(source));
}
/**
* List records.
*
* @param page Pageable
* @return the page
*/
@Override
public Page<T> list(Pageable page) {
page = Pagination.addSortByParams(page, idSortParams);
return repository.findAll(page);
}
/**
* Save entity.
*
* @param updated data
* @param target current entity
* @return the updated entity
*/
@Override
@Transactional
public abstract T update(T updated, T target);
@Override
@Transactional
public T update(T updated) {
entityManager.detach(updated); // Ensure that EM does not reuse incoming entity
T target = get(updated);
return update(updated, target);
}
/**
* The default MultiOp update implementation calls {@link #update(EmptyModel))} for each update.
* If any one update fails, the transaction is marked as roll-back only and cannot be used for further writes.
*/
@Override
@Transactional
public MultiOp<T> update(List<T> updates) {
var result = new MultiOp<T>();
result.success = new ArrayList<T>(updates.size());
for (T one : updates) {
result.success.add(this.update(one));
}
return result;
}
/**
* Removes the record.
*
* @param entity record to delete
* @return the removed entity
*/
@Override
@Transactional
public T remove(T entity) {
if (entity == null) {
throw new InvalidApiUsageException("Entity must be provided.");
}
entity = get(entity); // Remove, the entity should have the correct id + version!
repository.delete(entity); // FIXME for AuditedModel this should be automatic!
// FIXME clear id?
// entity.setId(null);
return entity;
}
/**
* The default MultiOp delete implementation calls {@link #remove(EmptyModel))} for each update.
* If any one delete fails, the transaction is marked as roll-back only and cannot be used for further writes.
*/
@Override
@Transactional
public MultiOp<T> remove(List<T> deletes) {
var result = new MultiOp<T>();
result.success = new ArrayList<T>(deletes.size());
for (T one : deletes) {
result.success.add(this.remove(one));
}
return result;
}
/**
* Get a JPA query that (optionally) includes lazy loaded data.
* <example><code>jpaQueryFactory.selectFrom(QTaxonomySpecies.taxonomySpecies).join(...).fetchJoin()</code></example>
*
* @return a custom JPA query for T
*/
protected JPAQuery<T> entityListQuery() {
return null;
}
/**
* The Entity id predicate of {link {@link #entityListQuery()}
* <example><code>QTaxonomySpecies.taxonomySpecies.id</code></example>
*
* @return the number path
*/
protected NumberPath<Long> entityIdPredicate() {
throw new UnsupportedOperationException("You must implement #entityIdPredicate() if you declare #entityListQuery()");
}
/**
* Get a list of entities with specified IDs in the order specified (if incoming collection of IDs is ordered).
* Override this method to lazy load more than default data.
*
* @param entityIds the entity ids
* @return the list
*/
public List<T> list(Collection<Long> entityIds) {
List<T> unsorted;
if (entityListQuery() != null) {
unsorted = entityListQuery().where(entityIdPredicate().in(entityIds)).fetch();
} else {
unsorted = repository.findAllById(entityIds);
}
if (entityIds instanceof List<?>) {
List<Long> idList = (List<Long>) entityIds;
unsorted.sort((e1, e2) -> idList.indexOf(e1.getId()) - idList.indexOf(e2.getId()));
}
return unsorted;
}
@Override
public ResponseEntity<StreamingResponseBody> generateReport(String reportTemplate, Set<Long> entityIds, Locale locale, HttpServletResponse response) throws Exception {
String entityName = getServiceEntityName();
List<T> entities = repository.findAllById(entityIds);
entities.forEach(this::_lazyLoad);
return jasperReportService.generatePdfReport(entityName, reportTemplate, Arrays.asList(entities.toArray()), locale, null);
}
@Override
public String getServiceEntityName() {
return ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]).getSimpleName();
}
}