org.apache.juneau.BeanMap Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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.common.internal.StringUtils.*;
import static org.apache.juneau.common.internal.ThrowableUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import java.io.*;
import java.util.*;
import java.util.function.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.common.internal.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.reflect.*;
import org.apache.juneau.swap.*;
/**
* Java bean wrapper class.
*
* Description
*
* 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.
*
*
* Use the {@link BeanContext} class to create instances of this class.
*
*
Bean property order
*
* The order of the properties returned by the {@link Map#keySet() keySet()} and {@link Map#entrySet() entrySet()}
* methods are as follows:
*
* -
* If {@link Bean @Bean} annotation is specified on class, then the order is the same as the list of properties
* in the annotation.
*
-
* 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).
*
*
* POJO swaps
*
* If {@link ObjectSwap ObjectSwaps} 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.
*
* See Also:
*
*
* @param Specifies the type of object that this map encapsulates.
*/
public class BeanMap extends AbstractMap implements Delegate {
/** The wrapped object. */
protected T bean;
/** Temporary holding cache for beans with read-only properties. Normally null. */
protected Map propertyCache;
/** Temporary holding cache for bean properties of array types when the add() method is being used. */
protected Map> arrayPropertyCache;
/** The BeanMeta associated with the class of the object. */
protected BeanMeta meta;
private final BeanSession session;
private final String typePropertyName;
/**
* Convenience method for wrapping a bean inside a {@link BeanMap}.
*
* @param The bean type.
* @param bean The bean being wrapped.
* @return A new {@link BeanMap} instance wrapping the bean.
*/
public static BeanMap of(T bean) {
return BeanContext.DEFAULT_SESSION.toBeanMap(bean);
}
/**
* Instance of this class are instantiated through the BeanContext class.
*
* @param session The bean session object that created this bean map.
* @param bean The bean to wrap inside this map.
* @param meta The metadata associated with the bean class.
*/
protected BeanMap(BeanSession session, T bean, BeanMeta meta) {
this.session = session;
this.bean = bean;
this.meta = meta;
if (meta.constructorArgs.length > 0)
propertyCache = new TreeMap<>();
this.typePropertyName = session.getBeanTypePropertyName(meta.classMeta);
}
/**
* Returns the metadata associated with this bean map.
*
* @return The metadata associated with this bean map.
*/
public BeanMeta getMeta() {
return meta;
}
/**
* Returns the bean session that created this bean map.
*
* @return The bean session that created this bean map.
*/
public final BeanSession getBeanSession() {
return session;
}
/**
* Returns the wrapped bean object.
*
*
* Triggers bean creation if bean has read-only properties set through a constructor defined by the
* {@link Beanc @Beanc} 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) {
arrayPropertyCache.forEach((k,v) -> {
try {
getPropertyMeta(k).setArray(b, v);
} catch (Exception e1) {
throw asRuntimeException(e1);
}
});
arrayPropertyCache = null;
}
// Initialize any null Optional fields.
for (BeanPropertyMeta pMeta : this.meta.propertyArray) {
ClassMeta> cm = pMeta.getClassMeta();
if (cm.isOptional() && pMeta.get(this, pMeta.getName()) == null)
pMeta.set(this, pMeta.getName(), cm.getOptionalDefault());
}
// Do the same for hidden fields.
this.meta.hiddenProperties.forEach((k,v) -> {
ClassMeta> cm = v.getClassMeta();
if (cm.isOptional() && v.get(this, v.getName()) == null)
v.set(this, v.getName(), cm.getOptionalDefault());
});
return b;
}
/**
* Returns the wrapped bean object.
*
*
* If create is false , then this method may return null if the bean has read-only
* properties set through a constructor defined by the {@link Beanc @Beanc} annotation.
*
*
* 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;
ConstructorInfo 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.invoke(args);
propertyCache.forEach((k,v) -> put(k, v));
propertyCache = null;
} catch (IllegalArgumentException e) {
throw new BeanRuntimeException(e, meta.classMeta.innerClass, "IllegalArgumentException occurred on call to class constructor ''{0}'' with argument types ''{1}''", c.getSimpleName(), Json5Serializer.DEFAULT.toString(ClassUtils.getClasses(args)));
} catch (Exception e) {
throw new BeanRuntimeException(e);
}
}
return bean;
}
/**
* Sets a property on the bean.
*
*
* If there is a {@link ObjectSwap} 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.swaps.TemporalDateSwap.IsoInstant} swap associated with it through the
* {@link Swap#value() @Swap(value)} annotation, the value being passed in must be
* a String containing an ISO8601 date-time string value.
*
*
Example:
*
* // Construct a bean with a 'birthDate' Date field
* Person person = new Person();
*
* // Create a bean context and add the ISO8601 date-time swap
* BeanContext beanContext = BeanContext.create ().swaps(DateSwap.ISO8601DT.class ).build();
*
* // Wrap our bean in a bean map
* BeanMap<Person> beanMap = beanContext .toBeanMap(person );
*
* // Set the field
* beanMap .put("birthDate" , "'1901-03-03T04:05:06-5000'" );
*
*
* @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 true , then the old value of the
* property is returned.
* Otherwise, this method always returns null .
* @throws
* RuntimeException if any of the following occur.
*
* - BeanMapEntry does not exist on the underlying object.
*
- Security settings prevent access to the underlying object setter method.
*
- An exception occurred inside the setter method.
*
*/
@Override /* Map */
public Object put(String property, Object value) {
BeanPropertyMeta p = getPropertyMeta(property);
if (p == null) {
if (meta.ctx.isIgnoreUnknownBeanProperties() || property.equals(typePropertyName))
return meta.onWriteProperty(bean, property, null);
p = getPropertyMeta("*");
if (p == null)
throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property);
}
return p.set(this, property, value);
}
@Override /* Map */
public boolean containsKey(Object property) {
if (getPropertyMeta(emptyIfNull(property)) != null)
return true;
return super.containsKey(property);
}
/**
* Add a value to a collection or array property.
*
*
* 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 org.apache.juneau.xml.annotation.Xml#childName() @Xml(childName)} is specified).
* @param value The value to add to the collection or array.
*/
public void add(String property, Object value) {
BeanPropertyMeta p = getPropertyMeta(property);
if (p == null) {
if (meta.ctx.isIgnoreUnknownBeanProperties())
return;
throw new BeanRuntimeException(meta.c, "Bean property ''{0}'' not found.", property);
}
p.add(this, property, value);
}
/**
* Gets a property on the bean.
*
*
* If there is a {@link ObjectSwap} 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.swaps.TemporalDateSwap.IsoInstant} swap associated with it through the
* {@link Swap#value() @Swap(value)} annotation, this method will return a String containing an
* ISO8601 date-time string value.
*
*
Example:
*
* // Construct a bean with a 'birthDate' Date field
* Person person = new Person();
* person .setBirthDate(new Date(1, 2, 3, 4, 5, 6));
*
* // Create a bean context and add the ISO8601 date-time swap
* BeanContext beanContext = BeanContext.create ().swaps(DateSwap.ISO8601DT.class ).build();
*
* // Wrap our bean in a bean map
* BeanMap<Person> beanMap = beanContext .toBeanMap(person );
*
* // Get the field as a string (i.e. "'1901-03-03T04:05:06-5000'")
* String birthDate = beanMap .get("birthDate" );
*
*
* @param property The name of the property to get.
* @return The property value.
* @throws RuntimeException if any of the following occur.
*
* - BeanMapEntry does not exist on the underlying object.
*
- Security settings prevent access to the underlying object getter method.
*
- An exception occurred inside the getter method.
*
*/
@Override /* Map */
public Object get(Object property) {
String pName = stringify(property);
BeanPropertyMeta p = getPropertyMeta(pName);
if (p == null)
return meta.onReadProperty(this.bean, pName, null);
return p.get(this, pName);
}
/**
* Same as {@link #get(Object)} but casts the value to the specific type.
*
* @param The type to cast to.
* @param property The name of the property to get.
* @param c The type to cast to.
* @return The property value.
* @throws RuntimeException if any of the following occur.
*
* - BeanMapEntry does not exist on the underlying object.
*
- Security settings prevent access to the underlying object getter method.
*
- An exception occurred inside the getter method.
*
* @throws ClassCastException if property is not the specified type.
*/
@SuppressWarnings("unchecked")
public T2 get(String property, Class c) {
String pName = stringify(property);
BeanPropertyMeta p = getPropertyMeta(pName);
if (p == null)
return (T2)meta.onReadProperty(this.bean, pName, null);
return (T2)p.get(this, pName);
}
/**
* Same as {@link #get(Object)} except bypasses the POJO filter associated with the bean property or bean filter
* associated with the bean class.
*
* @param property The name of the property to get.
* @return The raw property value.
*/
public Object getRaw(Object property) {
String pName = stringify(property);
BeanPropertyMeta p = getPropertyMeta(pName);
if (p == null)
return null;
return p.getRaw(this, pName);
}
/**
* Convenience method for setting multiple property values by passing in JSON text.
*
* Example:
*
* beanMap .load("{name:'John Smith',age:21}" )
*
*
* @param input The text that will get parsed into a map and then added to this map.
* @return This object.
* @throws ParseException Malformed input encountered.
*/
public BeanMap load(String input) throws ParseException {
putAll(JsonMap.ofJson(input));
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.
* @throws ParseException Malformed input encountered.
* @throws IOException Thrown by Reader .
*/
public BeanMap load(Reader r, ReaderParser p) throws ParseException, IOException {
putAll(JsonMap.ofText(r, p));
return this;
}
/**
* Convenience method for loading this map with the contents of the specified map.
*
*
* 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.
*/
@SuppressWarnings({"unchecked","rawtypes"})
public BeanMap load(Map entries) {
putAll(entries);
return this;
}
/**
* Returns the names of all properties associated with the bean.
*
*
* The returned set is unmodifiable.
*/
@Override /* Map */
public Set keySet() {
if (meta.dynaProperty == null)
return meta.properties.keySet();
Set l = set();
meta.properties.forEach((k,v) -> {
if (! "*".equals(k))
l.add(k);
});
try {
l.addAll(meta.dynaProperty.getDynaMap(bean).keySet());
} catch (Exception e) {
throw new BeanRuntimeException(e);
}
return l;
}
/**
* Returns the specified property on this bean map.
*
*
* 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.
*
*
* 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 getProperty(String propertyName) {
BeanPropertyMeta p = getPropertyMeta(propertyName);
if (p == null)
return null;
return new BeanMapEntry(this, p, propertyName);
}
/**
* Returns the metadata on the specified property.
*
* @param propertyName The name of the bean property.
* @return Metadata on the specified property, or null if that property does not exist.
*/
public BeanPropertyMeta getPropertyMeta(String propertyName) {
return meta.getPropertyMeta(propertyName);
}
/**
* Returns the {@link ClassMeta} of the wrapped bean.
*
* @return The class type of the wrapped bean.
*/
@Override /* Delegate */
public ClassMeta getClassMeta() {
return this.meta.getClassMeta();
}
/**
* Extracts the specified field values from this bean and returns it as a simple Map.
*
* @param fields The fields to extract.
* @return
* A new map with fields as key-value pairs.
*
Note that modifying the values in this map will also modify the underlying bean.
*/
public Map getProperties(String...fields) {
return new FilteredMap<>(null, this, fields);
}
/**
* Invokes all the getters on this bean and consumes the results.
*
* @param valueFilter Filter to apply to value before applying action.
* @param action The action to perform.
* @return The list of all bean property values.
*/
public BeanMap forEachValue(Predicate