IncludeElement.java

  1. /*
  2.  * Licensed under the Apache License, Version 2.0 (the "License");
  3.  * you may not use this file except in compliance with the License.
  4.  * You may obtain a copy of the License at
  5.  *
  6.  * http://www.apache.org/licenses/LICENSE-2.0
  7.  *
  8.  * Unless required by applicable law or agreed to in writing, software
  9.  * distributed under the License is distributed on an "AS IS" BASIS,
  10.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11.  * See the License for the specific language governing permissions and
  12.  * limitations under the License.
  13.  *
  14.  */

  15. package org.esigate.extension.parallelesi;

  16. import java.io.IOException;
  17. import java.util.ArrayList;
  18. import java.util.HashMap;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.Map.Entry;
  22. import java.util.concurrent.Callable;
  23. import java.util.concurrent.Executor;
  24. import java.util.concurrent.Future;
  25. import java.util.concurrent.FutureTask;
  26. import java.util.concurrent.RejectedExecutionException;
  27. import java.util.concurrent.RunnableFuture;
  28. import java.util.regex.Pattern;

  29. import org.apache.commons.io.output.StringBuilderWriter;
  30. import org.apache.http.client.methods.CloseableHttpResponse;
  31. import org.esigate.ConfigurationException;
  32. import org.esigate.Driver;
  33. import org.esigate.DriverFactory;
  34. import org.esigate.HttpErrorPage;
  35. import org.esigate.Parameters;
  36. import org.esigate.Renderer;
  37. import org.esigate.http.HttpResponseUtils;
  38. import org.esigate.impl.DriverRequest;
  39. import org.esigate.parser.future.CharSequenceFuture;
  40. import org.esigate.parser.future.FutureElement;
  41. import org.esigate.parser.future.FutureElementType;
  42. import org.esigate.parser.future.FutureParserContext;
  43. import org.esigate.parser.future.StringBuilderFutureAppendable;
  44. import org.esigate.xml.XpathRenderer;
  45. import org.esigate.xml.XsltRenderer;
  46. import org.slf4j.Logger;
  47. import org.slf4j.LoggerFactory;

  48. class IncludeElement extends BaseElement {
  49.     private static final String PROVIDER_PATTERN = "$(PROVIDER{";
  50.     private static final String LEGACY_PROVIDER_PATTERN = "$PROVIDER({";

  51.     private static final Logger LOG = LoggerFactory.getLogger(IncludeElement.class);

  52.     private static final class IncludeTask implements Callable<CharSequence> {
  53.         private String src;
  54.         private String alt;
  55.         private FutureParserContext ctx;
  56.         private boolean ignoreError;
  57.         private FutureElement current;
  58.         private Tag includeTag;
  59.         private Map<String, CharSequence> fragmentReplacements;
  60.         private Map<String, CharSequence> regexpReplacements;
  61.         private Executor executor;

  62.         private IncludeTask(Tag includeTag, String src, String alt, FutureParserContext ctx, FutureElement current,
  63.                 boolean ignoreError, Map<String, CharSequence> fragmentReplacements,
  64.                 Map<String, CharSequence> regexpReplacements, Executor executor) {
  65.             this.src = src;
  66.             this.alt = alt;
  67.             this.ctx = ctx;
  68.             this.ignoreError = ignoreError;
  69.             this.current = current;
  70.             this.includeTag = includeTag;
  71.             this.fragmentReplacements = fragmentReplacements;
  72.             this.regexpReplacements = regexpReplacements;
  73.             this.executor = executor;
  74.         }

  75.         @Override
  76.         public CharSequence call() throws IOException, HttpErrorPage {
  77.             LOG.debug("Starting include task {}", this.src);
  78.             StringBuilderWriter sw = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);

  79.             Exception currentException = null;
  80.             // Handle src
  81.             try {
  82.                 processPage(this.src, includeTag, sw);
  83.             } catch (IOException | HttpErrorPage e) {
  84.                 currentException = e;
  85.             } catch (ConfigurationException e) {
  86.                 // case uknown provider : log error
  87.                 currentException = e;
  88.                 LOG.error("Esi Include Tag with unknown Provider :" + e.getMessage());
  89.             }

  90.             // Handle Alt
  91.             if (currentException != null && alt != null) {
  92.                 // Reset exception
  93.                 currentException = null;
  94.                 try {
  95.                     processPage(alt, includeTag, sw);
  96.                 } catch (IOException | HttpErrorPage e) {
  97.                     currentException = e;
  98.                 } catch (ConfigurationException e) {
  99.                     // case uknown provider : log error
  100.                     currentException = e;
  101.                     LOG.error("Esi Include Tag with unknown Provider :" + e.getMessage());
  102.                 }
  103.             }

  104.             // Handle onerror
  105.             if (currentException != null && !ignoreError && !ctx.reportError(current, currentException)) {
  106.                 if (currentException instanceof IOException) {
  107.                     throw (IOException) currentException;
  108.                 } else if (currentException instanceof HttpErrorPage) {
  109.                     throw (HttpErrorPage) currentException;
  110.                 } else if (currentException instanceof ConfigurationException) {
  111.                     throw (ConfigurationException) currentException;
  112.                 }
  113.                 throw new IllegalStateException(
  114.                         "This type of exception is unexpected here. Should be IOException or HttpErrorPageException or ConfigurationException.",
  115.                         currentException);
  116.             }

  117.             // apply regexp replacements
  118.             String result = sw.toString();

  119.             if (!regexpReplacements.isEmpty()) {
  120.                 for (Entry<String, CharSequence> entry : regexpReplacements.entrySet()) {

  121.                     result = Pattern.compile(entry.getKey()).matcher(result).replaceAll(entry.getValue().toString());
  122.                 }
  123.             }

  124.             return result;
  125.         }

  126.         private void processPage(String srcOrAlt, Tag tag, Appendable out) throws IOException, HttpErrorPage {
  127.             String fragment = tag.getAttribute("fragment");
  128.             String xpath = tag.getAttribute("xpath");
  129.             String xslt = tag.getAttribute("stylesheet");

  130.             DriverRequest httpRequest = ctx.getHttpRequest();
  131.             List<Renderer> rendererList = new ArrayList<>();
  132.             Driver driver;
  133.             String page;

  134.             int idx = srcOrAlt.indexOf(PROVIDER_PATTERN);
  135.             int idxLegacyPattern = srcOrAlt.indexOf(LEGACY_PROVIDER_PATTERN);
  136.             if (idx < 0 && idxLegacyPattern < 0) {
  137.                 page = srcOrAlt;
  138.                 driver = httpRequest.getDriver();
  139.             } else if (idx >= 0) {
  140.                 int startIdx = idx + PROVIDER_PATTERN.length();
  141.                 int endIndex = srcOrAlt.indexOf("})", startIdx);
  142.                 String provider = srcOrAlt.substring(startIdx, endIndex);
  143.                 page = srcOrAlt.substring(endIndex + "})".length());
  144.                 driver = DriverFactory.getInstance(provider);
  145.                 if (LOG.isWarnEnabled() && idx > 0) {
  146.                     LOG.warn("Invalid src attribute : [{}], src should start with [{}{}})]."
  147.                             + " First characters [{}] have been ignored", srcOrAlt, PROVIDER_PATTERN, provider,
  148.                             srcOrAlt.substring(0, idx));
  149.                 }
  150.             } else {
  151.                 int startIdx = idxLegacyPattern + PROVIDER_PATTERN.length();
  152.                 int endIndex = srcOrAlt.indexOf("})", startIdx);
  153.                 String provider = srcOrAlt.substring(startIdx, endIndex);
  154.                 page = srcOrAlt.substring(endIndex + "})".length());
  155.                 driver = DriverFactory.getInstance(provider);
  156.                 if (LOG.isWarnEnabled() && idxLegacyPattern > 0) {
  157.                     LOG.warn("Invalid src attribute : [{}], src should start with [{}{}})]."
  158.                             + " First characters [{}] have been ignored", srcOrAlt, PROVIDER_PATTERN, provider,
  159.                             srcOrAlt.substring(0, idxLegacyPattern));
  160.                 }
  161.             }

  162.             InlineCache ic = InlineCache.getFragment(srcOrAlt);
  163.             if (ic != null && !ic.isExpired()) {
  164.                 String cache = ic.getFragment();
  165.                 out.append(cache);
  166.             } else {
  167.                 EsiRenderer esiRenderer;
  168.                 if (fragment != null) {
  169.                     esiRenderer = new EsiRenderer(page, fragment, executor);
  170.                 } else {
  171.                     esiRenderer = new EsiRenderer(executor);
  172.                 }
  173.                 if (fragmentReplacements != null && !fragmentReplacements.isEmpty()) {
  174.                     esiRenderer.setFragmentsToReplace(fragmentReplacements);
  175.                 }
  176.                 rendererList.add(esiRenderer);
  177.                 if (xpath != null) {
  178.                     rendererList.add(new XpathRenderer(xpath));
  179.                 } else if (xslt != null) {
  180.                     rendererList.add(new XsltRenderer(xslt, driver, httpRequest));
  181.                 }
  182.                 CloseableHttpResponse response =
  183.                         driver.render(page, httpRequest.getOriginalRequest(),
  184.                                 rendererList.toArray(new Renderer[rendererList.size()]));
  185.                 out.append(HttpResponseUtils.toString(response));
  186.             }
  187.         }

  188.     }

  189.     public static final FutureElementType TYPE = new BaseElementType("<esi:include", "</esi:include") {
  190.         @Override
  191.         public IncludeElement newInstance() {
  192.             return new IncludeElement();
  193.         }

  194.     };

  195.     private StringBuilderFutureAppendable buf;
  196.     private Map<String, CharSequence> fragmentReplacements;
  197.     private Map<String, CharSequence> regexpReplacements;
  198.     private Tag includeTag;
  199.     private boolean write = false;

  200.     IncludeElement() {
  201.     }

  202.     @Override
  203.     public void characters(Future<CharSequence> csq) {
  204.         if (write) {
  205.             buf.enqueueAppend(csq);
  206.         }
  207.     }

  208.     @Override
  209.     public void onTagEnd(String tag, FutureParserContext ctx) throws IOException, HttpErrorPage {
  210.         write = true;
  211.         String src = includeTag.getAttribute("src");
  212.         String alt = includeTag.getAttribute("alt");
  213.         boolean ignoreError = "continue".equals(includeTag.getAttribute("onerror"));
  214.         FutureElement current = ctx.getCurrent();
  215.         // write accumulated data into parent
  216.         Executor executor = (Executor) ctx.getData(EsiRenderer.DATA_EXECUTOR);
  217.         Future<CharSequence> result;
  218.         IncludeTask task =
  219.                 new IncludeTask(includeTag, src, alt, ctx, current, ignoreError, fragmentReplacements,
  220.                         regexpReplacements, null); // executor is null to disable parallel esi on recursive calls.
  221.         if (executor == null) {
  222.             // No threads.
  223.             CharSequence content = task.call();
  224.             result = new CharSequenceFuture(content);
  225.         } else {
  226.             // Start processing in a new thread.
  227.             try {
  228.                 RunnableFuture<CharSequence> r = new FutureTask<>(task);
  229.                 executor.execute(r);
  230.                 result = r;
  231.             } catch (RejectedExecutionException e) {
  232.                 throw new HttpErrorPage(509, "Limits exceeded", e);
  233.             }
  234.         }
  235.         ctx.getCurrent().characters(result);
  236.     }

  237.     @Override
  238.     protected boolean parseTag(Tag tag, FutureParserContext ctx) {
  239.         buf = new StringBuilderFutureAppendable();
  240.         fragmentReplacements = new HashMap<>();
  241.         regexpReplacements = new HashMap<>();
  242.         includeTag = tag;
  243.         return true;
  244.     }

  245.     void addFragmentReplacement(String fragment, CharSequence replacement) {
  246.         fragmentReplacements.put(fragment, replacement);
  247.     }

  248.     void addRegexpReplacement(String regexp, CharSequence replacement) {
  249.         regexpReplacements.put(regexp, replacement);
  250.     }

  251. }