http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/BeanMap.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/BeanMap.java b/juneau-core/src/main/java/org/apache/juneau/BeanMap.java new file mode 100644 index 0000000..28bee46 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/BeanMap.java @@ -0,0 +1,504 @@ +/*************************************************************************************************************************** + * 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.juneau; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.transform.*; +import org.apache.juneau.xml.annotation.*; + +/** + * Java bean wrapper class. + * + * + * <h6 class='topic'>Description</h6> + * <p> + * A wrapper that wraps Java bean instances inside of a {@link Map} interface that allows + * properties on the wrapped object can be accessed using the {@link Map#get(Object) get()} and {@link Map#put(Object,Object) put()} methods. + * <p> + * Use the {@link BeanContext} class to create instances of this class. + * + * + * <h6 class='topic'>Bean property order</h6> + * <p> + * The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()} methods are as follows: + * <ul class='spaced-list'> + * <li>If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties in the annotation. + * <li>If {@link Bean @Bean} annotation is not specified on the class, then the order is the same as that returned + * by the {@link java.beans.BeanInfo} class (i.e. ordered by definition in the class). + * </ul> + * <br> + * The order can also be overridden through the use of a {@link BeanTransform}. + * + * + * <h6 class='topic'>POJO transforms</h6> + * <p> + * If {@link PojoTransform PojoTransforms} are defined on the class types of the properties of this bean or the bean properties themselves, the + * {@link #get(Object)} and {@link #put(String, Object)} methods will automatically + * transform the property value to and from the serialized form. + * + * @author Barry M. Caceres + * @author James Bognar ([email protected]) + * @param <T> Specifies the type of object that this map encapsulates. + */ +public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T> { + + /** The wrapped object. */ + protected T bean; + + /** Temporary holding cache for beans with read-only properties. Normally null. */ + protected Map<String,Object> propertyCache; + + /** Temporary holding cache for bean properties of array types when the add() method is being used. */ + protected Map<String,List<?>> arrayPropertyCache; + + /** The BeanMeta associated with the class of the object. */ + protected BeanMeta<T> meta; + + /** + * Instance of this class are instantiated through the BeanContext class. + * + * @param bean The bean to wrap inside this map. + * @param meta The metadata associated with the bean class. + */ + protected BeanMap(T bean, BeanMeta<T> meta) { + this.bean = bean; + this.meta = meta; + if (meta.constructorArgs.length > 0) + propertyCache = new TreeMap<String,Object>(); + } + + /** + * Returns the metadata associated with this bean map. + * + * @return The metadata associated with this bean map. + */ + public BeanMeta<T> getMeta() { + return meta; + } + + /** + * Returns the wrapped bean object. + * Triggers bean creation if bean has read-only properties set through a constructor + * defined by the {@link BeanConstructor} annotation. + * + * @return The inner bean object. + */ + public T getBean() { + T b = getBean(true); + + // If we have any arrays that need to be constructed, do it now. + if (arrayPropertyCache != null) { + for (Map.Entry<String,List<?>> e : arrayPropertyCache.entrySet()) { + String key = e.getKey(); + List<?> value = e.getValue(); + BeanPropertyMeta<T> bpm = getPropertyMeta(key); + try { + bpm.setArray(b, value); + } catch (Exception e1) { + throw new RuntimeException(e1); + } + } + arrayPropertyCache = null; + } + return b; + } + + /** + * Returns the wrapped bean object. + * <p> + * If <code>create</code> is <jk>false</jk>, then this method may return <jk>null</jk> + * if the bean has read-only properties set through a constructor + * defined by the {@link BeanConstructor} annotation. + * <p> + * This method does NOT always return the bean in it's final state. + * Array properties temporary stored as ArrayLists are not finalized + * until the {@link #getBean()} method is called. + * + * @param create If bean hasn't been instantiated yet, then instantiate it. + * @return The inner bean object. + */ + public T getBean(boolean create) { + /** If this is a read-only bean, then we need to create it. */ + if (bean == null && create && meta.constructorArgs.length > 0) { + String[] props = meta.constructorArgs; + Constructor<T> c = meta.constructor; + Object[] args = new Object[props.length]; + for (int i = 0; i < props.length; i++) + args[i] = propertyCache.remove(props[i]); + try { + bean = c.newInstance(args); + for (Map.Entry<String,Object> e : propertyCache.entrySet()) + put(e.getKey(), e.getValue()); + propertyCache = null; + } catch (Exception e) { + throw new BeanRuntimeException(e); + } + } + return bean; + } + + /** + * Returns the value of the property identified as the URI property (annotated with {@link BeanProperty#beanUri()} as <jk>true</jk>). + * + * @return The URI value, or <jk>null</jk> if no URI property exists on this bean. + */ + public Object getBeanUri() { + BeanMeta<T> bm = getMeta(); + return bm.hasBeanUriProperty() ? bm.getBeanUriProperty().get(this) : null; + } + + /** + * Sets the bean URI property if the bean has a URI property. + * Ignored otherwise. + * + * @param o The bean URI object. + * @return If the bean context setting {@code beanMapPutReturnsOldValue} is <jk>true</jk>, then the old value of the property is returned. + * Otherwise, this method always returns <jk>null</jk>. + */ + public Object putBeanUri(Object o) { + BeanMeta<T> bm = getMeta(); + return bm.hasBeanUriProperty() ? bm.getBeanUriProperty().set(this, o) : null; + } + + /** + * Sets a property on the bean. + * <p> + * If there is a {@link PojoTransform} associated with this bean property or bean property type class, then + * you must pass in a transformed value. + * For example, if the bean property type class is a {@link Date} and the bean property has the + * {@link org.apache.juneau.transforms.DateTransform.ISO8601DT} transform associated with it through the + * {@link BeanProperty#transform() @BeanProperty.transform()} annotation, the value being passed in must be + * a String containing an ISO8601 date-time string value. + * + * <dl> + * <dt>Example:</dt> + * <dd> + * <p class='bcode'> + * <jc>// Construct a bean with a 'birthDate' Date field</jc> + * Person p = <jk>new</jk> Person(); + * + * <jc>// Create a bean context and add the ISO8601 date-time transform</jc> + * BeanContext beanContext = <jk>new</jk> BeanContext().addTransform(DateTransform.ISO8601DT.<jk>class</jk>); + * + * <jc>// Wrap our bean in a bean map</jc> + * BeanMap<Person> b = beanContext.forBean(p); + * + * <jc>// Set the field</jc> + * myBeanMap.put(<js>"birthDate"</js>, <js>"'1901-03-03T04:05:06-5000'"</js>); + * </p> + * </dd> + * </dl> + * + * @param property The name of the property to set. + * @param value The value to set the property to. + * @return If the bean context setting {@code beanMapPutReturnsOldValue} is <jk>true</jk>, then the old value of the property is returned. + * Otherwise, this method always returns <jk>null</jk>. + * @throws RuntimeException if any of the following occur. + * <ul class='spaced-list'> + * <li>BeanMapEntry does not exist on the underlying object. + * <li>Security settings prevent access to the underlying object setter method. + * <li>An exception occurred inside the setter method. + * </ul> + */ + @Override /* Map */ + public Object put(String property, Object value) { + BeanPropertyMeta<T> p = meta.properties.get(property); + if (p == null) { + if (meta.ctx.ignoreUnknownBeanProperties) + return null; + if (property.equals("<uri>") && meta.uriProperty != null) + return meta.uriProperty.set(this, value); + + // If this bean has subtypes, and we haven't set the subtype yet, + // store the property in a temporary cache until the bean can be instantiated. + // This eliminates the need for requiring that the sub type attribute be provided first. + if (meta.subTypeIdProperty != null) { + if (propertyCache == null) + propertyCache = new TreeMap<String,Object>(); + return propertyCache.put(property, value); + } + + throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property); + } + if (meta.transform != null) + if (meta.transform.writeProperty(this.bean, property, value)) + return null; + return p.set(this, value); + } + + /** + * Add a value to a collection or array property. + * <p> + * As a general rule, adding to arrays is not recommended since the array must be recreate each time + * this method is called. + * + * @param property Property name or child-element name (if {@link Xml#childName()} is specified). + * @param value The value to add to the collection or array. + */ + public void add(String property, Object value) { + BeanPropertyMeta<T> p = meta.properties.get(property); + if (p == null) { + if (meta.ctx.ignoreUnknownBeanProperties) + return; + throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property); + } + p.add(this, value); + } + + + /** + * Gets a property on the bean. + * <p> + * If there is a {@link PojoTransform} associated with this bean property or bean property type class, then + * this method will return the transformed value. + * For example, if the bean property type class is a {@link Date} and the bean property has the + * {@link org.apache.juneau.transforms.DateTransform.ISO8601DT} transform associated with it through the + * {@link BeanProperty#transform() @BeanProperty.transform()} annotation, this method will return a String + * containing an ISO8601 date-time string value. + * + * <dl> + * <dt>Example:</dt> + * <dd> + * <p class='bcode'> + * <jc>// Construct a bean with a 'birthDate' Date field</jc> + * Person p = <jk>new</jk> Person(); + * p.setBirthDate(<jk>new</jk> Date(1, 2, 3, 4, 5, 6)); + * + * <jc>// Create a bean context and add the ISO8601 date-time transform</jc> + * BeanContext beanContext = <jk>new</jk> BeanContext().addTransform(DateTransform.ISO8601DT.<jk>class</jk>); + * + * <jc>// Wrap our bean in a bean map</jc> + * BeanMap<Person> b = beanContext.forBean(p); + * + * <jc>// Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")</jc> + * String s = myBeanMap.get(<js>"birthDate"</js>); + * </p> + * </dd> + * </dl> + * + * @param property The name of the property to get. + * @throws RuntimeException if any of the following occur. + * <ol> + * <li>BeanMapEntry does not exist on the underlying object. + * <li>Security settings prevent access to the underlying object getter method. + * <li>An exception occurred inside the getter method. + * </ol> + */ + @Override /* Map */ + public Object get(Object property) { + BeanPropertyMeta<T> p = meta.properties.get(property); + if (p == null) + return null; + if (meta.transform != null && property != null) + return meta.transform.readProperty(this.bean, property.toString(), p.get(this)); + return p.get(this); + } + + /** + * Convenience method for setting multiple property values by passing in JSON (or other) text. + * <p> + * Typically the input is going to be JSON, although the actual data type + * depends on the default parser specified by the {@link BeanContext#BEAN_defaultParser} property + * value on the config that created the context that created this map. + * + * <dl> + * <dt>Example:</dt> + * <dd> + * <p class='bcode'> + * aPersonBean.load(<js>"{name:'John Smith',age:21}"</js>) + * </p> + * </dd> + * </dl> + * + * @param input The text that will get parsed into a map and then added to this map. + * @return This object (for method chaining). + * @throws ParseException If the input contains a syntax error or is malformed. + */ + public BeanMap<T> load(String input) throws ParseException { + putAll(new ObjectMap(input, this.meta.ctx.defaultParser)); + return this; + } + + /** + * Convenience method for setting multiple property values by passing in a reader. + * + * @param r The text that will get parsed into a map and then added to this map. + * @param p The parser to use to parse the text. + * @return This object (for method chaining). + * @throws ParseException If the input contains a syntax error or is malformed. + * @throws IOException Thrown by <code>Reader</code>. + */ + public BeanMap<T> load(Reader r, ReaderParser p) throws ParseException, IOException { + putAll(new ObjectMap(r, p)); + return this; + } + + /** + * Convenience method for loading this map with the contents of the specified map. + * <p> + * Identical to {@link #putAll(Map)} except as a fluent-style method. + * + * @param entries The map containing the entries to add to this map. + * @return This object (for method chaining). + */ + @SuppressWarnings({"unchecked","rawtypes"}) + public BeanMap<T> load(Map entries) { + putAll(entries); + return this; + } + + /** + * Returns the names of all properties associated with the bean. + * <p> + * The returned set is unmodifiable. + */ + @Override /* Map */ + public Set<String> keySet() { + return meta.properties.keySet(); + } + + /** + * Returns the specified property on this bean map. + * <p> + * Allows you to get and set an individual property on a bean without having a + * handle to the bean itself by using the {@link BeanMapEntry#getValue()} + * and {@link BeanMapEntry#setValue(Object)} methods. + * <p> + * This method can also be used to get metadata on a property by + * calling the {@link BeanMapEntry#getMeta()} method. + * + * @param propertyName The name of the property to look up. + * @return The bean property, or null if the bean has no such property. + */ + public BeanMapEntry<T> getProperty(String propertyName) { + BeanPropertyMeta<T> p = meta.properties.get(propertyName); + if (p == null) + return null; + return new BeanMapEntry<T>(this, p); + } + + /** + * Returns the metadata on the specified property. + * + * @param propertyName The name of the bean property. + * @return Metadata on the specified property, or <jk>null</jk> if that property does not exist. + */ + public BeanPropertyMeta<T> getPropertyMeta(String propertyName) { + return meta.properties.get(propertyName); + } + + /** + * Returns the {@link ClassMeta} of the wrapped bean. + * + * @return The class type of the wrapped bean. + */ + @Override /* Delagate */ + public ClassMeta<T> getClassMeta() { + return this.meta.getClassMeta(); + } + + /** + * Invokes all the getters on this bean and return the values as a list of {@link BeanPropertyValue} objects. + * <p> + * This allows a snapshot of all values to be grabbed from a bean in one call. + * + * @param addClassAttr Add a <jk>"_class"</jk> bean property to the returned list. + * @param ignoreNulls Don't return properties whose values are null. + * @return The list of all bean property values. + */ + public List<BeanPropertyValue> getValues(final boolean addClassAttr, final boolean ignoreNulls) { + Collection<BeanPropertyMeta<T>> properties = getProperties(); + int capacity = (ignoreNulls && properties.size() > 10) ? 10 : properties.size() + (addClassAttr ? 1 : 0); + List<BeanPropertyValue> l = new ArrayList<BeanPropertyValue>(capacity); + if (addClassAttr) + l.add(new BeanPropertyValue(meta.getClassProperty(), meta.c.getName(), null)); + for (BeanPropertyMeta<T> bpm : properties) { + try { + Object val = bpm.get(this); + if (val != null || ! ignoreNulls) + l.add(new BeanPropertyValue(bpm, val, null)); + } catch (Error e) { + // Errors should always be uncaught. + throw e; + } catch (Throwable t) { + l.add(new BeanPropertyValue(bpm, null, t)); + } + } + return l; + } + + /** + * Returns a simple collection of properties for this bean map. + * @return A simple collection of properties for this bean map. + */ + protected Collection<BeanPropertyMeta<T>> getProperties() { + return meta.properties.values(); + } + + /** + * Returns all the properties associated with the bean. + * @return A new set. + */ + @Override + public Set<Entry<String,Object>> entrySet() { + + // Construct our own anonymous set to implement this function. + Set<Entry<String,Object>> s = new AbstractSet<Entry<String,Object>>() { + + // Get the list of properties from the meta object. + // Note that the HashMap.values() method caches results, so this collection + // will really only be constructed once per bean type since the underlying + // map never changes. + final Collection<BeanPropertyMeta<T>> pSet = getProperties(); + + @Override /* Set */ + public Iterator<java.util.Map.Entry<String, Object>> iterator() { + + // Construct our own anonymous iterator that uses iterators against the meta.properties + // map to maintain position. This prevents us from having to construct any of our own + // collection objects. + return new Iterator<Entry<String,Object>>() { + + final Iterator<BeanPropertyMeta<T>> pIterator = pSet.iterator(); + + @Override /* Iterator */ + public boolean hasNext() { + return pIterator.hasNext(); + } + + @Override /* Iterator */ + public Map.Entry<String, Object> next() { + return new BeanMapEntry<T>(BeanMap.this, pIterator.next()); + } + + @Override /* Iterator */ + public void remove() { + throw new UnsupportedOperationException("Cannot remove item from iterator."); + } + }; + } + + @Override /* Set */ + public int size() { + return pSet.size(); + } + }; + + return s; + } +} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/BeanMapEntry.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/BeanMapEntry.java b/juneau-core/src/main/java/org/apache/juneau/BeanMapEntry.java new file mode 100644 index 0000000..34aad38 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/BeanMapEntry.java @@ -0,0 +1,125 @@ +/*************************************************************************************************************************** + * 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.juneau; + +import java.util.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.transform.*; + +/** + * Represents a single entry in a bean map. + * <p> + * This class can be used to get and set property values on a bean, or to get metadata on a property. + * + * <h6 class='topic'>Examples</h6> + * <p class='bcode'> + * <jc>// Construct a new bean</jc> + * Person p = <jk>new</jk> Person(); + * + * <jc>// Wrap it in a bean map</jc> + * BeanMap<Person> b = BeanContext.<jsf>DEFAULT</jsf>.forBean(p); + * + * <jc>// Get a reference to the birthDate property</jc> + * BeanMapEntry birthDate = b.getProperty(<js>"birthDate"</js>); + * + * <jc>// Set the property value</jc> + * birthDate.setValue(<jk>new</jk> Date(1, 2, 3, 4, 5, 6)); + * + * <jc>// Or if the DateTransform.DEFAULT_ISO8601DT is registered with the bean context, set a transformed value</jc> + * birthDate.setValue(<js>"'1901-03-03T04:05:06-5000'"</js>); + * </p> + * + * @author James Bognar ([email protected]) + * + * @param <T> The bean type. + */ +public class BeanMapEntry<T> implements Map.Entry<String,Object> { + private final BeanMap<T> beanMap; + private final BeanPropertyMeta<T> meta; + + /** + * Constructor. + * + * @param beanMap The bean map that this entry belongs to. + * @param property The bean property. + */ + protected BeanMapEntry(BeanMap<T> beanMap, BeanPropertyMeta<T> property) { + this.beanMap = beanMap; + this.meta = property; + } + + @Override /* Map.Entry */ + public String getKey() { + return meta.getName(); + } + + /** + * Returns the value of this property. + * <p> + * If there is a {@link PojoTransform} associated with this bean property or bean property type class, then + * this method will return the transformed value. + * For example, if the bean property type class is a {@link Date} and the bean property has the + * {@link org.apache.juneau.transforms.DateTransform.ISO8601DT} transform associated with it through the + * {@link BeanProperty#transform() @BeanProperty.transform()} annotation, this method will return a String + * containing an ISO8601 date-time string value. + */ + @Override /* Map.Entry */ + public Object getValue() { + return meta.get(this.beanMap); + } + + /** + * Sets the value of this property. + * <p> + * If the property is an array of type {@code X}, then the value can be a {@code Collection<X>} or {@code X[]} or {@code Object[]}. + * <p> + * If the property is a bean type {@code X}, then the value can either be an {@code X} or a {@code Map}. + * <p> + * If there is a {@link PojoTransform} associated with this bean property or bean property type class, then + * you must pass in a transformed value. + * For example, if the bean property type class is a {@link Date} and the bean property has the + * {@link org.apache.juneau.transforms.DateTransform.ISO8601DT} transform associated with it through the + * {@link BeanProperty#transform() @BeanProperty.transform()} annotation, the value being passed in must be + * a String containing an ISO8601 date-time string value. + * + * @return The set value after it's been converted. + */ + @Override /* Map.Entry */ + public Object setValue(Object value) { + return meta.set(this.beanMap, value); + } + + /** + * Returns the bean map that contains this property. + * + * @return The bean map that contains this property. + */ + public BeanMap<T> getBeanMap() { + return this.beanMap; + } + + /** + * Returns the metadata about this bean property. + * + * @return Metadata about this bean property. + */ + public BeanPropertyMeta<T> getMeta() { + return this.meta; + } + + @Override /* Object */ + public String toString() { + return this.getKey() + "=" + this.getValue(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/BeanMeta.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/BeanMeta.java b/juneau-core/src/main/java/org/apache/juneau/BeanMeta.java new file mode 100644 index 0000000..8d4d631 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/BeanMeta.java @@ -0,0 +1,767 @@ +/*************************************************************************************************************************** + * 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.juneau; + +import static org.apache.juneau.Visibility.*; +import static org.apache.juneau.internal.ClassUtils.*; + +import java.beans.*; +import java.io.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.Map.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.html.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.jena.*; +import org.apache.juneau.transform.*; +import org.apache.juneau.xml.*; + + +/** + * Encapsulates all access to the properties of a bean class (like a souped-up {@link java.beans.BeanInfo}). + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Uses introspection to find all the properties associated with this class. If the {@link Bean @Bean} annotation + * is present on the class, or the class has a {@link BeanTransform} registered with it in the bean context, + * then that information is used to determine the properties on the class. + * Otherwise, the {@code BeanInfo} functionality in Java is used to determine the properties on the class. + * + * + * <h6 class='topic'>Bean property ordering</h6> + * <p> + * The order of the properties are as follows: + * <ul class='spaced-list'> + * <li>If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties in the annotation. + * <li>If {@link Bean @Bean} annotation is not specified on the class, then the order is based on the following. + * <ul> + * <li>Public fields (same order as {@code Class.getFields()}). + * <li>Properties returned by {@code BeanInfo.getPropertyDescriptors()}. + * <li>Non-standard getters/setters with {@link BeanProperty @BeanProperty} annotation defined on them. + * </ul> + * </ul> + * <br> + * The order can also be overridden through the use of an {@link BeanTransform}. + * + * + * @param <T> The class type that this metadata applies to. + * @author Barry M. Caceres + * @author James Bognar ([email protected]) + */ +public class BeanMeta<T> { + + /** The target class type that this meta object describes. */ + protected ClassMeta<T> classMeta; + + /** The target class that this meta object describes. */ + protected Class<T> c; + + /** The properties on the target class. */ + protected Map<String,BeanPropertyMeta<T>> properties; + + /** The getter properties on the target class. */ + protected Map<Method,String> getterProps = new HashMap<Method,String>(); + + /** The setter properties on the target class. */ + protected Map<Method,String> setterProps = new HashMap<Method,String>(); + + /** The bean context that created this metadata object. */ + protected BeanContext ctx; + + /** Optional bean transform associated with the target class. */ + protected BeanTransform<? extends T> transform; + + /** Type variables implemented by this bean. */ + protected Map<Class<?>,Class<?>[]> typeVarImpls; + + /** The constructor for this bean. */ + protected Constructor<T> constructor; + + /** For beans with constructors with BeanConstructor annotation, this is the list of constructor arg properties. */ + protected String[] constructorArgs = new String[0]; + + /** XML-related metadata */ + protected XmlBeanMeta<T> xmlMeta; + + // Other fields + BeanPropertyMeta<T> uriProperty; // The property identified as the URI for this bean (annotated with @BeanProperty.beanUri). + BeanPropertyMeta<T> subTypeIdProperty; // The property indentified as the sub type differentiator property (identified by @Bean.subTypeProperty annotation). + PropertyNamer propertyNamer; // Class used for calculating bean property names. + BeanPropertyMeta<T> classProperty; // "_class" mock bean property. + + BeanMeta() {} + + /** + * Constructor. + * + * @param classMeta The target class. + * @param ctx The bean context that created this object. + * @param transform Optional bean transform associated with the target class. Can be <jk>null</jk>. + */ + protected BeanMeta(final ClassMeta<T> classMeta, BeanContext ctx, org.apache.juneau.transform.BeanTransform<? extends T> transform) { + this.classMeta = classMeta; + this.ctx = ctx; + this.transform = transform; + this.classProperty = new BeanPropertyMeta<T>(this, "_class", ctx.string()); + this.c = classMeta.getInnerClass(); + } + + /** + * Returns the {@link ClassMeta} of this bean. + * + * @return The {@link ClassMeta} of this bean. + */ + @BeanIgnore + public ClassMeta<T> getClassMeta() { + return classMeta; + } + + /** + * Initializes this bean meta, and returns an error message if the specified class is not + * a bean for any reason. + * + * @return Reason why this class isn't a bean, or <jk>null</jk> if no problems detected. + * @throws BeanRuntimeException If unexpected error occurs such as invalid annotations on the bean class. + */ + @SuppressWarnings("unchecked") + protected String init() throws BeanRuntimeException { + + try { + Visibility + conVis = ctx.beanConstructorVisibility, + cVis = ctx.beanClassVisibility, + mVis = ctx.beanMethodVisibility, + fVis = ctx.beanFieldVisibility; + + // If @Bean.interfaceClass is specified on the parent class, then we want + // to use the properties defined on that class, not the subclass. + Class<?> c2 = (transform != null && transform.getInterfaceClass() != null ? transform.getInterfaceClass() : c); + + Class<?> stopClass = (transform != null ? transform.getStopClass() : Object.class); + if (stopClass == null) + stopClass = Object.class; + + Map<String,BeanPropertyMeta<T>> normalProps = new LinkedHashMap<String,BeanPropertyMeta<T>>(); + + /// See if this class matches one the patterns in the exclude-class list. + if (ctx.isNotABean(c)) + return "Class matches exclude-class list"; + + if (! cVis.isVisible(c.getModifiers())) + return "Class is not public"; + + if (c.isAnnotationPresent(BeanIgnore.class)) + return "Class is annotated with @BeanIgnore"; + + // Make sure it's serializable. + if (transform == null && ctx.beansRequireSerializable && ! isParentClass(Serializable.class, c)) + return "Class is not serializable"; + + // Look for @BeanConstructor constructor. + for (Constructor<?> x : c.getConstructors()) { + if (x.isAnnotationPresent(BeanConstructor.class)) { + if (constructor != null) + throw new BeanRuntimeException(c, "Multiple instances of '@BeanConstructor' found."); + constructor = (Constructor<T>)x; + constructorArgs = x.getAnnotation(BeanConstructor.class).properties(); + if (constructorArgs.length != x.getParameterTypes().length) + throw new BeanRuntimeException(c, "Number of properties defined in '@BeanConstructor' annotation does not match number of parameters in constructor."); + if (! setAccessible(constructor)) + throw new BeanRuntimeException(c, "Could not set accessibility to true on method with @BeanConstructor annotation. Method=''{0}''", constructor.getName()); + } + } + + // If this is an interface, look for impl classes defined in the context. + if (constructor == null) + constructor = (Constructor<T>)ctx.getImplClassConstructor(c, conVis); + + if (constructor == null) + constructor = (Constructor<T>)ClassMeta.findNoArgConstructor(c, conVis); + + if (constructor == null && transform == null && ctx.beansRequireDefaultConstructor) + return "Class does not have the required no-arg constructor"; + + if (! setAccessible(constructor)) + throw new BeanRuntimeException(c, "Could not set accessibility to true on no-arg constructor"); + + // Explicitly defined property names in @Bean annotation. + Set<String> fixedBeanProps = new LinkedHashSet<String>(); + + if (transform != null) { + + // Get the 'properties' attribute if specified. + if (transform.getProperties() != null) + for (String p : transform.getProperties()) + fixedBeanProps.add(p); + + if (transform.getPropertyNamer() != null) + propertyNamer = transform.getPropertyNamer().newInstance(); + } + + if (propertyNamer == null) + propertyNamer = new PropertyNamerDefault(); + + // First populate the properties with those specified in the bean annotation to + // ensure that ordering first. + for (String name : fixedBeanProps) + normalProps.put(name, new BeanPropertyMeta<T>(this, name)); + + if (ctx.useJavaBeanIntrospector) { + BeanInfo bi = null; + if (! c2.isInterface()) + bi = Introspector.getBeanInfo(c2, stopClass); + else + bi = Introspector.getBeanInfo(c2, null); + if (bi != null) { + for (PropertyDescriptor pd : bi.getPropertyDescriptors()) { + String name = pd.getName(); + if (! normalProps.containsKey(name)) + normalProps.put(name, new BeanPropertyMeta<T>(this, name)); + normalProps.get(name).setGetter(pd.getReadMethod()).setSetter(pd.getWriteMethod()); + } + } + + } else /* Use 'better' introspection */ { + + for (Field f : findBeanFields(c2, stopClass, fVis)) { + String name = findPropertyName(f, fixedBeanProps); + if (name != null) { + if (! normalProps.containsKey(name)) + normalProps.put(name, new BeanPropertyMeta<T>(this, name)); + normalProps.get(name).setField(f); + } + } + + List<BeanMethod> bms = findBeanMethods(c2, stopClass, mVis, fixedBeanProps, propertyNamer); + + // Iterate through all the getters. + for (BeanMethod bm : bms) { + String pn = bm.propertyName; + Method m = bm.method; + if (! normalProps.containsKey(pn)) + normalProps.put(pn, new BeanPropertyMeta<T>(this, pn)); + BeanPropertyMeta<?> bpm = normalProps.get(pn); + if (! bm.isSetter) + bpm.setGetter(m); + } + + // Now iterate through all the setters. + for (BeanMethod bm : bms) { + if (bm.isSetter) { + BeanPropertyMeta<?> bpm = normalProps.get(bm.propertyName); + if (bm.matchesPropertyType(bpm)) + bpm.setSetter(bm.method); + } + } + } + + typeVarImpls = new HashMap<Class<?>,Class<?>[]>(); + findTypeVarImpls(c, typeVarImpls); + if (typeVarImpls.isEmpty()) + typeVarImpls = null; + + // Eliminate invalid properties, and set the contents of getterProps and setterProps. + for (Iterator<BeanPropertyMeta<T>> i = normalProps.values().iterator(); i.hasNext();) { + BeanPropertyMeta<T> p = i.next(); + try { + if (p.validate()) { + + if (p.getGetter() != null) + getterProps.put(p.getGetter(), p.getName()); + + if (p.getSetter() != null) + setterProps.put(p.getSetter(), p.getName()); + + if (p.isBeanUri()) + uriProperty = p; + + } else { + i.remove(); + } + } catch (ClassNotFoundException e) { + throw new BeanRuntimeException(c, e.getLocalizedMessage()); + } + } + + // Check for missing properties. + for (String fp : fixedBeanProps) + if (! normalProps.containsKey(fp)) + throw new BeanRuntimeException(c, "The property ''{0}'' was defined on the @Bean(properties=X) annotation but was not found on the class definition.", fp); + + // Mark constructor arg properties. + for (String fp : constructorArgs) { + BeanPropertyMeta<T> m = normalProps.get(fp); + if (m == null) + throw new BeanRuntimeException(c, "The property ''{0}'' was defined on the @BeanConstructor(properties=X) annotation but was not found on the class definition.", fp); + m.setAsConstructorArg(); + } + + // Make sure at least one property was found. + if (transform == null && ctx.beansRequireSomeProperties && normalProps.size() == 0) + return "No properties detected on bean class"; + + boolean sortProperties = (ctx.sortProperties || (transform != null && transform.isSortProperties())) && fixedBeanProps.isEmpty(); + + properties = sortProperties ? new TreeMap<String,BeanPropertyMeta<T>>() : new LinkedHashMap<String,BeanPropertyMeta<T>>(); + + if (transform != null && transform.getSubTypeProperty() != null) { + String subTypeProperty = transform.getSubTypeProperty(); + this.subTypeIdProperty = new SubTypePropertyMeta(subTypeProperty, transform.getSubTypes(), normalProps.remove(subTypeProperty)); + properties.put(subTypeProperty, this.subTypeIdProperty); + } + + properties.putAll(normalProps); + + // If a transform is defined, look for inclusion and exclusion lists. + if (transform != null) { + + // Eliminated excluded properties if BeanTransform.excludeKeys is specified. + String[] includeKeys = transform.getProperties(); + String[] excludeKeys = transform.getExcludeProperties(); + if (excludeKeys != null) { + for (String k : excludeKeys) + properties.remove(k); + + // Only include specified properties if BeanTransform.includeKeys is specified. + // Note that the order must match includeKeys. + } else if (includeKeys != null) { + Map<String,BeanPropertyMeta<T>> properties2 = new LinkedHashMap<String,BeanPropertyMeta<T>>(); + for (String k : includeKeys) { + if (properties.containsKey(k)) + properties2.put(k, properties.get(k)); + } + properties = properties2; + } + } + + xmlMeta = new XmlBeanMeta<T>(this, null); + + // We return this through the Bean.keySet() interface, so make sure it's not modifiable. + properties = Collections.unmodifiableMap(properties); + + } catch (BeanRuntimeException e) { + throw e; + } catch (Exception e) { + return "Exception: " + StringUtils.getStackTrace(e); + } + + return null; + } + + /** + * Returns the subtype ID property of this bean if it has one. + * <p> + * The subtype id is specified using the {@link Bean#subTypeProperty()} annotation. + * + * @return The meta property for the sub type property, or <jk>null</jk> if no subtype is defined for this bean. + */ + public BeanPropertyMeta<T> getSubTypeIdProperty() { + return subTypeIdProperty; + } + + /** + * Returns <jk>true</jk> if this bean has subtypes associated with it. + * Subtypes are defined using the {@link Bean#subTypes()} annotation. + * + * @return <jk>true</jk> if this bean has subtypes associated with it. + */ + public boolean isSubTyped() { + return subTypeIdProperty != null; + } + + /** + * Returns <jk>true</jk> if one of the properties on this bean is annotated with {@link BeanProperty#beanUri()} as <jk>true</jk> + * + * @return <jk>true</jk> if this bean has subtypes associated with it. <jk>true</jk> if there is a URI property associated with this bean. + */ + public boolean hasBeanUriProperty() { + return uriProperty != null; + } + + /** + * Returns the bean property marked as the URI for the bean (annotated with {@link BeanProperty#beanUri()} as <jk>true</jk>). + * + * @return The URI property, or <jk>null</jk> if no URI property exists on this bean. + */ + public BeanPropertyMeta<T> getBeanUriProperty() { + return uriProperty; + } + + /** + * Returns a mock bean property that resolves to the name <js>"_class"</js> and whose value always resolves + * to the class name of the bean. + * + * @return The class name property. + */ + public BeanPropertyMeta<T> getClassProperty() { + return classProperty; + } + + /* + * Temporary getter/setter method struct. + */ + private static class BeanMethod { + String propertyName; + boolean isSetter; + Method method; + Class<?> type; + + BeanMethod(String propertyName, boolean isSetter, Method method) { + this.propertyName = propertyName; + this.isSetter = isSetter; + this.method = method; + if (isSetter) + this.type = method.getParameterTypes()[0]; + else + this.type = method.getReturnType(); + } + + /* + * Returns true if this method matches the class type of the specified property. + * Only meant to be used for setters. + */ + boolean matchesPropertyType(BeanPropertyMeta<?> b) { + if (b == null) + return false; + + // Get the bean property type from the getter/field. + Class<?> pt = null; + if (b.getGetter() != null) + pt = b.getGetter().getReturnType(); + else if (b.getField() != null) + pt = b.getField().getType(); + + // Doesn't match if no getter/field defined. + if (pt == null) + return false; + + // Doesn't match if not same type or super type as getter/field. + if (! isParentClass(type, pt)) + return false; + + // If a setter was previously set, only use this setter if it's a closer + // match (e.g. prev type is a superclass of this type). + if (b.getSetter() == null) + return true; + + Class<?> prevType = b.getSetter().getParameterTypes()[0]; + return isParentClass(prevType, type, true); + } + + @Override /* Object */ + public String toString() { + return method.toString(); + } + } + + /* + * Find all the bean methods on this class. + * + * @param c The transformed class. + * @param stopClass Don't look above this class in the hierarchy. + * @param v The minimum method visibility. + * @param fixedBeanProps Only include methods whose properties are in this list. + * @param pn Use this property namer to determine property names from the method names. + */ + private static List<BeanMethod> findBeanMethods(Class<?> c, Class<?> stopClass, Visibility v, Set<String> fixedBeanProps, PropertyNamer pn) { + List<BeanMethod> l = new LinkedList<BeanMethod>(); + + for (Class<?> c2 : findClasses(c, stopClass)) { + for (Method m : c2.getDeclaredMethods()) { + int mod = m.getModifiers(); + if (Modifier.isStatic(mod) || Modifier.isTransient(mod)) + continue; + if (m.isAnnotationPresent(BeanIgnore.class)) + continue; + if (m.isBridge()) // This eliminates methods with covariant return types from parent classes on child classes. + continue; + if (! (v.isVisible(m) || m.isAnnotationPresent(BeanProperty.class))) + continue; + String n = m.getName(); + Class<?>[] pt = m.getParameterTypes(); + Class<?> rt = m.getReturnType(); + boolean isGetter = false, isSetter = false; + if (pt.length == 1 && n.startsWith("set") && (isParentClass(rt, c) || rt.equals(Void.TYPE))) { + isSetter = true; + n = n.substring(3); + } else if (pt.length == 0 && n.startsWith("get") && (! rt.equals(Void.TYPE))) { + isGetter = true; + n = n.substring(3); + } else if (pt.length == 0 && n.startsWith("is") && (rt.equals(Boolean.TYPE) || rt.equals(Boolean.class))) { + isGetter = true; + n = n.substring(2); + } + n = pn.getPropertyName(n); + if (isGetter || isSetter) { + BeanProperty bp = m.getAnnotation(BeanProperty.class); + if (bp != null && ! bp.name().equals("")) { + n = bp.name(); + if (! fixedBeanProps.isEmpty()) + if (! fixedBeanProps.contains(n)) + throw new BeanRuntimeException(c, "Method property ''{0}'' identified in @BeanProperty, but missing from @Bean", n); + } + l.add(new BeanMethod(n, isSetter, m)); + } + } + } + return l; + } + + private static Collection<Field> findBeanFields(Class<?> c, Class<?> stopClass, Visibility v) { + List<Field> l = new LinkedList<Field>(); + for (Class<?> c2 : findClasses(c, stopClass)) { + for (Field f : c2.getDeclaredFields()) { + int m = f.getModifiers(); + if (Modifier.isStatic(m) || Modifier.isTransient(m)) + continue; + if (f.isAnnotationPresent(BeanIgnore.class)) + continue; + if (! (v.isVisible(f) || f.isAnnotationPresent(BeanProperty.class))) + continue; + l.add(f); + } + } + return l; + } + + private static List<Class<?>> findClasses(Class<?> c, Class<?> stopClass) { + LinkedList<Class<?>> l = new LinkedList<Class<?>>(); + findClasses(c, l, stopClass); + return l; + } + + private static void findClasses(Class<?> c, LinkedList<Class<?>> l, Class<?> stopClass) { + while (c != null && stopClass != c) { + l.addFirst(c); + for (Class<?> ci : c.getInterfaces()) + findClasses(ci, l, stopClass); + c = c.getSuperclass(); + } + } + + /** + * Returns the metadata on all properties associated with this bean. + * + * @return Metadata on all properties associated with this bean. + */ + public Collection<BeanPropertyMeta<T>> getPropertyMetas() { + return this.properties.values(); + } + + /** + * Returns the metadata on the specified list of properties. + * + * @param pNames The list of properties to retrieve. If <jk>null</jk>, returns all properties. + * @return The metadata on the specified list of properties. + */ + public Collection<BeanPropertyMeta<T>> getPropertyMetas(final String...pNames) { + if (pNames == null) + return getPropertyMetas(); + List<BeanPropertyMeta<T>> l = new ArrayList<BeanPropertyMeta<T>>(pNames.length); + for (int i = 0; i < pNames.length; i++) + l.add(getPropertyMeta(pNames[i])); + return l; + } + + /** + * Returns XML related metadata for this bean type. + * + * @return The XML metadata for this bean type. + */ + public XmlBeanMeta<T> getXmlMeta() { + return xmlMeta; + } + + /** + * Returns metadata about the specified property. + * + * @param name The name of the property on this bean. + * @return The metadata about the property, or <jk>null</jk> if no such property exists + * on this bean. + */ + public BeanPropertyMeta<T> getPropertyMeta(String name) { + return this.properties.get(name); + } + + /** + * Creates a new instance of this bean. + * + * @param outer The outer object if bean class is a non-static inner member class. + * @return A new instance of this bean if possible, or <jk>null</jk> if not. + * @throws IllegalArgumentException Thrown by constructor. + * @throws InstantiationException Thrown by constructor. + * @throws IllegalAccessException Thrown by constructor. + * @throws InvocationTargetException Thrown by constructor. + */ + @SuppressWarnings("unchecked") + protected T newBean(Object outer) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { + if (classMeta.isMemberClass) { + if (constructor != null) + return constructor.newInstance(outer); + } else { + if (constructor != null) + return constructor.newInstance((Object[])null); + InvocationHandler h = classMeta.getProxyInvocationHandler(); + if (h != null) { + ClassLoader cl = classMeta.beanContext.classLoader; + if (cl == null) + cl = this.getClass().getClassLoader(); + return (T)Proxy.newProxyInstance(cl, new Class[] { classMeta.innerClass, java.io.Serializable.class }, h); + } + } + return null; + } + + /* + * Returns the property name of the specified field if it's a valid property. + * Returns null if the field isn't a valid property. + */ + private String findPropertyName(Field f, Set<String> fixedBeanProps) { + BeanProperty bp = f.getAnnotation(BeanProperty.class); + if (bp != null && ! bp.name().equals("")) { + String name = bp.name(); + if (fixedBeanProps.isEmpty() || fixedBeanProps.contains(name)) + return name; + throw new BeanRuntimeException(c, "Method property ''{0}'' identified in @BeanProperty, but missing from @Bean", name); + } + String name = propertyNamer.getPropertyName(f.getName()); + if (fixedBeanProps.isEmpty() || fixedBeanProps.contains(name)) + return name; + return null; + } + + /** + * Recursively determines the classes represented by parameterized types in the class hierarchy of + * the specified type, and puts the results in the specified map.<br> + * <p> + * For example, given the following classes... + * <p class='bcode'> + * public static class BeanA<T> { + * public T x; + * } + * public static class BeanB extends BeanA<Integer>} {...} + * <p> + * ...calling this method on {@code BeanB.class} will load the following data into {@code m} indicating + * that the {@code T} parameter on the BeanA class is implemented with an {@code Integer}: + * <p class='bcode'> + * {BeanA.class:[Integer.class]} + * <p> + * TODO: This code doesn't currently properly handle the following situation: + * <p class='bcode'> + * public static class BeanB<T extends Number> extends BeanA<T>; + * public static class BeanC extends BeanB<Integer>; + * <p> + * When called on {@code BeanC}, the variable will be detected as a {@code Number}, not an {@code Integer}.<br> + * If anyone can figure out a better way of doing this, please do so! + * + * @param t The type we're recursing. + * @param m Where the results are loaded. + */ + private static void findTypeVarImpls(Type t, Map<Class<?>,Class<?>[]> m) { + if (t instanceof Class) { + Class<?> c = (Class<?>)t; + findTypeVarImpls(c.getGenericSuperclass(), m); + for (Type ci : c.getGenericInterfaces()) + findTypeVarImpls(ci, m); + } else if (t instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType)t; + Type rt = pt.getRawType(); + if (rt instanceof Class) { + Type[] gImpls = pt.getActualTypeArguments(); + Class<?>[] gTypes = new Class[gImpls.length]; + for (int i = 0; i < gImpls.length; i++) { + Type gt = gImpls[i]; + if (gt instanceof Class) + gTypes[i] = (Class<?>)gt; + else if (gt instanceof TypeVariable) { + TypeVariable<?> tv = (TypeVariable<?>)gt; + for (Type upperBound : tv.getBounds()) + if (upperBound instanceof Class) + gTypes[i] = (Class<?>)upperBound; + } + } + m.put((Class<?>)rt, gTypes); + findTypeVarImpls(pt.getRawType(), m); + } + } + } + + /* + * Bean property for getting and setting bean subtype. + */ + @SuppressWarnings({"rawtypes","unchecked"}) + private class SubTypePropertyMeta extends BeanPropertyMeta<T> { + + private Map<Class<?>,String> subTypes; + private BeanPropertyMeta<T> realProperty; // Bean property if bean actually has a real subtype field. + + SubTypePropertyMeta(String subTypeAttr, Map<Class<?>,String> subTypes, BeanPropertyMeta<T> realProperty) { + super(BeanMeta.this, subTypeAttr, ctx.string()); + this.subTypes = subTypes; + this.realProperty = realProperty; + this.htmlMeta = new HtmlBeanPropertyMeta<T>(this); + this.xmlMeta = new XmlBeanPropertyMeta<T>(this); + this.rdfMeta = new RdfBeanPropertyMeta<T>(this); + } + + /* + * Setting this bean property causes the inner bean to be set to the subtype implementation. + */ + @Override /* BeanPropertyMeta */ + public Object set(BeanMap<T> m, Object value) throws BeanRuntimeException { + if (value == null) + throw new BeanRuntimeException("Attempting to set bean subtype property to null."); + String subTypeId = value.toString(); + for (Entry<Class<?>,String> e : subTypes.entrySet()) { + if (e.getValue().equals(subTypeId)) { + Class subTypeClass = e.getKey(); + m.meta = ctx.getBeanMeta(subTypeClass); + try { + m.bean = (T)subTypeClass.newInstance(); + if (realProperty != null) + realProperty.set(m, value); + // If subtype attribute wasn't specified first, set them again from the temporary cache. + if (m.propertyCache != null) + for (Map.Entry<String,Object> me : m.propertyCache.entrySet()) + m.put(me.getKey(), me.getValue()); + } catch (Exception e1) { + throw new BeanRuntimeException(e1); + } + return null; + } + } + throw new BeanRuntimeException(c, "Unknown subtype ID ''{0}''", subTypeId); + } + + @Override /* BeanPropertyMeta */ + public Object get(BeanMap<T> m) throws BeanRuntimeException { + String subTypeId = transform.getSubTypes().get(c); + if (subTypeId == null) + throw new BeanRuntimeException(c, "Unmapped sub type class"); + return subTypeId; + } + } + + @Override /* Object */ + public String toString() { + StringBuilder sb = new StringBuilder(c.getName()); + sb.append(" {\n"); + for (BeanPropertyMeta<?> pm : this.properties.values()) + sb.append('\t').append(pm.toString()).append(",\n"); + sb.append('}'); + return sb.toString(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/BeanMetaFiltered.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/BeanMetaFiltered.java b/juneau-core/src/main/java/org/apache/juneau/BeanMetaFiltered.java new file mode 100644 index 0000000..09b0d0d --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/BeanMetaFiltered.java @@ -0,0 +1,74 @@ +/*************************************************************************************************************************** + * 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.juneau; + +import java.util.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.xml.*; + +/** + * Sames as {@link BeanMeta}, except the list of bean properties are limited + * by a {@link BeanProperty#properties()} annotation. + * + * @param <T> The class type that this metadata applies to. + * @author James Bognar ([email protected]) + */ +public final class BeanMetaFiltered<T> extends BeanMeta<T> { + + private final BeanMeta<T> innerMeta; + + /** + * Wrapper constructor. + * + * @param innerMeta The untransformed bean meta of the bean property. + * @param pNames The list of transformed property names. + */ + public BeanMetaFiltered(BeanMeta<T> innerMeta, String[] pNames) { + this.innerMeta = innerMeta; + this.properties = new LinkedHashMap<String,BeanPropertyMeta<T>>(); + for (String p : pNames) + properties.put(p, innerMeta.getPropertyMeta(p)); + this.xmlMeta = new XmlBeanMeta<T>(innerMeta, pNames); + } + + /** + * Wrapper constructor. + * + * @param innerMeta The untransformed bean meta of the bean property. + * @param pNames The list of transformed property names. + */ + public BeanMetaFiltered(BeanMeta<T> innerMeta, Collection<String> pNames) { + this(innerMeta, pNames.toArray(new String[pNames.size()])); + } + + @Override /* Delagate */ + public ClassMeta<T> getClassMeta() { + return innerMeta.classMeta; + } + + @Override /* BeanMeta */ + public Collection<BeanPropertyMeta<T>> getPropertyMetas() { + return properties.values(); + } + + @Override /* BeanMeta */ + public BeanPropertyMeta<T> getPropertyMeta(String name) { + return properties.get(name); + } + + @Override /* Object */ + public String toString() { + return innerMeta.c.getName(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/BeanPropertyMeta.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/BeanPropertyMeta.java b/juneau-core/src/main/java/org/apache/juneau/BeanPropertyMeta.java new file mode 100644 index 0000000..48332cf --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/BeanPropertyMeta.java @@ -0,0 +1,813 @@ +/*************************************************************************************************************************** + * 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.juneau; + +import static org.apache.juneau.Visibility.*; +import static org.apache.juneau.internal.ClassUtils.*; +import static org.apache.juneau.internal.CollectionUtils.*; +import static org.apache.juneau.internal.ReflectionUtils.*; + +import java.lang.annotation.*; +import java.lang.reflect.*; +import java.net.*; +import java.net.URI; +import java.util.*; + +import org.apache.juneau.annotation.*; +import org.apache.juneau.html.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.jena.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.transform.*; +import org.apache.juneau.xml.*; + +/** + * Contains metadata about a bean property. + * <p> + * Contains information such as type of property (e.g. field/getter/setter), class type of property value, + * and whether any transforms are associated with this property. + * <p> + * Developers will typically not need access to this class. The information provided by it is already + * exposed through several methods on the {@link BeanMap} API. + * + * @param <T> The class type of the bean that this metadata applies to. + * @author James Bognar ([email protected]) + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class BeanPropertyMeta<T> { + + private Field field; + private Method getter, setter; + private boolean isConstructorArg, isBeanUri, isUri; + + private final BeanMeta<T> beanMeta; + + private String name; + private ClassMeta<?> + rawTypeMeta, // The real class type of the bean property. + typeMeta; // The transformed class type of the bean property. + private String[] properties; + private PojoTransform transform; // PojoTransform defined only via @BeanProperty annotation. + + /** HTML related metadata on this bean property. */ + protected HtmlBeanPropertyMeta<T> htmlMeta; + + /** XML related metadata on this bean property. */ + protected XmlBeanPropertyMeta<T> xmlMeta; + + /** RDF related metadata on this bean property. */ + protected RdfBeanPropertyMeta<T> rdfMeta; // + + /** + * Constructor. + * + * @param beanMeta The metadata of the bean containing this property. + * @param name This property name. + */ + protected BeanPropertyMeta(BeanMeta<T> beanMeta, String name) { + this.beanMeta = beanMeta; + this.name = name; + } + + BeanPropertyMeta(BeanMeta<T> beanMeta, String name, ClassMeta<?> rawTypeMeta) { + this(beanMeta, name); + this.rawTypeMeta = rawTypeMeta; + } + + BeanPropertyMeta(BeanMeta<T> beanMeta, String name, Method getter, Method setter) { + this(beanMeta, name); + setGetter(getter); + setSetter(setter); + } + + /** + * Returns the name of this bean property. + * + * @return The name of the bean property. + */ + public String getName() { + return name; + } + + /** + * Returns the bean meta that this property belongs to. + * + * @return The bean meta that this property belongs to. + */ + @BeanIgnore + public BeanMeta<T> getBeanMeta() { + return beanMeta; + } + + /** + * Returns the getter method for this property. + * + * @return The getter method for this bean property, or <jk>null</jk> if there is no getter method. + */ + public Method getGetter() { + return getter; + } + + /** + * Returns the setter method for this property. + * + * @return The setter method for this bean property, or <jk>null</jk> if there is no setter method. + */ + public Method getSetter() { + return setter; + } + + /** + * Returns the field for this property. + * + * @return The field for this bean property, or <jk>null</jk> if there is no field associated with this bean property. + */ + public Field getField() { + return field; + } + + /** + * Returns the {@link ClassMeta} of the class of this property. + * <p> + * If this property or the property type class has a {@link PojoTransform} associated with it, this + * method returns the transformed class meta. + * This matches the class type that is used by the {@link #get(BeanMap)} and {@link #set(BeanMap, Object)} methods. + * + * @return The {@link ClassMeta} of the class of this property. + */ + public ClassMeta<?> getClassMeta() { + if (typeMeta == null) + typeMeta = (transform != null ? transform.getTransformedClassMeta() : rawTypeMeta == null ? beanMeta.ctx.object() : rawTypeMeta.getTransformedClassMeta()); + return typeMeta; + } + + /** + * Sets the getter method for this property. + * + * @param getter The getter method to associate with this property. + * @return This object (for method chaining). + */ + BeanPropertyMeta<T> setGetter(Method getter) { + setAccessible(getter); + this.getter = getter; + return this; + } + + /** + * Sets the setter method for this property. + * + * @param setter The setter method to associate with this property. + * @return This object (for method chaining). + */ + BeanPropertyMeta<T> setSetter(Method setter) { + setAccessible(setter); + this.setter = setter; + return this; + } + + /** + * Sets the field for this property. + * + * @param field The field to associate with this property. + * @return This object (for method chaining). + */ + BeanPropertyMeta<T> setField(Field field) { + setAccessible(field); + this.field = field; + return this; + } + + /** + * Marks this property as only settable through a constructor arg. + * + * @return This object (for method chaining). + */ + BeanPropertyMeta<T> setAsConstructorArg() { + this.isConstructorArg = true; + return this; + } + + /** + * Returns <jk>true</jk> if this bean property is marked with {@link BeanProperty#beanUri()} as <jk>true</jk>. + * + * @return <jk>true</jk> if this bean property is marked with {@link BeanProperty#beanUri()} as <jk>true</jk>. + */ + public boolean isBeanUri() { + return isBeanUri; + } + + /** + * Returns <jk>true</jk> if this bean property is a URI. + * <p> + * A bean property can be considered a URI if any of the following are true: + * <ul class='spaced-list'> + * <li>Property class type is {@link URL} or {@link URI}. + * <li>Property class type is annotated with {@link org.apache.juneau.annotation.URI}. + * <li>Property getter, setter, or field is annotated with {@link org.apache.juneau.annotation.URI}. + * </ul> + * + * @return <jk>true</jk> if this bean property is a URI. + */ + public boolean isUri() { + return isUri; + } + + /** + * Returns the override list of properties defined through a {@link BeanProperty#properties()} annotation + * on this property. + * + * @return The list of override properties, or <jk>null</jk> if annotation not specified. + */ + public String[] getProperties() { + return properties; + } + + /** + * Returns the HTML-related metadata on this bean property. + * + * @return The HTML-related metadata on this bean property. Never <jk>null</jk>/. + */ + public HtmlBeanPropertyMeta<T> getHtmlMeta() { + return htmlMeta; + } + + /** + * Returns the XML-related metadata on this bean property. + * + * @return The XML-related metadata on this bean property. Never <jk>null</jk>/. + */ + public XmlBeanPropertyMeta<T> getXmlMeta() { + return xmlMeta; + } + + /** + * Returns the RDF-related metadata on this bean property. + * + * @return The RDF-related metadata on this bean property. Never <jk>null</jk>/. + */ + public RdfBeanPropertyMeta<T> getRdfMeta() { + return rdfMeta; + } + + boolean validate() throws Exception { + + BeanContext f = beanMeta.ctx; + Map<Class<?>,Class<?>[]> typeVarImpls = beanMeta.typeVarImpls; + + if (field == null && getter == null) + return false; + + if (field == null && setter == null && f.beansRequireSettersForGetters && ! isConstructorArg) + return false; + + if (field != null) { + BeanProperty p = field.getAnnotation(BeanProperty.class); + rawTypeMeta = f.getClassMeta(p, field.getGenericType(), typeVarImpls); + isUri |= (rawTypeMeta.isUri() || field.isAnnotationPresent(org.apache.juneau.annotation.URI.class)); + if (p != null) { + transform = getPropertyPojoTransform(p); + if (p.properties().length != 0) + properties = p.properties(); + isBeanUri |= p.beanUri(); + } + } + + if (getter != null) { + BeanProperty p = getter.getAnnotation(BeanProperty.class); + if (rawTypeMeta == null) + rawTypeMeta = f.getClassMeta(p, getter.getGenericReturnType(), typeVarImpls); + isUri |= (rawTypeMeta.isUri() || getter.isAnnotationPresent(org.apache.juneau.annotation.URI.class)); + if (p != null) { + if (transform == null) + transform = getPropertyPojoTransform(p); + if (properties != null && p.properties().length != 0) + properties = p.properties(); + isBeanUri |= p.beanUri(); + } + } + + if (setter != null) { + BeanProperty p = setter.getAnnotation(BeanProperty.class); + if (rawTypeMeta == null) + rawTypeMeta = f.getClassMeta(p, setter.getGenericParameterTypes()[0], typeVarImpls); + isUri |= (rawTypeMeta.isUri() || setter.isAnnotationPresent(org.apache.juneau.annotation.URI.class)); + if (p != null) { + if (transform == null) + transform = getPropertyPojoTransform(p); + if (properties != null && p.properties().length != 0) + properties = p.properties(); + isBeanUri |= p.beanUri(); + } + } + + if (rawTypeMeta == null) + return false; + + // Do some annotation validation. + Class<?> c = rawTypeMeta.getInnerClass(); + if (getter != null && ! isParentClass(getter.getReturnType(), c)) + return false; + if (setter != null && ! isParentClass(setter.getParameterTypes()[0], c)) + return false; + if (field != null && ! isParentClass(field.getType(), c)) + return false; + + htmlMeta = new HtmlBeanPropertyMeta(this); + xmlMeta = new XmlBeanPropertyMeta(this); + rdfMeta = new RdfBeanPropertyMeta(this); + + return true; + } + + private PojoTransform getPropertyPojoTransform(BeanProperty p) throws Exception { + Class<? extends PojoTransform> c = p.transform(); + if (c == PojoTransform.NULL.class) + return null; + try { + PojoTransform f = c.newInstance(); + f.setBeanContext(this.beanMeta.ctx); + return f; + } catch (Exception e) { + throw new BeanRuntimeException(this.beanMeta.c, "Could not instantiate PojoTransform ''{0}'' for bean property ''{1}''", c.getName(), this.name).initCause(e); + } + } + + /** + * Equivalent to calling {@link BeanMap#get(Object)}, but is faster since it avoids looking up the property meta. + * + * @param m The bean map to get the transformed value from. + * @return The property value. + */ + public Object get(BeanMap<T> m) { + try { + // Read-only beans have their properties stored in a cache until getBean() is called. + Object bean = m.bean; + if (bean == null) + return m.propertyCache.get(name); + + Object o = null; + + if (getter == null && field == null) + throw new BeanRuntimeException(beanMeta.c, "Getter or public field not defined on property ''{0}''", name); + + if (getter != null) + o = getter.invoke(bean, (Object[])null); + + else if (field != null) + o = field.get(bean); + + o = transform(o); + if (o == null) + return null; + if (properties != null) { + if (rawTypeMeta.isArray()) { + Object[] a = (Object[])o; + List l = new ArrayList(a.length); + ClassMeta childType = rawTypeMeta.getElementType(); + for (Object c : a) + l.add(applyChildPropertiesFilter(childType, c)); + return l; + } else if (rawTypeMeta.isCollection()) { + Collection c = (Collection)o; + List l = new ArrayList(c.size()); + ClassMeta childType = rawTypeMeta.getElementType(); + for (Object cc : c) + l.add(applyChildPropertiesFilter(childType, cc)); + return l; + } else { + return applyChildPropertiesFilter(rawTypeMeta, o); + } + } + return o; + } catch (SerializeException e) { + throw new BeanRuntimeException(e); + } catch (Throwable e) { + if (beanMeta.ctx.ignoreInvocationExceptionsOnGetters) { + if (rawTypeMeta.isPrimitive()) + return rawTypeMeta.getPrimitiveDefault(); + return null; + } + throw new BeanRuntimeException(beanMeta.c, "Exception occurred while getting property ''{0}''", name).initCause(e); + } + } + + /** + * Equivalent to calling {@link BeanMap#put(String, Object)}, but is faster since it avoids + * looking up the property meta. + * + * @param m The bean map to set the property value on. + * @param value The value to set. + * @return The previous property value. + * @throws BeanRuntimeException If property could not be set. + */ + public Object set(BeanMap<T> m, Object value) throws BeanRuntimeException { + try { + // Comvert to raw form. + value = normalize(value); + BeanContext bc = this.beanMeta.ctx; + + if (m.bean == null) { + + // If this bean has subtypes, and we haven't set the subtype yet, + // store the property in a temporary cache until the bean can be instantiated. + if (m.meta.subTypeIdProperty != null && m.propertyCache == null) + m.propertyCache = new TreeMap<String,Object>(); + + // Read-only beans get their properties stored in a cache. + if (m.propertyCache != null) + return m.propertyCache.put(name, value); + + throw new BeanRuntimeException("Non-existent bean instance on bean."); + } + + boolean isMap = rawTypeMeta.isMap(); + boolean isCollection = rawTypeMeta.isCollection(); + + if (field == null && setter == null && ! (isMap || isCollection)) { + if ((value == null && bc.ignoreUnknownNullBeanProperties) || bc.ignorePropertiesWithoutSetters) + return null; + throw new BeanRuntimeException(beanMeta.c, "Setter or public field not defined on property ''{0}''", name); + } + + Object bean = m.getBean(true); // Don't use getBean() because it triggers array creation! + + try { + + Object r = beanMeta.ctx.beanMapPutReturnsOldValue || isMap || isCollection ? get(m) : null; + Class<?> propertyClass = rawTypeMeta.getInnerClass(); + + if (value == null && (isMap || isCollection)) { + if (setter != null) { + setter.invoke(bean, new Object[] { null }); + return r; + } else if (field != null) { + field.set(bean, null); + return r; + } + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' to null because no setter or public field is defined", name); + } + + if (isMap) { + + if (! (value instanceof Map)) { + if (value instanceof CharSequence) + value = new ObjectMap((CharSequence)value).setBeanContext(beanMeta.ctx); + else + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}''", name, propertyClass.getName(), findClassName(value)); + } + + Map valueMap = (Map)value; + Map propMap = (Map)r; + ClassMeta<?> valueType = rawTypeMeta.getValueType(); + + // If the property type is abstract, then we either need to reuse the existing + // map (if it's not null), or try to assign the value directly. + if (! rawTypeMeta.canCreateNewInstance()) { + if (propMap == null) { + if (setter == null && field == null) + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter or public field is defined, and the current value is null", name, propertyClass.getName(), findClassName(value)); + + if (propertyClass.isInstance(valueMap)) { + if (! valueType.isObject()) { + for (Map.Entry e : (Set<Map.Entry>)valueMap.entrySet()) { + Object v = e.getValue(); + if (v != null && ! valueType.getInnerClass().isInstance(v)) + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because the value types in the assigned map do not match the specified ''elementClass'' attribute on the property, and the property value is currently null", name, propertyClass.getName(), findClassName(value)); + } + } + if (setter != null) + setter.invoke(bean, valueMap); + else + field.set(bean, valueMap); + return r; + } + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{2}'' to object of type ''{2}'' because the assigned map cannot be converted to the specified type because the property type is abstract, and the property value is currently null", name, propertyClass.getName(), findClassName(value)); + } + } else { + if (propMap == null) { + propMap = (Map)propertyClass.newInstance(); + if (setter != null) + setter.invoke(bean, propMap); + else if (field != null) + field.set(bean, propMap); + else + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter or public field is defined on this property, and the existing property value is null", name, propertyClass.getName(), findClassName(value)); + } else { + propMap.clear(); + } + } + + // Set the values. + for (Map.Entry e : (Set<Map.Entry>)valueMap.entrySet()) { + Object k = e.getKey(); + Object v = e.getValue(); + if (! valueType.isObject()) + v = beanMeta.ctx.convertToType(v, valueType); + propMap.put(k, v); + } + + } else if (isCollection) { + + if (! (value instanceof Collection)) { + if (value instanceof CharSequence) + value = new ObjectList((CharSequence)value).setBeanContext(beanMeta.ctx); + else + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}''", name, propertyClass.getName(), findClassName(value)); + } + + Collection valueList = (Collection)value; + Collection propList = (Collection)r; + ClassMeta elementType = rawTypeMeta.getElementType(); + + // If the property type is abstract, then we either need to reuse the existing + // collection (if it's not null), or try to assign the value directly. + if (! rawTypeMeta.canCreateNewInstance()) { + if (propList == null) { + if (setter == null && field == null) + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter or public field is defined, and the current value is null", name, propertyClass.getName(), findClassName(value)); + + if (propertyClass.isInstance(valueList)) { + if (! elementType.isObject()) { + List l = new ObjectList(valueList); + for (ListIterator<Object> i = l.listIterator(); i.hasNext(); ) { + Object v = i.next(); + if (v != null && (! elementType.getInnerClass().isInstance(v))) { + i.set(bc.convertToType(v, elementType)); + } + } + valueList = l; + } + if (setter != null) + setter.invoke(bean, valueList); + else + field.set(bean, valueList); + return r; + } + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because the assigned map cannot be converted to the specified type because the property type is abstract, and the property value is currently null", name, propertyClass.getName(), findClassName(value)); + } + propList.clear(); + } else { + if (propList == null) { + propList = (Collection)propertyClass.newInstance(); + if (setter != null) + setter.invoke(bean, propList); + else if (field != null) + field.set(bean, propList); + else + throw new BeanRuntimeException(beanMeta.c, "Cannot set property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is defined on this property, and the existing property value is null", name, propertyClass.getName(), findClassName(value)); + } else { + propList.clear(); + } + } + + // Set the values. + for (Object v : valueList) { + if (! elementType.isObject()) + v = beanMeta.ctx.convertToType(v, elementType); + propList.add(v); + } + + } else { + if (transform != null && value != null && isParentClass(transform.getTransformedClass(), value.getClass())) { + value = transform.normalize(value, rawTypeMeta); + } else { + value = beanMeta.ctx.convertToType(value, rawTypeMeta); + } + if (setter != null) + setter.invoke(bean, new Object[] { value }); + else if (field != null) + field.set(bean, value); + } + + return r; + + } catch (BeanRuntimeException e) { + throw e; + } catch (Exception e) { + if (beanMeta.ctx.ignoreInvocationExceptionsOnSetters) { + if (rawTypeMeta.isPrimitive()) + return rawTypeMeta.getPrimitiveDefault(); + return null; + } + throw new BeanRuntimeException(beanMeta.c, "Error occurred trying to set property ''{0}''", name).initCause(e); + } + } catch (ParseException e) { + throw new BeanRuntimeException(e); + } + } + + /** + * Sets an array field on this bean. + * Works on both <code>Object</code> and primitive arrays. + * + * @param bean The bean of the field. + * @param l The collection to use to set the array field. + * @throws IllegalArgumentException Thrown by method invocation. + * @throws IllegalAccessException Thrown by method invocation. + * @throws InvocationTargetException Thrown by method invocation. + */ + protected void setArray(T bean, List l) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + Object array = ArrayUtils.toArray(l, this.rawTypeMeta.getElementType().getInnerClass()); + if (setter != null) + setter.invoke(bean, array); + else if (field != null) + field.set(bean, array); + else + throw new BeanRuntimeException(beanMeta.c, "Attempt to initialize array property ''{0}'', but no setter or field defined.", name); + } + + /** + * Adds a value to a {@link Collection} or array property. + * Note that adding values to an array property is inefficient for large + * arrays since it must copy the array into a larger array on each operation. + * + * @param m The bean of the field being set. + * @param value The value to add to the field. + * @throws BeanRuntimeException If field is not a collection or array. + */ + public void add(BeanMap<T> m, Object value) throws BeanRuntimeException { + + BeanContext bc = beanMeta.ctx; + + // Read-only beans get their properties stored in a cache. + if (m.bean == null) { + if (! m.propertyCache.containsKey(name)) + m.propertyCache.put(name, new ObjectList(bc)); + ((ObjectList)m.propertyCache.get(name)).add(value); + return; + } + + boolean isCollection = rawTypeMeta.isCollection(); + boolean isArray = rawTypeMeta.isArray(); + + if (! (isCollection || isArray)) + throw new BeanRuntimeException(beanMeta.c, "Attempt to add element to property ''{0}'' which is not a collection or array", name); + + Object bean = m.getBean(true); + + ClassMeta<?> elementType = rawTypeMeta.getElementType(); + + try { + Object v = bc.convertToType(value, elementType); + + if (isCollection) { + Collection c = null; + if (getter != null) { + c = (Collection)getter.invoke(bean, (Object[])null); + } else if (field != null) { + c = (Collection)field.get(bean); + } else { + throw new BeanRuntimeException(beanMeta.c, "Attempt to append to collection property ''{0}'', but no getter or field defined.", name); + } + + if (c != null) { + c.add(v); + return; + } + + if (rawTypeMeta.canCreateNewInstance()) + c = (Collection)rawTypeMeta.newInstance(); + else + c = new ObjectList(bc); + + c.add(v); + + if (setter != null) + setter.invoke(bean, c); + else if (field != null) + field.set(bean, c); + else + throw new BeanRuntimeException(beanMeta.c, "Attempt to initialize collection property ''{0}'', but no setter or field defined.", name); + + } else /* isArray() */ { + + if (m.arrayPropertyCache == null) + m.arrayPropertyCache = new TreeMap<String,List<?>>(); + + List l = m.arrayPropertyCache.get(name); + if (l == null) { + l = new LinkedList(); // ArrayLists and LinkLists appear to perform equally. + m.arrayPropertyCache.put(name, l); + + // Copy any existing array values into the temporary list. + Object oldArray; + if (getter != null) + oldArray = getter.invoke(bean, (Object[])null); + else if (field != null) + oldArray = field.get(bean); + else + throw new BeanRuntimeException(beanMeta.c, "Attempt to append to array property ''{0}'', but no getter or field defined.", name); + ArrayUtils.copyToList(oldArray, l); + } + + // Add new entry to our array. + l.add(v); + } + + } catch (BeanRuntimeException e) { + throw e; + } catch (Exception e) { + throw new BeanRuntimeException(e); + } + } + + /** + * Returns all instances of the specified annotation in the hierarchy of this bean property. + * <p> + * Searches through the class hierarchy (e.g. superclasses, interfaces, packages) for all + * instances of the specified annotation. + * + * @param a The class to find annotations for. + * @return A list of annotations ordered in child-to-parent order. Never <jk>null</jk>. + */ + public <A extends Annotation> List<A> findAnnotations(Class<A> a) { + List<A> l = new LinkedList<A>(); + if (field != null) { + addIfNotNull(l, field.getAnnotation(a)); + appendAnnotations(a, field.getType(), l); + } + if (getter != null) { + addIfNotNull(l, getter.getAnnotation(a)); + appendAnnotations(a, getter.getReturnType(), l); + } + if (setter != null) { + addIfNotNull(l, setter.getAnnotation(a)); + appendAnnotations(a, setter.getReturnType(), l); + } + appendAnnotations(a, this.getBeanMeta().getClassMeta().getInnerClass(), l); + return l; + } + + private Object transform(Object o) throws SerializeException { + // First use transform defined via @BeanProperty. + if (transform != null) + return transform.transform(o); + if (o == null) + return null; + // Otherwise, look it up via bean context. + if (rawTypeMeta.hasChildPojoTransforms()) { + Class c = o.getClass(); + ClassMeta<?> cm = rawTypeMeta.innerClass == c ? rawTypeMeta : beanMeta.ctx.getClassMeta(c); + PojoTransform f = cm.getPojoTransform(); + if (f != null) + return f.transform(o); + } + return o; + } + + private Object normalize(Object o) throws ParseException { + if (transform != null) + return transform.normalize(o, rawTypeMeta); + if (o == null) + return null; + if (rawTypeMeta.hasChildPojoTransforms()) { + Class c = o.getClass(); + ClassMeta<?> cm = rawTypeMeta.innerClass == c ? rawTypeMeta : beanMeta.ctx.getClassMeta(c); + PojoTransform f = cm.getPojoTransform(); + if (f != null) + return f.normalize(o, rawTypeMeta); + } + return o; + } + + private Object applyChildPropertiesFilter(ClassMeta cm, Object o) { + if (o == null) + return null; + if (cm.isBean()) + return new BeanMap(o, new BeanMetaFiltered(cm.getBeanMeta(), properties)); + if (cm.isMap()) + return new FilteredMap((Map)o, properties); + if (cm.isObject()) { + if (o instanceof Map) + return new FilteredMap((Map)o, properties); + BeanMeta bm = this.getBeanMeta().ctx.getBeanMeta(o.getClass()); + if (bm != null) + return new BeanMap(o, new BeanMetaFiltered(cm.getBeanMeta(), properties)); + } + return o; + } + + private String findClassName(Object o) { + if (o == null) + return null; + if (o instanceof Class) + return ((Class<?>)o).getName(); + return o.getClass().getName(); + } + + @Override /* Object */ + public String toString() { + return name + ": " + this.rawTypeMeta.getInnerClass().getName() + ", field=["+field+"], getter=["+getter+"], setter=["+setter+"]"; + } +}
