package com.worldturner.commons.wicket.componentpage.link; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import org.apache.wicket.Component; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.Link; /** * A link that, when clicked on, will replace a component with the component set in this link. In behavior, this link * acts much like {@link BookmarkablePageLink}, except that it doesn't update the whole page, but rather a component on * the page. * * @author Erwin Bolwidt (ebolwidt@worldturner.nl) */ public class ComponentLink extends Link { private static final long serialVersionUID = 1L; /** * The component that will be replaced by the link. If null, then the component is looked up * dynamically through the ancestor chain. */ private Component targetComponent; private Class componentClass; private Object[] arguments; /** * Constructs a ComponentLink that will replace the target component with a new instance of * componentClass. The target component is dynamically determined, and will be the parent component of * the link by default. This can be overridden in subclasses. * * @param id * the id of this link in the containing component * @param componentClass * the class of the component, which will be instantiated when the user clicks on this link * @param arguments * the arguments to pass to the constructor of componentClass as the second and further * arguments. Please take note: the first argument to the constructor will always be the * id that the component gets. */ public ComponentLink(String id, Class componentClass, Object... arguments) { this(id, null, componentClass, arguments); } /** * As {@link ComponentLink#ComponentLink(String, Class, Object...)}, except that the component to replace is * explicitly specified, instead of dynamically looked up. * * @param id * @param targetComponent * @param componentClass * @param arguments */ public ComponentLink(String id, Component targetComponent, Class componentClass, Object... arguments) { super(id); this.targetComponent = targetComponent; this.componentClass = componentClass; this.arguments = arguments; } /** * Finds the constructor with the correct number of arguments and with argument types that can take the supplied * arguments. Note that this is not exactly the same as Java argument matching. Say if there are two constructors: *
    *
  1. public MyComponent(String id, Object argument)
  2. *
  3. public MyComponent(String id, String argument)
  4. *
* and you pass a String to {@link #findConstructor(Class, Object...)}, then you will get a IllegalArgumentException * since both constructors can take a String as their second argument. * * @return the constructor that was found * @throws IllegalArgumentException * if a matching constructor can't be found, or if there is more than one matching constructor */ @SuppressWarnings( { "unchecked" }) private static Constructor findConstructor(Class componentClass, Object... arguments) { Constructor[] constructors = (Constructor[]) componentClass.getConstructors(); Constructor foundConstructor = null; outer: for (Constructor c : constructors) { Class[] parameterTypes = c.getParameterTypes(); if (parameterTypes.length == 1 + arguments.length) { // Found a constructor with the correct number of arguments. Now see if the argument types match. for (int i = 0; i < arguments.length; i++) { if (!parameterTypes[i + 1].isInstance(arguments[i])) { continue outer; } } if (foundConstructor == null) { foundConstructor = c; } else { throw new IllegalArgumentException( "More than one constructor matches the supplied argument in componentClass: " + componentClass.getName()); } return c; } } if (foundConstructor != null) { return foundConstructor; } else { throw new IllegalArgumentException( "Constructor with correct number of arguments and types is not found for componentClass: " + componentClass.getName()); } } @Override public void onClick() { Component replaced = targetComponent; if (replaced == null) { replaced = determineTargetComponent(); } setResponseComponent(replaced, componentClass, arguments); } /** * Determines the target component dynamically. In {@link ComponentLink}, the dynamically determined component is * the parent component of the link. Subclasses may override this behavior. * * @return */ protected Component determineTargetComponent() { return getParent(); } /** * Instantiates a component and replaces an existing component with it. This method functions in a similar way to * {@link Component#setResponsePage(Class)}, except that it doesn't change the whole page, but rather one component * only. * * @param targetComponent * the existing component to replace * @param componentClass * the class of the new component to instantiate. the first argument to the constructor will be the * id of the existing targetComponent. * @param arguments * the second and further arguments to the constructor of componentClass. */ public static void setResponseComponent(Component targetComponent, Class componentClass, Object... arguments) { Component component = instantiateComponent(componentClass, targetComponent.getId(), arguments); targetComponent.replaceWith(component); } /** * Instantiates the componentClass. * * @param componentClass * the class to instantiate * @param id * the component id that the new component will get * @param arguments * the rest of the constructor arguments * @return */ private static Component instantiateComponent(Class componentClass, String id, Object... arguments) { Constructor c = findConstructor(componentClass, arguments); Object[] realArguments = new Object[arguments.length + 1]; realArguments[0] = id; System.arraycopy(arguments, 0, realArguments, 1, arguments.length); try { return c.newInstance(realArguments); } catch (InvocationTargetException e) { throw new RuntimeException("Could not instantiate component", e.getTargetException()); } catch (Exception e) { throw new RuntimeException("Could not instantiate component", e); } } }