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.lang.reflect.InvocationTargetException;
019: import java.lang.reflect.Method;
020: import java.lang.reflect.Modifier;
021: import java.util.Collections;
022: import java.util.HashMap;
023: import java.util.Iterator;
024: import java.util.Map;
025: import java.util.Map.Entry;
026:
027: import javax.servlet.http.HttpServletRequest;
028:
029: import org.apache.commons.logging.Log;
030: import org.apache.commons.logging.LogFactory;
031: import org.directwebremoting.AjaxFilter;
032: import org.directwebremoting.AjaxFilterChain;
033: import org.directwebremoting.WebContext;
034: import org.directwebremoting.WebContextFactory;
035: import org.directwebremoting.extend.AccessControl;
036: import org.directwebremoting.extend.AjaxFilterManager;
037: import org.directwebremoting.extend.Call;
038: import org.directwebremoting.extend.Calls;
039: import org.directwebremoting.extend.Converter;
040: import org.directwebremoting.extend.ConverterManager;
041: import org.directwebremoting.extend.Creator;
042: import org.directwebremoting.extend.CreatorManager;
043: import org.directwebremoting.extend.EnginePrivate;
044: import org.directwebremoting.extend.NamedConverter;
045: import org.directwebremoting.extend.Property;
046: import org.directwebremoting.extend.Remoter;
047: import org.directwebremoting.extend.Replies;
048: import org.directwebremoting.extend.Reply;
049: import org.directwebremoting.util.Continuation;
050: import org.directwebremoting.util.JavascriptUtil;
051: import org.directwebremoting.util.LocalUtil;
052:
053: /**
054: * In implementation of Remoter that delegates requests to a set of Modules
055: * @author Joe Walker [joe at getahead dot ltd dot uk]
056: * @author Mike Wilson
057: */
058: public class DefaultRemoter implements Remoter {
059: /* (non-Javadoc)
060: * @see org.directwebremoting.Remoter#generateInterfaceScript(java.lang.String, java.lang.String)
061: */
062: public String generateInterfaceScript(String scriptName,
063: String contextServletPath) throws SecurityException {
064: StringBuilder buffer = new StringBuilder();
065:
066: buffer.append(createParameterDefinitions(scriptName));
067: buffer.append(EnginePrivate.getEngineInitScript());
068: buffer.append(createClassDefinition(scriptName));
069: buffer.append(createPathDefinition(scriptName,
070: contextServletPath));
071: buffer.append(createMethodDefinitions(scriptName));
072:
073: return buffer.toString();
074: }
075:
076: /**
077: * Create a class definition string.
078: * This is similar to {@link EnginePrivate#getEngineInitScript()} except
079: * that it creates scripts for a specific class not for dwr.engine
080: * @see EnginePrivate#getEngineInitScript()
081: * @param scriptName
082: */
083: protected String createClassDefinition(String scriptName) {
084: return "if (typeof this['" + scriptName
085: + "'] == 'undefined') this." + scriptName
086: + " = {};\n\n";
087: }
088:
089: /**
090: * Create a _path member to point at DWR
091: * @param scriptName The class that we are creating a member for
092: * @param path The default path to the DWR servlet
093: */
094: protected String createPathDefinition(String scriptName, String path) {
095: return scriptName + "._path = '" + getPathToDwrServlet(path)
096: + "';\n\n";
097: }
098:
099: /* (non-Javadoc)
100: * @see org.directwebremoting.extend.Remoter#getPathToDwrServlet(java.lang.String)
101: */
102: public String getPathToDwrServlet(String contextServletPath) {
103: String actualPath = contextServletPath;
104: if (overridePath != null) {
105: actualPath = overridePath;
106: }
107:
108: if (useAbsolutePath) {
109: HttpServletRequest request = WebContextFactory.get()
110: .getHttpServletRequest();
111:
112: StringBuffer absolutePath = new StringBuffer(48);
113:
114: String scheme = request.getScheme();
115: int port = request.getServerPort();
116:
117: absolutePath.append(scheme);
118: absolutePath.append("://");
119: absolutePath.append(request.getServerName());
120:
121: if (port > 0
122: && ((scheme.equalsIgnoreCase("http") && port != 80) || (scheme
123: .equalsIgnoreCase("https") && port != 443))) {
124: absolutePath.append(':');
125: absolutePath.append(port);
126: }
127:
128: absolutePath.append(request.getContextPath());
129: absolutePath.append(request.getServletPath());
130:
131: actualPath = absolutePath.toString();
132: }
133:
134: return actualPath;
135: }
136:
137: /**
138: * Create a list of method definitions for the given creator.
139: * @param fullCreatorName To allow AccessControl to allow/deny requests
140: */
141: protected String createMethodDefinitions(String fullCreatorName) {
142: Creator creator = creatorManager.getCreator(fullCreatorName);
143: String scriptName = creator.getJavascript();
144:
145: StringBuilder buffer = new StringBuilder();
146:
147: Method[] methods = creator.getType().getMethods();
148: for (Method method : methods) {
149: String methodName = method.getName();
150:
151: // We don't need to check accessControl.getReasonToNotExecute()
152: // because the checks are made by the execute() method, but we do
153: // check if we can display it
154: try {
155: accessControl.assertIsDisplayable(creator, scriptName,
156: method);
157: } catch (SecurityException ex) {
158: if (!allowImpossibleTests) {
159: continue;
160: }
161: }
162:
163: // Is it on the list of banned names
164: if (JavascriptUtil.isReservedWord(methodName)) {
165: continue;
166: }
167:
168: // Check to see if the creator is reloadable
169: // If it is, then do not cache the generated Javascript
170: // See the notes on creator.isCacheable().
171: String script;
172: if (!creator.isCacheable()) {
173: script = getMethodJS(scriptName, method);
174: } else {
175: String key = scriptName + "." + method.getName();
176:
177: // For optimal performance we might use the Memoizer pattern
178: // JCiP#108 however performance isn't a big issue and we are
179: // prepared to cope with getMethodJS() being run more than once.
180: script = methodCache.get(key);
181: if (script == null) {
182: script = getMethodJS(scriptName, method);
183: methodCache.put(key, script);
184: }
185: }
186:
187: buffer.append(script);
188: }
189:
190: return buffer.toString();
191: }
192:
193: /**
194: * Output the class definitions for all the converted objects.
195: * An optimization for this class might be to only generate class
196: * definitions for classes used as parameters in the class that we are
197: * currently generating a proxy for.
198: * <p>Currently the <code>scriptName</code> parameter is not used, we just
199: * generate the class definitions for all types, however conceptually, it
200: * should be used
201: * @param scriptName The script for which we are generating parameter classes
202: */
203: protected String createParameterDefinitions(String scriptName) {
204: StringBuilder buffer = new StringBuilder();
205:
206: for (String match : converterManager.getConverterMatchStrings()) {
207: try {
208: StringBuilder paramBuffer = new StringBuilder();
209:
210: Converter conv = converterManager
211: .getConverterByMatchString(match);
212: // We will only generate JavaScript classes for compound objects/beans
213: if (conv instanceof NamedConverter) {
214: NamedConverter boConv = (NamedConverter) conv;
215: String jsClassName = boConv.getJavascript();
216:
217: // We need a configured JavaScript class name
218: if (jsClassName != null && !"".equals(jsClassName)) {
219: // Wildcard match strings are currently not supported
220: if (!match.contains("*")) {
221: paramBuffer.append('\n');
222:
223: // output: if (typeof this['<class>'] != 'function') { function <class>() {
224: paramBuffer.append("if (typeof this['"
225: + jsClassName
226: + "'] != 'function') {\n");
227: paramBuffer.append(" function "
228: + jsClassName + "() {\n");
229:
230: // output: this.<property> = <init-value>;
231: Class<?> mappedType;
232: try {
233: mappedType = LocalUtil
234: .classForName(match);
235: } catch (ClassNotFoundException ex) {
236: throw new IllegalArgumentException(ex
237: .getMessage());
238: }
239:
240: Map<String, Property> properties = boConv
241: .getPropertyMapFromClass(
242: mappedType, true, true);
243: for (Entry<String, Property> entry : properties
244: .entrySet()) {
245: String name = entry.getKey();
246: Property property = entry.getValue();
247: Class<?> propType = property
248: .getPropertyType();
249:
250: // Property name
251: paramBuffer.append(" this." + name
252: + " = ");
253:
254: // Default property values
255: if (propType.isArray()) {
256: paramBuffer.append("[]");
257: } else if (propType == boolean.class) {
258: paramBuffer.append("false");
259: } else if (propType.isPrimitive()) {
260: paramBuffer.append("0");
261: } else {
262: paramBuffer.append("null");
263: }
264:
265: paramBuffer.append(";\n");
266: }
267:
268: paramBuffer.append(" }\n");
269: paramBuffer.append("}\n");
270: }
271: }
272: }
273:
274: buffer.append(paramBuffer.toString());
275: } catch (Exception ex) {
276: log.warn("Failed to create parameter declaration for "
277: + match, ex);
278: buffer.append("// Missing parameter declaration for "
279: + match + ". See the server logs for details.");
280: }
281: }
282: buffer.append('\n');
283:
284: return buffer.toString();
285: }
286:
287: /**
288: * Generates Javascript for a given Java method
289: * @param scriptName Name of the Javascript file, without ".js" suffix
290: * @param method Target method
291: * @return Javascript implementing the DWR call for the target method
292: */
293: protected String getMethodJS(String scriptName, Method method) {
294: StringBuffer buffer = new StringBuffer();
295:
296: String methodName = method.getName();
297: buffer.append(scriptName + '.' + methodName + " = function(");
298: Class<?>[] paramTypes = method.getParameterTypes();
299: for (int j = 0; j < paramTypes.length; j++) {
300: if (!LocalUtil.isServletClass(paramTypes[j])) {
301: buffer.append("p" + j + ", ");
302: }
303: }
304:
305: buffer.append("callback) {\n");
306:
307: String executeFunctionName = EnginePrivate
308: .getExecuteFunctionName();
309: buffer.append(" return " + executeFunctionName + "("
310: + scriptName + "._path, '" + scriptName + "', '"
311: + methodName + "\', ");
312: for (int j = 0; j < paramTypes.length; j++) {
313: if (LocalUtil.isServletClass(paramTypes[j])) {
314: buffer.append("false, ");
315: } else {
316: buffer.append("p" + j + ", ");
317: }
318: }
319:
320: buffer.append("callback);\n");
321: buffer.append("};\n\n");
322:
323: return buffer.toString();
324: }
325:
326: /* (non-Javadoc)
327: * @see org.directwebremoting.Remoter#execute(org.directwebremoting.Calls)
328: */
329: public Replies execute(Calls calls) {
330: Replies replies = new Replies(calls.getBatchId());
331:
332: int callCount = calls.getCallCount();
333: if (callCount > maxCallCount) {
334: log
335: .error("Call count for batch exceeds maxCallCount. Add an init-param of maxCallCount to increase this limit");
336: throw new SecurityException(
337: "Call count for batch is too high");
338: }
339:
340: for (Call call : calls) {
341: Reply reply = execute(call);
342: replies.addReply(reply);
343: }
344:
345: return replies;
346: }
347:
348: /**
349: * Execute a single call object
350: * @param call The call to execute
351: * @return A Reply to the Call
352: */
353: public Reply execute(Call call) {
354: try {
355: Method method = call.getMethod();
356: if (method == null || call.getException() != null) {
357: return new Reply(call.getCallId(), null, call
358: .getException());
359: }
360:
361: // Get a list of the available matching methods with the coerced
362: // parameters that we will use to call it if we choose to use that
363: // method.
364: Creator creator = creatorManager.getCreator(call
365: .getScriptName());
366:
367: // We don't need to check accessControl.getReasonToNotExecute()
368: // because the checks are made by the doExec method, but we do check
369: // if we can display it
370: accessControl.assertExecutionIsPossible(creator, call
371: .getScriptName(), method);
372:
373: // Get ourselves an object to execute a method on unless the
374: // method is static
375: Object object = null;
376: String scope = creator.getScope();
377: boolean create = false;
378:
379: if (!Modifier.isStatic(method.getModifiers())) {
380: WebContext webcx = WebContextFactory.get();
381:
382: // Check the various scopes to see if it is there
383: if (scope.equals(Creator.APPLICATION)) {
384: object = webcx.getServletContext().getAttribute(
385: call.getScriptName());
386: } else if (scope.equals(Creator.SESSION)) {
387: object = webcx.getSession().getAttribute(
388: call.getScriptName());
389: } else if (scope.equals(Creator.SCRIPT)) {
390: object = webcx.getScriptSession().getAttribute(
391: call.getScriptName());
392: } else if (scope.equals(Creator.REQUEST)) {
393: object = webcx.getHttpServletRequest()
394: .getAttribute(call.getScriptName());
395: }
396: // Creator.PAGE scope means we create one every time anyway
397:
398: // If we don't have an object the call the creator
399: if (object == null) {
400: create = true;
401: object = creator.getInstance();
402: }
403:
404: // Remember it for next time
405: if (create) {
406: if (scope.equals(Creator.APPLICATION)) {
407: // This might also be done at application startup by
408: // DefaultCreatorManager.addCreator(String, Creator)
409: webcx.getServletContext().setAttribute(
410: call.getScriptName(), object);
411: } else if (scope.equals(Creator.SESSION)) {
412: webcx.getSession().setAttribute(
413: call.getScriptName(), object);
414: } else if (scope.equals(Creator.SCRIPT)) {
415: webcx.getScriptSession().setAttribute(
416: call.getScriptName(), object);
417: } else if (scope.equals(Creator.REQUEST)) {
418: webcx.getHttpServletRequest().setAttribute(
419: call.getScriptName(), object);
420: }
421: // Creator.PAGE scope means we create one every time anyway
422: }
423: }
424:
425: // Some debug
426: if (log.isDebugEnabled()) {
427: StringBuffer buffer = new StringBuffer();
428: buffer.append("Exec: ").append(call.getScriptName())
429: .append(".").append(call.getMethodName())
430: .append("()");
431:
432: if (create) {
433: buffer.append(" Object created, ");
434: if (!scope.equals(Creator.PAGE)) {
435: buffer.append(" stored in ");
436: buffer.append(scope);
437: } else {
438: buffer.append(" not stored");
439: }
440: } else {
441: buffer.append(" Object found in ");
442: buffer.append(scope);
443: }
444: buffer.append(". ");
445:
446: // It would be good to debug the params but it's not easy
447: //buffer.append("Call params (");
448: //for (int j = 0; j < inctx.getParameterCount(callNum); j++)
449: //{
450: // if (j != 0)
451: // {
452: // buffer.append(", ");
453: // }
454: // InboundVariable param = inctx.getParameter(callNum, j);
455: // buffer.append(param.toString());
456: //}
457: //buffer.append(") ");
458:
459: buffer.append("id=");
460: buffer.append(call.getCallId());
461:
462: log.debug(buffer.toString());
463: }
464:
465: // Execute the filter chain method.toString()
466: final Iterator<AjaxFilter> it = ajaxFilterManager
467: .getAjaxFilters(call.getScriptName());
468: AjaxFilterChain chain = new AjaxFilterChain() {
469: public Object doFilter(Object obj, Method meth,
470: Object[] p) throws Exception {
471: AjaxFilter next = it.next();
472: return next.doFilter(obj, meth, p, this );
473: }
474: };
475: Object reply = chain.doFilter(object, method, call
476: .getParameters());
477: return new Reply(call.getCallId(), reply);
478: } catch (SecurityException ex) {
479: log.debug("Security Exception: ", ex);
480:
481: // If we are in live mode, then we don't even say what went wrong
482: if (debug) {
483: return new Reply(call.getCallId(), null, ex);
484: } else {
485: return new Reply(call.getCallId(), null,
486: new SecurityException());
487: }
488: } catch (InvocationTargetException ex) {
489: // Allow Jetty RequestRetry exception to propagate to container
490: Continuation.rethrowIfContinuation(ex);
491:
492: log.debug("Method execution failed: ", ex
493: .getTargetException());
494: return new Reply(call.getCallId(), null, ex
495: .getTargetException());
496: } catch (Exception ex) {
497: // Allow Jetty RequestRetry exception to propagate to container
498: Continuation.rethrowIfContinuation(ex);
499:
500: log.debug("Method execution failed: ", ex);
501: return new Reply(call.getCallId(), null, ex);
502: }
503: }
504:
505: /**
506: * By default we use a relative path to the DWR servlet which can help if
507: * there are several routes to the servlet. However it can be a pain if
508: * the DWR engine is running on a different port from the web-server.
509: * However this is a minority case so this is not officially supported.
510: * @param useAbsolutePath Does DWR generate an absolute _path property
511: */
512: public void setUseAbsolutePath(boolean useAbsolutePath) {
513: this .useAbsolutePath = useAbsolutePath;
514: }
515:
516: /**
517: * Accessor for the CreatorManager that we configure
518: * @param creatorManager The new ConverterManager
519: */
520: public void setCreatorManager(CreatorManager creatorManager) {
521: this .creatorManager = creatorManager;
522: }
523:
524: /**
525: * Accessor for the ConverterManager that we configure
526: * @param converterManager The new ConverterManager
527: */
528: public void setConverterManager(ConverterManager converterManager) {
529: this .converterManager = converterManager;
530: }
531:
532: /**
533: * Accessor for the security manager
534: * @param accessControl The accessControl to set.
535: */
536: public void setAccessControl(AccessControl accessControl) {
537: this .accessControl = accessControl;
538: }
539:
540: /**
541: * Accessor for the AjaxFilterManager
542: * @param ajaxFilterManager The AjaxFilterManager to set.
543: */
544: public void setAjaxFilterManager(AjaxFilterManager ajaxFilterManager) {
545: this .ajaxFilterManager = ajaxFilterManager;
546: }
547:
548: /**
549: * If we need to override the default path
550: * @param overridePath The new override path
551: */
552: public void setOverridePath(String overridePath) {
553: this .overridePath = overridePath;
554: }
555:
556: /**
557: * Do we allow impossible tests for debug purposes
558: * @param allowImpossibleTests The allowImpossibleTests to set.
559: */
560: public void setAllowImpossibleTests(boolean allowImpossibleTests) {
561: this .allowImpossibleTests = allowImpossibleTests;
562: }
563:
564: /**
565: * To prevent a DoS attack we limit the max number of calls that can be
566: * made in a batch
567: * @param maxCallCount the maxCallCount to set
568: */
569: public void setMaxCallCount(int maxCallCount) {
570: this .maxCallCount = maxCallCount;
571: }
572:
573: /**
574: * Set the debug status
575: * @param debug The new debug setting
576: */
577: public void setDebug(boolean debug) {
578: this .debug = debug;
579: }
580:
581: /**
582: * Are we in debug-mode and therefore more helpful at the expense of security?
583: */
584: private boolean debug = false;
585:
586: /**
587: * What AjaxFilters apply to which Ajax calls?
588: */
589: private AjaxFilterManager ajaxFilterManager = null;
590:
591: /**
592: * How we create new beans
593: */
594: protected CreatorManager creatorManager = null;
595:
596: /**
597: * How we convert beans - or in this case create client side classes
598: */
599: protected ConverterManager converterManager = null;
600:
601: /**
602: * The security manager
603: */
604: protected AccessControl accessControl = null;
605:
606: /**
607: * If we need to override the default path
608: */
609: protected String overridePath = null;
610:
611: /**
612: * @see #setUseAbsolutePath(boolean)
613: */
614: protected boolean useAbsolutePath = false;
615:
616: /**
617: * This helps us test that access rules are being followed
618: */
619: protected boolean allowImpossibleTests = false;
620:
621: /**
622: * To prevent a DoS attack we limit the max number of calls that can be
623: * made in a batch
624: */
625: protected int maxCallCount = 20;
626:
627: /**
628: * Generated Javascript cache
629: */
630: protected Map<String, String> methodCache = Collections
631: .synchronizedMap(new HashMap<String, String>());
632:
633: /**
634: * The log stream
635: */
636: private static final Log log = LogFactory
637: .getLog(DefaultRemoter.class);
638: }
|