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