/* * Copyright (C) 2010-2017 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * * Akvo FLOW is free software: you can redistribute it and modify it under the terms of * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, * either version 3 of the License or any later version. * * Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License included below for more details. * * The full license text can also be seen at <http://www.gnu.org/licenses/agpl.html>. */ package com.gallatinsystems.framework.dao; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import javax.jdo.JDOObjectNotFoundException; import javax.jdo.PersistenceManager; import net.sf.jsr107cache.CacheException; import org.akvo.flow.domain.SecuredObject; import org.datanucleus.store.appengine.query.JDOCursorHelper; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.waterforpeople.mapping.app.web.rest.security.AppRole; import com.gallatinsystems.common.Constants; import com.gallatinsystems.framework.domain.BaseDomain; import com.gallatinsystems.framework.servlet.PersistenceFilter; import com.gallatinsystems.survey.domain.Survey; import com.gallatinsystems.survey.domain.SurveyGroup; import com.gallatinsystems.user.dao.UserAuthorizationDAO; import com.gallatinsystems.user.domain.UserAuthorization; import com.google.appengine.api.datastore.Cursor; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; /** * This is a reusable data access object that supports basic operations (save, find by property, * list). * * @author Christopher Fagiani * @param <T> a persistent class that extends BaseDomain */ public class BaseDAO<T extends BaseDomain> { public static final int DEFAULT_RESULT_COUNT = 20; protected static final int RETRY_INTERVAL_MILLIS = 200; protected static final String STRING_TYPE = "String"; protected static final String NOT_EQ_OP = "!="; protected static final String EQ_OP = " == "; protected static final String GTE_OP = " >= "; protected static final String LTE_OP = " <= "; private Class<T> concreteClass; protected Logger log; public enum CURSOR_TYPE { all }; public BaseDAO(Class<T> e) { setDomainClass(e); log = Logger.getLogger(this.getClass().getName()); } /** * Injected version of the actual Class to pass for the persistentClass in the query creation. * This must be set before using this implementation class or any derived class. * * @param e an instance of the type of object to use for this instance of the DAO * implementation. */ public void setDomainClass(Class<T> e) { this.concreteClass = e; } /** * saves an object to the data store. This method will set the lastUpdateDateTime on the domain * object prior to saving and will set the createdDateTime (if it is null). * * @param <E> * @param obj * @return */ public <E extends BaseDomain> E save(E obj) { PersistenceManager pm = PersistenceFilter.getManager(); obj.setLastUpdateDateTime(new Date()); if (obj.getCreatedDateTime() == null) { obj.setCreatedDateTime(obj.getLastUpdateDateTime()); } obj = pm.makePersistent(obj); return obj; } /** * saves all instances contained within the collection passed in. This will set the * lastUpdateDateTime for the objects prior to saving. * * @param <E> * @param objList * @return */ public <E extends BaseDomain> Collection<E> save(Collection<E> objList) { if (objList != null) { for (E item : objList) { item.setLastUpdateDateTime(new Date()); if (item.getCreatedDateTime() == null) { item.setCreatedDateTime(item.getLastUpdateDateTime()); } } PersistenceManager pm = PersistenceFilter.getManager(); objList = pm.makePersistentAll(objList); } return objList; } /** * gets the core persistent object for the dao concrete class using the string key (obtained * from KeyFactory.stringFromKey()) * * @param keyString * @return */ public T getByKey(String keyString) { return getByKey(keyString, concreteClass); } /** * gets an object by key * * @param key * @return */ public T getByKey(Key key) { return getByKey(key, concreteClass); } /** * convenience method to allow loading of other persistent objects by key from this dao * * @param keyString * @return */ public <E extends BaseDomain> E getByKey(String keyString, Class<E> clazz) { PersistenceManager pm = PersistenceFilter.getManager(); E result = null; Key k = KeyFactory.stringToKey(keyString); try { result = pm.getObjectById(clazz, k); } catch (JDOObjectNotFoundException nfe) { log.warning("No " + clazz.getCanonicalName() + " found with key: " + k); } return result; } /** * gets a single object identified by the key passed in. * * @param <E> * @param key * @param clazz * @return the object corresponding to the key (or null if not found) */ public <E extends BaseDomain> E getByKey(Key key, Class<E> clazz) { PersistenceManager pm = PersistenceFilter.getManager(); E result = null; try { result = pm.getObjectById(clazz, key); } catch (JDOObjectNotFoundException nfe) { log.warning("No " + clazz.getCanonicalName() + " found with key: " + key); } return result; } /** * gets a single object by key where the key is represented as a Long * * @param id * @return */ public T getByKey(Long id) { return getByKey(id, concreteClass); } /** * gets a single object by key where the key is represented as a Long and the type is the class * passed in via clazz * * @param <E> * @param id * @param clazz * @return */ public <E extends BaseDomain> E getByKey(Long id, Class<E> clazz) { PersistenceManager pm = PersistenceFilter.getManager(); String itemKey = KeyFactory.createKeyString(clazz.getSimpleName(), id); E result = null; try { result = pm.getObjectById(clazz, itemKey); } catch (JDOObjectNotFoundException nfe) { log.warning("No " + clazz.getCanonicalName() + " found with id: " + id); } return result; } /** * lists all of the concreteClass instances in the datastore, using a page size. * * @return */ public List<T> list(String cursorString, Integer pageSize) { return list(concreteClass, cursorString, pageSize); } /** * lists all of the concreteClass instances in the datastore. if we think we'll use this on * large tables, we should use Extents * * @return */ public List<T> list(String cursorString) { return list(concreteClass, cursorString); } /** * Lists all of the concreteClass instances in the datastore */ public <E extends BaseDomain> List<E> list(Class<E> c, String cursorString) { return list(c, cursorString, null); } /** * lists all of the type passed in. if we think we'll use this on large tables, we should use * Extents * * @return */ @SuppressWarnings("unchecked") public <E extends BaseDomain> List<E> list(Class<E> c, String cursorString, Integer pageSize) { PersistenceManager pm = PersistenceFilter.getManager(); javax.jdo.Query query = pm.newQuery(c); if (cursorString != null && !cursorString.trim().toLowerCase() .equals(Constants.ALL_RESULTS)) { Cursor cursor = Cursor.fromWebSafeString(cursorString); Map<String, Object> extensionMap = new HashMap<String, Object>(); extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor); query.setExtensions(extensionMap); } List<E> results = null; if (pageSize == null) { this.prepareCursor(cursorString, query); } else { this.prepareCursor(cursorString, pageSize, query); } results = (List<E>) query.execute(); return results; } /** * Return a list of survey groups or surveys that are accessible by the current user, filtered * by object ids * * @return */ public <E extends BaseDomain> List<E> filterByUserAuthorizationObjectId(List<E> allObjectsList, Long userId) { if (!concreteClass.isAssignableFrom(SurveyGroup.class) && !concreteClass.isAssignableFrom(Survey.class)) { throw new UnsupportedOperationException("Cannot filter " + concreteClass.getSimpleName()); } UserAuthorizationDAO userAuthorizationDAO = new UserAuthorizationDAO(); List<UserAuthorization> userAuthorizationList = userAuthorizationDAO.listByUser(userId); if (userAuthorizationList.isEmpty()) { return Collections.emptyList(); } Set<Long> securedObjectIds = new HashSet<>(); for (UserAuthorization auth : userAuthorizationList) { if (auth.getSecuredObjectId() != null) { securedObjectIds.add(auth.getSecuredObjectId()); } } // Set of all ancestor ids of secured objects Set<Long> securedAncestorIds = new HashSet<>(); for (Object obj : allObjectsList) { SecuredObject securedObject = (SecuredObject) obj; if (securedObjectIds.contains(securedObject.getObjectId())) { securedAncestorIds.addAll(securedObject.listAncestorIds()); } } Set<E> authorizedSet = new HashSet<>(); if (concreteClass.isAssignableFrom(SurveyGroup.class)) { for (E obj : allObjectsList) { SurveyGroup sg = (SurveyGroup) obj; Long sgId = sg.getKey().getId(); if (hasAuthorizedAncestors(sg.getAncestorIds(), securedObjectIds) || securedObjectIds.contains(sgId) || securedAncestorIds.contains(sgId)) { authorizedSet.add(obj); } } } else { for (E obj : allObjectsList) { Survey s = (Survey) obj; List<Long> ancestorIds = s.getAncestorIds(); if (hasAuthorizedAncestors(ancestorIds, securedObjectIds)) { authorizedSet.add(obj); } } } List<E> authorizedList = new ArrayList<>(); authorizedList.addAll(authorizedSet); return authorizedList; } public <E extends BaseDomain> List<E> filterByUserAuthorizationObjectId(List<E> allObjectsList) { final Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); final Long userId = (Long) authentication.getCredentials(); // super admin list all if (authentication.getAuthorities().contains(AppRole.SUPER_ADMIN)) { return allObjectsList; } return filterByUserAuthorizationObjectId(allObjectsList, userId); } /** * Check whether one or more items in the list of an entity's ancestor ids is present in the * list of authorized objects for a user. Return true if this is the case * * @param ancestorIds * @param securedObjectIds * @return */ private boolean hasAuthorizedAncestors(List<Long> ancestorIds, Set<Long> securedObjectIds) { // use new List object to prevent side effects on SurveyGroup.ancestorIds property List<Long> idsList = new ArrayList<Long>(ancestorIds); return idsList != null && idsList.removeAll(securedObjectIds); } /** * returns a single object based on the property value * * @param propertyName * @param propertyValue * @param propertyType * @return */ protected T findByProperty(String propertyName, Object propertyValue, String propertyType) { T result = null; List<T> results = listByProperty(propertyName, propertyValue, propertyType); if (results.size() > 0) { result = results.get(0); } return result; } /** * gets a List of object by key where the key is represented as a Long * * @param ids Array of Long representing the keys of objects * @return null if ids is null, otherwise a list of objects */ public List<T> listByKeys(Long[] ids) { if (ids == null) { return null; } final List<T> list = new ArrayList<T>(); for (Long id : ids) { final T obj = getByKey(id); if (obj != null) { list.add(obj); } } return list; } /** * Retrieves a List of objects by key where the keys are represented by a Collection of Longs * * @param ids List of Long representing the keys of objects * @return empty list if ids is null, otherwise a list of objects */ public List<T> listByKeys(List<Long> ids) { if (ids == null) { return Collections.emptyList(); } final List<T> list = new ArrayList<T>(); for (Long id : ids) { final T obj = getByKey(id); if (obj != null) { list.add(obj); } } return list; } /** * lists all the objects of the same type as the concreteClass with property equal to the value * passed in since using this requires the caller know the persistence data type of the field * and the field name, this method is protected so that it can only be used by subclass DAOs. We * don't want those details to leak into higher layers of the code. * * @param propertyName * @param propertyValue * @param propertyType * @return */ protected List<T> listByProperty(String propertyName, Object propertyValue, String propertyType) { return listByProperty(propertyName, propertyValue, propertyType, null, null, EQ_OP, concreteClass); } /** * lists all objects of type class that have the property name/value passed in * * @param <E> * @param propertyName * @param propertyValue * @param propertyType * @param clazz * @return */ protected <E extends BaseDomain> List<E> listByProperty( String propertyName, Object propertyValue, String propertyType, Class<E> clazz) { return listByProperty(propertyName, propertyValue, propertyType, null, null, EQ_OP, clazz); } /** * lists all instances of type clazz that have the property equal to the value passed in and * orders the results by the field specified. NOTE: for this to work on the datastore, you may * need to have an index defined. * * @param <E> * @param propertyName * @param propertyValue * @param propertyType * @param orderBy * @param clazz * @return */ protected <E extends BaseDomain> List<E> listByProperty( String propertyName, Object propertyValue, String propertyType, String orderBy, Class<E> clazz) { return listByProperty(propertyName, propertyValue, propertyType, orderBy, null, EQ_OP, clazz); } /** * lists all instances that have the property name/value matching those passed in optionally * sorted by the order by column and direction. NOTE: depending on the sort being done, you may * need an index in the datastore for this to work. * * @param propertyName * @param propertyValue * @param propertyType * @param orderByCol * @param orderByDir * @return */ protected List<T> listByProperty(String propertyName, Object propertyValue, String propertyType, String orderByCol, String orderByDir) { return listByProperty(propertyName, propertyValue, propertyType, orderByCol, orderByDir, EQ_OP, concreteClass); } /** * lists all instances that have the property name/value matching those passed in optionally * sorted by the order by column. NOTE: depending on the sort being done, you may need an index * in the datastore for this to work. * * @param propertyName * @param propertyValue * @param propertyType * @param orderByCol * @return */ protected List<T> listByProperty(String propertyName, Object propertyValue, String propertyType, String orderByCol) { return listByProperty(propertyName, propertyValue, propertyType, orderByCol, null, EQ_OP, concreteClass); } /** * convenience method to list all instances of the type passed in that match the property since * using this requires the caller know the persistence data type of the field and the field * name, this method is protected so that it can only be used by subclass DAOs. We don't want * those details to leak into higher layers of the code. * * @param propertyName * @param propertyValue * @param propertyType * @return */ @SuppressWarnings("unchecked") protected <E extends BaseDomain> List<E> listByProperty( String propertyName, Object propertyValue, String propertyType, String orderByField, String orderByDir, String operator, Class<E> clazz) { PersistenceManager pm = PersistenceFilter.getManager(); List<E> results = null; String paramName = propertyName + "Param"; if (paramName.contains(".")) { paramName = paramName.substring(paramName.indexOf(".") + 1); } javax.jdo.Query query = pm.newQuery(clazz); query.setFilter(propertyName + " " + operator + " " + paramName); if (orderByField != null) { query.setOrdering(orderByField + (orderByDir != null ? " " + orderByDir : "")); } query.declareParameters(propertyType + " " + paramName); if (propertyValue instanceof Date) { query.declareImports("import java.util.Date"); } results = (List<E>) query.execute(propertyValue); return results; } /** * deletes an object from the db * * @param <E> * @param obj */ public <E extends BaseDomain> void delete(E obj) { PersistenceManager pm = PersistenceFilter.getManager(); pm.deletePersistent(obj); } /** * deletes a list of objects in a single datastore interaction */ public <E extends BaseDomain> void delete(Collection<E> obj) { PersistenceManager pm = PersistenceFilter.getManager(); pm.deletePersistentAll(obj); } /** * utility method to form a hash map of query parameters using an equality operator * * @param paramName - name of object property * @param filter - in/out stringBuilder of query filters * @param param -in/out stringBuilder of param names * @param type - data type of field * @param value - value to bind to param * @param paramMap - in/out parameter map */ protected void appendNonNullParam(String paramName, StringBuilder filter, StringBuilder param, String type, Object value, Map<String, Object> paramMap) { appendNonNullParam(paramName, filter, param, type, value, paramMap, EQ_OP); } /** * utility method to form a hash map of query parameters * * @param paramName - name of object property * @param filter - in/out stringBuilder of query filters * @param param -in/out stringBuilder of param names * @param type - data type of field * @param value - value to bind to param * @param paramMap - in/out parameter map * @param operator - operator to use */ protected void appendNonNullParam(String paramName, StringBuilder filter, StringBuilder param, String type, Object value, Map<String, Object> paramMap, String operator) { if (value != null) { if (paramMap.keySet().size() > 0) { filter.append(" && "); param.append(", "); } String paramValName = paramName + "Param" + paramMap.keySet().size(); filter.append(paramName).append(" ").append(operator).append(" ") .append(paramValName); param.append(type).append(" ").append(paramValName); paramMap.put(paramValName, value); } } /** * gets a GAE datastore cursor based on the results list passed in. The list must be a non-null * list of persistent entities (entites retrived from the datastore in the same session). * * @param results * @return */ @SuppressWarnings("rawtypes") public static String getCursor(List results) { if (results != null && results.size() > 0) { Cursor cursor = JDOCursorHelper.getCursor(results); if (cursor != null) { return cursor.toWebSafeString(); } else { return null; } } return null; } /** * sets up the cursor with the given page size (or no page size if the cursor string is set to * the ALL_RESULTS constant) * * @param cursorString * @param pageSize * @param query */ protected void prepareCursor(String cursorString, Integer pageSize, javax.jdo.Query query) { if (cursorString != null && !cursorString.trim().toLowerCase() .equals(Constants.ALL_RESULTS)) { Cursor cursor = Cursor.fromWebSafeString(cursorString); Map<String, Object> extensionMap = new HashMap<String, Object>(); extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor); query.setExtensions(extensionMap); } if (cursorString == null || !cursorString.equals(Constants.ALL_RESULTS)) { if (pageSize == null) { query.setRange(0, DEFAULT_RESULT_COUNT); } else { query.setRange(0, pageSize); } } } /** * this method should only be used when running a lot of datastore operations in a single * request to a task queue and or backend (regular online requests can't accumulate enough data * to require a flush without timing out). */ public void flushBatch() { PersistenceManager pm = PersistenceFilter.getManager(); pm.flush(); } /** * sets up the cursor using the default page size * * @param cursorString * @param query */ protected void prepareCursor(String cursorString, javax.jdo.Query query) { prepareCursor(cursorString, DEFAULT_RESULT_COUNT, query); } /** * method used to sleep in the event of a retry */ protected static void sleep() { try { Thread.sleep(RETRY_INTERVAL_MILLIS); } catch (InterruptedException e) { // no-op } } /** * Default format for cache key string * * @param object * @return * @throws CacheException */ public String getCacheKey(BaseDomain object) throws CacheException { if (object.getKey() == null) { throw new CacheException("Trying to get cache key from an unsaved object"); } return object.getClass().getSimpleName() + "-" + object.getKey().getId(); } /** * Default format for cache key string * * @param objectId * @return * @throws CacheException */ public String getCacheKey(String objectId) throws CacheException { if (objectId == null) { throw new CacheException("Trying to get cache key from an unsaved object"); } return concreteClass.getSimpleName() + "-" + objectId; } }