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