/* * Copyright 2013-2017 Erudika. https://erudika.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.para.aop; import com.erudika.para.IOListener; import com.erudika.para.Para; import com.erudika.para.annotations.Cached; import com.erudika.para.annotations.Indexed; import com.erudika.para.cache.Cache; import com.erudika.para.core.ParaObject; import com.erudika.para.persistence.DAO; import com.erudika.para.search.Search; import com.erudika.para.utils.Config; import com.erudika.para.validation.ValidationUtils; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.inject.Inject; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is the core method interceptor which enables caching and indexing. * It listens for calls to annotated {@link com.erudika.para.persistence.DAO} methods * and adds searching and caching functionality to them. This technique allows us to control * the caching and searching centrally for all implementations * of the {@link com.erudika.para.persistence.DAO} interface. * @author Alex Bogdanovski [alex@erudika.com] * @see com.erudika.para.persistence.DAO */ @SuppressWarnings("unchecked") public class IndexAndCacheAspect implements MethodInterceptor { private static final Logger logger = LoggerFactory.getLogger(IndexAndCacheAspect.class); private Search search; private Cache cache; /** * @return {@link Search} */ public Search getSearch() { return search; } /** * @param search {@link Search} */ @Inject public void setSearch(Search search) { this.search = search; } /** * @return {@link Cache} */ public Cache getCache() { return cache; } /** * @param cache {@link Cache} */ @Inject public void setCache(Cache cache) { this.cache = cache; } /** * Executes code when a method is invoked. A big switch statement. * @param mi method invocation * @return the returned value of the method invoked or something else (decided here) * @throws Throwable error */ public Object invoke(MethodInvocation mi) throws Throwable { if (!Modifier.isPublic(mi.getMethod().getModifiers())) { return mi.proceed(); } Method m = mi.getMethod(); Method superMethod = null; Indexed indexedAnno = null; Cached cachedAnno = null; try { superMethod = DAO.class.getMethod(m.getName(), m.getParameterTypes()); indexedAnno = Config.isSearchEnabled() ? superMethod.getAnnotation(Indexed.class) : null; cachedAnno = Config.isCacheEnabled() ? superMethod.getAnnotation(Cached.class) : null; } catch (Exception e) { logger.error(null, e); } Object[] args = mi.getArguments(); String appid = AOPUtils.getFirstArgOfString(args); List<IOListener> ioListeners = Para.getIOListeners(); for (IOListener ioListener : ioListeners) { ioListener.onPreInvoke(superMethod, args); logger.debug("Executed {}.onPreInvoke().", ioListener.getClass().getName()); } Object result = handleIndexing(indexedAnno, appid, args, mi); Object cachingResult = handleCaching(cachedAnno, appid, args, mi); // we have a read operation without any result but we get back objects from cache if (result == null && cachingResult != null) { result = cachingResult; } // both searching and caching are disabled - pass it through if (indexedAnno == null && cachedAnno == null) { result = mi.proceed(); } for (IOListener ioListener : ioListeners) { ioListener.onPostInvoke(superMethod, result); logger.debug("Executed {}.onPostInvoke().", ioListener.getClass().getName()); } return result; } private Object handleIndexing(Indexed indexedAnno, String appid, Object[] args, MethodInvocation mi) throws Throwable { Object result = null; if (indexedAnno != null) { switch (indexedAnno.action()) { case ADD: result = addToIndexOperation(appid, args, mi); break; case REMOVE: result = removeFromIndexOperation(appid, args, mi); break; case ADD_ALL: result = addToIndexBatchOperation(appid, args, mi); break; case REMOVE_ALL: result = removeFromIndexBatchOperation(appid, args, mi); break; default: break; } } return result; } private Object handleCaching(Cached cachedAnno, String appid, Object[] args, MethodInvocation mi) throws Throwable { Object result = null; if (cachedAnno != null) { switch (cachedAnno.action()) { case GET: result = readFromCacheOperation(appid, args, mi); break; case PUT: addToCacheOperation(appid, args); break; case DELETE: removeFromCacheOperation(appid, args); break; case GET_ALL: result = readFromCacheBatchOperation(appid, args, mi); break; case PUT_ALL: addToCacheBatchOperation(appid, args); break; case DELETE_ALL: removeFromCacheBatchOperation(appid, args); break; default: break; } } return result; } private Object addToIndexOperation(String appid, Object[] args, MethodInvocation mi) throws Throwable { ParaObject addMe = AOPUtils.getArgOfParaObject(args); String[] errors = ValidationUtils.validateObject(addMe); Object result = null; if (addMe != null && errors.length == 0) { AOPUtils.checkAndFixType(addMe); if (addMe.getStored()) { result = mi.proceed(); } if (addMe.getIndexed()) { search.index(appid, addMe); logger.debug("{}: Indexed {}->{}", getClass().getSimpleName(), appid, addMe.getId()); } } else { logger.warn("{}: Invalid object {}->{} errors: [{}]. Changes weren't persisted.", getClass().getSimpleName(), appid, addMe, String.join("; ", errors)); } return result; } private Object removeFromIndexOperation(String appid, Object[] args, MethodInvocation mi) throws Throwable { Object result = mi.proceed(); // delete from DB even if "isStored = false" ParaObject removeMe = AOPUtils.getArgOfParaObject(args); AOPUtils.checkAndFixType(removeMe); search.unindex(appid, removeMe); // remove from index even if "isIndexed = false" logger.debug("{}: Unindexed {}->{}", getClass().getSimpleName(), appid, (removeMe == null) ? null : removeMe.getId()); return result; } private Object addToIndexBatchOperation(String appid, Object[] args, MethodInvocation mi) throws Throwable { List<ParaObject> addUs = AOPUtils.getArgOfListOfType(args, ParaObject.class); List<ParaObject> indexUs = new LinkedList<ParaObject>(); List<ParaObject> removedObjects = AOPUtils.removeNotStoredNotIndexed(addUs, indexUs); Object result = mi.proceed(); search.indexAll(appid, indexUs); // restore removed objects - needed if we have to cache them later // do not remove this line - breaks tests if (addUs != null) { addUs.addAll(removedObjects); // don't delete! } logger.debug("{}: Indexed all {}->{}", getClass().getSimpleName(), appid, indexUs.size()); return result; } private Object removeFromIndexBatchOperation(String appid, Object[] args, MethodInvocation mi) throws Throwable { List<ParaObject> removeUs = AOPUtils.getArgOfListOfType(args, ParaObject.class); Object result = mi.proceed(); // delete from DB even if "isStored = false" search.unindexAll(appid, removeUs); // remove from index even if "isIndexed = false" logger.debug("{}: Unindexed all {}->{}", getClass().getSimpleName(), appid, (removeUs == null) ? null : removeUs.size()); return result; } private Object readFromCacheOperation(String appid, Object[] args, MethodInvocation mi) throws Throwable { Object result = null; String getMeId = (args != null && args.length > 1) ? (String) args[1] : null; if (cache.contains(appid, getMeId)) { result = cache.get(appid, getMeId); logger.debug("{}: Cache hit: {}->{}", getClass().getSimpleName(), appid, getMeId); } else if (getMeId != null) { result = mi.proceed(); if (result != null && ((ParaObject) result).getCached()) { cache.put(appid, getMeId, result); logger.debug("{}: Cache miss: {}->{}", getClass().getSimpleName(), appid, getMeId); } } return result; } private void addToCacheOperation(String appid, Object[] args) { ParaObject putMe = AOPUtils.getArgOfParaObject(args); if (putMe != null && putMe.getCached()) { cache.put(appid, putMe.getId(), putMe); logger.debug("{}: Cache put: {}->{}", getClass().getSimpleName(), appid, putMe.getId()); } } private void removeFromCacheOperation(String appid, Object[] args) { ParaObject deleteMe = AOPUtils.getArgOfParaObject(args); if (deleteMe != null) { // clear from cache even if "isCached = false" cache.remove(appid, deleteMe.getId()); logger.debug("{}: Cache delete: {}->{}", getClass().getSimpleName(), appid, deleteMe.getId()); } } private Object readFromCacheBatchOperation(String appid, Object[] args, MethodInvocation mi) throws Throwable { Object result = Collections.emptyMap(); List<String> getUs = AOPUtils.getArgOfListOfType(args, String.class); if (getUs != null) { Map<String, ParaObject> cached = cache.getAll(appid, getUs); logger.debug("{}: Cache getAll(): {}->{}", getClass().getSimpleName(), appid, getUs); // hit the database if even a single object is missing from cache, then cache it if (cached.size() < getUs.size()) { logger.debug("{}: Cache getAll() will read from DB: {}", getClass().getSimpleName(), appid); result = mi.proceed(); if (result != null) { for (String id : getUs) { logger.debug("{}: Cache getAll() got from DB: {}", getClass().getSimpleName(), id); if (!cached.containsKey(id)) { ParaObject obj = ((Map<String, ParaObject>) result).get(id); if (obj != null && obj.getCached()) { cache.put(appid, obj.getId(), obj); logger.debug("{}: Cache miss on readAll: {}->{}", getClass().getSimpleName(), appid, id); } } } } } if (result == null || ((Map) result).isEmpty()) { result = cached; } } return result; } private void addToCacheBatchOperation(String appid, Object[] args) { List<ParaObject> putUs = AOPUtils.getArgOfListOfType(args, ParaObject.class); if (putUs != null && !putUs.isEmpty()) { Map<String, ParaObject> map1 = new LinkedHashMap<String, ParaObject>(putUs.size()); for (ParaObject obj : putUs) { if (obj != null && obj.getCached()) { map1.put(obj.getId(), obj); } } if (!map1.isEmpty()) { cache.putAll(appid, map1); } logger.debug("{}: Cache put page: {}->{}", getClass().getSimpleName(), appid, map1.keySet()); } } private void removeFromCacheBatchOperation(String appid, Object[] args) { List<ParaObject> deleteUs = AOPUtils.getArgOfListOfType(args, ParaObject.class); if (deleteUs != null && !deleteUs.isEmpty()) { List<String> list = new ArrayList<String>(deleteUs.size()); for (ParaObject paraObject : deleteUs) { list.add(paraObject.getId()); } // clear from cache even if "isCached = false" cache.removeAll(appid, list); logger.debug("{}: Cache delete page: {}->{}", getClass().getSimpleName(), appid, list); } } }