AutoCloseable EntityManager

Sembra che io non sia l’unico a chiederselo e sembra anche che le risposte date non siano necessariamente soddisfacenti. Non sarebbe bello poter usare il try-with-resources con gli EntityManager?
Pare che il motivo per cui EntityManager non implementa AutoCloseable sia che se lo facesse, potrebbe solo fare la close() di sè stesso, ma non la rollback() dell’eventuale transazione. Questo renderebbe l’intero sforzo inutile, dovendo comunque gestire la transazione attraverso la finally come al solito. Oppure illuderebbe l’utente che l’EntityManager, essendo AutoCloseable, faccia cose che in realtà non fa (la rollback() appunto). Onestamente mi sfugge il motivo per cui l’EntityManager non possa tenere traccia della EntityTransaction, così come fa il mio codice qui sotto, ma è probabile che sia un problema di specifiche e di ricerca della purezza a tutti costi, per venire incontro a qualche possibile implementazione di JPA proveniente da Marte o altri pianeti più lontani…

Ad ogni modo il mio codice funziona, o almeno funziona per me e per il mio stile di programmazione. Può darsi che in realtà non sia sufficientemente generico e che in altri casi non funzioni… se capita a voi fatemi sapere, grazie.

Notate che il fatto che io abbia aggiunto AutoCloseable è una conseguenza di un bug che avevo lasciato in un mio software per cui un EntityManager non veniva chiuso. Per scoprire quale fosse, ho dovuto aggiungere tutto il monitoraggio attraverso JConsole ed interfaccia MBean. Ora quindi avete una classe che vi permette di ottenere un EntityManager (in realtà lo dovete usare per quel che è, ovvero EMWrapper) che nel frattempo è anche monitorabile attraverso JConsole. A voi il codice.

/*
Copyright © 2014 Lucio Crusca <lucio@sulweb.org>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 
*/

package com.virtual_bit.salix.web.persistence;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;

public class PersistenceManager
{
  private static final PersistenceManager singleton = new PersistenceManager();

  private HashMap<String, EntityManagerFactory> emfs;
  
  private static final String mainUnitName = "my-unit";
  
  private static final String[] databases = {
    "some-unit",
    "other-unit"
  };
  
  public static interface EMapMBean extends Map<String, List<EMWrapper>>
  {
    
  }
  
  public static class EMap extends HashMap<String, List<EMWrapper>> implements EMapMBean
  {
    
  }
  
  public static interface EntityMapMXBean
  {
    EMap getMap();
    String[] getOverview();
    String[] getDetails();
  }

  private static EMap activeWrappers;
  
  public static class EntityMap implements EntityMapMXBean
  {
    @Override
    public EMap getMap()
    {
      return activeWrappers;
    }
    
    public String[] getOverview()
    {
      List<String> result = new ArrayList<>();
      synchronized(activeWrappers)
      {
        Set<String> keys = activeWrappers.keySet();
        for (String s: keys)
          result.add(s + " -> " + activeWrappers.get(s).size());
      }
      String[] ares = new String[result.size()];
      return result.toArray(ares);
    }

    @Override
    public String[] getDetails()
    {
      List<String> lresult = new LinkedList<>();
      synchronized(activeWrappers)
      {
        Set<String> keys = activeWrappers.keySet();
        int ki = 0;
        for (String s: keys)
        {
          List<EMWrapper> wrappers = activeWrappers.get(s);
          String[][] traces = new String[wrappers.size()][];
          int i = 0;
          for (EMWrapper emw: wrappers)
            traces[i++] = emw.getStackStrace();
          lresult.add(flatten(traces));
        }
      }
      String[] result = new String[lresult.size()];
      return lresult.toArray(result);
    }

    private String flatten(String[][] traces)
    {
      StringBuilder sb = new StringBuilder();
      for (String[] key: traces)
        for (String values: key)
          sb.append(values).append("\n");
      return sb.toString();
    }

  }

  private PersistenceManager()
  {
  }
  
