package co.codewizards.cloudstore.local.persistence; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.ReflectionUtil.*; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.jdo.JDOHelper; import javax.jdo.JDOObjectNotFoundException; import javax.jdo.PersistenceManager; import javax.jdo.Query; import javax.jdo.identity.LongIdentity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.repo.local.DaoProvider; import co.codewizards.cloudstore.core.util.AssertUtil; import co.codewizards.cloudstore.local.ContextWithPersistenceManager; /** * Base class for all data access objects (Daos). * <p> * Usually an instance of a Dao is obtained using * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDao(Class) LocalRepoTransaction.getDao(...)}. * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de */ public abstract class Dao<E extends Entity, D extends Dao<E, D>> implements ContextWithPersistenceManager { private final Logger logger; private final Class<E> entityClass; private final Class<D> daoClass; private DaoProvider daoProvider; /** * Instantiate the Dao. * <p> * It is recommended <b>not</b> to invoke this constructor directly, but instead use * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDao(Class) LocalRepoTransaction.getDao(...)}, * if a {@code LocalRepoTransaction} is available (which should be in most situations). * <p> * After constructing, you must {@linkplain #persistenceManager(PersistenceManager) assign a <code>PersistenceManager</code>}, * before you can use the Dao. This is already done when using the {@code LocalRepoTransaction}'s factory method. */ public Dao() { final Type[] actualTypeArguments = resolveActualTypeArguments(Dao.class, this); if (! (actualTypeArguments[0] instanceof Class<?>)) throw new IllegalStateException("Subclass " + getClass().getName() + " misses generic type info for 'E'!"); @SuppressWarnings("unchecked") final Class<E> c = (Class<E>) actualTypeArguments[0]; this.entityClass = c; if (this.entityClass == null) throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!"); if (! (actualTypeArguments[1] instanceof Class<?>)) throw new IllegalStateException("Subclass " + getClass().getName() + " misses generic type info for 'D'!"); @SuppressWarnings("unchecked") final Class<D> k = (Class<D>) actualTypeArguments[1]; this.daoClass = k; if (this.daoClass == null) throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!"); logger = LoggerFactory.getLogger(String.format("%s<%s>", Dao.class.getName(), entityClass.getSimpleName())); } private PersistenceManager pm; /** * Gets the {@code PersistenceManager} assigned to this Dao. * @return the {@code PersistenceManager} assigned to this Dao. May be <code>null</code>, if none * was assigned, yet. * @see #setPersistenceManager(PersistenceManager) * @see #persistenceManager(PersistenceManager) */ @Override public PersistenceManager getPersistenceManager() { return pm; } /** * Assigns the given {@code PersistenceManager} to this Dao. * <p> * The Dao cannot be used, before a non-<code>null</code> value was set using this method. * @param persistenceManager the {@code PersistenceManager} to be used by this Dao. May be <code>null</code>, * but a non-<code>null</code> value must be set to make this Dao usable. * @see #persistenceManager(PersistenceManager) */ public void setPersistenceManager(final PersistenceManager persistenceManager) { if (this.pm != persistenceManager) { daoClass2DaoInstance.clear(); this.pm = persistenceManager; } } protected PersistenceManager pm() { if (pm == null) { throw new IllegalStateException("persistenceManager not assigned!"); } return pm; } public DaoProvider getDaoProvider() { return daoProvider; } public void setDaoProvider(DaoProvider daoProvider) { this.daoProvider = daoProvider; } /** * Assigns the given {@code PersistenceManager} to this Dao and returns {@code this}. * <p> * This method delegates to {@link #setPersistenceManager(PersistenceManager)}. * @param persistenceManager the {@code PersistenceManager} to be used by this Dao. May be <code>null</code>, * but a non-<code>null</code> value must be set to make this Dao usable. * @return {@code this} for a fluent API. * @see #setPersistenceManager(PersistenceManager) */ public D persistenceManager(final PersistenceManager persistenceManager) { setPersistenceManager(persistenceManager); return thisDao(); } protected D thisDao() { return daoClass.cast(this); } /** * Get the type of the entity. * @return the type of the entity; never <code>null</code>. */ public Class<E> getEntityClass() { return entityClass; } /** * Get the entity-instance referenced by the specified identifier. * * @param id the identifier referencing the desired entity. Must not be <code>null</code>. * @return the entity-instance referenced by the specified identifier. Never <code>null</code>. * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist. */ public E getObjectByIdOrFail(final long id) throws JDOObjectNotFoundException { return getObjectById(id, true); } /** * Get the entity-instance referenced by the specified identifier. * * @param id the identifier referencing the desired entity. Must not be <code>null</code>. * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no * such entity exists. */ public E getObjectByIdOrNull(final long id) { return getObjectById(id, false); } /** * Get the entity-instance referenced by the specified identifier. * * @param id the identifier referencing the desired entity. Must not be <code>null</code>. * @param throwExceptionIfNotFound <code>true</code> to (re-)throw a {@link JDOObjectNotFoundException}, * if the referenced entity does not exist; <code>false</code> to return <code>null</code> instead. * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no * such entity exists and <code>throwExceptionIfNotFound == false</code>. * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist * and <code>throwExceptionIfNotFound == true</code>. */ private E getObjectById(final long id, final boolean throwExceptionIfNotFound) throws JDOObjectNotFoundException { try { final Object result = pm().getObjectById(new LongIdentity(entityClass, id)); return entityClass.cast(result); } catch (final JDOObjectNotFoundException x) { if (throwExceptionIfNotFound) throw x; else return null; } } public Collection<E> getObjects() { final ArrayList<E> result = new ArrayList<E>(); final Iterator<E> iterator = pm().getExtent(entityClass).iterator(); while (iterator.hasNext()) { result.add(iterator.next()); } return result; } public long getObjectsCount() { final Query query = pm().newQuery(entityClass); query.setResult("count(this)"); final Long result = (Long) query.execute(); if (result == null) throw new IllegalStateException("Query for count(this) returned null!"); return result; } public <P extends E> P makePersistent(final P entity) { AssertUtil.assertNotNull(entity, "entity"); try { final P result = pm().makePersistent(entity); logger.debug("makePersistent: entityID={}", JDOHelper.getObjectId(result)); return result; } catch (final RuntimeException x) { logger.warn("makePersistent: FAILED for entityID={}: {}", JDOHelper.getObjectId(entity), x); throw x; } } public void deletePersistent(final E entity) { AssertUtil.assertNotNull(entity, "entity"); logger.debug("deletePersistent: entityID={}", JDOHelper.getObjectId(entity)); pm().deletePersistent(entity); } public void deletePersistentAll(final Collection<? extends E> entities) { AssertUtil.assertNotNull(entities, "entities"); if (logger.isDebugEnabled()) { for (final E entity : entities) { logger.debug("deletePersistentAll: entityID={}", JDOHelper.getObjectId(entity)); } } pm().deletePersistentAll(entities); } protected Collection<E> load(final Collection<E> entities) { AssertUtil.assertNotNull(entities, "entities"); final Collection<E> result = new ArrayList<>(); final Map<Class<? extends Entity>, Set<Long>> entityClass2EntityIDs = new HashMap<>(); for (final E entity : entities) { Set<Long> entityIDs = entityClass2EntityIDs.get(entity.getClass()); if (entityIDs == null) { entityIDs = new HashSet<>(); entityClass2EntityIDs.put(entity.getClass(), entityIDs); } entityIDs.add(entity.getId()); } for (final Map.Entry<Class<? extends Entity>, Set<Long>> me : entityClass2EntityIDs.entrySet()) { final Class<? extends Entity> entityClass = me.getKey(); final Query query = pm().newQuery(pm().getExtent(entityClass, false)); query.setFilter(":entityIDs.contains(this.id)"); final Set<Long> entityIDs = me.getValue(); int idx = -1; final Set<Long> entityIDSubSet = new HashSet<>(300); for (final Long entityID : entityIDs) { ++idx; entityIDSubSet.add(entityID); if (idx > 200) { idx = -1; populateLoadResult(result, query, entityIDSubSet); } } populateLoadResult(result, query, entityIDSubSet); } return result; } private void populateLoadResult(final Collection<E> result, final Query query, final Set<Long> entityIDSubSet) { if (entityIDSubSet.isEmpty()) return; @SuppressWarnings("unchecked") final Collection<E> c = (Collection<E>) query.execute(entityIDSubSet); result.addAll(c); query.closeAll(); entityIDSubSet.clear(); } private final Map<Class<? extends Dao<?,?>>, Dao<?,?>> daoClass2DaoInstance = new HashMap<>(3); protected <T extends Dao<?, ?>> T getDao(final Class<T> daoClass) { assertNotNull(daoClass, "daoClass"); final DaoProvider daoProvider = getDaoProvider(); if (daoProvider != null) return daoProvider.getDao(daoClass); T dao = daoClass.cast(daoClass2DaoInstance.get(daoClass)); if (dao == null) { try { dao = daoClass.newInstance(); } catch (final InstantiationException e) { throw new RuntimeException(e); } catch (final IllegalAccessException e) { throw new RuntimeException(e); } dao.setPersistenceManager(pm); daoClass2DaoInstance.put(daoClass, dao); } return dao; } }