/*
 * 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.jasper.compiler;

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 jakarta.el.MethodExpression;
import jakarta.el.ValueExpression;
import jakarta.servlet.jsp.tagext.JspFragment;
import jakarta.servlet.jsp.tagext.TagAttributeInfo;
import jakarta.servlet.jsp.tagext.TagFileInfo;
import jakarta.servlet.jsp.tagext.TagInfo;
import jakarta.servlet.jsp.tagext.TagLibraryInfo;
import jakarta.servlet.jsp.tagext.TagVariableInfo;
import jakarta.servlet.jsp.tagext.VariableInfo;

import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;
import org.apache.jasper.runtime.JspSourceDependent;
import org.apache.jasper.servlet.JspServletWrapper;
import org.apache.tomcat.Jar;
import org.apache.tomcat.util.descriptor.tld.TldResourcePath;

/**
 * 1. Processes and extracts the directive info in a tag file. 2. Compiles and loads tag files used in a JSP file.
 *
 * @author Kin-man Chung
 */

public class TagFileProcessor {

    private List<Compiler> tempVector;

    /**
     * A visitor the tag file
     */
    private static class TagFileDirectiveVisitor extends Node.Visitor {

        private static final JspUtil.ValidAttribute[] tagDirectiveAttrs = { new JspUtil.ValidAttribute("display-name"),
                new JspUtil.ValidAttribute("body-content"), new JspUtil.ValidAttribute("dynamic-attributes"),
                new JspUtil.ValidAttribute("small-icon"), new JspUtil.ValidAttribute("large-icon"),
                new JspUtil.ValidAttribute("description"), new JspUtil.ValidAttribute("example"),
                new JspUtil.ValidAttribute("pageEncoding"), new JspUtil.ValidAttribute("language"),
                new JspUtil.ValidAttribute("import"), new JspUtil.ValidAttribute("deferredSyntaxAllowedAsLiteral"),
                new JspUtil.ValidAttribute("trimDirectiveWhitespaces"), new JspUtil.ValidAttribute("isELIgnored"),
                new JspUtil.ValidAttribute("errorOnELNotFound") };

        private static final JspUtil.ValidAttribute[] attributeDirectiveAttrs = {
                new JspUtil.ValidAttribute("name", true), new JspUtil.ValidAttribute("required"),
                new JspUtil.ValidAttribute("fragment"), new JspUtil.ValidAttribute("rtexprvalue"),
                new JspUtil.ValidAttribute("type"), new JspUtil.ValidAttribute("deferredValue"),
                new JspUtil.ValidAttribute("deferredValueType"), new JspUtil.ValidAttribute("deferredMethod"),
                new JspUtil.ValidAttribute("deferredMethodSignature"), new JspUtil.ValidAttribute("description") };

        private static final JspUtil.ValidAttribute[] variableDirectiveAttrs =
                { new JspUtil.ValidAttribute("name-given"), new JspUtil.ValidAttribute("name-from-attribute"),
                        new JspUtil.ValidAttribute("alias"), new JspUtil.ValidAttribute("variable-class"),
                        new JspUtil.ValidAttribute("scope"), new JspUtil.ValidAttribute("declare"),
                        new JspUtil.ValidAttribute("description") };

        private final ErrorDispatcher err;

        private final TagLibraryInfo tagLibInfo;

        private final String name;

        private final String path;

        private String bodyContent = null;

        private String description = null;

        private String displayName = null;

        private String smallIcon = null;

        private String largeIcon = null;

        private String dynamicAttrsMapName;

        private String example = null;

        private final List<TagAttributeInfo> attributeList;

        private final List<TagVariableInfo> variableList;

        private static final String ATTR_NAME = "the name attribute of the attribute directive";

        private static final String VAR_NAME_GIVEN = "the name-given attribute of the variable directive";

        private static final String VAR_NAME_FROM = "the name-from-attribute attribute of the variable directive";

        private static final String VAR_ALIAS = "the alias attribute of the variable directive";

        private static final String TAG_DYNAMIC = "the dynamic-attributes attribute of the tag directive";

        private final Map<String,NameEntry> nameTable = new HashMap<>();

