001: /*
002: * argun 1.0
003: * Web 2.0 delivery framework
004: * Copyright (C) 2007 Hammurapi Group
005: *
006: * This program is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation; either
009: * version 2 of the License, or (at your option) any later version.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * You should have received a copy of the GNU Lesser General Public
017: * License along with this library; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: *
020: * URL: http://www.hammurapi.biz
021: * e-Mail: support@hammurapi.biz
022: */
023: package biz.hammurapi.web;
024:
025: import java.io.IOException;
026: import java.io.OutputStreamWriter;
027: import java.io.PrintWriter;
028: import java.io.StringWriter;
029: import java.io.Writer;
030: import java.lang.reflect.InvocationTargetException;
031: import java.lang.reflect.Method;
032: import java.util.HashMap;
033: import java.util.Map;
034:
035: import javax.servlet.ServletConfig;
036: import javax.servlet.ServletException;
037: import javax.servlet.http.HttpServletRequest;
038: import javax.servlet.http.HttpServletResponse;
039: import javax.servlet.http.HttpSession;
040: import javax.xml.parsers.FactoryConfigurationError;
041: import javax.xml.parsers.ParserConfigurationException;
042: import javax.xml.transform.Result;
043: import javax.xml.transform.stream.StreamResult;
044:
045: import org.apache.log4j.Logger;
046: import org.w3c.dom.Document;
047: import org.w3c.dom.Element;
048: import org.w3c.dom.Node;
049:
050: import biz.hammurapi.authorization.AuthorizationProvider;
051: import biz.hammurapi.config.Context;
052: import biz.hammurapi.metrics.MeasurementCategoryFactory;
053: import biz.hammurapi.metrics.TimeIntervalCategory;
054: import biz.hammurapi.web.menu.MenuFilter;
055: import biz.hammurapi.xml.dom.AbstractDomObject;
056: import biz.hammurapi.xml.dom.CompositeDomSerializer;
057:
058: /**
059: * Dispatching servlet dispatches requests to action methods. Action method is any method which takes 3 parameters:
060: * HttpServletRequest, HttpServletResponse, DispatchingServlet or two parameters HttpServletRequest and HttpServletResponse.
061: * Return value of action method is processed in the following way:
062: * a) If it is instance of Forward then forward is performed
063: * b) If it is instance of String then it is gets written to response output stream.
064: * c) Otherwise it is XML-ized and then styled.
065: */
066: public abstract class DispatchingServlet extends StylingServlet {
067: private static final String AUTH_NONE = "none";
068: private static final String AUTH_TRUST_MENU = "trust-menu";
069: private static final String AUTH_STRICT = "strict";
070: private static final String AUTHORIZATION = "authorization";
071: private static final String EXCEPTION_HANDLING = "exception-handling";
072: private static final String PERMISSIONS_ELEMENT = "permissions-element";
073: private String permissionsElement;
074: private String exceptionHandling;
075: private String authorization;
076:
077: private static final Logger logger = Logger
078: .getLogger(DispatchingServlet.class);
079: private static final TimeIntervalCategory tic = MeasurementCategoryFactory
080: .getTimeIntervalCategory(DispatchingServlet.class);
081: private static final TimeIntervalCategory atic = MeasurementCategoryFactory
082: .getTimeIntervalCategory(DispatchingServlet.class.getName()
083: + ".action");
084: private static final String AUTHORIZATION_PROVIDER_ATTRIBUTE = AuthorizationProvider.class
085: .getName();
086:
087: /**
088: * Override this method to return custom serializer if needed.
089: * @return DomSerializer
090: */
091: protected CompositeDomSerializer getDomSerializer() {
092: return CompositeDomSerializer.getThreadInstance();
093: }
094:
095: public void init(ServletConfig config) throws ServletException {
096: super .init(config);
097: config.getServletContext().log(
098: "Initialization: " + config.getServletName());
099:
100: authorization = config.getInitParameter(AUTHORIZATION);
101: if (authorization == null) {
102: authorization = AUTH_STRICT;
103: }
104:
105: if (!(AUTH_STRICT.equals(authorization)
106: || AUTH_NONE.equals(authorization) || AUTH_TRUST_MENU
107: .equals(authorization))) {
108: throw new ServletException("Invalid authorization mode: "
109: + authorization);
110: }
111:
112: String initParameter = config
113: .getInitParameter(PERMISSIONS_ELEMENT);
114: if (initParameter != null) {
115: permissionsElement = initParameter;
116: config.getServletContext().log(
117: "permissions-element=" + permissionsElement);
118: }
119:
120: initParameter = config.getInitParameter(EXCEPTION_HANDLING);
121: if (initParameter != null) {
122: exceptionHandling = initParameter;
123: config.getServletContext().log(
124: "exception-handling=" + exceptionHandling);
125: }
126:
127: config.getServletContext().setAttribute(
128: "servlet/" + config.getServletName(), this );
129: }
130:
131: /**
132: * Extracts instance name from the path and returns instance for the path.
133: * @param actionPath
134: * @return Object to invoke action methods
135: * @throws HammurapiWebException
136: */
137: protected abstract Object getActionInstance(String path)
138: throws HammurapiWebException;
139:
140: /**
141: * @param path
142: * @return Action method name
143: * @throws HammurapiWebException
144: */
145: protected abstract String getActionName(String path)
146: throws HammurapiWebException;
147:
148: /**
149: * @return Position of "/" in the servlet path info which separates action name from style info.
150: */
151: protected abstract int styleSeparatorPosition();
152:
153: private Map actions = new HashMap();
154:
155: private class Action {
156: private Method method;
157: private Object instance;
158: private int argCount;
159: private boolean isContextMethod; // true if the first parameter is context
160:
161: public Action(Method method, Object instance) {
162: super ();
163: this .method = method;
164: this .instance = instance;
165: argCount = method.getParameterTypes().length;
166: isContextMethod = Context.class.equals(method
167: .getParameterTypes()[0]);
168: }
169:
170: public Object execute(HttpServletRequest request,
171: HttpServletResponse response, String path)
172: throws HammurapiWebException {
173: // Do authorization
174: if (AUTH_STRICT.equals(authorization)
175: || (AUTH_TRUST_MENU.equals(authorization) && request
176: .getAttribute(MenuFilter.MENU_MATCHED_ATTRIBUTE) == null)) {
177: Object ap = request
178: .getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
179: if (ap == null) {
180: HttpSession session = request.getSession(false);
181: ap = session == null ? null
182: : session
183: .getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
184: }
185:
186: if (ap instanceof AuthorizationProvider) {
187: if (!((AuthorizationProvider) ap)
188: .hasInstancePermission(instance, method
189: .getName())) {
190: return "You are not authorized to access this page"; // Replace with proper Http error if needed.
191: }
192: } else if (ap != null) {
193: logger
194: .warn("Configuration problem with authorization provider: "
195: + ap.getClass().getName()
196: + " does not implement "
197: + AuthorizationProvider.class
198: .getName());
199: }
200: }
201:
202: long start = atic.getTime();
203: try {
204: Object[] args;
205: if (isContextMethod) {
206: RequestContext rc = new RequestContext(request);
207: switch (argCount) {
208: case 1:
209: args = new Object[] { rc };
210: break;
211: case 2:
212: args = new Object[] { rc,
213: DispatchingServlet.this };
214: break;
215: case 3:
216: args = new Object[] { rc,
217: DispatchingServlet.this , path };
218: break;
219: default:
220: throw new HammurapiWebException(
221: "Invalid number of arguments in method "
222: + method);
223: }
224: } else {
225: switch (argCount) {
226: case 2:
227: args = new Object[] { request, response };
228: break;
229: case 3:
230: args = new Object[] { request, response,
231: DispatchingServlet.this };
232: break;
233: case 4:
234: args = new Object[] { request, response,
235: DispatchingServlet.this , path };
236: break;
237: default:
238: throw new HammurapiWebException(
239: "Invalid number of arguments in method "
240: + method);
241: }
242: }
243: return method.invoke(instance, args);
244: } catch (IllegalAccessException e) {
245: logger.error(
246: "Exception is action " + method.toString(), e);
247: throw new HammurapiWebException(e);
248: } catch (InvocationTargetException e) {
249: logger.error(
250: "Exception is action " + method.toString(), e);
251: if (e.getCause() != null) {
252: if ("message".equals(exceptionHandling)) {
253: return e.getCause().getMessage();
254: } else if ("to-string".equals(exceptionHandling)) {
255: return e.getCause().toString();
256: } else if ("stack-trace".equals(exceptionHandling)) {
257: StringWriter sw = new StringWriter();
258: PrintWriter pw = new PrintWriter(sw);
259: e.getCause().printStackTrace(pw);
260: pw.close();
261: try {
262: sw.close();
263: } catch (IOException e1) {
264: throw new HammurapiWebException(
265: "Should never ever happen", e1);
266: }
267: return "<pre>" + sw.toString() + "</pre>";
268: } else if ("cause-message"
269: .equals(exceptionHandling)) {
270: return getToTheCause(e).getMessage();
271: } else if ("cause-to-string"
272: .equals(exceptionHandling)) {
273: return getToTheCause(e).toString();
274: } else if ("cause-stack-trace"
275: .equals(exceptionHandling)) {
276: StringWriter sw = new StringWriter();
277: PrintWriter pw = new PrintWriter(sw);
278: getToTheCause(e).printStackTrace(pw);
279: pw.close();
280: try {
281: sw.close();
282: } catch (IOException e1) {
283: throw new HammurapiWebException(
284: "Should never ever happen", e1);
285: }
286: return "<pre>" + sw.toString() + "</pre>";
287: }
288: }
289: throw new HammurapiWebException(e);
290: } finally {
291: atic.addInterval(method.toString(), start);
292: }
293: }
294:
295: /**
296: * Executes action without authorization checks
297: * @param ctx - Context, typically request context
298: * @param path - action path tail
299: * @return
300: * @throws HammurapiWebException
301: */
302: public Object execute(Context ctx, String path)
303: throws HammurapiWebException {
304:
305: long start = atic.getTime();
306: try {
307: Object[] args;
308: if (isContextMethod) {
309: switch (argCount) {
310: case 1:
311: args = new Object[] { ctx };
312: break;
313: case 2:
314: args = new Object[] { ctx,
315: DispatchingServlet.this };
316: break;
317: case 3:
318: args = new Object[] { ctx,
319: DispatchingServlet.this , path };
320: break;
321: default:
322: throw new HammurapiWebException(
323: "Invalid number of arguments in method "
324: + method);
325: }
326: } else {
327: throw new HammurapiWebException(
328: "Cannot execute non-context method: "
329: + method);
330: }
331: return method.invoke(instance, args);
332: } catch (IllegalAccessException e) {
333: logger.error(
334: "Exception is action " + method.toString(), e);
335: throw new HammurapiWebException(e);
336: } catch (InvocationTargetException e) {
337: logger.error(
338: "Exception is action " + method.toString(), e);
339: if (e.getCause() != null) {
340: if ("message".equals(exceptionHandling)) {
341: return e.getCause().getMessage();
342: } else if ("to-string".equals(exceptionHandling)) {
343: return e.getCause().toString();
344: } else if ("stack-trace".equals(exceptionHandling)) {
345: StringWriter sw = new StringWriter();
346: PrintWriter pw = new PrintWriter(sw);
347: e.getCause().printStackTrace(pw);
348: pw.close();
349: try {
350: sw.close();
351: } catch (IOException e1) {
352: throw new HammurapiWebException(
353: "Should never ever happen", e1);
354: }
355: return "<pre>" + sw.toString() + "</pre>";
356: } else if ("cause-message"
357: .equals(exceptionHandling)) {
358: return getToTheCause(e).getMessage();
359: } else if ("cause-to-string"
360: .equals(exceptionHandling)) {
361: return getToTheCause(e).toString();
362: } else if ("cause-stack-trace"
363: .equals(exceptionHandling)) {
364: StringWriter sw = new StringWriter();
365: PrintWriter pw = new PrintWriter(sw);
366: getToTheCause(e).printStackTrace(pw);
367: pw.close();
368: try {
369: sw.close();
370: } catch (IOException e1) {
371: throw new HammurapiWebException(
372: "Should never ever happen", e1);
373: }
374: return "<pre>" + sw.toString() + "</pre>";
375: }
376: }
377: throw new HammurapiWebException(e);
378: } finally {
379: atic.addInterval(method.toString(), start);
380: }
381: }
382: }
383:
384: private Throwable getToTheCause(Throwable th) {
385: while (th.getCause() != null && th.getCause() != th) {
386: th = th.getCause();
387: }
388:
389: return th;
390: }
391:
392: /**
393: * Subclasses can override this method and throw an exception of methods
394: * which are not supposed to be dispatched to.
395: * @param method
396: * @throws HammurapiWebException
397: */
398: protected void verifyMethod(Method method)
399: throws HammurapiWebException {
400:
401: }
402:
403: private synchronized Action getAction(String path)
404: throws HammurapiWebException {
405: Action ret = (Action) actions.get(path);
406: if (ret == null) {
407: Object instance = getActionInstance(path);
408: if (instance == null) {
409: throw new HammurapiWebException(
410: "Action instance not found for path " + path);
411: }
412: String actionName = getActionName(path);
413:
414: Method candidate = null;
415:
416: Method[] methods = instance.getClass().getMethods();
417: for (int i = 0; i < methods.length; ++i) {
418: Method method = methods[i];
419: Class[] parameterTypes = method.getParameterTypes();
420: if (parameterTypes.length == 4
421: && method.getName().equals(actionName)
422: && HttpServletRequest.class
423: .equals(parameterTypes[0])
424: && HttpServletResponse.class
425: .equals(parameterTypes[1])
426: && parameterTypes[2].isInstance(this )
427: && String.class.equals(parameterTypes[3])) {
428: candidate = method;
429: break;
430: }
431: }
432:
433: if (candidate == null) {
434: for (int i = 0; i < methods.length; ++i) {
435: Method method = methods[i];
436: Class[] parameterTypes = method.getParameterTypes();
437: if (parameterTypes.length == 3
438: && method.getName().equals(actionName)
439: && HttpServletRequest.class
440: .equals(parameterTypes[0])
441: && HttpServletResponse.class
442: .equals(parameterTypes[1])
443: && parameterTypes[2].isInstance(this )) {
444: candidate = method;
445: break;
446: }
447: }
448: }
449:
450: if (candidate == null) {
451: for (int i = 0; i < methods.length; ++i) {
452: Method method = methods[i];
453: Class[] parameterTypes = method.getParameterTypes();
454: if (parameterTypes.length == 2
455: && method.getName().equals(actionName)
456: && HttpServletRequest.class
457: .equals(parameterTypes[0])
458: && HttpServletResponse.class
459: .equals(parameterTypes[1])) {
460: candidate = method;
461: break;
462: }
463: }
464: }
465:
466: // Methods which take context instead or request and response
467: for (int i = 0; i < methods.length; ++i) {
468: Method method = methods[i];
469: Class[] parameterTypes = method.getParameterTypes();
470: if (parameterTypes.length == 3
471: && method.getName().equals(actionName)
472: && Context.class.equals(parameterTypes[0])
473: && parameterTypes[2].isInstance(this )
474: && String.class.equals(parameterTypes[3])) {
475: candidate = method;
476: break;
477: }
478: }
479:
480: if (candidate == null) {
481: for (int i = 0; i < methods.length; ++i) {
482: Method method = methods[i];
483: Class[] parameterTypes = method.getParameterTypes();
484: if (parameterTypes.length == 2
485: && method.getName().equals(actionName)
486: && Context.class.equals(parameterTypes[0])
487: && parameterTypes[2].isInstance(this )) {
488: candidate = method;
489: break;
490: }
491: }
492: }
493:
494: if (candidate == null) {
495: for (int i = 0; i < methods.length; ++i) {
496: Method method = methods[i];
497: Class[] parameterTypes = method.getParameterTypes();
498: if (parameterTypes.length == 1
499: && method.getName().equals(actionName)
500: && Context.class.equals(parameterTypes[0])) {
501: candidate = method;
502: break;
503: }
504: }
505: }
506:
507: if (candidate == null) {
508: throw new HammurapiWebException("Action method "
509: + actionName + " not found for path " + path
510: + " in class " + instance.getClass().getName());
511: }
512:
513: ret = new Action(candidate, instance);
514: actions.put(path, ret);
515: }
516:
517: return ret;
518: }
519:
520: /** Handles the HTTP <code>GET</code> method.
521: * @param request servlet request
522: * @param response servlet response
523: */
524: protected void doGet(HttpServletRequest request,
525: HttpServletResponse response) throws ServletException,
526: java.io.IOException {
527: processRequest(request, response);
528: }
529:
530: /** Handles the HTTP <code>POST</code> method.
531: * @param request servlet request
532: * @param response servlet response
533: */
534: protected void doPost(HttpServletRequest request,
535: HttpServletResponse response) throws ServletException,
536: java.io.IOException {
537: processRequest(request, response);
538: }
539:
540: protected void processRequest(HttpServletRequest request,
541: HttpServletResponse response) throws ServletException,
542: IOException {
543: long start = tic.getTime();
544: String pathInfo = request.getPathInfo();
545: logger.debug("Serving " + pathInfo);
546: try {
547: if (pathInfo == null || "/".equals(pathInfo)) {
548: response.sendError(404, "Invalid action path");
549: return;
550: }
551:
552: int styleIdx = pathInfo.indexOf("/",
553: styleSeparatorPosition());
554: if (styleIdx == -1) {
555: response.sendError(404, "Invalid action path");
556: return;
557: }
558:
559: styleIdx = pathInfo.indexOf("/", styleIdx + 1);
560:
561: Action action = getAction(styleIdx == -1 ? pathInfo
562: .substring(1) : pathInfo.substring(1, styleIdx));
563: String styleName = styleIdx == -1 ? null : pathInfo
564: .substring(styleIdx + 1);
565:
566: Object ret = action.execute(request, response, styleName);
567: if (ret == null) {
568: return;
569: }
570:
571: if (ret instanceof Forward) {
572: getServletContext().getRequestDispatcher(
573: ((Forward) ret).getUri()).forward(request,
574: response);
575: return;
576: }
577:
578: if (ret instanceof Include) {
579: getServletContext().getRequestDispatcher(
580: ((Include) ret).getUri()).include(request,
581: response);
582: return;
583: }
584:
585: if (ret instanceof Redirect) {
586: Redirect redirect = (Redirect) ret;
587: response.sendRedirect(redirect.getLocation());
588: PrintWriter out = response.getWriter();
589: out.write("<HTML><HEAD></HEAD><BODY>");
590: out.write(redirect.getMessage() == null ? "Redirecting"
591: : redirect.getMessage());
592: out.write("</BODY></HTML>");
593: return;
594: }
595:
596: if (ret instanceof HttpError) {
597: HttpError error = (HttpError) ret;
598: if (error.getMessage() == null) {
599: response.sendError(error.getErrorCode());
600: } else {
601: response.sendError(error.getErrorCode(), error
602: .getMessage());
603: }
604: return;
605: }
606:
607: if (ret instanceof String) {
608: Writer w = new OutputStreamWriter(response
609: .getOutputStream());
610: w.write((String) ret);
611: w.close();
612: return;
613: }
614:
615: style(ret, styleName, request, response);
616: } catch (HammurapiWebException e) {
617: logger.error("Exception serving '" + pathInfo + "': " + e,
618: e);
619: throw new ServletException(e);
620: } catch (ParserConfigurationException e) {
621: throw new ServletException(e);
622: } finally {
623: tic.addInterval(pathInfo, start);
624: }
625: }
626:
627: public String executeAction(String pathInfo, Context ctx)
628: throws ServletException, IOException {
629: long start = tic.getTime();
630: logger.debug("Serving " + pathInfo);
631: try {
632: if (pathInfo == null || "/".equals(pathInfo)) {
633: return "Invalid action path";
634: }
635:
636: int styleIdx = pathInfo.indexOf("/",
637: styleSeparatorPosition());
638: if (styleIdx == -1) {
639: return "Invalid action path";
640: }
641:
642: styleIdx = pathInfo.indexOf("/", styleIdx + 1);
643:
644: Action action = getAction(styleIdx == -1 ? pathInfo
645: : pathInfo.substring(0, styleIdx));
646: String styleName = styleIdx == -1 ? null : pathInfo
647: .substring(styleIdx + 1);
648:
649: Object ret = action.execute(ctx, styleName);
650: if (ret == null) {
651: return null;
652: }
653:
654: if (ret instanceof Forward) {
655: return "Forward: " + ((Forward) ret).getUri();
656: }
657:
658: if (ret instanceof Include) {
659: return "Include: " + ((Include) ret).getUri();
660: }
661:
662: if (ret instanceof Redirect) {
663: Redirect redirect = (Redirect) ret;
664: return "Redirect to " + redirect.getLocation()
665: + " with message " + redirect.getMessage();
666: }
667:
668: if (ret instanceof HttpError) {
669: HttpError error = (HttpError) ret;
670: if (error.getMessage() == null) {
671: return "Error " + error.getErrorCode();
672: }
673:
674: return "Error " + error.getErrorCode() + ": "
675: + error.getMessage();
676: }
677:
678: if (ret instanceof String) {
679: return (String) ret;
680: }
681:
682: StringWriter sw = new StringWriter();
683: style(ret, styleName, ctx, new StreamResult(sw));
684: sw.close();
685: return sw.toString();
686: } catch (HammurapiWebException e) {
687: logger.error("Exception serving '" + pathInfo + "': " + e,
688: e);
689: throw new ServletException(e);
690: } catch (ParserConfigurationException e) {
691: throw new ServletException(e);
692: } finally {
693: tic.addInterval(pathInfo, start);
694: }
695: }
696:
697: /**
698: * Converts object returned from action to XML and applies style
699: * @param ret Object to be styled
700: * @param styleName Style name
701: * @param request request
702: * @param response response
703: * @throws ParserConfigurationException
704: * @throws FactoryConfigurationError
705: * @throws ServletException
706: */
707: public void style(Object ret, String styleName,
708: HttpServletRequest request, HttpServletResponse response)
709: throws ParserConfigurationException,
710: FactoryConfigurationError, ServletException {
711: if (ret instanceof Document) {
712: getTransformer(styleName).transform((Node) ret,
713: getSetParametersCallback(request, styleName),
714: response);
715: } else {
716: Document doc = newDocumentBuilder().newDocument();
717: Element re = doc.createElement("response");
718: re.setAttribute("context-path", request.getContextPath());
719: doc.appendChild(re);
720: getDomSerializer().toDomSerializable(ret).toDom(re);
721: if (permissionsElement != null) {
722: Object ap = request
723: .getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
724: if (ap == null) {
725: HttpSession session = request.getSession(false);
726: ap = session == null ? null
727: : session
728: .getAttribute(AUTHORIZATION_PROVIDER_ATTRIBUTE);
729: }
730:
731: if (ap instanceof AuthorizationProvider) {
732: getDomSerializer().toDomSerializable(
733: ((AuthorizationProvider) ap)
734: .getPermissions()).toDom(
735: AbstractDomObject.addElement(re,
736: permissionsElement));
737: } else if (ap != null) {
738: logger
739: .warn("Configuration problem with authorization provider: "
740: + ap.getClass().getName()
741: + " does not implement "
742: + AuthorizationProvider.class
743: .getName());
744: }
745: }
746: getTransformer(styleName).transform(doc,
747: getSetParametersCallback(request, styleName),
748: response);
749: }
750: }
751:
752: /**
753: * Converts object returned from action to XML and applies style
754: * @param ret Object to be styled
755: * @param styleName Style name
756: * @param request request
757: * @param response response
758: * @throws ParserConfigurationException
759: * @throws FactoryConfigurationError
760: * @throws ServletException
761: */
762: public void style(Object ret, String styleName, Context context,
763: Result result) throws ParserConfigurationException,
764: HammurapiWebException {
765: Document doc;
766: if (ret instanceof Document) {
767: doc = (Document) ret;
768: } else {
769: doc = newDocumentBuilder().newDocument();
770: Element re = doc.createElement("response");
771: String contextPath = (String) context.get("context-path");
772: if (contextPath != null) {
773: re.setAttribute("context-path", contextPath);
774: }
775: doc.appendChild(re);
776: getDomSerializer().toDomSerializable(ret).toDom(re);
777: }
778:
779: getTransformer(styleName).transform(doc, result);
780: }
781: }
|