Java tutorial
/******************************************************************************* * SORMAS - Surveillance Outbreak Response Management & Analysis System * Copyright 2016-2018 Helmholtz-Zentrum fr Infektionsforschung GmbH (HZI) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. *******************************************************************************/ package de.symeda.sormas.ui.utils; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import com.vaadin.ui.Component; import com.vaadin.ui.CustomLayout; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.themes.ValoTheme; import com.vaadin.v7.data.Item; import com.vaadin.v7.data.Validator; import com.vaadin.v7.data.Validator.InvalidValueException; import com.vaadin.v7.data.fieldgroup.BeanFieldGroup; import com.vaadin.v7.data.fieldgroup.FieldGroup; import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitEvent; import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitException; import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitHandler; import com.vaadin.v7.data.util.BeanItem; import com.vaadin.v7.data.util.converter.Converter.ConversionException; import com.vaadin.v7.ui.AbstractField; import com.vaadin.v7.ui.ComboBox; import com.vaadin.v7.ui.CustomField; import com.vaadin.v7.ui.DateField; import com.vaadin.v7.ui.Field; import com.vaadin.v7.ui.OptionGroup; import de.symeda.sormas.api.Disease; import de.symeda.sormas.api.EntityDto; import de.symeda.sormas.api.FacadeProvider; import de.symeda.sormas.api.i18n.I18nProperties; import de.symeda.sormas.api.user.UserRight; import de.symeda.sormas.api.utils.Diseases; import de.symeda.sormas.api.utils.Outbreaks; import de.symeda.sormas.ui.UserProvider; @SuppressWarnings("serial") public abstract class AbstractEditForm<DTO extends EntityDto> extends CustomField<DTO> implements CommitHandler {// implements DtoEditForm<DTO> { private final BeanFieldGroup<DTO> fieldGroup; private final String propertyI18nPrefix; private Class<DTO> type; private boolean hideValidationUntilNextCommit = false; private List<Field<?>> customFields = new ArrayList<>(); private List<Field<?>> visibleAllowedFields = new ArrayList<>(); protected AbstractEditForm(Class<DTO> type, String propertyI18nPrefix, UserRight editOrCreateUserRight) { this(type, propertyI18nPrefix, editOrCreateUserRight, true); } protected AbstractEditForm(Class<DTO> type, String propertyI18nPrefix, UserRight editOrCreateUserRight, boolean addFields) { this.type = type; this.propertyI18nPrefix = propertyI18nPrefix; fieldGroup = new BeanFieldGroup<DTO>(type) { @Override protected void configureField(Field<?> field) { field.setBuffered(isBuffered()); if (!isEnabled()) { field.setEnabled(false); } if (field.getPropertyDataSource().isReadOnly()) { field.setReadOnly(true); } else if (isReadOnly()) { field.setReadOnly(true); } } }; fieldGroup.addCommitHandler(this); fieldGroup.setFieldFactory(new SormasFieldGroupFieldFactory(editOrCreateUserRight)); setWidth(900, Unit.PIXELS); setHeightUndefined(); if (addFields) { addFields(); } if (editOrCreateUserRight != null && !UserProvider.getCurrent().hasUserRight(editOrCreateUserRight)) { getFieldGroup().setReadOnly(true); } } @SuppressWarnings("rawtypes") public static CommitDiscardWrapperComponent<? extends AbstractEditForm> buildCommitDiscardWrapper( AbstractEditForm wrappedForm) { return new CommitDiscardWrapperComponent<>(wrappedForm, wrappedForm.getFieldGroup()); } @SuppressWarnings("rawtypes") public static CommitDiscardWrapperComponent<VerticalLayout> buildCommitDiscardWrapper( AbstractEditForm... wrappedForms) { VerticalLayout formsLayout = new VerticalLayout(); if (wrappedForms.length > 0) { // not perfect, but necessary to make this work in grid views like CaseDataView formsLayout.setWidth(wrappedForms[0].getWidth(), wrappedForms[0].getWidthUnits()); } FieldGroup[] fieldGroups = new FieldGroup[wrappedForms.length]; for (int i = 0; i < wrappedForms.length; i++) { formsLayout.addComponent(wrappedForms[i]); wrappedForms[i].setWidth(100, Unit.PERCENTAGE); fieldGroups[i] = wrappedForms[i].getFieldGroup(); } return new CommitDiscardWrapperComponent<>(formsLayout, fieldGroups); } @Override public CustomLayout initContent() { String htmlLayout = createHtmlLayout(); CustomLayout layout = new CustomLayout(); layout.setTemplateContents(htmlLayout); layout.setWidth(100, Unit.PERCENTAGE); layout.setHeightUndefined(); return layout; } @Override public Class<? extends DTO> getType() { return type; } @Override protected CustomLayout getContent() { return (CustomLayout) super.getContent(); } protected abstract String createHtmlLayout(); protected abstract void addFields(); @Override public void setValue(DTO newFieldValue) throws com.vaadin.v7.data.Property.ReadOnlyException, ConversionException { super.setValue(newFieldValue); } @Override protected DTO getInternalValue() { BeanItem<DTO> beanItem = getFieldGroup().getItemDataSource(); if (beanItem == null) { return null; } else { return beanItem.getBean(); } } @Override protected void setInternalValue(DTO newValue) { super.setInternalValue(newValue); BeanFieldGroup<DTO> fieldGroup = getFieldGroup(); fieldGroup.setItemDataSource(newValue); } @Override public boolean isModified() { if (getFieldGroup().isModified()) { return true; } return super.isModified(); } @Override public void preCommit(CommitEvent commitEvent) throws CommitException { if (hideValidationUntilNextCommit) { hideValidationUntilNextCommit = false; for (Field<?> field : getFieldGroup().getFields()) { if (field instanceof AbstractField) { AbstractField<?> abstractField = (AbstractField<?>) field; abstractField.setValidationVisible(true); } } for (Field<?> field : customFields) { if (field instanceof AbstractField) { AbstractField<?> abstractField = (AbstractField<?>) field; abstractField.setValidationVisible(true); } } } for (Field<?> field : customFields) { field.validate(); } } /** * Attention (!!!!) * This method is not called when used with CommitDiscardWrapperComponent (uses FieldGroup instead) */ @Override public void commit() throws SourceException, InvalidValueException { try { getFieldGroup().commit(); } catch (CommitException e) { if (e.getInvalidFields().size() > 0) { throw new InvalidValueException( e.getInvalidFields().keySet().stream().map(f -> f.getCaption()) .collect(Collectors.joining(", ")), e.getInvalidFields().values().stream().toArray(InvalidValueException[]::new)); } else { throw new SourceException(this, e); } } super.commit(); } @Override public void postCommit(CommitEvent commitEvent) throws CommitException { } @Override public void discard() throws SourceException { getFieldGroup().discard(); super.discard(); } public BeanFieldGroup<DTO> getFieldGroup() { return this.fieldGroup; } protected void addFields(String... properties) { for (String property : properties) { addField(property); } } @SuppressWarnings("rawtypes") protected <T extends Field> void addFields(Class<T> fieldType, String... properties) { for (String property : properties) { addField(property, fieldType); } } @SuppressWarnings("rawtypes") protected <T extends Field> T addCustomField(String fieldId, Class<?> dataType, Class<T> fieldType) { T field = getFieldGroup().getFieldFactory().createField(dataType, fieldType); formatField(field, fieldId); addDefaultAdditionalValidators(field); getContent().addComponent(field, fieldId); customFields.add(field); return field; } /** * Adds the field to the form by using addField(fieldId, fieldType), but additionally sets up a ValueChangeListener * that makes sure the value that is about to be selected is added to the list of allowed values. This is intended * to be used for Disease fields that might contain a disease that is no longer active in the system and thus will * not be returned by DiseaseHelper.isActivePrimaryDisease(disease). */ @SuppressWarnings("rawtypes") protected ComboBox addDiseaseField(String fieldId, boolean showNonPrimaryDiseases) { ComboBox field = addField(fieldId, ComboBox.class); if (showNonPrimaryDiseases) { addNonPrimaryDiseasesTo(field); } // Make sure that the ComboBox still contains a pre-selected inactive disease field.addValueChangeListener(e -> { Object value = e.getProperty().getValue(); if (value != null && !field.containsId(value)) { Item newItem = field.addItem(value); newItem.getItemProperty(SormasFieldGroupFieldFactory.CAPTION_PROPERTY_ID) .setValue(value.toString()); } }); return field; } @SuppressWarnings({ "unchecked", "rawtypes" }) protected <T extends Field> T addField(String propertyId) { return (T) addField(propertyId, Field.class); } @SuppressWarnings("rawtypes") protected <T extends Field> T addField(String propertyId, Class<T> fieldType) { T field = getFieldGroup().buildAndBind(propertyId, (Object) propertyId, fieldType); formatField(field, propertyId); field.setId(propertyId); getContent().addComponent(field, propertyId); addDefaultAdditionalValidators(field); return field; } @SuppressWarnings("rawtypes") /** * @param allowedDaysInFuture How many days in the future the value of this field can be or * -1 for no restriction at all */ protected <T extends Field> T addDateField(String propertyId, Class<T> fieldType, int allowedDaysInFuture) { T field = getFieldGroup().buildAndBind(propertyId, (Object) propertyId, fieldType); formatField(field, propertyId); field.setId(propertyId); getContent().addComponent(field, propertyId); addFutureDateValidator(field, allowedDaysInFuture); return field; } @SuppressWarnings("rawtypes") protected <T extends Field> T formatField(T field, String propertyId) { String caption = I18nProperties.getPrefixCaption(getPropertyI18nPrefix(), propertyId, field.getCaption()); field.setCaption(caption); if (field instanceof AbstractField) { AbstractField abstractField = (AbstractField) field; abstractField.setDescription(I18nProperties.getPrefixDescription(getPropertyI18nPrefix(), propertyId, abstractField.getDescription())); if (hideValidationUntilNextCommit) { if (!abstractField.isInvalidCommitted()) { abstractField.setValidationVisible(false); } } } String validationError = I18nProperties.getPrefixValidationError(getPropertyI18nPrefix(), propertyId, caption); field.setRequiredError(validationError); field.setWidth(100, Unit.PERCENTAGE); return field; } @SuppressWarnings("rawtypes") protected <T extends Field> T addDefaultAdditionalValidators(T field) { addFutureDateValidator(field, 0); return field; } @SuppressWarnings("rawtypes") protected <T extends Field> T addFutureDateValidator(T field, int amountOfDays) { if (amountOfDays < 0) { return field; } if (DateField.class.isAssignableFrom(field.getClass()) || DateTimeField.class.isAssignableFrom(field.getClass())) { field.addValidator(new FutureDateValidator(field, amountOfDays, field.getCaption())); } return field; } public Field<?> getField(String fieldOrPropertyId) { Field<?> field = getFieldGroup().getField(fieldOrPropertyId); if (field == null) { // try to get the field from the layout Component component = getContent().getComponent(fieldOrPropertyId); if (component instanceof Field<?>) { field = (Field<?>) component; } } return field; } protected void styleAsOptionGroupHorizontal(List<String> fields) { for (String field : fields) { CssStyles.style((OptionGroup) getFieldGroup().getField(field), ValoTheme.OPTIONGROUP_HORIZONTAL); } } protected void setReadOnly(boolean readOnly, String... fieldOrPropertyIds) { for (String propertyId : fieldOrPropertyIds) { getField(propertyId).setReadOnly(readOnly); } } protected void setVisible(boolean visible, String... fieldOrPropertyIds) { for (String propertyId : fieldOrPropertyIds) { if (visible == false || isVisibleAllowed(propertyId)) { getField(propertyId).setVisible(visible); } } } protected void discard(String... propertyIds) { for (String propertyId : propertyIds) { getField(propertyId).discard(); } } protected void setRequired(boolean required, String... fieldOrPropertyIds) { for (String propertyId : fieldOrPropertyIds) { Field<?> field = getField(propertyId); field.setRequired(required); } } protected void setSoftRequired(boolean required, String... fieldOrPropertyIds) { for (String propertyId : fieldOrPropertyIds) { Field<?> field = getField(propertyId); if (required) { FieldHelper.addSoftRequiredStyle(field); } else { FieldHelper.removeSoftRequiredStyle(field); } } } protected void addFieldListeners(String fieldOrPropertyId, ValueChangeListener... listeners) { for (ValueChangeListener listener : listeners) { getField(fieldOrPropertyId).addValueChangeListener(listener); } } protected void addValidators(String fieldOrPropertyId, Validator... validators) { for (Validator validator : validators) { getField(fieldOrPropertyId).addValidator(validator); } } protected String getPropertyI18nPrefix() { return propertyI18nPrefix; } public void hideValidationUntilNextCommit() { this.hideValidationUntilNextCommit = true; for (Field<?> field : getFieldGroup().getFields()) { if (field instanceof AbstractField) { AbstractField<?> abstractField = (AbstractField<?>) field; if (!abstractField.isInvalidCommitted()) { abstractField.setValidationVisible(false); } } } for (Field<?> field : customFields) { if (field instanceof AbstractField) { AbstractField<?> abstractField = (AbstractField<?>) field; if (!abstractField.isInvalidCommitted()) { abstractField.setValidationVisible(false); } } } } protected void addNonPrimaryDiseasesTo(ComboBox diseaseField) { List<Disease> diseases = FacadeProvider.getDiseaseConfigurationFacade().getAllActiveDiseases(); for (Disease disease : diseases) { if (diseaseField.getItem(disease) != null) { continue; } Item newItem = diseaseField.addItem(disease); newItem.getItemProperty(SormasFieldGroupFieldFactory.CAPTION_PROPERTY_ID).setValue(disease.toString()); } } /** * Sets the initial visibilities based on annotations and builds a list of all fields in a form that are allowed to be visible - * this is either because the @Diseases and @Outbreaks annotations are not relevant or at least one of these annotations are present on the respective field. * * @param disease Not null if the @Diseases annotation should be taken into account * @param viewMode Not null if the @Outbreaks annotation should be taken into account */ protected void initializeVisibilitiesAndAllowedVisibilities(Disease disease, ViewMode viewMode) { for (Object propertyId : getFieldGroup().getBoundPropertyIds()) { Field<?> field = getFieldGroup().getField(propertyId); boolean diseaseVisibility = true; boolean outbreakVisibility = true; if (disease != null) { if (!Diseases.DiseasesConfiguration.isDefinedOrMissing(getType(), (String) propertyId, disease)) { diseaseVisibility = false; } } if (viewMode != null && viewMode == ViewMode.SIMPLE) { if (!Outbreaks.OutbreaksConfiguration.isDefined(getType(), (String) propertyId)) { outbreakVisibility = false; } } if (diseaseVisibility && outbreakVisibility) { visibleAllowedFields.add(field); } else { field.setVisible(false); } } } /** * Returns true if the visibleAllowedFields list is either empty (because all fields are allowed to be visible) or contains * the given field. This needs to be called before EVERY setVisible or setVisibleWhen call. */ protected boolean isVisibleAllowed(Field<?> field) { return visibleAllowedFields.isEmpty() || visibleAllowedFields.contains(field); } protected boolean isVisibleAllowed(String propertyId) { return isVisibleAllowed(getFieldGroup().getField(propertyId)); } }