        private final Map<String,NameEntry> nameFromTable = new HashMap<>();

        TagFileDirectiveVisitor(Compiler compiler, TagLibraryInfo tagLibInfo, String name, String path) {
            err = compiler.getErrorDispatcher();
            this.tagLibInfo = tagLibInfo;
            this.name = name;
            this.path = path;
            attributeList = new ArrayList<>();
            variableList = new ArrayList<>();
        }

        @Override
        public void visit(Node.TagDirective n) throws JasperException {

            JspUtil.checkAttributes("Tag directive", n, tagDirectiveAttrs, err);

            bodyContent = checkConflict(n, bodyContent, "body-content");
            if (bodyContent != null && !bodyContent.equalsIgnoreCase(TagInfo.BODY_CONTENT_EMPTY) &&
                    !bodyContent.equalsIgnoreCase(TagInfo.BODY_CONTENT_TAG_DEPENDENT) &&
                    !bodyContent.equalsIgnoreCase(TagInfo.BODY_CONTENT_SCRIPTLESS)) {
                err.jspError(n, "jsp.error.tagdirective.badbodycontent", bodyContent);
            }
            dynamicAttrsMapName = checkConflict(n, dynamicAttrsMapName, "dynamic-attributes");
            if (dynamicAttrsMapName != null) {
                checkUniqueName(dynamicAttrsMapName, TAG_DYNAMIC, n);
            }
            smallIcon = checkConflict(n, smallIcon, "small-icon");
            largeIcon = checkConflict(n, largeIcon, "large-icon");
            description = checkConflict(n, description, "description");
            displayName = checkConflict(n, displayName, "display-name");
            example = checkConflict(n, example, "example");
        }

        private String checkConflict(Node n, String oldAttrValue, String attr) throws JasperException {

            String result = oldAttrValue;
            String attrValue = n.getAttributeValue(attr);
            if (attrValue != null) {
                if (oldAttrValue != null && !oldAttrValue.equals(attrValue)) {
                    err.jspError(n, "jsp.error.tag.conflict.attr", attr, oldAttrValue, attrValue);
                }
                result = attrValue;
            }
            return result;
        }

        @Override
        public void visit(Node.AttributeDirective n) throws JasperException {

            JspUtil.checkAttributes("Attribute directive", n, attributeDirectiveAttrs, err);

            // JSP 2.1 Table JSP.8-3
            // handle deferredValue and deferredValueType
            boolean deferredValue = false;
            boolean deferredValueSpecified = false;
            String deferredValueString = n.getAttributeValue("deferredValue");
            if (deferredValueString != null) {
                deferredValueSpecified = true;
                deferredValue = JspUtil.booleanValue(deferredValueString);
            }
            String deferredValueType = n.getAttributeValue("deferredValueType");
            if (deferredValueType != null) {
                if (deferredValueSpecified && !deferredValue) {
                    err.jspError(n, "jsp.error.deferredvaluetypewithoutdeferredvalue");
                } else {
                    deferredValue = true;
                }
            } else if (deferredValue) {
                deferredValueType = "java.lang.Object";
            } else {
                deferredValueType = "java.lang.String";
            }

            // JSP 2.1 Table JSP.8-3
            // handle deferredMethod and deferredMethodSignature
            boolean deferredMethod = false;
            boolean deferredMethodSpecified = false;
            String deferredMethodString = n.getAttributeValue("deferredMethod");
            if (deferredMethodString != null) {
                deferredMethodSpecified = true;
                deferredMethod = JspUtil.booleanValue(deferredMethodString);
            }
            String deferredMethodSignature = n.getAttributeValue("deferredMethodSignature");
            if (deferredMethodSignature != null) {
                if (deferredMethodSpecified && !deferredMethod) {
                    err.jspError(n, "jsp.error.deferredmethodsignaturewithoutdeferredmethod");
                } else {
                    deferredMethod = true;
                }
            } else if (deferredMethod) {
                deferredMethodSignature = "void methodname()";
            }

            if (deferredMethod && deferredValue) {
                err.jspError(n, "jsp.error.deferredmethodandvalue");
            }

            String attrName = n.getAttributeValue("name");
            boolean required = JspUtil.booleanValue(n.getAttributeValue("required"));
            boolean rtexprvalue = true;
            String rtexprvalueString = n.getAttributeValue("rtexprvalue");
            if (rtexprvalueString != null) {
                rtexprvalue = JspUtil.booleanValue(rtexprvalueString);
            }
            boolean fragment = JspUtil.booleanValue(n.getAttributeValue("fragment"));
            String type = n.getAttributeValue("type");
            if (fragment) {
                // type is fixed to "JspFragment" and a translation error
                // must occur if specified.
                if (type != null) {
                    err.jspError(n, "jsp.error.fragmentwithtype", JspFragment.class.getName());
                }
                // rtexprvalue is fixed to "true" and a translation error
                // must occur if specified.
                rtexprvalue = true;
                if (rtexprvalueString != null) {
                    err.jspError(n, "jsp.error.frgmentwithrtexprvalue");
                }
            } else {
                if (type == null) {
                    type = "java.lang.String";
                }

                if (deferredValue) {
                    type = ValueExpression.class.getName();
                } else if (deferredMethod) {
                    type = MethodExpression.class.getName();
                }
            }

            if (("2.0".equals(tagLibInfo.getRequiredVersion()) || ("1.2".equals(tagLibInfo.getRequiredVersion()))) &&
                    (deferredMethodSpecified || deferredMethod || deferredValueSpecified || deferredValue)) {
                err.jspError("jsp.error.invalid.version", path);
            }

            TagAttributeInfo tagAttributeInfo = new TagAttributeInfo(attrName, required, type, rtexprvalue, fragment,
                    null, deferredValue, deferredMethod, deferredValueType, deferredMethodSignature);
            attributeList.add(tagAttributeInfo);
            checkUniqueName(attrName, ATTR_NAME, n, tagAttributeInfo);
        }

