IncludeElement.java
/*
* 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.esigate.extension.parallelesi;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RunnableFuture;
import java.util.regex.Pattern;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.esigate.ConfigurationException;
import org.esigate.Driver;
import org.esigate.DriverFactory;
import org.esigate.HttpErrorPage;
import org.esigate.Parameters;
import org.esigate.Renderer;
import org.esigate.http.HttpResponseUtils;
import org.esigate.impl.DriverRequest;
import org.esigate.parser.future.CharSequenceFuture;
import org.esigate.parser.future.FutureElement;
import org.esigate.parser.future.FutureElementType;
import org.esigate.parser.future.FutureParserContext;
import org.esigate.parser.future.StringBuilderFutureAppendable;
import org.esigate.xml.XpathRenderer;
import org.esigate.xml.XsltRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class IncludeElement extends BaseElement {
private static final String PROVIDER_PATTERN = "$(PROVIDER{";
private static final String LEGACY_PROVIDER_PATTERN = "$PROVIDER({";
private static final Logger LOG = LoggerFactory.getLogger(IncludeElement.class);
private static final class IncludeTask implements Callable<CharSequence> {
private String src;
private String alt;
private FutureParserContext ctx;
private boolean ignoreError;
private FutureElement current;
private Tag includeTag;
private Map<String, CharSequence> fragmentReplacements;
private Map<String, CharSequence> regexpReplacements;
private Executor executor;
private IncludeTask(Tag includeTag, String src, String alt, FutureParserContext ctx, FutureElement current,
boolean ignoreError, Map<String, CharSequence> fragmentReplacements,
Map<String, CharSequence> regexpReplacements, Executor executor) {
this.src = src;
this.alt = alt;
this.ctx = ctx;
this.ignoreError = ignoreError;
this.current = current;
this.includeTag = includeTag;
this.fragmentReplacements = fragmentReplacements;
this.regexpReplacements = regexpReplacements;
this.executor = executor;
}
@Override
public CharSequence call() throws IOException, HttpErrorPage {
LOG.debug("Starting include task {}", this.src);
StringBuilderWriter sw = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
Exception currentException = null;
// Handle src
try {
processPage(this.src, includeTag, sw);
} catch (IOException | HttpErrorPage e) {
currentException = e;
} catch (ConfigurationException e) {
// case uknown provider : log error
currentException = e;
LOG.error("Esi Include Tag with unknown Provider :" + e.getMessage());
}
// Handle Alt
if (currentException != null && alt != null) {
// Reset exception
currentException = null;
try {
processPage(alt, includeTag, sw);
} catch (IOException | HttpErrorPage e) {
currentException = e;
} catch (ConfigurationException e) {
// case uknown provider : log error
currentException = e;
LOG.error("Esi Include Tag with unknown Provider :" + e.getMessage());
}
}
// Handle onerror
if (currentException != null && !ignoreError && !ctx.reportError(current, currentException)) {
if (currentException instanceof IOException) {
throw (IOException) currentException;
} else if (currentException instanceof HttpErrorPage) {
throw (HttpErrorPage) currentException;
} else if (currentException instanceof ConfigurationException) {
throw (ConfigurationException) currentException;
}
throw new IllegalStateException(
"This type of exception is unexpected here. Should be IOException or HttpErrorPageException or ConfigurationException.",
currentException);
}
// apply regexp replacements
String result = sw.toString();
if (!regexpReplacements.isEmpty()) {
for (Entry<String, CharSequence> entry : regexpReplacements.entrySet()) {
result = Pattern.compile(entry.getKey()).matcher(result).replaceAll(entry.getValue().toString());
}
}
return result;
}
private void processPage(String srcOrAlt, Tag tag, Appendable out) throws IOException, HttpErrorPage {
String fragment = tag.getAttribute("fragment");
String xpath = tag.getAttribute("xpath");
String xslt = tag.getAttribute("stylesheet");
DriverRequest httpRequest = ctx.getHttpRequest();
List<Renderer> rendererList = new ArrayList<>();
Driver driver;
String page;
int idx = srcOrAlt.indexOf(PROVIDER_PATTERN);
int idxLegacyPattern = srcOrAlt.indexOf(LEGACY_PROVIDER_PATTERN);
if (idx < 0 && idxLegacyPattern < 0) {
page = srcOrAlt;
driver = httpRequest.getDriver();
} else if (idx >= 0) {
int startIdx = idx + PROVIDER_PATTERN.length();
int endIndex = srcOrAlt.indexOf("})", startIdx);
String provider = srcOrAlt.substring(startIdx, endIndex);
page = srcOrAlt.substring(endIndex + "})".length());
driver = DriverFactory.getInstance(provider);
if (LOG.isWarnEnabled() && idx > 0) {
LOG.warn("Invalid src attribute : [{}], src should start with [{}{}})]."
+ " First characters [{}] have been ignored", srcOrAlt, PROVIDER_PATTERN, provider,
srcOrAlt.substring(0, idx));
}
} else {
int startIdx = idxLegacyPattern + PROVIDER_PATTERN.length();
int endIndex = srcOrAlt.indexOf("})", startIdx);
String provider = srcOrAlt.substring(startIdx, endIndex);
page = srcOrAlt.substring(endIndex + "})".length());
driver = DriverFactory.getInstance(provider);
if (LOG.isWarnEnabled() && idxLegacyPattern > 0) {
LOG.warn("Invalid src attribute : [{}], src should start with [{}{}})]."
+ " First characters [{}] have been ignored", srcOrAlt, PROVIDER_PATTERN, provider,
srcOrAlt.substring(0, idxLegacyPattern));
}
}
InlineCache ic = InlineCache.getFragment(srcOrAlt);
if (ic != null && !ic.isExpired()) {
String cache = ic.getFragment();
out.append(cache);
} else {
EsiRenderer esiRenderer;
if (fragment != null) {
esiRenderer = new EsiRenderer(page, fragment, executor);
} else {
esiRenderer = new EsiRenderer(executor);
}
if (fragmentReplacements != null && !fragmentReplacements.isEmpty()) {
esiRenderer.setFragmentsToReplace(fragmentReplacements);
}
rendererList.add(esiRenderer);
if (xpath != null) {
rendererList.add(new XpathRenderer(xpath));
} else if (xslt != null) {
rendererList.add(new XsltRenderer(xslt, driver, httpRequest));
}
CloseableHttpResponse response =
driver.render(page, httpRequest.getOriginalRequest(),
rendererList.toArray(new Renderer[rendererList.size()]));
out.append(HttpResponseUtils.toString(response));
}
}
}
public static final FutureElementType TYPE = new BaseElementType("<esi:include", "</esi:include") {
@Override
public IncludeElement newInstance() {
return new IncludeElement();
}
};
private StringBuilderFutureAppendable buf;
private Map<String, CharSequence> fragmentReplacements;
private Map<String, CharSequence> regexpReplacements;
private Tag includeTag;
private boolean write = false;
IncludeElement() {
}
@Override
public void characters(Future<CharSequence> csq) {
if (write) {
buf.enqueueAppend(csq);
}
}
@Override
public void onTagEnd(String tag, FutureParserContext ctx) throws IOException, HttpErrorPage {
write = true;
String src = includeTag.getAttribute("src");
String alt = includeTag.getAttribute("alt");
boolean ignoreError = "continue".equals(includeTag.getAttribute("onerror"));
FutureElement current = ctx.getCurrent();
// write accumulated data into parent
Executor executor = (Executor) ctx.getData(EsiRenderer.DATA_EXECUTOR);
Future<CharSequence> result;
IncludeTask task =
new IncludeTask(includeTag, src, alt, ctx, current, ignoreError, fragmentReplacements,
regexpReplacements, null); // executor is null to disable parallel esi on recursive calls.
if (executor == null) {
// No threads.
CharSequence content = task.call();
result = new CharSequenceFuture(content);
} else {
// Start processing in a new thread.
try {
RunnableFuture<CharSequence> r = new FutureTask<>(task);
executor.execute(r);
result = r;
} catch (RejectedExecutionException e) {
throw new HttpErrorPage(509, "Limits exceeded", e);
}
}
ctx.getCurrent().characters(result);
}
@Override
protected boolean parseTag(Tag tag, FutureParserContext ctx) {
buf = new StringBuilderFutureAppendable();
fragmentReplacements = new HashMap<>();
regexpReplacements = new HashMap<>();
includeTag = tag;
return true;
}
void addFragmentReplacement(String fragment, CharSequence replacement) {
fragmentReplacements.put(fragment, replacement);
}
void addRegexpReplacement(String regexp, CharSequence replacement) {
regexpReplacements.put(regexp, replacement);
}
}