/* * Copyright (c) 2014 the original author or authors * * 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 io.werval.modules.jpa; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Function; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import javax.persistence.PersistenceUtil; import io.werval.api.Mode; import io.werval.api.context.Context; import io.werval.api.context.CurrentContext; import io.werval.api.filters.Filter; import io.werval.api.filters.FilterChain; import io.werval.api.filters.FilterWith; import io.werval.api.outcomes.Outcome; import io.werval.modules.jpa.internal.MetricsSessionCustomizer; import io.werval.modules.jpa.internal.Slf4jSessionLogger; import io.werval.modules.metrics.Metrics; import io.werval.util.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static io.werval.api.Mode.DEV; import static io.werval.api.Mode.TEST; import static io.werval.util.IllegalArguments.ensureNotEmpty; import static io.werval.modules.jpa.JPAContext.METADATA_CONTEXT_KEY; /** * JPA 2 Plugin API. */ // JPA Properties -> http://eclipse.org/eclipselink/documentation/2.4/jpa/extensions/persistenceproperties_ref.htm public final class JPA { /** * Transactional annotation. */ @FilterWith( TransactionalFilter.class ) @Target( { ElementType.METHOD, ElementType.TYPE } ) @Retention( RetentionPolicy.RUNTIME ) @Inherited @Documented public static @interface Transactional { String persistenceUnit() default ""; boolean readOnly() default false; } /** * Transactional filter. */ public static final class TransactionalFilter implements Filter<Transactional> { @Override public CompletableFuture<Outcome> filter( FilterChain chain, Context context, Optional<Transactional> config ) { JPA jpa = context.application().plugin( JPA.class ); Function<EntityManager, CompletableFuture<Outcome>> action = (em) -> chain.next( context ); if( config.isPresent() ) { String persistenceUnit = config.get().persistenceUnit(); boolean readOnly = config.get().readOnly(); if( Strings.isEmpty( persistenceUnit ) ) { return jpa.supplyWithTx( readOnly, action ); } return jpa.supplyWithTx( persistenceUnit, readOnly, action ); } return jpa.supplyWithTx( false, action ); } } private static final Logger LOG = LoggerFactory.getLogger( JPA.class ); private static final Map<String, Object> GLOBAL_UNITS_PROPERTIES; static { // Theses are set on all persistence units but can be overrided in application.conf GLOBAL_UNITS_PROPERTIES = new HashMap<>(); GLOBAL_UNITS_PROPERTIES.put( "eclipselink.deploy-on-startup", "true" ); GLOBAL_UNITS_PROPERTIES.put( "eclipselink.logging.logger", Slf4jSessionLogger.class.getName() ); GLOBAL_UNITS_PROPERTIES.put( "eclipselink.logging.timestamp", "false" ); GLOBAL_UNITS_PROPERTIES.put( "eclipselink.logging.session", "false" ); GLOBAL_UNITS_PROPERTIES.put( "eclipselink.logging.connection", "false" ); GLOBAL_UNITS_PROPERTIES.put( "eclipselink.logging.thread", "false" ); } private final Mode mode; private final ClassLoader loader; private final Map<String, Map<String, Object>> unitsProperties; private final Map<String, EntityManagerFactory> emfs = new HashMap<>(); private final String defaultPersistanceUnitName; private final Metrics metrics; // Only used out of interaction context private final ThreadLocal<JPAContext> threadLocalContext = ThreadLocal.withInitial( () -> new JPAContext() ); /* package */ JPA( Mode mode, ClassLoader loader, Map<String, Map<String, Object>> properties, String defaultPersistanceUnitName, Metrics metrics ) { this.mode = mode; this.loader = loader; this.unitsProperties = properties; this.defaultPersistanceUnitName = defaultPersistanceUnitName; this.metrics = metrics; } public PersistenceUtil util() { return Persistence.getPersistenceUtil(); } public EntityManagerFactory emf() { ensureNotEmpty( "jpa.default_pu_name", defaultPersistanceUnitName ); return emf( defaultPersistanceUnitName ); } public EntityManagerFactory emf( String persistenceUnitName ) { synchronized( emfs ) { if( !emfs.containsKey( persistenceUnitName ) ) { Map<String, Object> props = new HashMap<>(); props.putAll( GLOBAL_UNITS_PROPERTIES ); if( mode == DEV || mode == TEST ) { // Log query parameters in dev mode props.put( "eclipselink.logging.parameters", "true" ); } if( unitsProperties.containsKey( persistenceUnitName ) ) { props.putAll( unitsProperties.get( persistenceUnitName ) ); } props.put( "eclipselink.classloader", loader ); if( metrics != null ) { MetricsSessionCustomizer.metricsHack = metrics; props.put( "eclipselink.session.customizer", MetricsSessionCustomizer.class.getName() ); } EntityManagerFactory emf = Persistence.createEntityManagerFactory( persistenceUnitName, props ); emfs.put( persistenceUnitName, emf ); if( metrics != null ) { MetricsSessionCustomizer.metricsHack = null; } } return emfs.get( persistenceUnitName ); } } /** * @return A new EntityManager, remember to close it */ public EntityManager newEntityManager() { return newEntityManager( defaultPersistanceUnitName ); } /** * @param persistenceUnitName Name of the PersistenceUnit to use * * @return A new EntityManager, remember to close it */ public EntityManager newEntityManager( String persistenceUnitName ) { EntityManager em = emf( persistenceUnitName ).createEntityManager(); LOG.debug( "Created new EntityManager for the '{}' persistence unit", persistenceUnitName ); return em; } /** * @return Current Context EntityManager if any, creating one if needed, stored in current Context. Otherwise, if * no current Context, a new EntityManager stored in a ThreadLocal. */ public EntityManager em() { return em( defaultPersistanceUnitName ); } /** * @param persistenceUnitName Name of the PersistenceUnit to use * * @return Current Context EntityManager if any, creating one if needed, stored in current Context. Otherwise, if * no current Context, a new EntityManager stored in a ThreadLocal. */ public EntityManager em( String persistenceUnitName ) { return CurrentContext.optional().map( // In context, using JPAContext from Context's MetaData (ctx) -> ( (JPAContext) ctx.metaData().computeIfAbsent( METADATA_CONTEXT_KEY, key -> new JPAContext() ) ) .entityManagers() .computeIfAbsent( persistenceUnitName, puName -> newEntityManager( puName ) ) ).orElseGet( // Out of context, using ThreadLocal JPAContext () -> threadLocalContext.get().entityManagers().computeIfAbsent( persistenceUnitName, puName -> newEntityManager( puName ) ) ); } public void runWithReadOnlyTx( Consumer<EntityManager> block ) { runWithTx( defaultPersistanceUnitName, true, block ); } public <T> T supplyWithReadOnlyTx( Function<EntityManager, T> block ) { return supplyWithTx( defaultPersistanceUnitName, true, block ); } public void runWithReadOnlyTx( String persistenceUnitName, Consumer<EntityManager> block ) { runWithTx( persistenceUnitName, true, block ); } public <T> T supplyWithReadOnlyTx( String persistenceUnitName, Function<EntityManager, T> block ) { return supplyWithTx( persistenceUnitName, true, block ); } public void runWithReadWriteTx( Consumer<EntityManager> block ) { runWithTx( defaultPersistanceUnitName, false, block ); } public <T> T supplyWithReadWriteTx( Function<EntityManager, T> block ) { return supplyWithTx( defaultPersistanceUnitName, false, block ); } public void runWithReadWriteTx( String persistenceUnitName, Consumer<EntityManager> block ) { runWithTx( persistenceUnitName, false, block ); } public <T> T supplyWithReadWriteTx( String persistenceUnitName, Function<EntityManager, T> block ) { return supplyWithTx( persistenceUnitName, false, block ); } public void runWithTx( boolean readOnly, Consumer<EntityManager> block ) { runWithTx( defaultPersistanceUnitName, readOnly, block ); } public <T> T supplyWithTx( boolean readOnly, Function<EntityManager, T> block ) { return supplyWithTx( defaultPersistanceUnitName, readOnly, block ); } public void runWithTx( String persistenceUnitName, boolean readOnly, Consumer<EntityManager> block ) { supplyWithTx( persistenceUnitName, readOnly, (em) -> { block.accept( em ); return null; } ); } public <T> T supplyWithTx( String persistenceUnitName, boolean readOnly, Function<EntityManager, T> block ) { EntityManager em = em( persistenceUnitName ); EntityTransaction tx = null; if( !readOnly ) { tx = em.getTransaction(); tx.begin(); LOG.trace( "Opened transaction with '{}' persistence unit.", persistenceUnitName ); } try { T result = block.apply( em ); if( tx != null ) { if( tx.getRollbackOnly() ) { tx.rollback(); LOG.trace( "Rollbacked transaction with '{}' persistence unit.", persistenceUnitName ); } else { tx.commit(); LOG.trace( "Commited transaction with '{}' persistence unit.", persistenceUnitName ); } } return result; } catch( Exception ex ) { if( tx != null ) { try { if( tx.isActive() ) { tx.rollback(); LOG.trace( "Rollbacked transaction with '{}' persistence unit, on error. " + "See next error message for the root cause", persistenceUnitName ); } } catch( Exception ignored ) { LOG.warn( "Failed to rollback on error while using '{}' persistence unit. {} - " + "See next error message for the root cause.", persistenceUnitName, ignored.getMessage(), ignored ); } } throw ex; } } /* package */ void passivate() { emfs.values().forEach( emf -> emf.close() ); } }