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}