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}