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}