org.apache.sling.scripting.sightly.impl.html.dom.MarkupHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.scripting.sightly.impl.html.dom.MarkupHandler.java

Source

/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.sling.scripting.sightly.impl.html.dom;

import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.commons.lang.StringUtils;
import org.apache.sling.scripting.sightly.compiler.RuntimeFunction;
import org.apache.sling.scripting.sightly.compiler.SightlyCompilerException;
import org.apache.sling.scripting.sightly.compiler.commands.Conditional;
import org.apache.sling.scripting.sightly.compiler.commands.OutText;
import org.apache.sling.scripting.sightly.compiler.commands.OutputVariable;
import org.apache.sling.scripting.sightly.compiler.commands.VariableBinding;
import org.apache.sling.scripting.sightly.compiler.expression.Expression;
import org.apache.sling.scripting.sightly.compiler.expression.ExpressionNode;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperation;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.BinaryOperator;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.BooleanConstant;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.Identifier;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.RuntimeCall;
import org.apache.sling.scripting.sightly.compiler.expression.nodes.StringConstant;
import org.apache.sling.scripting.sightly.impl.compiler.Patterns;
import org.apache.sling.scripting.sightly.impl.compiler.PushStream;
import org.apache.sling.scripting.sightly.impl.compiler.Syntax;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.CompilerContext;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.ElementContext;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.ExpressionParser;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.ExpressionWrapper;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.Fragment;
import org.apache.sling.scripting.sightly.impl.compiler.frontend.Interpolation;
import org.apache.sling.scripting.sightly.impl.compiler.util.SymbolGenerator;
import org.apache.sling.scripting.sightly.impl.filter.ExpressionContext;
import org.apache.sling.scripting.sightly.impl.filter.Filter;
import org.apache.sling.scripting.sightly.impl.html.MarkupUtils;
import org.apache.sling.scripting.sightly.compiler.expression.MarkupContext;
import org.apache.sling.scripting.sightly.impl.plugin.Plugin;
import org.apache.sling.scripting.sightly.impl.plugin.PluginCallInfo;
import org.apache.sling.scripting.sightly.impl.plugin.PluginInvoke;

/**
 * Implementation for the markup handler
 */
public class MarkupHandler {

    private final PushStream stream;
    private final SymbolGenerator symbolGenerator = new SymbolGenerator();
    private final ExpressionParser expressionParser = new ExpressionParser();
    private final Map<String, Plugin> pluginRegistry;
    private final CompilerContext compilerContext;
    private final ExpressionWrapper expressionWrapper;

    private final Stack<ElementContext> elementStack = new Stack<>();

    public MarkupHandler(PushStream stream, Map<String, Plugin> pluginRegistry, List<Filter> filters) {
        this.stream = stream;
        this.pluginRegistry = pluginRegistry;
        this.expressionWrapper = new ExpressionWrapper(filters);
        this.compilerContext = new CompilerContext(symbolGenerator, expressionWrapper, stream);
    }

    public void onOpenTagStart(String markup, String tagName) {
        ElementContext context = new ElementContext(tagName, markup);
        elementStack.push(context);
    }

    public void onAttribute(String name, String value, char quoteChar) {
        ElementContext context = elementStack.peek();
        if (Syntax.isPluginAttribute(name)) {
            handlePlugin(name, StringUtils.defaultString(value, ""), context);
        } else {
            context.addAttribute(name, value, quoteChar);
        }
    }

    public void onOpenTagEnd(String markup) {
        ElementContext context = elementStack.peek();
        PluginInvoke invoke = context.pluginInvoke();
        invoke.beforeElement(stream, context.getTagName());
        boolean slyTag = "sly".equalsIgnoreCase(context.getTagName());
        if (slyTag) {
            Patterns.beginStreamIgnore(stream);
        }
        invoke.beforeTagOpen(stream);
        out(context.getOpenTagStartMarkup());
        invoke.beforeAttributes(stream);
        traverseAttributes(context, invoke);
        invoke.afterAttributes(stream);
        out(markup);
        invoke.afterTagOpen(stream);
        if (slyTag) {
            Patterns.endStreamIgnore(stream);
        }
        invoke.beforeChildren(stream);
    }

