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