001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.jsp;
031:
032: import com.caucho.java.JavaCompiler;
033: import com.caucho.java.LineMap;
034: import com.caucho.log.Log;
035: import com.caucho.server.connection.CauchoRequest;
036: import com.caucho.server.connection.CauchoResponse;
037: import com.caucho.server.connection.RequestAdapter;
038: import com.caucho.server.connection.ResponseAdapter;
039: import com.caucho.server.dispatch.ServletConfigImpl;
040: import com.caucho.server.webapp.WebApp;
041: import com.caucho.util.Base64;
042: import com.caucho.util.CharBuffer;
043: import com.caucho.util.RegistryException;
044: import com.caucho.util.Semaphore;
045: import com.caucho.vfs.Depend;
046: import com.caucho.vfs.Path;
047: import com.caucho.vfs.PersistentDependency;
048: import com.caucho.vfs.ReadStream;
049: import com.caucho.vfs.Vfs;
050: import com.caucho.vfs.WriteStream;
051: import com.caucho.xml.CauchoDocument;
052: import com.caucho.xml.Html;
053: import com.caucho.xml.Xml;
054: import com.caucho.xml.XmlParser;
055: import com.caucho.xml.XmlUtil;
056: import com.caucho.xpath.XPath;
057: import com.caucho.xpath.XPathException;
058: import com.caucho.xsl.CauchoStylesheet;
059: import com.caucho.xsl.StylesheetImpl;
060: import com.caucho.xsl.TransformerImpl;
061: import com.caucho.xsl.XslParseException;
062:
063: import org.w3c.dom.Document;
064: import org.w3c.dom.ProcessingInstruction;
065:
066: import javax.servlet.ServletException;
067: import javax.servlet.ServletRequest;
068: import javax.servlet.ServletResponse;
069: import javax.servlet.ServletConfig;
070: import javax.servlet.http.HttpServletRequest;
071: import javax.servlet.http.HttpServletResponse;
072: import javax.servlet.jsp.JspException;
073: import javax.servlet.jsp.JspFactory;
074: import javax.servlet.jsp.PageContext;
075: import javax.xml.transform.OutputKeys;
076: import javax.xml.transform.Templates;
077: import javax.xml.transform.TransformerConfigurationException;
078: import javax.xml.transform.dom.DOMSource;
079: import javax.xml.transform.stream.StreamResult;
080: import java.io.FileNotFoundException;
081: import java.io.IOException;
082: import java.lang.ref.SoftReference;
083: import java.util.ArrayList;
084: import java.util.HashMap;
085: import java.util.Iterator;
086: import java.util.Properties;
087: import java.util.logging.Level;
088: import java.util.logging.Logger;
089: import java.util.concurrent.*;
090:
091: /**
092: * XtpPage represents the compiled page.
093: */
094: class XtpPage extends Page {
095: private static final Logger log = Log.open(XtpPage.class);
096:
097: private boolean _strictXml;
098: private boolean _toLower = true;
099: private boolean _entitiesAsText = false;
100:
101: private Path _sourcePath;
102: private Path _pwd;
103:
104: private String _uri;
105: private String _className;
106: private String _errorPage;
107:
108: private WebApp _webApp;
109:
110: private XslManager _xslManager;
111:
112: private Page _page;
113:
114: private HashMap<String, SoftReference<Page>> _varyMap;
115: private ArrayList<String> _paramNames;
116:
117: private JspManager _jspManager;
118:
119: private final Semaphore _compileSemaphore = new Semaphore(1, false);
120:
121: /**
122: * Creates a new XTP page.
123: *
124: * @param path file containing the xtp page
125: * @param uri the request uri for the page
126: * @param className the mangled classname for the page
127: * @param uriPwd uri working dir for include() or forward()
128: * @param req the servlet request
129: * @param xslManager manager for the XSL stylesheets
130: * @param strictXml if true, use strict XML, now HTML
131: */
132: XtpPage(Path path, String uri, String className, WebApp app,
133: XslManager xslManager, boolean strictXml)
134: throws ServletException, RegistryException {
135: _sourcePath = path;
136: _sourcePath.setUserPath(uri);
137: _pwd = _sourcePath.getParent();
138: _className = className;
139: _webApp = app;
140: _strictXml = strictXml;
141: _xslManager = xslManager;
142: _uri = uri;
143:
144: ServletConfigImpl config = new ServletConfigImpl();
145: config.setServletContext(_webApp);
146:
147: init(config);
148: }
149:
150: /**
151: * Sets the JspManager for the page.
152: */
153: void setManager(JspManager manager) {
154: _jspManager = manager;
155: }
156:
157: /**
158: * When true, HTML in XTP is normalized to lower-case.
159: */
160: void setHtmlToLower(boolean toLower) {
161: _toLower = toLower;
162: }
163:
164: /**
165: * When true, XML entities are parsed as text.
166: */
167: void setEntitiesAsText(boolean entitiesAsText) {
168: _entitiesAsText = entitiesAsText;
169: }
170:
171: /**
172: * Returns true if the sources creating this page have been modified.
173: */
174: public boolean _caucho_isModified() {
175: return false;
176: }
177:
178: /**
179: * Handle a request.
180: *
181: * @param req the servlet request
182: * @param res the servlet response
183: */
184: public void service(ServletRequest request, ServletResponse response)
185: throws IOException, ServletException {
186: CauchoRequest req;
187:
188: if (request instanceof CauchoRequest)
189: req = (CauchoRequest) request;
190: else
191: req = RequestAdapter.create((HttpServletRequest) request,
192: _webApp);
193:
194: CauchoResponse res;
195: ResponseAdapter resAdapt = null;
196:
197: if (response instanceof CauchoResponse)
198: res = (CauchoResponse) response;
199: else {
200: resAdapt = ResponseAdapter
201: .create((HttpServletResponse) response);
202: res = resAdapt;
203: }
204:
205: try {
206: service(req, res);
207: } catch (InterruptedException e) {
208: log.log(Level.FINE, e.toString(), e);
209:
210: log.warning("XTP: interrupted for " + req.getPageURI());
211:
212: res.sendError(503, "Server busy: XTP generation delayed");
213: } finally {
214: if (resAdapt != null)
215: resAdapt.close();
216: }
217: }
218:
219: /**
220: * Handle a request.
221: *
222: * @param req the servlet request
223: * @param res the servlet response
224: */
225: public void service(CauchoRequest req, CauchoResponse res)
226: throws IOException, ServletException, InterruptedException {
227: Page page = getPage(req, res);
228:
229: if (page != null) {
230: page.pageservice(req, res);
231: } else {
232: log.warning("XTP: server busy on " + req.getPageURI());
233:
234: res.setHeader("Retry-After", "15");
235: res.sendError(503, "Server busy: XTP generation delayed");
236: }
237: }
238:
239: /**
240: * Returns the page.
241: */
242: private Page getPage(CauchoRequest req, CauchoResponse res)
243: throws IOException, ServletException, InterruptedException {
244: String ss = null;
245: String varyName = null;
246: Page page = _page;
247: Page deadPage = null;
248:
249: if (page == null) {
250: if (_varyMap != null) {
251: varyName = generateVaryName(req);
252:
253: if (varyName != null) {
254: SoftReference<Page> ref = _varyMap.get(varyName);
255: page = ref != null ? ref.get() : null;
256: }
257: }
258: }
259:
260: if (page != null && !page.cauchoIsModified())
261: return page;
262:
263: deadPage = page;
264: page = null;
265:
266: long timeout = deadPage == null ? 30L : 5L;
267:
268: Thread.interrupted();
269: if (_compileSemaphore.tryAcquire(timeout, TimeUnit.SECONDS)) {
270: try {
271: varyName = generateVaryName(req);
272:
273: page = getPrecompiledPage(req, varyName);
274:
275: if (page == null) {
276: CauchoDocument doc;
277:
278: try {
279: doc = parseXtp();
280: } catch (FileNotFoundException e) {
281: res.sendError(404);
282: throw e;
283: }
284:
285: Templates stylesheet = compileStylesheet(req, doc);
286:
287: // the new stylesheet affects the vary name
288: varyName = generateVaryName(req);
289:
290: page = getPrecompiledPage(req, varyName);
291:
292: if (page == null)
293: page = compileJspPage(req, res, doc,
294: stylesheet, varyName);
295: }
296:
297: if (page != null) {
298: ServletConfigImpl config = new ServletConfigImpl();
299: config.setServletContext(_webApp);
300:
301: page.init(config);
302:
303: if (varyName != null && _varyMap == null)
304: _varyMap = new HashMap<String, SoftReference<Page>>(
305: 8);
306:
307: if (varyName != null)
308: _varyMap.put(varyName, new SoftReference<Page>(
309: page));
310: else
311: _page = page;
312: } else if (deadPage != null) {
313: _page = null;
314:
315: if (varyName != null && _varyMap != null)
316: _varyMap.remove(varyName);
317: }
318: } finally {
319: _compileSemaphore.release();
320: }
321: } else {
322: log.warning("XTP: semaphore timed out on "
323: + req.getPageURI());
324: }
325:
326: if (page != null)
327: return page;
328: else
329: return deadPage;
330: }
331:
332: /**
333: * Try to load a precompiled version of the page.
334: *
335: * @param req the request for the page.
336: * @param varyName encoding for the variable stylesheet and parameters
337: * @return the precompiled page or null
338: */
339: private Page getPrecompiledPage(CauchoRequest req, String varyName)
340: throws IOException, ServletException {
341: Page page = null;
342:
343: String className = getClassName(varyName);
344:
345: try {
346: page = _jspManager.preload(className, _webApp
347: .getClassLoader(), _webApp.getAppDir(), null);
348:
349: if (page != null) {
350: if (log.isLoggable(Level.FINE))
351: log.fine("XTP using precompiled page " + className);
352:
353: return page;
354: }
355: } catch (Throwable e) {
356: log.log(Level.FINE, e.toString(), e);
357: }
358:
359: return null;
360: }
361:
362: /**
363: * Parses the XTP file as either an XML document or an HTML document.
364: */
365: private CauchoDocument parseXtp() throws IOException,
366: ServletException {
367: ReadStream is = _sourcePath.openRead();
368: try {
369: XmlParser parser;
370:
371: if (_strictXml) {
372: parser = new Xml();
373: parser.setEntitiesAsText(_entitiesAsText);
374: } else {
375: parser = new Html();
376: parser.setAutodetectXml(true);
377: parser.setEntitiesAsText(true);
378: // parser.setXmlEntitiesAsText(entitiesAsText);
379: parser.setToLower(_toLower);
380: }
381:
382: parser.setResinInclude(true);
383: parser.setJsp(true);
384:
385: return (CauchoDocument) parser.parseDocument(is);
386: } catch (Exception e) {
387: JspParseException jspE = JspParseException.create(e);
388:
389: jspE.setErrorPage(_errorPage);
390:
391: throw jspE;
392: } finally {
393: is.close();
394: }
395: }
396:
397: /**
398: * Compiles a stylesheet pages on request parameters and the parsed
399: * XML file.
400: *
401: * @param req the servlet request.
402: * @param doc the parsed XTP file as a DOM tree.
403: *
404: * @return the compiled stylesheet
405: */
406: private Templates compileStylesheet(CauchoRequest req,
407: CauchoDocument doc) throws IOException, ServletException {
408: String ssName = (String) req
409: .getAttribute("caucho.xsl.stylesheet");
410:
411: Templates stylesheet = null;
412:
413: try {
414: if (ssName == null)
415: ssName = getStylesheetHref(doc, null);
416:
417: stylesheet = _xslManager.get(ssName, req);
418: } catch (XslParseException e) {
419: JspParseException jspE;
420: if (e.getException() != null)
421: jspE = new JspParseException(e.getException());
422: else
423: jspE = new JspParseException(e);
424:
425: jspE.setErrorPage(_errorPage);
426:
427: throw jspE;
428: } catch (Exception e) {
429: JspParseException jspE;
430:
431: jspE = new JspParseException(e);
432:
433: jspE.setErrorPage(_errorPage);
434:
435: throw jspE;
436: }
437:
438: ArrayList<String> params = null;
439: if (stylesheet instanceof StylesheetImpl) {
440: StylesheetImpl ss = (StylesheetImpl) stylesheet;
441: params = (ArrayList) ss
442: .getProperty(CauchoStylesheet.GLOBAL_PARAM);
443: }
444:
445: for (int i = 0; params != null && i < params.size(); i++) {
446: String param = params.get(i);
447:
448: if (_paramNames == null)
449: _paramNames = new ArrayList<String>();
450:
451: if (param.equals("xtp:context_path")
452: || param.equals("xtp:servlet_path"))
453: continue;
454:
455: if (!_paramNames.contains(param))
456: _paramNames.add(param);
457: }
458:
459: return stylesheet;
460: }
461:
462: /**
463: * Mangles the page name to generate a unique page name.
464: *
465: * @param req the servlet request.
466: * @param res the servlet response.
467: * @param stylesheet the stylesheet.
468: * @param varyName the unique query.
469: *
470: * @return the compiled page.
471: */
472: private Page compileJspPage(CauchoRequest req, CauchoResponse res,
473: CauchoDocument doc, Templates stylesheet, String varyName)
474: throws IOException, ServletException {
475: // changing paramNames changes the varyName
476: varyName = generateVaryName(req);
477:
478: String className = getClassName(varyName);
479:
480: try {
481: return getJspPage(doc, stylesheet, req, res, className);
482: } catch (TransformerConfigurationException e) {
483: throw new ServletException(e);
484: } catch (JspException e) {
485: throw new ServletException(e);
486: }
487: }
488:
489: /**
490: * Mangles the classname
491: */
492: private String getClassName(String varyName) {
493: if (varyName == null)
494: return _className;
495: else
496: return _className + JavaCompiler.mangleName("?" + varyName);
497: }
498:
499: /**
500: * Generates a unique string for the variable parameters. The parameters
501: * depend on:
502: * <ul>
503: * <li>The value of caucho.xsl.stylesheet selecting the stylesheet.
504: * <li>The top-level xsl:param variables, which use request parameters.
505: * <li>The request's path-info.
506: * </ul>
507: *
508: * @param req the page request.
509: *
510: * @return a unique string encoding the important variations of the request.
511: */
512: private String generateVaryName(CauchoRequest req) {
513: CharBuffer cb = CharBuffer.allocate();
514:
515: String ss = (String) req.getAttribute("caucho.xsl.stylesheet");
516:
517: if (ss == null
518: && (_paramNames == null || _paramNames.size() == 0))
519: return null;
520:
521: if (ss != null) {
522: cb.append("ss.");
523: cb.append(ss);
524: }
525:
526: for (int i = 0; _paramNames != null && i < _paramNames.size(); i++) {
527: String name = (String) _paramNames.get(i);
528:
529: String value;
530:
531: if (name.equals("xtp:path_info"))
532: value = req.getPathInfo();
533: else
534: value = req.getParameter(name);
535:
536: cb.append(".");
537: cb.append(name);
538:
539: if (value != null) {
540: cb.append(".");
541: cb.append(value);
542: }
543: }
544:
545: if (cb.length() == 0)
546: return null;
547:
548: if (cb.length() < 64)
549: return cb.close();
550:
551: long hash = 37;
552: for (int i = 0; i < cb.length(); i++)
553: hash = 65521 * hash + cb.charAt(i);
554:
555: cb.setLength(32);
556: Base64.encode(cb, hash);
557:
558: return cb.close();
559: }
560:
561: /**
562: * Compile a JSP page.
563: *
564: * @param doc the parsed Serif page.
565: * @param stylesheet the stylesheet
566: * @param req the servlet request
567: * @param res the servlet response
568: * @param className the className of the generated page
569: *
570: * @return the compiled JspPage
571: */
572: private Page getJspPage(CauchoDocument doc, Templates stylesheet,
573: CauchoRequest req, CauchoResponse res, String className)
574: throws IOException, ServletException, JspException,
575: TransformerConfigurationException {
576: Path workDir = _jspManager.getClassDir();
577: String fullClassName = className;
578: Path path = workDir.lookup(fullClassName.replace('.', '/')
579: + ".jsp");
580: path.getParent().mkdirs();
581:
582: Properties output = stylesheet.getOutputProperties();
583:
584: String encoding = (String) output.get(OutputKeys.ENCODING);
585: String mimeType = (String) output.get(OutputKeys.MEDIA_TYPE);
586: String method = (String) output.get(OutputKeys.METHOD);
587:
588: if (method == null || encoding != null) {
589: } else if (method.equals("xml"))
590: encoding = "UTF-8";
591:
592: javax.xml.transform.Transformer transformer;
593: transformer = stylesheet.newTransformer();
594:
595: for (int i = 0; _paramNames != null && i < _paramNames.size(); i++) {
596: String param = (String) _paramNames.get(i);
597:
598: transformer.setParameter(param, req.getParameter(param));
599: }
600:
601: String contextPath = req.getContextPath();
602: if (contextPath != null && !contextPath.equals(""))
603: transformer.setParameter("xtp:context_path", contextPath);
604:
605: String servletPath = req.getServletPath();
606: if (servletPath != null && !servletPath.equals(""))
607: transformer.setParameter("xtp:servlet_path", servletPath);
608:
609: String pathInfo = req.getPathInfo();
610: if (pathInfo != null && !pathInfo.equals(""))
611: transformer.setParameter("xtp:path_info", pathInfo);
612:
613: transformer.setOutputProperty("caucho.jsp", "true");
614:
615: LineMap lineMap = null;
616: WriteStream os = path.openWrite();
617: try {
618: if (encoding != null) {
619: os.setEncoding(encoding);
620: if (mimeType == null)
621: mimeType = "text/html";
622:
623: os.print("<%@ page contentType=\"" + mimeType
624: + "; charset=" + encoding + "\" %>");
625: } else if (mimeType != null)
626: os
627: .print("<%@ page contentType=\"" + mimeType
628: + "\" %>");
629:
630: lineMap = writeJspDoc(os, doc, transformer, req, res);
631: } finally {
632: os.close();
633: }
634:
635: StylesheetImpl ss = null;
636: if (stylesheet instanceof StylesheetImpl)
637: ss = (StylesheetImpl) stylesheet;
638:
639: try {
640: path.setUserPath(_sourcePath.getPath());
641:
642: boolean cacheable = true; // jspDoc.isCacheable();
643: ArrayList<PersistentDependency> depends = new ArrayList<PersistentDependency>();
644:
645: ArrayList<Depend> styleDepends = null;
646: if (ss != null)
647: styleDepends = (ArrayList) ss
648: .getProperty(StylesheetImpl.DEPENDS);
649: for (int i = 0; styleDepends != null
650: && i < styleDepends.size(); i++) {
651: Depend depend = styleDepends.get(i);
652:
653: Depend jspDepend = new Depend(depend.getPath(), depend
654: .getLastModified(), depend.getLength());
655: jspDepend.setRequireSource(true);
656:
657: if (!depends.contains(jspDepend))
658: depends.add(jspDepend);
659: }
660:
661: // Fill the page dependency information from the document into
662: // the jsp page.
663: ArrayList<Path> docDepends;
664: docDepends = (ArrayList) doc
665: .getProperty(CauchoDocument.DEPENDS);
666: for (int i = 0; docDepends != null && i < docDepends.size(); i++) {
667: Path depend = docDepends.get(i);
668:
669: Depend jspDepend = new Depend(depend);
670: if (!depends.contains(jspDepend))
671: depends.add(jspDepend);
672: }
673:
674: // stylesheet cache dependencies are normal dependencies for JSP
675: ArrayList<Path> cacheDepends = null;
676: TransformerImpl xform = null;
677: if (transformer instanceof TransformerImpl)
678: xform = (TransformerImpl) transformer;
679: if (xform != null)
680: cacheDepends = (ArrayList) xform
681: .getProperty(TransformerImpl.CACHE_DEPENDS);
682: for (int i = 0; cacheDepends != null
683: && i < cacheDepends.size(); i++) {
684: Path depend = cacheDepends.get(i);
685: Depend jspDepend = new Depend(depend);
686: if (!depends.contains(jspDepend))
687: depends.add(jspDepend);
688: }
689:
690: ServletConfig config = null;
691: Page page = _jspManager.createGeneratedPage(path, _uri,
692: className, config, depends);
693:
694: return page;
695: } catch (IOException e) {
696: throw e;
697: } catch (ServletException e) {
698: throw e;
699: } catch (Exception e) {
700: throw new QJspException(e);
701: }
702: }
703:
704: /**
705: * Transform XTP page with the stylesheet to JSP source.
706: *
707: * @param os the output stream to the generated JSP.
708: * @param doc the parsed XTP file.
709: * @param transformed the XSL stylesheet with parameters applied.
710: * @param req the servlet request.
711: * @param res the servlet response.
712: *
713: * @return the line map from the JSP to the original source.
714: */
715: private LineMap writeJspDoc(WriteStream os, Document doc,
716: javax.xml.transform.Transformer transformer,
717: CauchoRequest req, CauchoResponse res) throws IOException,
718: ServletException {
719: PageContext pageContext;
720:
721: JspFactory factory = JspFactory.getDefaultFactory();
722:
723: TransformerImpl xform = null;
724: if (transformer instanceof TransformerImpl)
725: xform = (TransformerImpl) transformer;
726: String errorPage = null;
727: if (xform != null)
728: errorPage = (String) xform.getProperty("caucho.error.page");
729: pageContext = factory.getPageContext(this , req, res, errorPage,
730: false, 8192, // bufferSize,
731: false); // autoFlush);
732:
733: try {
734: if (xform != null) {
735: xform.setProperty("caucho.page.context", pageContext);
736: xform.setProperty("caucho.pwd", Vfs.lookup());
737: }
738:
739: DOMSource source = new DOMSource(doc);
740: StreamResult result = new StreamResult(os);
741:
742: xform.setFeature(TransformerImpl.GENERATE_LOCATION, true);
743: transformer.transform(source, result);
744:
745: if (xform != null)
746: return (LineMap) xform
747: .getProperty(TransformerImpl.LINE_MAP);
748: else
749: return null;
750: } catch (Exception e) {
751: pageContext.handlePageException(e);
752: } finally {
753: factory.releasePageContext(pageContext);
754: }
755:
756: return null;
757: }
758:
759: /**
760: * Returns the stylesheet specified by the page.
761: *
762: * The syntax is:
763: * <pre>
764: * <?xml-stylesheet href='...' media='...'?>
765: * </pre>
766: *
767: * @param doc the XTP document
768: * @param media the http request media
769: *
770: * @return the href of the xml-stylesheet processing-instruction or
771: * "default.xsl" if none is found.
772: */
773: private String getStylesheetHref(Document doc, String media)
774: throws XPathException {
775: Iterator iter = XPath.select(
776: "//processing-instruction('xml-stylesheet')", doc);
777: while (iter.hasNext()) {
778: ProcessingInstruction pi = (ProcessingInstruction) iter
779: .next();
780: String value = pi.getNodeValue();
781: String piMedia = XmlUtil.getPIAttribute(value, "media");
782:
783: if (piMedia == null || piMedia.equals(media))
784: return XmlUtil.getPIAttribute(value, "href");
785: }
786:
787: return "default.xsl"; // xslManager.getDefaultStylesheet();
788: }
789:
790: /**
791: * Returns true if the document varies according to the "media".
792: * (Currently unused.)
793: */
794: private boolean varyMedia(Document doc) throws XPathException {
795: Iterator iter = XPath.select(
796: "//processing-instruction('xml-stylesheet')", doc);
797: while (iter.hasNext()) {
798: ProcessingInstruction pi = (ProcessingInstruction) iter
799: .next();
800: String value = pi.getNodeValue();
801: String piMedia = XmlUtil.getPIAttribute(value, "media");
802:
803: if (piMedia != null)
804: return true;
805: }
806:
807: return false;
808: }
809:
810: public boolean disableLog() {
811: return true;
812: }
813:
814: /**
815: * Returns a printable version of the page object
816: */
817: public String toString() {
818: return "XtpPage[" + _uri + "]";
819: }
820: }
|