    private void traverseAttributes(ElementContext context, PluginInvoke invoke) {
        for (ElementContext.Attribute attribute : context.getAttributes()) {
            String attrName = attribute.getName();
            Object contentObj = attribute.getValue();
            if (contentObj == null || contentObj instanceof String) {
                String content = (String) contentObj;
                emitAttribute(attrName, content, attribute.getQuoteChar(), invoke);
            } else if (contentObj instanceof Map.Entry) {
                Map.Entry entry = (Map.Entry) contentObj;
                PluginCallInfo info = (PluginCallInfo) entry.getKey();
                Expression expression = (Expression) entry.getValue();
                invoke.onPluginCall(stream, info, expression);
            }
        }
    }

    private void emitAttribute(String name, String content, char quoteChar, PluginInvoke invoke) {
        invoke.beforeAttribute(stream, name);
        if (content == null) {
            emitSimpleTextAttribute(name, null, quoteChar, invoke);
        } else {
            Interpolation interpolation = expressionParser.parseInterpolation(content);
            String text = tryAsSimpleText(interpolation);
            if (text != null) {
                emitSimpleTextAttribute(name, text, quoteChar, invoke);
            } else {
                emitExpressionAttribute(name, interpolation, quoteChar, invoke);
            }
        }
        invoke.afterAttribute(stream, name);
    }

    private void emitSimpleTextAttribute(String name, String textValue, char quoteChar, PluginInvoke invoke) {
        emitAttributeStart(name);
        invoke.beforeAttributeValue(stream, name, new StringConstant(textValue));
        if (textValue != null) {
            emitAttributeValueStart(quoteChar);
            textValue = escapeQuotes(textValue);
            out(textValue);
            emitAttributeEnd(quoteChar);
        }
        invoke.afterAttributeValue(stream, name);
    }

    private String escapeQuotes(String textValue) {
        return textValue.replace("\"", "&quot;");
    }

    private void emitExpressionAttribute(String name, Interpolation interpolation, char quoteChar,
            PluginInvoke invoke) {
        interpolation = attributeChecked(name, interpolation);
        if (interpolation.size() == 1) {
            emitSingleFragment(name, interpolation, quoteChar, invoke);
        } else {
            emitMultipleFragment(name, interpolation, quoteChar, invoke);
        }
    }

