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