        @Override
        public void visit(Node.VariableDirective n) throws JasperException {

            JspUtil.checkAttributes("Variable directive", n, variableDirectiveAttrs, err);

            String nameGiven = n.getAttributeValue("name-given");
            String nameFromAttribute = n.getAttributeValue("name-from-attribute");
            if (nameGiven == null && nameFromAttribute == null) {
                err.jspError("jsp.error.variable.either.name");
            }

            if (nameGiven != null && nameFromAttribute != null) {
                err.jspError("jsp.error.variable.both.name");
            }

            String alias = n.getAttributeValue("alias");
            if (nameFromAttribute != null && alias == null || nameFromAttribute == null && alias != null) {
                err.jspError("jsp.error.variable.alias");
            }

            String className = n.getAttributeValue("variable-class");
            if (className == null) {
                className = "java.lang.String";
            }

            String declareStr = n.getAttributeValue("declare");
            boolean declare = true;
            if (declareStr != null) {
                declare = JspUtil.booleanValue(declareStr);
            }

            int scope = VariableInfo.NESTED;
            String scopeStr = n.getAttributeValue("scope");
            if (scopeStr != null) {
                if ("NESTED".equals(scopeStr)) {
                    // Already the default
                } else if ("AT_BEGIN".equals(scopeStr)) {
                    scope = VariableInfo.AT_BEGIN;
                } else if ("AT_END".equals(scopeStr)) {
                    scope = VariableInfo.AT_END;
                }
            }

            if (nameFromAttribute != null) {
                /*
                 * An alias has been specified. We use 'nameGiven' to hold the value of the alias, and
                 * 'nameFromAttribute' to hold the name of the attribute whose value (at invocation-time) denotes the
                 * name of the variable that is being aliased
                 */
                nameGiven = alias;
                checkUniqueName(nameFromAttribute, VAR_NAME_FROM, n);
                checkUniqueName(alias, VAR_ALIAS, n);
            } else {
                // name-given specified
                checkUniqueName(nameGiven, VAR_NAME_GIVEN, n);
            }

            variableList.add(new TagVariableInfo(nameGiven, nameFromAttribute, className, declare, scope));
        }

