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    package org.apache.commons.chain.generic;
018    
019    import org.apache.commons.chain.Command;
020    import org.apache.commons.chain.Context;
021    
022    import java.lang.reflect.InvocationTargetException;
023    import java.lang.reflect.Method;
024    import java.util.Map;
025    import java.util.WeakHashMap;
026    
027    /**
028     * An abstract base command which uses introspection to look up a method to execute.
029     * For use by developers who prefer to group related functionality into a single class
030     * rather than an inheritance family.
031     *
032     * @since Chain 1.1
033     */
034    public abstract class DispatchCommand implements Command {
035    
036        /** Cache of methods */
037        private Map methods = new WeakHashMap();
038    
039        /** Method name */
040        private String method = null;
041    
042        /** Method key */
043        private String methodKey = null;
044    
045        /**
046         * The base implementation expects dispatch methods to take a <code>Context</code>
047         * as their only argument.
048         */
049        protected static final Class[] DEFAULT_SIGNATURE = new Class[] {Context.class};
050    
051    
052        /**
053         * Look up the method specified by either "method" or "methodKey" and invoke it,
054         * returning a boolean value as interpreted by <code>evaluateResult</code>.
055         * @param context The Context to be processed by this Command.
056         * @return the result of method being dispatched to.
057         * @throws IllegalStateException if neither 'method' nor 'methodKey' properties are defined
058         * @throws Exception if any is thrown by the invocation.  Note that if invoking the method
059         * results in an InvocationTargetException, the cause of that exception is thrown instead of
060         * the exception itself, unless the cause is an <code>Error</code> or other <code>Throwable</code>
061         * which is not an <code>Exception</code>.
062         */
063        public boolean execute(Context context) throws Exception {
064    
065            if (this.getMethod() == null && this.getMethodKey() == null) {
066                throw new IllegalStateException("Neither 'method' nor 'methodKey' properties are defined ");
067            }
068    
069            Method methodObject = extractMethod(context);
070    
071            try {
072                return evaluateResult(methodObject.invoke(this, getArguments(context)));
073            } catch (InvocationTargetException e) {
074                Throwable cause = e.getTargetException();
075                if (cause instanceof Exception) {
076                    throw (Exception)cause;
077                }
078                throw e;
079            }
080        }
081    
082        /**
083         * Extract the dispatch method.  The base implementation uses the command's
084         * <code>method</code> property as the name of a method to look up, or, if that is not defined,
085         * looks up the the method name in the Context using the <code>methodKey</code>.
086         *
087         * @param context The Context being processed by this Command.
088         * @return The method to execute
089         * @throws NoSuchMethodException if no method can be found under the specified name.
090         * @throws NullPointerException if no methodName cannot be determined
091         */
092        protected Method extractMethod(Context context) throws NoSuchMethodException {
093    
094            String methodName = this.getMethod();
095    
096            if (methodName == null) {
097                Object methodContextObj = context.get(this.getMethodKey());
098                if (methodContextObj == null) {
099                    throw new NullPointerException("No value found in context under " + this.getMethodKey());
100                }
101                methodName = methodContextObj.toString();
102            }
103    
104    
105            Method theMethod = null;
106    
107            synchronized (methods) {
108                theMethod = (Method) methods.get(methodName);
109    
110                if (theMethod == null) {
111                    theMethod = getClass().getMethod(methodName, getSignature());
112                    methods.put(methodName, theMethod);
113                }
114            }
115    
116            return theMethod;
117        }
118    
119        /**
120         * Evaluate the result of the method invocation as a boolean value.  Base implementation
121         * expects that the invoked method returns boolean true/false, but subclasses might
122         * implement other interpretations.
123         * @param o The result of the methid execution
124         * @return The evaluated result/
125         */
126        protected boolean evaluateResult(Object o) {
127    
128            Boolean result = (Boolean) o;
129            return (result != null && result.booleanValue());
130    
131        }
132    
133        /**
134         * Return a <code>Class[]</code> describing the expected signature of the method.
135         * @return The method signature.
136         */
137        protected Class[] getSignature() {
138            return DEFAULT_SIGNATURE;
139        }
140    
141        /**
142         * Get the arguments to be passed into the dispatch method.
143         * Default implementation simply returns the context which was passed in, but subclasses
144         * could use this to wrap the context in some other type, or extract key values from the
145         * context to pass in.  The length and types of values returned by this must coordinate
146         * with the return value of <code>getSignature()</code>
147         * @param context The Context being processed by this Command.
148         * @return The method arguments.
149         */
150        protected Object[] getArguments(Context context) {
151            return new Object[] {context};
152        }
153    
154        /**
155         * Return the method name.
156         * @return The method name.
157         */
158        public String getMethod() {
159            return method;
160        }
161    
162        /**
163         * Return the Context key for the method name.
164         * @return The Context key for the method name.
165         */
166        public String getMethodKey() {
167            return methodKey;
168        }
169    
170        /**
171         * Set the method name.
172         * @param method The method name.
173         */
174        public void setMethod(String method) {
175            this.method = method;
176        }
177    
178        /**
179         * Set the Context key for the method name.
180         * @param methodKey The Context key for the method name.
181         */
182        public void setMethodKey(String methodKey) {
183            this.methodKey = methodKey;
184        }
185    
186    }