/*
* 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.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.jsp.tagext.PageData;
import org.apache.jasper.JasperException;
import org.apache.tomcat.util.security.Escape;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
/**
* An implementation of <code>jakarta.servlet.jsp.tagext.PageData</code> which builds the XML view of a given page. The
* XML view is built in two passes: During the first pass, the FirstPassVisitor collects the attributes of the top-level
* jsp:root and those of the jsp:root elements of any included pages, and adds them to the jsp:root element of the XML
* view. In addition, any taglib directives are converted into xmlns: attributes and added to the jsp:root element of
* the XML view. This pass ignores any nodes other than JspRoot and TaglibDirective. During the second pass, the
* SecondPassVisitor produces the XML view, using the combined jsp:root attributes determined in the first pass and any
* remaining pages nodes (this pass ignores any JspRoot and TaglibDirective nodes).
*/
class PageDataImpl extends PageData implements TagConstants {
private static final String JSP_VERSION = "2.0";
private static final String CDATA_START_SECTION = "<![CDATA[ ";
private static final String CDATA_END_SECTION = "]]> ";
// string buffer used to build XML view
private final StringBuilder buf;
/**
* @param page the page nodes from which to generate the XML view
* @param compiler The compiler for this page
*
* @throws JasperException If an error occurs
*/
PageDataImpl(Node.Nodes page, Compiler compiler) throws JasperException {
// First pass
FirstPassVisitor firstPass = new FirstPassVisitor(page.getRoot(), compiler.getPageInfo());
page.visit(firstPass);
// Second pass
buf = new StringBuilder();
SecondPassVisitor secondPass = new SecondPassVisitor(page.getRoot(), buf, compiler, firstPass.getJspIdPrefix());
page.visit(secondPass);
}
/**
* Returns the input stream of the XML view.
*
* @return the input stream of the XML view
*/
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(buf.toString().getBytes(StandardCharsets.UTF_8));
}
/*
* First-pass Visitor for JspRoot nodes (representing jsp:root elements) and TaglibDirective nodes, ignoring any
* other nodes.
*
* The purpose of this Visitor is to collect the attributes of the top-level jsp:root and those of the jsp:root
* elements of any included pages, and add them to the jsp:root element of the XML view. In addition, this Visitor
* converts any taglib directives into xmlns: attributes and adds them to the jsp:root element of the XML view.
*/
private static class FirstPassVisitor extends Node.Visitor implements TagConstants {
private final Node.Root root;
private final AttributesImpl rootAttrs;
private final PageInfo pageInfo;
// Prefix for the 'id' attribute
private String jspIdPrefix;
/*
* Constructor
*/
FirstPassVisitor(Node.Root root, PageInfo pageInfo) {
this.root = root;
this.pageInfo = pageInfo;
this.rootAttrs = new AttributesImpl();
this.rootAttrs.addAttribute("", "", "version", "CDATA", JSP_VERSION);
this.jspIdPrefix = "jsp";
}
@Override
public void visit(Node.Root n) throws JasperException {
visitBody(n);
if (n == root) {
/*
* Top-level page.
*
* Add xmlns:jsp="http://java.sun.com/JSP/Page" attribute only if not already present.
*/
if (!JSP_URI.equals(rootAttrs.getValue("xmlns:jsp"))) {
rootAttrs.addAttribute("", "", "xmlns:jsp", "CDATA", JSP_URI);
}
if (pageInfo.isJspPrefixHijacked()) {
/*
* 'jsp' prefix has been hijacked, that is, bound to a namespace other than the JSP namespace. This
* means that when adding an 'id' attribute to each element, we can't use the 'jsp' prefix.
* Therefore, create a new prefix (one that is unique across the translation unit) for use by the
* 'id' attribute, and bind it to the JSP namespace
*/
do {
jspIdPrefix += "jsp";
} while (pageInfo.containsPrefix(jspIdPrefix));
rootAttrs.addAttribute("", "", "xmlns:" + jspIdPrefix, "CDATA", JSP_URI);
}
root.setAttributes(rootAttrs);
}
}
@Override
public void visit(Node.JspRoot n) throws JasperException {
addAttributes(n.getTaglibAttributes());
addAttributes(n.getNonTaglibXmlnsAttributes());
addAttributes(n.getAttributes());
visitBody(n);
}
/*
* Converts taglib directive into "xmlns:..." attribute of jsp:root element.
*/
@Override
public void visit(Node.TaglibDirective n) throws JasperException {
Attributes attrs = n.getAttributes();
if (attrs != null) {
String qName = "xmlns:" + attrs.getValue("prefix");
/*
* According to javadocs of org.xml.sax.helpers.AttributesImpl, the addAttribute method does not check
* to see if the specified attribute is already contained in the list: This is the application's
* responsibility!
*/
if (rootAttrs.getIndex(qName) == -1) {
String location = attrs.getValue("uri");
if (location != null) {
if (location.startsWith("/")) {
location = URN_JSPTLD + location;
}
rootAttrs.addAttribute("", "", qName, "CDATA", location);
} else {
location = attrs.getValue("tagdir");
rootAttrs.addAttribute("", "", qName, "CDATA", URN_JSPTAGDIR + location);
}
}
}
}
public String getJspIdPrefix() {
return jspIdPrefix;
}
private void addAttributes(Attributes attrs) {
if (attrs != null) {
int len = attrs.getLength();
for (int i = 0; i < len; i++) {
String qName = attrs.getQName(i);
if ("version".equals(qName)) {
continue;
}
// Bugzilla 35252: https://bz.apache.org/bugzilla/show_bug.cgi?id=35252
if (rootAttrs.getIndex(qName) == -1) {
rootAttrs.addAttribute(attrs.getURI(i), attrs.getLocalName(i), qName, attrs.getType(i),
attrs.getValue(i));
}
}
}
}
}
/*
* Second-pass Visitor responsible for producing XML view and assigning each element a unique jsp:id attribute.
*/
private static class SecondPassVisitor extends Node.Visitor implements TagConstants {
private final Node.Root root;
private final StringBuilder buf;
private final Compiler compiler;
private final String jspIdPrefix;
private boolean resetDefaultNS = false;
// Current value of jsp:id attribute
private int jspId;
/*
* Constructor
*/
SecondPassVisitor(Node.Root root, StringBuilder buf, Compiler compiler, String jspIdPrefix) {
this.root = root;
this.buf = buf;
this.compiler = compiler;
this.jspIdPrefix = jspIdPrefix;
}
/*
* Visits root node.
*/
@Override
public void visit(Node.Root n) throws JasperException {
if (n == this.root) {
// top-level page
appendXmlProlog();
appendTag(n);
} else {
boolean resetDefaultNSSave = resetDefaultNS;
if (n.isXmlSyntax()) {
resetDefaultNS = true;
}
visitBody(n);
resetDefaultNS = resetDefaultNSSave;
}
}
/*
* Visits jsp:root element of JSP page in XML syntax.
*
* Any nested jsp:root elements (from pages included via an include directive) are ignored.
*/
@Override
public void visit(Node.JspRoot n) throws JasperException {
visitBody(n);
}
@Override
public void visit(Node.PageDirective n) throws JasperException {
appendPageDirective(n);
}
@Override
public void visit(Node.IncludeDirective n) throws JasperException {
// expand in place
visitBody(n);
}
@Override
public void visit(Node.Comment n) throws JasperException {
// Comments are ignored in XML view
}
@Override
public void visit(Node.Declaration n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.Expression n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.Scriptlet n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.JspElement n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.ELExpression n) throws JasperException {
if (!n.getRoot().isXmlSyntax()) {
buf.append('<').append(JSP_TEXT_ACTION);
buf.append(' ');
buf.append(jspIdPrefix);
buf.append(":id=\"");
buf.append(jspId++).append("\">");
}
buf.append("${");
buf.append(Escape.xml(n.getText()));
buf.append('}');
if (!n.getRoot().isXmlSyntax()) {
buf.append(JSP_TEXT_ACTION_END);
}
buf.append(" ");
}
@Override
public void visit(Node.IncludeAction n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.ForwardAction n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.GetProperty n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.SetProperty n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.ParamAction n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.UseBean n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.NamedAttribute n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.JspBody n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.CustomTag n) throws JasperException {
boolean resetDefaultNSSave = resetDefaultNS;
appendTag(n, resetDefaultNS);
resetDefaultNS = resetDefaultNSSave;
}
@Override
public void visit(Node.UninterpretedTag n) throws JasperException {
boolean resetDefaultNSSave = resetDefaultNS;
appendTag(n, resetDefaultNS);
resetDefaultNS = resetDefaultNSSave;
}
@Override
public void visit(Node.JspText n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.DoBodyAction n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.InvokeAction n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.TagDirective n) throws JasperException {
appendTagDirective(n);
}
@Override
public void visit(Node.AttributeDirective n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.VariableDirective n) throws JasperException {
appendTag(n);
}
@Override
public void visit(Node.TemplateText n) throws JasperException {
/*
* If the template text came from a JSP page written in JSP syntax, create a jsp:text element for it (JSP
* 5.3.2).
*/
appendText(n.getText(), !n.getRoot().isXmlSyntax());
}
/*
* Appends the given tag, including its body, to the XML view.
*/
private void appendTag(Node n) throws JasperException {
appendTag(n, false);
}
/*
* Appends the given tag, including its body, to the XML view, and optionally reset default namespace to "", if
* none specified.
*/
private void appendTag(Node n, boolean addDefaultNS) throws JasperException {
Node.Nodes body = n.getBody();
String text = n.getText();
buf.append('<').append(n.getQName());
buf.append(" ");
printAttributes(n, addDefaultNS);
buf.append(" ").append(jspIdPrefix).append(":id").append("=\"");
buf.append(jspId++).append("\" ");
if (ROOT_ACTION.equals(n.getLocalName()) || body != null || text != null) {
buf.append("> ");
if (ROOT_ACTION.equals(n.getLocalName())) {
if (compiler.getCompilationContext().isTagFile()) {
appendTagDirective();
} else {
appendPageDirective();
}
}
if (body != null) {
body.visit(this);
} else {
appendText(text, false);
}
buf.append("</").append(n.getQName()).append("> ");
} else {
buf.append("/> ");
}
}
/*
* Appends the page directive with the given attributes to the XML view.
*
* Since the import attribute of the page directive is the only page attribute that is allowed to appear
* multiple times within the same document, and since XML allows only single-value attributes, the values of
* multiple import attributes must be combined into one, separated by comma.
*
* If the given page directive contains just 'contentType' and/or 'pageEncoding' attributes, we ignore it, as
* we've already appended a page directive containing just these two attributes.
*/
private void appendPageDirective(Node.PageDirective n) {
boolean append = false;
Attributes attrs = n.getAttributes();
int len = (attrs == null) ? 0 : attrs.getLength();
for (int i = 0; i < len; i++) {
@SuppressWarnings("null") // If attrs==null, len == 0
String attrName = attrs.getQName(i);
if (!"pageEncoding".equals(attrName) && !"contentType".equals(attrName)) {
append = true;
break;
}
}
if (!append) {
return;
}
buf.append('<').append(n.getQName());
buf.append(" ");
// append jsp:id
buf.append(" ").append(jspIdPrefix).append(":id").append("=\"");
buf.append(jspId++).append("\" ");
// append remaining attributes
for (int i = 0; i < len; i++) {
@SuppressWarnings("null") // If attrs==null, len == 0
String attrName = attrs.getQName(i);
if ("import".equals(attrName) || "contentType".equals(attrName) || "pageEncoding".equals(attrName)) {
/*
* Page directive's 'import' attribute is considered further down, and its 'pageEncoding' and
* 'contentType' attributes are ignored, since we've already appended a new page directive
* containing just these two attributes
*/
continue;
}
String value = attrs.getValue(i);
buf.append(" ").append(attrName).append("=\"");
buf.append(JspUtil.getExprInXml(value)).append("\" ");
}
if (!n.getImports().isEmpty()) {
// Concatenate names of imported classes/packages
boolean first = true;
for (String i : n.getImports()) {
if (first) {
first = false;
buf.append(" import=\"");
} else {
buf.append(',');
}
buf.append(JspUtil.getExprInXml(i));
}
buf.append("\" ");
}
buf.append("/> ");
}
/*
* Appends a page directive with 'pageEncoding' and 'contentType' attributes.
*
* The value of the 'pageEncoding' attribute is hard-coded to UTF-8, whereas the value of the 'contentType'
* attribute, which is identical to what the container will pass to ServletResponse.setContentType(), is derived
* from the pageInfo.
*/
private void appendPageDirective() {
buf.append('<').append(JSP_PAGE_DIRECTIVE_ACTION);
buf.append(" ");
// append jsp:id
buf.append(" ").append(jspIdPrefix).append(":id").append("=\"");
buf.append(jspId++).append("\" ");
buf.append(" ").append("pageEncoding").append("=\"UTF-8\" ");
buf.append(" ").append("contentType").append("=\"");
buf.append(compiler.getPageInfo().getContentType()).append("\" ");
buf.append("/> ");
}
/*
* Appends the tag directive with the given attributes to the XML view.
*
* If the given tag directive contains just a 'pageEncoding' attributes, we ignore it, as we've already appended
* a tag directive containing just this attributes.
*/
private void appendTagDirective(Node.TagDirective n) throws JasperException {
boolean append = false;
Attributes attrs = n.getAttributes();
int len = (attrs == null) ? 0 : attrs.getLength();
for (int i = 0; i < len; i++) {
@SuppressWarnings("null") // If attrs==null, len == 0
String attrName = attrs.getQName(i);
if (!"pageEncoding".equals(attrName)) {
append = true;
break;
}
}
if (!append) {
return;
}
appendTag(n);
}
/*
* Appends a tag directive containing a single 'pageEncoding' attribute whose value is hard-coded to UTF-8.
*/
private void appendTagDirective() {
buf.append('<').append(JSP_TAG_DIRECTIVE_ACTION);
buf.append(" ");
// append jsp:id
buf.append(" ").append(jspIdPrefix).append(":id").append("=\"");
buf.append(jspId++).append("\" ");
buf.append(" ").append("pageEncoding").append("=\"UTF-8\" ");
buf.append("/> ");
}
private void appendText(String text, boolean createJspTextElement) {
if (createJspTextElement) {
buf.append('<').append(JSP_TEXT_ACTION);
buf.append(" ");
// append jsp:id
buf.append(" ").append(jspIdPrefix).append(":id").append("=\"");
buf.append(jspId++).append("\" ");
buf.append("> ");
appendCDATA(text);
buf.append(JSP_TEXT_ACTION_END);
buf.append(" ");
} else {
appendCDATA(text);
}
}
/*
* Appends the given text as a CDATA section to the XML view, unless the text has already been marked as CDATA.
*/
private void appendCDATA(String text) {
buf.append(CDATA_START_SECTION);
buf.append(escapeCDATA(text));
buf.append(CDATA_END_SECTION);
}
/*
* Escapes any occurrences of "]]>" (by replacing them with "]]>") within the given text, so it can be
* included in a CDATA section.
*/
private String escapeCDATA(String text) {
if (text == null) {
return "";
}
int len = text.length();
CharArrayWriter result = new CharArrayWriter(len);
for (int i = 0; i < len; i++) {
if (((i + 2) < len) && (text.charAt(i) == ']') && (text.charAt(i + 1) == ']') &&
(text.charAt(i + 2) == '>')) {
// match found
result.write(']');
result.write(']');
result.write('&');
result.write('g');
result.write('t');
result.write(';');
i += 2;
} else {
result.write(text.charAt(i));
}
}
return result.toString();
}
/*
* Appends the attributes of the given Node to the XML view.
*/
private void printAttributes(Node n, boolean addDefaultNS) {
/*
* Append "xmlns" attributes that represent tag libraries
*/
Attributes attrs = n.getTaglibAttributes();
int len = (attrs == null) ? 0 : attrs.getLength();
for (int i = 0; i < len; i++) {
@SuppressWarnings("null") // If attrs==null, len == 0
String name = attrs.getQName(i);
String value = attrs.getValue(i);
buf.append(" ").append(name).append("=\"").append(value).append("\" ");
}
/*
* Append "xmlns" attributes that do not represent tag libraries
*/
attrs = n.getNonTaglibXmlnsAttributes();
len = (attrs == null) ? 0 : attrs.getLength();
boolean defaultNSSeen = false;
for (int i = 0; i < len; i++) {
@SuppressWarnings("null") // If attrs==null, len == 0
String name = attrs.getQName(i);
String value = attrs.getValue(i);
buf.append(" ").append(name).append("=\"").append(value).append("\" ");
defaultNSSeen |= "xmlns".equals(name);
}
if (addDefaultNS && !defaultNSSeen) {
buf.append(" xmlns=\"\" ");
}
resetDefaultNS = false;
/*
* Append all other attributes
*/
attrs = n.getAttributes();
len = (attrs == null) ? 0 : attrs.getLength();
for (int i = 0; i < len; i++) {
@SuppressWarnings("null") // If attrs==null, len == 0
String name = attrs.getQName(i);
String value = attrs.getValue(i);
buf.append(" ").append(name).append("=\"");
buf.append(JspUtil.getExprInXml(value)).append("\" ");
}
}
/*
* Appends XML prolog with encoding declaration.
*/
private void appendXmlProlog() {
buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?> ");
}
}
}