001package co.codewizards.cloudstore.local.persistence;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004
005import java.lang.reflect.ParameterizedType;
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
024/**
025 * Base class for all data access objects (DAOs).
026 * <p>
027 * Usually an instance of a DAO is obtained using
028 * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDAO(Class) LocalRepoTransaction.getDAO(...)}.
029 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
030 */
031public abstract class DAO<E extends Entity, D extends DAO<E, D>>
032{
033        private final Logger logger;
034        private final Class<E> entityClass;
035        private final Class<D> daoClass;
036
037        /**
038         * Instantiate the DAO.
039         * <p>
040         * It is recommended <b>not</b> to invoke this constructor directly, but instead use
041         * {@link co.codewizards.cloudstore.local.LocalRepoTransactionImpl#getDAO(Class) LocalRepoTransaction.getDAO(...)},
042         * if a {@code LocalRepoTransaction} is available (which should be in most situations).
043         * <p>
044         * After constructing, you must {@linkplain #persistenceManager(PersistenceManager) assign a <code>PersistenceManager</code>},
045         * before you can use the DAO. This is already done when using the {@code LocalRepoTransaction}'s factory method.
046         */
047        public DAO() {
048                ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass();
049                Type[] actualTypeArguments = superclass.getActualTypeArguments();
050                if (actualTypeArguments == null || actualTypeArguments.length < 2)
051                        throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!");
052
053                @SuppressWarnings("unchecked")
054                Class<E> c = (Class<E>) actualTypeArguments[0];
055                this.entityClass = c;
056                if (this.entityClass == null)
057                        throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!");
058
059                @SuppressWarnings("unchecked")
060                Class<D> k = (Class<D>) actualTypeArguments[1];
061                this.daoClass = k;
062                if (this.daoClass == null)
063                        throw new IllegalStateException("Subclass " + getClass().getName() + " has no generic type argument!");
064
065                logger = LoggerFactory.getLogger(String.format("%s<%s>", DAO.class.getName(), entityClass.getSimpleName()));
066        }
067
068        private PersistenceManager pm;
069
070        /**
071         * Gets the {@code PersistenceManager} assigned to this DAO.
072         * @return the {@code PersistenceManager} assigned to this DAO. May be <code>null</code>, if none
073         * was assigned, yet.
074         * @see #setPersistenceManager(PersistenceManager)
075         * @see #persistenceManager(PersistenceManager)
076         */
077        public PersistenceManager getPersistenceManager() {
078                return pm;
079        }
080        /**
081         * Assigns the given {@code PersistenceManager} to this DAO.
082         * <p>
083         * The DAO cannot be used, before a non-<code>null</code> value was set using this method.
084         * @param persistenceManager the {@code PersistenceManager} to be used by this DAO. May be <code>null</code>,
085         * but a non-<code>null</code> value must be set to make this DAO usable.
086         * @see #persistenceManager(PersistenceManager)
087         */
088        public void setPersistenceManager(PersistenceManager persistenceManager) {
089                if (this.pm != persistenceManager) {
090                        daoClass2DaoInstance.clear();
091                        this.pm = persistenceManager;
092                }
093        }
094
095        protected PersistenceManager pm() {
096                if (pm == null) {
097                        throw new IllegalStateException("persistenceManager not assigned!");
098                }
099                return pm;
100        }
101
102        /**
103         * Assigns the given {@code PersistenceManager} to this DAO and returns {@code this}.
104         * <p>
105         * This method delegates to {@link #setPersistenceManager(PersistenceManager)}.
106         * @param persistenceManager the {@code PersistenceManager} to be used by this DAO. May be <code>null</code>,
107         * but a non-<code>null</code> value must be set to make this DAO usable.
108         * @return {@code this} for a fluent API.
109         * @see #setPersistenceManager(PersistenceManager)
110         */
111        public D persistenceManager(PersistenceManager persistenceManager) {
112                setPersistenceManager(persistenceManager);
113                return thisDAO();
114        }
115
116        protected D thisDAO() {
117                return daoClass.cast(this);
118        }
119
120        /**
121         * Get the type of the entity.
122         * @return the type of the entity; never <code>null</code>.
123         */
124        public Class<E> getEntityClass() {
125                return entityClass;
126        }
127
128        /**
129         * Get the entity-instance referenced by the specified identifier.
130         *
131         * @param id the identifier referencing the desired entity. Must not be <code>null</code>.
132         * @return the entity-instance referenced by the specified identifier. Never <code>null</code>.
133         * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist.
134         */
135        public E getObjectByIdOrFail(long id)
136        throws JDOObjectNotFoundException
137        {
138                return getObjectById(id, true);
139        }
140
141        /**
142         * Get the entity-instance referenced by the specified identifier.
143         *
144         * @param id the identifier referencing the desired entity. Must not be <code>null</code>.
145         * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no
146         * such entity exists.
147         */
148        public E getObjectByIdOrNull(long id)
149        {
150                return getObjectById(id, false);
151        }
152
153        /**
154         * Get the entity-instance referenced by the specified identifier.
155         *
156         * @param id the identifier referencing the desired entity. Must not be <code>null</code>.
157         * @param throwExceptionIfNotFound <code>true</code> to (re-)throw a {@link JDOObjectNotFoundException},
158         * if the referenced entity does not exist; <code>false</code> to return <code>null</code> instead.
159         * @return the entity-instance referenced by the specified identifier or <code>null</code>, if no
160         * such entity exists and <code>throwExceptionIfNotFound == false</code>.
161         * @throws JDOObjectNotFoundException if the entity referenced by the given identifier does not exist
162         * and <code>throwExceptionIfNotFound == true</code>.
163         */
164        private E getObjectById(long id, boolean throwExceptionIfNotFound)
165        throws JDOObjectNotFoundException
166        {
167                try {
168                        Object result = pm().getObjectById(new LongIdentity(entityClass, id));
169                        return entityClass.cast(result);
170                } catch (JDOObjectNotFoundException x) {
171                        if (throwExceptionIfNotFound)
172                                throw x;
173                        else
174                                return null;
175                }
176        }
177
178        public Collection<E> getObjects() {
179                ArrayList<E> result = new ArrayList<E>();
180                Iterator<E> iterator = pm().getExtent(entityClass).iterator();
181                while (iterator.hasNext()) {
182                        result.add(iterator.next());
183                }
184                return result;
185        }
186
187        public long getObjectsCount() {
188                Query query = pm().newQuery(entityClass);
189                query.setResult("count(this)");
190                Long result = (Long) query.execute();
191                if (result == null)
192                        throw new IllegalStateException("Query for count(this) returned null!");
193
194                return result;
195        }
196
197        public <P extends E> P makePersistent(final P entity)
198        {
199                assertNotNull("entity", entity);
200                try {
201                        final P result = pm().makePersistent(entity);
202                        logger.debug("makePersistent: entityID={}", JDOHelper.getObjectId(result));
203                        return result;
204                } catch (final RuntimeException x) {
205                        logger.warn("makePersistent: FAILED for entityID={}: {}", JDOHelper.getObjectId(entity), x);
206                        throw x;
207                }
208        }
209
210        public void deletePersistent(final E entity)
211        {
212                assertNotNull("entity", entity);
213                logger.debug("deletePersistent: entityID={}", JDOHelper.getObjectId(entity));
214                pm().deletePersistent(entity);
215        }
216
217        public void deletePersistentAll(final Collection<? extends E> entities)
218        {
219                assertNotNull("entities", entities);
220                if (logger.isDebugEnabled()) {
221                        for (E entity : entities) {
222                                logger.debug("deletePersistentAll: entityID={}", JDOHelper.getObjectId(entity));
223                        }
224                }
225                pm().deletePersistentAll(entities);
226        }
227
228        protected Collection<E> load(final Collection<E> entities) {
229                final Collection<E> result = new ArrayList<>();
230                final Map<Class<? extends Entity>, Set<Long>> entityClass2EntityIDs = new HashMap<>();
231                for (E entity : entities) {
232                        Set<Long> entityIDs = entityClass2EntityIDs.get(entity.getClass());
233                        if (entityIDs == null) {
234                                entityIDs = new HashSet<>();
235                                entityClass2EntityIDs.put(entity.getClass(), entityIDs);
236                        }
237                        entityIDs.add(entity.getId());
238                }
239
240                for (Map.Entry<Class<? extends Entity>, Set<Long>> me : entityClass2EntityIDs.entrySet()) {
241                        Class<? extends Entity> entityClass = me.getKey();
242                        Query query = pm().newQuery(pm().getExtent(entityClass, false));
243                        query.setFilter(":entityIDs.contains(this.id)");
244
245                        Set<Long> entityIDs = me.getValue();
246                        int idx = -1;
247                        Set<Long> entityIDSubSet = new HashSet<>(300);
248                        for (Long entityID : entityIDs) {
249                                ++idx;
250                                entityIDSubSet.add(entityID);
251                                if (idx > 200) {
252                                        idx = -1;
253                                        populateLoadResult(result, query, entityIDSubSet);
254                                }
255                        }
256                        populateLoadResult(result, query, entityIDSubSet);
257                }
258                return result;
259        }
260
261        private void populateLoadResult(Collection<E> result, Query query, Set<Long> entityIDSubSet) {
262                if (entityIDSubSet.isEmpty())
263                        return;
264
265                @SuppressWarnings("unchecked")
266                Collection<E> c = (Collection<E>) query.execute(entityIDSubSet);
267                result.addAll(c);
268                query.closeAll();
269                entityIDSubSet.clear();
270        }
271
272        private final Map<Class<? extends DAO<?,?>>, DAO<?,?>> daoClass2DaoInstance = new HashMap<>(3);
273
274        protected <T extends DAO<?, ?>> T getDAO(Class<T> daoClass) {
275                T dao = daoClass.cast(daoClass2DaoInstance.get(assertNotNull("daoClass", daoClass)));
276                if (dao == null) {
277                        try {
278                                dao = daoClass.newInstance();
279                        } catch (InstantiationException e) {
280                                throw new RuntimeException(e);
281                        } catch (IllegalAccessException e) {
282                                throw new RuntimeException(e);
283                        }
284                        dao.persistenceManager(pm);
285                        daoClass2DaoInstance.put(daoClass, dao);
286                }
287                return dao;
288        }
289}