/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.rendering.internal.macro.cache; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.xwiki.cache.Cache; import org.xwiki.cache.CacheException; import org.xwiki.cache.CacheManager; import org.xwiki.cache.config.LRUCacheConfiguration; import org.xwiki.component.annotation.Component; import org.xwiki.rendering.block.Block; import org.xwiki.rendering.macro.AbstractMacro; import org.xwiki.rendering.macro.MacroContentParser; import org.xwiki.rendering.macro.MacroExecutionException; import org.xwiki.rendering.macro.cache.CacheMacroParameters; import org.xwiki.rendering.macro.descriptor.DefaultContentDescriptor; import org.xwiki.rendering.renderer.BlockRenderer; import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter; import org.xwiki.rendering.renderer.printer.WikiPrinter; import org.xwiki.rendering.transformation.MacroTransformationContext; /** * Provides Caching for the content of the macro. * * @version $Id: 5742eb61fc9cd8768bfb5b0a232882ce4ac9127a $ * @since 3.0M1 */ @Component @Named("cache") @Singleton public class CacheMacro extends AbstractMacro<CacheMacroParameters> { /** * The description of the macro. */ private static final String DESCRIPTION = "Caches content."; /** * The description of the macro content. */ private static final String CONTENT_DESCRIPTION = "the content to cache."; /** * Used to create the macro content cache. */ @Inject private CacheManager cacheManager; /** * The parser used to parse the content (when not cached). */ @Inject private MacroContentParser contentParser; /** * Renders the optional id parameter as plain text to use the result as a cache key. */ @Inject @Named("plain/1.0") private BlockRenderer plainTextBlockRenderer; /** * Map of all caches. There's one cache per timeToLive/maxEntry combination since currently we cannot set these * configuration values at the cache entry level but only for the whole cache. */ private Map<CacheKey, Cache<List<Block>>> contentCacheMap = new ConcurrentHashMap<>(); /** * Create and initialize the descriptor of the macro. */ public CacheMacro() { super("Cache", DESCRIPTION, new DefaultContentDescriptor(CONTENT_DESCRIPTION), CacheMacroParameters.class); setDefaultCategory(DEFAULT_CATEGORY_DEVELOPMENT); } @Override public boolean supportsInlineMode() { return true; } @Override public List<Block> execute(CacheMacroParameters parameters, String content, MacroTransformationContext context) throws MacroExecutionException { // Idea for improvement: use context.getId() (which contains the document name) as part of the cache key to // make it even more unique (when the cache macro parameter id is not specified). String cacheKey; if (parameters.getId() != null) { // Consider that the id contains wiki syntax and parse it with the same wiki parser than the current // transformation is using and render the result as plain text. WikiPrinter printer = new DefaultWikiPrinter(); this.plainTextBlockRenderer.render(this.contentParser.parse(parameters.getId(), context, true, false), printer); cacheKey = printer.toString(); } else { cacheKey = content; } Cache<List<Block>> contentCache = getContentCache(parameters.getTimeToLive(), parameters.getMaxEntries()); List<Block> result = contentCache.get(cacheKey); if (result == null) { // Run the parser for the syntax on the content // We run the current transformation on the cache macro content. We need to do this since we want to cache // the XDOM resulting from the execution of Macros because that's where lengthy processing happens. result = this.contentParser.parse(content, context, true, context.isInline()).getChildren(); contentCache.set(cacheKey, result); } return result; } /** * Get a cache matching the passed time to live and max entries. * <p> * Note that whenever a new cache is created it currently means a new thread is used too (since the JBoss cache used * underneath uses a thread for evicting entries from the cache). We need to modify our xwiki-cache module to allow * setting time to live on cache items, see https://jira.xwiki.org/browse/XWIKI-5907 * </p> * * @param lifespan the number of seconds to cache the content * @param maxEntries the maximum number of entries in the cache (Least Recently Used entries are ejected) * @return the matching cache (a new cache is created if no existing one is found) * @throws MacroExecutionException in case we fail to create the new cache */ Cache<List<Block>> getContentCache(int lifespan, int maxEntries) throws MacroExecutionException { CacheKey cacheKey = new CacheKey(lifespan, maxEntries); Cache<List<Block>> contentCache = this.contentCacheMap.get(cacheKey); if (contentCache == null) { // Create Cache LRUCacheConfiguration configuration = new LRUCacheConfiguration(String.format("cacheMacro.%s", cacheKey.toString()), maxEntries); configuration.getLRUEvictionConfiguration().setLifespan(lifespan); try { contentCache = this.cacheManager.createNewLocalCache(configuration); } catch (CacheException e) { throw new MacroExecutionException("Failed to create content cache", e); } this.contentCacheMap.put(cacheKey, contentCache); } return contentCache; } }