        public TagInfo getTagInfo(String packageName) throws JasperException {

            if (name == null) {
                // XXX Get it from tag file name
            }

            if (bodyContent == null) {
                bodyContent = TagInfo.BODY_CONTENT_SCRIPTLESS;
            }

            String tagClassName = JspUtil.getTagHandlerClassName(path, packageName, tagLibInfo.getReliableURN(), err);

            TagVariableInfo[] tagVariableInfos = variableList.toArray(new TagVariableInfo[0]);
            TagAttributeInfo[] tagAttributeInfo = attributeList.toArray(new TagAttributeInfo[0]);

            return new JasperTagInfo(name, tagClassName, bodyContent, description, tagLibInfo, null, tagAttributeInfo,
                    displayName, smallIcon, largeIcon, tagVariableInfos, dynamicAttrsMapName);
        }

        static class NameEntry {
            private final String type;

            private final Node node;

            private final TagAttributeInfo attr;

            NameEntry(String type, Node node, TagAttributeInfo attr) {
                this.type = type;
                this.node = node;
                this.attr = attr;
            }

            String getType() {
                return type;
            }

            Node getNode() {
                return node;
            }

            TagAttributeInfo getTagAttributeInfo() {
                return attr;
            }
        }

        /*
         * Reports a translation error if names specified in attributes of directives are not unique in this translation
         * unit.
         *
         * The value of the following attributes must be unique. 1. 'name' attribute of an attribute directive 2.
         * 'name-given' attribute of a variable directive 3. 'alias' attribute of variable directive 4.
         * 'dynamic-attributes' of a tag directive except that 'dynamic-attributes' can (and must) have the same value
         * when it appears in multiple tag directives.
         *
         * Also, 'name-from' attribute of a variable directive cannot have the same value as that from another variable
         * directive.
         */
        private void checkUniqueName(String name, String type, Node n) throws JasperException {
            checkUniqueName(name, type, n, null);
        }

        private void checkUniqueName(String name, String type, Node n, TagAttributeInfo attr) throws JasperException {

            Map<String,NameEntry> table = (VAR_NAME_FROM.equals(type)) ? nameFromTable : nameTable;
            NameEntry nameEntry = table.get(name);
            if (nameEntry != null) {
                if (!TAG_DYNAMIC.equals(type) || !TAG_DYNAMIC.equals(nameEntry.getType())) {
                    int line = nameEntry.getNode().getStart().getLineNumber();
                    err.jspError(n, "jsp.error.tagfile.nameNotUnique", type, nameEntry.getType(),
                            Integer.toString(line));
                }
            } else {
                table.put(name, new NameEntry(type, n, attr));
            }
        }

        /**
         * Perform miscellaneous checks after the nodes are visited.
         */
        void postCheck() throws JasperException {
            // Check that var.name-from-attributes has valid values.
            for (Entry<String,NameEntry> entry : nameFromTable.entrySet()) {
                String key = entry.getKey();
                NameEntry nameEntry = nameTable.get(key);
                NameEntry nameFromEntry = entry.getValue();
                Node nameFromNode = nameFromEntry.getNode();
                if (nameEntry == null) {
                    err.jspError(nameFromNode, "jsp.error.tagfile.nameFrom.noAttribute", key);
                } else {
                    Node node = nameEntry.getNode();
                    TagAttributeInfo tagAttr = nameEntry.getTagAttributeInfo();
                    if (!"java.lang.String".equals(tagAttr.getTypeName()) || !tagAttr.isRequired() ||
                            tagAttr.canBeRequestTime()) {
                        err.jspError(nameFromNode, "jsp.error.tagfile.nameFrom.badAttribute", key,
                                Integer.toString(node.getStart().getLineNumber()));
                    }
                }
            }
        }
    }

