001package co.codewizards.cloudstore.local.persistence; 002 003import static co.codewizards.cloudstore.core.util.AssertUtil.*; 004import static co.codewizards.cloudstore.core.util.ReflectionUtil.*; 005 006import java.lang.reflect.Type; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.HashMap; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.Map; 013import java.util.Set; 014 015import javax.jdo.JDOHelper; 016import javax.jdo.JDOObjectNotFoundException; 017import javax.jdo.PersistenceManager; 018import javax.jdo.Query; 019import javax.jdo.identity.LongIdentity; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024import co.codewizards.cloudstore.core.repo.local.DaoProvider; 025import co.codewizards.cloudstore.core.util.AssertUtil; 026import co.codewizards.cloudstore.local.ContextWithPersistenceManager; 027 028/** 029 * Base class for all data access objects (Daos). 030 * <p> 031 * Usually an instance of a Dao is obtained using 032 * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDao(Class) LocalRepoTransaction.getDao(...)}. 033 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de 034 */ 035public abstract class Dao<E extends Entity, D extends Dao<E, D>> implements ContextWithPersistenceManager 036{ 037 private final Logger logger; 038 private final Class<E> entityClass; 039 private final Class<D> daoClass; 040 private DaoProvider daoProvider; 041 042 /** 043 * Instantiate the Dao. 044 * <p> 045 * It is recommended <b>not</b> to invoke this constructor directly, but instead use 046 * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDao(Class) LocalRepoTransaction.getDao(...)}, 047 * if a {@code LocalRepoTransaction} is available (which should be in most situations). 048 * <p> 049 * After constructing, you must {@linkplain #persistenceManager(PersistenceManager) assign a <code>PersistenceManager</code>}, 050 * before you can use the Dao. This is already done when using the {@code LocalRepoTransaction}'s factory method. 051 */ 052 public Dao() { 053 final Type[] actualTypeArguments = resolveActualTypeArguments(Dao.class, this); 054 055 if (! (actualTypeArguments[0] instanceof Class<?>)) 056 throw new IllegalStateException("Subclass " + getClass().getName() + " misses generic type info for 'E'!"); 057 058 @SuppressWarnings("unchecked") 059 final Class<E> c = (Class<E>) actualTypeArguments[0]; 060 this.entityClass = c; 061 if (this.entityClass == null) 062 throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!"); 063 064 if (! (actualTypeArguments[1] instanceof Class<?>)) 065 throw new IllegalStateException("Subclass " + getClass().getName() + " misses generic type info for 'D'!"); 066 067 @SuppressWarnings("unchecked") 068 final Class<D> k = (Class<D>) actualTypeArguments[1]; 069 this.daoClass = k; 070 if (this.daoClass == null) 071 throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!"); 072 073 logger = LoggerFactory.getLogger(String.format("%s<%s>", Dao.class.getName(), entityClass.getSimpleName())); 074 } 075 076 private PersistenceManager pm; 077 078 /** 079 * Gets the {@code PersistenceManager} assigned to this Dao. 080 * @return the {@code PersistenceManager} assigned to this Dao. May be <code>null</code>, if none 081 * was assigned, yet. 082 * @see #setPersistenceManager(PersistenceManager) 083 * @see #persistenceManager(PersistenceManager) 084 */ 085 @Override 086 public PersistenceManager getPersistenceManager() { 087 return pm; 088 } 089 /** 090 * Assigns the given {@code PersistenceManager} to this Dao. 091 * <p> 092 * The Dao cannot be used, before a non-<code>null</code> value was set using this method. 093 * @param persistenceManager the {@code PersistenceManager} to be used by this Dao. May be <code>null</code>, 094 * but a non-<code>null</code> value must be set to make this Dao usable. 095 * @see #persistenceManager(PersistenceManager) 096 */ 097 public void setPersistenceManager(final PersistenceManager persistenceManager) { 098 if (this.pm != persistenceManager) { 099 daoClass2DaoInstance.clear(); 100 this.pm = persistenceManager; 101 } 102 } 103 104 protected PersistenceManager pm() { 105 if (pm == null) { 106 throw new IllegalStateException("persistenceManager not assigned!"); 107 } 108 return pm; 109 } 110 111 public DaoProvider getDaoProvider() { 112 return daoProvider; 113 } 114 public void setDaoProvider(DaoProvider daoProvider) { 115 this.daoProvider = daoProvider; 116 } 117 118 /** 119 * Assigns the given {@code PersistenceManager} to this Dao and returns {@code this}. 120 * <p> 121 * This method delegates to {@link #setPersistenceManager(PersistenceManager)}. 122 * @param persistenceManager the {@code PersistenceManager} to be used by this Dao. May be <code>null</code>, 123 * but a non-<code>null</code> value must be set to make this Dao usable. 124 * @return {@code this} for a fluent API. 125 * @see #setPersistenceManager(PersistenceManager) 126 */ 127 public D persistenceManager(final PersistenceManager persistenceManager) { 128 setPersistenceManager(persistenceManager); 129 return thisDao(); 130 } 131 132 protected D thisDao() { 133 return daoClass.cast(this); 134 } 135 136 /** 137 * Get the type of the entity. 138 * @return the type of the entity; never <code>null</code>. 139 */ 140 public Class<E> getEntityClass() { 141 return entityClass; 142 } 143 144 /** 145 * Get the entity-instance referenced by the specified identifier. 146 * 147 * @param id the identifier referencing the desired entity. Must not be <code>null</code>. 148 * @return the entity-instance referenced by the specified identifier. Never <code>null</code>. 149 * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist. 150 */ 151 public E getObjectByIdOrFail(final long id) 152 throws JDOObjectNotFoundException 153 { 154 return getObjectById(id, true); 155 } 156 157 /** 158 * Get the entity-instance referenced by the specified identifier. 159 * 160 * @param id the identifier referencing the desired entity. Must not be <code>null</code>. 161 * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no 162 * such entity exists. 163 */ 164 public E getObjectByIdOrNull(final long id) 165 { 166 return getObjectById(id, false); 167 } 168 169 /** 170 * Get the entity-instance referenced by the specified identifier. 171 * 172 * @param id the identifier referencing the desired entity. Must not be <code>null</code>. 173 * @param throwExceptionIfNotFound <code>true</code> to (re-)throw a {@link JDOObjectNotFoundException}, 174 * if the referenced entity does not exist; <code>false</code> to return <code>null</code> instead. 175 * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no 176 * such entity exists and <code>throwExceptionIfNotFound == false</code>. 177 * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist 178 * and <code>throwExceptionIfNotFound == true</code>. 179 */ 180 private E getObjectById(final long id, final boolean throwExceptionIfNotFound) 181 throws JDOObjectNotFoundException 182 { 183 try { 184 final Object result = pm().getObjectById(new LongIdentity(entityClass, id)); 185 return entityClass.cast(result); 186 } catch (final JDOObjectNotFoundException x) { 187 if (throwExceptionIfNotFound) 188 throw x; 189 else 190 return null; 191 } 192 } 193 194 public Collection<E> getObjects() { 195 final ArrayList<E> result = new ArrayList<E>(); 196 final Iterator<E> iterator = pm().getExtent(entityClass).iterator(); 197 while (iterator.hasNext()) { 198 result.add(iterator.next()); 199 } 200 return result; 201 } 202 203 public long getObjectsCount() { 204 final Query query = pm().newQuery(entityClass); 205 query.setResult("count(this)"); 206 final Long result = (Long) query.execute(); 207 if (result == null) 208 throw new IllegalStateException("Query for count(this) returned null!"); 209 210 return result; 211 } 212 213 public <P extends E> P makePersistent(final P entity) 214 { 215 AssertUtil.assertNotNull(entity, "entity"); 216 try { 217 final P result = pm().makePersistent(entity); 218 logger.debug("makePersistent: entityID={}", JDOHelper.getObjectId(result)); 219 return result; 220 } catch (final RuntimeException x) { 221 logger.warn("makePersistent: FAILED for entityID={}: {}", JDOHelper.getObjectId(entity), x); 222 throw x; 223 } 224 } 225 226 public void deletePersistent(final E entity) 227 { 228 AssertUtil.assertNotNull(entity, "entity"); 229 logger.debug("deletePersistent: entityID={}", JDOHelper.getObjectId(entity)); 230 pm().deletePersistent(entity); 231 } 232 233 public void deletePersistentAll(final Collection<? extends E> entities) 234 { 235 AssertUtil.assertNotNull(entities, "entities"); 236 if (logger.isDebugEnabled()) { 237 for (final E entity : entities) { 238 logger.debug("deletePersistentAll: entityID={}", JDOHelper.getObjectId(entity)); 239 } 240 } 241 pm().deletePersistentAll(entities); 242 } 243 244 protected Collection<E> load(final Collection<E> entities) { 245 AssertUtil.assertNotNull(entities, "entities"); 246 final Collection<E> result = new ArrayList<>(); 247 final Map<Class<? extends Entity>, Set<Long>> entityClass2EntityIDs = new HashMap<>(); 248 for (final E entity : entities) { 249 Set<Long> entityIDs = entityClass2EntityIDs.get(entity.getClass()); 250 if (entityIDs == null) { 251 entityIDs = new HashSet<>(); 252 entityClass2EntityIDs.put(entity.getClass(), entityIDs); 253 } 254 entityIDs.add(entity.getId()); 255 } 256 257 for (final Map.Entry<Class<? extends Entity>, Set<Long>> me : entityClass2EntityIDs.entrySet()) { 258 final Class<? extends Entity> entityClass = me.getKey(); 259 final Query query = pm().newQuery(pm().getExtent(entityClass, false)); 260 query.setFilter(":entityIDs.contains(this.id)"); 261 262 final Set<Long> entityIDs = me.getValue(); 263 int idx = -1; 264 final Set<Long> entityIDSubSet = new HashSet<>(300); 265 for (final Long entityID : entityIDs) { 266 ++idx; 267 entityIDSubSet.add(entityID); 268 if (idx > 200) { 269 idx = -1; 270 populateLoadResult(result, query, entityIDSubSet); 271 } 272 } 273 populateLoadResult(result, query, entityIDSubSet); 274 } 275 return result; 276 } 277 278 private void populateLoadResult(final Collection<E> result, final Query query, final Set<Long> entityIDSubSet) { 279 if (entityIDSubSet.isEmpty()) 280 return; 281 282 @SuppressWarnings("unchecked") 283 final Collection<E> c = (Collection<E>) query.execute(entityIDSubSet); 284 result.addAll(c); 285 query.closeAll(); 286 entityIDSubSet.clear(); 287 } 288 289 private final Map<Class<? extends Dao<?,?>>, Dao<?,?>> daoClass2DaoInstance = new HashMap<>(3); 290 291 protected <T extends Dao<?, ?>> T getDao(final Class<T> daoClass) { 292 assertNotNull(daoClass, "daoClass"); 293 294 final DaoProvider daoProvider = getDaoProvider(); 295 if (daoProvider != null) 296 return daoProvider.getDao(daoClass); 297 298 T dao = daoClass.cast(daoClass2DaoInstance.get(daoClass)); 299 if (dao == null) { 300 try { 301 dao = daoClass.newInstance(); 302 } catch (final InstantiationException e) { 303 throw new RuntimeException(e); 304 } catch (final IllegalAccessException e) { 305 throw new RuntimeException(e); 306 } 307 dao.setPersistenceManager(pm); 308 daoClass2DaoInstance.put(daoClass, dao); 309 } 310 return dao; 311 } 312}