  private static void initMBean()
  {
    synchronized(PersistenceManager.class)
    {
      if (activeWrappers != null)
        return;
      
      try
      {
        activeWrappers = new EMap();
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();  
        EntityMap map = new EntityMap();
        ObjectName name = new ObjectName(map.getClass().getPackage().getName() + ":type=" + map.getClass().getSimpleName());
        mbs.registerMBean(map, name);
      }
      catch (InstanceAlreadyExistsException | MBeanRegistrationException | MalformedObjectNameException | NotCompliantMBeanException ex)
      {
        Logger.getLogger(PersistenceManager.class.getName()).log(Level.SEVERE, null, ex);
      }    
    }
  }

  private static PersistenceManager getInstance()
  {
    return singleton;
  }

  private EMWrapper createEntityManager(String jndiName)
  {
    EntityManagerFactory emf = _getFactory(jndiName);
    EMWrapper result = new EMWrapper(emf.createEntityManager(), jndiName);
    return result;
  }
  
  private EntityManagerFactory _getFactory(String jndiName)
  {
    if (emfs == null)
      emfs = new HashMap<>();
    if (!emfs.containsKey(jndiName))
      emfs.put(jndiName, Persistence.createEntityManagerFactory(jndiName));
    return emfs.get(jndiName);
  }

  public void closeFactories()
  {
    if (emfs == null)
      return;
    for (EntityManagerFactory emf: emfs.values())
      emf.close();
    emfs.clear();
    emfs = null;
  }
  
  public static EMWrapper getEM()
  {
    return getInstance().createEntityManager(nameEM());
  }
  
  private static String suffix()
  {
    Thread current = Thread.currentThread();
    if (current.isDaemon())
      return "-bg";
    return "";
            
  }

  public static String nameEM()
  {
    return mainUnitName + suffix();
  }
  
  
  public static String nameSomeEM()
  {
    return databases[0] + suffix();
  }
  
  public static String nameOtherEM()
  {
    return databases[1] + suffix();
  }
  
  
  public static EMWrapper getSomeEM()
  {
    return getInstance().createEntityManager(nameSomeEM());
  }
  
  public static EMWrapper getOtherEM()
  {
    return getInstance().createEntityManager(nameOtherEM());
  }
  
  public static EMWrapper getEM(String unitname)
  {
    return getInstance().createEntityManager(unitname);
  }

  public static Iterable<String> nameDatabases()
  {
    ArrayList<String> dbs = new ArrayList<>();
    for (String d: databases)
      dbs.add(d + suffix());
    return dbs;
  }

  public static interface EMWrapperMBean extends EntityManager
  {
    
  };
  
  public static class EMWrapper implements EMWrapperMBean, AutoCloseable
  {
    private final EntityManager wrapped;
    private final String jndiName;
    private StackTraceElement[] ste;
    
    private EMWrapper(EntityManager wrapped, String jndiName)
    {
      this.wrapped = wrapped;
      this.jndiName = jndiName;
      this.ste = Thread.currentThread().getStackTrace();
      initMBean();
      synchronized(activeWrappers)
      {
        List<EMWrapper> active = activeWrappers.get(jndiName);
        if (active == null)
          active = new LinkedList<>();
        active.add(this);
        activeWrappers.put(jndiName, active);
      }
    }

    @Override
    public void persist(Object entity)
    {
      wrapped.persist(entity);
    }

    @Override
    public <T> T merge(T entity)
    {
      return wrapped.<T>merge(entity);
    }

    @Override
    public void remove(Object entity)
    {
      wrapped.remove(entity);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey)
    {
      return wrapped.<T>find(entityClass, primaryKey);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties)
    {
      return wrapped.<T>find(entityClass, primaryKey, properties);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode)
    {
      return wrapped.<T>find(entityClass, primaryKey, lockMode);
    }

    @Override
    public <T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode, Map<String, Object> properties)
    {
      return wrapped.<T>find(entityClass, primaryKey, lockMode, properties);
    }

    @Override
    public <T> T getReference(Class<T> entityClass, Object primaryKey)
    {
      return wrapped.<T>getReference(entityClass, primaryKey);
    }

    @Override
    public void flush()
    {
      wrapped.flush();
    }

    @Override
    public void setFlushMode(FlushModeType flushMode)
    {
      wrapped.setFlushMode(flushMode);
    }

    @Override
    public FlushModeType getFlushMode()
    {
      return wrapped.getFlushMode();
    }

    @Override
    public void lock(Object entity, LockModeType lockMode)
    {
      wrapped.lock(entity, lockMode);
    }

