001package co.codewizards.cloudstore.local.persistence; 002 003import static co.codewizards.cloudstore.core.util.ReflectionUtil.*; 004import static java.util.Objects.*; 005 006import java.lang.reflect.Type; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.HashMap; 010import java.util.Iterator; 011import java.util.LinkedList; 012import java.util.List; 013import java.util.Map; 014import java.util.SortedSet; 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 043 private static final int LOAD_ID_RANGE_PACKAGE_SIZE = 100; 044 private static final int[] LOAD_ID_RANGE_PACKAGE_SIZES_SHRINKED = { 1, 10 }; 045 046 /** 047 * Instantiate the Dao. 048 * <p> 049 * It is recommended <b>not</b> to invoke this constructor directly, but instead use 050 * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDao(Class) LocalRepoTransaction.getDao(...)}, 051 * if a {@code LocalRepoTransaction} is available (which should be in most situations). 052 * <p> 053 * After constructing, you must {@linkplain #persistenceManager(PersistenceManager) assign a <code>PersistenceManager</code>}, 054 * before you can use the Dao. This is already done when using the {@code LocalRepoTransaction}'s factory method. 055 */ 056 public Dao() { 057 final Type[] actualTypeArguments = resolveActualTypeArguments(Dao.class, this); 058 059 if (! (actualTypeArguments[0] instanceof Class<?>)) 060 throw new IllegalStateException("Subclass " + getClass().getName() + " misses generic type info for 'E'!"); 061 062 @SuppressWarnings("unchecked") 063 final Class<E> c = (Class<E>) actualTypeArguments[0]; 064 this.entityClass = c; 065 if (this.entityClass == null) 066 throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!"); 067 068 if (! (actualTypeArguments[1] instanceof Class<?>)) 069 throw new IllegalStateException("Subclass " + getClass().getName() + " misses generic type info for 'D'!"); 070 071 @SuppressWarnings("unchecked") 072 final Class<D> k = (Class<D>) actualTypeArguments[1]; 073 this.daoClass = k; 074 if (this.daoClass == null) 075 throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!"); 076 077 logger = LoggerFactory.getLogger(String.format("%s<%s>", Dao.class.getName(), entityClass.getSimpleName())); 078 } 079 080 private PersistenceManager pm; 081 082 /** 083 * Gets the {@code PersistenceManager} assigned to this Dao. 084 * @return the {@code PersistenceManager} assigned to this Dao. May be <code>null</code>, if none 085 * was assigned, yet. 086 * @see #setPersistenceManager(PersistenceManager) 087 * @see #persistenceManager(PersistenceManager) 088 */ 089 @Override 090 public PersistenceManager getPersistenceManager() { 091 return pm; 092 } 093 /** 094 * Assigns the given {@code PersistenceManager} to this Dao. 095 * <p> 096 * The Dao cannot be used, before a non-<code>null</code> value was set using this method. 097 * @param persistenceManager the {@code PersistenceManager} to be used by this Dao. May be <code>null</code>, 098 * but a non-<code>null</code> value must be set to make this Dao usable. 099 * @see #persistenceManager(PersistenceManager) 100 */ 101 public void setPersistenceManager(final PersistenceManager persistenceManager) { 102 if (this.pm != persistenceManager) { 103 daoClass2DaoInstance.clear(); 104 this.pm = persistenceManager; 105 } 106 } 107 108 protected PersistenceManager pm() { 109 if (pm == null) { 110 throw new IllegalStateException("persistenceManager not assigned!"); 111 } 112 return pm; 113 } 114 115 public DaoProvider getDaoProvider() { 116 return daoProvider; 117 } 118 public void setDaoProvider(DaoProvider daoProvider) { 119 this.daoProvider = daoProvider; 120 } 121 122 /** 123 * Assigns the given {@code PersistenceManager} to this Dao and returns {@code this}. 124 * <p> 125 * This method delegates to {@link #setPersistenceManager(PersistenceManager)}. 126 * @param persistenceManager the {@code PersistenceManager} to be used by this Dao. May be <code>null</code>, 127 * but a non-<code>null</code> value must be set to make this Dao usable. 128 * @return {@code this} for a fluent API. 129 * @see #setPersistenceManager(PersistenceManager) 130 */ 131 public D persistenceManager(final PersistenceManager persistenceManager) { 132 setPersistenceManager(persistenceManager); 133 return thisDao(); 134 } 135 136 protected D thisDao() { 137 return daoClass.cast(this); 138 } 139 140 /** 141 * Get the type of the entity. 142 * @return the type of the entity; never <code>null</code>. 143 */ 144 public Class<E> getEntityClass() { 145 return entityClass; 146 } 147 148 /** 149 * Get the entity-instance referenced by the specified identifier. 150 * 151 * @param id the identifier referencing the desired entity. Must not be <code>null</code>. 152 * @return the entity-instance referenced by the specified identifier. Never <code>null</code>. 153 * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist. 154 */ 155 public E getObjectByIdOrFail(final long id) 156 throws JDOObjectNotFoundException 157 { 158 return getObjectById(id, true); 159 } 160 161 /** 162 * Get the entity-instance referenced by the specified identifier. 163 * 164 * @param id the identifier referencing the desired entity. Must not be <code>null</code>. 165 * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no 166 * such entity exists. 167 */ 168 public E getObjectByIdOrNull(final long id) 169 { 170 return getObjectById(id, false); 171 } 172 173 /** 174 * Get the entity-instance referenced by the specified identifier. 175 * 176 * @param id the identifier referencing the desired entity. Must not be <code>null</code>. 177 * @param throwExceptionIfNotFound <code>true</code> to (re-)throw a {@link JDOObjectNotFoundException}, 178 * if the referenced entity does not exist; <code>false</code> to return <code>null</code> instead. 179 * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no 180 * such entity exists and <code>throwExceptionIfNotFound == false</code>. 181 * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist 182 * and <code>throwExceptionIfNotFound == true</code>. 183 */ 184 private E getObjectById(final long id, final boolean throwExceptionIfNotFound) 185 throws JDOObjectNotFoundException 186 { 187 try { 188 final Object result = pm().getObjectById(new LongIdentity(entityClass, id)); 189 return entityClass.cast(result); 190 } catch (final JDOObjectNotFoundException x) { 191 if (throwExceptionIfNotFound) 192 throw x; 193 else 194 return null; 195 } 196 } 197 198 public Collection<E> getObjects() { 199 final ArrayList<E> result = new ArrayList<E>(); 200 final Iterator<E> iterator = pm().getExtent(entityClass).iterator(); 201 while (iterator.hasNext()) { 202 result.add(iterator.next()); 203 } 204 return result; 205 } 206 207 public long getObjectsCount() { 208 final Query query = pm().newQuery(entityClass); 209 query.setResult("count(this)"); 210 final Long result = (Long) query.execute(); 211 if (result == null) 212 throw new IllegalStateException("Query for count(this) returned null!"); 213 214 return result; 215 } 216 217 public <P extends E> P makePersistent(final P entity) 218 { 219 requireNonNull(entity, "entity"); 220 try { 221 final P result = pm().makePersistent(entity); 222 logger.debug("makePersistent: entityID={}", JDOHelper.getObjectId(result)); 223 return result; 224 } catch (final RuntimeException x) { 225 logger.warn("makePersistent: FAILED for entityID={}: {}", JDOHelper.getObjectId(entity), x); 226 throw x; 227 } 228 } 229 230 public void deletePersistent(final E entity) 231 { 232 requireNonNull(entity, "entity"); 233 logger.debug("deletePersistent: entityID={}", JDOHelper.getObjectId(entity)); 234 pm().deletePersistent(entity); 235 } 236 237 public void deletePersistentAll(final Collection<? extends E> entities) 238 { 239 requireNonNull(entities, "entities"); 240 if (logger.isDebugEnabled()) { 241 for (final E entity : entities) { 242 logger.debug("deletePersistentAll: entityID={}", JDOHelper.getObjectId(entity)); 243 } 244 } 245 pm().deletePersistentAll(entities); 246 } 247 248 protected Collection<E> load(final Collection<E> entities) { 249 requireNonNull(entities, "entities"); 250 final Map<Class<? extends Entity>, SortedSet<Long>> entityClass2EntityIds = new HashMap<>(); 251 int entitiesSize = 0; 252 for (final E entity : entities) { 253 SortedSet<Long> entityIds = entityClass2EntityIds.get(entity.getClass()); 254 if (entityIds == null) { 255 entityIds = new TreeSet<>(); 256 entityClass2EntityIds.put(entity.getClass(), entityIds); 257 } 258 entityIds.add(entity.getId()); 259 ++entitiesSize; 260 } 261 262 final Collection<E> result = new ArrayList<>(entitiesSize); 263 for (final Map.Entry<Class<? extends Entity>, SortedSet<Long>> me : entityClass2EntityIds.entrySet()) { 264 final Class<? extends Entity> entityClass = me.getKey(); 265 final Query query = pm().newQuery(pm().getExtent(entityClass, false)); 266 267 final SortedSet<Long> entityIds = me.getValue(); 268 List<List<IdRange>> idRangePackages = buildIdRangePackages(entityIds); 269 int idRangePackageSize = shrinkIdRangePackageSizeIfPossible(idRangePackages); 270 query.setFilter(buildIdRangePackageFilter(idRangePackageSize)); 271 272 for (List<IdRange> idRangePackage : idRangePackages) { 273 @SuppressWarnings("unchecked") 274 final Collection<E> c = (Collection<E>) query.executeWithMap(buildIdRangePackageQueryMap(idRangePackage)); 275 result.addAll(c); 276 query.closeAll(); 277 } 278 } 279 return result; 280 } 281 282 protected <T> List<T> loadDtos(final Collection<E> entities, final Class<T> dtoClass, final String queryResult) { 283 requireNonNull(entities, "entities"); 284 requireNonNull(dtoClass, "dtoClass"); 285 final Map<Class<? extends Entity>, SortedSet<Long>> entityClass2EntityIDs = new HashMap<>(); 286 int entitiesSize = 0; 287 for (final E entity : entities) { 288 SortedSet<Long> entityIds = entityClass2EntityIDs.get(entity.getClass()); 289 if (entityIds == null) { 290 entityIds = new TreeSet<>(); 291 entityClass2EntityIDs.put(entity.getClass(), entityIds); 292 } 293 entityIds.add(entity.getId()); 294 ++entitiesSize; 295 } 296 297 final List<T> result = new ArrayList<>(entitiesSize); 298 for (final Map.Entry<Class<? extends Entity>, SortedSet<Long>> me : entityClass2EntityIDs.entrySet()) { 299 final Class<? extends Entity> entityClass = me.getKey(); 300 final Query query = pm().newQuery(pm().getExtent(entityClass, false)); 301 query.setResultClass(dtoClass); 302 query.setResult(queryResult); 303 304 final SortedSet<Long> entityIds = me.getValue(); 305 List<List<IdRange>> idRangePackages = buildIdRangePackages(entityIds); 306 int idRangePackageSize = shrinkIdRangePackageSizeIfPossible(idRangePackages); 307 query.setFilter(buildIdRangePackageFilter(idRangePackageSize)); 308 309 for (List<IdRange> idRangePackage : idRangePackages) { 310 @SuppressWarnings("unchecked") 311 final Collection<T> c = (Collection<T>) query.executeWithMap(buildIdRangePackageQueryMap(idRangePackage)); 312 result.addAll(c); 313 query.closeAll(); 314 } 315 } 316 return result; 317 } 318 319 protected static final class IdRange { 320 public static final long NULL_ID = -1; 321 322 public long fromIdIncl = NULL_ID; 323 public long toIdIncl = NULL_ID; 324 325 @Override 326 public String toString() { 327 return "[" + fromIdIncl + ',' + toIdIncl + ']'; 328 } 329 } 330 331 protected static int shrinkIdRangePackageSizeIfPossible(List<List<IdRange>> idRangePackages) { 332 requireNonNull(idRangePackages, "idRangePackages"); 333 if (idRangePackages.size() == 1) { 334 List<IdRange> idRangePackage = idRangePackages.get(0); 335 int usedIdRangeCount = 0; 336 for (IdRange idRange : idRangePackage) { 337 if (idRange.fromIdIncl != IdRange.NULL_ID) 338 ++usedIdRangeCount; 339 } 340 int result; 341 for (int idx = 0; idx < LOAD_ID_RANGE_PACKAGE_SIZES_SHRINKED.length; ++idx) { 342 result = LOAD_ID_RANGE_PACKAGE_SIZES_SHRINKED[idx]; 343 if (result >= usedIdRangeCount) { 344 while (idRangePackage.size() > result) 345 idRangePackage.remove(idRangePackage.size() - 1); 346 347 return result; 348 } 349 } 350 } 351 return LOAD_ID_RANGE_PACKAGE_SIZE; 352 } 353 354 protected String buildIdRangePackageFilter(final int idRangePackageSize) { 355 StringBuilder filter = new StringBuilder(); 356 for (int idx = 0; idx < idRangePackageSize; ++idx) { 357 if (idx > 0) { 358 filter.append(" || "); 359 } 360 filter.append("(:fromId").append(idx).append(" <= this.id && this.id <= :toId").append(idx).append(")"); 361 } 362 return filter.toString(); 363 } 364 365 /** 366 * Build the query-argument-map corresponding to {@link #buildIdRangePackageFilter()}. 367 * @param idRangePackage the id-range-package for which to build the argument-map. Never <code>null</code>. 368 * @return the query-argument-map used by {@link Query#executeWithMap(Map)}. Never <code>null</code>. 369 */ 370 protected Map<String, Object> buildIdRangePackageQueryMap(List<IdRange> idRangePackage) { 371 requireNonNull(idRangePackage, "idRangePackage"); 372 373 Map<String, Object> map = new HashMap<>(idRangePackage.size() * 2); 374 int idx = -1; 375 for (IdRange idRange : idRangePackage) { 376 ++idx; 377 map.put("fromId" + idx, idRange.fromIdIncl); 378 map.put("toId" + idx, idRange.toIdIncl); 379 } 380 return map; 381 } 382 383 /** 384 * Organise the given entity-IDs in {@link IdRange}s, which itself are grouped into packages. 385 * <p> 386 * Each package, i.e. each element in the returned main {@code List} has a fixed size of 387 * {@value #LOAD_ID_RANGE_PACKAGE_SIZE} elements. 388 * 389 * @param entityIds entity-IDs to be organised in ranges. Must not be <code>null</code>. 390 * @return id-range-packages. Never <code>null</code>. 391 */ 392 protected static List<List<IdRange>> buildIdRangePackages(SortedSet<Long> entityIds) { 393 return buildIdRangePackages(entityIds, LOAD_ID_RANGE_PACKAGE_SIZE); 394 } 395 396 /** 397 * @deprecated Only used for junit-test! Use {@link #buildIdRangePackages(SortedSet)} instead! Don't use this method directly! 398 */ 399 @Deprecated 400 protected static List<List<IdRange>> buildIdRangePackages(SortedSet<Long> entityIds, int idRangePackageSize) { 401 requireNonNull(entityIds, "entityIds"); 402 LinkedList<List<IdRange>> result = new LinkedList<List<IdRange>>(); 403 404 if (entityIds.isEmpty()) { 405 return result; 406 } 407 408 List<IdRange> idRangePackage = new ArrayList<>(idRangePackageSize); 409 result.add(idRangePackage); 410 IdRange idRange = new IdRange(); 411 idRangePackage.add(idRange); 412 413 for (Iterator<Long> it = entityIds.iterator(); it.hasNext();) { 414 long entityId = it.next(); 415 if (entityId < 0) 416 throw new IllegalArgumentException("entityIds contains negative element! entityId = " + entityId); 417 418 if (idRange.fromIdIncl != IdRange.NULL_ID 419 && (idRange.toIdIncl + 1 != entityId || ! it.hasNext())) { 420 421 if (idRange.toIdIncl +1 == entityId) { 422 idRange.toIdIncl = entityId; 423 entityId = IdRange.NULL_ID; 424 } 425 426 if (idRangePackage.size() >= idRangePackageSize) { 427 idRangePackage = new ArrayList<>(idRangePackageSize); 428 result.add(idRangePackage); 429 } 430 idRange = new IdRange(); 431 idRangePackage.add(idRange); 432 } 433 434 if (idRange.fromIdIncl == IdRange.NULL_ID) { 435 idRange.fromIdIncl = entityId; 436 } 437 idRange.toIdIncl = entityId; 438 } 439 440 if (isIdRangePackageEmpty(idRangePackage)) { 441 // Remove, if it is empty. 442 List<IdRange> removed = result.removeLast(); 443 if (idRangePackage != removed) 444 throw new IllegalStateException("idRangePackage != removed"); 445 } else { 446 // Fill to fixed size, if it is not empty. 447 while (idRangePackage.size() < idRangePackageSize) { 448 idRangePackage.add(new IdRange()); 449 } 450 } 451 return result; 452 } 453 454 private static boolean isIdRangePackageEmpty(List<IdRange> idRangePackage) { 455 requireNonNull(idRangePackage, "idRangePackage"); 456 457 if (idRangePackage.isEmpty()) 458 return true; 459 460 IdRange idRange = idRangePackage.get(0); 461 return idRange.fromIdIncl == IdRange.NULL_ID; 462 } 463 464 private final Map<Class<? extends Dao<?,?>>, Dao<?,?>> daoClass2DaoInstance = new HashMap<>(3); 465 466 protected <T extends Dao<?, ?>> T getDao(final Class<T> daoClass) { 467 requireNonNull(daoClass, "daoClass"); 468 469 final DaoProvider daoProvider = getDaoProvider(); 470 if (daoProvider != null) 471 return daoProvider.getDao(daoClass); 472 473 T dao = daoClass.cast(daoClass2DaoInstance.get(daoClass)); 474 if (dao == null) { 475 try { 476 dao = daoClass.newInstance(); 477 } catch (final InstantiationException e) { 478 throw new RuntimeException(e); 479 } catch (final IllegalAccessException e) { 480 throw new RuntimeException(e); 481 } 482 dao.setPersistenceManager(pm); 483 daoClass2DaoInstance.put(daoClass, dao); 484 } 485 return dao; 486 } 487 488 protected void clearFetchGroups() { 489 // Workaround for missing ID, if there is really no fetch-group at all. 490 pm().getFetchPlan().setGroup(FetchGroupConst.OBJECT_ID); 491 } 492}