    001package org.apache.turbine.modules;
    002
    003/*
    004 * Licensed to the Apache Software Foundation (ASF) under one
    005 * or more contributor license agreements.  See the NOTICE file
    006 * distributed with this work for additional information
    007 * regarding copyright ownership.  The ASF licenses this file
    008 * to you under the Apache License, Version 2.0 (the
    009 * "License"); you may not use this file except in compliance
    010 * with the License.  You may obtain a copy of the License at
    011 *
    012 *   http://www.apache.org/licenses/LICENSE-2.0
    013 *
    014 * Unless required by applicable law or agreed to in writing,
    015 * software distributed under the License is distributed on an
    016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    017 * KIND, either express or implied.  See the License for the
    018 * specific language governing permissions and limitations
    019 * under the License.
    020 */
    021
    022import java.lang.reflect.InvocationTargetException;
    023import java.lang.reflect.Method;
    024import java.util.Arrays;
    025
    026import org.apache.commons.collections.map.MultiKeyMap;
    027import org.apache.commons.lang.StringUtils;
    028import org.apache.commons.logging.Log;
    029import org.apache.commons.logging.LogFactory;
    030import org.apache.fulcrum.parser.ParameterParser;
    031import org.apache.fulcrum.parser.ValueParser.URLCaseFolding;
    032import org.apache.turbine.Turbine;
    033import org.apache.turbine.TurbineConstants;
    034import org.apache.turbine.annotation.TurbineActionEvent;
    035import org.apache.turbine.pipeline.PipelineData;
    036
    037/**
    038 * <p>
    039 *
    040 * This is an alternative to the Action class that allows you to do
    041 * event based actions. Essentially, you label all your submit buttons
    042 * with the prefix of "eventSubmit_" and the suffix of "methodName".
    043 * For example, "eventSubmit_doDelete". Then any class that subclasses
    044 * this class will get its "doDelete(PipelineData data)" method executed.
    045 * If for any reason, it was not able to execute the method, it will
    046 * fall back to executing the doPerform() method which is required to
    047 * be implemented.
    048 *
    049 * <p>
    050 *
    051 * Limitations:
    052 *
    053 * <p>
    054 *
    055 * Because ParameterParser makes all the key values lowercase, we have
    056 * to do some work to format the string into a method name. For
    057 * example, a button name eventSubmit_doDelete gets converted into
    058 * eventsubmit_dodelete. Thus, we need to form some sort of naming
    059 * convention so that dodelete can be turned into doDelete.
    060 *
    061 * <p>
    062 *
    063 * Thus, the convention is this:
    064 *
    065 * <ul>
    066 * <li>The variable name MUST have the prefix "eventSubmit_".</li>
    067 * <li>The variable name after the prefix MUST begin with the letters
    068 * "do".</li>
    069 * <li>The first letter after the "do" will be capitalized and the
    070 * rest will be lowercase</li>
    071 * </ul>
    072 *
    073 * If you follow these conventions, then you should be ok with your
    074 * method naming in your Action class.
    075 *
    076 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens </a>
    077 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
    078 * @author <a href="quintonm@bellsouth.net">Quinton McCombs</a>
    079 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
    080 * @version $Id: ActionEvent.java 1706425 2015-10-02 14:47:51Z tv $
    081 */
    082public abstract class ActionEvent extends Action
    083{
    084        /** Logging */
    085        protected Log log = LogFactory.getLog(this.getClass());
    086
    087        /** The name of the button to look for. */
    088        protected static final String BUTTON = "eventSubmit_";
    089        /** The length of the button to look for. */
    090        protected static final int BUTTON_LENGTH = BUTTON.length();
    091    /** The default method. */
    092    protected static final String DEFAULT_METHOD = "doPerform";
    093        /** The prefix of the method name. */
    094        protected static final String METHOD_NAME_PREFIX = "do";
    095        /** The length of the method name. */
    096        protected static final int METHOD_NAME_LENGTH = METHOD_NAME_PREFIX.length();
    097        /** The length of the button to look for. */
    098        protected static final int LENGTH = BUTTON.length();
    099
    100        /**
    101         * If true, the eventSubmit_do<xxx> variable must contain
    102         * a not null value to be executed.
    103         */
    104        private boolean submitValueKey = false;
    105
    106        /**
    107         * If true, then exceptions raised in eventSubmit_do<xxx> methods
    108         * as well as in doPerform methods are bubbled up to the Turbine
    109         * servlet's handleException method.
    110         */
    111        protected boolean bubbleUpException = true;
    112
    113        /**
    114         * Cache for the methods to invoke
    115         */
    116        private MultiKeyMap/* <String, Method> */ methodCache = new MultiKeyMap/* <String, Method> */();
    117
    118        /**
    119         * C'tor
    120         */
    121        public ActionEvent()
    122        {
    123                super();
    124
    125                submitValueKey = Turbine.getConfiguration()
    126                                .getBoolean(TurbineConstants.ACTION_EVENTSUBMIT_NEEDSVALUE_KEY,
    127                                                TurbineConstants.ACTION_EVENTSUBMIT_NEEDSVALUE_DEFAULT);
    128                bubbleUpException = Turbine.getConfiguration()
    129                                .getBoolean(TurbineConstants.ACTION_EVENT_BUBBLE_EXCEPTION_UP,
    130                                                TurbineConstants.ACTION_EVENT_BUBBLE_EXCEPTION_UP_DEFAULT);
    131
    132                if (log.isDebugEnabled())
    133                {
    134                log.debug(submitValueKey
    135                                ? "ActionEvent accepts only eventSubmit_do Keys with a value != 0"
    136                                : "ActionEvent accepts all eventSubmit_do Keys");
    137                log.debug(bubbleUpException
    138                                  ? "ActionEvent will bubble exceptions up to Turbine.handleException() method"
    139                                  : "ActionEvent will not bubble exceptions up.");
    140                }
    141        }
    142
    143        /**
    144         * Retrieve a method of the given name and signature. The value is cached.
    145         *
    146         * @param name the name of the method
    147         * @param signature an array of classes forming the signature of the method
    148         * @param pp ParameterParser for correct folding
    149         *
    150         * @return the method object
    151         * @throws NoSuchMethodException if the method does not exist
    152         */
    153        protected Method getMethod(String name, Class<?>[] signature, ParameterParser pp) throws NoSuchMethodException
    154        {
    155            Method method = (Method) this.methodCache.get(name, signature);
    156
    157            if (method == null)
    158            {
    159                // Try annotations of public methods
    160                Method[] methods = getClass().getMethods();
    161                for (Method m : methods)
    162                {
    163                    if (m.isAnnotationPresent(TurbineActionEvent.class))
    164                    {
    165                        TurbineActionEvent tae = m.getAnnotation(TurbineActionEvent.class);
    166                        if (name.equals(pp.convert(tae.value()))
    167                        && Arrays.equals(signature, m.getParameterTypes()))
    168                        {
    169                            method = m;
    170                            break;
    171                        }
    172                    }
    173                }
    174
    175                // Try legacy mode
    176                if (method == null)
    177                {
    178                String tmp = name.toLowerCase().substring(METHOD_NAME_LENGTH);
    179                    method = getClass().getMethod(METHOD_NAME_PREFIX + StringUtils.capitalize(tmp), signature);
    180                }
    181
    182                this.methodCache.put(name, signature, method);
    183            }
    184
    185            return method;
    186        }
    187
    188        /**
    189         * This overrides the default Action.doPerform() to execute the
    190         * doEvent() method. If that fails, then it will execute the
    191         * doPerform() method instead.
    192         *
    193         * @param pipelineData Turbine information.
    194         * @exception Exception a generic exception.
    195         */
    196        @Override
    197    public void doPerform(PipelineData pipelineData)
    198                        throws Exception
    199        {
    200            ParameterParser pp = pipelineData.get(Turbine.class, ParameterParser.class);
    201                executeEvents(pp, new Class<?>[]{ PipelineData.class }, new Object[]{ pipelineData });
    202        }
    203
    204        /**
    205         * This method should be called to execute the event based system.
    206         *
    207         * @param pp the parameter parser
    208         * @param signature the signature of the method to call
    209         * @param parameters the parameters for the method to call
    210         *
    211         * @exception Exception a generic exception.
    212         */
    213        protected void executeEvents(ParameterParser pp, Class<?>[] signature, Object[] parameters)
    214                        throws Exception
    215        {
    216                // Name of the button.
    217                String theButton = null;
    218
    219                String button = pp.convert(BUTTON);
    220                String key = null;
    221
    222                // Loop through and find the button.
    223                for (String k : pp)
    224                {
    225                        key = k;
    226                        if (key.startsWith(button))
    227                        {
    228                                if (considerKey(key, pp))
    229                                {
    230                                        theButton = key;
    231                                        break;
    232                                }
    233                        }
    234                }
    235
    236                if (theButton == null)
    237                {
    238                    theButton = BUTTON + DEFAULT_METHOD;
    239                    key = null;
    240                }
    241
    242                theButton = formatString(theButton, pp);
    243                Method method = null;
    244
    245        try
    246        {
    247            method = getMethod(theButton, signature, pp);
    248        }
    249        catch (NoSuchMethodException e)
    250        {
    251            method = getMethod(DEFAULT_METHOD, signature, pp);
    252        }
    253        finally
    254        {
    255            if (key != null)
    256            {
    257                pp.remove(key);
    258            }
    259        }
    260
    261                try
    262                {
    263                        if (log.isDebugEnabled())
    264                        {
    265                                log.debug("Invoking " + method);
    266                        }
    267
    268                        method.invoke(this, parameters);
    269                }
    270                catch (InvocationTargetException ite)
    271                {
    272                        Throwable t = ite.getTargetException();
    273                        if (bubbleUpException)
    274                        {
    275                if (t instanceof Exception)
    276                {
    277                    throw (Exception) t;
    278                }
    279                else
    280                {
    281                    throw ite;
    282                }
    283                        }
    284                        else
    285                        {
    286                            log.error("Invokation of " + method , t);
    287                        }
    288                }
    289        }
    290
    291        /**
    292         * This method does the conversion of the lowercase method name
    293         * into the proper case.
    294         *
    295         * @param input The unconverted method name.
    296         * @param pp The parameter parser (for correct folding)
    297         * @return A string with the method name in the proper case.
    298         */
    299        protected String formatString(String input, ParameterParser pp)
    300        {
    301                String tmp = input;
    302
    303                if (StringUtils.isNotEmpty(input))
    304                {
    305                        tmp = input.toLowerCase();
    306
    307                        // Chop off suffixes (for image type)
    308                        String methodName = (tmp.endsWith(".x") || tmp.endsWith(".y"))
    309                                        ? input.substring(0, input.length() - 2)
    310                                        : input;
    311
    312                        if (pp.getUrlFolding() == URLCaseFolding.NONE)
    313                        {
    314                tmp = methodName.substring(BUTTON_LENGTH);
    315                        }
    316                        else
    317                        {
    318                tmp = methodName.toLowerCase().substring(BUTTON_LENGTH);
    319                        }
    320                }
    321
    322                return tmp;
    323        }
    324
    325        /**
    326         * Checks whether the selected key really is a valid event.
    327         *
    328         * @param key The selected key
    329         * @param pp The parameter parser to look for the key value
    330         *
    331         * @return true if this key is really an ActionEvent Key
    332         */
    333        protected boolean considerKey(String key, ParameterParser pp)
    334        {
    335                if (!submitValueKey)
    336                {
    337                        log.debug("No Value required, accepting " + key);
    338                        return true;
    339                }
    340                else
    341                {
    342                        // If the action.eventsubmit.needsvalue key is true,
    343                        // events with a "0" or empty value are ignored.
    344                        // This can be used if you have multiple eventSubmit_do<xxx>
    345                        // fields in your form which are selected by client side code,
    346                        // e.g. JavaScript.
    347                        //
    348                        // If this key is unset or missing, nothing changes for the
    349                        // current behavior.
    350                        //
    351                        String keyValue = pp.getString(key);
    352                        log.debug("Key Value is " + keyValue);
    353                        if (StringUtils.isEmpty(keyValue))
    354                        {
    355                                log.debug("Key is empty, rejecting " + key);
    356                                return false;
    357                        }
    358
    359                        try
    360                        {
    361                                if (Integer.parseInt(keyValue) != 0)
    362                                {
    363                                        log.debug("Integer != 0, accepting " + key);
    364                                        return true;
    365                                }
    366                        }
    367                        catch (NumberFormatException nfe)
    368                        {
    369                                // Not a number. So it might be a
    370                                // normal Key like "continue" or "exit". Accept
    371                                // it.
    372                                log.debug("Not a number, accepting " + key);
    373                                return true;
    374                        }
    375                }
    376                log.debug("Rejecting " + key);
    377                return false;
    378        }
    379}