io.katharsis.jpa.JpaModule Maven / Gradle / Ivy
package io.katharsis.jpa;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.metamodel.ManagedType;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.katharsis.internal.boot.TransactionRunner;
import io.katharsis.dispatcher.filter.AbstractFilter;
import io.katharsis.dispatcher.filter.FilterChain;
import io.katharsis.dispatcher.filter.FilterRequestContext;
import io.katharsis.jpa.internal.JpaResourceInformationBuilder;
import io.katharsis.jpa.internal.OptimisticLockExceptionMapper;
import io.katharsis.jpa.internal.meta.MetaAttribute;
import io.katharsis.jpa.internal.meta.MetaDataObject;
import io.katharsis.jpa.internal.meta.MetaElement;
import io.katharsis.jpa.internal.meta.MetaEntity;
import io.katharsis.jpa.internal.meta.MetaLookup;
import io.katharsis.jpa.internal.meta.MetaType;
import io.katharsis.jpa.internal.meta.impl.MetaResourceImpl;
import io.katharsis.jpa.mapping.JpaMapper;
import io.katharsis.jpa.mapping.JpaMapping;
import io.katharsis.jpa.query.JpaQueryFactory;
import io.katharsis.jpa.query.JpaQueryFactoryContext;
import io.katharsis.jpa.query.criteria.JpaCriteriaQueryFactory;
import io.katharsis.module.Module;
import io.katharsis.queryspec.QuerySpecRelationshipRepository;
import io.katharsis.queryspec.QuerySpecResourceRepository;
import io.katharsis.resource.information.ResourceInformationBuilder;
import io.katharsis.resource.registry.ResourceLookup;
import io.katharsis.response.BaseResponseContext;
import io.katharsis.utils.PreconditionUtil;
/**
* Katharsis module that adds support to expose JPA entities as repositories. It
* supports:
*
*
* - Sorting
* - Filtering
* - Access to relationships for any operation (sorting, filtering, etc.)
* - Includes for relationships
* - Paging
* - Mapping to DTOs
* - Criteria API and QueryDSL support
* - Computated attributes that map JPA Criteria/QueryDSL expressions to DTO attributes
* - JpaRepositoryFilter to customize the repositories
* - Client and server support
* - No need for katharsis annotations by default. Reads the entity annotations.
*
*
*
* Not supported so far:
*
*
* - Selection of fields, always all fields are returned.
* - Sorting and filtering on related resources. Consider doing separate
* requests on the relations where necessary.
*
*
*/
public class JpaModule implements Module {
private Logger logger = LoggerFactory.getLogger(JpaModule.class);
private static final String MODULE_NAME = "jpa";
private String resourceSearchPackage;
private EntityManagerFactory emFactory;
private EntityManager em;
private JpaQueryFactory queryFactory;
private ResourceInformationBuilder resourceInformationBuilder;
private TransactionRunner transactionRunner;
private ModuleContext context;
private MetaLookup metaLookup = new MetaLookup();
private HashSet> entityClasses = new HashSet<>();
private Map, MappedRegistration, ?>> mappings = new HashMap<>();
private JpaRepositoryFactory repositoryFactory;
private List filters = new CopyOnWriteArrayList<>();
/**
* Constructor used on client side.
*/
private JpaModule(String resourceSearchPackage) {
this.resourceSearchPackage = resourceSearchPackage;
Reflections reflections;
if (resourceSearchPackage != null) {
String[] packageNames = resourceSearchPackage.split(",");
Object[] objPackageNames = new Object[packageNames.length];
System.arraycopy(packageNames, 0, objPackageNames, 0, packageNames.length);
reflections = new Reflections(objPackageNames);
}
else {
reflections = new Reflections(resourceSearchPackage);
}
this.entityClasses.addAll(reflections.getTypesAnnotatedWith(Entity.class));
}
/**
* Constructor used on server side.
*/
private JpaModule(EntityManagerFactory emFactory, EntityManager em, TransactionRunner transactionRunner) {
this.emFactory = emFactory;
this.em = em;
this.transactionRunner = transactionRunner;
setQueryFactory(JpaCriteriaQueryFactory.newInstance());
if (emFactory != null) {
Set> managedTypes = emFactory.getMetamodel().getManagedTypes();
for (ManagedType> managedType : managedTypes) {
Class> managedJavaType = managedType.getJavaType();
MetaElement meta = metaLookup.getMeta(managedJavaType);
if (meta instanceof MetaEntity) {
entityClasses.add(managedJavaType);
}
}
}
this.setRepositoryFactory(new DefaultJpaRepositoryFactory());
}
/**
* Creates a new JpaModule for a Katharsis client.
*
* @param resourceSearchPackage where to find the entity classes.
* @return module
*/
public static JpaModule newClientModule(String resourceSearchPackage) {
return new JpaModule(resourceSearchPackage);
}
/**
* Creates a new JpaModule for a Katharsis server. No entities are
* by default exposed as JSON API resources. Make use of
* {@link #addEntityClass(Class)} andd {@link #addMappedEntityClass(Class, Class, JpaMapper)}
* to add resources.
*
* @param em to use
* @param transactionRunner to use
* @return created module
*/
public static JpaModule newServerModule(EntityManager em, TransactionRunner transactionRunner) {
return new JpaModule(null, em, transactionRunner);
}
/**
* Creates a new JpaModule for a Katharsis server. All entities managed by
* the provided EntityManagerFactory are registered to the module
* and exposed as JSON API resources if not later configured otherwise.
*
* @param emFactory to retrieve the managed entities.
* @param em to use
* @param transactionRunner to use
* @return created module
*/
public static JpaModule newServerModule(EntityManagerFactory emFactory, EntityManager em,
TransactionRunner transactionRunner) {
return new JpaModule(emFactory, em, transactionRunner);
}
/**
* Adds the given filter to this module. Filter will be used by all repositories managed by this module.
*
* @param filter to add
*/
public void addFilter(JpaRepositoryFilter filter) {
filters.add(filter);
}
/**
* Removes the given filter to this module.
*
* @param filter to remove
*/
public void removeFilter(JpaRepositoryFilter filter) {
filters.remove(filter);
}
/**
* @return all filters
*/
public List getFilters() {
return filters;
}
public MetaLookup getMetaLookup() {
return metaLookup;
}
public void setRepositoryFactory(JpaRepositoryFactory repositoryFactory) {
checkNotInitialized();
this.repositoryFactory = repositoryFactory;
}
/**
* @return set of entity classes made available as repository.
*/
public Set> getEntityClasses() {
return Collections.unmodifiableSet(entityClasses);
}
/**
* Adds the given entity class to expose the entity as repository.
*
* @param entityClass to expose as repository
*/
public void addEntityClass(Class> entityClass) {
checkNotInitialized();
entityClasses.add(entityClass);
}
/**
* Adds the given entity class which is mapped to a DTO with the provided mapper.
*
* @param entity class
* @param dto class
* @param entityClass to add as repository
* @param dtoClass to map the entity to
* @param mapper to use to map the entity to the dto
*/
public void addMappedEntityClass(Class entityClass, Class dtoClass, JpaMapper mapper) {
checkNotInitialized();
if (mappings.containsKey(dtoClass)) {
throw new IllegalArgumentException(dtoClass.getName() + " is already registered");
}
mappings.put(dtoClass, new MappedRegistration<>(entityClass, dtoClass, mapper));
}
/**
* Adds the given entity class which is mapped to a DTO with the provided mapper.
*
* @param dto type
* @param dtoClass to remove
*/
public void removeMappedEntityClass(Class dtoClass) {
checkNotInitialized();
mappings.remove(dtoClass);
}
private static class MappedRegistration implements JpaMapping {
Class entityClass;
Class dtoClass;
JpaMapper mapper;
MappedRegistration(Class entityClass, Class dtoClass, JpaMapper mapper) {
this.entityClass = entityClass;
this.dtoClass = dtoClass;
this.mapper = mapper;
}
@Override
public Class getEntityClass() {
return entityClass;
}
@Override
public Class getDtoClass() {
return dtoClass;
}
@Override
public JpaMapper getMapper() {
return mapper;
}
}
/**
* Removes the given entity class to not expose the entity as repository.
*
* @param entityClass to remove
*/
public void removeEntityClass(Class> entityClass) {
checkNotInitialized();
entityClasses.remove(entityClass);
}
/**
* Removes all entity classes registered by default. Use {@link #addEntityClass(Class)} or
* {@link #addMappedEntityClass(Class, Class, JpaMapper)} to register classes manually.
*/
public void removeAllEntityClasses() {
checkNotInitialized();
entityClasses.clear();
}
@Override
public String getModuleName() {
return MODULE_NAME;
}
private void checkNotInitialized() {
PreconditionUtil.assertNull("module is already initialized, no further changes can be performed", context);
}
@Override
public void setupModule(ModuleContext context) {
this.context = context;
context.addResourceInformationBuilder(getResourceInformationBuilder());
context.addExceptionMapper(new OptimisticLockExceptionMapper());
if (resourceSearchPackage != null) {
setupClientResourceLookup();
}
else {
context.addResourceLookup(new MappingsResourceLookup());
setupServerRepositories();
setupTransactionMgmt();
}
}
/**
* Makes all the mapped DTO classes available to katharsis.
*/
private class MappingsResourceLookup implements ResourceLookup {
@Override
public Set> getResourceClasses() {
return Collections.unmodifiableSet(mappings.keySet());
}
@Override
public Set> getResourceRepositoryClasses() {
return Collections.emptySet();
}
}
protected void setupTransactionMgmt() {
context.addFilter(new AbstractFilter() {
@Override
public BaseResponseContext filter(final FilterRequestContext context, final FilterChain chain) {
return transactionRunner.doInTransaction(new Callable() {
@Override
public BaseResponseContext call() throws Exception {
return chain.doFilter(context);
}
});
}
});
}
private void setupClientResourceLookup() {
context.addResourceLookup(new JpaEntityResourceLookup(resourceSearchPackage));
}
public class JpaEntityResourceLookup implements ResourceLookup {
public JpaEntityResourceLookup(String packageName) {
}
@Override
public Set> getResourceClasses() {
return entityClasses;
}
@Override
public Set> getResourceRepositoryClasses() {
return Collections.emptySet();
}
}
private void setupServerRepositories() {
for (Class> entityClass : entityClasses) {
MetaElement meta = metaLookup.getMeta(entityClass);
setupRepository(meta);
}
for (MappedRegistration, ?> mapping : mappings.values()) {
setupMappedRepository(mapping);
}
}
private void setupMappedRepository(MappedRegistration, ?> mapping) {
MetaEntity metaEntity = metaLookup.getMeta(mapping.getEntityClass()).asEntity();
if (isValidEntity(metaEntity)) {
QuerySpecResourceRepository, ?> repository = filterResourceCreation(mapping.getDtoClass(),
repositoryFactory.createEntityRepository(this, mapping.getDtoClass()));
context.addRepository(mapping.getDtoClass(), repository);
setupRelationshipRepositories(mapping.getDtoClass());
}
}
private QuerySpecResourceRepository, ?> filterResourceCreation(Class> resourceClass,
JpaEntityRepository, ?> repository) {
JpaEntityRepository, ?> filteredRepository = repository;
for (JpaRepositoryFilter filter : filters) {
if (filter.accept(resourceClass)) {
filteredRepository = filter.filterCreation(filteredRepository);
}
}
return filteredRepository;
}
private QuerySpecRelationshipRepository, ?, ?, ?> filterRelationshipCreation(Class> resourceClass,
JpaRelationshipRepository, ?, ?, ?> repository) {
JpaRelationshipRepository, ?, ?, ?> filteredRepository = repository;
for (JpaRepositoryFilter filter : filters) {
if (filter.accept(resourceClass)) {
filteredRepository = filter.filterCreation(filteredRepository);
}
}
return filteredRepository;
}
@SuppressWarnings({ "rawtypes" })
private void setupRepository(MetaElement meta) {
if (!(meta instanceof MetaEntity))
return;
MetaEntity metaEntity = meta.asEntity();
if (isValidEntity(metaEntity)) {
Class> resourceClass = metaEntity.getImplementationClass();
QuerySpecResourceRepository repository = filterResourceCreation(resourceClass,
repositoryFactory.createEntityRepository(this, resourceClass));
context.addRepository(resourceClass, repository);
setupRelationshipRepositories(resourceClass);
}
}
/**
* Sets up relationship repositories for the given resource class. In case of a mapper
* the resource class might not correspond to the entity class.
*/
private void setupRelationshipRepositories(Class> resourceClass) {
MetaDataObject meta = metaLookup.getMeta(resourceClass).asDataObject();
for (MetaAttribute attr : meta.getAttributes()) {
if (!attr.isAssociation()) {
continue;
}
MetaType attrType = attr.getType().getElementType();
if (attrType instanceof MetaEntity) {
// normal entity association
Class> attrImplClass = attr.getType().getElementType().getImplementationClass();
// only include relations that are exposed as repositories
if (entityClasses.contains(attrImplClass)) {
QuerySpecRelationshipRepository, ?, ?, ?> relationshipRepository = filterRelationshipCreation(attrImplClass,
repositoryFactory.createRelationshipRepository(this, resourceClass, attrImplClass));
context.addRepository(resourceClass, attrImplClass, relationshipRepository);
}
}
else if (attrType instanceof MetaResourceImpl) {
Class> attrImplClass = attrType.getImplementationClass();
if (!mappings.containsKey(attrImplClass)) {
throw new IllegalStateException(
"no mapped entity for " + attrType.getName() + " reference by " + attr.getId() + " registered");
}
MappedRegistration, ?> targetMapping = mappings.get(attrImplClass);
Class> targetDtoClass = targetMapping.getDtoClass();
QuerySpecRelationshipRepository, ?, ?, ?> relationshipRepository = filterRelationshipCreation(targetDtoClass,
repositoryFactory.createRelationshipRepository(this, resourceClass, targetDtoClass));
context.addRepository(resourceClass, targetDtoClass, relationshipRepository);
}
else {
throw new IllegalStateException(
"unable to process relation: " + attr.getId() + ", neither a entity nor a mapped entity is referenced");
}
}
}
private boolean isValidEntity(MetaEntity metaEntity) {
if (metaEntity.getPrimaryKey() == null) {
logger.warn("{} has no primary key and will be ignored", metaEntity.getName());
return false;
}
if (metaEntity.getPrimaryKey().getElements().size() > 1) {
logger.warn("{} has a compound primary key and will be ignored", metaEntity.getName());
return false;
}
return true;
}
/**
* @return ResourceInformationBuilder used to describe JPA entity classes.
*/
public ResourceInformationBuilder getResourceInformationBuilder() {
if (resourceInformationBuilder == null)
resourceInformationBuilder = new JpaResourceInformationBuilder(metaLookup, em, entityClasses);
return resourceInformationBuilder;
}
/**
* @return {@link JpaQueryFactory}} implementation used to create JPA queries.
*/
public JpaQueryFactory getQueryFactory() {
return queryFactory;
}
public void setQueryFactory(JpaQueryFactory queryFactory) {
this.queryFactory = queryFactory;
queryFactory.initalize(new JpaQueryFactoryContext() {
@Override
public EntityManager getEntityManager() {
return em;
}
@Override
public MetaLookup getMetaLookup() {
return metaLookup;
}
});
}
/**
* @return {@link EntityManager}} in use.
*/
public EntityManager getEntityManager() {
return em;
}
/**
* @return {@link EntityManagerFactory}} in use.
*/
public EntityManagerFactory getEntityManagerFactory() {
return emFactory;
}
/**
* Returns the mapper used for the given dto class.
*
* @param entity
* @param dto
* @param dtoClass to find the mapper for
* @return mapper for this dtoClass
*/
@SuppressWarnings("unchecked")
public JpaMapping getMapping(Class dtoClass) {
return (JpaMapping) mappings.get(dtoClass);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy