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.esi;

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.regex.Pattern;

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.Adapter;
import org.esigate.parser.ElementType;
import org.esigate.parser.ParserContext;
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);
    public static final ElementType TYPE = new BaseElementType("<esi:include", "</esi:include") {
        @Override
        public IncludeElement newInstance() {
            return new IncludeElement();
        }

    };

    private final Appendable outAdapter = new Adapter(IncludeElement.this);
    private StringBuilder buf;
    private Map<String, CharSequence> fragmentReplacements;
    private Map<String, CharSequence> regexpReplacements;
    private Tag includeTag;
    private boolean write = false;

    IncludeElement() {
    }

    @Override
    public void characters(CharSequence csq, int start, int end) {
        if (write) {
            buf.append(csq, start, end);
        }
    }

    @Override
    public void onTagEnd(String tag, ParserContext ctx) throws IOException, HttpErrorPage {
        write = true;
        String src = includeTag.getAttribute("src");
        String alt = includeTag.getAttribute("alt");
        boolean ignoreError = "continue".equals(includeTag.getAttribute("onerror"));

        Exception currentException = null;
        // Handle src
        try {
            processPage(src, includeTag, ctx);
        } 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, ctx);
            } 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(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
        if (!regexpReplacements.isEmpty()) {
            for (Entry<String, CharSequence> entry : regexpReplacements.entrySet()) {
                buf =
                        new StringBuilder(Pattern.compile(entry.getKey()).matcher(buf)
                                .replaceAll(entry.getValue().toString()));
            }
        }

        // write accumulated data into parent
        ctx.getCurrent().characters(buf, 0, buf.length());
    }

    @Override
    protected boolean parseTag(Tag tag, ParserContext ctx) {
        buf = new StringBuilder(Parameters.DEFAULT_BUFFER_SIZE);
        fragmentReplacements = new HashMap<>();
        regexpReplacements = new HashMap<>();
        includeTag = tag;
        return true;
    }

    private void processPage(String src, Tag tag, ParserContext ctx) 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 = src.indexOf(PROVIDER_PATTERN);
        int idxLegacyPattern = src.indexOf(LEGACY_PROVIDER_PATTERN);
        if (idx < 0 && idxLegacyPattern < 0) {
            page = src;
            driver = httpRequest.getDriver();
        } else if (idx >= 0) {

            int startIdx = idx + PROVIDER_PATTERN.length();
            int endIndex = src.indexOf("})", startIdx);
            String provider = src.substring(startIdx, endIndex);
            page = src.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", src, PROVIDER_PATTERN, provider,
                        src.substring(0, idx));
            }

        } else {
            int startIdx = idxLegacyPattern + PROVIDER_PATTERN.length();
            int endIndex = src.indexOf("})", startIdx);
            String provider = src.substring(startIdx, endIndex);
            page = src.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", src, PROVIDER_PATTERN, provider,
                        src.substring(0, idxLegacyPattern));
            }
        }

        InlineCache ic = InlineCache.getFragment(src);
        if (ic != null && !ic.isExpired()) {
            String cache = ic.getFragment();
            characters(cache, 0, cache.length());
        } else {
            EsiRenderer esiRenderer;
            if (fragment != null) {
                esiRenderer = new EsiRenderer(page, fragment);
            } else {
                esiRenderer = new EsiRenderer();
            }
            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()]));
            outAdapter.append(HttpResponseUtils.toString(response));
        }
    }

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

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

}