/* | |
* 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.struts2.jasper.compiler; | |
import org.apache.struts2.jasper.JasperException; | |
import org.xml.sax.Attributes; | |
import org.xml.sax.helpers.AttributesImpl; | |
import javax.servlet.jsp.tagext.PageData; | |
import java.io.ByteArrayInputStream; | |
import java.io.CharArrayWriter; | |
import java.io.InputStream; | |
import java.io.UnsupportedEncodingException; | |
import java.util.ListIterator; | |
/** | |
* An implementation of <tt>javax.servlet.jsp.tagext.PageData</tt> 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). | |
* | |
* @author Jan Luehe | |
*/ | |
class PageDataImpl extends PageData implements TagConstants { | |
private static final String JSP_VERSION = "2.0"; | |
private static final String CDATA_START_SECTION = "<![CDATA[\n"; | |
private static final String CDATA_END_SECTION = "]]>\n"; | |
// string buffer used to build XML view | |
private StringBuffer buf; | |
/** | |
* Constructor. | |
* | |
* @param page the page nodes from which to generate the XML view | |
*/ | |
public 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 StringBuffer(); | |
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 | |
*/ | |
public InputStream getInputStream() { | |
// Turn StringBuffer into InputStream | |
try { | |
return new ByteArrayInputStream(buf.toString().getBytes("UTF-8")); | |
} catch (UnsupportedEncodingException uee) { | |
// should never happen | |
throw new RuntimeException(uee.toString()); | |
} | |
} | |
/* | |
* First-pass Visitor for JspRoot nodes (representing jsp:root elements) | |
* and TablibDirective 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. | |
*/ | |
static class FirstPassVisitor | |
extends Node.Visitor implements TagConstants { | |
private Node.Root root; | |
private AttributesImpl rootAttrs; | |
private PageInfo pageInfo; | |
// Prefix for the 'id' attribute | |
private String jspIdPrefix; | |
/* | |
* Constructor | |
*/ | |
public 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"; | |
} | |
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 | |
*/ | |
jspIdPrefix += "jsp"; | |
while (pageInfo.containsPrefix(jspIdPrefix)) { | |
jspIdPrefix += "jsp"; | |
} | |
rootAttrs.addAttribute("", "", "xmlns:" + jspIdPrefix, | |
"CDATA", JSP_URI); | |
} | |
root.setAttributes(rootAttrs); | |
} | |
} | |
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. | |
*/ | |
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: http://issues.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. | |
*/ | |
static class SecondPassVisitor extends Node.Visitor | |
implements TagConstants { | |
private Node.Root root; | |
private StringBuffer buf; | |
private Compiler compiler; | |
private String jspIdPrefix; | |
private boolean resetDefaultNS = false; | |
// Current value of jsp:id attribute | |
private int jspId; | |
/* | |
* Constructor | |
*/ | |
public SecondPassVisitor(Node.Root root, StringBuffer buf, | |
Compiler compiler, String jspIdPrefix) { | |
this.root = root; | |
this.buf = buf; | |
this.compiler = compiler; | |
this.jspIdPrefix = jspIdPrefix; | |
} | |
/* | |
* Visits root node. | |
*/ | |
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. | |
*/ | |
public void visit(Node.JspRoot n) throws JasperException { | |
visitBody(n); | |
} | |
public void visit(Node.PageDirective n) throws JasperException { | |
appendPageDirective(n); | |
} | |
public void visit(Node.IncludeDirective n) throws JasperException { | |
// expand in place | |
visitBody(n); | |
} | |
public void visit(Node.Comment n) throws JasperException { | |
// Comments are ignored in XML view | |
} | |
public void visit(Node.Declaration n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.Expression n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.Scriptlet n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.JspElement n) throws JasperException { | |
appendTag(n); | |
} | |
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(JspUtil.escapeXml(n.getText())); | |
buf.append("}"); | |
if (!n.getRoot().isXmlSyntax()) { | |
buf.append(JSP_TEXT_ACTION_END); | |
} | |
buf.append("\n"); | |
} | |
public void visit(Node.IncludeAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.ForwardAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.GetProperty n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.SetProperty n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.ParamAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.ParamsAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.FallBackAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.UseBean n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.PlugIn n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.NamedAttribute n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.JspBody n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.CustomTag n) throws JasperException { | |
boolean resetDefaultNSSave = resetDefaultNS; | |
appendTag(n, resetDefaultNS); | |
resetDefaultNS = resetDefaultNSSave; | |
} | |
public void visit(Node.UninterpretedTag n) throws JasperException { | |
boolean resetDefaultNSSave = resetDefaultNS; | |
appendTag(n, resetDefaultNS); | |
resetDefaultNS = resetDefaultNSSave; | |
} | |
public void visit(Node.JspText n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.DoBodyAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.InvokeAction n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.TagDirective n) throws JasperException { | |
appendTagDirective(n); | |
} | |
public void visit(Node.AttributeDirective n) throws JasperException { | |
appendTag(n); | |
} | |
public void visit(Node.VariableDirective n) throws JasperException { | |
appendTag(n); | |
} | |
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("\n"); | |
printAttributes(n, addDefaultNS); | |
buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); | |
buf.append(jspId++).append("\"\n"); | |
if (ROOT_ACTION.equals(n.getLocalName()) || body != null | |
|| text != null) { | |
buf.append(">\n"); | |
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("</" + n.getQName() + ">\n"); | |
} else { | |
buf.append("/>\n"); | |
} | |
} | |
/* | |
* 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++) { | |
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("\n"); | |
// append jsp:id | |
buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); | |
buf.append(jspId++).append("\"\n"); | |
// append remaining attributes | |
for (int i=0; i<len; i++) { | |
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("\"\n"); | |
} | |
if (n.getImports().size() > 0) { | |
// Concatenate names of imported classes/packages | |
boolean first = true; | |
ListIterator iter = n.getImports().listIterator(); | |
while (iter.hasNext()) { | |
if (first) { | |
first = false; | |
buf.append(" import=\""); | |
} else { | |
buf.append(","); | |
} | |
buf.append(JspUtil.getExprInXml((String) iter.next())); | |
} | |
buf.append("\"\n"); | |
} | |
buf.append("/>\n"); | |
} | |
/* | |
* 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("\n"); | |
// append jsp:id | |
buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); | |
buf.append(jspId++).append("\"\n"); | |
buf.append(" ").append("pageEncoding").append("=\"UTF-8\"\n"); | |
buf.append(" ").append("contentType").append("=\""); | |
buf.append(compiler.getPageInfo().getContentType()).append("\"\n"); | |
buf.append("/>\n"); | |
} | |
/* | |
* 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++) { | |
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("\n"); | |
// append jsp:id | |
buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); | |
buf.append(jspId++).append("\"\n"); | |
buf.append(" ").append("pageEncoding").append("=\"UTF-8\"\n"); | |
buf.append("/>\n"); | |
} | |
private void appendText(String text, boolean createJspTextElement) { | |
if (createJspTextElement) { | |
buf.append("<").append(JSP_TEXT_ACTION); | |
buf.append("\n"); | |
// append jsp:id | |
buf.append(" ").append(jspIdPrefix).append(":id").append("=\""); | |
buf.append(jspId++).append("\"\n"); | |
buf.append(">\n"); | |
appendCDATA(text); | |
buf.append(JSP_TEXT_ACTION_END); | |
buf.append("\n"); | |
} 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++) { | |
String name = attrs.getQName(i); | |
String value = attrs.getValue(i); | |
buf.append(" ").append(name).append("=\"").append(value).append("\"\n"); | |
} | |
/* | |
* 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++) { | |
String name = attrs.getQName(i); | |
String value = attrs.getValue(i); | |
buf.append(" ").append(name).append("=\"").append(value).append("\"\n"); | |
defaultNSSeen |= "xmlns".equals(name); | |
} | |
if (addDefaultNS && !defaultNSSeen) { | |
buf.append(" xmlns=\"\"\n"); | |
} | |
resetDefaultNS = false; | |
/* | |
* Append all other attributes | |
*/ | |
attrs = n.getAttributes(); | |
len = (attrs == null) ? 0 : attrs.getLength(); | |
for (int i=0; i<len; i++) { | |
String name = attrs.getQName(i); | |
String value = attrs.getValue(i); | |
buf.append(" ").append(name).append("=\""); | |
buf.append(JspUtil.getExprInXml(value)).append("\"\n"); | |
} | |
} | |
/* | |
* Appends XML prolog with encoding declaration. | |
*/ | |
private void appendXmlProlog() { | |
buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"); | |
} | |
} | |
} | |