Java tutorial
/** * Copyright (c) 2015 Bosch Software Innovations GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.hawkbit.ui.rollout.rollout; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.ui.common.builder.ComboBoxBuilder; import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; import org.eclipse.hawkbit.ui.common.builder.TextAreaBuilder; import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleNoBorderWithIcon; import org.eclipse.hawkbit.ui.filtermanagement.TargetFilterBeanQuery; import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.util.StringUtils; import org.springframework.util.concurrent.ListenableFuture; import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Validator; import com.vaadin.data.util.converter.StringToFloatConverter; import com.vaadin.data.util.converter.StringToIntegerConverter; import com.vaadin.data.validator.FloatRangeValidator; import com.vaadin.data.validator.IntegerRangeValidator; import com.vaadin.server.FontAwesome; import com.vaadin.server.UserError; import com.vaadin.ui.Button; import com.vaadin.ui.ComboBox; import com.vaadin.ui.Component; import com.vaadin.ui.GridLayout; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.TextArea; import com.vaadin.ui.TextField; import com.vaadin.ui.UI; /** * Define groups for a Rollout */ public class DefineGroupsLayout extends GridLayout { private static final long serialVersionUID = 1L; private static final String MESSAGE_ROLLOUT_MAX_GROUP_SIZE_EXCEEDED = "message.rollout.max.group.size.exceeded.advanced"; private final VaadinMessageSource i18n; private transient EntityFactory entityFactory; private transient RolloutManagement rolloutManagement; private transient RolloutGroupManagement rolloutGroupManagement; private final transient QuotaManagement quotaManagement; private transient TargetFilterQueryManagement targetFilterQueryManagement; private String defaultTriggerThreshold; private String defaultErrorThreshold; private String targetFilter; private transient List<GroupRow> groupRows; private int groupsCount; private transient List<RolloutGroupCreate> savedRolloutGroups; private transient ValidationListener validationListener; private ValidationStatus validationStatus = ValidationStatus.VALID; private transient RolloutGroupsValidation groupsValidation; private final AtomicInteger runningValidationsCounter; DefineGroupsLayout(final VaadinMessageSource i18n, final EntityFactory entityFactory, final RolloutManagement rolloutManagement, final TargetFilterQueryManagement targetFilterQueryManagement, final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement) { this.i18n = i18n; this.entityFactory = entityFactory; this.rolloutManagement = rolloutManagement; this.rolloutGroupManagement = rolloutGroupManagement; this.quotaManagement = quotaManagement; this.targetFilterQueryManagement = targetFilterQueryManagement; runningValidationsCounter = new AtomicInteger(0); groupRows = new ArrayList<>(10); setSizeUndefined(); buildLayout(); } private void buildLayout() { setSpacing(Boolean.TRUE); setSizeUndefined(); setRows(3); setColumns(6); setStyleName("marginTop"); addComponent(getLabel("caption.rollout.group.definition.desc"), 0, 0, 5, 0); final int headerRow = 1; addComponent(getLabel("header.name"), 0, headerRow); addComponent(getLabel("header.target.filter.query"), 1, headerRow); addComponent(getLabel("header.target.percentage"), 2, headerRow); addComponent(getLabel("header.rolloutgroup.threshold"), 3, headerRow); addComponent(getLabel("header.rolloutgroup.threshold.error"), 4, headerRow); addComponent(createAddButton(), 0, 2, 5, 2); } /** * @param targetFilter * the target filter which is required for verification */ public void setTargetFilter(final String targetFilter) { this.targetFilter = targetFilter; updateValidation(); } private int addRow() { final int insertIndex = getRows() - 1; insertRow(insertIndex); return insertIndex; } private Label getLabel(final String key) { return new LabelBuilder().name(i18n.getMessage(key)).buildLabel(); } private Button createAddButton() { final Button button = SPUIComponentProvider.getButton(UIComponentIdProvider.ROLLOUT_GROUP_ADD_ID, i18n.getMessage("button.rollout.add.group"), "", "", true, FontAwesome.PLUS, SPUIButtonStyleNoBorderWithIcon.class); button.setSizeUndefined(); button.addStyleName("default-color"); button.setEnabled(true); button.setVisible(true); button.addClickListener(event -> addGroupRowAndValidate()); return button; } private GroupRow addGroupRow() { final int rowIndex = addRow(); final GroupRow groupRow = new GroupRow(); groupRow.addToGridRow(this, rowIndex); groupRows.add(groupRow); return groupRow; } private GroupRow addGroupRowAndValidate() { final GroupRow groupRow = addGroupRow(); updateValidation(); return groupRow; } public List<RolloutGroupCreate> getSavedRolloutGroups() { return savedRolloutGroups; } /** * @return the validation instance if was already validated */ public RolloutGroupsValidation getGroupsValidation() { return groupsValidation; } private void removeAllRows() { for (int i = getRows() - 2; i > 1; i--) { removeRow(i); } groupRows.clear(); } public void setDefaultTriggerThreshold(final String defaultTriggerThreshold) { this.defaultTriggerThreshold = defaultTriggerThreshold; } public void setDefaultErrorThreshold(final String defaultErrorThreshold) { this.defaultErrorThreshold = defaultErrorThreshold; } /** * Reset the field values. */ public void resetComponents() { validationStatus = ValidationStatus.VALID; groupsCount = 0; removeAllRows(); addGroupRowAndValidate(); } /** * Populate groups by rollout * * @param rollout * the rollout */ public void populateByRollout(final Rollout rollout) { if (rollout == null) { return; } removeAllRows(); final List<RolloutGroup> groups = rolloutGroupManagement .findByRollout(PageRequest.of(0, quotaManagement.getMaxRolloutGroupsPerRollout()), rollout.getId()) .getContent(); for (final RolloutGroup group : groups) { final GroupRow groupRow = addGroupRow(); groupRow.populateByGroup(group); } updateValidation(); } /** * @return whether the groups definition form is valid */ public boolean isValid() { if (groupRows.isEmpty() || validationStatus != ValidationStatus.VALID) { return false; } return groupRows.stream().allMatch(GroupRow::isValid); } private void updateValidation() { validationStatus = ValidationStatus.VALID; if (isValid()) { setValidationStatus(ValidationStatus.LOADING); savedRolloutGroups = getGroupsFromRows(); validateRemainingTargets(); } else { resetErrors(); setValidationStatus(ValidationStatus.INVALID); } } private void setValidationStatus(final ValidationStatus status) { validationStatus = status; if (validationListener != null) { validationListener.validation(status); } } private void resetErrors() { groupRows.forEach(GroupRow::resetError); } private void validateRemainingTargets() { resetErrors(); if (targetFilter == null) { return; } if (runningValidationsCounter.incrementAndGet() == 1) { final ListenableFuture<RolloutGroupsValidation> validateTargetsInGroups = rolloutManagement .validateTargetsInGroups(savedRolloutGroups, targetFilter, System.currentTimeMillis()); final UI ui = UI.getCurrent(); validateTargetsInGroups.addCallback(validation -> ui.access(() -> setGroupsValidation(validation)), throwable -> ui.access(() -> setGroupsValidation(null))); return; } runningValidationsCounter.incrementAndGet(); } /** * YOU SHOULD NOT CALL THIS METHOD MANUALLY. It's only for the callback. * Only 1 runningValidation should be executed. If this runningValidation is * done, then this method is called. Maybe then a new runningValidation is * executed. * */ private void setGroupsValidation(final RolloutGroupsValidation validation) { final int runningValidation = runningValidationsCounter.getAndSet(0); if (runningValidation > 1) { validateRemainingTargets(); return; } groupsValidation = validation; final int lastIdx = groupRows.size() - 1; final GroupRow lastRow = groupRows.get(lastIdx); if (groupsValidation != null && groupsValidation.isValid() && validationStatus != ValidationStatus.INVALID) { lastRow.resetError(); setValidationStatus(ValidationStatus.VALID); } else { lastRow.setError(i18n.getMessage("message.rollout.remaining.targets.error")); setValidationStatus(ValidationStatus.INVALID); } // validate the single groups final int maxTargets = quotaManagement.getMaxTargetsPerRolloutGroup(); final boolean hasRemainingTargetsError = validationStatus == ValidationStatus.INVALID; for (int i = 0; i < groupRows.size(); ++i) { final GroupRow row = groupRows.get(i); // do not mask the 'remaining targets' error if (hasRemainingTargetsError && row.equals(lastRow)) { continue; } row.resetError(); final Long count = groupsValidation.getTargetsPerGroup().get(i); if (count != null && count > maxTargets) { row.setError(i18n.getMessage(MESSAGE_ROLLOUT_MAX_GROUP_SIZE_EXCEEDED, maxTargets)); setValidationStatus(ValidationStatus.INVALID); } } } private List<RolloutGroupCreate> getGroupsFromRows() { return groupRows.stream().map(GroupRow::getGroupEntity).collect(Collectors.toList()); } public void setValidationListener(final ValidationListener validationListener) { this.validationListener = validationListener; } /** * Status of the groups validation */ public enum ValidationStatus { VALID, INVALID, LOADING } /** * Implement the interface and set the instance with setValidationListener * to receive updates for any changes within the group rows. */ @FunctionalInterface public interface ValidationListener { /** * Is called after user input * * @param isValid * whether the input of the group rows is valid */ void validation(ValidationStatus isValid); } private class GroupRow { private TextField groupName; private ComboBox targetFilterQueryCombo; private TextArea targetFilterQuery; private TextField targetPercentage; private TextField triggerThreshold; private TextField errorThreshold; private HorizontalLayout optionsLayout; private boolean populated; private boolean initialized; public GroupRow() { init(); } private void init() { groupsCount += 1; groupName = createTextField("textfield.name", UIComponentIdProvider.ROLLOUT_GROUP_LIST_GRID_ID); groupName.setValue(i18n.getMessage("textfield.rollout.group.default.name", groupsCount)); groupName.setStyleName("rollout-group-name"); groupName.addValueChangeListener(event -> valueChanged()); targetFilterQueryCombo = createTargetFilterQueryCombo(); populateTargetFilterQuery(); targetFilterQueryCombo.addValueChangeListener(event -> valueChanged()); targetFilterQuery = createTargetFilterQuery(); targetPercentage = createPercentageWithDecimalsField("textfield.target.percentage", UIComponentIdProvider.ROLLOUT_GROUP_TARGET_PERC_ID); targetPercentage.setValue("100"); targetPercentage.addValueChangeListener(event -> valueChanged()); triggerThreshold = createPercentageField("prompt.tigger.threshold", UIComponentIdProvider.ROLLOUT_TRIGGER_THRESOLD_ID); triggerThreshold.setValue(defaultTriggerThreshold); triggerThreshold.addValueChangeListener(event -> valueChanged()); errorThreshold = createPercentageField("prompt.error.threshold", UIComponentIdProvider.ROLLOUT_ERROR_THRESOLD_ID); errorThreshold.setValue(defaultErrorThreshold); errorThreshold.addValueChangeListener(event -> valueChanged()); optionsLayout = new HorizontalLayout(); optionsLayout.addComponent(createRemoveButton()); initialized = true; } private TextField createTextField(final String in18Key, final String id) { final TextField textField = new TextFieldBuilder(RolloutGroup.NAME_MAX_SIZE).required(true, i18n) .prompt(i18n.getMessage(in18Key)).id(id).buildTextComponent(); textField.setSizeUndefined(); return textField; } private TextField createPercentageField(final String in18Key, final String id) { final TextField textField = new TextFieldBuilder(32).prompt(i18n.getMessage(in18Key)).id(id) .buildTextComponent(); textField.setWidth(80, Unit.PIXELS); textField.setConverter(new StringToIntegerConverter()); textField.addValidator(this::validateMandatoryPercentage); return textField; } private TextField createPercentageWithDecimalsField(final String in18Key, final String id) { final TextField textField = createPercentageField(in18Key, id); textField.setConverter(new StringToFloatConverter()); return textField; } private void removeGroupRow(final GroupRow groupRow) { groupRows.remove(groupRow); updateValidation(); } private void validateMandatoryPercentage(final Object value) { if (value != null) { final String message = i18n.getMessage("message.rollout.field.value.range", 0, 100); if (value instanceof Float) { new FloatRangeValidator(message, 0F, 100F).validate(value); } if (value instanceof Integer) { new IntegerRangeValidator(message, 0, 100).validate(value); } } else { throw new Validator.EmptyValueException(i18n.getMessage("message.enter.number")); } } private void valueChanged() { if (initialized) { updateValidation(); } } private ComboBox createTargetFilterQueryCombo() { return new ComboBoxBuilder().setId(UIComponentIdProvider.ROLLOUT_TARGET_FILTER_COMBO_ID) .setPrompt(i18n.getMessage("prompt.target.filter")).buildCombBox(); } private TextArea createTargetFilterQuery() { final TextArea filterField = new TextAreaBuilder(TargetFilterQuery.QUERY_MAX_SIZE) .style("text-area-style").id(UIComponentIdProvider.ROLLOUT_TARGET_FILTER_QUERY_FIELD) .buildTextComponent(); filterField.setEnabled(false); filterField.setSizeUndefined(); return filterField; } private void populateTargetFilterQuery() { final Container container = createTargetFilterComboContainer(); targetFilterQueryCombo.setContainerDataSource(container); } private void populateTargetFilterQuery(final RolloutGroup group) { if (StringUtils.isEmpty(group.getTargetFilterQuery())) { targetFilterQueryCombo.setValue(null); } else { final Page<TargetFilterQuery> filterQueries = targetFilterQueryManagement .findByQuery(PageRequest.of(0, 1), group.getTargetFilterQuery()); if (filterQueries.getTotalElements() > 0) { final TargetFilterQuery filterQuery = filterQueries.getContent().get(0); targetFilterQueryCombo.setValue(filterQuery.getName()); } } } private Container createTargetFilterComboContainer() { final BeanQueryFactory<TargetFilterBeanQuery> targetFilterQF = new BeanQueryFactory<>( TargetFilterBeanQuery.class); return new LazyQueryContainer( new LazyQueryDefinition(true, SPUIDefinitions.PAGE_SIZE, SPUILabelDefinitions.VAR_NAME), targetFilterQF); } private Button createRemoveButton() { final Button button = SPUIComponentProvider.getButton(UIComponentIdProvider.ROLLOUT_GROUP_REMOVE_ID, "", "", "", true, FontAwesome.MINUS, SPUIButtonStyleNoBorderWithIcon.class); button.setSizeUndefined(); button.addStyleName("default-color"); button.setEnabled(true); button.setVisible(true); button.addClickListener(event -> onRemove()); return button; } private void onRemove() { final int index = findRowIndexFor(groupName, 0); if (index != -1) { removeRow(index); } removeGroupRow(this); } private int findRowIndexFor(final Component component, final int col) { final int rows = getRows(); for (int i = 0; i < rows; i++) { final Component rowComponent = getComponent(col, i); if (component.equals(rowComponent)) { return i; } } return -1; } private String getTargetFilterQuery() { if (!StringUtils.hasText((String) targetFilterQueryCombo.getValue())) { return null; } final Item filterItem = targetFilterQueryCombo.getContainerDataSource() .getItem(targetFilterQueryCombo.getValue()); return (String) filterItem.getItemProperty("query").getValue(); } /** * Adds this group row to a grid layout * * @param layout * the grid layout * @param rowIndex * the row of the grid layout */ public void addToGridRow(final GridLayout layout, final int rowIndex) { layout.addComponent(groupName, 0, rowIndex); if (populated) { layout.addComponent(targetFilterQuery, 1, rowIndex); } else { layout.addComponent(targetFilterQueryCombo, 1, rowIndex); } layout.addComponent(targetPercentage, 2, rowIndex); layout.addComponent(triggerThreshold, 3, rowIndex); layout.addComponent(errorThreshold, 4, rowIndex); layout.addComponent(optionsLayout, 5, rowIndex); } /** * Builds a group definition from this group row * * @return the RolloutGroupCreate definition */ public RolloutGroupCreate getGroupEntity() { final RolloutGroupConditionBuilder conditionBuilder = new RolloutGroupConditionBuilder() .successAction(RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP, null).successCondition( RolloutGroup.RolloutGroupSuccessCondition.THRESHOLD, triggerThreshold.getValue()); if (!StringUtils.isEmpty(errorThreshold.getValue())) { conditionBuilder .errorCondition(RolloutGroup.RolloutGroupErrorCondition.THRESHOLD, errorThreshold.getValue()) .errorAction(RolloutGroup.RolloutGroupErrorAction.PAUSE, null); } final String percentageString = targetPercentage.getValue().replace(",", "."); final Float percentage = Float.parseFloat(percentageString); return entityFactory.rolloutGroup().create().name(groupName.getValue()) .description(groupName.getValue()).targetFilterQuery(getTargetFilterQuery()) .targetPercentage(percentage).conditions(conditionBuilder.build()); } /** * Populates the row with the data from the provided groups. * * @param group * the data source */ public void populateByGroup(final RolloutGroup group) { initialized = false; groupName.setValue(group.getName()); targetFilterQuery.setValue(group.getTargetFilterQuery()); populateTargetFilterQuery(group); targetPercentage.setValue(String.format("%.2f", group.getTargetPercentage())); triggerThreshold.setValue(group.getSuccessConditionExp()); errorThreshold.setValue(group.getErrorConditionExp()); populated = true; initialized = true; } /** * @return whether the data entered in this row is valid */ public boolean isValid() { return !StringUtils.isEmpty(groupName.getValue()) && targetPercentage.isValid() && triggerThreshold.isValid() && errorThreshold.isValid(); } private void setError(final String error) { targetPercentage.setComponentError(new UserError(error)); } /** * Hides an error of the row */ private void resetError() { targetPercentage.setComponentError(null); } } }