/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Jspresso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.model.descriptor.basic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.jspresso.framework.model.descriptor.DescriptorException;
import org.jspresso.framework.model.descriptor.IPropertyDescriptor;
import org.jspresso.framework.util.bean.integrity.IPropertyProcessor;
import org.jspresso.framework.util.descriptor.DefaultIconDescriptor;
import org.jspresso.framework.util.exception.NestedRuntimeException;
import org.jspresso.framework.util.gate.IGate;
import org.jspresso.framework.util.lang.StringUtils;
/**
* This is the abstract base class for all property descriptors. It mainly
* serves for factorizing all commons properties for property descriptors.
* <p/>
* A property descriptor is used for describing a component/entity/interface
* property (<i>Java Beans</i> semantic).
* <p/>
* You will never use {@code BasicPropertyDescriptor} as such but rather
* use its concrete descendants.
* <p/>
* Please note that {@code BasicPropertyDescriptor} enforces its name to
* start with a lower case letter, following the JavaBean convention. So even if
* you name it "MyProperty", it will actually end up to
* "myProperty".
*
* @author Vincent Vandenschrick
*/
public abstract class BasicPropertyDescriptor extends DefaultIconDescriptor
implements IPropertyDescriptor, BeanFactoryAware {
private BeanFactory beanFactory;
private boolean computed;
private Class<?> delegateClass;
private String delegateClassName;
private Boolean delegateWritable;
private Collection<String> grantedRoles;
private List<String> integrityProcessorBeanNames;
private List<String> integrityProcessorClassNames;
private List<IPropertyProcessor<?, ?>> integrityProcessors;
private Boolean mandatory;
private Integer preferredWidth;
private Collection<IGate> readabilityGates;
private Boolean readOnly;
private Boolean sortable;
private boolean cacheable;
private String sqlName;
private String unicityScope;
private boolean versionControl;
private Collection<IGate> writabilityGates;
private Boolean filterComparable;
private boolean filterOnly;
/**
* Constructs a new {@code BasicPropertyDescriptor} instance.
*/
public BasicPropertyDescriptor() {
computed = false;
versionControl = true;
cacheable = false;
filterOnly = false;
}
/**
* {@inheritDoc}
*/
@Override
public BasicPropertyDescriptor clone() {
return (BasicPropertyDescriptor) super.clone();
}
/**
* {@inheritDoc}
*/
@Override
public BasicPropertyDescriptor createQueryDescriptor() {
BasicPropertyDescriptor queryPropertyDescriptor = clone();
queryPropertyDescriptor.setMandatory(false);
queryPropertyDescriptor.setReadOnly(false);
queryPropertyDescriptor.integrityProcessors = null;
queryPropertyDescriptor.setIntegrityProcessorClassNames(null);
queryPropertyDescriptor.writabilityGates = null;
queryPropertyDescriptor.readabilityGates = null;
queryPropertyDescriptor.preferredWidth = null;
return queryPropertyDescriptor;
}
/**
* {@inheritDoc}
*/
@Override
public Class<?> getDelegateClass() {
if (delegateClass == null) {
String className = getDelegateClassName();
if (className != null) {
try {
delegateClass = Class.forName(className);
} catch (ClassNotFoundException ex) {
throw new NestedRuntimeException(ex);
}
}
}
return delegateClass;
}
/**
* Sets the delegate class name.
*
* @return The class name of the extension delegate used to compute this
* property.
*/
@Override
public String getDelegateClassName() {
return delegateClassName;
}
/**
* Gets the grantedRoles.
*
* @return the grantedRoles.
*/
@Override
public Collection<String> getGrantedRoles() {
return grantedRoles;
}
/**
* {@inheritDoc}
*/
@Override
public List<IPropertyProcessor<?, ?>> getIntegrityProcessors() {
registerIntegrityProcessorsIfNecessary();
return integrityProcessors;
}
/**
* Gets the preferredWidth.
*
* @return the preferredWidth.
*/
@Override
public Integer getPreferredWidth() {
return preferredWidth;
}
/**
* Gets the readabilityGates.
*
* @return the readabilityGates.
*/
@Override
public Collection<IGate> getReadabilityGates() {
return readabilityGates;
}
/**
* Gets the sqlName.
*
* @return the sqlName.
*/
public String getSqlName() {
return sqlName;
}
/**
* {@inheritDoc}
*/
@Override
public String getUnicityScope() {
return unicityScope;
}
/**
* Gets the writabilityGates.
*
* @return the writabilityGates.
*/
@Override
public Collection<IGate> getWritabilityGates() {
return writabilityGates;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public Object interceptSetter(Object component, Object newValue) {
Object interceptedValue = newValue;
List<IPropertyProcessor<?, ?>> processors = getIntegrityProcessors();
if (processors == null) {
return interceptedValue;
}
for (IPropertyProcessor<?, ?> processor : processors) {
interceptedValue = ((IPropertyProcessor<Object, Object>) processor).interceptSetter(component, interceptedValue);
}
return interceptedValue;
}
/**
* Returns true if a delegate class is registered to compute the property
* value. A property can be made {@code computed} even if its delegate
* class is null by calling {@code setComputed(true)}. This way, the
* property should be ignored by the ORM.
* <p/>
* {@inheritDoc}
*/
@Override
public boolean isComputed() {
return getDelegateClassName() != null || computed || isFilterOnly();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isMandatory() {
if (mandatory != null) {
return mandatory;
}
return getDefaultMandatory();
}
/**
* Gets the default value of the mandatory attribute.
*
* @return {@code false}.
*/
protected boolean getDefaultMandatory() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isModifiable() {
if (getDelegateClassName() == null) {
return true;
}
if (delegateWritable != null) {
return delegateWritable;
}
if (isFilterOnly()) {
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isQueryable() {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isReadOnly() {
if (readOnly != null) {
return readOnly;
}
return !isModifiable();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSortable() {
if (sortable != null) {
return sortable;
}
return getDefaultSortablility();
}
/**
* Default property sortability.
*
* @return {@code true} by default unless overridden in subclasses.
*/
protected boolean getDefaultSortablility() {
return true;
}
/**
* Gets the versionControl.
*
* @return the versionControl.
*/
public boolean isVersionControl() {
return versionControl;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public void postprocessSetter(Object component, Object oldValue, Object newValue) {
List<IPropertyProcessor<?, ?>> processors = getIntegrityProcessors();
if (processors == null) {
return;
}
for (IPropertyProcessor<?, ?> processor : processors) {
((IPropertyProcessor<Object, Object>) processor).postprocessSetter(component, oldValue, newValue);
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public void preprocessSetter(final Object component, Object newValue) {
// Mandatory checking should only happen on save. See bug #776.
// if (isMandatory()
// && (newValue == null || newValue instanceof Collection<?>
// && ((Collection<?>) newValue).isEmpty())) {
// throw new MandatoryPropertyException(this, component);
// }
List<IPropertyProcessor<?, ?>> processors = getIntegrityProcessors();
if (processors == null) {
return;
}
for (IPropertyProcessor<?, ?> processor : processors) {
((IPropertyProcessor<Object, Object>) processor).preprocessSetter(component, newValue);
}
}
/**
* Sets the beanFactory.
*
* @param beanFactory
* the beanFactory to set.
* @internal
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
/**
* Forces a property to be considered as a computed property by the framework.
* A computed property will be completely ignored by the persistence layer and
* its management is left to the developer.
* <p/>
* Properties declared with a delegate computing class are considered computed
* by default so there is no need to explicitly set them
* {@code computed=true}. However, there is sometimes a need to declare a
* property at some level (e.g. in an interface descriptor) and let lower
* level implementation decide how to handle this common property concretely
* (either computing it or handling it as a real persistent property). In that
* case, you can declare this property {@code computed=true} in the super
* type and refine the actual implementation (computed or not) in the
* sub-types.
* <p/>
* Default value is {@code false}.
*
* @param computed
* the computed to set.
*/
public void setComputed(boolean computed) {
this.computed = computed;
}
/**
* Instructs the framework that this property is computed by a delegate
* attached to the owning component. The {@code delegateClassName}
* property must be set with the fully qualified class name of the delegate
* instance to use.
* <p/>
* Delegate instances are stateful. This allows for some caching of computing
* intensive properties. There is exactly one delegate of a certain class per
* owning component instance. Delegate instances are lazily created when
* needed, i.e. whe the computed property is accessed either programmatically
* or by the binding layer.
* <p/>
* The delegate class must implement the
* {@code IComponentExtension<T>} interface (where <T> is
* assignable from the owning component class) and provide a public
* constructor taking exactly 1 parameter : the component instance. Jspresso
* provides an adapter class to inherit from :
* {@code AbstractComponentExtension<T>}. This helper class
* provides the methods to access the enclosing component from the delegate
* implementation as well as the Spring context it comes from, when needed.
* <p/>
* A delegate-computed property is most of the time read-only but it can be
* made writable by setting the property descriptor
* {@code delegateWritable=true}. In that case the delegate class must
* also provide a setter for the computed property.
*
* @param delegateClassName
* The class name of the extension delegate used to compute this
* property.
*/
public void setDelegateClassName(String delegateClassName) {
this.delegateClassName = delegateClassName;
}
/**
* Instructs the framework that a delegate-computed property is writable. Most
* of the time, a computed property is read-only. Whenever a computed property
* is made writable through the use of {@code delegateWritable=true}, the
* delegate class must also provide a setter for the computed property.
* <p/>
* Default value is {@code false}.
*
* @param delegateWritable
* the delegateWritable to set.
*/
public void setDelegateWritable(boolean delegateWritable) {
this.delegateWritable = delegateWritable;
}
/**
* Assigns the roles that are authorized to manipulate the property backed by
* this descriptor. It supports "<b>!</b>" prefix to negate the
* role(s). This will directly influence the UI behaviour and even composition
* (e.g. show/hide columns or fields). Setting the collection of granted roles
* to {@code null} (default value) disables role based authorization on
* this property level. Note that this authorization enforcement does not
* prevent programmatic access that is of the developer responsibility.
*
* @param grantedRoles
* the grantedRoles to set.
*/
public void setGrantedRoles(Collection<String> grantedRoles) {
this.grantedRoles = StringUtils.ensureSpaceFree(grantedRoles);
}
/**
* Registers a list of property processor instances that will be triggered on
* the different phases of the property modification, i.e. :
* <ul>
* <li><i>before</i> the property is modified, usually for controlling the
* incoming value</li>
* <li><i>while</i> (actually just before the actual assignment) the property
* is modified, allowing to intercept and change the incoming value</li>
* <li><i>after</i> the property is modified, allowing to trigger some
* post-modification behaviour (e.g. tracing, domain integrity management,
* ...)</li>
* </ul>
* This property must be set with Spring bean names (i.e. Spring ids). When
* needed, Jspresso will query the Spring application context to retrieve the
* processor instances. This property is equivalent to setting
* {@code integrityProcessorClassNames} except that it allows to register
* processor instances that are configured externally in the Spring context.
* <p/>
* Property processor instances must implement the
* {@code IPropertyProcessor<E, F>} interface where <E, F>
* represent respectively the type of the owning component and the type of the
* property. Since there are 3 methods to implement in the interface (1 for
* each of the phase described above), Jspresso provides an adapter class with
* empty implementations to override :
* {@code EmptyPropertyProcessor<E, F>}.
* <p/>
* Whenever the underlying property is a collection property, the interface to
* implement is {@code ICollectionPropertyProcessor<E, F>} (or
* extend {@code EmptyCollectionPropertyProcessor<E, F>}) with 4
* new phases introduced :
* <ul>
* <li><i>before</i> an element is <i>added</i> to the collection property</li>
* <li><i>after</i> an element is <i>added</i> to the collection property</li>
* <li><i>before</i> an element is <i>removed</i> from the collection property
* </li>
* <li><i>after</i> an element is <i>removed</i> from the collection property</li>
* </ul>
*
* @param integrityProcessorBeanNames
* the integrityProcessorBeanNames to set.
*/
public void setIntegrityProcessorBeanNames(List<String> integrityProcessorBeanNames) {
this.integrityProcessorBeanNames = StringUtils.ensureSpaceFree(integrityProcessorBeanNames);
}
/**
* Much the same as {@code integrityProcessorBeanNames} except that
* instead of providing a list of Spring bean names, you provide a list of
* fully qualified class names. These classes must :
* <ul>
* <li>provide a default constructor</li>
* <li>implement the {@code ILifecycleInterceptor<E>} interface.</li>
* </ul>
* When needed, Jspresso will create lifecycle interceptor instances.
*
* @param integrityProcessorClassNames
* the integrityProcessorClassNames to set.
*/
public void setIntegrityProcessorClassNames(List<String> integrityProcessorClassNames) {
this.integrityProcessorClassNames = StringUtils.ensureSpaceFree(integrityProcessorClassNames);
}
/**
* Declare a property as mandatory. This will enforce mandatory checks when
* the owning component is persisted as well as when the property is updated
* individually. Moreover, this information allows the views bound to the
* property to be configured accordingly, e.g. display the property with a
* slightly modified label indicating it is mandatory. This constraint is also
* enforced programmatically.
* <p/>
* Default value is false.
*
* @param mandatory
* the mandatory to set.
*/
public void setMandatory(boolean mandatory) {
this.mandatory = mandatory;
}
/**
* This property allows for setting an indication of width for representing
* this property in a view.
* <p/>
* Default value is {@code null}, so that the view factory will make its
* decision based on the type and/or other characteristics of the property
* (e.g. max length).
*
* @param preferredWidth
* the preferredWidth to set.
*/
public void setPreferredWidth(Integer preferredWidth) {
this.preferredWidth = preferredWidth;
}
/**
* Sets the readabilityGates.
*
* @param readabilityGates
* the readabilityGates to set.
* @internal
*/
public void setReadabilityGates(Collection<IGate> readabilityGates) {
this.readabilityGates = readabilityGates;
}
/**
* Enforces a property to be read-only. This is only enforced at the UI level,
* i.e. the property can still be updated programmatically. The UI may take
* decisions like changing text fields into labels if it knows the underlying
* property is read-only.
*
* @param readOnly
* the readOnly to set.
*/
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
}
/**
* Enforces a property sortability. This is only enforced at the UI level,
* i.e. the property can still be used for sorting programmatically.
*
* @param sortable
* the sortable to set.
*/
public void setSortable(boolean sortable) {
this.sortable = sortable;
}
/**
* Instructs Jspresso to use this name when translating this property name to
* the data store namespace. This includes , but is not limited to, database
* column names.
* <p/>
* Default value is {@code null} so that Jspresso uses its default naming
* policy.
*
* @param sqlName
* the sqlName to set.
*/
public void setSqlName(String sqlName) {
this.sqlName = sqlName;
}
/**
* Makes this property part of a unicity scope. All tuples of properties
* belonging to the same unicity scope are enforced to be unique in the
* component type scope. This concretely translates to unique constraints in
* the data store spanning the properties composing the unicity scope.
* <p/>
* Note that, for performance reasons, unicity scopes are only enforced by the
* persistence layer.
*
* @param unicityScope
* the unicityScope to set.
*/
public void setUnicityScope(String unicityScope) {
this.unicityScope = unicityScope;
}
/**
* This property allows to fine tune whether this component property
* participates in optimistic versioning. It mainly allows to declare some
* properties that should be ignored regarding optimistic versioning thus
* lowering the risk of version conflicts between concurrent users. Of course,
* this feature has to be used with care since it may generate phantom updates
* to the data store.
* <p/>
* Default value is {@code true} so that any change in the described
* property increases the owning component version.
*
* @param versionControl
* the versionControl to set.
*/
public void setVersionControl(boolean versionControl) {
this.versionControl = versionControl;
}
/**
* Assigns a collection of gates to determine property <i>writability</i>. A
* property will be considered writable if and only if all gates are open.
* This mechanism is mainly used for dynamic UI authorization based on model
* state, e.g. a validated invoice should not be editable anymore.
* <p/>
* Descriptor assigned gates will be cloned for each property instance created
* and backed by this descriptor. So basically, each property instance will
* have its own, unshared collection of writability gates.
* <p/>
* Jspresso provides a useful set of gate types, like the binary property gate
* that open/close based on the value of a boolean property of owning
* component.
* <p/>
* By default, property descriptors are not assigned any gates collection,
* i.e. there is no writability restriction. Note that gates do not enforce
* programmatic writability of a property; only UI is impacted.
*
* @param writabilityGates
* the writabilityGates to set.
*/
public void setWritabilityGates(Collection<IGate> writabilityGates) {
this.writabilityGates = writabilityGates;
}
private void registerIntegrityProcessor(IPropertyProcessor<?, ?> integrityProcessor) {
if (integrityProcessors == null) {
integrityProcessors = new ArrayList<>();
}
integrityProcessors.add(integrityProcessor);
}
private synchronized void registerIntegrityProcessorsIfNecessary() {
if (integrityProcessorClassNames != null) {
// process creation of integrity processors.
for (String integrityProcessorClassName : integrityProcessorClassNames) {
try {
registerIntegrityProcessor(
(IPropertyProcessor<?, ?>) Class.forName(integrityProcessorClassName).newInstance());
} catch (InstantiationException | ClassNotFoundException | IllegalAccessException ex) {
throw new DescriptorException(ex);
}
}
integrityProcessorClassNames = null;
}
if (integrityProcessorBeanNames != null && beanFactory != null) {
// process creation of integrity processors.
for (String integrityProcessorBeanName : integrityProcessorBeanNames) {
registerIntegrityProcessor(beanFactory.getBean(integrityProcessorBeanName, IPropertyProcessor.class));
}
integrityProcessorBeanNames = null;
}
}
/**
* Enforces its name to start with a lower case letter, following the JavaBean
* convention. So even if you name it "MyProperty", it will actually
* end up to "myProperty".
* <p/>
* {@inheritDoc}
*/
@Override
public void setName(String name) {
if (name != null && name.length() > 0) {
super.setName(name.substring(0, 1).toLowerCase() + name.substring(1));
} else {
super.setName(name);
}
}
/**
* Returns sqlName.
* <p/>
* {@inheritDoc}
*/
@Override
public String getPersistenceFormula() {
return getSqlName();
}
/**
* Returns the property name.
* <p/>
* {@inheritDoc}
*/
@Override
public String getPermId() {
return getName();
}
/**
* A property permanent id is forced to be its name. Trying to set it to
* another value will raise an exception.
* <p/>
* {@inheritDoc}
*/
@Override
public void setPermId(String permId) {
throw new UnsupportedOperationException();
}
/**
* Gets the cacheable.
*
* @return the cacheable.
*/
@Override
public boolean isCacheable() {
return cacheable;
}
/**
* Configures the fact that this property can be cached. This is only used for
* computed properties. Note that the cached value will be reset whenever a
* firePropertyChange regarding this property is detected to be fired.
* <p/>
* Default value is {@code false} in order to prevent un-desired
* side-effects if computed property change notification is not correctly
* wired.
*
* @param cacheable
* the cacheable to set.
*/
public void setCacheable(boolean cacheable) {
this.cacheable = cacheable;
}
/**
* {@inheritDoc}
*/
@Override
public String getModelTypeName() {
Class<?> modelType = getModelType();
if (modelType.isArray()) {
return getModelType().getComponentType() + "[]";
}
return modelType.getName();
}
/**
* Is filter comparable.
*
* @return the boolean
*/
@Override
public boolean isFilterComparable() {
if (filterComparable == null) {
return isDefaultFilterComparable();
}
return filterComparable;
}
/**
* Sets filter comparable.
*
* @param filterComparable
* the filter comparable
*/
public void setFilterComparable(Boolean filterComparable) {
this.filterComparable = filterComparable;
}
/**
* Gets whether this type of property descriptor is comparable by default in filter views. Can be overridden on a
* per-instance basis using the settFilterComparable setter.
*
* @return the {@code false} by default.
*/
protected boolean isDefaultFilterComparable() {
return false;
}
/**
* Gets whether this property is only used in filters, i.e. it is not persistent and only serves for storing criteria
* data that can be further leveraged by criteria refiners. Using {@code filterOnly} properties relieves the developer
* from having to declare fake computed properties. see {@link
* <a href="https://github.com/jspresso/jspresso-ce/issues/253">
* this request for enhancement</a>}
*
* @return {@code true} if this property is only used in filters.
*/
@Override
public boolean isFilterOnly() {
return filterOnly;
}
/**
* Configures whether this property is only used in filters, i.e. it is not persistent and only serves for storing
* criteria
* data that can be further leveraged by criteria refiners. Using {@code filterOnly} properties relieves the
* developer
* from having to declare fake computed properties. see {@link
* <a href="https://github.com/jspresso/jspresso-ce/issues/253">
* this request for enhancement</a>}
*
* @param filterOnly
* {@code true} if this property is only used in filters.
*/
public void setFilterOnly(boolean filterOnly) {
this.filterOnly = filterOnly;
}
}