/* | |
* 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 < 0 || index > 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(); | |
} | |
} |