    /**
     * Parses the tag file, and collects information on the directives included in it. The method is used to obtain the
     * info on the tag file, when the handler that it represents is referenced. The tag file is not compiled here.
     *
     * @param pc         the current ParserController used in this compilation
     * @param name       the tag name as specified in the TLD
     * @param path       the path for the tagfile
     * @param jar        the Jar resource containing the tag file
     * @param tagLibInfo the TagLibraryInfo object associated with this TagInfo
     *
     * @return a TagInfo object assembled from the directives in the tag file.
     *
     * @throws JasperException If an error occurs during parsing
     */
    @SuppressWarnings("null") // page can't be null
    public static TagInfo parseTagFileDirectives(ParserController pc, String name, String path, Jar jar,
            TagLibraryInfo tagLibInfo) throws JasperException {


        ErrorDispatcher err = pc.getCompiler().getErrorDispatcher();

        Node.Nodes page = null;
        try {
            page = pc.parseTagFileDirectives(path, jar);
        } catch (IOException ioe) {
            err.jspError("jsp.error.file.not.found", path);
        }

        TagFileDirectiveVisitor tagFileVisitor = new TagFileDirectiveVisitor(pc.getCompiler(), tagLibInfo, name, path);
        page.visit(tagFileVisitor);
        tagFileVisitor.postCheck();

        return tagFileVisitor.getTagInfo(pc.getJspCompilationContext().getOptions().getGeneratedTagFilePackageName());
    }

    /**
     * Compiles and loads a tagfile.
     */
    private Class<?> loadTagFile(Compiler compiler, String tagFilePath, TagInfo tagInfo, PageInfo parentPageInfo)
            throws JasperException {

        Jar tagJar = null;
        Jar tagJarOriginal = null;
        try {
            if (tagFilePath.startsWith("/META-INF/")) {
                try {
                    tagJar = compiler.getCompilationContext().getTldResourcePath(tagInfo.getTagLibrary().getURI())
                            .openJar();
                } catch (IOException ioe) {
                    throw new JasperException(ioe);
                }
            }
            String wrapperUri;
            if (tagJar == null) {
                wrapperUri = tagFilePath;
            } else {
                wrapperUri = tagJar.getURL(tagFilePath);
            }

            JspCompilationContext ctxt = compiler.getCompilationContext();
            JspRuntimeContext rctxt = ctxt.getRuntimeContext();

            synchronized (rctxt) {
                JspServletWrapper wrapper = null;
                try {
                    wrapper = rctxt.getWrapper(wrapperUri);
                    if (wrapper == null) {
                        wrapper = new JspServletWrapper(ctxt.getServletContext(), ctxt.getOptions(), tagFilePath,
                                tagInfo, ctxt.getRuntimeContext(), tagJar);
                        // Use same classloader and classpath for compiling tag files
                        wrapper.getJspEngineContext().setClassLoader(ctxt.getClassLoader());
                        wrapper.getJspEngineContext().setClassPath(ctxt.getClassPath());
                        rctxt.addWrapper(wrapperUri, wrapper);
                    } else {
                        // Make sure that JspCompilationContext gets the latest TagInfo
                        // for the tag file. TagInfo instance was created the last
                        // time the tag file was scanned for directives, and the tag
                        // file may have been modified since then.
                        wrapper.getJspEngineContext().setTagInfo(tagInfo);
                        // This compilation needs to use the current tagJar.
                        // Compilation may be nested in which case the old tagJar
                        // will need to be restored
                        tagJarOriginal = wrapper.getJspEngineContext().getTagFileJar();
                        wrapper.getJspEngineContext().setTagFileJar(tagJar);
                    }

                    Class<?> tagClazz;
                    int tripCount = wrapper.incTripCount();
                    try {
                        if (tripCount > 0) {
                            // When tripCount is greater than zero, a circular
                            // dependency exists. The circularly dependent tag
                            // file is compiled in prototype mode, to avoid infinite
                            // recursion.

                            JspServletWrapper tempWrapper = new JspServletWrapper(ctxt.getServletContext(),
                                    ctxt.getOptions(), tagFilePath, tagInfo, ctxt.getRuntimeContext(), tagJar);
                            // Use same classloader and classpath for compiling tag files
                            tempWrapper.getJspEngineContext().setClassLoader(ctxt.getClassLoader());
                            tempWrapper.getJspEngineContext().setClassPath(ctxt.getClassPath());
                            tagClazz = tempWrapper.loadTagFilePrototype();
                            tempVector.add(tempWrapper.getJspEngineContext().getCompiler());
                        } else {
                            tagClazz = wrapper.loadTagFile();
                        }
                    } finally {
                        wrapper.decTripCount();
                    }

                    // Add the dependents for this tag file to its parent's
                    // Dependent list. The only reliable dependency information
                    // can only be obtained from the tag instance.
                    try {
                        Object tagIns = tagClazz.getConstructor().newInstance();
                        if (tagIns instanceof JspSourceDependent) {
                            for (Entry<String,Long> entry : ((JspSourceDependent) tagIns).getDependants().entrySet()) {
                                parentPageInfo.addDependant(entry.getKey(), entry.getValue());
                            }
                        }
                    } catch (RuntimeException | ReflectiveOperationException e) {
                        // ignore errors
                    }

                    return tagClazz;
                } finally {
                    if (wrapper != null && tagJarOriginal != null) {
                        wrapper.getJspEngineContext().setTagFileJar(tagJarOriginal);
                    }
                }
            }
        } finally {
            if (tagJar != null) {
                tagJar.close();
            }
        }
    }

