View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.struts2.jasper.compiler;
18  
19  import org.apache.struts2.jasper.JasperException;
20  import org.xml.sax.Attributes;
21  import org.xml.sax.helpers.AttributesImpl;
22  
23  import javax.servlet.jsp.tagext.PageData;
24  import java.io.ByteArrayInputStream;
25  import java.io.CharArrayWriter;
26  import java.io.InputStream;
27  import java.io.UnsupportedEncodingException;
28  import java.util.ListIterator;
29  
30  /***
31   * An implementation of <tt>javax.servlet.jsp.tagext.PageData</tt> which
32   * builds the XML view of a given page.
33   * <p/>
34   * The XML view is built in two passes:
35   * <p/>
36   * During the first pass, the FirstPassVisitor collects the attributes of the
37   * top-level jsp:root and those of the jsp:root elements of any included
38   * pages, and adds them to the jsp:root element of the XML view.
39   * In addition, any taglib directives are converted into xmlns: attributes and
40   * added to the jsp:root element of the XML view.
41   * This pass ignores any nodes other than JspRoot and TaglibDirective.
42   * <p/>
43   * During the second pass, the SecondPassVisitor produces the XML view, using
44   * the combined jsp:root attributes determined in the first pass and any
45   * remaining pages nodes (this pass ignores any JspRoot and TaglibDirective
46   * nodes).
47   *
48   * @author Jan Luehe
49   */
50  class PageDataImpl extends PageData implements TagConstants {
51  
52      private static final String JSP_VERSION = "2.0";
53      private static final String CDATA_START_SECTION = "<![CDATA[\n";
54      private static final String CDATA_END_SECTION = "]]>\n";
55  
56      // string buffer used to build XML view
57      private StringBuffer buf;
58  
59      /***
60       * Constructor.
61       *
62       * @param page the page nodes from which to generate the XML view
63       */
64      public PageDataImpl(Node.Nodes page, Compiler compiler)
65              throws JasperException {
66  
67          // First pass
68          FirstPassVisitor firstPass = new FirstPassVisitor(page.getRoot(),
69                  compiler.getPageInfo());
70          page.visit(firstPass);
71  
72          // Second pass
73          buf = new StringBuffer();
74          SecondPassVisitor secondPass
75                  = new SecondPassVisitor(page.getRoot(), buf, compiler,
76                  firstPass.getJspIdPrefix());
77          page.visit(secondPass);
78      }
79  
80      /***
81       * Returns the input stream of the XML view.
82       *
83       * @return the input stream of the XML view
84       */
85      public InputStream getInputStream() {
86          // Turn StringBuffer into InputStream
87          try {
88              return new ByteArrayInputStream(buf.toString().getBytes("UTF-8"));
89          } catch (UnsupportedEncodingException uee) {
90              // should never happen
91              throw new RuntimeException(uee.toString());
92          }
93      }
94  
95      /*
96       * First-pass Visitor for JspRoot nodes (representing jsp:root elements)
97       * and TablibDirective nodes, ignoring any other nodes.
98       *
99       * The purpose of this Visitor is to collect the attributes of the
100      * top-level jsp:root and those of the jsp:root elements of any included
101      * pages, and add them to the jsp:root element of the XML view.
102      * In addition, this Visitor converts any taglib directives into xmlns:
103      * attributes and adds them to the jsp:root element of the XML view.
104      */
105     static class FirstPassVisitor
106             extends Node.Visitor implements TagConstants {
107 
108         private Node.Root root;
109         private AttributesImpl rootAttrs;
110         private PageInfo pageInfo;
111 
112         // Prefix for the 'id' attribute
113         private String jspIdPrefix;
114 
115         /*
116          * Constructor
117          */
118         public FirstPassVisitor(Node.Root root, PageInfo pageInfo) {
119             this.root = root;
120             this.pageInfo = pageInfo;
121             this.rootAttrs = new AttributesImpl();
122             this.rootAttrs.addAttribute("", "", "version", "CDATA",
123                     JSP_VERSION);
124             this.jspIdPrefix = "jsp";
125         }
126 
127         public void visit(Node.Root n) throws JasperException {
128             visitBody(n);
129             if (n == root) {
130                 /*
131                  * Top-level page.
132                  *
133                  * Add
134                  *   xmlns:jsp="http://java.sun.com/JSP/Page"
135                  * attribute only if not already present.
136                  */
137                 if (!JSP_URI.equals(rootAttrs.getValue("xmlns:jsp"))) {
138                     rootAttrs.addAttribute("", "", "xmlns:jsp", "CDATA",
139                             JSP_URI);
140                 }
141 
142                 if (pageInfo.isJspPrefixHijacked()) {
143                     /*
144                      * 'jsp' prefix has been hijacked, that is, bound to a
145                      * namespace other than the JSP namespace. This means that
146                      * when adding an 'id' attribute to each element, we can't
147                      * use the 'jsp' prefix. Therefore, create a new prefix 
148                      * (one that is unique across the translation unit) for use
149                      * by the 'id' attribute, and bind it to the JSP namespace
150                      */
151                     jspIdPrefix += "jsp";
152                     while (pageInfo.containsPrefix(jspIdPrefix)) {
153                         jspIdPrefix += "jsp";
154                     }
155                     rootAttrs.addAttribute("", "", "xmlns:" + jspIdPrefix,
156                             "CDATA", JSP_URI);
157                 }
158 
159                 root.setAttributes(rootAttrs);
160             }
161         }
162 
163         public void visit(Node.JspRoot n) throws JasperException {
164             addAttributes(n.getTaglibAttributes());
165             addAttributes(n.getNonTaglibXmlnsAttributes());
166             addAttributes(n.getAttributes());
167 
168             visitBody(n);
169         }
170 
171         /*
172          * Converts taglib directive into "xmlns:..." attribute of jsp:root
173          * element.
174          */
175         public void visit(Node.TaglibDirective n) throws JasperException {
176             Attributes attrs = n.getAttributes();
177             if (attrs != null) {
178                 String qName = "xmlns:" + attrs.getValue("prefix");
179                 /*
180                  * According to javadocs of org.xml.sax.helpers.AttributesImpl,
181                  * the addAttribute method does not check to see if the
182                  * specified attribute is already contained in the list: This
183                  * is the application's responsibility!
184                  */
185                 if (rootAttrs.getIndex(qName) == -1) {
186                     String location = attrs.getValue("uri");
187                     if (location != null) {
188                         if (location.startsWith("/")) {
189                             location = URN_JSPTLD + location;
190                         }
191                         rootAttrs.addAttribute("", "", qName, "CDATA",
192                                 location);
193                     } else {
194                         location = attrs.getValue("tagdir");
195                         rootAttrs.addAttribute("", "", qName, "CDATA",
196                                 URN_JSPTAGDIR + location);
197                     }
198                 }
199             }
200         }
201 
202         public String getJspIdPrefix() {
203             return jspIdPrefix;
204         }
205 
206         private void addAttributes(Attributes attrs) {
207             if (attrs != null) {
208                 int len = attrs.getLength();
209 
210                 for (int i = 0; i < len; i++) {
211                     String qName = attrs.getQName(i);
212                     if ("version".equals(qName)) {
213                         continue;
214                     }
215 
216                     // Bugzilla 35252: http://issues.apache.org/bugzilla/show_bug.cgi?id=35252
217                     if (rootAttrs.getIndex(qName) == -1) {
218                         rootAttrs.addAttribute(attrs.getURI(i),
219                                 attrs.getLocalName(i),
220                                 qName,
221                                 attrs.getType(i),
222                                 attrs.getValue(i));
223                     }
224                 }
225             }
226         }
227     }
228 
229 
230     /*
231      * Second-pass Visitor responsible for producing XML view and assigning
232      * each element a unique jsp:id attribute.
233      */
234     static class SecondPassVisitor extends Node.Visitor
235             implements TagConstants {
236 
237         private Node.Root root;
238         private StringBuffer buf;
239         private Compiler compiler;
240         private String jspIdPrefix;
241         private boolean resetDefaultNS = false;
242 
243         // Current value of jsp:id attribute
244         private int jspId;
245 
246         /*
247          * Constructor
248          */
249         public SecondPassVisitor(Node.Root root, StringBuffer buf,
250                                  Compiler compiler, String jspIdPrefix) {
251             this.root = root;
252             this.buf = buf;
253             this.compiler = compiler;
254             this.jspIdPrefix = jspIdPrefix;
255         }
256 
257         /*
258          * Visits root node.
259          */
260         public void visit(Node.Root n) throws JasperException {
261             if (n == this.root) {
262                 // top-level page
263                 appendXmlProlog();
264                 appendTag(n);
265             } else {
266                 boolean resetDefaultNSSave = resetDefaultNS;
267                 if (n.isXmlSyntax()) {
268                     resetDefaultNS = true;
269                 }
270                 visitBody(n);
271                 resetDefaultNS = resetDefaultNSSave;
272             }
273         }
274 
275         /*
276          * Visits jsp:root element of JSP page in XML syntax.
277          *
278          * Any nested jsp:root elements (from pages included via an
279          * include directive) are ignored.
280          */
281         public void visit(Node.JspRoot n) throws JasperException {
282             visitBody(n);
283         }
284 
285         public void visit(Node.PageDirective n) throws JasperException {
286             appendPageDirective(n);
287         }
288 
289         public void visit(Node.IncludeDirective n) throws JasperException {
290             // expand in place
291             visitBody(n);
292         }
293 
294         public void visit(Node.Comment n) throws JasperException {
295             // Comments are ignored in XML view
296         }
297 
298         public void visit(Node.Declaration n) throws JasperException {
299             appendTag(n);
300         }
301 
302         public void visit(Node.Expression n) throws JasperException {
303             appendTag(n);
304         }
305 
306         public void visit(Node.Scriptlet n) throws JasperException {
307             appendTag(n);
308         }
309 
310         public void visit(Node.JspElement n) throws JasperException {
311             appendTag(n);
312         }
313 
314         public void visit(Node.ELExpression n) throws JasperException {
315             if (!n.getRoot().isXmlSyntax()) {
316                 buf.append("<").append(JSP_TEXT_ACTION);
317                 buf.append(" ");
318                 buf.append(jspIdPrefix);
319                 buf.append(":id=\"");
320                 buf.append(jspId++).append("\">");
321             }
322             buf.append("${");
323             buf.append(JspUtil.escapeXml(n.getText()));
324             buf.append("}");
325             if (!n.getRoot().isXmlSyntax()) {
326                 buf.append(JSP_TEXT_ACTION_END);
327             }
328             buf.append("\n");
329         }
330 
331         public void visit(Node.IncludeAction n) throws JasperException {
332             appendTag(n);
333         }
334 
335         public void visit(Node.ForwardAction n) throws JasperException {
336             appendTag(n);
337         }
338 
339         public void visit(Node.GetProperty n) throws JasperException {
340             appendTag(n);
341         }
342 
343         public void visit(Node.SetProperty n) throws JasperException {
344             appendTag(n);
345         }
346 
347         public void visit(Node.ParamAction n) throws JasperException {
348             appendTag(n);
349         }
350 
351         public void visit(Node.ParamsAction n) throws JasperException {
352             appendTag(n);
353         }
354 
355         public void visit(Node.FallBackAction n) throws JasperException {
356             appendTag(n);
357         }
358 
359         public void visit(Node.UseBean n) throws JasperException {
360             appendTag(n);
361         }
362 
363         public void visit(Node.PlugIn n) throws JasperException {
364             appendTag(n);
365         }
366 
367         public void visit(Node.NamedAttribute n) throws JasperException {
368             appendTag(n);
369         }
370 
371         public void visit(Node.JspBody n) throws JasperException {
372             appendTag(n);
373         }
374 
375         public void visit(Node.CustomTag n) throws JasperException {
376             boolean resetDefaultNSSave = resetDefaultNS;
377             appendTag(n, resetDefaultNS);
378             resetDefaultNS = resetDefaultNSSave;
379         }
380 
381         public void visit(Node.UninterpretedTag n) throws JasperException {
382             boolean resetDefaultNSSave = resetDefaultNS;
383             appendTag(n, resetDefaultNS);
384             resetDefaultNS = resetDefaultNSSave;
385         }
386 
387         public void visit(Node.JspText n) throws JasperException {
388             appendTag(n);
389         }
390 
391         public void visit(Node.DoBodyAction n) throws JasperException {
392             appendTag(n);
393         }
394 
395         public void visit(Node.InvokeAction n) throws JasperException {
396             appendTag(n);
397         }
398 
399         public void visit(Node.TagDirective n) throws JasperException {
400             appendTagDirective(n);
401         }
402 
403         public void visit(Node.AttributeDirective n) throws JasperException {
404             appendTag(n);
405         }
406 
407         public void visit(Node.VariableDirective n) throws JasperException {
408             appendTag(n);
409         }
410 
411         public void visit(Node.TemplateText n) throws JasperException {
412             /*
413              * If the template text came from a JSP page written in JSP syntax,
414              * create a jsp:text element for it (JSP 5.3.2).
415              */
416             appendText(n.getText(), !n.getRoot().isXmlSyntax());
417         }
418 
419         /*
420          * Appends the given tag, including its body, to the XML view.
421          */
422         private void appendTag(Node n) throws JasperException {
423             appendTag(n, false);
424         }
425 
426         /*
427          * Appends the given tag, including its body, to the XML view,
428          * and optionally reset default namespace to "", if none specified.
429          */
430         private void appendTag(Node n, boolean addDefaultNS)
431                 throws JasperException {
432 
433             Node.Nodes body = n.getBody();
434             String text = n.getText();
435 
436             buf.append("<").append(n.getQName());
437             buf.append("\n");
438 
439             printAttributes(n, addDefaultNS);
440             buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
441             buf.append(jspId++).append("\"\n");
442 
443             if (ROOT_ACTION.equals(n.getLocalName()) || body != null
444                     || text != null) {
445                 buf.append(">\n");
446                 if (ROOT_ACTION.equals(n.getLocalName())) {
447                     if (compiler.getCompilationContext().isTagFile()) {
448                         appendTagDirective();
449                     } else {
450                         appendPageDirective();
451                     }
452                 }
453                 if (body != null) {
454                     body.visit(this);
455                 } else {
456                     appendText(text, false);
457                 }
458                 buf.append("</" + n.getQName() + ">\n");
459             } else {
460                 buf.append("/>\n");
461             }
462         }
463 
464         /*
465          * Appends the page directive with the given attributes to the XML
466          * view.
467          *
468          * Since the import attribute of the page directive is the only page
469          * attribute that is allowed to appear multiple times within the same
470          * document, and since XML allows only single-value attributes,
471          * the values of multiple import attributes must be combined into one,
472          * separated by comma.
473          *
474          * If the given page directive contains just 'contentType' and/or
475          * 'pageEncoding' attributes, we ignore it, as we've already appended
476          * a page directive containing just these two attributes.
477          */
478         private void appendPageDirective(Node.PageDirective n) {
479             boolean append = false;
480             Attributes attrs = n.getAttributes();
481             int len = (attrs == null) ? 0 : attrs.getLength();
482             for (int i = 0; i < len; i++) {
483                 String attrName = attrs.getQName(i);
484                 if (!"pageEncoding".equals(attrName)
485                         && !"contentType".equals(attrName)) {
486                     append = true;
487                     break;
488                 }
489             }
490             if (!append) {
491                 return;
492             }
493 
494             buf.append("<").append(n.getQName());
495             buf.append("\n");
496 
497             // append jsp:id
498             buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
499             buf.append(jspId++).append("\"\n");
500 
501             // append remaining attributes
502             for (int i = 0; i < len; i++) {
503                 String attrName = attrs.getQName(i);
504                 if ("import".equals(attrName) || "contentType".equals(attrName)
505                         || "pageEncoding".equals(attrName)) {
506                     /*
507                      * Page directive's 'import' attribute is considered
508                      * further down, and its 'pageEncoding' and 'contentType'
509                      * attributes are ignored, since we've already appended
510                      * a new page directive containing just these two
511                      * attributes
512                      */
513                     continue;
514                 }
515                 String value = attrs.getValue(i);
516                 buf.append("  ").append(attrName).append("=\"");
517                 buf.append(JspUtil.getExprInXml(value)).append("\"\n");
518             }
519             if (n.getImports().size() > 0) {
520                 // Concatenate names of imported classes/packages
521                 boolean first = true;
522                 ListIterator iter = n.getImports().listIterator();
523                 while (iter.hasNext()) {
524                     if (first) {
525                         first = false;
526                         buf.append("  import=\"");
527                     } else {
528                         buf.append(",");
529                     }
530                     buf.append(JspUtil.getExprInXml((String) iter.next()));
531                 }
532                 buf.append("\"\n");
533             }
534             buf.append("/>\n");
535         }
536 
537         /*
538          * Appends a page directive with 'pageEncoding' and 'contentType'
539          * attributes.
540          *
541          * The value of the 'pageEncoding' attribute is hard-coded
542          * to UTF-8, whereas the value of the 'contentType' attribute, which
543          * is identical to what the container will pass to
544          * ServletResponse.setContentType(), is derived from the pageInfo.
545          */
546         private void appendPageDirective() {
547             buf.append("<").append(JSP_PAGE_DIRECTIVE_ACTION);
548             buf.append("\n");
549 
550             // append jsp:id
551             buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
552             buf.append(jspId++).append("\"\n");
553             buf.append("  ").append("pageEncoding").append("=\"UTF-8\"\n");
554             buf.append("  ").append("contentType").append("=\"");
555             buf.append(compiler.getPageInfo().getContentType()).append("\"\n");
556             buf.append("/>\n");
557         }
558 
559         /*
560          * Appends the tag directive with the given attributes to the XML
561          * view.
562          *
563          * If the given tag directive contains just a 'pageEncoding'
564          * attributes, we ignore it, as we've already appended
565          * a tag directive containing just this attributes.
566          */
567         private void appendTagDirective(Node.TagDirective n)
568                 throws JasperException {
569 
570             boolean append = false;
571             Attributes attrs = n.getAttributes();
572             int len = (attrs == null) ? 0 : attrs.getLength();
573             for (int i = 0; i < len; i++) {
574                 String attrName = attrs.getQName(i);
575                 if (!"pageEncoding".equals(attrName)) {
576                     append = true;
577                     break;
578                 }
579             }
580             if (!append) {
581                 return;
582             }
583 
584             appendTag(n);
585         }
586 
587         /*
588          * Appends a tag directive containing a single 'pageEncoding'
589          * attribute whose value is hard-coded to UTF-8.
590          */
591         private void appendTagDirective() {
592             buf.append("<").append(JSP_TAG_DIRECTIVE_ACTION);
593             buf.append("\n");
594 
595             // append jsp:id
596             buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
597             buf.append(jspId++).append("\"\n");
598             buf.append("  ").append("pageEncoding").append("=\"UTF-8\"\n");
599             buf.append("/>\n");
600         }
601 
602         private void appendText(String text, boolean createJspTextElement) {
603             if (createJspTextElement) {
604                 buf.append("<").append(JSP_TEXT_ACTION);
605                 buf.append("\n");
606 
607                 // append jsp:id
608                 buf.append("  ").append(jspIdPrefix).append(":id").append("=\"");
609                 buf.append(jspId++).append("\"\n");
610                 buf.append(">\n");
611 
612                 appendCDATA(text);
613                 buf.append(JSP_TEXT_ACTION_END);
614                 buf.append("\n");
615             } else {
616                 appendCDATA(text);
617             }
618         }
619 
620         /*
621         * Appends the given text as a CDATA section to the XML view, unless
622         * the text has already been marked as CDATA.
623         */
624         private void appendCDATA(String text) {
625             buf.append(CDATA_START_SECTION);
626             buf.append(escapeCDATA(text));
627             buf.append(CDATA_END_SECTION);
628         }
629 
630         /*
631          * Escapes any occurrences of "]]>" (by replacing them with "]]&gt;")
632          * within the given text, so it can be included in a CDATA section.
633          */
634         private String escapeCDATA(String text) {
635             if (text == null) return "";
636             int len = text.length();
637             CharArrayWriter result = new CharArrayWriter(len);
638             for (int i = 0; i < len; i++) {
639                 if (((i + 2) < len)
640                         && (text.charAt(i) == ']')
641                         && (text.charAt(i + 1) == ']')
642                         && (text.charAt(i + 2) == '>')) {
643                     // match found
644                     result.write(']');
645                     result.write(']');
646                     result.write('&');
647                     result.write('g');
648                     result.write('t');
649                     result.write(';');
650                     i += 2;
651                 } else {
652                     result.write(text.charAt(i));
653                 }
654             }
655             return result.toString();
656         }
657 
658         /*
659          * Appends the attributes of the given Node to the XML view.
660          */
661         private void printAttributes(Node n, boolean addDefaultNS) {
662 
663             /*
664              * Append "xmlns" attributes that represent tag libraries
665              */
666             Attributes attrs = n.getTaglibAttributes();
667             int len = (attrs == null) ? 0 : attrs.getLength();
668             for (int i = 0; i < len; i++) {
669                 String name = attrs.getQName(i);
670                 String value = attrs.getValue(i);
671                 buf.append("  ").append(name).append("=\"").append(value).append("\"\n");
672             }
673 
674             /*
675              * Append "xmlns" attributes that do not represent tag libraries
676              */
677             attrs = n.getNonTaglibXmlnsAttributes();
678             len = (attrs == null) ? 0 : attrs.getLength();
679             boolean defaultNSSeen = false;
680             for (int i = 0; i < len; i++) {
681                 String name = attrs.getQName(i);
682                 String value = attrs.getValue(i);
683                 buf.append("  ").append(name).append("=\"").append(value).append("\"\n");
684                 defaultNSSeen |= "xmlns".equals(name);
685             }
686             if (addDefaultNS && !defaultNSSeen) {
687                 buf.append("  xmlns=\"\"\n");
688             }
689             resetDefaultNS = false;
690 
691             /*
692              * Append all other attributes
693              */
694             attrs = n.getAttributes();
695             len = (attrs == null) ? 0 : attrs.getLength();
696             for (int i = 0; i < len; i++) {
697                 String name = attrs.getQName(i);
698                 String value = attrs.getValue(i);
699                 buf.append("  ").append(name).append("=\"");
700                 buf.append(JspUtil.getExprInXml(value)).append("\"\n");
701             }
702         }
703 
704         /*
705          * Appends XML prolog with encoding declaration.
706          */
707         private void appendXmlProlog() {
708             buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
709         }
710     }
711 }
712