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}