    /*
     * Visitor which scans the page and looks for tag handlers that are tag files, compiling (if necessary) and loading
     * them.
     */
    private class TagFileLoaderVisitor extends Node.Visitor {

        private final Compiler compiler;

        private final PageInfo pageInfo;

        TagFileLoaderVisitor(Compiler compiler) {

            this.compiler = compiler;
            this.pageInfo = compiler.getPageInfo();
        }

        @Override
        public void visit(Node.CustomTag n) throws JasperException {
            TagFileInfo tagFileInfo = n.getTagFileInfo();
            if (tagFileInfo != null) {
                String tagFilePath = tagFileInfo.getPath();
                if (tagFilePath.startsWith("/META-INF/")) {
                    // For tags in JARs, add the TLD and the tag as a dependency
                    TldResourcePath tldResourcePath = compiler.getCompilationContext()
                            .getTldResourcePath(tagFileInfo.getTagInfo().getTagLibrary().getURI());

                    try (Jar jar = tldResourcePath.openJar()) {

                        if (jar != null) {
                            // Add TLD
                            pageInfo.addDependant(jar.getURL(tldResourcePath.getEntryName()),
                                    Long.valueOf(jar.getLastModified(tldResourcePath.getEntryName())));
                            // Add Tag
                            pageInfo.addDependant(jar.getURL(tagFilePath.substring(1)),
                                    Long.valueOf(jar.getLastModified(tagFilePath.substring(1))));
                        } else {
                            pageInfo.addDependant(tagFilePath,
                                    compiler.getCompilationContext().getLastModified(tagFilePath));
                        }
                    } catch (IOException ioe) {
                        throw new JasperException(ioe);
                    }
                } else {
                    pageInfo.addDependant(tagFilePath, compiler.getCompilationContext().getLastModified(tagFilePath));
                }
                Class<?> c = loadTagFile(compiler, tagFilePath, n.getTagInfo(), pageInfo);
                n.setTagHandlerClass(c);
            }
            visitBody(n);
        }
    }

    /**
     * Implements a phase of the translation that compiles (if necessary) the tag files used in a JSP files. The
     * directives in the tag files are assumed to have been processed and encapsulated as TagFileInfo in the CustomTag
     * nodes.
     *
     * @param compiler Compiler to use to compile tag files
     * @param page     The page from to scan for tag files to compile
     *
     * @throws JasperException If an error occurs during the scan or compilation
     */
    public void loadTagFiles(Compiler compiler, Node.Nodes page) throws JasperException {

        tempVector = new ArrayList<>();
        page.visit(new TagFileLoaderVisitor(compiler));
    }

    /**
     * Removed the java and class files for the tag prototype generated from the current compilation.
     *
     * @param classFileName If non-null, remove only the class file with this name.
     */
    public void removeProtoTypeFiles(String classFileName) {
        for (Compiler c : tempVector) {
            if (classFileName == null) {
                c.removeGeneratedClassFiles();
            } else if (classFileName.equals(c.getCompilationContext().getClassFileName())) {
                c.removeGeneratedClassFiles();
                tempVector.remove(c);
                return;
            }
        }
    }
}
