001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: * $Header:$
018: */
019: package org.apache.beehive.netui.tags.databinding.invoke;
020:
021: import java.lang.reflect.InvocationTargetException;
022: import java.lang.reflect.Method;
023: import java.util.ArrayList;
024: import java.util.List;
025: import java.util.Collections;
026: import javax.servlet.jsp.JspException;
027:
028: import org.apache.beehive.netui.tags.AbstractClassicTag;
029: import org.apache.beehive.netui.util.Bundle;
030: import org.apache.beehive.netui.util.internal.InternalStringBuilder;
031: import org.apache.beehive.netui.util.logging.Logger;
032: import org.apache.beehive.netui.util.type.TypeUtils;
033:
034: /**
035: * <p>
036: * An abstract base class for tags that are capable of reflectively invoking methods. Specializations of this
037: * tag provide method implementations that locate the object on which to invoke the method and that handle
038: * any return value from the invoked method.
039: * <p/>
040: * <p>
041: * The <code>CallMethod</code> tag can have child tags of type {@link MethodParameter}; these tags must be in the same
042: * order as the parameter list in the method signature of the method that will be invoked. To invoke an overloaded
043: * method, the {@link MethodParameter#setType(String)} property must be set to the String name of the type to pass
044: * to the method. If the type attribute values on nested {@link MethodParameter} tags do not match any method
045: * signature, an error will be reported in the page.
046: * </p>
047: */
048: public abstract class AbstractCallMethod extends AbstractClassicTag {
049:
050: private static final Logger LOGGER = Logger
051: .getInstance(AbstractCallMethod.class);
052:
053: private static final Object[] EMPTY_ARGS = new Object[0];
054: private static final String EMPTY_STRING = "";
055:
056: private List _parameters = null;
057: private String _method = null;
058: private boolean _failOnError = true;
059: private String _resultId = null;
060: private boolean _verifyTypes = false;
061:
062: /**
063: * Sets the identifier at which the result of invoking the method will stored. Once stored, the
064: * result of the reflective invocation will be available via the JSP EL implicit object
065: * <code>${pageScope}</code> with the attribute name set via this property.
066: *
067: * @param resultId a String that names an attribute in the PageContext's
068: * attribute map where any resulting object will be stored.
069: * @jsptagref.attributedescription
070: * Sets the identifier at which the result of invoking the method will stored. Once stored, the
071: * result of the reflective invocation will be available via the JSP EL implicit object
072: * <code>${pageScope}</code> with the attribute name set via this property.
073: * @netui:attribute required="false"
074: */
075: public void setResultId(String resultId) {
076: _resultId = resultId;
077: }
078:
079: /**
080: * Sets whether or not to report exceptions to the page when errors occur invoking a method on an object.
081: *
082: * @param failOnError a boolean that defines whether or not exceptions should be thrown when invocation fails.
083: * @jsptagref.attributedescription
084: * Sets whether or not to report exceptions to the page when errors occur invoking a method on an object.
085: * @netui:attribute required="false"
086: */
087: public void setFailOnError(boolean failOnError) {
088: _failOnError = failOnError;
089: }
090:
091: /**
092: * Sets the name of a method to invoke on the target object.
093: *
094: * @param method the name of the method to invoke
095: * @jsptagref.attributedescription
096: * Sets the name of a method to invoke on the target object.
097: * @netui:attribute required="true"
098: */
099: public void setMethod(String method) {
100: _method = method;
101: }
102:
103: /**
104: * Add a paramter that will be passed as an argument to the method that will be invoked. This method
105: * is implemented to allow the the {@link MethodParameter} tags to register their parameters. This
106: * object is passed in the position that it appeared in the set of child {@link MethodParameter} tags.
107: *
108: * @param type a String of the type or class name of this parameter
109: * @param parameter an object that should be passed as an argument to the invoked method
110: * @see MethodParameter
111: */
112: public void addParameter(String type, Object parameter) {
113: if (_parameters == null)
114: _parameters = new ArrayList();
115:
116: // only check the types if necessary
117: if (type != null)
118: _verifyTypes = true;
119:
120: ParamNode pn = new ParamNode();
121: pn.typeName = type;
122: pn.paramValue = parameter;
123:
124: _parameters.add(pn);
125: }
126:
127: /**
128: * Causes the body of this tag to be rendered; only {@link MethodParameter} tags are allowed to be
129: * contained inside of this tag. The output of rendering the body is never written into the
130: * output stream of the servlet.
131: *
132: * @return {@link #EVAL_BODY_BUFFERED}
133: */
134: public int doStartTag() throws JspException {
135: return EVAL_BODY_BUFFERED;
136: }
137:
138: /**
139: * Reflectively invokes the method specified by the <code>method</code> attribute,
140: * {@link #findMethod(Object, String, boolean)}. The arguments passed to the method are taken from any nested
141: * {@link MethodParameter} tags. When the parameters which are added by the
142: * {@link MethodParameter} tags are {@link java.lang.String} types, an attempt is made to convert each of
143: * these parameters into the type expected by the method. This conversion is done using the
144: * {@link TypeUtils#convertToObject(java.lang.String, java.lang.Class)} method. If a String can not
145: * be converted do the type expected by the method, an exception is thrown and the error is reported
146: * in the tag. Any return value that results from invoking the given method is passed to the
147: * subclass implementation of the method {@link #handleReturnValue(java.lang.Object)}.
148: *
149: * @return {@link #EVAL_PAGE} to continue evaluating the page
150: * @throws JspException if there are errors. All exceptions that may be thrown
151: * in the process of reflectively invoking the method and performing type
152: * conversion are reported as {@link JspException}
153: * @see #findMethod(Object, String, boolean)
154: * @see #handleReturnValue(java.lang.Object)
155: * @see MethodParameter
156: * @see ObjectNotFoundException
157: * @see TypeUtils#convertToObject(java.lang.String, java.lang.Class)
158: * @see java.lang.String
159: */
160: public int doEndTag() throws JspException {
161:
162: // find the object on which to invoke the method
163: Object object = null;
164: try {
165: object = resolveObject();
166: } catch (ObjectNotFoundException onf) {
167: Throwable cause = (onf.getCause() != null ? onf.getCause()
168: : onf);
169: String msg = Bundle.getErrorString(
170: "Tags_AbstractCallMethod_noSuchObject",
171: new Object[] { getObjectName(), _method, cause });
172: registerTagError(msg, null);
173: }
174:
175: // if this tag can accept null invocation targets,
176: if (object == null) {
177: if (allowNullInvocationTarget()) {
178: // each implementation does this on their own
179: handleReturnValue(null);
180: localRelease();
181: return EVAL_PAGE;
182: } else {
183: String msg = Bundle.getErrorString(
184: "Tags_AbstractCallMethod_objectIsNull",
185: new Object[] { getObjectName(), _method });
186: registerTagError(msg, null);
187: }
188: }
189:
190: if (hasErrors()) {
191: reportErrors();
192: localRelease();
193: return EVAL_PAGE;
194: }
195:
196: Method m = findMethod(object, _method, _verifyTypes);
197:
198: if (m == null) {
199: String msg = null;
200: if (_verifyTypes) {
201: String paramString = prettyPrintParameterTypes(_parameters);
202: msg = Bundle
203: .getErrorString(
204: "Tags_AbstractCallMethod_noSuchMethodWithTypes",
205: new Object[] {
206: _method,
207: (_parameters != null ? new Integer(
208: _parameters.size())
209: : new Integer(0)),
210: paramString, getObjectName() });
211: } else
212: msg = Bundle.getErrorString(
213: "Tags_AbstractCallMethod_noSuchMethod",
214: new Object[] {
215: _method,
216: (_parameters != null ? new Integer(
217: _parameters.size())
218: : new Integer(0)),
219: getObjectName() });
220:
221: registerTagError(msg, null);
222: reportErrors();
223: localRelease();
224: return EVAL_PAGE;
225: }
226:
227: Object[] args = null;
228: try {
229: args = getArguments(m.getParameterTypes());
230: } catch (IllegalArgumentException iae) {
231: registerTagError(iae.getMessage(), null);
232: reportErrors();
233: localRelease();
234: return EVAL_PAGE;
235: }
236:
237: // invoke method
238: Object result = null;
239: try {
240: if (LOGGER.isDebugEnabled()) {
241: LOGGER.debug("method: " + m.toString());
242: for (int i = 0; i < args.length; i++)
243: LOGGER.debug("arg["
244: + i
245: + "]: "
246: + (args[i] != null ? args[i].getClass()
247: .getName() : "null"));
248: }
249:
250: result = m.invoke(object, args);
251: } catch (Exception e) {
252: assert e instanceof IllegalAccessException
253: || e instanceof InvocationTargetException
254: || e instanceof IllegalArgumentException;
255:
256: if (LOGGER.isErrorEnabled())
257: LOGGER.error("Could not invoke method \"" + _method
258: + "\" on the object named \"" + getObjectName()
259: + "\" because: " + e, e);
260:
261: if (_failOnError) {
262: String msg = Bundle.getErrorString(
263: "Tags_AbstractCallMethod_invocationError",
264: new Object[] { _method, getObjectName(), e });
265: registerTagError(msg, null);
266: reportErrors();
267: localRelease();
268: return EVAL_PAGE;
269: }
270: }
271:
272: if (LOGGER.isDebugEnabled()) {
273: LOGGER
274: .debug((result != null ? "return value is non-null and is of type \""
275: + result.getClass().getName() + "\""
276: : "return value is null."));
277: }
278:
279: /* allow the tag to handle the return value */
280: handleReturnValue(result);
281:
282: localRelease();
283:
284: return EVAL_PAGE;
285: }
286:
287: /**
288: * Reset all of the fields of this tag.
289: */
290: protected void localRelease() {
291: super .localRelease();
292: _parameters = null;
293: _method = null;
294: _failOnError = true;
295: _resultId = null;
296: _verifyTypes = false;
297: }
298:
299: /**
300: * <p/>
301: * Resolve the object on which the method should be invoked. If there are errors resolving this object,
302: * this method will throw an {@link ObjectNotFoundException}.
303: * </p>
304: * <p>
305: * If the object is not found but no exception occurred, this method returns <code>null</code>.
306: * </p>
307: *
308: * @return the object on which to reflectively invoke the method or <code>null</code> if the
309: * object was not found and no exception occurred.
310: * @throws ObjectNotFoundException if an exception occurred attempting to resolve an object
311: */
312: protected abstract Object resolveObject()
313: throws ObjectNotFoundException, JspException;
314:
315: /**
316: * The default findMethod implementation is an uncached search of all
317: * of the methods available on the Class of the <code>target</code>
318: *
319: * @param target the object from which to find the method
320: * @param methodName the name of the method to find
321: * @param verifyTypes a boolean that if true will match the type names in addition to the String method name
322: * @return a Method object matching the methodName and types, if <code>verifyTypes</code> is true.
323: * <code>null</code> otherwise.
324: */
325: protected abstract Method findMethod(Object target,
326: String methodName, boolean verifyTypes);
327:
328: /**
329: * Get the name of the object that is the target of the invocation. This is a generic method for this
330: * tag that enables more specific error reporting.
331: *
332: * @return a name for the object on which the method will be invoked.
333: */
334: protected abstract String getObjectName();
335:
336: /**
337: * When implemented to return true, this method allows a tag invoking a method to
338: * accept a null invocation target and simply return null. The default
339: * implementation returns false.
340: *
341: * @return true if the object on which to invoke the method can be null; false otherwise.
342: */
343: protected boolean allowNullInvocationTarget() {
344: return false;
345: }
346:
347: /**
348: * <p/>
349: * A method that allows concrete classes to handle the result of the
350: * reflective invocation in an implementation specific way.
351: * </p>
352: * <p/>
353: * The default beahavior is to set the return value resulting from invoking the method
354: * in the {@link javax.servlet.jsp.PageContext} attribute map of the current JSP page.
355: * The result is set as an attribute if the <code>result</code> is not null and the
356: * {@link CallMethod#setResultId(java.lang.String)} String is not null. If the value returned
357: * from calling a method is null and the {@link CallMethod#setResultId(java.lang.String)} is non-null,
358: * the {@link javax.servlet.jsp.PageContext#removeAttribute(java.lang.String)}
359: * is called to remove the attribute from the attribute map.
360: * </p>
361: *
362: * @param result the object that was returned by calling the method on the object
363: */
364: protected void handleReturnValue(Object result) {
365: if (_resultId != null) {
366: if (result != null) {
367: if (LOGGER.isInfoEnabled()
368: && pageContext.getAttribute(_resultId) != null)
369: LOGGER
370: .info("Overwriting attribute named \""
371: + _resultId
372: + "\" in the PageContext with a new attribute of type \""
373: + result.getClass().getName()
374: + "\" returned from calling the method \""
375: + _method
376: + "\" on an object named \""
377: + getObjectName() + "\".");
378:
379: pageContext.setAttribute(_resultId, result);
380: } else {
381: if (LOGGER.isInfoEnabled())
382: LOGGER
383: .info("Removing attribute named \""
384: + _resultId
385: + "\" from the PageContext. "
386: + "The value returned from calling the method \""
387: + _method
388: + "\" on an object named \""
389: + getObjectName() + "\" is null.");
390:
391: pageContext.removeAttribute(_resultId);
392: }
393: }
394: }
395:
396: /**
397: * Internal, read-only property used by subclasses to get
398: * the list of parameters to be used when reflectively
399: * invoking a method. If the method takes no parameters, this
400: * list will be of size zero.
401: *
402: * @return the list of parameters
403: */
404: protected List getParameterNodes() {
405: return _parameters != null ? _parameters
406: : Collections.EMPTY_LIST;
407: }
408:
409: /**
410: * Convert the arguments for a method from Strings set as attributes
411: * on JSP tags to the types represented by teh list of Class[] objects
412: * provided here.
413: *
414: * @return an Object[] that contains the parameters to pass to the method
415: * @throws IllegalArgumentException if an error occurs converting an
416: * argument to a specific type.
417: */
418: private Object[] getArguments(Class[] paramTypes) {
419: if (_parameters == null)
420: return EMPTY_ARGS;
421:
422: Object[] args = new Object[paramTypes.length];
423:
424: for (int i = 0; i < _parameters.size(); i++) {
425: ParamNode pn = (ParamNode) _parameters.get(i);
426:
427: if (LOGGER.isDebugEnabled())
428: LOGGER.debug("argTypes[" + i + "]: " + paramTypes[i]);
429:
430: /* if the parameter should have been null, leave it null */
431: if (pn.paramValue == MethodParameter.NULL_ARG)
432: continue;
433:
434: Object value = pn.paramValue;
435: try {
436: /* if the value wasn't a String, assume it was referenced via an expression language
437: and is already of the correct type */
438: if (!(value instanceof String) || value == null)
439: args[i] = value;
440: /* convert a non-null String value using the registered type converters */
441: else
442: args[i] = TypeUtils.convertToObject((String) value,
443: paramTypes[i]);
444: }
445: /* catch Exception here because almost anything can be thrown by TypeUtils.convertToObject(). */
446: catch (Exception e) {
447: String msg = Bundle.getErrorString(
448: "Tags_AbstractCallMethod_parameterError",
449: new Object[] { paramTypes[i], new Integer(i),
450: value, e.toString() });
451: throw new IllegalArgumentException(msg);
452: }
453: }
454:
455: return args;
456: }
457:
458: /**
459: * Utility method that pretty-prints the types of the parameters
460: * passed to a method; this is used in debugging.
461: *
462: * @param parameters the list of parameters
463: * @return a String that represents the types of each of these paramters in order
464: */
465: private static String prettyPrintParameterTypes(List parameters) {
466: InternalStringBuilder paramString;
467: if (parameters != null && parameters.size() > 0) {
468: paramString = new InternalStringBuilder(128);
469: paramString.append("(");
470: for (int i = 0; i < parameters.size(); i++) {
471: if (i > 0)
472: paramString.append(", ");
473: paramString
474: .append(((ParamNode) parameters.get(i)).typeName);
475: }
476: paramString.append(")");
477:
478: return paramString.toString();
479: } else
480: return "";
481: }
482:
483: /**
484: * An internal struct that represents a parameter that will be passed to a
485: * reflective method invocation call. Instances of <code>ParamNode</code>
486: * map 1:1 to the methodParameter tags that appear within the body of
487: * an AbstrctCallMethod tag.
488: *
489: * @exclude
490: */
491: protected class ParamNode {
492: /**
493: * The fully qualified class name of the parameter type. This value
494: * can be null if parameter type checking does not need to occur.
495: */
496: String typeName = null;
497:
498: /**
499: * The value of the parameter. Often, this is a String expression
500: * which is evaluated later and converted into some Object
501: * type such as Integer or Foobar.
502: */
503: Object paramValue = null;
504: }
505: }
|