blob: 7c0b6d4e43c49e17f97fcd876109927ca90c9678 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.click.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.servlet.ServletContext;
import org.apache.click.Context;
import org.apache.click.Control;
import org.apache.click.Page;
import org.apache.click.control.Button;
import org.apache.click.control.Container;
import org.apache.click.control.Field;
import org.apache.click.control.FieldSet;
import org.apache.click.control.Form;
import org.apache.click.control.Label;
import org.apache.click.service.ConfigService;
import org.apache.click.service.LogService;
import org.apache.click.service.PropertyService;
import org.apache.commons.lang.ClassUtils;
/**
* Provides Container access and copy utilities.
*/
public class ContainerUtils {
/**
* Populate the given object attributes from the Containers field values.
* <p/>
* If a Field and object attribute matches, the object attribute is set to
* the Object returned from the method
* {@link org.apache.click.control.Field#getValueObject()}. If an object
* attribute is a primitive, the Object returned from
* {@link org.apache.click.control.Field#getValueObject()} will be converted
* into the specific primitive e.g. Integer will become int and Boolean will
* become boolean.
* <p/>
* The fieldList specifies which fields to copy to the object. This allows
* one to include or exclude certain Container fields before populating the
* object.
* <p/>
* The following example shows how to exclude disabled fields from
* populating a customer object:
* <pre class="prettyprint">
* public void onInit() {
* List formFields = new ArrayList();
* for(Iterator it = form.getFieldList().iterator(); it.hasNext(); ) {
* Field field = (Field) formFields.next();
* // Exclude disabled fields
* if (!field.isDisabled()) {
* formFields.add(field);
* }
* }
* Customer customer = new Customer();
* ContainerUtils.copyContainerToObject(form, customer, formFields);
* }
* </pre>
*
* The specified Object can either be a POJO (plain old java object) or
* a {@link java.util.Map}. If a POJO is specified, its attributes are
* populated from matching container fields. If a map is specified, its
* key/value pairs are populated from matching container fields.
*
* @param container the fieldList Container
* @param object the object to populate with field values
* @param fieldList the list of fields to obtain values from
*
* @throws IllegalArgumentException if container, object or fieldList is
* null
*/
public static void copyContainerToObject(Container container,
Object object, List<Field> fieldList) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
if (object == null) {
throw new IllegalArgumentException("Null object parameter");
}
if (fieldList == null) {
throw new IllegalArgumentException("Null fieldList parameter");
}
if (fieldList.isEmpty()) {
LogService logService = ClickUtils.getLogService();
if (logService.isDebugEnabled()) {
String containerClassName =
ClassUtils.getShortClassName(container.getClass());
logService.debug(" " + containerClassName
+ " has no fields to copy from");
}
//Exit early.
return;
}
String objectClassname = object.getClass().getName();
objectClassname =
objectClassname.substring(objectClassname.lastIndexOf(".") + 1);
// If the given object is a map, its key/value pair is populated from
// the fields name/value pair.
if (object instanceof Map<?, ?>) {
copyFieldsToMap(fieldList, (Map) object);
// Exit after populating the map.
return;
}
LogService logService = ClickUtils.getLogService();
Set<String> properties = getObjectPropertyNames(object);
for (Field field : fieldList) {
// Ignore disabled field as their values are not submitted in HTML
// forms
if (field.isDisabled()) {
continue;
}
if (!hasMatchingProperty(field, properties)) {
continue;
}
String fieldName = field.getName();
ensureObjectPathNotNull(object, fieldName);
ConfigService configService = ClickUtils.getConfigService();
PropertyService propertyService = configService.getPropertyService();
try {
propertyService.setValue(object, fieldName, field.getValueObject());
if (logService.isDebugEnabled()) {
String containerClassName =
ClassUtils.getShortClassName(container.getClass());
String msg = " " + containerClassName + " -> "
+ objectClassname + "." + fieldName + " : "
+ field.getValueObject();
logService.debug(msg);
}
} catch (Exception e) {
String msg =
"Error incurred invoking " + objectClassname + "."
+ fieldName + " with " + field.getValueObject()
+ " error: " + e.toString();
logService.debug(msg);
}
}
}
/**
* Populate the given object attributes from the Containers field values.
*
* @see #copyContainerToObject(org.apache.click.control.Container, java.lang.Object, java.util.List)
*
* @param container the Container to obtain field values from
* @param object the object to populate with field values
*/
public static void copyContainerToObject(Container container,
Object object) {
List<Field> fieldList = getInputFields(container);
copyContainerToObject(container, object, fieldList);
}
/**
* Populate the given Container field values from the object attributes.
* <p/>
* If a Field and object attribute matches, the Field value is set to the
* object attribute using the method
* {@link org.apache.click.control.Field#setValueObject(java.lang.Object)}. If
* an object attribute is a primitive it is first converted to its proper
* wrapper class e.g. int will become Integer and boolean will become
* Boolean.
* <p/>
* The fieldList specifies which fields to populate from the object. This
* allows one to exclude or include specific fields.
* <p/>
* The specified Object can either be a POJO (plain old java object) or
* a {@link java.util.Map}. If a POJO is specified, its attributes are
* copied to matching container fields. If a map is specified, its key/value
* pairs are copied to matching container fields.
*
* @param object the object to obtain attribute values from
* @param container the Container to populate
* @param fieldList the list of fields to populate from the object
* attributes
*/
public static void copyObjectToContainer(Object object,
Container container, List<Field> fieldList) {
if (object == null) {
throw new IllegalArgumentException("Null object parameter");
}
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
if (fieldList == null) {
throw new IllegalArgumentException("Null fieldList parameter");
}
if (fieldList.isEmpty()) {
LogService logService = ClickUtils.getLogService();
if (logService.isDebugEnabled()) {
String containerClassName =
ClassUtils.getShortClassName(container.getClass());
logService.debug(" " + containerClassName
+ " has no fields to copy to");
}
//Exit early.
return;
}
String objectClassname = object.getClass().getName();
objectClassname =
objectClassname.substring(objectClassname.lastIndexOf(".") + 1);
//If the given object is a map, populate the fields name/value from
//the maps key/value pair.
if (object instanceof Map<?, ?>) {
copyMapToFields((Map) object, fieldList);
//Exit after populating the fields.
return;
}
Set<String> properties = getObjectPropertyNames(object);
LogService logService = ClickUtils.getLogService();
for (Field field : fieldList) {
if (!hasMatchingProperty(field, properties)) {
continue;
}
String fieldName = field.getName();
try {
Object result = getPropertyService().getValue(object, fieldName);
field.setValueObject(result);
if (logService.isDebugEnabled()) {
String containerClassName =
ClassUtils.getShortClassName(container.getClass());
String msg = " " + containerClassName + " <- "
+ objectClassname + "." + fieldName + " : "
+ result;
logService.debug(msg);
}
} catch (Exception e) {
String msg = "Error incurred invoking " + objectClassname + "."
+ fieldName + " error: " + e.toString();
logService.debug(msg);
}
}
}
/**
* Populate the given Container field values from the object attributes.
*
* @see #copyObjectToContainer(java.lang.Object, org.apache.click.control.Container, java.util.List)
*
* @param object the object to obtain attribute values from
* @param container the Container to populate
*/
public static void copyObjectToContainer(Object object,
Container container) {
List<Field> fieldList = getInputFields(container);
copyObjectToContainer(object, container, fieldList);
}
/**
* Find and return the first control with a matching name in the specified
* container.
* <p/>
* If no matching control is found in the specified container, child
* containers will be recursively scanned for a match.
*
* @param container the container that is searched for a control with a
* matching name
* @param name the name of the control to find
* @return the control which name matched the given name
*/
public static Control findControlByName(Container container, String name) {
Control control = container.getControl(name);
if (control != null) {
return control;
} else {
for (Control childControl : container.getControls()) {
if (childControl instanceof Container) {
Container childContainer = (Container) childControl;
Control found = findControlByName(childContainer, name);
if (found != null) {
return found;
}
}
}
}
return null;
}
/**
* Find and return the specified controls parent Form or null
* if no Form is present.
*
* @param control the control to check for Form
* @return the controls parent Form or null if no parent is a Form
*/
public static Form findForm(Control control) {
while (control.getParent() != null && !(control.getParent() instanceof Page)) {
control = (Control) control.getParent();
if (control instanceof Form) {
return (Form) control;
}
}
return null;
}
/**
* Return the list of Buttons for the given Container, recursively including
* any Fields contained in child containers.
*
* @param container the container to obtain the buttons from
* @return the list of contained buttons
*/
public static List<Button> getButtons(Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
List<Button> buttons = new ArrayList<Button>();
addButtons(container, buttons);
return buttons;
}
/**
* Return a list of container fields which are not valid, not hidden and not
* disabled.
* <p/>
* The list of returned fields will exclude any <tt>Button</tt> fields.
*
* @param container the container to obtain the invalid fields from
* @return list of container fields which are not valid, not hidden and not
* disabled
*/
public static List<Field> getErrorFields(Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
List<Field> fields = new ArrayList<Field>();
addErrorFields(container, fields);
return fields;
}
/**
* Return a map of all Fields for the given Container, recursively including
* any Fields contained in child containers.
* <p/>
* The map's key / value pair will consist of the control name and instance.
*
* @param container the container to obtain the fields from
* @return the map of contained fields
*/
public static Map<String, Field> getFieldMap(Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
Map<String, Field> fields = new HashMap<String, Field>();
addFields(container, fields);
return fields;
}
/**
* Return the list of Fields for the given Container, recursively including
* any Fields contained in child containers.
*
* @param container the container to obtain the fields from
* @return the list of contained fields
*/
public static List<Field> getFields(Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
List<Field> fields = new ArrayList<Field>();
addFields(container, fields);
return fields;
}
/**
* Return the list of Fields for the given Container, recursively including
* any Fields contained in child containers. The list of returned fields
* will exclude any <tt>Button</tt> and <tt>FieldSet</tt> fields.
*
* @param container the container to obtain the fields from
* @return the list of contained fields
*/
public static List<Field> getFieldsAndLabels(Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
List<Field> fields = new ArrayList<Field>();
addFieldsAndLabels(container, fields);
return fields;
}
/**
* Return the list of hidden Fields for the given Container, recursively including
* any Fields contained in child containers. The list of returned fields
* will exclude any <tt>Button</tt>, <tt>FieldSet</tt> and <tt>Label</tt>
* fields.
*
* @param container the container to obtain the fields from
* @return the list of contained fields
*/
public static List<Field> getHiddenFields(final Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
List<Field> fields = new ArrayList<Field>();
addHiddenFields(container, fields);
return fields;
}
/**
* Return the list of input Fields (TextField, Select, Radio, Checkbox etc).
* for the given Container, recursively including any Fields contained in
* child containers. The list of returned fields will exclude any
* <tt>Button</tt>, <tt>FieldSet</tt> and <tt>Label</tt> fields.
*
* @param container the container to obtain the fields from
* @return the list of contained fields
*/
public static List<Field> getInputFields(final Container container) {
if (container == null) {
throw new IllegalArgumentException("Null container parameter");
}
List<Field> fields = new ArrayList<Field>();
addInputFields(container, fields);
return fields;
}
/**
* Add the given control to the container at the specified index, and return
* the added instance.
* <p/>
* <b>Please note</b>: an exception is raised if the container contains a
* control with the same name as the given control. It is the responsibility
* of the caller to replace existing controls.
* <p/>
* <b>Also note</b> if the specified control already has a parent assigned,
* it will automatically be removed from that parent and inserted as a child
* of the container instead.
* <p/>
* This method is useful for developers needing to implement the
* {@link org.apache.click.control.Container} interface but cannot for one
* reason or another extend from {@link org.apache.click.control.AbstractContainer}.
* For example if the Container already extends from an existing <tt>Control</tt>
* such as a <tt>Field</tt>, it won't be possible to extend
* <tt>AbstractContainer</tt> as well. In such scenarios instead of
* reimplementing {@link org.apache.click.control.Container#insert(org.apache.click.Control, int) insert},
* one can delegate to this method.
* <p/>
* For example, a custom Container that extends <tt>Field</tt> and
* implements <tt>Container</tt> could implement the <tt>insert</tt> method
* as follows:
* <pre class="prettyprint">
* public class MyContainer extends Field implements Container {
*
* public Control insert(Control control, int index) {
* return ContainerUtils.insert(this, control, index, getControlMap());
* }
*
* ...
* } </pre>
*
* @param container the container to insert the given control into
* @param control the control to add to the container
* @param index the index at which the control is to be inserted
* @param controlMap the container's map of controls keyed on control name
* @return the control that was added to the container
*
* @throws IllegalArgumentException if the control is null or if the control
* and container is the same instance
*
* @throws IndexOutOfBoundsException if index is out of range
* <tt>(index &lt; 0 || index &gt; container.getControls().size())</tt>
*/
public static Control insert(Container container, Control control, int index,
Map<String, Control> controlMap) {
// Pre conditions start
if (control == null) {
throw new IllegalArgumentException("Null control parameter");
}
if (control == container) {
throw new IllegalArgumentException("Cannot add container to itself");
}
int size = container.getControls().size();
if (index > size || index < 0) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: "
+ size);
}
// Check if container already contains the control
if (controlMap.containsKey(control.getName())
&& !(control instanceof Label)) {
throw new IllegalArgumentException(
"Container already contains control named: " + control.getName());
}
// Pre conditions end
// Check if control already has parent
// If parent references the given container, there is no need to remove it
Object currentParent = control.getParent();
if (currentParent != null && currentParent != container) {
// Remove control from parent Page or Container
if (currentParent instanceof Page) {
((Page) currentParent).removeControl(control);
} else if (currentParent instanceof Container) {
((Container) currentParent).remove(control);
}
// Create warning message to users that the parent has been reset
logParentReset(container, control, currentParent);
}
// Note: set parent first since setParent might veto further processing
control.setParent(container);
container.getControls().add(index, control);
String controlName = control.getName();
if (controlName != null) {
controlMap.put(controlName, control);
}
return control;
}
/**
* Replace the current control in the container at the specified index, and
* return the newly added control.
* <p/>
* <b>Please note</b> if the new control already has a parent assigned,
* it will automatically be removed from that parent and inserted as a child
* of the container instead.
* <p/>
* This method is useful for developers needing to implement the
* {@link org.apache.click.control.Container} interface but cannot for one
* reason or another extend from {@link org.apache.click.control.AbstractContainer}.
* For example if the Container already extends from an existing <tt>Control</tt>
* such as a <tt>Field</tt>, it won't be possible to extend
* <tt>AbstractContainer</tt> as well. In such scenarios instead of
* reimplementing {@link org.apache.click.control.Container#replace(org.apache.click.Control, org.apache.click.Control) replace},
* one can delegate to this method.
* <p/>
* For example, a custom Container that extends <tt>Field</tt> and
* implements <tt>Container</tt> could implement the <tt>replace</tt> method
* as follows:
*
* <pre class="prettyprint">
* public class MyContainer extends Field implements Container {
*
* public Control replace(Control currentControl, Control newControl) {
* int controlIndex = getControls().indexOf(currentControl);
* return ContainerUtils.replace(this, currentControl, newControl,
* controlIndex, getControlMap());
* }
*
* ...
* } </pre>
*
* @param container the container to insert the new control into
* @param currentControl the control currently contained in the container
* @param newControl the control to replace the current control contained in
* the container
* @param controlIndex the index of the current control in the container
* @param controlMap the container's map of controls keyed on control name
* @return the new control that replaced the current control
*
* @deprecated this method was used for stateful pages, which have been deprecated
*
* @throws IllegalArgumentException if the currentControl or newControl is
* null
* @throws IllegalStateException if the controlIndex = -1
*/
public static Control replace(Container container, Control currentControl,
Control newControl, int controlIndex, Map<String, Control> controlMap) {
// Pre conditions start
// Current and new control is the same instance - exit early
if (currentControl == newControl) {
return newControl;
}
if (currentControl == null) {
throw new IllegalArgumentException("Null current control parameter");
}
if (newControl == null) {
throw new IllegalArgumentException("Null new control parameter");
}
if (controlIndex == -1) {
throw new IllegalStateException("Cannot replace the given control"
+ " because it is not present in the container");
}
// Pre conditions end
// Check if control already has parent
// If parent references the given container, there is no need to remove it
Object currentParent = newControl.getParent();
if (currentParent != null && currentParent != container) {
// Remove new control from parent Page or Container
if (currentParent instanceof Page) {
((Page) currentParent).removeControl(newControl);
} else if (currentParent instanceof Container) {
((Container) currentParent).remove(newControl);
}
// Create warning message to users that the parent has been reset
logParentReset(container, newControl, currentParent);
}
// Note: set parent first since setParent might veto further processing
newControl.setParent(container);
currentControl.setParent(null);
// Replace currentControl with newControl
container.getControls().set(controlIndex, newControl);
// Update controlMap
String controlName = newControl.getName();
if (controlName != null) {
controlMap.put(controlName, newControl);
} else {
controlName = currentControl.getName();
if (controlName != null) {
controlMap.remove(controlName);
}
}
return newControl;
}
/**
* Remove the given control from the container, returning <tt>true</tt> if
* the control was found in the container and removed, or <tt>false</tt> if
* the control was not found.
* <p/>
* This method is useful for developers needing to implement the
* {@link org.apache.click.control.Container} interface but cannot for one
* reason or another extend from {@link org.apache.click.control.AbstractContainer}.
* For example if the Container already extends from an existing <tt>Control</tt>
* such as a <tt>Field</tt>, it won't be possible to extend
* <tt>AbstractContainer</tt> as well. In such scenarios instead of
* reimplementing {@link org.apache.click.control.Container#remove(org.apache.click.Control) remove},
* one can delegate to this method.
* <p/>
* For example, a custom Container that extends <tt>Field</tt> and
* implements <tt>Container</tt> could implement the <tt>remove</tt> method
* as follows:
* <pre class="prettyprint">
* public class MyContainer extends Field implements Container {
*
* public boolean remove (Control control) {
* return ContainerUtils.remove(this, control, getControlMap());
* }
*
* ...
* } </pre>
*
* @param container the container to remove the given control from
* @param control the control to remove from the container
* @param controlMap the container's map of controls keyed on control name
*
* @return true if the control was removed from the container
* @throws IllegalArgumentException if the control is null
*/
public static boolean remove(Container container, Control control,
Map<String, Control> controlMap) {
if (control == null) {
throw new IllegalArgumentException("Control cannot be null");
}
boolean contains = container.getControls().remove(control);
if (contains) {
// Only nullify if the container is parent. This check is for the
// case where a Control has two parents e.g. Page and Form.
// NOTE the current #insert logic does not allow Controls to have
// two parents so this check might be redundant.
if (control.getParent() == container) {
control.setParent(null);
}
String controlName = control.getName();
if (controlName != null) {
controlMap.remove(controlName);
}
}
return contains;
}
// -------------------------------------------------------- Private Methods
/**
* Extract and return the specified object property names.
* <p/>
* If the object is a Map instance, this method returns the maps key set.
*
* @param object the object to extract property names from
* @return the unique set of property names
*/
private static Set<String> getObjectPropertyNames(Object object) {
if (object instanceof Map) {
return ((Map) object).keySet();
}
Set<String> hashSet = new TreeSet<String>();
Method[] methods = object.getClass().getMethods();
for (Method method : methods) {
String methodName = method.getName();
if (methodName.startsWith("get") && methodName.length() > 3) {
String propertyName =
Character.toLowerCase(methodName.charAt(3))
+ methodName.substring(4);
hashSet.add(propertyName);
}
if (methodName.startsWith("is") && methodName.length() > 2) {
String propertyName =
Character.toLowerCase(methodName.charAt(2))
+ methodName.substring(3);
hashSet.add(propertyName);
}
if (methodName.startsWith("set") && methodName.length() > 3) {
String propertyName =
Character.toLowerCase(methodName.charAt(3))
+ methodName.substring(4);
hashSet.add(propertyName);
}
}
return hashSet;
}
/**
* Return true if the specified field name is contained within the
* specified set of properties.
*
* @param field the field which name should be checked
* @param properties set of properties to check
* @return true if the specified field name is contained in the properties,
* false otherwise
*/
private static boolean hasMatchingProperty(Field field, Set<String> properties) {
String fieldName = field.getName();
if (fieldName.contains(".")) {
fieldName = fieldName.substring(0, fieldName.indexOf("."));
}
return properties.contains(fieldName);
}
/**
* This method ensures that the object can safely be navigated according
* to the specified path.
* <p/>
* If any object in the graph is null, a new instance of that object class
* is instantiated.
*
* @param object the object which path must be navigable without
* encountering null values
* @param path the navigation path
*/
private static void ensureObjectPathNotNull(Object object, String path) {
final int index = path.indexOf('.');
if (index == -1) {
return;
}
String property = path.substring(0, index);
Method getterMethod = findGetter(object, property, path);
Object result = invokeGetter(getterMethod, object, property, path);
if (result == null) {
// Find the target class of the object in the path to create
Class<?> targetClass = getterMethod.getReturnType();
Constructor<?> constructor = null;
try {
// Lookup default no-arg constructor
constructor = targetClass.getConstructor((Class[]) null);
} catch (NoSuchMethodException e) {
// Log detailed error message of looking up constructor failed
HtmlStringBuffer buffer = new HtmlStringBuffer();
logBasicDescription(buffer, object, path, property);
buffer.append("Attempt to construct instance of class '");
buffer.append(targetClass.getName()).append("' resulted in error: '");
buffer.append(targetClass.getName()).append("' does not seem");
buffer.append(" to have a default no argument constructor.");
buffer.append(" Please note another common problem is that the");
buffer.append(" class is either not public or not static.");
throw new RuntimeException(buffer.toString(), e);
}
try {
// Create target object instance
result = constructor.newInstance(new Object[]{});
} catch (Exception e) {
// Log detailed error message of why creating target failed
HtmlStringBuffer buffer = new HtmlStringBuffer();
logBasicDescription(buffer, object, path, property);
buffer.append("Result: could not create");
buffer.append(" object with constructor '");
buffer.append(constructor.getName()).append("'.");
throw new RuntimeException(buffer.toString(), e);
}
Method setterMethod = findSetter(object, property, targetClass, path);
invokeSetter(setterMethod, object, result, property, path);
}
String remainingPath = path.substring(index + 1);
ensureObjectPathNotNull(result, remainingPath);
}
/**
* Find the object getter method for the given property.
* <p/>
* If this method cannot find a 'get' property it tries to lookup an 'is'
* property.
*
* @param object the object to find the getter method on
* @param property the getter property name specifying the getter to lookup
* @param path the full expression path (used for logging purposes)
* @return the getter method
*/
private static Method findGetter(Object object, String property,
String path) {
// Find the getter for property
String getterName = ClickUtils.toGetterName(property);
Method method = null;
Class<?> sourceClass = object.getClass();
try {
method = sourceClass.getMethod(getterName, (Class[]) null);
} catch (Exception e) {
/* ignore */
}
if (method == null) {
String isGetterName = ClickUtils.toIsGetterName(property);
try {
method = sourceClass.getMethod(isGetterName, (Class[]) null);
} catch (Exception e) {
HtmlStringBuffer buffer = new HtmlStringBuffer();
logBasicDescription(buffer, object, path, property);
buffer.append("Result: neither getter methods '");
buffer.append(getterName).append("()' nor '");
buffer.append(isGetterName).append("()' was found on class: '");
buffer.append(object.getClass().getName()).append("'.");
throw new RuntimeException(buffer.toString(), e);
}
}
return method;
}
/**
* Invoke the getterMethod for the given source object.
*
* @param getterMethod the getter method to invoke
* @param source the source object to invoke the getter method on
* @param property the getter method property name (used for logging)
* @param path the full expression path (used for logging)
* @return the getter result
*/
private static Object invokeGetter(Method getterMethod, Object source,
String property, String path) {
try {
// Retrieve target object from getter
return getterMethod.invoke(source, new Object[0]);
} catch (Exception e) {
// Log detailed error message of why getter failed
HtmlStringBuffer buffer = new HtmlStringBuffer();
logBasicDescription(buffer, source, path, property);
buffer.append("Result: error occurred while trying to get");
buffer.append(" instance of '");
buffer.append(getterMethod.getReturnType().getName());
buffer.append("' using method: '");
buffer.append(getterMethod.getName()).append("()' of class '");
buffer.append(source.getClass().getName()).append("'.");
throw new RuntimeException(buffer.toString(), e);
}
}
/**
* Find the source object setter method for the given property.
*
* @param source the source object to find the setter method on
* @param property the property which setter needs to be looked up
* @param targetClass the setter parameter type
* @param path the full expression path (used for logging purposes)
* @return the setter method
*/
private static Method findSetter(Object source,
String property, Class<?> targetClass, String path) {
Method method = null;
// Find the setter for property
String setterName = ClickUtils.toSetterName(property);
Class<?> sourceClass = source.getClass();
Class<?>[] classArgs = { targetClass };
try {
method = sourceClass.getMethod(setterName, classArgs);
} catch (Exception e) {
// Log detailed error message of why setter lookup failed
HtmlStringBuffer buffer = new HtmlStringBuffer();
logBasicDescription(buffer, source, path, property);
buffer.append("Result: setter method '");
buffer.append(setterName).append("(").append(targetClass.getName());
buffer.append(")' was not found on class '");
buffer.append(source.getClass().getName()).append("'.");
throw new RuntimeException(buffer.toString(), e);
}
return method;
}
/**
* Invoke the setter method for the given source and target object.
*
* @param setterMethod the setter method to invoke
* @param source the source object to invoke the setter method on
* @param target the target object to set
* @param property the setter method property name (used for logging)
* @param path the full expression path (used for logging)
*/
private static void invokeSetter(Method setterMethod, Object source,
Object target, String property, String path) {
try {
Object[] objectArgs = {target};
setterMethod.invoke(source, objectArgs);
} catch (Exception e) {
// Log detailed error message of why setter failed
HtmlStringBuffer buffer = new HtmlStringBuffer();
logBasicDescription(buffer, source, path, property);
buffer.append("Result: error occurred while trying to set an");
buffer.append(" instance of '");
buffer.append(target.getClass().getName()).append("' using method '");
buffer.append(setterMethod.getName()).append("(");
buffer.append(target.getClass());
buffer.append(")' of class '").append(source.getClass()).append("'.");
throw new RuntimeException(buffer.toString(), e);
}
}
/**
* Log a generic error message to the specified buffer for the given object,
* path and property.
*
* @param buffer the buffer to append log message to
* @param object the active object when the exception occurred
* @param path the current expression path
* @param property the current property being processed
*/
private static void logBasicDescription(HtmlStringBuffer buffer, Object object,
String path, String property) {
buffer.append("Invoked ensureObjectPathNotNull");
buffer.append(" for class: '").append(object.getClass().getName());
buffer.append("', path: '").append(path).append("' and property: '");
buffer.append(property).append("'. ");
}
/**
* Populate the given map from the values of the specified fieldList. The
* map's key/value pairs are populated from the fields name/value. The keys
* of the map are matched against each field name. If a key matches a field
* name, the value of the field will be copied to the map.
*
* @param fieldList the forms list of fields to obtain field values from
* @param map the map to populate with field values
*/
private static void copyFieldsToMap(List<Field> fieldList, Map<String, Object> map) {
LogService logService = ClickUtils.getLogService();
String objectClassname = map.getClass().getName();
objectClassname =
objectClassname.substring(objectClassname.lastIndexOf(".") + 1);
for (Field field : fieldList) {
// Check if the map contains the fields name. The fields name can
// also be a path for example 'foo.bar'
String fieldName = field.getName();
if (map.containsKey(fieldName)) {
map.put(fieldName, field.getValueObject());
if (logService.isDebugEnabled()) {
String msg = " Form -> " + objectClassname + "."
+ fieldName + " : " + field.getValueObject();
logService.debug(msg);
}
}
}
}
/**
* Copy the map values to the specified fieldList. For every field in the
* field list, a lookup is done in the map for a matching value. A match is
* found if a field name matches against a key in the map. The matching
* value is then copied to the field.
*
* @param map the map containing values to populate the fields with
* @param fieldList the forms list of fields to be populated
*/
private static void copyMapToFields(Map<String, Object> map, List<Field> fieldList) {
LogService logService = ClickUtils.getLogService();
String objectClassname = map.getClass().getName();
objectClassname =
objectClassname.substring(objectClassname.lastIndexOf(".") + 1);
for (Field field : fieldList) {
String fieldName = field.getName();
// Check if the fieldName is contained in the map. For
// example if a field has the name 'user.address', check if
// 'user.address' is contained in the map.
if (map.containsKey(fieldName)) {
Object result = map.get(fieldName);
field.setValueObject(result);
if (logService.isDebugEnabled()) {
String msg = " Form <- " + objectClassname + "."
+ fieldName + " : " + result;
logService.debug(msg);
}
}
}
}
/**
* Add buttons for the given Container to the specified buttons list,
* recursively including any Fields contained in child containers. The list
* of returned buttons will exclude any <tt>Button</tt> or <tt>Label</tt>
* fields.
*
* @param container the container to obtain the fields from
* @param buttons the list of contained fields
*/
private static void addButtons(final Container container, final List<Button> buttons) {
for (Control control : container.getControls()) {
if (control instanceof Container) {
// Include buttons that are containers
if (control instanceof Button) {
buttons.add((Button) control);
}
Container childContainer = (Container) control;
addButtons(childContainer, buttons);
} else if (control instanceof Button) {
buttons.add((Button) control);
}
}
}
/**
* Add fields for the given Container to the specified field list,
* recursively including any Fields contained in child containers.
*
* @param container the container to obtain the fields from
* @param fields the list of contained fields
*/
private static void addFields(final Container container, final List<Field> fields) {
for (Control control : container.getControls()) {
if (control instanceof Container) {
// Include fields that are containers
if (control instanceof Field) {
fields.add((Field) control);
}
Container childContainer = (Container) control;
addFields(childContainer, fields);
} else if (control instanceof Field) {
fields.add((Field) control);
}
}
}
/**
* Add input fields (TextField, TextArea, Select, Radio, Checkbox etc.) for
* the given Container to the specified field list, recursively including
* any Fields contained in child containers. The list of returned fields
* will exclude any <tt>Button</tt>, <tt>FieldSet</tt> and <tt>Label</tt>
* fields.
*
* @param container the container to obtain the fields from
* @param fields the list of contained fields
*/
private static void addInputFields(final Container container, final List<Field> fields) {
for (Control control : container.getControls()) {
if (control instanceof Label || control instanceof Button) {
// Skip buttons and labels
continue;
} else if (control instanceof Container) {
// Include fields but skip fieldSets
if (control instanceof Field && !(control instanceof FieldSet)) {
fields.add((Field) control);
}
Container childContainer = (Container) control;
addInputFields(childContainer, fields);
} else if (control instanceof Field) {
fields.add((Field) control);
}
}
}
/**
* Add hidden fields for the given Container to the specified field list,
* recursively including any Fields contained in child containers. The list
* of returned fields will exclude any <tt>Button</tt>, <tt>FieldSet</tt>
* and <tt>Label</tt> fields.
*
* @param container the container to obtain the hidden fields from
* @param fields the list of contained fields
*/
private static void addHiddenFields(final Container container, final List<Field> fields) {
for (Control control : container.getControls()) {
if (control instanceof Label || control instanceof Button) {
// Skip buttons and labels
continue;
} else if (control instanceof Container) {
// Include fields but skip fieldSets
if (control instanceof Field && !(control instanceof FieldSet)) {
Field field = (Field) control;
if (field.isHidden()) {
fields.add((Field) control);
}
}
Container childContainer = (Container) control;
addHiddenFields(childContainer, fields);
} else if (control instanceof Field) {
Field field = (Field) control;
if (field.isHidden()) {
fields.add((Field) control);
}
}
}
}
/**
* Add fields for the container to the specified field list, recursively
* including any Fields contained in child containers. The list
* of returned fields will exclude any <tt>Button</tt> and <tt>FieldSet</tt>
* fields.
*
* @param container the container to obtain the fields from
* @param fields the list of contained fields
*/
private static void addFieldsAndLabels(final Container container, final List<Field> fields) {
for (Control control : container.getControls()) {
if (control instanceof Button) {
// Skip buttons
continue;
} else if (control instanceof Container) {
// Include fields but skip fieldSets
if (control instanceof Field && !(control instanceof FieldSet)) {
fields.add((Field) control);
}
Container childContainer = (Container) control;
addFieldsAndLabels(childContainer, fields);
} else if (control instanceof Field) {
fields.add((Field) control);
}
}
}
/**
* Add all the Fields for the given Container to the specified map,
* recursively including any Fields contained in child containers.
* <p/>
* The map's key / value pair will consist of the control name and instance.
*
* @param container the container to obtain the fields from
* @param fields the map of contained fields
*/
private static void addFields(final Container container, final Map<String, Field> fields) {
for (Control control : container.getControls()) {
if (control instanceof Container) {
// Include fields that are containers
if (control instanceof Field) {
fields.put(control.getName(), (Field) control);
}
Container childContainer = (Container) control;
addFields(childContainer, fields);
} else if (control instanceof Field) {
fields.put(control.getName(), (Field) control);
}
}
}
/**
* Add the list of container fields to the specified list of fields, which
* are not valid, not hidden and not disabled.
* <p/>
* The list of returned invalid fields will exclude any <tt>Button</tt>
* fields.
*
* @param container the container to obtain the fields from
* @param fields the map of contained fields
*/
private static void addErrorFields(final Container container, final List<Field> fields) {
for (Control control : container.getControls()) {
if (control instanceof Button) {
// Skip buttons
continue;
} else if (control instanceof Container) {
if (control instanceof Field) {
Field field = (Field) control;
if (!field.isValid()
&& !field.isHidden()
&& !field.isDisabled()) {
fields.add((Field) control);
}
}
Container childContainer = (Container) control;
addErrorFields(childContainer, fields);
} else if (control instanceof Field) {
Field field = (Field) control;
if (!field.isValid()
&& !field.isHidden()
&& !field.isDisabled()) {
fields.add((Field) control);
}
}
}
}
/**
* Log a warning that the parent of the given control will be set to
* the specified container.
*
* @param container the parent container
* @param control the control which parent is being reset
* @param currentParent the control current parent
*/
private static void logParentReset(Container container, Control control,
Object currentParent) {
HtmlStringBuffer message = new HtmlStringBuffer();
message.append("Changed ");
message.append(ClassUtils.getShortClassName(control.getClass()));
String controlId = control.getId();
if (controlId != null) {
message.append("[");
message.append(controlId);
message.append("]");
} else {
message.append("#");
message.append(control.hashCode());
}
message.append(" parent from ");
if (currentParent instanceof Page) {
message.append(ClassUtils.getShortClassName(currentParent.getClass()));
} else if (currentParent instanceof Container) {
Container parentContainer = (Container) currentParent;
message.append(ClassUtils.getShortClassName(parentContainer.getClass()));
String parentId = parentContainer.getId();
if (parentId != null) {
message.append("[");
message.append(parentId);
message.append("]");
} else {
message.append("#");
message.append(parentContainer.hashCode());
}
}
message.append(" to ");
message.append(ClassUtils.getShortClassName(container.getClass()));
String id = container.getId();
if (id != null) {
message.append("[");
message.append(id);
message.append("]");
} else {
message.append("#");
message.append(container.hashCode());
}
ClickUtils.getLogService().warn(message);
}
private static PropertyService getPropertyService() {
ServletContext sc = Context.getThreadLocalContext().getServletContext();
ConfigService configService = ClickUtils.getConfigService(sc);
return configService.getPropertyService();
}
}