Java tutorial
/* * Copyright (c) 2009, 2010, 2011, 2012, 2013, B3log Team * * 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. */ package org.b3log.latke.cache; import java.io.Serializable; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.b3log.latke.Latkes; import org.b3log.latke.RuntimeEnv; import org.b3log.latke.event.Event; import org.b3log.latke.event.EventException; import org.b3log.latke.event.EventManager; import org.b3log.latke.util.Requests; import org.b3log.latke.util.Serializer; import org.b3log.latke.util.Strings; import org.b3log.latke.util.freemarker.Templates; import org.json.JSONObject; /** * Page cache. * * <p> * This cache contains some pages and their statistics as the following: * <pre> * <pageCacheKey1, JSONObject1{oId, title, title, type}> * <pageCacheKey2, JSONObject2{oId, title, title, type}> * .... * </pre> * </p> * * <p> * <i>Page Cache Key</i> generated by method {@linkplain #getPageCacheKey(java.lang.String, java.lang.String)}. * </p> * * <p> * <b>Note</b>: The method <a href="http://code.google.com/appengine/docs/java/javadoc/ * com/google/appengine/api/memcache/MemcacheService.html#clearAll%28%29"> * clearAll</a> of <a href="http://code.google.com/appengine/docs/java/javadoc/ * com/google/appengine/api/memcache/MemcacheService.html">MemcacheService</a> * does not respect namespaces - this flushes the cache for every namespace. * </p> * * @author <a href="mailto:DL88250@gmail.com">Liang Ding</a> * @version 1.0.3.5, May 28, 2013 * @since 0.3.1 */ @SuppressWarnings("unchecked") public final class PageCaches { /** * Logger. */ private static final Logger LOGGER = Logger.getLogger(PageCaches.class.getName()); /** * Page cache. * <p> * <requestURI?queryString, page info> * </p> */ private static final Cache<String, Serializable> CACHE; /** * Event manager. */ private static final EventManager EVT_SVC = EventManager.getInstance(); /** * Cached page keys. */ private static final Set<String> KEYS = new HashSet<String>(); /** * Indicates an remove cache event. */ public static final String REMOVE_CACHE = "Remove Cache"; /** * Key of page cache name. */ public static final String PAGE_CACHE_NAME = "page"; /** * Key of cached time. */ public static final String CACHED_TIME = "cachedTime"; /** * Key of cached bytes length. */ public static final String CACHED_BYTES_LENGTH = "cachedBtypesLength"; /** * key of cached hit count. */ public static final String CACHED_HIT_COUNT = "cachedHitCount"; /** * Key of cached title. */ public static final String CACHED_TITLE = "cachedTitle"; /** * Key of cached object id. */ public static final String CACHED_OID = "cachedOid"; /** * Key of cached HTML content. */ public static final String CACHED_CONTENT = "cachedContent"; /** * Key of cached password. */ public static final String CACHED_PWD = "cachedPwd"; /** * Key of cached type. */ public static final String CACHED_TYPE = "cachedType"; /** * Key of cached link. */ public static final String CACHED_LINK = "cachedLink"; /** * Initializes the cache. */ static { CACHE = (Cache<String, Serializable>) CacheFactory.getCache(PAGE_CACHE_NAME); final RuntimeEnv runtimeEnv = Latkes.getRuntimeEnv(); if (RuntimeEnv.LOCAL == runtimeEnv || RuntimeEnv.BAE == runtimeEnv) { int maxCnt = Integer.MAX_VALUE; final String maxPageCntStr = Latkes.getLocalProperty("cache.maxPageCnt"); if (!Strings.isEmptyOrNull(maxPageCntStr)) { maxCnt = Integer.parseInt(maxPageCntStr); } if (0 >= maxCnt) { Latkes.disablePageCache(); } else { CACHE.setMaxCount(maxCnt); LOGGER.log(Level.INFO, "Initialized page cache[maxCount={0}]", maxCnt); } } } /** * Gets a page cache key by the specified URI and query string. * * @param uri the specified URI * @param queryString the specified query string * @return cache key */ public static String getPageCacheKey(final String uri, final String queryString) { String ret = uri; try { if (!Strings.isEmptyOrNull(queryString)) { ret += "?" + queryString; } return ret; } catch (final Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } return ret; } /** * Gets all cached page keys. * * <p> * <b>Note</b>: Do NOT modify the returned keys set directly. * </p> * * @return cached page keys, returns an empty set if not found */ @SuppressWarnings("unchecked") public static Set<String> getKeys() { syncKeys(); return KEYS; } /** * Gets cache. * * @return cache */ public static Cache<String, ?> getCache() { return CACHE; } /** * Gets a cached page with the specified page cache key. * * <p> * Invoking this method will NOT change statistic (cache view count), * see {@link #get(java.lang.String, javax.servlet.http.HttpServletRequest)} * if you want to update the statistic. * </p> * * <p> * <b>Note</b>: Do NOT modify properties of the returned json object, * </p> * * @param pageCacheKey the specified page cache key * @return for example, * <pre> * { * "cachedContent: "", * "cachedOid": "", * "cachedTitle": "", * "cachedType": "", * "cachedBytesLength": int, * "cachedHitCount": long, * "cachedTime": long * } * </pre> */ public static JSONObject get(final String pageCacheKey) { return (JSONObject) CACHE.get(pageCacheKey); } /** * Gets a cached page with the specified page cache key and update * stat. flag. * * <p> * Invoking this method may change statistic, such as to update the * cache hit count. But if the specified request made from a search engine * bot, will NOT change statistic field. * </p> * * <p> * The {@link #get(java.lang.String)} method will return a cached page * without update statistic. * </p> * * <p> * <b>Note</b>: Do NOT modify properties of the returned json object, * </p> * * @param pageCacheKey the specified page cache key * @param request the specified request * @param response the specified response * @return for example, * <pre> * { * "cachedContent: "", * "cachedOid": "", * "cachedTitle": "", * "cachedType": "", * "cachedBytesLength": int, * "cachedHitCount": long, * "cachedTime": long * } * </pre> * @see Requests#searchEngineBotRequest(javax.servlet.http.HttpServletRequest) * @see Requests#hasBeenServed(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) * @see #get(java.lang.String) */ public static JSONObject get(final String pageCacheKey, final HttpServletRequest request, final HttpServletResponse response) { final JSONObject ret = (JSONObject) CACHE.get(pageCacheKey); if (null == ret) { return null; } try { if (!Requests.searchEngineBotRequest(request) && !Requests.hasBeenServed(request, response)) { final long hitCount = ret.getLong(CACHED_HIT_COUNT); ret.put(CACHED_HIT_COUNT, hitCount + 1); } CACHE.put(pageCacheKey, ret); KEYS.add(pageCacheKey); } catch (final Exception e) { LOGGER.log(Level.WARNING, "Set stat. of cached page[pageCacheKey=" + pageCacheKey + "] failed", e); } return ret; } /** * Puts a page into cache. * * <p> * Invoking this method may change statistic, such as to initialize the * cache hit count to 1. But if the specified request made from a search * engine bot, will initialize the cache hit count to 0. * </p> * * @param pageKey key of the page to put * @param cachedValue value to put, for example, * <pre> * { * "cachedContent: "", * "cachedOid": "", * "cachedTitle": "", * "cachedType": "", * "cachedBytesLength": int, * "cachedHitCount": long, * "cachedTime": long * } * </pre> * @param request the specified request */ public static void put(final String pageKey, final JSONObject cachedValue, final HttpServletRequest request) { check(cachedValue); try { final String content = cachedValue.getString(CACHED_CONTENT); final byte[] bytes = Serializer.serialize(content); if (Requests.searchEngineBotRequest(request)) { cachedValue.put(CACHED_HIT_COUNT, 0L); } else { cachedValue.put(CACHED_HIT_COUNT, 1L); } cachedValue.put(CACHED_BYTES_LENGTH, bytes.length); cachedValue.put(CACHED_TIME, System.currentTimeMillis()); } catch (final Exception e) { LOGGER.log(Level.WARNING, "Cache stat. failed[pageKey=" + pageKey + "]", e); } CACHE.put(pageKey, cachedValue); KEYS.add(pageKey); LOGGER.log(Level.FINEST, "Put a page[key={0}] into page cache, cached keys[size={1}, {2}]", new Object[] { pageKey, KEYS.size(), KEYS }); } /** * Removes a cached pages specified by the given page key. * * <p> * <b>Note</b>: In addition to remove cached page content, invoking this * method will remove template of the cached page corresponds to. * </p> * * @param pageKey the given page key */ public static void remove(final String pageKey) { CACHE.remove(pageKey); KEYS.remove(pageKey); Templates.CACHE.clear(); } /** * Removes all cached pages and cached templates. * * <p> * A synchronous event will be fired before the removal, event type is {@value #REMOVE_CACHE}, see the {@link #REMOVE_CACHE}. * </p> * * <p> * <b>Note</b>: This method will flush the cache for every namespace (clears all caches). * </p> */ public static void removeAll() { try { EVT_SVC.fireEventSynchronously(new Event<Void>(REMOVE_CACHE, null)); } catch (final EventException e) { LOGGER.log(Level.SEVERE, "Remove page cache event execute failed, can not remove all cache", e); return; } CacheFactory.removeAll(); Templates.CACHE.clear(); KEYS.clear(); LOGGER.info("Removed all cache...."); } /** * Synchronizes the {@linkplain #KEYS keys} collection and cached page * objects. */ private static void syncKeys() { @SuppressWarnings("unchecked") final Iterator<String> iterator = KEYS.iterator(); final Set<String> toRemove = new HashSet<String>(); while (iterator.hasNext()) { final String key = iterator.next(); if (!CACHE.contains(key)) { toRemove.add(key); // iterator.remove() will also throw ConcurrentModificationException on GAE } } if (!toRemove.isEmpty()) { KEYS.removeAll(toRemove); LOGGER.log(Level.FINER, "Removed page cache keys[{0}] for sync", toRemove); } } /** * Checks if all keys of the specified cached page are ready. * * @param cachedPage the specified cached page */ private static void check(final JSONObject cachedPage) { if (!cachedPage.has(CACHED_CONTENT) || !cachedPage.has(CACHED_OID) || !cachedPage.has(CACHED_TITLE) || !cachedPage.has(CACHED_TYPE) || !cachedPage.has(CACHED_LINK)) { throw new IllegalArgumentException("Illegal arguments for caching page, resolve this bug first!"); } } /** * Private default constructor. */ private PageCaches() { } }