blob: 6bb462940efbebadf65f98cad1078c0676720353 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2008 Oracle Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Oracle Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jst.jsf.common.runtime.internal.model.component;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.eclipse.jst.jsf.common.runtime.internal.model.ViewObject;
import org.eclipse.jst.jsf.common.runtime.internal.model.component.AbstractVisitor.VisitationPolicy;
import org.eclipse.jst.jsf.common.runtime.internal.model.decorator.FacetDecorator;
/**
* Models a basic UI component instance
*
* TODO: should implement a visitor pattern to traverse component trees
*
* @author cbateman
*
*/
public class ComponentInfo extends ViewObject implements Serializable,
IVisitable
{
/**
* serializable id
*/
private static final long serialVersionUID = 2517204356825585699L;
private final static int DEFAULT_ARRAY_SIZE = 4;
private transient BeanPropertyManager _beanPropertyManager;
/**
* Encapsulates all of the data for the view object
*/
protected final ComponentInfoData _data;
// initialized
// by
// getBeanProperties
/**
* @param id
* @param parent
* @param componentTypeInfo
* @param isRendered
*/
protected ComponentInfo(final String id, final ComponentInfo parent,
final ComponentTypeInfo componentTypeInfo, final boolean isRendered)
{
super(new ComponentInfoData(id, parent, componentTypeInfo, isRendered));
_data = (ComponentInfoData) super.getData();
final Set propExclude = new HashSet();
propExclude.add("attributeNames"); //$NON-NLS-1$
propExclude.add("componentTypeInfo"); //$NON-NLS-1$
propExclude.add("valueChangeListeners"); //$NON-NLS-1$
propExclude.add("visitableChildren"); //$NON-NLS-1$
_beanPropertyManager = new BeanPropertyManager(this, propExclude);
}
/**
* @param data
*/
protected ComponentInfo(final ComponentInfoData data)
{
super(data);
_data = data;
}
/**
* Construct a new component info using the attributes keyed by name in
* attributes to set values. The names must match the corresponding bean
* property names. Primitives should be wrapped in their corresponding
* object types. Exceptions will be thrown if there is a type mismatch on an
* expected type. Number will be used for all numeric primitive wrappers an
* the corresponding "to" will be called.
*
* @param parent
* @param componentTypeInfo
* @param attributes
* @throws ClassCastException
* if an attribute's value doesn't match the expected type
* @throws NullPointerException
* if an attribute value is null for a value whose type is
* expected to be primitive
* @throws IllegalArgumentException
* if attributes does not contain a required key.
*/
protected ComponentInfo(final ComponentInfo parent,
final ComponentTypeInfo componentTypeInfo, final Map attributes)
{
this(getStringProperty("id", attributes, false), parent, //$NON-NLS-1$
componentTypeInfo, getBooleanProperty("rendered", attributes, false)); //$NON-NLS-1$
}
/**
* @param key
* @param attributes
* @param mandatory
* @return the value in attributes at location key, forcing a
* ClassCastException if it turns out not to be a String.
* @throws ClassCastException
* if the attribute for key is not a String
* @throws IllegalArgumentException
* if the attribute for key is null but mandatory is true.
*/
protected static String getStringProperty(final String key,
final Map attributes, final boolean mandatory)
{
final Object value = attributes.get(key);
if (mandatory && value == null)
{
throw new IllegalArgumentException(key
+ " is a mandatory attribute"); //$NON-NLS-1$
}
return (String) value;
}
/**
* @param key
* @param attributes
* @param mandatory
*
* @return the value in attributes at location, forcing a ClassCastExceptio
* if it is not a Boolean and mandatory. returns false if no value
* and not mandatory
* @throws IllegalArgumentException
* if key is not found and value is mandatory
*/
protected static boolean getBooleanProperty(final String key,
final Map attributes, final boolean mandatory)
{
final Boolean value = (Boolean) attributes.get(key);
if (value == null)
{
if (mandatory)
{
throw new IllegalArgumentException(key + "is mandatory"); //$NON-NLS-1$
}
return false;
}
return value.booleanValue();
}
/**
* @param key
* @param attributes
* @return the integer property for key. Casts the value to Number and calls
* Number.intValue(). 0 if no value.
*/
protected static int getIntegerProperty(final String key,
final Map attributes)
{
final Number value = (Number) attributes.get(key);
if (value == null)
{
return 0;
}
return value.intValue();
}
/**
* @param key
* @param attributes
* @return the component info value from attributes
*/
protected static ComponentInfo getComponentProperty(final String key,
final Map attributes)
{
return (ComponentInfo) attributes.get(key);
}
/**
* @return the id
*/
public final String getId()
{
return _data.getId();
}
/**
* @return the component type info
*/
public final ComponentTypeInfo getComponentTypeInfo()
{
return _data.getComponentTypeInfo();
}
/**
* Pre-condition: isModifiable() == true Post-condition: getChildren() will
* return an empty list.
*/
protected final void clearChildren()
{
_data.getChildren().clear();
}
/**
* @return the children. List is unmodifiable. List contains all children
* including facets.
*/
public final List/* <ComponentInfo> */getChildren()
{
if (_data.isProtected())
{
return _data.getChildren();
}
return Collections.unmodifiableList(_data.getChildren());
}
/**
* Get the sub-set of {@link #getChildren()} that are facets. This is a
* convenience method for {@link #getDecorators(Class)}
*
* @return all component children that are facets
*/
public final List getFacets()
{
return getDecorators(ComponentFactory.FACET);
}
/**
* @param childComponent
*/
public final void addChild(final ComponentInfo childComponent)
{
if (childComponent == this)
{
throw new IllegalArgumentException(
"A component cannot be its own child"); //$NON-NLS-1$
}
_data.addChild(childComponent);
// we need to reset the child's parent to me
childComponent.setParent(this);
}
/**
* @param parent
*/
public final void setParent(ComponentInfo parent)
{
_data.setParent(parent);
}
/**
* @param name
* @param facetComponent
*/
public final void addFacet(final String name,
final ComponentInfo facetComponent)
{
addChild(facetComponent);
addDecorator(new FacetDecorator(name, facetComponent));
}
/**
* @param component
* @return if component corresponds to a facet of this component, returns
* the name of that facet. Returns null if not found.
*/
public final String getFacetName(final ComponentInfo component)
{
if (component == null)
{
return null;
}
final List facets = getDecorators(ComponentFactory.FACET);
for (final Iterator it = facets.iterator(); it.hasNext();)
{
final FacetDecorator facet = (FacetDecorator) it.next();
if (component == facet.getDecorates())
{
return facet.getName();
}
}
// component is not a facet
return null;
}
/**
* @param name
* @return if this has a facet called name, then returns it's single root
* component.
*/
public final ComponentInfo getFacet(final String name)
{
if (name == null)
{
return null;
}
final List facets = getDecorators(ComponentFactory.FACET);
for (final Iterator it = facets.iterator(); it.hasNext();)
{
final FacetDecorator facet = (FacetDecorator) it.next();
if (name.equals(facet.getName()))
{
return facet.getDecorates();
}
}
// not found
return null;
}
public String toString()
{
final String parentId = getParent() != null ? getParent().getId()
: "null"; //$NON-NLS-1$
String toString = getMostSpecificComponentName() + ": id=" //$NON-NLS-1$
+ _data.getId() + ", parentId: " + parentId + ", family=" //$NON-NLS-1$ //$NON-NLS-2$
+ getComponentTypeInfo().getComponentFamily() + ", render=" //$NON-NLS-1$
+ getComponentTypeInfo().getRenderFamily() + ", rendered=" //$NON-NLS-1$
+ isRendered();
// use bean introspection to dump child properties
if (this.getClass() != ComponentInfo.class)
{
toString += dumpProperties();
}
return toString;
}
private String dumpProperties()
{
String properties = ""; //$NON-NLS-1$
try
{
final BeanInfo beanInfo = Introspector.getBeanInfo(this.getClass(),
ComponentInfo.class);
final PropertyDescriptor[] descriptors = beanInfo
.getPropertyDescriptors();
for (int i = 0; i < descriptors.length; i++)
{
final PropertyDescriptor desc = descriptors[i];
final String name = desc.getName();
final Object valueObj = desc.getValue(name);
final String value = valueObj != null ? valueObj.toString()
: "null"; //$NON-NLS-1$
properties += ", " + name + "=" + value; //$NON-NLS-1$ //$NON-NLS-2$
}
}
catch (final IntrospectionException e)
{
return "Error introspecting bean: " + e.getLocalizedMessage(); //$NON-NLS-1$
}
return properties;
}
/**
* @return used for toString. Clients should not use.
*/
protected String getMostSpecificComponentName()
{
return "UIComponent"; //$NON-NLS-1$
}
/**
* @return the parent of this component or null.
*/
public final ComponentInfo getParent()
{
return _data.getParent();
}
/**
* @return the rendered flag
*/
public final boolean isRendered()
{
return _data.isRendered();
}
public synchronized void addAdapter(final Class adapterType,
final Object adapter)
{
super.addAdapter(adapterType, adapter);
// force an update on the next call to getBeanProperties
_beanPropertyManager.reset();
}
public synchronized Object removeAdapter(final Class adapterType)
{
final Object removed = super.removeAdapter(adapterType);
_beanPropertyManager.reset();
return removed;
}
/**
* @return the set of all bean property names for this component. The set is
* unmodifiable and will throw exceptions if modification is
* attempted.
*/
protected final Map/* <String, ComponentBeanProperty> */getBeanProperties()
{
return Collections.unmodifiableMap(_beanPropertyManager
.getBeanProperties());
}
/**
* @author cbateman
*
*/
public static class ComponentInfoData extends ViewObjectData
{
/**
*
*/
private static final long serialVersionUID = 5052732412917986062L;
/**
* the component id
*/
private final String _id;
/**
* the component's parent or null if none
*/
private ComponentInfo _parent;
/**
* the type info for this component
*/
protected final ComponentTypeInfo _componentTypeInfo;
/**
* the rendered flage
*/
protected final boolean _isRendered;
private List /* <ComponentInfo> */_children = new ArrayList(
DEFAULT_ARRAY_SIZE);
/**
* @param id
* @param parent
* @param componentTypeInfo
* @param isRendered
*/
public ComponentInfoData(final String id, ComponentInfo parent,
ComponentTypeInfo componentTypeInfo, boolean isRendered)
{
super(false);
_id = id;
_parent = parent;
_componentTypeInfo = componentTypeInfo;
_isRendered = isRendered;
}
/**
* @param childComponent
*/
protected void addChild(ComponentInfo childComponent)
{
enforceProtection();
getChildren().add(childComponent);
}
/**
* @return the modifiable list of children
*/
protected final List/* <ComponentInfo> */getChildren()
{
return _children;
}
protected void doBeforeProtecting()
{
super.doBeforeProtecting();
// compact the children array list
if (_children.size() > 0)
{
_children = Collections.unmodifiableList(_children);
}
else
{
_children = Collections.EMPTY_LIST;
}
}
/**
* @return the isRendered flag
*/
protected final boolean isRendered()
{
return _isRendered;
}
/**
* @return the component type info flag
*/
protected final ComponentTypeInfo getComponentTypeInfo()
{
return _componentTypeInfo;
}
/**
* @return the parent or null if no parent
*/
protected final ComponentInfo getParent()
{
return _parent;
}
/**
* @param parent
*/
protected final void setParent(ComponentInfo parent)
{
enforceProtection();
_parent = parent;
}
/**
* @return the component id
*/
protected final String getId()
{
return _id;
}
}
/**
* This is similar to the runtime getAttributes().get(name) call. The reason
* we don't implement a Map of all attribute values is that the implicit
* property structure can change at any time due to add/removeAdapter. To
* get all attributes known for a component, instead use:
*
* The synchronized block is advised to protect against concurrent
* modification exceptions on the keySet iterator.
*
* @param name
*
* @return the value of the attribute or null if none.
*
*/
public synchronized ComponentBeanProperty getAttribute(final String name)
{
return (ComponentBeanProperty) getBeanProperties().get(name);
}
/**
* @return the set of valid attribute names. The Set is not modifiable.
*/
public synchronized Set/* <String> */getAttributeNames()
{
return getBeanProperties().keySet();
}
/**
* Stores a bean property descriptor along information about which
* implementation class declares it and what key to pass to getAdapter() in
* order to get it.
*
*/
public final static class ComponentBeanProperty
{
private final PropertyDescriptor _propertyDescriptor;
private final Object _declaringImplementation;
private final Class _adapterKeyClass;
// only instantiable locally
private ComponentBeanProperty(Class adapterKeyClass,
Object declaringImplementationClass,
PropertyDescriptor propertyDescriptor)
{
super();
_adapterKeyClass = adapterKeyClass;
_declaringImplementation = declaringImplementationClass;
_propertyDescriptor = propertyDescriptor;
}
/**
* @return the value of property
*/
public final Object getValue()
{
final Method method = _propertyDescriptor.getReadMethod();
if (method != null)
{
try
{
method.setAccessible(true);
return method.invoke(_declaringImplementation,
new Object[0]);
}
catch (IllegalArgumentException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InvocationTargetException e)
{
e.printStackTrace();
}
}
// if any step fails, return null
return null;
}
/**
* @return the property descriptor
*/
public final PropertyDescriptor getPropertyDescriptor()
{
return _propertyDescriptor;
}
/**
* @return the implemenation
*/
public final Object getDeclaringImplementationClass()
{
return _declaringImplementation;
}
/**
* @return the adapter class for the interface that the declaring
* implementation is providing the impl for
*/
public final Class getAdapterKeyClass()
{
return _adapterKeyClass;
}
}
/**
* Manages bean property information for a component
*
* @author cbateman
*
*/
protected final static class BeanPropertyManager
{
/**
* a map of the bean property names exposed by this component including
* all those added by addAdapter().
*
* this is synthetic based the class definition and installed adapters
* so as long that info is available, no need to serialize.
*/
protected transient Map /*
* <String,
* ComponentBeanProperty>
*/_beanProperties; // lazily
private final transient ComponentInfo _component;
private final transient Set _excludeNames;
/**
* @param component
* @param excludeNames
*/
protected BeanPropertyManager(final ComponentInfo component,
final Set excludeNames)
{
_component = component;
_excludeNames = excludeNames;
}
/**
* Will throw exception of the calling thread already holds the "this"
* monitor lock. This is to ensure that caller always acquires locks in
* appropriate order to prevent deadlock.
*
* @return the internal set of bean properties. This Set may be modified
* internally.
*/
public Map getBeanProperties()
{
if (Thread.holdsLock(this))
{
throw new IllegalStateException(
"Must not already own this lock"); //$NON-NLS-1$
}
// must always acquire component lock first to prevent deadlock
synchronized (_component)
{
synchronized (this)
{
if (_beanProperties == null)
{
_beanProperties = calculateAllBeanPropNames(ViewObject.class);
}
return _beanProperties;
}
}
}
/**
* Will throw exception if the calling thread already holds the "this"
* monitor lock. This is to ensure that caller always acquires locks in
* appropriate order to prevent deadlock.
*
* Clears the internal map and sets to null. This will force it to be
* completely new built on the next call to getBeanProperties
*/
public void reset()
{
if (Thread.holdsLock(this))
{
throw new IllegalStateException(
"Must not already own this lock"); //$NON-NLS-1$
}
// must always acquire component lock first to prevent deadlock
synchronized (_component)
{
synchronized (this)
{
if (_beanProperties != null)
{
_beanProperties.clear();
_beanProperties = null;
}
}
}
}
/**
* @param stopClass
* @return a synchronized map of all bean property names on this class
* up to stopClass, as well as all adapter property names (as
* though this really implemented them).
*/
private Map calculateAllBeanPropNames(final Class stopClass)
{
// use a set to prevents the duplicates
final Map allProperties = new HashMap();
{
final Class myClass = _component.getClass();
final List myProperties = getOrCreateBeanProperties(myClass,
stopClass);
addToMap(myProperties, _component, myClass, allProperties, _excludeNames);
}
{
for (final Iterator it = _component.getAdapterMap().entrySet()
.iterator(); it.hasNext();)
{
Map.Entry entry = (Entry) it.next();
final Class adapterClass = (Class) entry.getKey();
final Object declaringClass = entry.getValue();
// get all props, excluding the ones on Object.
final List props = getOrCreateBeanProperties(adapterClass,
null);
addToMap(props, declaringClass, adapterClass, allProperties,
_excludeNames);
}
}
return Collections.synchronizedMap(allProperties);
}
private static void addToMap(
final List/* <ComponentBeanProperty> */addThese,
final Object declaringObject, final Class declaringAdapter,
final Map toMe,
Set excludeNames)
{
for (final Iterator it = addThese.iterator(); it.hasNext();)
{
final PropertyDescriptor desc = (PropertyDescriptor) it.next();
if (!toMe.containsKey(desc.getName())
&& !excludeNames.contains(desc.getName()))
{
toMe.put(desc.getName(), new ComponentBeanProperty(
declaringAdapter, declaringObject, desc));
}
}
}
/**
* lazily loaded with the local properties (those not defined using
* adapters)
*
* MUST INITIALIZE early so can synchronize on it
*/
private transient static Map /* <Class, List<PropertyDescriptor> */PROPERTY_MAP = new HashMap();
/**
* @param startClass
* @param stopClass
* @return a unmodifiable list of properties starting from startClass.
* stopClass is only used if an entry doesn't already exist in
* PROPERTY_MAP for startClass. The method is synchronized on
* the PROPERTY_MAP it updates.
*/
protected static List/* <PropertyDescriptor */getOrCreateBeanProperties(
final Class startClass, final Class stopClass)
{
synchronized (PROPERTY_MAP)
{
List localBeanProps = (List) PROPERTY_MAP.get(startClass);
if (localBeanProps == null)
{
localBeanProps = calculateBeanProperties(startClass,
stopClass);
PROPERTY_MAP.put(startClass, Collections
.unmodifiableList(localBeanProps));
}
return localBeanProps;
}
}
/**
* @param startClass
* @param stopClass
* @return a List<String> containing all of the bean names between
* startClass and stopClass. Start class must be a descendant
* (sub-class, sub-sub-class etc.) of stopClass. The properties
* on stopClass are excluded from analysis.
*/
private static List/* <PropertyDescriptor> */calculateBeanProperties(
final Class startClass, final Class stopClass)
{
BeanInfo beanInfo;
List names = new ArrayList();
try
{
beanInfo = Introspector.getBeanInfo(startClass, stopClass);
final PropertyDescriptor[] descriptors = beanInfo
.getPropertyDescriptors();
if (descriptors != null)
{
names = Arrays.asList(descriptors);
}
}
catch (final IntrospectionException e)
{
e.printStackTrace();
}
return names;
}
}
/**
* Visits this node and it's entire tree and makes all nodes protected.
*/
public final void setSubtreeProtected()
{
// lock children first
final ComponentTreeVisitor protectionVisitor = new ComponentTreeVisitor(VisitationPolicy.ChildrenFirstPolicy)
{
public void visit(ComponentInfo component)
{
component.setProtected();
}
};
accept(protectionVisitor);
}
public void accept(AbstractVisitor visitor)
{
// check policy ordering
if (visitor.getPolicy().getOrdering() == VisitationPolicy.VISIT_PARENT_FIRST)
{
visitor.visit(this);
visitChildren(visitor);
}
else
{
visitChildren(visitor);
visitor.visit(this);
}
}
private void visitChildren(AbstractVisitor visitor)
{
for (final Iterator it = getVisitableChildren(); it.hasNext();)
{
visitor.visit(it.next());
}
}
public Iterator getVisitableChildren()
{
return getChildren().iterator();
}
}