    @Override
    public void lock(Object entity, LockModeType lockMode, Map<String, Object> properties)
    {
      wrapped.lock(entity, lockMode, properties);
    }

    @Override
    public void refresh(Object entity)
    {
      wrapped.refresh(entity);
    }

    @Override
    public void refresh(Object entity, Map<String, Object> properties)
    {
      wrapped.refresh(entity, properties);
    }

    @Override
    public void refresh(Object entity, LockModeType lockMode)
    {
      wrapped.refresh(entity, lockMode);
    }

    @Override
    public void refresh(Object entity, LockModeType lockMode, Map<String, Object> properties)
    {
      wrapped.refresh(entity, lockMode, properties);
    }

    @Override
    public void clear()
    {
      wrapped.clear();
    }

    @Override
    public void detach(Object entity)
    {
      wrapped.detach(entity);
    }

    @Override
    public boolean contains(Object entity)
    {
      return wrapped.contains(entity);
    }

    @Override
    public LockModeType getLockMode(Object entity)
    {
      return wrapped.getLockMode(entity);
    }

    @Override
    public void setProperty(String propertyName, Object value)
    {
      wrapped.setProperty(propertyName, value);
    }

    @Override
    public Map<String, Object> getProperties()
    {
      return wrapped.getProperties();
    }

    @Override
    public Query createQuery(String qlString)
    {
      return wrapped.createQuery(qlString);
    }

    @Override
    public <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery)
    {
      return wrapped.<T>createQuery(criteriaQuery);
    }

    @Override
    public <T> TypedQuery<T> createQuery(String qlString, Class<T> resultClass)
    {
      return wrapped.<T>createNamedQuery(qlString, resultClass);
    }

    @Override
    public Query createNamedQuery(String name)
    {
      return wrapped.createNamedQuery(name);
    }

    @Override
    public <T> TypedQuery<T> createNamedQuery(String name, Class<T> resultClass)
    {
      return wrapped.<T>createNamedQuery(name, resultClass);
    }

    @Override
    public Query createNativeQuery(String sqlString)
    {
      return wrapped.createNativeQuery(sqlString);
    }

    @Override
    public Query createNativeQuery(String sqlString, Class resultClass)
    {
      return wrapped.createNativeQuery(sqlString, resultClass);
    }

    @Override
    public Query createNativeQuery(String sqlString, String resultSetMapping)
    {
      return wrapped.createNativeQuery(sqlString, resultSetMapping);
    }

    @Override
    public void joinTransaction()
    {
      wrapped.joinTransaction();         
    }

    @Override
    public <T> T unwrap(Class<T> cls)
    {
      return wrapped.<T>unwrap(cls);
    }

    @Override
    public Object getDelegate()
    {
      return wrapped.getDelegate();
    }
    
    private EntityTransaction transaction;
    
    @Override
    public void close()
    {
      if (transaction != null)
        if (transaction.isActive())
          transaction.rollback();
       
      wrapped.close();
      this.ste = null;
      synchronized(activeWrappers)
      {
        List<EMWrapper> active = activeWrappers.get(jndiName);
        active.remove(this);
        activeWrappers.put(jndiName, active);
      }
    }

    @Override
    public boolean isOpen()
    {
      return wrapped.isOpen();
    }

    @Override
    public EntityTransaction getTransaction()
    {
      transaction = wrapped.getTransaction();
      return transaction;
    }

    @Override
    public EntityManagerFactory getEntityManagerFactory()
    {
      return wrapped.getEntityManagerFactory();
    }

    @Override
    public CriteriaBuilder getCriteriaBuilder()
    {
      return wrapped.getCriteriaBuilder();
    }

    @Override
    public Metamodel getMetamodel()
    {
      return wrapped.getMetamodel();
    }
    
    public static HashMap<String, List<EMWrapper>> getWrappers()
    {
      return activeWrappers;
    }
    
    public String[] getStackStrace()
    {
      if (ste == null)
        return null;
      String[] result = new String[ste.length];
      int i = 0;
      for (StackTraceElement s: ste)
        result[i++] = s.getClassName() + "#" + s.getMethodName() + ":" + s.getLineNumber();
      return result;
    } 
  }
}