package com.psddev.cms.db;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;
import javax.servlet.jsp.tagext.TryCatchFinally;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.psddev.dari.util.JspUtils;
import com.psddev.dari.util.Settings;
public class CacheTag extends BodyTagSupport implements TryCatchFinally {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LoggerFactory.getLogger(CacheTag.class);
private static final ConcurrentMap<String, Output> OUTPUT_LOCKS = new ConcurrentHashMap<String, Output>();
private static final Cache<String, Output> OUTPUT_CACHE = CacheBuilder
.newBuilder()
.maximumSize(Settings.getOrDefault(int.class, "brightspot/cacheTagOutputMaximumSize", 100000))
.removalListener(new RemovalListener<String, Output>() {
@Override
public void onRemoval(RemovalNotification<String, Output> notification) {
OUTPUT_LOCKS.remove(notification.getKey());
}
})
.build();
private String name;
private long duration;
private Output output;
public void setName(String name) {
this.name = name;
}
public void setDuration(long duration) {
this.duration = duration;
}
// --- TagSupport support ---
@Override
public int doStartTag() throws JspException {
String key = JspUtils.getCurrentServletPath((HttpServletRequest) pageContext.getRequest()) + "/" + name;
bodyContent = null;
output = OUTPUT_LOCKS.get(key);
// Output is expired? While producing, it's not considered expired
// because lastProduced field is set far in the future.
if (output != null
&& System.currentTimeMillis() - output.lastProduced > duration) {
setOutput(output, null);
OUTPUT_LOCKS.remove(key);
OUTPUT_CACHE.invalidate(key);
output = null;
}
// Output isn't cached, so flag it to be produced.
if (output == null) {
output = new Output();
output.key = key;
// Make sure there's only one producing output at [R].
Output o = OUTPUT_LOCKS.putIfAbsent(key, output);
if (o == null) {
OUTPUT_CACHE.put(key, output);
LOGGER.debug("Producing [{}] in [{}]", key, Thread.currentThread());
return EVAL_BODY_BUFFERED;
}
output = o;
}
return SKIP_BODY;
}
private void setOutput(Output output, String body) {
synchronized (output) {
output.body = body;
output.lastProduced = System.currentTimeMillis();
output.notifyAll();
LOGGER.debug("Produced [{}] at [{}]", output.key, output.lastProduced);
}
}
@Override
public int doEndTag() throws JspException {
String body;
// [R] Cache the produced output and wake up all other threads
// that might be waiting.
if (bodyContent != null) {
body = bodyContent.getString();
setOutput(output, body);
// Wait if another thread is producing output.
} else {
try {
synchronized (output) {
while (output.lastProduced == Output.PRODUCING) {
LOGGER.debug("Waiting for production of [{}] in [{}]", output.key, Thread.currentThread());
output.wait(1000);
}
}
} catch (InterruptedException ex) {
throw new JspException(ex);
}
body = output.body;
}
try {
if (body != null) {
pageContext.getOut().write(body);
}
} catch (IOException ex) {
throw new JspException(ex);
}
return EVAL_PAGE;
}
// --- TryCatchFinally support ---
@Override
public void doCatch(Throwable error) throws Throwable {
setOutput(output, null);
OUTPUT_LOCKS.remove(output.key);
OUTPUT_CACHE.invalidate(output.key);
throw error;
}
@Override
public void doFinally() {
}
private static class Output {
public static final long PRODUCING = Long.MAX_VALUE;
public String key;
public String body;
public long lastProduced = PRODUCING;
}
}