/* * 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.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import gnu.trove.map.hash.THashMap; import gnu.trove.set.hash.THashSet; import gnu.trove.set.hash.TLinkedHashSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.jspresso.framework.model.component.IComponent; import org.jspresso.framework.model.component.service.IComponentService; import org.jspresso.framework.model.component.service.ILifecycleInterceptor; import org.jspresso.framework.model.descriptor.DescriptorException; import org.jspresso.framework.model.descriptor.ICollectionPropertyDescriptor; import org.jspresso.framework.model.descriptor.IComponentDescriptor; import org.jspresso.framework.model.descriptor.IComponentDescriptorProvider; import org.jspresso.framework.model.descriptor.IObjectPropertyDescriptor; import org.jspresso.framework.model.descriptor.IPropertyDescriptor; import org.jspresso.framework.model.descriptor.IReferencePropertyDescriptor; import org.jspresso.framework.model.descriptor.IStringPropertyDescriptor; import org.jspresso.framework.model.descriptor.ITextPropertyDescriptor; import org.jspresso.framework.model.entity.EntityHelper; import org.jspresso.framework.model.entity.IEntity; import org.jspresso.framework.util.accessor.IAccessor; import org.jspresso.framework.util.collection.ESort; 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.ObjectUtils; import org.jspresso.framework.util.lang.StringUtils; import org.jspresso.framework.util.sql.SqlHelper; /** * This is the abstract base descriptor for all component-like part of the * domain model. All the properties included in this base descriptor can of * course be used in concrete sub-types. * <p/> * These sub-types include : * <ul> * <li><i>BasicEntityDescriptor</i> for defining a persistent entity</li> * <li><i>BasicInterfaceDescriptor</i> for defining a common interface that will * be implemented by other entities, components or even sub-interfaces.</li> * <li><i>BasicComponentDescriptor</i> for defining reusable structures that can * be inline in an entity. It also allows to describe an arbitrary POJO and * make use of it in Jspresso UIs.</li> * </ul> * * @param <E> * the concrete type of components. * @author Vincent Vandenschrick */ public abstract class AbstractComponentDescriptor<E> extends DefaultIconDescriptor implements IComponentDescriptor<E>, BeanFactoryAware { /** * IInterface descriptor for IComponent {@code COMPONENT_DESCRIPTOR}. */ protected static final IComponentDescriptor<IComponent> COMPONENT_DESCRIPTOR = createComponentDescriptor(); private static final Logger LOG = LoggerFactory.getLogger(AbstractComponentDescriptor.class); private List<IComponentDescriptor<?>> ancestorDescriptors; private BeanFactory beanFactory; private Class<?> componentContract; private Collection<String> grantedRoles; private volatile List<String> lifecycleInterceptorBeanNames; private volatile List<String> lifecycleInterceptorClassNames; private List<ILifecycleInterceptor<?>> lifecycleInterceptors; private Map<String, ESort> orderingProperties; private Integer pageSize; private Map<String, IPropertyDescriptor> propertyDescriptorsMap; private volatile List<String> queryableProperties; private volatile List<String> renderedProperties; private volatile IComponentDescriptor<E> queryDescriptor; private boolean isQueryDescriptor; private Collection<IGate> readabilityGates; private Set<Class<?>> serviceContracts; private volatile Map<String, String> serviceDelegateBeanNames; private volatile Map<String, String> serviceDelegateClassNames; private Map<String, IComponentService> serviceDelegates; private String sqlName; private List<IPropertyDescriptor> tempPropertyBuffer; private volatile boolean initializingPropertyDescriptorsMap; private volatile String toStringProperty; private String toHtmlProperty; private volatile String autoCompleteProperty; private Collection<String> unclonedProperties; private Collection<IGate> writabilityGates; private Map<String, IPropertyDescriptor> propertyDescriptorsCache; private volatile Collection<IPropertyDescriptor> allPropertyDescriptorsCache; private static BasicCollectionPropertyDescriptor<IComponent> componentTranslationsDescriptorTemplate; /** * Constructs a new {@code AbstractComponentDescriptor} instance. * * @param name * the name of the descriptor which has to be the fully-qualified * class name of its contract. */ public AbstractComponentDescriptor(String name) { setName(name); propertyDescriptorsCache = new ConcurrentHashMap<>(); initializingPropertyDescriptorsMap = false; // Force initialization of ancestor descriptors setAncestorDescriptors(null); isQueryDescriptor = false; } private static IComponentDescriptor<IComponent> createComponentDescriptor() { BasicInterfaceDescriptor<IComponent> componentDescriptor = new BasicInterfaceDescriptor<>( IComponent.class.getName()); return componentDescriptor; } private final Object queryDescriptorLock = new Object(); /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public IComponentDescriptor<E> createQueryDescriptor() { if (queryDescriptor == null) { synchronized (queryDescriptorLock) { if (queryDescriptor == null) { IComponentDescriptor<E> tempQueryDescriptor; if (isQueryDescriptor) { queryDescriptor = this; } else { tempQueryDescriptor = (AbstractComponentDescriptor<E>) super.clone(); ((AbstractComponentDescriptor<E>) tempQueryDescriptor).isQueryDescriptor = true; List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { List<IComponentDescriptor<?>> queryAncestorDescriptors = new ArrayList<>(); for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { queryAncestorDescriptors.add(ancestorDescriptor.createQueryDescriptor()); } ((AbstractComponentDescriptor<E>) tempQueryDescriptor).setAncestorDescriptors(queryAncestorDescriptors); } queryDescriptor = tempQueryDescriptor; Collection<IPropertyDescriptor> declaredPropertyDescs = getDeclaredPropertyDescriptors(); if (declaredPropertyDescs != null) { Collection<IPropertyDescriptor> queryPropertyDescriptors = new ArrayList<>(); for (IPropertyDescriptor propertyDescriptor : declaredPropertyDescs) { queryPropertyDescriptors.add(propertyDescriptor.createQueryDescriptor()); } ((AbstractComponentDescriptor<E>) tempQueryDescriptor).setPropertyDescriptors(queryPropertyDescriptors); } } } } } return queryDescriptor; } /** * Gets the descriptor ancestors collection. It directly translates the * components inheritance hierarchy since the component property descriptors * are the union of the declared property descriptors of the component and of * its ancestors one. A component may have multiple ancestors which means that * complex multi-inheritance hierarchy can be mapped. * * @return ancestorDescriptors The list of ancestor entity descriptors. */ public List<IComponentDescriptor<?>> getAncestorDescriptors() { return ancestorDescriptors; } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public Class<? extends E> getComponentContract() { if (componentContract == null && getName() != null) { try { componentContract = Class.forName(getName()); } catch (ClassNotFoundException ex) { throw new NestedRuntimeException(ex); } } return (Class<? extends E>) componentContract; } /** * {@inheritDoc} */ @Override public IComponentDescriptor<E> getComponentDescriptor() { return this; } /** * {@inheritDoc} */ @Override public Collection<IPropertyDescriptor> getDeclaredPropertyDescriptors() { processPropertiesBufferIfNecessary(); if (propertyDescriptorsMap != null) { return propertyDescriptorsMap.values(); } return null; } /** * Gets the grantedRoles. * * @return the grantedRoles. */ @Override public Collection<String> getGrantedRoles() { return grantedRoles; } /** * Gets the lifecycleInterceptors. * * @return the lifecycleInterceptors. */ @Override public List<ILifecycleInterceptor<?>> getLifecycleInterceptors() { List<ILifecycleInterceptor<?>> allInterceptors = new ArrayList<>(); List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { allInterceptors.addAll(ancestorDescriptor.getLifecycleInterceptors()); } } registerLifecycleInterceptorsIfNecessary(); if (lifecycleInterceptors != null) { allInterceptors.addAll(lifecycleInterceptors); } return allInterceptors; } /** * {@inheritDoc} */ @Override public Class<?> getModelType() { return getComponentContract(); } /** * {@inheritDoc} */ @Override public String getModelTypeName() { return getName().replaceAll("\\$","."); } /** * {@inheritDoc} */ @Override public Map<String, ESort> getOrderingProperties() { // use a set to avoid duplicates. Map<String, ESort> properties = new LinkedHashMap<>(); if (orderingProperties != null) { properties.putAll(orderingProperties); } List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { if (ancestorDescriptor.getOrderingProperties() != null) { properties.putAll(ancestorDescriptor.getOrderingProperties()); } } } if (properties.isEmpty()) { return null; } return properties; } /** * {@inheritDoc} */ @Override public Integer getPageSize() { return pageSize; } private static final BasicObjectPropertyDescriptor NULL_PROPERTY_DESCRIPTOR = new BasicObjectPropertyDescriptor(); /** * {@inheritDoc} */ @Override public IPropertyDescriptor getPropertyDescriptor(String propertyName) { if (propertyName == null) { return null; } IPropertyDescriptor descriptor = propertyDescriptorsCache.get(propertyName); if (descriptor != null) { if (descriptor == NULL_PROPERTY_DESCRIPTOR) { return null; } return descriptor; } int nestedDotIndex = propertyName.indexOf(IAccessor.NESTED_DELIM); if (nestedDotIndex > 0) { IPropertyDescriptor rootProp = getPropertyDescriptor(propertyName.substring(0, nestedDotIndex)); if (rootProp instanceof IComponentDescriptorProvider<?>) { IComponentDescriptor<?> componentDescriptor = ((IComponentDescriptorProvider<?>) rootProp) .getComponentDescriptor(); descriptor = componentDescriptor.getPropertyDescriptor(propertyName.substring(nestedDotIndex + 1)); if (descriptor != null) { descriptor = descriptor.clone(); if (descriptor instanceof BasicPropertyDescriptor) { ((BasicPropertyDescriptor) descriptor).setName(propertyName); } } } } else { descriptor = getDeclaredPropertyDescriptor(propertyName); List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); // Ancestor descriptors must be walked the reverse order // in order to match the getPropertyDescriptors() method. if (descriptor == null && ancestorDescs != null) { for (int i = ancestorDescs.size() - 1; i >= 0 && descriptor == null; i--) { IComponentDescriptor<?> ancestorDescriptor = ancestorDescs.get(i); descriptor = ancestorDescriptor.getPropertyDescriptor(propertyName); } } } descriptor = refinePropertyDescriptor(descriptor); if (descriptor == null) { propertyDescriptorsCache.put(propertyName, NULL_PROPERTY_DESCRIPTOR); } else { propertyDescriptorsCache.put(propertyName, descriptor); } return descriptor; } private final Object allPropertyDescriptorsLock = new Object(); /** * {@inheritDoc} */ @Override public Collection<IPropertyDescriptor> getPropertyDescriptors() { if (allPropertyDescriptorsCache == null) { synchronized (allPropertyDescriptorsLock) { if (allPropertyDescriptorsCache == null) { Collection<IPropertyDescriptor> tempAllPropertyDescriptorsCache = new ArrayList<>(); // A map is used instead of a set since a set does not replace an // element it already contains. Map<String, IPropertyDescriptor> allDescriptors = new LinkedHashMap<>(); List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { for (IPropertyDescriptor propertyDescriptor : ancestorDescriptor.getPropertyDescriptors()) { allDescriptors.put(propertyDescriptor.getName(), propertyDescriptor); } } } Collection<IPropertyDescriptor> declaredPropertyDescriptors = getDeclaredPropertyDescriptors(); if (declaredPropertyDescriptors != null) { for (IPropertyDescriptor propertyDescriptor : declaredPropertyDescriptors) { propertyDescriptor = refinePropertyDescriptor(propertyDescriptor); allDescriptors.put(propertyDescriptor.getName(), propertyDescriptor); } } tempAllPropertyDescriptorsCache.addAll(allDescriptors.values()); allPropertyDescriptorsCache = tempAllPropertyDescriptorsCache; } } } return allPropertyDescriptorsCache; } private final Object queryablePropertiesLock = new Object(); /** * {@inheritDoc} */ @Override public List<String> getQueryableProperties() { if (queryableProperties == null) { synchronized (queryablePropertiesLock) { if (queryableProperties == null) { Set<String> queryablePropertiesSet = new TLinkedHashSet<>(1); List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { queryablePropertiesSet.addAll(ancestorDescriptor.getQueryableProperties()); } } for (String renderedProperty : getRenderedProperties()) { IPropertyDescriptor declaredPropertyDescriptor = getDeclaredPropertyDescriptor(renderedProperty); if (declaredPropertyDescriptor != null && declaredPropertyDescriptor.isQueryable()) { queryablePropertiesSet.add(renderedProperty); } } queryableProperties = new ArrayList<>(queryablePropertiesSet); } } } return explodeComponentReferences(this, queryableProperties); } /** * Gets the readabilityGates. * * @return the readabilityGates. */ @Override public Collection<IGate> getReadabilityGates() { Set<IGate> gates = new THashSet<>(1); if (readabilityGates != null) { gates.addAll(readabilityGates); } List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { gates.addAll(ancestorDescriptor.getReadabilityGates()); } } return gates; } private final Object renderedPropertiesLock = new Object(); /** * {@inheritDoc} */ @Override public List<String> getRenderedProperties() { if (renderedProperties == null) { synchronized (renderedPropertiesLock) { if (renderedProperties == null) { Set<String> renderedPropertiesSet = new TLinkedHashSet<>(1); List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { renderedPropertiesSet.addAll(ancestorDescriptor.getRenderedProperties()); } } Collection<IPropertyDescriptor> declaredPropertyDescriptors = getDeclaredPropertyDescriptors(); if (declaredPropertyDescriptors != null) { for (IPropertyDescriptor propertyDescriptor : declaredPropertyDescriptors) { if (!(propertyDescriptor instanceof ICollectionPropertyDescriptor<?>) && !(propertyDescriptor instanceof ITextPropertyDescriptor) && !(propertyDescriptor instanceof IObjectPropertyDescriptor)) { String propertyName = propertyDescriptor.getName(); if (!propertyName.endsWith(RAW_SUFFIX) && !propertyName.endsWith(NLS_SUFFIX)) { renderedPropertiesSet.add(propertyName); } } } } renderedProperties = new ArrayList<>(renderedPropertiesSet); } } } return explodeComponentReferences(this, renderedProperties); } /** * {@inheritDoc} */ @Override public Collection<String> getServiceContractClassNames() { Set<String> serviceContractClassNames = new TLinkedHashSet<>(1); if (serviceContracts != null) { for (Class<?> serviceContract : serviceContracts) { serviceContractClassNames.add(serviceContract.getName()); } } else { if (serviceDelegateClassNames != null) { serviceContractClassNames.addAll(serviceDelegateClassNames.keySet()); } if (serviceDelegateBeanNames != null) { serviceContractClassNames.addAll(serviceDelegateBeanNames.keySet()); } } return serviceContractClassNames; } /** * {@inheritDoc} */ @Override public Collection<Class<?>> getServiceContracts() { registerDelegateServicesIfNecessary(); if (serviceContracts != null) { return new ArrayList<>(serviceContracts); } return null; } /** * {@inheritDoc} */ @Override public IComponentService getServiceDelegate(Method targetMethod) { registerDelegateServicesIfNecessary(); IComponentService service = null; if (serviceDelegates != null) { service = serviceDelegates.get(targetMethod.getName()); } List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (service == null && ancestorDescs != null) { for (Iterator<IComponentDescriptor<?>> ite = ancestorDescs.iterator(); service == null && ite.hasNext(); ) { IComponentDescriptor<?> ancestorDescriptor = ite.next(); service = ancestorDescriptor.getServiceDelegate(targetMethod); } } return service; } /** * Gets the sqlName. * * @return the sqlName. */ public String getSqlName() { return sqlName; } private final Object toStringLock = new Object(); /** * Gets the toStringProperty. * * @return the toStringProperty. */ @Override public String getToStringProperty() { if (toStringProperty == null) { synchronized (toStringLock) { if (toStringProperty == null) { String tempToStringProperty = null; List<String> rp = getRenderedProperties(); if (rp != null && !rp.isEmpty()) { for (String renderedProperty : rp) { if (getPropertyDescriptor(renderedProperty) instanceof IStringPropertyDescriptor) { tempToStringProperty = renderedProperty; break; } } if (tempToStringProperty == null) { tempToStringProperty = rp.get(0); } } else if (getPropertyDescriptor("id") != null) { tempToStringProperty = "id"; } else { tempToStringProperty = getPropertyDescriptors().iterator().next().getName(); } toStringProperty = tempToStringProperty; } } } return toStringProperty; } /** * Gets the toStringProperty. * * @return the toStringProperty. */ @Override public String getToHtmlProperty() { if (toHtmlProperty == null) { return getToStringProperty(); } return toHtmlProperty; } private final Object autoCompleteLock = new Object(); /** * Gets the autocomplete property. * <p/> * {@inheritDoc} */ @Override public String getAutoCompleteProperty() { if (autoCompleteProperty == null) { synchronized (autoCompleteLock) { if (autoCompleteProperty == null) { String tempAutoCompleteProperty = null; IPropertyDescriptor lpd = getPropertyDescriptor(getToStringProperty()); if (lpd != null && !lpd.isComputed()) { tempAutoCompleteProperty = lpd.getName(); } else { List<String> rp = getRenderedProperties(); if (rp != null && !rp.isEmpty()) { for (String renderedProperty : rp) { if (getPropertyDescriptor(renderedProperty) instanceof IStringPropertyDescriptor) { tempAutoCompleteProperty = renderedProperty; break; } } if (tempAutoCompleteProperty == null) { Collection<IPropertyDescriptor> allProps = getPropertyDescriptors(); for (IPropertyDescriptor pd : allProps) { if (pd instanceof IStringPropertyDescriptor && !IEntity.ID.equals(pd.getName())) { tempAutoCompleteProperty = pd.getName(); break; } } } if (tempAutoCompleteProperty == null) { tempAutoCompleteProperty = IEntity.ID; } } } autoCompleteProperty = tempAutoCompleteProperty; } } } return autoCompleteProperty; } /** * The properties returned include the uncloned properties of the ancestors. * <p/> * {@inheritDoc} */ @Override public Collection<String> getUnclonedProperties() { Set<String> properties = new THashSet<>(1); if (unclonedProperties != null) { properties.addAll(unclonedProperties); } List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { properties.addAll(ancestorDescriptor.getUnclonedProperties()); } } return properties; } /** * Gets the writabilityGates. * * @return the writabilityGates. */ @Override public Collection<IGate> getWritabilityGates() { Set<IGate> gates = new THashSet<>(1); if (writabilityGates != null) { gates.addAll(writabilityGates); } List<IComponentDescriptor<?>> ancestorDescs = getAncestorDescriptors(); if (ancestorDescs != null) { for (IComponentDescriptor<?> ancestorDescriptor : ancestorDescs) { gates.addAll(ancestorDescriptor.getWritabilityGates()); } } return gates; } /** * {@inheritDoc} */ @Override public boolean isReadOnly() { return false; } /** * Registers this descriptor with a collection of ancestors. It directly * translates the components inheritance hierarchy since the component * property descriptors are the union of the declared property descriptors of * the component and of its ancestors one. A component may have multiple * ancestors which means that complex multiple-inheritance hierarchy can be * mapped. * * @param ancestorDescriptors * The list of ancestor component descriptors. */ public void setAncestorDescriptors(List<IComponentDescriptor<?>> ancestorDescriptors) { this.ancestorDescriptors = ancestorDescriptors; } /** * {@inheritDoc} * * @internal */ @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } /** * Assigns the roles that are authorized to manipulate components backed by * this descriptor. It supports "<b>!</b>" prefix to negate the * role(s). This will directly influence the UI behaviour and even * composition. Setting the collection of granted roles to {@code null} * (default value) disables role based authorization on this component 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 lifecycle interceptor instances that will be triggered * on the different phases of the component lifecycle, i.e. : * <ul> * <li>when the component is <i>instantiated</i> in memory</li> * <li>when the component is <i>created</i> in the data store</li> * <li>when the component is <i>updated</i> in the data store</li> * <li>when the component is <i>loaded</i> from the data store</li> * <li>when the component is <i>deleted</i> from the data store</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 * interceptors instances. This property is equivalent to setting * {@code lifecycleInterceptorClassNames} except that it allows to * register interceptor instances that are configured externally in the Spring * context. lifecycle interceptor instances must implement the * {@code ILifecycleInterceptor<E>} interface where <E> is a * type assignable from the component type. * * @param lifecycleInterceptorBeanNames * the lifecycleInterceptorBeanNames to set. They are used to * retrieve interceptor instances from the Spring bean factory this * descriptor comes from if any. */ public void setLifecycleInterceptorBeanNames(List<String> lifecycleInterceptorBeanNames) { this.lifecycleInterceptorBeanNames = StringUtils.ensureSpaceFree(lifecycleInterceptorBeanNames); } /** * Much the same as {@code lifecycleInterceptorBeanNames} 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 IPropertyProcessor<E, F>} interface.</li> * </ul> * When needed, Jspresso will create the property processor instances. * * @param lifecycleInterceptorClassNames * the lifecycleInterceptorClassNames to set. */ public void setLifecycleInterceptorClassNames(List<String> lifecycleInterceptorClassNames) { this.lifecycleInterceptorClassNames = StringUtils.ensureSpaceFree(lifecycleInterceptorClassNames); } /** * Ordering properties are used to sort un-indexed collections of instances of * components backed by this descriptor. This sort order can be overridden on * the finer collection property level to change the way a specific collection * is sorted. This property consist of a {@code Map} whose entries are * composed with : * <ul> * <li>the property name as key</li> * <li>the sort order for this property as value. This is either a value of * the {@code ESort} enum (<i>ASCENDING</i> or <i>DESCENDING</i>) or its * equivalent string representation.</li> * </ul> * Ordering properties are considered following their order in the map * iterator. A {@code null} value (default) will not give any indication * for the collection sort order. * * @param untypedOrderingProperties * the orderingProperties to set. */ public void setOrderingProperties(Map<String, ?> untypedOrderingProperties) { if (untypedOrderingProperties != null) { orderingProperties = new LinkedHashMap<>(); for (Map.Entry<String, ?> untypedOrderingProperty : untypedOrderingProperties.entrySet()) { if (untypedOrderingProperty.getValue() instanceof ESort) { orderingProperties.put(untypedOrderingProperty.getKey(), (ESort) untypedOrderingProperty.getValue()); } else if (untypedOrderingProperty.getValue() instanceof String) { orderingProperties.put(untypedOrderingProperty.getKey(), ESort.valueOf((String) untypedOrderingProperty.getValue())); } else { orderingProperties.put(untypedOrderingProperty.getKey(), ESort.ASCENDING); } } } else { orderingProperties = null; } } /** * Whenever a collection of this component type is presented in a pageable UI, * this property gives the size (number of component instances) of one page. * This size can usually be refined at a lower level (e.g. at reference * property descriptor for "lists of values"). A {@code null} * value (default) disables paging for this component. * * @param pageSize * the pageSize to set. */ public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } /** * This property allows to describe the properties of the components backed by * this descriptor. Like in classic OO programming, the actual set of * properties available to a component is the union of its properties and of * its ancestors' ones. Jspresso also allows you to refine a property * descriptor in a child component descriptor exactly as you would do it in a * subclass. In that case, the attributes of the property defined in the child * descriptor prevails over the definition of its ancestors. Naturally, * properties are keyed by their names. * * @param descriptors * the propertyDescriptors to set. */ public void setPropertyDescriptors(Collection<IPropertyDescriptor> descriptors) { // This is important to use an intermediate structure since all descriptors // may not have their names fully initialized. if (descriptors != null) { tempPropertyBuffer = new ArrayList<>(descriptors); propertyDescriptorsMap = null; } else { tempPropertyBuffer = null; propertyDescriptorsMap = null; } resetPropertyDescriptorCaches(); } private void resetPropertyDescriptorCaches() { propertyDescriptorsCache = new ConcurrentHashMap<>(); allPropertyDescriptorsCache = null; } /** * This property allows to define which of the component properties are to be * used in the filter UIs that are based on this component family (a QBE * screen for instance). Since this is a {@code List} queryable * properties are rendered in the same order. * <p/> * Whenever this this property is {@code null} (default value), Jspresso * chooses the default set of queryable properties based on their type. For * instance, collection properties and binary properties are not used but * string, numeric, reference, ... properties are. A computed property cannot * be used since it has no data store existence and thus cannot be queried * upon. * <p/> * Note that this property is not inherited by children descriptors, i.e. even * if an ancestor defines an explicit set of queryable properties, its * children ignore this setting. * * @param queryableProperties * the queryableProperties to set. */ public void setQueryableProperties(List<String> queryableProperties) { this.queryableProperties = StringUtils.ensureSpaceFree(queryableProperties); } /** * Sets the readabilityGates. * * @param readabilityGates * the readabilityGates to set. * @internal */ public void setReadabilityGates(Collection<IGate> readabilityGates) { this.readabilityGates = readabilityGates; } /** * This property allows to define which of the component properties are to be * rendered by default when displaying a UI based on this component family. * For instance, a table will render 1 column per rendered property of the * component. Any type of property can be used except collection properties. * Since this is a {@code List} queryable properties are rendered in the * same order. * <p/> * Whenever this property is {@code null} (default value) Jspresso * determines the default set of properties to render based on their types, * e.g. ignores collection properties. * <p/> * Note that this property is not inherited by children descriptors, i.e. even * if an ancestor defines an explicit set of rendered properties, its children * ignore this setting. * * @param renderedProperties * the renderedProperties to set. */ public void setRenderedProperties(List<String> renderedProperties) { this.renderedProperties = StringUtils.ensureSpaceFree(renderedProperties); } /** * Registers the collection of service delegate instances attached to this * component. These delegate instances will automatically be triggered * whenever a method of the service interface it implements get executed. For * instance : * <ul> * <li>the component interface is {@code MyBeanClass}. It implements the * service interface {@code MyService}.</li> * <li>the service interface {@code MyService} contains method * {@code int foo(String)}.</li> * <li>the service delegate class, e.g. {@code MyServiceImpl} must * implement the method {@code int foo(MyBeanClass,String)}. Note that * the parameter list is augmented with the owing component type as 1st * parameter. This allows to have stateless implementation for delegates, thus * sharing instances of delegates among instances of components.</li> * <li>when {@code foo(String)} is executed on an instance of * {@code MyBeanClass}, the framework will trigger the delegate * implementation, passing the instance of the component itself as parameter.</li> * </ul> * This property must be set with a map keyed by service interfaces and valued * by Spring bean names (i.e. Spring ids). Each bean name corresponds to an * instance of service delegate. When needed, Jspresso will query the Spring * application context to retrieve the delegate instances. This property is * equivalent to setting {@code serviceDelegateClassNames} except that it * allows to register delegate instances that are configured externally in the * Spring context. lifecycle interceptor instances must implement the * {@code IComponentService} marker interface. * * @param serviceDelegateBeanNames * the serviceDelegateBeanNames to set. They are used to retrieve * delegate instances from the Spring bean factory this descriptor * comes from if any. */ public void setServiceDelegateBeanNames(Map<String, String> serviceDelegateBeanNames) { this.serviceDelegateBeanNames = StringUtils.ensureSpaceFree(serviceDelegateBeanNames); } /** * Much the same as {@code serviceDelegateBeanNames} except that instead * of providing a map valued with Spring bean names, you provide a map valued * with fully qualified class names. These class must : * <ul> * <li>provide a default constructor</li> * <li>implement the {@code IComponentService} marker interface.</li> * </ul> * When needed, Jspresso will create service delegate instances. * * @param serviceDelegateClassNames * the component services to be registered keyed by their contract. A * service contract is an interface class defining the service * methods to be registered as implemented by the service delegate. * Map values must be instances of {@code IComponentService}. */ public void setServiceDelegateClassNames(Map<String, String> serviceDelegateClassNames) { this.serviceDelegateClassNames = StringUtils.ensureSpaceFree(serviceDelegateClassNames); } /** * Instructs Jspresso to use this name when translating this component type * name to the data store namespace. This includes , but is not limited to, * database table 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; } /** * Allows to customize the string representation of a component instance. The * property name assigned will be used when displaying the component instance * as a string. It may be a computed property that composes several other * properties in a human friendly format. * <p/> * Whenever this property is {@code null}, the following rule apply to * determine the <i>toString</i> property : * <ol> * <li>the first string property from the rendered property</li> * <li>the first rendered property if no string property is found among them</li> * </ol> * Note that this property is not inherited by children descriptors, i.e. even * if an ancestor defines an explicit <i>toString</i> property, its children * ignore this setting. * * @param toStringProperty * the toStringProperty to set. */ public void setToStringProperty(String toStringProperty) { this.toStringProperty = toStringProperty; } /** * Allows to customize the HTML representation of a component instance. The * property name assigned will be used when displaying the component instance * as HTML. It may be a computed property that composes several other * properties in a human friendly format. * <p/> * Whenever this property is {@code null}, the * {@code toStringProperty} is used. Note that this property is not * inherited by children descriptors, i.e. even if an ancestor defines an * explicit <i>toHtmlProperty</i> property, its children ignore this setting. * * @param toHtmlProperty * the toHtmlProperty to set. */ public void setToHtmlProperty(String toHtmlProperty) { this.toHtmlProperty = toHtmlProperty; } /** * Allows to customize the property used to autocomplete reference fields on * this component. * <p/> * Whenever this property is {@code null}, the following rule apply to * determine the <i>lovProperty</i> : * <ol> * <li>the toString property if not a computed one</li> * <li>the first string property from the rendered property</li> * <li>the first rendered property if no string property is found among them</li> * </ol> * Note that this property is not inherited by children descriptors, i.e. even * if an ancestor defines an explicit <i>lovProperty</i>, its children ignore * this setting. * * @param autoCompleteProperty * the autoCompleteProperty to set. */ public void setAutoCompleteProperty(String autoCompleteProperty) { this.autoCompleteProperty = autoCompleteProperty; } /** * Configures the properties that must not be cloned when this component is * duplicated. For instance, tracing information like a created timestamp * should not be cloned; a SSN neither. For a given component, the uncloned * properties are the ones it defines augmented by the ones its ancestors * define. There is no mean to make a component property cloneable if one of * the ancestor declares it un-cloneable. * * @param unclonedProperties * the unclonedProperties to set. */ public void setUnclonedProperties(Collection<String> unclonedProperties) { this.unclonedProperties = StringUtils.ensureSpaceFree(unclonedProperties); } /** * Assigns a collection of gates to determine component <i>writability</i>. A * component 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 component instance * created and backed by this descriptor. So basically, each component * 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, component descriptors are not assigned any gates collection, * i.e. there is no writability restriction. Note that gates do not enforce * programmatic writability of a component; only UI is impacted. * * @param writabilityGates * the writabilityGates to set. */ public void setWritabilityGates(Collection<IGate> writabilityGates) { this.writabilityGates = writabilityGates; } private static ThreadLocal<Collection<Class<?>>> sofeWatchdog = new ThreadLocal<>(); static List<String> explodeComponentReferences(IComponentDescriptor<?> componentDescriptor, List<String> propertyNames) { boolean createdWatchDog = false; Collection<Class<?>> watchDog = sofeWatchdog.get(); if (watchDog == null) { createdWatchDog = true; watchDog = new LinkedHashSet<>(); sofeWatchdog.set(watchDog); } try { List<String> explodedProperties = new ArrayList<>(); for (String propertyName : propertyNames) { IPropertyDescriptor propertyDescriptor = componentDescriptor.getPropertyDescriptor(propertyName); if (propertyDescriptor instanceof IReferencePropertyDescriptor<?> && EntityHelper.isInlineComponentReference( (IReferencePropertyDescriptor<?>) propertyDescriptor)) { if (watchDog.contains(componentDescriptor.getComponentContract())) { // Whenever there are circular references between inline components, do not try to resolve them since it's // impossible, but log the warning. LOG.warn("A circular reference has been detected on inline {} components. You should explicitly declare " + "their rendered properties since they cannot be computed automatically.", watchDog); } else { watchDog.add(componentDescriptor.getComponentContract()); List<String> nestedProperties = new ArrayList<>(); List<String> nestedRenderedProperties; nestedRenderedProperties = ((IReferencePropertyDescriptor<?>) propertyDescriptor).getReferencedDescriptor() .getRenderedProperties(); for (String nestedRenderedProperty : nestedRenderedProperties) { nestedProperties.add(propertyName + "." + nestedRenderedProperty); } explodedProperties.addAll(explodeComponentReferences(componentDescriptor, nestedProperties)); } } else { explodedProperties.add(propertyName); } } return explodedProperties; } finally { if (createdWatchDog) { sofeWatchdog.remove(); } } } private IPropertyDescriptor getDeclaredPropertyDescriptor(String propertyName) { processPropertiesBufferIfNecessary(); if (propertyDescriptorsMap != null) { return propertyDescriptorsMap.get(propertyName); } return null; } private final Object propertiesBufferLock = new Object(); @SuppressWarnings("unchecked") private void processPropertiesBufferIfNecessary() { if (tempPropertyBuffer != null && !initializingPropertyDescriptorsMap) { synchronized (propertiesBufferLock) { if (tempPropertyBuffer != null && !initializingPropertyDescriptorsMap) { initializingPropertyDescriptorsMap = true; propertyDescriptorsMap = new LinkedHashMap<>(); for (IPropertyDescriptor descriptor : tempPropertyBuffer) { if (descriptor instanceof IStringPropertyDescriptor) { if (((IStringPropertyDescriptor) descriptor).isTranslatable()) { String rawSqlName = ((BasicStringPropertyDescriptor) descriptor).getSqlName(); if (rawSqlName == null) { rawSqlName = new SqlHelper().transformToSql(descriptor.getName(), null); } ((BasicStringPropertyDescriptor) descriptor).setSqlName(rawSqlName); if (!descriptor.getName().endsWith(RAW_SUFFIX)) { ((BasicStringPropertyDescriptor) descriptor).setName(descriptor.getName() + RAW_SUFFIX); } } } propertyDescriptorsMap.put(descriptor.getName(), descriptor); } tempPropertyBuffer = null; if (isTranslatable()) { for (IPropertyDescriptor translatablePropertyDescriptor : getPropertyDescriptors()) { if (translatablePropertyDescriptor instanceof IStringPropertyDescriptor && ((IStringPropertyDescriptor) translatablePropertyDescriptor).isTranslatable() && !translatablePropertyDescriptor.getName().endsWith(NLS_SUFFIX)) { completeWithComputedNlsDescriptors(translatablePropertyDescriptor); } } if (!isPurelyAbstract()) { BasicCollectionPropertyDescriptor<IComponent> translationsPropertyDescriptor = getComponentTranslationsDescriptorTemplate() .clone(); translationsPropertyDescriptor.setSqlName("T"); BasicCollectionDescriptor<IComponent> translationsCollectionDescriptor = (BasicCollectionDescriptor<IComponent>) ((BasicCollectionDescriptor<IComponent>) translationsPropertyDescriptor .getReferencedDescriptor()).clone(); BasicComponentDescriptor<IComponent> translationDescriptor = (BasicComponentDescriptor<IComponent>) ( (BasicComponentDescriptor<IComponent>) translationsCollectionDescriptor .getElementDescriptor()).clone(); translationsPropertyDescriptor.setReferencedDescriptor(translationsCollectionDescriptor); translationsCollectionDescriptor.setElementDescriptor(translationDescriptor); translationDescriptor.setName(getName() + "$Translation"); propertyDescriptorsMap.put(translationsPropertyDescriptor.getName(), translationsPropertyDescriptor); } } resetPropertyDescriptorCaches(); initializingPropertyDescriptorsMap = false; } } } } private void completeWithComputedNlsDescriptors(IPropertyDescriptor rawDescriptor) { String barePropertyName = rawDescriptor.getName(); if (barePropertyName.endsWith(RAW_SUFFIX)) { barePropertyName = barePropertyName.substring(0, barePropertyName.length() - RAW_SUFFIX.length()); } BasicStringPropertyDescriptor nlsDescriptor = (BasicStringPropertyDescriptor) rawDescriptor.clone(); nlsDescriptor.setName(barePropertyName + NLS_SUFFIX); nlsDescriptor.setDelegateWritable(true); nlsDescriptor.setComputed(true); if (!isPurelyAbstract()) { nlsDescriptor.setSqlName("(SELECT T.TRANSLATED_VALUE FROM {tableName}_T T WHERE T." + "T_{tableName}_ID = ID AND T.LANGUAGE = :JspressoSessionGlobals.language AND " + "T.PROPERTY_NAME = '" + barePropertyName + "')"); } BasicStringPropertyDescriptor rawOrNlsDescriptor = (BasicStringPropertyDescriptor) rawDescriptor.clone(); rawOrNlsDescriptor.setName(barePropertyName); rawOrNlsDescriptor.setDelegateWritable(true); rawOrNlsDescriptor.setComputed(true); if (!isPurelyAbstract()) { rawOrNlsDescriptor.setSqlName( "CASE WHEN " + nlsDescriptor.getSqlName() + " IS NULL THEN " + ((BasicPropertyDescriptor) rawDescriptor) .getSqlName() + " ELSE " + nlsDescriptor.getSqlName() + " END"); } propertyDescriptorsMap.put(nlsDescriptor.getName(), nlsDescriptor); propertyDescriptorsMap.put(rawOrNlsDescriptor.getName(), rawOrNlsDescriptor); } private final Object delegateServicesLock = new Object(); private void registerDelegateServicesIfNecessary() { if (serviceDelegateClassNames != null) { synchronized (delegateServicesLock) { if (serviceDelegateClassNames != null) { for (Entry<String, String> nextEntry : serviceDelegateClassNames.entrySet()) { try { IComponentService delegate = null; String serviceClassName = nextEntry.getKey(); String serviceDelegateClassName = nextEntry.getValue(); if (!(serviceDelegateClassName == null || "".equals(serviceDelegateClassName) || "null".equalsIgnoreCase( serviceDelegateClassName))) { delegate = (IComponentService) Class.forName(serviceDelegateClassName).newInstance(); } registerService(Class.forName(ObjectUtils.extractRawClassName(serviceClassName)), delegate); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException ex) { throw new DescriptorException(ex); } } serviceDelegateClassNames = null; } } } if (serviceDelegateBeanNames != null && beanFactory != null) { synchronized (delegateServicesLock) { if (serviceDelegateBeanNames != null && beanFactory != null) { for (Entry<String, String> nextEntry : serviceDelegateBeanNames.entrySet()) { try { String serviceClassName = nextEntry.getKey(); String serviceDelegateBeanName = nextEntry.getValue(); if (!(serviceDelegateBeanName == null || "".equals(serviceDelegateBeanName) || "null".equalsIgnoreCase( serviceDelegateBeanName))) { registerService(Class.forName(ObjectUtils.extractRawClassName(serviceClassName)), beanFactory.getBean(serviceDelegateBeanName, IComponentService.class)); } } catch (ClassNotFoundException ex) { throw new DescriptorException(ex); } } serviceDelegateBeanNames = null; } } } } private void registerLifecycleInterceptor(ILifecycleInterceptor<?> lifecycleInterceptor) { if (lifecycleInterceptors == null) { lifecycleInterceptors = new ArrayList<>(); } lifecycleInterceptors.add(lifecycleInterceptor); } private final Object lifecycleInterceptorsLock = new Object(); private void registerLifecycleInterceptorsIfNecessary() { if (lifecycleInterceptorClassNames != null) { synchronized (lifecycleInterceptorsLock) { // process creation of lifecycle interceptors. if (lifecycleInterceptorClassNames != null) { for (String lifecycleInterceptorClassName : lifecycleInterceptorClassNames) { try { registerLifecycleInterceptor( (ILifecycleInterceptor<?>) Class.forName(lifecycleInterceptorClassName).newInstance()); } catch (InstantiationException | ClassNotFoundException | IllegalAccessException ex) { throw new DescriptorException(ex); } } lifecycleInterceptorClassNames = null; } } } if (lifecycleInterceptorBeanNames != null && beanFactory != null) { synchronized (lifecycleInterceptorsLock) { if (lifecycleInterceptorBeanNames != null && beanFactory != null) { for (String lifecycleInterceptorBeanName : lifecycleInterceptorBeanNames) { registerLifecycleInterceptor( beanFactory.getBean(lifecycleInterceptorBeanName, ILifecycleInterceptor.class)); } lifecycleInterceptorBeanNames = null; } } } } private void registerService(Class<?> serviceContract, IComponentService service) { if (serviceDelegates == null) { serviceDelegates = new THashMap<>(1, 1.0f); serviceContracts = new THashSet<>(1); } serviceContracts.add(serviceContract); Method[] contractServices = serviceContract.getMethods(); for (Method serviceMethod : contractServices) { serviceDelegates.put(serviceMethod.getName(), service); } } /** * Returns the component contract class name. * <p/> * {@inheritDoc} */ @Override public String getPermId() { return getName(); } /** * A component permanent id is forced to be its fully-qualified class name. * Trying to set it to another value will raise an exception. * <p/> * {@inheritDoc} */ @Override public void setPermId(String permId) { throw new UnsupportedOperationException(); } /** * Allow subclasses to hook up and potentially transform the actual property * descriptor. * * @param propertyDescriptor * the original property descriptor. * @return the transformed property descriptor. */ protected IPropertyDescriptor refinePropertyDescriptor(IPropertyDescriptor propertyDescriptor) { return propertyDescriptor; } /** * Is translated. * * @return the boolean */ @Override public boolean isTranslatable() { if (isDeclaredTranslatable()) { return true; } if (getAncestorDescriptors() != null) { for (IComponentDescriptor<?> ancestorDescriptor : getAncestorDescriptors()) { if (ancestorDescriptor.isTranslatable()) { return true; } } } return false; } /** * Is declared translatable. * * @return the boolean */ protected boolean isDeclaredTranslatable() { Collection<IPropertyDescriptor> propertyDescriptors = getDeclaredPropertyDescriptors(); if (propertyDescriptors != null) { for (IPropertyDescriptor pDesc : propertyDescriptors) { if (pDesc instanceof IStringPropertyDescriptor) { if (((IStringPropertyDescriptor) pDesc).isTranslatable()) { return true; } } } } return false; } /** * Sets component translations descriptor template. * * @param template * the template */ public static synchronized void setComponentTranslationsDescriptorTemplate( BasicCollectionPropertyDescriptor<IComponent> template) { componentTranslationsDescriptorTemplate = template; } /** * Gets component translations descriptor template. * * @return the component translation descriptor template */ public static synchronized BasicCollectionPropertyDescriptor<IComponent> getComponentTranslationsDescriptorTemplate() { return componentTranslationsDescriptorTemplate; } }