blob: 4258188c370f192a4d13e8c8034a5b11e8db49cc [file] [log] [blame]
// Copyright 2004 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.listener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import ognl.OgnlRuntime;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.ApplicationRuntimeException;
import org.apache.tapestry.IActionListener;
import org.apache.tapestry.IComponent;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.Tapestry;
/**
* Maps a class to a set of listeners based on the public methods of the class.
* {@link org.apache.tapestry.listener.ListenerMapPropertyAccessor} is setup
* to provide these methods as named properties of the ListenerMap.
*
* @author Howard Ship
* @version $Id$
* @since 1.0.2
*
**/
public class ListenerMap
{
private static final Log LOG = LogFactory.getLog(ListenerMap.class);
static {
OgnlRuntime.setPropertyAccessor(ListenerMap.class, new ListenerMapPropertyAccessor());
}
private Object _target;
/**
* A {@link Map} of relevant {@link Method}s, keyed on method name.
* This is just the public void methods that take an {@link IRequestCycle}
* and throw nothing or just {@link ApplicationRuntimeException}.
*/
private Map _methodMap;
/**
* A {@link Map} of cached listener instances, keyed on method name
*
**/
private Map _listenerCache = new HashMap();
/**
* A {@link Map}, keyed on Class, of {@link Map} ... the method map
* for any particular instance of the given class.
*
**/
private static Map _classMap = new HashMap();
/**
* Implements both listener interfaces.
*
**/
private class SyntheticListener implements IActionListener
{
private Method _method;
SyntheticListener(Method method)
{
_method = method;
}
private void invoke(IRequestCycle cycle)
{
Object[] args = new Object[] { cycle };
invokeTargetMethod(_target, _method, args);
}
public void actionTriggered(IComponent component, IRequestCycle cycle)
{
invoke(cycle);
}
public String toString()
{
StringBuffer buffer = new StringBuffer("SyntheticListener[");
buffer.append(_target);
buffer.append(' ');
buffer.append(_method);
buffer.append(']');
return buffer.toString();
}
}
public ListenerMap(Object target)
{
_target = target;
}
/**
* Gets a listener for the given name (which is both a property name
* and a method name). The listener is created as needed, but is
* also cached for later use.
*
* @throws ApplicationRuntimeException if the listener can not be created.
**/
public synchronized Object getListener(String name)
{
Object listener = null;
listener = _listenerCache.get(name);
if (listener == null)
{
listener = createListener(name);
_listenerCache.put(name, listener);
}
return listener;
}
/**
* Returns an object that implements {@link IActionListener}.
* This involves looking up the method by name and determining which
* inner class to create.
**/
private synchronized Object createListener(String name)
{
if (_methodMap == null)
getMethodMap();
Method method = (Method) _methodMap.get(name);
if (method == null)
throw new ApplicationRuntimeException(
Tapestry.format("ListenerMap.object-missing-method", _target, name));
return new SyntheticListener(method);
}
/**
* Gets the method map for the current instance. If necessary, it is constructed and cached (for other instances
* of the same class).
*
**/
private synchronized Map getMethodMap()
{
if (_methodMap != null)
return _methodMap;
Class beanClass = _target.getClass();
synchronized (_classMap)
{
_methodMap = (Map) _classMap.get(beanClass);
if (_methodMap == null)
{
_methodMap = buildMethodMap(beanClass);
_classMap.put(beanClass, _methodMap);
}
}
return _methodMap;
}
private static Map buildMethodMap(Class beanClass)
{
if (LOG.isDebugEnabled())
LOG.debug("Building method map for class " + beanClass.getName());
Map result = new HashMap();
Method[] methods = beanClass.getMethods();
for (int i = 0; i < methods.length; i++)
{
Method m = methods[i];
int mods = m.getModifiers();
if (Modifier.isStatic(mods))
continue;
// Probably not necessary, getMethods() returns only public
// methods.
if (!Modifier.isPublic(mods))
continue;
// Must return void
if (m.getReturnType() != Void.TYPE)
continue;
Class[] parmTypes = m.getParameterTypes();
if (parmTypes.length != 1)
continue;
// parm must be IRequestCycle
if (!parmTypes[0].equals(IRequestCycle.class))
continue;
// Ha! Passed all tests.
result.put(m.getName(), m);
}
return result;
}
/**
* Invoked by the inner listener/adaptor classes to
* invoke the method.
*
**/
private static void invokeTargetMethod(Object target, Method method, Object[] args)
{
if (LOG.isDebugEnabled())
LOG.debug("Invoking listener method " + method + " on " + target);
try
{
try
{
method.invoke(target, args);
}
catch (InvocationTargetException ex)
{
Throwable inner = ex.getTargetException();
if (inner instanceof ApplicationRuntimeException)
throw (ApplicationRuntimeException) inner;
// Edit out the InvocationTargetException, if possible.
if (inner instanceof RuntimeException)
throw (RuntimeException) inner;
throw ex;
}
}
catch (ApplicationRuntimeException ex)
{
throw ex;
}
catch (Exception ex)
{
// Catch InvocationTargetException or, preferrably,
// the inner exception here (if its a runtime exception).
throw new ApplicationRuntimeException(
Tapestry.format("ListenerMap.unable-to-invoke-method", method.getName(), target, ex.getMessage()),
ex);
}
}
/**
* Returns an unmodifiable collection of the
* names of the listeners implemented by the target class.
*
* @since 1.0.6
*
**/
public synchronized Collection getListenerNames()
{
return Collections.unmodifiableCollection(getMethodMap().keySet());
}
/**
* Returns true if this ListenerMap can provide a listener
* with the given name.
*
* @since 2.2
*
**/
public synchronized boolean canProvideListener(String name)
{
return getMethodMap().containsKey(name);
}
public String toString()
{
return "ListenerMap[" + _target + "]";
}
}