    private void emitMultipleFragment(String name, Interpolation interpolation, char quoteChar,
            PluginInvoke invoke) {
        // Simplified algorithm for attribute output, which works when the interpolation is not of size 1. In this
        // case we are certain that the attribute value cannot be the boolean value true, so we can skip this test
        // altogether
        Expression expression = expressionWrapper.transform(interpolation, getAttributeMarkupContext(name),
                ExpressionContext.ATTRIBUTE);
        String attrContent = symbolGenerator.next("attrContent");
        String shouldDisplayAttr = symbolGenerator.next("shouldDisplayAttr");
        stream.write(new VariableBinding.Start(attrContent, expression.getRoot()));
        stream.write(new VariableBinding.Start(shouldDisplayAttr, new BinaryOperation(BinaryOperator.OR,
                new Identifier(attrContent),
                new BinaryOperation(BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrContent)))));
        stream.write(new Conditional.Start(shouldDisplayAttr, true));
        emitAttributeStart(name);
        invoke.beforeAttributeValue(stream, name, expression.getRoot());
        emitAttributeValueStart(quoteChar);
        stream.write(new OutputVariable(attrContent));
        emitAttributeEnd(quoteChar);
        invoke.afterAttributeValue(stream, name);
        stream.write(Conditional.END);
        stream.write(VariableBinding.END);
        stream.write(VariableBinding.END);
    }

    private void emitSingleFragment(String name, Interpolation interpolation, char quoteChar, PluginInvoke invoke) {
        Expression valueExpression = expressionWrapper.transform(interpolation, null, ExpressionContext.ATTRIBUTE); //raw expression
        String attrValue = symbolGenerator.next("attrValue"); //holds the raw attribute value
        String attrContent = symbolGenerator.next("attrContent"); //holds the escaped attribute value
        String isTrueVar = symbolGenerator.next("isTrueAttr"); // holds the comparison (attrValue == true)
        String shouldDisplayAttr = symbolGenerator.next("shouldDisplayAttr");
        MarkupContext markupContext = getAttributeMarkupContext(name);
        boolean alreadyEscaped = false;
        if (valueExpression.getRoot() instanceof RuntimeCall) {
            RuntimeCall rc = (RuntimeCall) valueExpression.getRoot();
            if (RuntimeFunction.XSS.equals(rc.getFunctionName())) {
                alreadyEscaped = true;
            }
        }
        ExpressionNode node = valueExpression.getRoot();
        stream.write(new VariableBinding.Start(attrValue, node)); //attrContent = <expr>
        if (!alreadyEscaped) {
            Expression contentExpression = valueExpression.withNode(new Identifier(attrValue));
            stream.write(new VariableBinding.Start(attrContent,
                    adjustContext(compilerContext, contentExpression, markupContext, ExpressionContext.ATTRIBUTE)
                            .getRoot()));
            stream.write(new VariableBinding.Start(shouldDisplayAttr,
                    new BinaryOperation(BinaryOperator.OR, new Identifier(attrContent), new BinaryOperation(
                            BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrValue)))));

        } else {
            stream.write(new VariableBinding.Start(shouldDisplayAttr,
                    new BinaryOperation(BinaryOperator.OR, new Identifier(attrValue), new BinaryOperation(
                            BinaryOperator.EQ, new StringConstant("false"), new Identifier(attrValue)))));
        }
        stream.write(new Conditional.Start(shouldDisplayAttr, true)); // if (attrContent)

        emitAttributeStart(name); //write("attrName");
        invoke.beforeAttributeValue(stream, name, node);
        stream.write(new VariableBinding.Start(isTrueVar, //isTrueAttr = (attrValue == true)
                new BinaryOperation(BinaryOperator.EQ, new Identifier(attrValue), BooleanConstant.TRUE)));
        stream.write(new Conditional.Start(isTrueVar, false)); //if (!isTrueAttr)
        emitAttributeValueStart(quoteChar); // write("='");
        if (!alreadyEscaped) {
            stream.write(new OutputVariable(attrContent)); //write(attrContent)
        } else {
            stream.write(new OutputVariable(attrValue)); // write(attrValue)
        }
        emitAttributeEnd(quoteChar); //write("'");
        stream.write(Conditional.END); //end if isTrueAttr
        stream.write(VariableBinding.END); //end scope for isTrueAttr
        invoke.afterAttributeValue(stream, name);
        stream.write(Conditional.END); //end if attrContent
        stream.write(VariableBinding.END); //end scope for attrContent
        if (!alreadyEscaped) {
            stream.write(VariableBinding.END);
        }
        stream.write(VariableBinding.END); //end scope for attrValue
    }

    private void emitAttributeStart(String name) {
        out(" " + name);
    }

    private void emitAttributeValueStart(char quoteChar) {
        char quote = '"';
        if (quoteChar != 0) {
            quote = quoteChar;
        }
        out("=");
        out(String.valueOf(quote));
    }

    private void emitAttributeEnd(char quoteChar) {
        char quote = '"';
        if (quoteChar != 0) {
            quote = quoteChar;
        }
        out(String.valueOf(quote));
    }

    public void onCloseTag(String markup) {
        ElementContext context = elementStack.pop();
        PluginInvoke invoke = context.pluginInvoke();
        invoke.afterChildren(stream);
        boolean selfClosingTag = StringUtils.isEmpty(markup);
        boolean slyTag = "sly".equalsIgnoreCase(context.getTagName());
        if (slyTag) {
            Patterns.beginStreamIgnore(stream);
        }
        invoke.beforeTagClose(stream, selfClosingTag);
        out(markup);
        invoke.afterTagClose(stream, selfClosingTag);
        if (slyTag) {
            Patterns.endStreamIgnore(stream);
        }
        invoke.afterElement(stream);
    }

    public void onText(String text) {
        String tag = currentElementTag();
        boolean explicitContextRequired = isExplicitContextRequired(tag);
        MarkupContext markupContext = (explicitContextRequired) ? null : MarkupContext.TEXT;
        outText(text, markupContext);
    }

    public void onComment(String markup) {
        if (!Syntax.isSightlyComment(markup)) {
            outText(markup, MarkupContext.COMMENT);
        }
    }

    public void onDataNode(String markup) {
        out(markup);
    }

    public void onDocType(String markup) {
        out(markup);
    }

    public void onDocumentFinished() {
        this.stream.close();
    }

    private void outText(String content, MarkupContext context) {
        Interpolation interpolation = expressionParser.parseInterpolation(content);
        if (context == null) {
            interpolation = requireContext(interpolation);
        }
        String text = tryAsSimpleText(interpolation);
        if (text != null) {
            out(text);
        } else {
            outExprNode(expressionWrapper.transform(interpolation, context, ExpressionContext.TEXT).getRoot());
        }
    }

    private Interpolation requireContext(Interpolation interpolation) {
        Interpolation result = new Interpolation();
        for (Fragment fragment : interpolation.getFragments()) {
            Fragment addedFragment;
            if (fragment.isString()) {
                addedFragment = fragment;
            } else {
                if (fragment.getExpression().containsOption(Syntax.CONTEXT_OPTION)) {
                    addedFragment = fragment;
                } else {
                    String currentTag = currentElementTag();
                    String warningMessage = String
                            .format("Element %s requires that all expressions have an explicit context specified. "
                                    + "The expression will be replaced with an empty string.", currentTag);
                    stream.warn(
                            new PushStream.StreamMessage(warningMessage, fragment.getExpression().getRawText()));
                    addedFragment = new Fragment.Expr(new Expression(StringConstant.EMPTY));
                }
            }
            result.addFragment(addedFragment);
        }
        return result;
    }

    private Interpolation attributeChecked(String attributeName, Interpolation interpolation) {
        if (!MarkupUtils.isSensitiveAttribute(attributeName)) {
            return interpolation;
        }
        Interpolation newInterpolation = new Interpolation();
        for (Fragment fragment : interpolation.getFragments()) {
            Fragment addedFragment = fragment;
            if (fragment.isExpression()) {
                Expression expression = fragment.getExpression();
                if (!expression.containsOption(Syntax.CONTEXT_OPTION)) {
                    String warningMessage = String.format(
                            "Expressions within the value of attribute %s need to have an explicit context "
                                    + "option. The expression will be replaced with an empty string.",
                            attributeName);
                    stream.warn(new PushStream.StreamMessage(warningMessage, expression.getRawText()));
                    addedFragment = new Fragment.Text("");
                }
            }
            newInterpolation.addFragment(addedFragment);
        }
        return newInterpolation;
    }

    private void outExprNode(ExpressionNode node) {
        String variable = symbolGenerator.next();
        stream.write(new VariableBinding.Start(variable, node));
        stream.write(new OutputVariable(variable));
        stream.write(VariableBinding.END);
    }

    private String tryAsSimpleText(Interpolation interpolation) {
        if (interpolation.size() == 1) {
            Fragment fragment = interpolation.getFragment(0);
            if (fragment.isString()) {
                return fragment.getText();
            }
        } else if (interpolation.size() == 0) {
            return "";
        }
        return null;
    }

    private void out(String text) {
        stream.write(new OutText(text));
    }

    private void handlePlugin(String name, String value, ElementContext context) {
        PluginCallInfo callInfo = Syntax.parsePluginAttribute(name);
        if (callInfo != null) {
            Plugin plugin = obtainPlugin(callInfo.getName());
            ExpressionContext expressionContext = ExpressionContext.getContextForPlugin(plugin.name());
            Expression expr = expressionWrapper.transform(expressionParser.parseInterpolation(value), null,
                    expressionContext);
            PluginInvoke invoke = plugin.invoke(expr, callInfo, compilerContext);
            context.addPlugin(invoke, plugin.priority());
            context.addPluginCall(name, callInfo, expr);
        }
    }

    private Plugin obtainPlugin(String name) {
        Plugin plugin = pluginRegistry.get(name);
        if (plugin == null) {
            throw new SightlyCompilerException(
                    String.format("None of the registered plugins can handle the data-sly-%s block element.", name),
                    "data-sly-" + name);
        }
        return plugin;
    }

    private MarkupContext getAttributeMarkupContext(String attributeName) {
        if ("src".equalsIgnoreCase(attributeName) || "href".equalsIgnoreCase(attributeName)) {
            return MarkupContext.URI;
        }
        return MarkupContext.ATTRIBUTE;
    }

    private String currentElementTag() {
        if (elementStack.isEmpty()) {
            return null;
        }
        ElementContext current = elementStack.peek();
        return current.getTagName();
    }

    private boolean isExplicitContextRequired(String parentElementName) {
        return parentElementName != null
                && ("script".equals(parentElementName) || "style".equals(parentElementName));
    }

    private Expression adjustContext(CompilerContext compilerContext, Expression expression,
            MarkupContext markupContext, ExpressionContext expressionContext) {
        ExpressionNode root = expression.getRoot();
        if (root instanceof RuntimeCall) {
            RuntimeCall runtimeCall = (RuntimeCall) root;
            if (runtimeCall.getFunctionName().equals(RuntimeFunction.XSS)) {
                return expression;
            }
        }
        return compilerContext.adjustToContext(expression, markupContext, expressionContext);
    }
}