All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.katharsis.jpa.JpaModule Maven / Gradle / Ivy

There is a newer version: 3.0.2
Show newest version
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