de.tsl2.nano.bean.def.BeanValue Maven / Gradle / Ivy
/*
* File: $HeadURL$
* Id : $Id$
*
* created by: Thomas Schneider
* created on: Oct 15, 2010
*
* Copyright: (c) Thomas Schneider 2010, all rights reserved
*/
package de.tsl2.nano.bean.def;
import static de.tsl2.nano.bean.def.IBeanCollector.MODE_ALL;
import static de.tsl2.nano.bean.def.IBeanCollector.MODE_ALL_SINGLE;
import static de.tsl2.nano.bean.def.IPresentable.POSTFIX_SELECTOR;
import java.io.File;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.text.Format;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import de.tsl2.nano.action.CommonAction;
import de.tsl2.nano.action.IAction;
import de.tsl2.nano.action.IConstraint;
import de.tsl2.nano.bean.BeanUtil;
import de.tsl2.nano.bean.IValueAccess;
import de.tsl2.nano.bean.ValueHolder;
import de.tsl2.nano.collection.CollectionUtil;
import de.tsl2.nano.collection.Entry;
import de.tsl2.nano.collection.MapEntrySet;
import de.tsl2.nano.core.ENV;
import de.tsl2.nano.core.ManagedException;
import de.tsl2.nano.core.cls.BeanAttribute;
import de.tsl2.nano.core.cls.BeanClass;
import de.tsl2.nano.core.cls.IAttribute;
import de.tsl2.nano.core.cls.PrimitiveUtil;
import de.tsl2.nano.core.log.LogFactory;
import de.tsl2.nano.core.messaging.ChangeEvent;
import de.tsl2.nano.core.messaging.EventController;
import de.tsl2.nano.core.messaging.IListener;
import de.tsl2.nano.core.util.ByteUtil;
import de.tsl2.nano.core.util.FileUtil;
import de.tsl2.nano.core.util.ListSet;
import de.tsl2.nano.core.util.StringUtil;
import de.tsl2.nano.core.util.Util;
/**
* BeanAttribute holding the bean instance, observers and exact attribute definitions - with validation.
* {@link IAttributeDefinition} will constrain the attribute value. it is able to add a value change listener as
* observer ({@link #changeHandler()}. {@link EventController#addListener(IListener)}.
*
*
* @author Thomas Schneider
* @version $Revision$
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class BeanValue extends AttributeDefinition implements IValueDefinition {
/** serialVersionUID */
private static final long serialVersionUID = 8690371851484504875L;
private static final Log LOG = LogFactory.getLog(BeanValue.class);
transient Object instance;
transient Selector selector;
protected transient Bean> parent;
/** a cache of all created beanvalues - if bean cache is not deaktivated */
protected static final List beanValueCache = new LinkedList();
/** for performance enhancement one bean to be used to search inside the {@link #beanValueCache} */
protected static final BeanValue searchBV = new BeanValue();
/**
* constructor to be serializable
*/
protected BeanValue() {
super();
}
/**
* constructor
*
* @param bean the instance to wrap and reflect
* @param attribute beanattribute or attribute-expression
*/
public BeanValue(Object bean, IAttribute attribute) {
super(attribute);
this.instance = bean;
beanValueCache.add(this);
}
public BeanValue(Object bean, String name, IConstraint constraint) {
super(name, constraint);
this.instance = bean;
beanValueCache.add(this);
}
/**
* constructor
*
* @param bean the instance to wrap and reflect
* @param readAccessMethod getter-method defining the beans attribute
*/
protected BeanValue(Object bean, Method readAccessMethod) {
super(readAccessMethod);
this.instance = bean;
}
/**
* getBean
*
* @return
*/
@Override
public Object getInstance() {
return instance;
}
/**
* @param beanInstance The bean to set.
*/
void setInstance(Object beanInstance) {
if (isVirtualAccess() && beanInstance != null && !(beanInstance instanceof IValueAccess)) {
throw new IllegalArgumentException("instance of virtual attribute " + this
+ " must be of type IValueAccess, but is: " + beanInstance);
}
this.instance = beanInstance;
}
@Override
public Class getType() {
//TODO: set UNDEFINED instead of object
if (getConstraint().getType() == Object.class || getConstraint().getType().isInterface()) {
//if a value-expression was defined, the valueexpression-type has to be used!
if (attribute.isVirtual()) {
getConstraint().setType(super.getType());
} else if (temporalType() != null) {
getConstraint().setType((Class) temporalType());
} else if (isVirtual() && instance != null) {
getConstraint().setType(((IValueAccess) instance).getType());
} else if (instance != null && ENV.get("value.use.instancetype", true)) {
try {
T value = getValue();
//don't use inner class infos or enum values
if (value != null && !value.getClass().isAnonymousClass()
&& value.getClass().getDeclaringClass() == null) {
getConstraint().setType((Class) BeanClass.getDefiningClass(value.getClass()));
}
} catch (Exception e) {
LOG.warn("couldn't evaluate type through instance. using method-returntype instead. error was: "
+ e.toString());
}
} else {
getConstraint().setType(super.getType());
}
}
return getConstraint().getType();
}
@Override
public Format getFormat() {
if (getConstraint().getFormat() == null) {
/*
* on jpa persistable collections, the origin instance has to be hold for orphan removals.
*/
if (Collection.class.isAssignableFrom(getType())) {
T value = getValue();
if (value != null) {
getConstraint().setFormat(
new CollectionExpressionFormat(getGenericType(0), (Collection) value));
}
}
}
return super.getFormat();
}
/**
* getValue
*
* @return bean attribute value
*/
@Override
public T getValue() {
return getValue(instance);
}
/**
* getValueText
*
* @return formatted value
*/
public String getValueText() {
T v = getValue();
try {
return v != null ? getFormat().format(v) : "";
} catch (Exception e) {
LOG.warn(e);
status = isValid(v);
/*
* the toString() conversion should not break the application flow.
* if a PersistentSet is not initialized but already serialized, the toString()
* would throw a LazyInitializationException
*/
try {
return v.toString();
} catch (Exception e1) {
Bean.clearCache();
return v.getClass() + ": " + e.toString();
}
}
}
/**
* can be used, if for example a bean value is a byte-array to be used from outside (perhaps on an html-page loading
* an image). if the value is not a byte-array, it will be created through serialization. this byte-array will be
* saved (if not saved before) to a file inside a temp-directory of your environment.
*
* @return temporary file-path of the current bean-value, saved as byte-array.
*/
public File getValueFile() {
return String.class.isAssignableFrom(getType()) ? new File(Attachment.getFilename(instance, getName(), (String) getValue())) : Attachment.getValueFile(getId(), getValue());
}
@Override
public T getParsedValue(String source) {
if (Attachment.isAttachment(this)) {
//--> attachment, holding the data (not a file-name only)
//first: load the transferred temporary file
byte[] transferredBytes = FileUtil.getFileBytes(Attachment.getFilename(instance, getName(), source), null);
//second: save that as new file - only for this instance!
byte[] bytes = Attachment.getFileBytes(getId(), transferredBytes);
//perhaps, convert the byte[] to an instanceof of Blob, InputStream,...
return ByteUtil.toByteStream(bytes, getType());
} else {
return super.getParsedValue(source);
}
}
/**
* setParsedValue
*
* @param source source to parse
* @return object for given source text or null
*/
public T setParsedValue(String source) {
try {
T v = null;
/*
* if 'allowed values' are defined, re-use their instances!
* it is not possible to move that block to value-expression,
* because value-expression doesn't have access to the attribute-definition!
*/
if (!Util.isEmpty(source) && getConstraint().getAllowedValues() != null
&& !getConstraint().getType().isEnum()) {
String name;
Object id;
for (Object allowed : getConstraint().getAllowedValues()) {
id = Bean.getBean((Serializable) allowed).getId();
name = Bean.getBean((Serializable) allowed).toString();
if ((id != null && id.equals(source)) || name.equals(source)) {
LOG.debug("recognition of selected value '" + name + "' successful!");
v = (T) allowed;
break;
}
}
if (v == null) {
throw ManagedException.illegalArgument(source, getConstraint().getAllowedValues());
}
} else {
v = getParsedValue(source);
}
setValue(v);
return v;
} catch (Exception ex) {
//ok, don't set the new value!
// LOG.error(ex.toString());
ManagedException.forward(ex);
}
return null;
}
/**
* setValue. informs all registered listeners
*
* @param value new value
*/
@Override
public void setValue(T value) {
final Object oldValue = getValue();
final ChangeEvent event = new ChangeEvent(this, false, false, oldValue, value);
changeHandler().fireEvent(event);
if (!event.breakEvent) {
if (LOG.isDebugEnabled()) {
LOG.debug("setting new value for attribute '" + getName() + "': " + value);
}
setValue(instance, value);
if (isDoValidation()) {
T assignValue = getValue();
value = //the type may be changed through wrap()
value != null && oldValue != assignValue
&& !PrimitiveUtil.isAssignableFrom(getType(), value.getClass()) ? assignValue : value;
status = isValid(value);
}
event.hasChanged = true;
changeHandler().fireEvent(event);
}
}
@Override
public IValueDefinition getRelation(String name) {
return getRelation(instance, ValuePath.splitChain(name));
}
/**
* TODO: check for duplication with ValueBean and BeanClass
* get the beanvalue instance of the given attribute value. useful to walk through relations (recursive!). will stop
* on the first relation having a value of null!
*
* @param root root instance to start from
* @param chain of attribute names
* @return bean value or null
*/
protected IValueDefinition getRelation(Object root, String... chain) {
assert chain.length > 0 : "chain must not be empty!";
BeanValue bv = root == null ? null : getBeanValue(root, chain[0]);
if (bv == null || chain.length == 1) {
return bv;
}
return getRelation(bv.getValue(), Arrays.copyOfRange(chain, 1, chain.length));
}
protected IAttributeDefinition createAttributeDefinition(String name) {
return BeanValue.getBeanValue(instance, name);
}
/**
* creates a new bean value or - if existing in cache, reuses an already created bean value with that bean-instance
* and attributename.
*
*
* @param bean bean instance
* @param attributeName attribute definition
* @return new bean value instance
*/
public static final BeanValue getBeanValue(Object bean, String attributeName) {
final BeanAttribute attribute = BeanAttribute.getBeanAttribute(bean.getClass(), attributeName, true);
searchBV.instance = bean;
searchBV.attribute = attribute;
int i = beanValueCache.indexOf(searchBV);
if (i != -1) {
return beanValueCache.get(i);
} else {
BeanValue tbv = new BeanValue(bean, attribute.getAccessMethod());
beanValueCache.add(tbv);
return tbv;
}
}
/**
* removes this bean value from internal cache
*
* @return result of {@link List#remove(Object)}
*/
public boolean removeFromCache() {
return beanValueCache.remove(this);
}
/**
* clears cache of already created bean values.
*/
public static final int clearCache() {
int cleared = beanValueCache.size();
LOG.info("clearing beanvalue cache of " + cleared + " elements");
beanValueCache.clear();
searchBV.instance = null;
searchBV.attribute = null;
return cleared;
}
/**
* a new listener will be created to set the new value of this bean to the given beanvalue.
*
* @param anotherValue beanvalue to bind
*/
public void bind(final BeanValue anotherValue) {
// if (!getType().equals(anotherValue.getType()))
// ManagedException.implementationError("binding beanvalues must have the same type", anotherValue.getType());
changeHandler().addListener(new IListener() {
@Override
public void handleEvent(ChangeEvent changeEvent) {
if (changeEvent.hasChanged) {
anotherValue.setValue((T) changeEvent.newValue);
}
}
});
}
/**
* {@inheritDoc}
*/
@Override
public String getId() {
if (isVirtual()) {
String name;
if (description != null) {
return description;
} else if ((name=getName()) != null){
return StringUtil.toString(getParent()) + getName();
} else {
return BeanUtil.createUUID();
}
} else {
return super.getId();
}
}
/**
* {@inheritDoc}
*/
@Override
public String getName() {
if (isVirtualAccess()) {
if (description == null) {
description = StringUtil.toFirstUpper(super.getName());
}
return description;
} else {
return super.getName();
}
}
@Override
public boolean equals(Object obj) {
return hashCode() == obj.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((instance == null) ? 0 : instance.hashCode());
/*
* on using e.g. ValueHolders, the instance and its attribute (=value) are identical.
* then it is possible to distinguish them through the description, if defined
*/
result = prime * result + ((description == null) ? 0 : description.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public int compareTo(IAttribute o) {
//not really a compareTo...but a base for equals
if (!(o instanceof BeanValue)) {
return -1;
}
if (instance != ((BeanValue) o).instance) {
return -1;
}
if (description != null && !description.equals(((BeanValue) o).description)) {
return -1;
}
return super.compareTo(o);
}
/**
* if the bean instance is of type {@link ValueHolder}, the attribute is virtual - no special bean-attribute is
* available, the attribute name is always {@link ValueHolder#getValue()} .
*
* @return true, if the instance is of type {@link ValueHolder}.
*/
@Override
public boolean isVirtual() {
return super.isVirtual() || instance instanceof IValueAccess;
}
/**
* a beans value may be a bean again (stored as {@link #instance}).
*
* @return true, if instance is of type {@link Bean}
*/
public boolean isBean() {
return instance instanceof Bean;
}
/**
* a beans value may be a bean-collector (stored as {@link #instance}).
*
* @return true, if instance is of type {@link BeanCollector}
*/
public boolean isBeanCollector() {
return (instance instanceof IValueAccess) && ((IValueAccess>) instance).getValue() instanceof BeanCollector;
}
/**
* the parent may be a bean definition, holding this attribute. setting this parent through {@link #setParent(Bean)}
* will provide a bean-value-tree.
*
* @return Returns a given parent or the standard parent evaluated from instance.
*/
public Bean> getParent() {
return parent != null || instance == null ? parent : Bean.getBean((Serializable) instance);
}
/**
* see {@link #getParent()}.
*
* @param parent The parent to set.
*/
public void setParent(Bean> parent) {
this.parent = parent;
}
/**
* as {@link #getId()} returns an identifier for the beans attribute, this id is an identifier for the value of the
* beans attribute - a combination of it's parent bean-id and the attribute name.
*
* @return bean-value identifier
*/
public String getValueId() {
return parent != null ? parent.getName() + "." + parent.getId() + "." + getName() : getId();
}
Selector selector() {
if (selector == null) {
selector = new Selector(this);
}
return selector;
}
/** returns an optional action as finder/assigner - useful to select a new value */
@SuppressWarnings("serial")
public IAction> getSelectorAction() {
//TODO: move that to the attribute: IPresentable
return new SecureAction>(getName() + POSTFIX_SELECTOR,
ENV.get("field.selector.text", "...")) {
@Override
public IBeanCollector, T> action() throws Exception {
BeanCollector, ?> beanCollector;
Composition comp = composition() ? CompositionFactory.createComposition(BeanValue.this) : null;
boolean enabled = BeanValue.this.getPresentation().getEnabler().isActive();
if (isMultiValue()) {
Selector vsel = selector();
Collection collection = vsel.getValueAsCollection();
beanCollector =
(BeanCollector, ?>) Util.untyped(BeanCollector.getBeanCollector(
vsel.getCollectionEntryType(),
collection,
enabled ? MODE_ALL : 0,
comp));
if (collection == null) {
vsel.createCollectionValue();
}
beanCollector.setSelectionProvider(new SelectionProvider(beanCollector.getCurrentData()));
} else {
LinkedList selection = new LinkedList();
T v = getValue();
if (v != null) {
selection.add(v);
}
beanCollector =
(BeanCollector, ?>) Util.untyped(BeanCollector.getBeanCollector(getType(), selection, enabled
? MODE_ALL_SINGLE : 0, comp));
beanCollector.setSelectionProvider(new SelectionProvider(selection));
}
return (IBeanCollector, T>) beanCollector;
}
};
}
@Override
public String toString() {
return (isVirtual() ? description : getName()) + "=" + getValue();
}
/**
* connects this attribute to a selector (see {@link #getSelectorAction()} to assign values from a list.
*
* @param parent the parent bean of this attribute (must be given as {@link #getParent()} not allways is filled.
* @return selector (see {@link #getSelectorAction()}.
*/
@SuppressWarnings("serial")
public IBeanCollector, ?> connectToSelector(BeanDefinition> parent) {
final IAction> selectorAction = getSelectorAction();
final IBeanCollector, ?> collector = (IBeanCollector, ?>) selectorAction.activate();
final ISelectionProvider> selectionProvider = collector.getSelectionProvider();
parent.connect(getName(), selectionProvider, new CommonAction © 2015 - 2025 Weber Informatics LLC | Privacy Policy