001// SPDX-License-Identifier: GPL-3.0-or-later 002 003package es.uvigo.esei.sing.textproc.persistence; 004 005import java.util.Iterator; 006import java.util.Map; 007import java.util.Set; 008import java.util.concurrent.ConcurrentHashMap; 009import java.util.concurrent.ConcurrentSkipListSet; 010import java.util.logging.Level; 011 012import javax.persistence.EntityManager; 013import javax.persistence.EntityManagerFactory; 014import javax.persistence.EntityTransaction; 015import javax.persistence.MappedSuperclass; 016import javax.persistence.PersistenceException; 017 018import org.hibernate.cfg.Configuration; 019import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; 020import org.hibernate.jpa.boot.internal.PersistenceXmlParser; 021 022import es.uvigo.esei.sing.textproc.logging.TextProcLogging; 023import lombok.AccessLevel; 024import lombok.NoArgsConstructor; 025import lombok.NonNull; 026 027/** 028 * Entry point to the TextProc persistence access functionalities. 029 * 030 * @author Alejandro González García 031 * @implNote The implementation of this class is thread-safe. 032 */ 033@NoArgsConstructor(access = AccessLevel.PRIVATE) 034public class TextProcPersistence { 035 private static final String NOT_RUNNING_ERROR_MESSAGE = "The persistence access layer is not running"; 036 037 private volatile EntityManagerFactory entityManagerFactory = null; 038 private final Map<Thread, EntityManager> threadEntityManager = new ConcurrentHashMap<>(); 039 private final Set<TimestampedEntityTransaction> dirtyTransactions = new ConcurrentSkipListSet<>(); 040 private final Object runningStatusChangeLock = new Object(); 041 042 /** 043 * Utility class for the <a href= 044 * "https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom">initialization 045 * on demand holder idiom</a>. 046 * 047 * @author Alejandro González García 048 */ 049 private static final class TextProcPersistenceInstanceHolder { 050 private static final TextProcPersistence INSTANCE = new TextProcPersistence(); 051 } 052 053 /** 054 * Returns the only instance of this class in the JVM. This method always 055 * returns the same object. 056 * 057 * @return The only instance of this class in the JVM. 058 */ 059 public static TextProcPersistence get() { 060 return TextProcPersistenceInstanceHolder.INSTANCE; 061 } 062 063 /** 064 * Checks whether this persistence access layer is running. When not running, 065 * most method calls on it will throw a {@link IllegalStateException}, as 066 * documented. 067 * 068 * @return True if the persistence access layer is running, false otherwise. 069 */ 070 public boolean isRunning() { 071 return entityManagerFactory != null; 072 } 073 074 /** 075 * Starts the TextProc persistence access layer, so it becomes ready to process 076 * data access operations. 077 * 078 * @param entityTypes A set of entity types that the JPA provider will analyze, 079 * in order to make them available for use. The set doesn't 080 * need to be modifiable. 081 * 082 * @throws PersistenceException If some error occurs while instantiating a 083 * entity manager factory for the persistence 084 * unit. 085 * @throws IllegalArgumentException If {@code entityTypes} is {@code null}, or 086 * contains a {@code null} element. 087 */ 088 public void start(@NonNull final Set<Class<?>> entityTypes) { 089 synchronized (runningStatusChangeLock) { 090 if (entityManagerFactory == null) { 091 final Configuration entityManagerFactoryConfiguration = new Configuration(); 092 093 // We need to use Hibernate specific API to add entities that are not in 094 // our JAR, because they were provided by other modules, and do this before 095 // creating the session factory (EntityManagerFactory), because it is immutable. 096 // It's not pretty, but it's the only possibility that allows dynamic entities 097 for (final Class<?> entityType : entityTypes) { 098 if (entityType == null) { 099 throw new IllegalArgumentException("A entity type can't be null"); 100 } 101 102 entityManagerFactoryConfiguration.addAnnotatedClass(entityType); 103 104 // Add mapped superclasses too 105 Class<?> entitySuperclass = entityType.getSuperclass(); 106 while ( 107 entitySuperclass != null && 108 entitySuperclass.getDeclaredAnnotation(MappedSuperclass.class) != null 109 ) { 110 entityManagerFactoryConfiguration.addAnnotatedClass(entitySuperclass); 111 entitySuperclass = entitySuperclass.getSuperclass(); 112 } 113 } 114 115 final ParsedPersistenceXmlDescriptor persistenceDescriptor = PersistenceXmlParser.locateNamedPersistenceUnit( 116 TextProcPersistence.class.getResource("/META-INF/persistence.xml"), 117 TextProcPersistence.class.getSimpleName() 118 ); 119 120 // Code examination of the previous method call reveals that an exception should 121 // be thrown in this case. But Javadoc is weak about this, so be safe 122 if (persistenceDescriptor == null) { 123 throw new PersistenceException("Couldn't parse the persistence descriptor"); 124 } 125 126 entityManagerFactory = entityManagerFactoryConfiguration.addProperties( 127 persistenceDescriptor.getProperties() 128 ).buildSessionFactory(); 129 } 130 } 131 } 132 133 /** 134 * Stops the TextProc persistence access layer, so that all pending transactions 135 * are finished and no new data access operations can start until it is started 136 * again (if ever). 137 */ 138 public void stop() { 139 synchronized (runningStatusChangeLock) { 140 final EntityManagerFactory currentEntityManagerFactory = entityManagerFactory; 141 if (currentEntityManagerFactory != null) { 142 // Null the attribute first, so other threads see us as stopped 143 entityManagerFactory = null; 144 145 // Now close the entity manager factory 146 currentEntityManagerFactory.close(); 147 flushEntities(true); 148 } 149 } 150 } 151 152 /** 153 * Returns a entity manager ready for use for the current thread. At any given 154 * time, there can be one entity manager per thread that requests one. 155 * 156 * @return The entity manager exclusive to the current thread. 157 * @throws IllegalStateException If the persistence access layer is not running. 158 */ 159 public EntityManager getEntityManager() { 160 if (!isRunning()) { 161 throw new IllegalStateException(NOT_RUNNING_ERROR_MESSAGE); 162 } 163 164 return threadEntityManager.compute( 165 Thread.currentThread(), 166 (final Thread key, final EntityManager value) -> { 167 return value == null ? 168 new EntityManagerTransactionDecorator( 169 entityManagerFactory.createEntityManager(), 170 (final EntityTransaction transaction) -> 171 dirtyTransactions.add(new TimestampedEntityTransaction(transaction)) 172 ) 173 : value; 174 } 175 ); 176 } 177 178 /** 179 * Commits the dirty transactions which were not rolled back, and then closes 180 * all the opened entity managers. If other parts of the application are using 181 * entity transactions when this method is invoked, its outcome is undefined. 182 * Otherwise, when this method returns, no transaction or entity manager will 183 * remain to be committed, closed or strongly referenced by internal data 184 * structures (therefore, they can be garbage collected, barring strong 185 * references elsewhere). Subsequent calls to {@link #getEntityManager()} will 186 * return new entity manager objects no matter what. 187 * 188 * @throws IllegalStateException If the application is not running. 189 */ 190 public void flushEntities() { 191 flushEntities(false); 192 } 193 194 /** 195 * Commits the dirty transactions which were not rolled back, and then closes 196 * all the opened entity managers. If other parts of the application are using 197 * entity transactions when this method is invoked, its outcome is undefined. 198 * Otherwise, when this method returns, no transaction or entity manager will 199 * remain to be committed, closed or strongly referenced by internal data 200 * structures (therefore, they can be garbage collected, barring strong 201 * references elsewhere). Subsequent calls to {@link #getEntityManager()} will 202 * return new entity manager objects no matter what. 203 * 204 * @param ignoreRunningStatus If {@code true}, the code won't check that this 205 * data access layer is running before finishing 206 * pending transactions. This is only meant for 207 * internal use. 208 * 209 * @throws IllegalStateException If the application is not running and 210 * {@code ignoreRunningStatus} is set to 211 * {@code false}. 212 */ 213 private void flushEntities(final boolean ignoreRunningStatus) { 214 if (!ignoreRunningStatus && !isRunning()) { 215 throw new IllegalStateException(NOT_RUNNING_ERROR_MESSAGE); 216 } 217 218 // Commit transactions in the same order that were created. 219 // This avoids database lock wait timeouts that can occur with RDBMS 220 // that schedule transactions in a way that doesn't guarantee that no 221 // deadlocks will occur when committing them in any order. See: 222 // https://stackoverflow.com/questions/13966467/how-to-avoid-lock-wait-timeout-exceeded-exception 223 final Iterator<TimestampedEntityTransaction> dirtyTransactionsIter = dirtyTransactions.iterator(); 224 while (dirtyTransactionsIter.hasNext()) { 225 final EntityTransaction transaction = dirtyTransactionsIter.next().getEntityTransaction(); 226 227 try { 228 if (transaction.isActive()) { 229 if (transaction.getRollbackOnly()) { 230 transaction.rollback(); 231 } else { 232 transaction.commit(); 233 } 234 } 235 } catch (final Exception exc) { 236 TextProcLogging.getLogger().log( 237 Level.WARNING, 238 "An exception occurred while committing or rolling back a transaction. The database status may be inconsistent", 239 exc 240 ); 241 } 242 243 dirtyTransactionsIter.remove(); 244 } 245 246 threadEntityManager 247 .values().parallelStream() 248 .forEach( 249 (final EntityManager entityManager) -> { 250 try { 251 if (entityManager.isOpen()) { 252 entityManager.close(); 253 } 254 } catch (final Exception exc) { 255 TextProcLogging.getLogger().log( 256 Level.WARNING, "An exception occurred while closing a entity manager", exc 257 ); 258 } 259 } 260 ); 261 threadEntityManager.clear(); 262 } 263}