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);
    }

}