Java tutorial
/* Licensed 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 com.ocs.dynamo.domain.model.impl; import java.io.Serializable; import java.math.BigDecimal; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.commons.lang.StringUtils; import com.google.common.collect.Lists; import com.ocs.dynamo.constants.DynamoConstants; import com.ocs.dynamo.domain.AbstractEntity; import com.ocs.dynamo.domain.model.AttributeDateType; import com.ocs.dynamo.domain.model.AttributeModel; import com.ocs.dynamo.domain.model.AttributeSelectMode; import com.ocs.dynamo.domain.model.AttributeTextFieldMode; import com.ocs.dynamo.domain.model.AttributeType; import com.ocs.dynamo.domain.model.EntityModel; import com.ocs.dynamo.exception.OCSRuntimeException; import com.ocs.dynamo.service.BaseService; import com.ocs.dynamo.service.MessageService; import com.ocs.dynamo.ui.ServiceLocator; import com.ocs.dynamo.ui.component.EntityComboBox; import com.ocs.dynamo.ui.component.EntityComboBox.SelectMode; import com.ocs.dynamo.ui.component.EntityListSelect; import com.ocs.dynamo.ui.component.EntityLookupField; import com.ocs.dynamo.ui.component.FancyListSelect; import com.ocs.dynamo.ui.component.QuickAddEntityComboBox; import com.ocs.dynamo.ui.component.QuickAddListSelect; import com.ocs.dynamo.ui.component.TimeField; import com.ocs.dynamo.ui.component.TokenFieldSelect; import com.ocs.dynamo.ui.component.URLField; import com.ocs.dynamo.ui.composite.form.CollectionTable; import com.ocs.dynamo.ui.composite.form.FormOptions; import com.ocs.dynamo.ui.converter.ConverterFactory; import com.ocs.dynamo.ui.converter.WeekCodeConverter; import com.ocs.dynamo.ui.utils.VaadinUtils; import com.ocs.dynamo.ui.validator.URLValidator; import com.ocs.dynamo.utils.SystemPropertyUtils; import com.vaadin.data.Container; import com.vaadin.data.Container.Filter; import com.vaadin.data.Item; import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory; import com.vaadin.data.sort.SortOrder; import com.vaadin.data.validator.BeanValidator; import com.vaadin.data.validator.EmailValidator; import com.vaadin.server.VaadinSession; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.combobox.FilteringMode; import com.vaadin.shared.ui.datefield.Resolution; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.AbstractField; import com.vaadin.ui.AbstractSelect; import com.vaadin.ui.AbstractTextField; import com.vaadin.ui.ComboBox; import com.vaadin.ui.Component; import com.vaadin.ui.DateField; import com.vaadin.ui.DefaultFieldFactory; import com.vaadin.ui.Field; import com.vaadin.ui.TableFieldFactory; import com.vaadin.ui.TextArea; import com.vaadin.ui.TextField; /** * Extension of the standard Vaadin field factory for creating custom fields * * @author bas.rutten * @param <T> * the type of the entity for which to create a field */ public class ModelBasedFieldFactory<T> extends DefaultFieldGroupFieldFactory implements TableFieldFactory { private static ConcurrentMap<String, ModelBasedFieldFactory<?>> nonValidatingInstances = new ConcurrentHashMap<>(); private static ConcurrentMap<String, ModelBasedFieldFactory<?>> searchInstances = new ConcurrentHashMap<>(); private static final long serialVersionUID = -5684112523268959448L; private static ConcurrentMap<String, ModelBasedFieldFactory<?>> validatingInstances = new ConcurrentHashMap<>(); private MessageService messageService; private EntityModel<T> model; // indicates whether the system is in search mode. In search mode, components for // some attributes are constructed differently (e.g. we render two search fields to be able to // search for a range of integers) private boolean search; // indicates whether extra validators must be added. This is the case when // using the field factory in an // editable table private boolean validate; /** * Returns an appropriate instance from the pool, or creates a new one * * @param model * the entity model * @param messageService * @return */ @SuppressWarnings("unchecked") public static <T> ModelBasedFieldFactory<T> getInstance(EntityModel<T> model, MessageService messageService) { if (!nonValidatingInstances.containsKey(model.getReference())) { nonValidatingInstances.put(model.getReference(), new ModelBasedFieldFactory<>(model, messageService, false, false)); } return (ModelBasedFieldFactory<T>) nonValidatingInstances.get(model.getReference()); } /** * Returns an appropriate instance from the pool, or creates a new one * * @param model * @param messageService * @return */ @SuppressWarnings("unchecked") public static <T> ModelBasedFieldFactory<T> getSearchInstance(EntityModel<T> model, MessageService messageService) { if (!searchInstances.containsKey(model.getReference())) { searchInstances.put(model.getReference(), new ModelBasedFieldFactory<>(model, messageService, false, true)); } return (ModelBasedFieldFactory<T>) searchInstances.get(model.getReference()); } /** * Returns an appropriate instance from the pool, or creates a new one * * @param model * @param messageService * @return */ @SuppressWarnings("unchecked") public static <T> ModelBasedFieldFactory<T> getValidatingInstance(EntityModel<T> model, MessageService messageService) { if (!validatingInstances.containsKey(model.getReference())) { validatingInstances.put(model.getReference(), new ModelBasedFieldFactory<>(model, messageService, true, false)); } return (ModelBasedFieldFactory<T>) validatingInstances.get(model.getReference()); } /** * Constructor * * @param model * the entity model * @param messageService * the message service * @param validate * whether to add extra validators (this is the case when the field is displayed * inside a table) * @param search * whether the fields are displayed inside a search form (this has an effect on the * construction of some fields) */ public ModelBasedFieldFactory(EntityModel<T> model, MessageService messageService, boolean validate, boolean search) { this.model = model; this.messageService = messageService; this.validate = validate; this.search = search; } /** * Constructs a combo box- the sort order will be taken from the entity model * * @param entityModel * the entity model to base the combo box on * @param attributeModel * the attribute model * @param filter * optional field filter - only items that match the filter will be included * @return */ @SuppressWarnings("unchecked") public <ID extends Serializable, S extends AbstractEntity<ID>> AbstractField<?> constructComboBox( EntityModel<?> entityModel, AttributeModel attributeModel, Filter filter, boolean search) { entityModel = resolveEntityModel(entityModel, attributeModel); BaseService<ID, S> service = (BaseService<ID, S>) ServiceLocator .getServiceForEntity(entityModel.getEntityClass()); SortOrder[] sos = constructSortOrder(entityModel); if (attributeModel != null && attributeModel.isQuickAddAllowed() && !search) { return new QuickAddEntityComboBox<ID, S>((EntityModel<S>) entityModel, attributeModel, service, SelectMode.FILTERED, filter, null, sos); } else { return new EntityComboBox<ID, S>((EntityModel<S>) entityModel, attributeModel, service, filter, sos); } } /** * Constructs a field based on an attribute model and possibly a field filter * * @param attributeModel * the attribute model * @param fieldFilters * the list of field filters * @param fieldEntityModel * the custom entity model for the field * @return */ public Field<?> constructField(AttributeModel attributeModel, Map<String, Filter> fieldFilters, EntityModel<?> fieldEntityModel) { Filter fieldFilter = fieldFilters == null ? null : fieldFilters.get(attributeModel.getPath()); Field<?> field = null; if (fieldFilter != null) { if (AttributeType.MASTER.equals(attributeModel.getAttributeType())) { // create a combo box or lookup field field = constructSelectField(attributeModel, fieldEntityModel, fieldFilter); } else { // detail relationship, render a multiple select field = this.constructCollectionSelect(fieldEntityModel, attributeModel, fieldFilter, true, search); } } else { // no field filter present - delegate to default construction field = this.createField(attributeModel.getPath(), fieldEntityModel); } // mark the field as required (this is skipped for search fields since // making search fields required makes no sense) if (!search) { field.setRequired(attributeModel.isRequired()); } if (field instanceof AbstractComponent) { ((AbstractComponent) field).setImmediate(true); } return field; } /** * Constructs a select component for selecting multiple values * * @param entityModel * the entity model of the entity to display in the field * @param attributeModel * the attribute model of the property that is displayed in the ListSelect * @param fieldFilter * optional field filter * @param multipleSelect * is multiple select supported? * @param search * indicates whether the component is being used in a search screen * @return */ @SuppressWarnings("unchecked") public <ID extends Serializable, S extends AbstractEntity<ID>> Field<?> constructCollectionSelect( EntityModel<?> fieldEntityModel, AttributeModel attributeModel, Filter fieldFilter, boolean multipleSelect, boolean search) { EntityModel<?> em = resolveEntityModel(fieldEntityModel, attributeModel); BaseService<ID, S> service = (BaseService<ID, S>) ServiceLocator.getServiceForEntity(em.getEntityClass()); SortOrder[] sos = constructSortOrder(em); boolean searchToken = (search && AttributeSelectMode.TOKEN.equals(attributeModel.getSearchSelectMode())); boolean searchLookup = (search && AttributeSelectMode.LOOKUP.equals(attributeModel.getSearchSelectMode())); if (searchLookup || AttributeSelectMode.LOOKUP.equals(attributeModel.getSelectMode())) { // lookup field - warning: do not use nested entity model here! Field<?> field = (Field<?>) constructLookupField((EntityModel<S>) fieldEntityModel, attributeModel, fieldFilter, search, true); return field; } else if (searchToken || AttributeSelectMode.TOKEN.equals(attributeModel.getSelectMode())) { // token field in case of detail relation or multiple search field TokenFieldSelect<ID, S> tokenFieldSelect = new TokenFieldSelect<ID, S>((EntityModel<S>) em, attributeModel, service, fieldFilter, search, sos); return tokenFieldSelect; } else if (AttributeSelectMode.FANCY_LIST.equals(attributeModel.getSelectMode()) || (search && AttributeSelectMode.FANCY_LIST.equals(attributeModel.getSearchSelectMode()))) { // fancy list in case specified or when searching for multiple values FancyListSelect<ID, S> listSelect = new FancyListSelect<ID, S>(service, (EntityModel<S>) em, attributeModel, fieldFilter, search, sos); listSelect.setRows(SystemPropertyUtils.getDefaultListSelectRows()); return listSelect; } else if (attributeModel.isQuickAddAllowed() && !search) { // quick add list select when in edit mode QuickAddListSelect<ID, S> quickSelect = new QuickAddListSelect<ID, S>((EntityModel<S>) em, attributeModel, service, fieldFilter, multipleSelect, SystemPropertyUtils.getDefaultListSelectRows(), sos); return quickSelect; } else { // simple list select if everything else fails EntityListSelect<ID, S> listSelect = new EntityListSelect<ID, S>((EntityModel<S>) em, attributeModel, service, fieldFilter, sos); listSelect.setMultiSelect(multipleSelect); listSelect.setRows(SystemPropertyUtils.getDefaultListSelectRows()); return listSelect; } } /** * Constructs a lookup field (field that brings up a popup search dialog) * * @param entityModel * the entity model of the entity to display in the field * @param attributeModel * the attribute model of the property that is bound to the field * @param fieldFilter * optional field filter * @return */ @SuppressWarnings("unchecked") public <ID extends Serializable, S extends AbstractEntity<ID>> EntityLookupField<ID, S> constructLookupField( EntityModel<?> overruled, AttributeModel attributeModel, Filter fieldFilter, boolean search, boolean multiSelect) { // for a lookup field, don't use the nested model but the base model - this is // because the search in the popup screen is conducted on the non-nested entity list EntityModel<?> entityModel = overruled != null ? overruled : ServiceLocator.getEntityModelFactory().getModel(attributeModel.getType()); BaseService<ID, S> service = (BaseService<ID, S>) ServiceLocator .getServiceForEntity(entityModel.getEntityClass()); SortOrder[] sos = constructSortOrder(entityModel); return new EntityLookupField<ID, S>(service, (EntityModel<S>) entityModel, attributeModel, fieldFilter == null ? null : Lists.newArrayList(fieldFilter), search, multiSelect, sos.length == 0 ? null : sos[0]); } /** * Create a combo box for searching on a boolean. This combo box contains three values (yes, no, * and null) * * @return */ public ComboBox constructSearchBooleanComboBox(AttributeModel am) { ComboBox cb = new ComboBox(); cb.addItem(Boolean.TRUE); cb.setItemCaption(Boolean.TRUE, am.getTrueRepresentation()); cb.addItem(Boolean.FALSE); cb.setItemCaption(Boolean.FALSE, am.getFalseRepresentation()); return cb; } /** * Construct a combo box that contains a list of String values * * @param values * the list of values * @param am * the attribute model * @return */ public static ComboBox constructStringListCombo(List<String> values, AttributeModel am) { ComboBox cb = new ComboBox(); cb.setCaption(am.getDisplayName()); cb.addItems(values); cb.setFilteringMode(FilteringMode.CONTAINS); return cb; } /** * Constructs the default sort order of a component based on an Entity Model * * @param entityModel * the entity model * @return */ private SortOrder[] constructSortOrder(EntityModel<?> entityModel) { SortOrder[] sos = new SortOrder[entityModel.getSortOrder().size()]; int i = 0; for (AttributeModel am : entityModel.getSortOrder().keySet()) { sos[i++] = new SortOrder(am.getName(), entityModel.getSortOrder().get(am) ? SortDirection.ASCENDING : SortDirection.DESCENDING); } return sos; } /** * Creates a field for displaying an enumeration * * @param type * the type of enum the values to display * @param fieldType * @return */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected <E extends Field<?>> E createEnumCombo(Class<?> type, Class<E> fieldType) { AbstractSelect s = createCompatibleSelect((Class<? extends AbstractSelect>) fieldType); s.setNullSelectionAllowed(true); fillEnumField(s, (Class<? extends Enum>) type); return (E) s; } /** * Creates a field - overridden from the default field factory * * @param type * the type of the property that is bound to the field * @param fieldType * the type of the field * @return */ @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public <F extends Field> F createField(Class<?> type, Class<F> fieldType) { if (Enum.class.isAssignableFrom(type)) { if (AbstractSelect.class.isAssignableFrom(fieldType)) { return createEnumCombo(type, fieldType); } else { ComboBox cb = (ComboBox) createEnumCombo(type, ComboBox.class); cb.setFilteringMode(FilteringMode.CONTAINS); return (F) cb; } } else if (AbstractEntity.class.isAssignableFrom(type)) { // inside a table, always use a combo box EntityModel<?> entityModel = ServiceLocator.getEntityModelFactory().getModel(type); return (F) constructComboBox(entityModel, null, null, search); } return super.createField(type, fieldType); } /** * Creates a field (called when creating the field inside a table) * * @param container * @param itemId * @param propertyId */ @Override public Field<?> createField(Container container, Object itemId, Object propertyId, Component uiContext) { return createField(propertyId.toString(), null); } /** * * Creates a field for a certain property ID * * @param propertyId * @return */ public Field<?> createField(String propertyId) { return createField(propertyId, null); } /** * Creates a field * * @param propertyId * the name of the property that can be edited by this field * @param fieldEntityModel * the custom entity model for the field * @return */ public Field<?> createField(String propertyId, EntityModel<?> fieldEntityModel) { // in case of a read-only field, return <code>null</code> so Vaadin will // render a label instead AttributeModel attributeModel = model.getAttributeModel(propertyId); if (attributeModel.isReadOnly() && (!attributeModel.isUrl() && !AttributeType.DETAIL.equals(attributeModel.getAttributeType())) && !search) { return null; } Field<?> field = null; if (AttributeTextFieldMode.TEXTAREA.equals(attributeModel.getTextFieldMode()) && !search) { // text area field field = new TextArea(); } else if (attributeModel.isWeek()) { // special case - week field in a table TextField tf = new TextField(); tf.setConverter(new WeekCodeConverter()); field = tf; } else if (search && attributeModel.getType().equals(Boolean.class)) { // in a search screen, we need to offer the true, false, and // undefined options field = constructSearchBooleanComboBox(attributeModel); } else if (AbstractEntity.class.isAssignableFrom(attributeModel.getType())) { // lookup or combo field for an entity field = constructSelectField(attributeModel, fieldEntityModel, null); } else if (AttributeType.ELEMENT_COLLECTION.equals(attributeModel.getAttributeType())) { // use a "collection table" for an element collection FormOptions fo = new FormOptions(); fo.setShowRemoveButton(true); if (String.class.equals(attributeModel.getMemberType())) { CollectionTable<String> table = new CollectionTable<>(false, fo, String.class); table.setMinLength(attributeModel.getMinLength()); table.setMaxLength(attributeModel.getMaxLength()); field = table; } else if (Integer.class.equals(attributeModel.getMemberType())) { CollectionTable<Integer> table = new CollectionTable<>(false, fo, Integer.class); field = table; } else { // other types not supported for now throw new OCSRuntimeException(); } } else if (Collection.class.isAssignableFrom(attributeModel.getType())) { // render a multiple select component for a collection field = constructCollectionSelect(attributeModel.getNestedEntityModel(), attributeModel, null, true, search); } else if (AttributeDateType.TIME.equals(attributeModel.getDateType())) { TimeField tf = new TimeField(); tf.setResolution(Resolution.MINUTE); tf.setLocale(VaadinSession.getCurrent() == null ? DynamoConstants.DEFAULT_LOCALE : VaadinSession.getCurrent().getLocale()); field = tf; } else if (attributeModel.isUrl()) { // URL field (offers clickable link in readonly mode) TextField tf = (TextField) createField(attributeModel.getType(), Field.class); tf.addValidator(new URLValidator(messageService.getMessage("ocs.no.valid.url"))); tf.setNullRepresentation(null); tf.setSizeFull(); // wrap text field in URL field field = new URLField(tf, attributeModel, false); field.setSizeFull(); } else { // just a regular field field = createField(attributeModel.getType(), Field.class); } field.setCaption(attributeModel.getDisplayName()); postProcessField(field, attributeModel); // add a field validator based on JSR-303 bean validation if (validate) { field.addValidator(new BeanValidator(model.getEntityClass(), (String) propertyId)); // disable the field if it cannot be edited if (!attributeModel.isUrl()) { field.setEnabled(!attributeModel.isReadOnly()); } if (attributeModel.isNumerical()) { field.addStyleName(DynamoConstants.CSS_NUMERICAL); } } return field; } /** * Add additional field settings to a field * * @param field * @param attributeModel */ private void postProcessField(Field<?> field, AttributeModel attributeModel) { if (field instanceof AbstractTextField) { AbstractTextField textField = (AbstractTextField) field; textField.setDescription(attributeModel.getDescription()); textField.setNullSettingAllowed(true); textField.setNullRepresentation(""); if (!StringUtils.isEmpty(attributeModel.getPrompt())) { textField.setInputPrompt(attributeModel.getPrompt()); } // set converters setConverters(textField, attributeModel); // add email validator if (attributeModel.isEmail()) { field.addValidator(new EmailValidator(messageService.getMessage("ocs.no.valid.email"))); } } else if (field instanceof DateField) { // set a separate format for a date field DateField dateField = (DateField) field; if (attributeModel.getDisplayFormat() != null) { dateField.setDateFormat(attributeModel.getDisplayFormat()); } // display minutes only when dealing with time stamps if (AttributeDateType.TIMESTAMP.equals(attributeModel.getDateType())) { dateField.setResolution(Resolution.MINUTE); } } } /** * Creates a select field for a single-valued attribute * * @param attributeModel * the attribute * @param fieldEntityModel * the (overruled) entity model * @param fieldFilter * the field filter * @return */ protected Field<?> constructSelectField(AttributeModel attributeModel, EntityModel<?> fieldEntityModel, Filter fieldFilter) { Field<?> field = null; if (search && attributeModel.isMultipleSearch()) { // complex search field with multiple selection field = this.constructCollectionSelect(fieldEntityModel, attributeModel, fieldFilter, true, search); } else if (AttributeSelectMode.COMBO.equals(attributeModel.getSelectMode())) { // combo box field = (Field<?>) constructComboBox(fieldEntityModel, attributeModel, fieldFilter, search); } else if (AttributeSelectMode.LOOKUP.equals(attributeModel.getSelectMode())) { // single select lookup field field = (Field<?>) constructLookupField(fieldEntityModel, attributeModel, fieldFilter, search, false); } else { // list select (single select) field = this.constructCollectionSelect(fieldEntityModel, attributeModel, fieldFilter, false, search); } return field; } /** * Fills an enumeration field with messages from the message bundle * * @param select * @param enumClass */ @SuppressWarnings("unchecked") private <E extends Enum<E>> void fillEnumField(AbstractSelect select, Class<E> enumClass) { select.removeAllItems(); for (Object p : select.getContainerPropertyIds()) { select.removeContainerProperty(p); } select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, ""); select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID); for (E e : enumClass.getEnumConstants()) { Item newItem = select.addItem(e); String msg = messageService.getEnumMessage(enumClass, e); if (msg != null) { newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(msg); } else { newItem.getItemProperty(CAPTION_PROPERTY_ID) .setValue(DefaultFieldFactory.createCaptionByPropertyId(e.name())); } } } public EntityModel<T> getModel() { return model; } /** * Resolves an entity model by falling back first to the nested attribute model and then to the * default model * * @param entityModel * the entity model * @param attributeModel * the attribute model * @return */ private EntityModel<?> resolveEntityModel(EntityModel<?> entityModel, AttributeModel attributeModel) { if (entityModel == null) { if (attributeModel.getNestedEntityModel() != null) { entityModel = attributeModel.getNestedEntityModel(); } else { Class<?> type = attributeModel.getMemberType() != null ? attributeModel.getMemberType() : attributeModel.getType(); entityModel = ServiceLocator.getEntityModelFactory() .getModel(type.asSubclass(AbstractEntity.class)); } } return entityModel; } /** * Set the appropriate converter on a text field * * @param textField * the field * @param attributeModel * the attribute model of the attribute to bind to the field */ protected void setConverters(AbstractTextField textField, AttributeModel attributeModel) { if (attributeModel.getType().equals(BigDecimal.class)) { textField.setConverter(ConverterFactory.createBigDecimalConverter(attributeModel.isCurrency(), attributeModel.isPercentage(), false, attributeModel.getPrecision(), VaadinUtils.getCurrencySymbol())); } else if (attributeModel.getType().equals(Integer.class)) { textField.setConverter( ConverterFactory.createIntegerConverter(SystemPropertyUtils.useThousandsGroupingInEditMode())); } else if (attributeModel.getType().equals(Long.class)) { textField.setConverter( ConverterFactory.createLongConverter(SystemPropertyUtils.useThousandsGroupingInEditMode())); } } }