/*
* Copyright 2012 OmniFaces.
*
* 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.omnifaces.component.output;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static javax.faces.event.PhaseId.RENDER_RESPONSE;
import static org.omnifaces.component.output.Cache.PropertyKeys.disabled;
import static org.omnifaces.component.output.Cache.PropertyKeys.key;
import static org.omnifaces.component.output.Cache.PropertyKeys.reset;
import static org.omnifaces.component.output.Cache.PropertyKeys.scope;
import static org.omnifaces.component.output.Cache.PropertyKeys.time;
import static org.omnifaces.component.output.Cache.PropertyKeys.useBuffer;
import static org.omnifaces.filter.OnDemandResponseBufferFilter.BUFFERED_RESPONSE;
import static org.omnifaces.util.Events.subscribeToRequestAfterPhase;
import static org.omnifaces.util.Events.subscribeToViewEvent;
import static org.omnifaces.util.Faces.getRequestAttribute;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import javax.faces.component.FacesComponent;
import javax.faces.component.visit.VisitContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.PreRenderViewEvent;
import javax.faces.event.SystemEvent;
import org.omnifaces.component.output.cache.CacheFactory;
import org.omnifaces.component.output.cache.CacheInitializer;
import org.omnifaces.component.output.cache.el.CacheValue;
import org.omnifaces.filter.OnDemandResponseBufferFilter;
import org.omnifaces.servlet.BufferedHttpServletResponse;
import org.omnifaces.util.Callback;
import org.omnifaces.util.State;
/**
* <p>
* The <code><o:cache></code> component allows to cache a fragment of rendered markup. The first
* request for a page that has this component on it will cause this markup to be put into the cache. Then
* for subsequent requests the cached content is used directly and none of the components, backing beans
* and services that were used to generate this content in the first place will be consulted.
* <p>
* Caching can take place in application scope, or in session scope. For individual fragments a
* time can be specified for which the cached content is valid. After this time is elapsed, the very
* first request to the page containing the cache component in question will cause new content to be
* rendered and put into the cache. A default time can be set per scope in web.xml.
* <p>
* For each scope a maximum capacity can be set. If the capacity for that scope is exceeded, an element will be
* removed following a least recently used policy (LRU).
* <p>
* Via a cache provider mechanism an alternative cache implementation can be configured in web.xml. The default
* cache is based on <a href="http://code.google.com/p/concurrentlinkedhashmap">http://code.google.com/p/concurrentlinkedhashmap</a>.
*
* @since 1.1
* @author Arjan Tijms
* @see CacheValue
*/
@FacesComponent(Cache.COMPONENT_TYPE)
public class Cache extends OutputFamily {
public static final String COMPONENT_TYPE = "org.omnifaces.component.output.Cache";
public static final String VALUE_SET = "org.omnifaces.cache.VALUE_SET";
public static final String DEFAULT_SCOPE = "session";
public static final String START_CONTENT_MARKER = "<!-- START CACHE FOR %s -->";
public static final String END_CONTENT_MARKER = "<!-- END CACHE FOR %s -->";
private static final String ERROR_NO_BUFFERED_RESPONSE = String.format(
"No buffered response found in request, but 'useBuffer' set to true. Check setting the '%s' context parameter or installing the '%s' filter manually.",
CacheInitializer.CACHE_INSTALL_BUFFER_FILTER, OnDemandResponseBufferFilter.class
);
private static Class<? extends SystemEvent> PRE_RENDER = PreRenderViewEvent.class;
private final State state = new State(getStateHelper());
enum PropertyKeys {
key, scope, time, useBuffer, reset, disabled
}
public Cache() {
final FacesContext context = FacesContext.getCurrentInstance();
// Execute the following code in PreRenderView, since at construction time the "useBuffer" and "key" attributes
// have not been set, and there is no @PostContruct for UIComponents.
subscribeToViewEvent(PRE_RENDER, new Callback.Void() {
@Override
public void invoke() {
if (!isDisabled() && isUseBuffer() && !hasCachedValue(context)) {
final BufferedHttpServletResponse bufferedHttpServletResponse = getRequestAttribute(BUFFERED_RESPONSE);
if (bufferedHttpServletResponse == null) {
throw new IllegalStateException(ERROR_NO_BUFFERED_RESPONSE);
}
// Start buffering the response from now on
bufferedHttpServletResponse.setPassThrough(false);
// After the RENDER_RESPONSE phase, copy the area we need to cache from the response buffer
// and insert it into our cache
subscribeToRequestAfterPhase(RENDER_RESPONSE, new Callback.Void() {
@Override
public void invoke() {
String content = null;
try {
content = getContentFromBuffer(bufferedHttpServletResponse.getBufferAsString());
}
catch (IOException e) {
throw new IllegalStateException(e);
}
if (content != null) {
cacheContent(context, content);
}
}
});
}
}
});
}
@Override
public void encodeChildren(FacesContext context) throws IOException {
if (isDisabled()) {
super.encodeChildren(context);
return;
}
String key = getKeyWithDefault(context);
ResponseWriter responseWriter = context.getResponseWriter();
org.omnifaces.component.output.cache.Cache scopedCache = getCacheImpl(context);
if (isReset()) {
scopedCache.remove(key);
}
String childRendering = scopedCache.get(key);
if (childRendering == null) {
Writer bufferWriter = new StringWriter();
ResponseWriter bufferedResponseWriter = responseWriter.cloneWithWriter(bufferWriter);
context.setResponseWriter(bufferedResponseWriter);
try {
if (isUseBuffer()) {
bufferedResponseWriter.write(getStartContentMarker());
}
super.encodeChildren(context);
if (isUseBuffer()) {
bufferedResponseWriter.write(getEndContentMarker());
}
} finally {
context.setResponseWriter(responseWriter);
}
childRendering = bufferWriter.toString();
cacheContent(context, scopedCache, key, childRendering);
}
responseWriter.write(childRendering);
}
/**
* Gets a named attribute associated with the main cache entry this component is using to store
* the rendering of its child components.
*
* @param context the current FacesContext
* @param name name of the attribute to retrieve a value for
* @return value associated with the named attribute
* @since 1.2
*/
public Object getCacheAttribute(FacesContext context, String name) {
return getCacheImpl(context).getAttribute(getKeyWithDefault(context), name);
}
/**
* Sets a named attribute associated with the main cache entry this component is using to store
* the rendering of its child components.
*
* @param context the current FacesContext
* @param name name of the attribute under which the value is stored
* @param value the value that is to be stored
* @since 1.2
*/
public void setCacheAttribute(FacesContext context, String name, Object value) {
getCacheImpl(context).putAttribute(getKeyWithDefault(context), name, value, getTime());
}
@Override
protected boolean isVisitable(VisitContext visitContext) {
FacesContext context = visitContext.getFacesContext();
// Visit us and our children if a value for the cache was set in this request, or
// if no value was cached yet.
return isDisabled() || isCachedValueJustSet(context) || !hasCachedValue(context);
}
private void cacheContent(FacesContext context, String content) {
cacheContent(context, CacheFactory.getCache(context, getScope()), getKeyWithDefault(context), content);
}
private void cacheContent(FacesContext context, org.omnifaces.component.output.cache.Cache scopedCache, String key, String content) {
int time = getTime();
if (time > 0) {
scopedCache.put(key, content, time);
} else {
scopedCache.put(key, content);
}
// Marker to register we added a value to the cache during this request
context.getExternalContext().getRequestMap().put(VALUE_SET, TRUE);
}
private String getKeyWithDefault(FacesContext context) {
String key = getKey();
if (key == null) {
key = context.getViewRoot().getViewId() + "_" + this.getClientId(context);
}
return key;
}
private org.omnifaces.component.output.cache.Cache getCacheImpl(FacesContext context) {
return CacheFactory.getCache(context, getScope());
}
/**
*
* @param context the FacesContext
* @return true if a value was inserted in the cache during this request, false otherwise
*/
private boolean isCachedValueJustSet(FacesContext context) {
return TRUE.equals(context.getExternalContext().getRequestMap().get(VALUE_SET));
}
/**
*
* @param context the FacesContext
* @return true if there is a value in the cache corresponding to this component, false otherwise
*/
private boolean hasCachedValue(FacesContext context) {
return CacheFactory.getCache(context, getScope()).get(getKeyWithDefault(context)) != null;
}
private String getStartContentMarker() {
return String.format(START_CONTENT_MARKER, getClientId());
}
private String getEndContentMarker() {
return String.format(END_CONTENT_MARKER, getClientId());
}
private String getContentFromBuffer(String buffer) {
String startMarker = getStartContentMarker();
int startIndex = buffer.indexOf(startMarker);
if (startIndex != -1) {
String endMarker = getEndContentMarker();
int endIndex = buffer.indexOf(endMarker);
if (endIndex != -1) {
return buffer.substring(startIndex + startMarker.length(), endIndex);
}
}
return null;
}
// Attribute getters/setters --------------------------------------------------------------------------------------
public String getKey() {
return state.get(key);
}
public void setKey(String keyValue) {
state.put(key, keyValue);
}
public String getScope() {
return state.get(scope, DEFAULT_SCOPE);
}
public void setScope(String scopeValue) {
state.put(scope, scopeValue);
}
public Integer getTime() {
return state.get(time, -1);
}
public void setTime(Integer timeValue) {
state.put(time, timeValue);
}
public Boolean isUseBuffer() {
return state.get(useBuffer, FALSE);
}
public void setUseBuffer(Boolean useBufferValue) {
state.put(useBuffer, useBufferValue);
}
public Boolean isReset() {
return state.get(reset, FALSE);
}
public void setReset(Boolean resetValue) {
state.put(reset, resetValue);
}
/**
* Returns whether this cache is disabled.
* @return Whether this cache is disabled.
* @since 1.8
*/
public Boolean isDisabled() {
return state.get(disabled, FALSE);
}
/**
* Sets whether this cache is disabled.
* @param disabled Whether this cache is disabled.
* @since 1.8
*/
public void setDisabled(Boolean disabled) {
state.put(disabled, disabled);
}
}