001: /*
002: * Copyright 2005 Joe Walker
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.directwebremoting.impl;
017:
018: import java.io.IOException;
019: import java.io.InputStream;
020: import java.lang.reflect.Method;
021: import java.util.Arrays;
022: import java.util.HashMap;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.StringTokenizer;
026:
027: import javax.servlet.ServletContext;
028: import javax.xml.parsers.DocumentBuilder;
029: import javax.xml.parsers.DocumentBuilderFactory;
030: import javax.xml.parsers.ParserConfigurationException;
031:
032: import org.apache.commons.logging.LogFactory;
033: import org.apache.commons.logging.Log;
034: import org.directwebremoting.AjaxFilter;
035: import org.directwebremoting.Container;
036: import org.directwebremoting.WebContextFactory;
037: import org.directwebremoting.extend.AccessControl;
038: import org.directwebremoting.extend.AjaxFilterManager;
039: import org.directwebremoting.extend.Configurator;
040: import org.directwebremoting.extend.ConverterManager;
041: import org.directwebremoting.extend.Creator;
042: import org.directwebremoting.extend.CreatorManager;
043: import org.directwebremoting.extend.TypeHintContext;
044: import org.directwebremoting.util.LocalUtil;
045: import org.directwebremoting.util.LogErrorHandler;
046: import org.directwebremoting.util.Messages;
047: import org.w3c.dom.Document;
048: import org.w3c.dom.Element;
049: import org.w3c.dom.NamedNodeMap;
050: import org.w3c.dom.Node;
051: import org.w3c.dom.NodeList;
052: import org.xml.sax.SAXException;
053:
054: /**
055: * A configurator that gets its configuration by reading a dwr.xml file.
056: * @author Joe Walker [joe at getahead dot ltd dot uk]
057: */
058: public class DwrXmlConfigurator implements Configurator {
059: /**
060: * Setter for the resource name that we can use to read a file from the
061: * servlet context
062: * @param servletResourceName The name to lookup
063: * @throws IOException On file read failure
064: * @throws ParserConfigurationException On XML setup failure
065: * @throws SAXException On XML parse failure
066: */
067: public void setServletResourceName(String servletResourceName)
068: throws IOException, ParserConfigurationException,
069: SAXException {
070: this .servletResourceName = servletResourceName;
071:
072: ServletContext servletContext = WebContextFactory.get()
073: .getServletContext();
074: if (servletContext == null) {
075: throw new IOException(
076: Messages
077: .getString("DwrXmlConfigurator.MissingServletContext"));
078: }
079:
080: InputStream in = null;
081: try {
082: in = servletContext
083: .getResourceAsStream(servletResourceName);
084: if (in == null) {
085: throw new IOException(Messages.getString(
086: "DwrXmlConfigurator.MissingConfigFile",
087: servletResourceName));
088: }
089:
090: log.debug("Configuring from servlet resource: "
091: + servletResourceName);
092: setInputStream(in);
093: } finally {
094: LocalUtil.close(in);
095: }
096: }
097:
098: /**
099: * Setter for a classpath based lookup
100: * @param classResourceName The resource to lookup in the classpath
101: * @throws IOException On file read failure
102: * @throws ParserConfigurationException On XML setup failure
103: * @throws SAXException On XML parse failure
104: */
105: public void setClassResourceName(String classResourceName)
106: throws IOException, ParserConfigurationException,
107: SAXException {
108: this .classResourceName = classResourceName;
109:
110: InputStream in = getClass().getResourceAsStream(
111: classResourceName);
112: if (in == null) {
113: throw new IOException(Messages.getString(
114: "DwrXmlConfigurator.MissingConfigFile",
115: classResourceName));
116: }
117:
118: log.debug("Configuring from class resource: "
119: + classResourceName);
120: setInputStream(in);
121: }
122:
123: /**
124: * Setter for a direct input stream to configure from
125: * @param in The input stream to read from.
126: * @throws IOException On file read failure
127: * @throws ParserConfigurationException On XML setup failure
128: * @throws SAXException On XML parse failure
129: */
130: public void setInputStream(InputStream in)
131: throws ParserConfigurationException, SAXException,
132: IOException {
133: DocumentBuilderFactory dbf = DocumentBuilderFactory
134: .newInstance();
135: dbf.setValidating(true);
136:
137: DocumentBuilder db = dbf.newDocumentBuilder();
138: db.setEntityResolver(new DTDEntityResolver());
139: db.setErrorHandler(new LogErrorHandler());
140:
141: document = db.parse(in);
142: }
143:
144: /**
145: * To set the configuration document directly
146: * @param document The new configuration document
147: */
148: public void setDocument(Document document) {
149: this .document = document;
150: }
151:
152: /* (non-Javadoc)
153: * @see org.directwebremoting.Configurator#configure(org.directwebremoting.Container)
154: */
155: public void configure(Container container) {
156: accessControl = container.getBean(AccessControl.class);
157: ajaxFilterManager = container.getBean(AjaxFilterManager.class);
158: converterManager = container.getBean(ConverterManager.class);
159: creatorManager = container.getBean(CreatorManager.class);
160:
161: Element root = document.getDocumentElement();
162:
163: NodeList rootChildren = root.getChildNodes();
164: for (int i = 0; i < rootChildren.getLength(); i++) {
165: Node node = rootChildren.item(i);
166: if (node.getNodeType() == Node.ELEMENT_NODE) {
167: Element child = (Element) node;
168:
169: if (child.getNodeName().equals(ELEMENT_INIT)) {
170: loadInits(child);
171: } else if (child.getNodeName().equals(ELEMENT_ALLOW)) {
172: loadAllows(child);
173: } else if (child.getNodeName().equals(
174: ELEMENT_SIGNATURES)) {
175: loadSignature(child);
176: }
177: }
178: }
179: }
180:
181: /**
182: * Internal method to load the inits element
183: * @param child The element to read
184: */
185: private void loadInits(Element child) {
186: NodeList inits = child.getChildNodes();
187: for (int j = 0; j < inits.getLength(); j++) {
188: if (inits.item(j).getNodeType() == Node.ELEMENT_NODE) {
189: Element initer = (Element) inits.item(j);
190:
191: if (initer.getNodeName().equals(ATTRIBUTE_CREATOR)) {
192: String id = initer.getAttribute(ATTRIBUTE_ID);
193: String className = initer
194: .getAttribute(ATTRIBUTE_CLASS);
195: creatorManager.addCreatorType(id, className);
196: } else if (initer.getNodeName().equals(
197: ATTRIBUTE_CONVERTER)) {
198: String id = initer.getAttribute(ATTRIBUTE_ID);
199: String className = initer
200: .getAttribute(ATTRIBUTE_CLASS);
201: converterManager.addConverterType(id, className);
202: }
203: }
204: }
205: }
206:
207: /**
208: * Internal method to load the create/convert elements
209: * @param child The element to read
210: */
211: private void loadAllows(Element child) {
212: NodeList allows = child.getChildNodes();
213: for (int j = 0; j < allows.getLength(); j++) {
214: if (allows.item(j).getNodeType() == Node.ELEMENT_NODE) {
215: Element allower = (Element) allows.item(j);
216:
217: if (allower.getNodeName().equals(ELEMENT_CREATE)) {
218: loadCreate(allower);
219: } else if (allower.getNodeName()
220: .equals(ELEMENT_CONVERT)) {
221: loadConvert(allower);
222: } else if (allower.getNodeName().equals(ELEMENT_FILTER)) {
223: loadFilter(allower);
224: }
225: }
226: }
227: }
228:
229: /**
230: * Internal method to load the convert element
231: * @param allower The element to read
232: */
233: private void loadConvert(Element allower) {
234: String match = allower.getAttribute(ATTRIBUTE_MATCH);
235: String type = allower.getAttribute(ATTRIBUTE_CONVERTER);
236:
237: try {
238: Map<String, String> params = createSettingMap(allower);
239: converterManager.addConverter(match, type, params);
240: } catch (NoClassDefFoundError ex) {
241: log
242: .info("Convertor '"
243: + type
244: + "' not loaded due to NoClassDefFoundError. (match='"
245: + match + "'). Cause: " + ex.getMessage());
246: } catch (Exception ex) {
247: log.error("Failed to add convertor: match=" + match
248: + ", type=" + type, ex);
249: }
250: }
251:
252: /**
253: * Internal method to load the create element
254: * @param allower The element to read
255: */
256: private void loadCreate(Element allower) {
257: String type = allower.getAttribute(ATTRIBUTE_CREATOR);
258: String javascript = allower.getAttribute(ATTRIBUTE_JAVASCRIPT);
259:
260: try {
261: Map<String, String> params = createSettingMap(allower);
262: creatorManager.addCreator(javascript, type, params);
263:
264: processPermissions(javascript, allower);
265: processAuth(javascript, allower);
266: processParameters(javascript, allower);
267: processAjaxFilters(javascript, allower);
268: } catch (NoClassDefFoundError ex) {
269: log
270: .info("Creator '"
271: + type
272: + "' not loaded due to NoClassDefFoundError. (javascript='"
273: + javascript + "'). Cause: "
274: + ex.getMessage());
275: } catch (Exception ex) {
276: log.error("Failed to add creator: type=" + type
277: + ", javascript=" + javascript, ex);
278: }
279: }
280:
281: /**
282: * Internal method to load the convert element
283: * @param allower The element to read
284: */
285: private void loadFilter(Element allower) {
286: String type = allower.getAttribute(ATTRIBUTE_CLASS);
287:
288: try {
289: Class<?> impl = LocalUtil.classForName(type);
290: AjaxFilter object = (AjaxFilter) impl.newInstance();
291:
292: LocalUtil.setParams(object, createSettingMap(allower),
293: ignore);
294:
295: ajaxFilterManager.addAjaxFilter(object);
296: } catch (ClassCastException ex) {
297: log.error(type + " does not implement "
298: + AjaxFilter.class.getName(), ex);
299: } catch (NoClassDefFoundError ex) {
300: log.info("Missing class for filter (class='" + type
301: + "'). Cause: " + ex.getMessage());
302: } catch (Exception ex) {
303: log.error("Failed to add filter: class=" + type, ex);
304: }
305: }
306:
307: /**
308: * Create a parameter map from nested <param name="foo" value="blah"/>
309: * elements
310: * @param parent The parent element
311: * @return A map of parameters
312: */
313: private static Map<String, String> createSettingMap(Element parent) {
314: Map<String, String> params = new HashMap<String, String>();
315:
316: // Go through the attributes in the allower element, adding to the param map
317: NamedNodeMap attrs = parent.getAttributes();
318: for (int i = 0; i < attrs.getLength(); i++) {
319: Node node = attrs.item(i);
320: String name = node.getNodeName();
321: String value = node.getNodeValue();
322: params.put(name, value);
323: }
324:
325: // Go through the param elements in the allower element, adding to the param map
326: NodeList locNodes = parent.getElementsByTagName(ELEMENT_PARAM);
327: for (int i = 0; i < locNodes.getLength(); i++) {
328: // Since this comes from getElementsByTagName we can assume that
329: // all the nodes are elements.
330: Element element = (Element) locNodes.item(i);
331:
332: // But getElementsByTagName(ELEMENT_PARAM) includes param nodes that
333: // are nested down inside filters, so we need to check that the
334: // parent node is 'parent'. $&*?! DOM!
335: if (element.getParentNode() != parent) {
336: continue;
337: }
338:
339: String name = element.getAttribute(ATTRIBUTE_NAME);
340: if (name != null) {
341: String value = element.getAttribute(ATTRIBUTE_VALUE);
342: if (value == null || value.length() == 0) {
343: StringBuffer buffer = new StringBuffer();
344: NodeList textNodes = element.getChildNodes();
345:
346: for (int j = 0; j < textNodes.getLength(); j++) {
347: buffer.append(textNodes.item(j).getNodeValue());
348: }
349:
350: value = buffer.toString();
351: }
352:
353: params.put(name, value);
354: }
355: }
356:
357: return params;
358: }
359:
360: /**
361: * Process the include and exclude elements, passing them on to the creator
362: * manager.
363: * @param javascript The name of the creator
364: * @param parent The container of the include and exclude elements.
365: */
366: private void processPermissions(String javascript, Element parent) {
367: NodeList incNodes = parent
368: .getElementsByTagName(ELEMENT_INCLUDE);
369: for (int i = 0; i < incNodes.getLength(); i++) {
370: Element include = (Element) incNodes.item(i);
371: String method = include.getAttribute(ATTRIBUTE_METHOD);
372: accessControl.addIncludeRule(javascript, method);
373: }
374:
375: NodeList excNodes = parent
376: .getElementsByTagName(ELEMENT_EXCLUDE);
377: for (int i = 0; i < excNodes.getLength(); i++) {
378: Element include = (Element) excNodes.item(i);
379: String method = include.getAttribute(ATTRIBUTE_METHOD);
380: accessControl.addExcludeRule(javascript, method);
381: }
382: }
383:
384: /**
385: * J2EE role based method level security added here.
386: * @param javascript The name of the creator
387: * @param parent The container of the include and exclude elements.
388: */
389: private void processAuth(String javascript, Element parent) {
390: NodeList nodes = parent.getElementsByTagName(ELEMENT_AUTH);
391: for (int i = 0; i < nodes.getLength(); i++) {
392: Element include = (Element) nodes.item(i);
393:
394: String method = include.getAttribute(ATTRIBUTE_METHOD);
395: String role = include.getAttribute(ATTRIBUTE_ROLE);
396:
397: accessControl.addRoleRestriction(javascript, method, role);
398: }
399: }
400:
401: /**
402: * J2EE role based method level security added here.
403: * @param javascript The name of the creator
404: * @param parent The container of the include and exclude elements.
405: */
406: private void processAjaxFilters(String javascript, Element parent) {
407: NodeList nodes = parent.getElementsByTagName(ELEMENT_FILTER);
408: for (int i = 0; i < nodes.getLength(); i++) {
409: Element include = (Element) nodes.item(i);
410:
411: String type = include.getAttribute(ATTRIBUTE_CLASS);
412: AjaxFilter filter = LocalUtil.classNewInstance(javascript,
413: type, AjaxFilter.class);
414: if (filter != null) {
415: LocalUtil.setParams(filter, createSettingMap(include),
416: ignore);
417: ajaxFilterManager.addAjaxFilter(filter, javascript);
418: }
419: }
420: }
421:
422: /**
423: * Parse and extra type info from method signatures
424: * @param element The element to read
425: */
426: private void loadSignature(Element element) {
427: StringBuffer sigtext = new StringBuffer();
428:
429: // This coagulates text nodes, not sure if we need to do this?
430: element.normalize();
431:
432: NodeList nodes = element.getChildNodes();
433: for (int i = 0; i < nodes.getLength(); i++) {
434: Node node = nodes.item(i);
435: short type = node.getNodeType();
436: if (type != Node.TEXT_NODE
437: && type != Node.CDATA_SECTION_NODE) {
438: log.warn("Ignoring illegal node type: " + type);
439: continue;
440: }
441:
442: sigtext.append(node.getNodeValue());
443: }
444:
445: SignatureParser sigp = new SignatureParser(converterManager,
446: creatorManager);
447: sigp.parse(sigtext.toString());
448: }
449:
450: /**
451: * Collections often have missing information. This helps fill the missing
452: * data in.
453: * @param javascript The name of the creator
454: * @param parent The container of the include and exclude elements.
455: * @throws ClassNotFoundException If the type attribute can't be converted into a Class
456: */
457: private void processParameters(String javascript, Element parent)
458: throws ClassNotFoundException {
459: NodeList nodes = parent.getElementsByTagName(ELEMENT_PARAMETER);
460: for (int i = 0; i < nodes.getLength(); i++) {
461: Element include = (Element) nodes.item(i);
462:
463: String methodName = include.getAttribute(ATTRIBUTE_METHOD);
464:
465: // Try to find the method that we are annotating
466: Creator creator = creatorManager.getCreator(javascript);
467: Class<?> dest = creator.getType();
468:
469: Method method = null;
470: for (Method test : dest.getMethods()) {
471: if (test.getName().equals(methodName)) {
472: if (method == null) {
473: method = test;
474: } else {
475: log
476: .warn("Setting extra type info to overloaded methods may fail with <parameter .../>");
477: }
478: }
479: }
480:
481: if (method == null) {
482: log.error("Unable to find method called: " + methodName
483: + " on type: " + dest.getName()
484: + " from creator: " + javascript);
485: continue;
486: }
487:
488: String number = include.getAttribute(ATTRIBUTE_NUMBER);
489: int paramNo = Integer.parseInt(number);
490:
491: String types = include.getAttribute(ATTRIBUTE_TYPE);
492: StringTokenizer st = new StringTokenizer(types, ",");
493:
494: int j = 0;
495: while (st.hasMoreTokens()) {
496: String type = st.nextToken();
497: Class<?> clazz = LocalUtil.classForName(type.trim());
498: TypeHintContext thc = new TypeHintContext(
499: converterManager, method, paramNo)
500: .createChildContext(j);
501: j++;
502: converterManager.setExtraTypeInfo(thc, clazz);
503: }
504: }
505: }
506:
507: /* (non-Javadoc)
508: * @see java.lang.Object#toString()
509: */
510: @Override
511: public String toString() {
512: if (servletResourceName != null) {
513: return "DwrXmlConfigurator[ServletResource:"
514: + servletResourceName + "]";
515: } else {
516: return "DwrXmlConfigurator[ClassResource:"
517: + classResourceName + "]";
518: }
519: }
520:
521: /**
522: * The parsed document
523: */
524: private Document document;
525:
526: /**
527: * The properties that we don't warn about if they don't exist.
528: */
529: private static List<String> ignore = Arrays.asList("class");
530:
531: /**
532: * The log stream
533: */
534: public static final Log log = LogFactory
535: .getLog(DwrXmlConfigurator.class);
536:
537: /**
538: * What AjaxFilters apply to which Ajax calls?
539: */
540: private AjaxFilterManager ajaxFilterManager = null;
541:
542: /**
543: * The converter manager that decides how parameters are converted
544: */
545: private ConverterManager converterManager = null;
546:
547: /**
548: * The DefaultCreatorManager to which we delegate creation of new objects.
549: */
550: private CreatorManager creatorManager = null;
551:
552: /**
553: * The security manager
554: */
555: private AccessControl accessControl = null;
556:
557: /**
558: * For debug purposes, the classResourceName that we were configured with.
559: * Either this or {@link #servletResourceName} will be null
560: */
561: private String classResourceName;
562:
563: /**
564: * For debug purposes, the servletResourceName that we were configured with
565: * Either this or {@link #classResourceName} will be null
566: */
567: private String servletResourceName;
568:
569: /*
570: * The element names
571: */
572: private static final String ELEMENT_INIT = "init";
573:
574: private static final String ELEMENT_ALLOW = "allow";
575:
576: private static final String ELEMENT_CREATE = "create";
577:
578: private static final String ELEMENT_CONVERT = "convert";
579:
580: private static final String ELEMENT_PARAM = "param";
581:
582: private static final String ELEMENT_INCLUDE = "include";
583:
584: private static final String ELEMENT_EXCLUDE = "exclude";
585:
586: private static final String ELEMENT_PARAMETER = "parameter";
587:
588: private static final String ELEMENT_AUTH = "auth";
589:
590: private static final String ELEMENT_SIGNATURES = "signatures";
591:
592: private static final String ELEMENT_FILTER = "filter";
593:
594: /*
595: * The attribute names
596: */
597: private static final String ATTRIBUTE_ID = "id";
598:
599: private static final String ATTRIBUTE_CLASS = "class";
600:
601: private static final String ATTRIBUTE_CONVERTER = "converter";
602:
603: private static final String ATTRIBUTE_MATCH = "match";
604:
605: private static final String ATTRIBUTE_JAVASCRIPT = "javascript";
606:
607: private static final String ATTRIBUTE_CREATOR = "creator";
608:
609: private static final String ATTRIBUTE_NAME = "name";
610:
611: private static final String ATTRIBUTE_VALUE = "value";
612:
613: private static final String ATTRIBUTE_METHOD = "method";
614:
615: private static final String ATTRIBUTE_ROLE = "role";
616:
617: private static final String ATTRIBUTE_NUMBER = "number";
618:
619: private static final String ATTRIBUTE_TYPE = "type";
620: }
|