All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.holonplatform.vaadin7.internal.components.DefaultPropertyInputGroup Maven / Gradle / Ivy

/*
 * Copyright 2016-2017 Axioma srl.
 * 
 * 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.holonplatform.vaadin7.internal.components;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.holonplatform.core.Validator;
import com.holonplatform.core.Validator.ValidationException;
import com.holonplatform.core.i18n.Localizable;
import com.holonplatform.core.internal.utils.ObjectUtils;
import com.holonplatform.core.property.Property;
import com.holonplatform.core.property.PropertyBox;
import com.holonplatform.core.property.PropertyRenderer;
import com.holonplatform.core.property.PropertyRendererRegistry.NoSuitableRendererAvailableException;
import com.holonplatform.vaadin7.Registration;
import com.holonplatform.vaadin7.components.Input;
import com.holonplatform.vaadin7.components.PropertyBinding;
import com.holonplatform.vaadin7.components.PropertyInputGroup;
import com.holonplatform.vaadin7.components.PropertyValueComponentSource;
import com.holonplatform.vaadin7.components.ValidationStatusHandler;
import com.holonplatform.vaadin7.components.ValueComponent;
import com.holonplatform.vaadin7.components.PropertyBinding.PostProcessor;
import com.holonplatform.vaadin7.components.ValidationStatusHandler.Status;
import com.holonplatform.core.property.VirtualProperty;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.ui.Field;

/**
 * Default {@link PropertyInputGroup} implementation.
 *
 * @since 5.0.0
 */
public class DefaultPropertyInputGroup implements PropertyInputGroup, PropertyValueComponentSource {

	private static final long serialVersionUID = -5441417959315472240L;

	/**
	 * Current value
	 */
	private PropertyBox value;

	/**
	 * Property set
	 */
	@SuppressWarnings("rawtypes")
	private final List propertySet = new LinkedList<>();

	/**
	 * Property configurations
	 */
	@SuppressWarnings("rawtypes")
	private final Map properties = new LinkedHashMap<>();

	/**
	 * Value change listeners
	 */
	private final List> valueChangeListeners = new LinkedList<>();

	/**
	 * Validators
	 */
	private final List> validators = new LinkedList<>();

	/**
	 * Input post-processors
	 */
	private final List>> postProcessors = new LinkedList<>();

	/**
	 * Overall validation status handler
	 */
	private ValidationStatusHandler validationStatusHandler = ValidationStatusHandler.notification();

	/**
	 * Validation status handler for all the properties
	 */
	private ValidationStatusHandler propertiesValidationStatusHandler = null;

	/**
	 * Whether to validate inputs at value change
	 */
	private boolean validateOnValueChange = true;

	/**
	 * Validation behaviour
	 */
	private boolean stopValidationAtFirstFailure = false;

	/**
	 * Overall validation behaviour
	 */
	private boolean stopOverallValidationAtFirstFailure = false;

	/**
	 * Ignore validation
	 */
	private boolean ignorePropertyValidation = false;

	/**
	 * Whether to ignore missing inputs
	 */
	private boolean ignoreMissingInputs = false;

	/**
	 * External {@link ValueComponent} supplier
	 */
	private Supplier> valueComponentSupplier;

	/**
	 * Constructor
	 */
	public DefaultPropertyInputGroup() {
		super();
	}

	/**
	 * Get the external {@link ValueComponent} supplier.
	 * @return Optional external {@link ValueComponent} supplier
	 */
	protected Optional> getOverallValueComponent() {
		return Optional.ofNullable(valueComponentSupplier != null ? valueComponentSupplier.get() : null);
	}

	/**
	 * Set the external {@link ValueComponent} supplier.
	 * @param valueComponentSupplier the {@link ValueComponent} supplier to set
	 */
	void setValueComponentSupplier(Supplier> valueComponentSupplier) {
		this.valueComponentSupplier = valueComponentSupplier;
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertySetBound#getProperties()
	 */
	@SuppressWarnings("rawtypes")
	@Override
	public Iterable getProperties() {
		return Collections.unmodifiableList(propertySet);
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertySetBound#hasProperty(com.holonplatform.core.property.Property)
	 */
	@Override
	public boolean hasProperty(Property property) {
		ObjectUtils.argumentNotNull(property, "Property must be not null");
		return propertySet.contains(property);
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertySetBound#propertyStream()
	 */
	@SuppressWarnings("rawtypes")
	@Override
	public Stream propertyStream() {
		return propertySet.stream();
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputContainer#getInputs()
	 */
	@Override
	public Iterable> getInputs() {
		return propertySet.stream().filter(p -> _propertyConfiguration(p).getInput().isPresent())
				.map(p -> _propertyConfiguration(p).getInput().get()).collect(Collectors.toList());
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * com.holonplatform.vaadin.components.PropertyInputContainer#getInput(com.holonplatform.core.property.Property)
	 */
	@Override
	public  Optional> getInput(Property property) {
		ObjectUtils.argumentNotNull(property, "Property must be not null");
		if (propertySet.contains(property)) {
			return getPropertyConfiguration(property).getInput();
		}
		return Optional.empty();
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputContainer#stream()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public  Stream>> stream() {
		return propertySet.stream().filter(p -> _propertyConfiguration(p).getInput().isPresent())
				.map(p -> PropertyBinding.create(p, _propertyConfiguration(p).getInput().get()));
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyValueComponentSource#getValueComponents()
	 */
	@SuppressWarnings("rawtypes")
	@Override
	public Iterable getValueComponents() {
		return propertySet.stream().filter(p -> _propertyConfiguration(p).getInput().isPresent())
				.map(p -> _propertyConfiguration(p).getInput().get()).collect(Collectors.toList());
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyValueComponentSource#getValueComponent(com.holonplatform.core.
	 * property.Property)
	 */
	@Override
	public  Optional> getValueComponent(Property property) {
		ObjectUtils.argumentNotNull(property, "Property must be not null");
		if (propertySet.contains(property)) {
			return getPropertyConfiguration(property).getInput().map(i -> i);
		}
		return Optional.empty();
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyValueComponentSource#streamOfValueComponents()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Stream>> streamOfValueComponents() {
		return propertySet.stream().filter(p -> _propertyConfiguration(p).getInput().isPresent())
				.map(p -> PropertyBinding.create(p, _propertyConfiguration(p).getInput().get()));
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.InputGroup#clear()
	 */
	@Override
	public void clear() {
		setValue(null, false);
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.Validatable#validate()
	 */
	@Override
	public void validate() throws ValidationException {
		// validate inputs
		validateInputs();
		// validate value
		validate(getValue(false));
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputGroup#getValue(boolean)
	 */
	@Override
	public PropertyBox getValue(boolean validate) {
		PropertyBox value = PropertyBox.builder(propertySet).build();
		flush(value, validate);
		return value;
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputGroup#getValueIfValid()
	 */
	@Override
	public Optional getValueIfValid() {
		try {
			return Optional.of(getValue(true));
		} catch (@SuppressWarnings("unused") ValidationException e) {
			return Optional.empty();
		}
	}

	/**
	 * Writes the property bound {@link Input} components values, obtained through the {@link Input#getValue()} method,
	 * to given propertyBox,
	 * @param propertyBox the {@link PropertyBox} into which to write the property values (not null)
	 * @param validate whether to perform inputs and overall validation
	 */
	@SuppressWarnings("unchecked")
	private void flush(PropertyBox propertyBox, boolean validate) {
		ObjectUtils.argumentNotNull(propertyBox, "PropertyBox must be not null");

		if (validate) {
			// inputs validation
			validateInputs();
		}

		propertySet.forEach(p -> {
			final PropertyConfiguration cfg = getPropertyConfiguration(p);
			if (cfg.isHidden()) {
				getCurrentPropertyValue(p).ifPresent(v -> {
					propertyBox.setValue(p, v);
				});
			} else {
				cfg.getInput().ifPresent(i -> {
					propertyBox.setValue(p, i.getValue());
				});
			}
		});

		if (validate) {
			// Overall validation
			validate(propertyBox);
		}
	}

	/**
	 * Reset all the {@link Input}s values.
	 * @param setDefaultValue Whether to set the default value when available
	 */
	@SuppressWarnings({ "unchecked", "rawtypes", "unused" })
	protected void resetValues(boolean setDefaultValue) {
		propertySet.forEach(p -> {
			final PropertyConfiguration cfg = _propertyConfiguration(p);
			cfg.getInput().ifPresent(i -> {
				try {
					// clear input
					boolean ro = i.isReadOnly();
					if (ro)
						i.setReadOnly(false);
					i.clear();
					if (ro)
						i.setReadOnly(true);

					// check default value
					if (setDefaultValue && !cfg.isReadOnly()) {
						cfg.getDefaultValueProvider().ifPresent(dvp -> ((Input) i).setValue(dvp.getDefaultValue(p)));
					}
				} catch (ValidationException | InvalidValueException ve) {
					// ignore any validation error
				}

				// reset validation status
				resetValidationStatus(i, p);
			});
		});

		// reset overall validation status
		resetValidationStatus(getOverallValueComponent().orElse(null), null);

	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputGroup#setValue(com.holonplatform.core.property.PropertyBox,
	 * boolean)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void setValue(final PropertyBox propertyBox, boolean validate) {
		this.value = propertyBox;

		// reset
		resetValues(propertyBox == null);

		// load
		if (propertyBox != null) {
			propertySet.forEach(p -> {
				final PropertyConfiguration cfg = getPropertyConfiguration(p);
				cfg.getInput().ifPresent(i -> {
					Object value = getPropertyValue(propertyBox, p);
					if (value != null) {
						// ignore read-only
						boolean ro = i.isReadOnly();
						if (ro)
							i.setReadOnly(false);
						i.setValue(value);
						if (ro)
							i.setReadOnly(true);
					} else {
						i.clear();
					}
				});
			});
		}

		// check validation
		if (validate) {
			validate();
		}

		// fire value change
		fireValueChange(propertyBox);
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.ValueHolder#isEmpty()
	 */
	@Override
	public boolean isEmpty() {
		return getValue(false).propertyValues().filter(v -> v.hasValue()).findAny().isPresent();
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.ValueHolder#addValueChangeListener(com.holonplatform.vaadin.components.
	 * ValueHolder.ValueChangeListener)
	 */
	@Override
	public Registration addValueChangeListener(ValueChangeListener listener) {
		ObjectUtils.argumentNotNull(listener, "ValueChangeListener must be not null");
		valueChangeListeners.add(listener);
		return () -> valueChangeListeners.remove(listener);
	}

	/**
	 * Emits the value change event
	 * @param value the changed value
	 */
	protected void fireValueChange(PropertyBox value) {
		final ValueChangeEvent valueChangeEvent = new DefaultValueChangeEvent<>(this, value);
		valueChangeListeners.forEach(l -> l.valueChange(valueChangeEvent));
	}

	/**
	 * Get whether to validate inputs at value change.
	 * @return true to validate inputs at value change
	 */
	protected boolean isValidateOnValueChange() {
		return validateOnValueChange;
	}

	/**
	 * Set whether to validate inputs at value change.
	 * @param validateOnValueChange true to validate inputs at value change
	 */
	public void setValidateOnValueChange(boolean validateOnValueChange) {
		this.validateOnValueChange = validateOnValueChange;
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputGroup#setEnabled(boolean)
	 */
	@Override
	public void setEnabled(boolean enabled) {
		getInputs().forEach(i -> i.getComponent().setEnabled(enabled));
	}

	/*
	 * (non-Javadoc)
	 * @see com.holonplatform.vaadin.components.PropertyInputGroup#setReadOnly(boolean)
	 */
	@Override
	public void setReadOnly(boolean readOnly) {
		propertySet.stream().filter(p -> !_propertyConfiguration(p).isReadOnly()).forEach(p -> {
			_propertyConfiguration(p).getInput().ifPresent(i -> {
				i.setReadOnly(readOnly);
			});
		});
	}

	/**
	 * Get the property configuration bound to given property.
	 * @param property Property for which to obtain the configuration
	 * @return The {@link PropertyConfiguration}
	 */
	protected PropertyConfiguration _propertyConfiguration(Property property) {
		return getPropertyConfiguration(property);
	}

	/**
	 * Get the property configuration bound to given property.
	 * @param property Property for which to obtain the configuration
	 * @param  Property type
	 * @return The {@link PropertyConfiguration}
	 */
	@SuppressWarnings("unchecked")
	protected  PropertyConfiguration getPropertyConfiguration(Property property) {
		properties.putIfAbsent(property, new PropertyConfiguration<>(property));
		return properties.get(property);
	}

	/**
	 * Add a property to the property set
	 * @param property Property to add (not null)
	 */
	public void addProperty(Property property) {
		ObjectUtils.argumentNotNull(property, "Property must be not null");
		if (!propertySet.contains(property)) {
			propertySet.add(property);
		}
	}

	/**
	 * Add an overall validator
	 * @param validator the {@link Validator} to add (not null)
	 */
	public void addValidator(Validator validator) {
		ObjectUtils.argumentNotNull(validator, "Validator must be not null");
		validators.add(validator);
	}

	/**
	 * Get the overall {@link Validator}s.
	 * @return the overall validators
	 */
	protected List> getValidators() {
		return validators;
	}

	/**
	 * Set the overall {@link ValidationStatusHandler}.
	 * @param validationStatusHandler the {@link ValidationStatusHandler} to set
	 */
	public void setValidationStatusHandler(ValidationStatusHandler validationStatusHandler) {
		this.validationStatusHandler = validationStatusHandler;
	}

	/**
	 * Get the overall {@link ValidationStatusHandler}, if available.
	 * @return the optional overall {@link ValidationStatusHandler}
	 */
	protected Optional getValidationStatusHandler() {
		return Optional.ofNullable(validationStatusHandler);
	}

	/**
	 * Set the {@link ValidationStatusHandler} to use for all the properties.
	 * @param propertiesValidationStatusHandler the {@link ValidationStatusHandler} to set
	 */
	public void setPropertiesValidationStatusHandler(ValidationStatusHandler propertiesValidationStatusHandler) {
		this.propertiesValidationStatusHandler = propertiesValidationStatusHandler;
	}

	/**
	 * Get the {@link ValidationStatusHandler} to use for all the properties.
	 * @return properties {@link ValidationStatusHandler}
	 */
	protected Optional getPropertiesValidationStatusHandler() {
		return Optional.ofNullable(propertiesValidationStatusHandler);
	}

	/**
	 * Get whether to stop validation at first validation failure.
	 * @return whether to stop validation at first validation failure
	 */
	protected boolean isStopValidationAtFirstFailure() {
		return stopValidationAtFirstFailure;
	}

	/**
	 * Set whether to stop validation at first validation failure.
	 * @param stopValidationAtFirstFailure true to stop validation at first validation failure
	 */
	public void setStopValidationAtFirstFailure(boolean stopValidationAtFirstFailure) {
		this.stopValidationAtFirstFailure = stopValidationAtFirstFailure;
	}

	/**
	 * Get whether to stop overall validation at first validation failure.
	 * @return whether to stop overall validation at first validation failure
	 */
	protected boolean isStopOverallValidationAtFirstFailure() {
		return stopOverallValidationAtFirstFailure;
	}

	/**
	 * Set whether to stop overall validation at first validation failure.
	 * @param stopOverallValidationAtFirstFailure true to stop overall validation at first validation
	 *        failure
	 */
	public void setStopOverallValidationAtFirstFailure(boolean stopOverallValidationAtFirstFailure) {
		this.stopOverallValidationAtFirstFailure = stopOverallValidationAtFirstFailure;
	}

	/**
	 * Get whether to ignore {@link Property} validators.
	 * @return true if {@link Property} validators must be ignored
	 */
	protected boolean isIgnorePropertyValidation() {
		return ignorePropertyValidation;
	}

	/**
	 * Set whether to ignore {@link Property} validators.
	 * @param ignorePropertyValidation true to ignore {@link Property} validators
	 */
	public void setIgnorePropertyValidation(boolean ignorePropertyValidation) {
		this.ignorePropertyValidation = ignorePropertyValidation;
	}

	/**
	 * Whether to ignore missing property {@link Input}s.
	 * @return true if missing property inputs must be ignored
	 */
	protected boolean isIgnoreMissingInputs() {
		return ignoreMissingInputs;
	}

	/**
	 * Set whether to ignore missing property inputs
	 * @param ignoreMissingInputs true to ignore missing property inputs
	 */
	public void setIgnoreMissingInputs(boolean ignoreMissingInputs) {
		this.ignoreMissingInputs = ignoreMissingInputs;
	}

	/**
	 * Add an {@link Input} {@link PostProcessor}.
	 * @param postProcessor the post-processor to add
	 */
	public void addInputPostProcessor(PostProcessor> postProcessor) {
		ObjectUtils.argumentNotNull(postProcessor, "InputPostProcessor must be not null");
		postProcessors.add(postProcessor);
	}

	/**
	 * Get the registered {@link Input} post processors.
	 * @return the {@link Input} post processors
	 */
	protected List>> getPostProcessors() {
		return postProcessors;
	}

	/**
	 * Build and bind {@link Input}s to the properties of the property set.
	 */
	public void build() {
		// render and bind inputs
		propertySet.forEach(p -> {
			// exclude hidden properties
			PropertyConfiguration configuration = _propertyConfiguration(p);
			if (!configuration.isHidden()) {
				renderAndBind(configuration);
			}
		});
	}

	/**
	 * Render the {@link Input} and set it up in given property configuration.
	 * @param  Property type
	 * @param configuration Property configuration
	 */
	private  void renderAndBind(PropertyConfiguration configuration) {
		final Optional> input = render(configuration.getProperty());
		if (!input.isPresent() && !isIgnoreMissingInputs()) {
			throw new NoSuitableRendererAvailableException(
					"No Input renderer available to render the property [" + configuration.getProperty() + "]");
		}
		input.ifPresent(i -> {
			// configure
			configureInput(configuration, i);
			// bind
			configuration.setInput(i);
		});
	}

	/**
	 * Render given property as a {@link Input}.
	 * @param  Property type
	 * @param property Property to render
	 * @return Rendered input
	 */
	@SuppressWarnings("unchecked")
	protected  Optional> render(Property property) {
		// check custom renderer
		final PropertyConfiguration cfg = getPropertyConfiguration(property);
		if (cfg.getRenderer().isPresent()) {
			final PropertyRenderer, T> r = cfg.getRenderer().get();
			// check render type
			if (!Input.class.isAssignableFrom(r.getRenderType())) {
				throw new IllegalStateException(
						"Renderer for property [" + property + "] is not of Input type: [" + r.getRenderType() + "]");
			}
			return Optional.ofNullable(r.render(property));
		}
		// use registry
		return property.renderIfAvailable(Input.class).map(i -> i);
	}

	/**
	 * Configure {@link Input} component before binding it to a {@link Property}.
	 * @param  Property type
	 * @param configuration Property configuration (not null)
	 * @param input {@link Input} component to configure
	 */
	protected  void configureInput(final PropertyConfiguration configuration, final Input input) {
		// Read-only
		if (configuration.isReadOnly()) {
			input.setReadOnly(true);
		}
		// Required
		if (configuration.isRequired()) {
			if (input instanceof RequiredIndicatorSupport) {
				((RequiredIndicatorSupport) input).setRequiredIndicatorVisible(true);
			} else {
				// fallback to default required setup
				if (input instanceof Field) {
					((Field) input).setRequired(true);
				} else if (input.getComponent() != null && input.getComponent() instanceof Field) {
					((Field) input.getComponent()).setRequired(true);
				}
			}
			// add required validator
			configuration
					.addValidatorAsFirst(configuration.getRequiredValidator().orElse(new RequiredInputValidator<>(input,
							configuration.getRequiredMessage().orElse(RequiredInputValidator.DEFAULT_REQUIRED_ERROR))));
		}
		// Check validation status handler
		if (!configuration.getPropertyValidationStatusHandler().isPresent()) {
			configuration.setPropertyValidationStatusHandler(
					getPropertiesValidationStatusHandler().orElse(ValidationStatusHandler.getDefault()));
		}
		// Validate on value change
		if (isValidateOnValueChange()) {
			input.addValueChangeListener(e -> validateOnChange(configuration, e.getValue()));
		}
		// post processors
		getPostProcessors().forEach(fc -> fc.process(configuration.getProperty(), input));
	}

	/**
	 * Overall validation
	 * @param value Value to validate
	 * @throws OverallValidationException If validation fails
	 */
	protected void validate(PropertyBox value) throws OverallValidationException {

		LinkedList failures = new LinkedList<>();
		for (Validator validator : getValidators()) {
			try {
				validator.validate(value);
			} catch (ValidationException ve) {
				failures.add(ve);
				if (isStopOverallValidationAtFirstFailure()) {
					break;
				}
			}
		}

		// collect validation exceptions, if any
		if (!failures.isEmpty()) {

			OverallValidationException validationException = (failures.size() == 1)
					? new OverallValidationException(failures.getFirst().getMessage(),
							failures.getFirst().getMessageCode(), failures.getFirst().getMessageArguments())
					: new OverallValidationException(failures.toArray(new ValidationException[failures.size()]));

			// notify validation status
			notifyInvalidValidationStatus(validationException, getOverallValueComponent().orElse(null), null);

			throw validationException;
		}

		// notify validation status
		notifyValidValidationStatus(getOverallValueComponent().orElse(null), null);
	}

	/**
	 * Validate all the {@link Input}s.
	 * @throws ValidationException If one or more input is not valid
	 */
	private void validateInputs() throws ValidationException {

		LinkedList failures = new LinkedList<>();

		// get all property configurations
		List> configurations = propertySet.stream().map(p -> _propertyConfiguration(p))
				.filter(cfg -> !cfg.isReadOnly() && cfg.getInput().isPresent()).collect(Collectors.toList());

		if (configurations != null) {

			if (isStopValidationAtFirstFailure()) {
				// reset validation status
				configurations.forEach(c -> resetValidationStatus(c.getInput().get(), c.getProperty()));
			}

			for (PropertyConfiguration configuration : configurations) {
				try {
					validateProperty(configuration);
				} catch (ValidationException e) {
					failures.add(e);

					if (isStopValidationAtFirstFailure()) {
						// break if stop validation at first failure
						break;
					}
				}
			}
		}

		// collect validation exceptions, if any
		if (!failures.isEmpty()) {
			if (failures.size() == 1) {
				throw failures.getFirst();
			} else {
				throw new ValidationException(failures.toArray(new ValidationException[0]));
			}
		}
	}

	/**
	 * Reset the validation status, if a {@link ValidationStatusHandler} is available.
	 * @param source Source component
	 * @param property Validation property, if null resets the overall validation status
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	protected void resetValidationStatus(ValueComponent source, Property property) {
		if (property != null) {
			getPropertyConfiguration(property).getPropertyValidationStatusHandler()
					.ifPresent(vsh -> vsh.validationStatusChange(
							new DefaultValidationStatusEvent(Status.UNRESOLVED, null, source, property)));
		} else {
			getValidationStatusHandler().ifPresent(vsh -> vsh
					.validationStatusChange(new DefaultValidationStatusEvent<>(Status.UNRESOLVED, null, null, null)));
		}
	}

	/**
	 * Notify a valid validation status, if a {@link ValidationStatusHandler} is available.
	 * @param  Property type
	 * @param source Source component
	 * @param property Validation property, if null notify the overall validation status
	 */
	protected  void notifyValidValidationStatus(ValueComponent source, Property property) {
		if (property != null) {
			getPropertyConfiguration(property).getPropertyValidationStatusHandler().ifPresent(vsh -> vsh
					.validationStatusChange(new DefaultValidationStatusEvent<>(Status.VALID, null, source, property)));
		} else {
			getValidationStatusHandler().ifPresent(vsh -> vsh
					.validationStatusChange(new DefaultValidationStatusEvent<>(Status.VALID, null, null, null)));
		}
	}

	/**
	 * Notify a invalid validation status, if a {@link ValidationStatusHandler} is available.
	 * @param  Property type
	 * @param e Validation exception
	 * @param source Source component
	 * @param property Validation property, if null notify the overall validation status
	 */
	protected  void notifyInvalidValidationStatus(ValidationException e, ValueComponent source,
			Property property) {
		if (property != null) {
			getPropertyConfiguration(property).getPropertyValidationStatusHandler()
					.ifPresent(vsh -> vsh.validationStatusChange(new DefaultValidationStatusEvent<>(Status.INVALID,
							e.getValidationMessages(), source, property)));
		} else {
			getValidationStatusHandler().ifPresent(vsh -> vsh.validationStatusChange(
					new DefaultValidationStatusEvent<>(Status.INVALID, e.getValidationMessages(), null, null)));
		}
	}

	/**
	 * Perform the validation of the input bound to given property configuration.
	 * @param  Property type
	 * @param configuration Property configuration
	 * @throws ValidationException If a validation error occurred
	 */
	private  void validateProperty(PropertyConfiguration configuration) throws ValidationException {
		if (configuration.getInput().isPresent()) {
			validateProperty(configuration, configuration.getInput().get().getValue());
		}
	}

	/**
	 * Validate the input bound to given property configuration, if available, swallowing any
	 * {@link ValidationException}.
	 * @param  Property type
	 * @param configuration Property configuration
	 * @param value Value to validate
	 */
	private  void validateOnChange(final PropertyConfiguration configuration, final T value) {
		try {
			validateProperty(configuration, value);
		} catch (@SuppressWarnings("unused") ValidationException e) {
			// ignore
		}
	}

	/**
	 * Validate the input bound to given property configuration.
	 * @param  Property type
	 * @param configuration Property configuration
	 * @param value Value to validate
	 * @throws ValidationException If a validation error occurred
	 */
	private  void validateProperty(final PropertyConfiguration configuration, final T value)
			throws ValidationException {
		if (configuration.getInput().isPresent()) {
			// input
			final Input input = configuration.getInput().get();

			final LinkedList failures = new LinkedList<>();

			try {
				// property validators
				if (!isIgnorePropertyValidation()) {
					configuration.getProperty().getValidators().forEach(v -> {
						v.validate(value);
					});
				}
				// input validators
				configuration.getValidators().forEach(v -> {
					v.validate(value);
				});
			} catch (ValidationException ve) {
				failures.add(ve);
			}

			if (!failures.isEmpty()) {

				ValidationException ve = (failures.size() == 1) ? failures.getFirst()
						: new ValidationException(failures.toArray(new ValidationException[0]));

				// notify status
				notifyInvalidValidationStatus(ve, input, configuration.getProperty());

				throw ve;
			}

			// notify validation status
			notifyValidValidationStatus(input, configuration.getProperty());
		}
	}

	/**
	 * Get the value of given property using given propertyBox.
	 * @param  Property type
	 * @param propertyBox PropertyBox
	 * @param property Property
	 * @return Property value
	 */
	protected  T getPropertyValue(PropertyBox propertyBox, Property property) {
		if (VirtualProperty.class.isAssignableFrom(property.getClass())) {
			if (((VirtualProperty) property).getValueProvider() != null) {
				return ((VirtualProperty) property).getValueProvider().getPropertyValue(propertyBox);
			}
			return null;
		}
		if (propertyBox.containsValue(property)) {
			return propertyBox.getValue(property);
		}
		return null;
	}

	/**
	 * Get the value of given property using current value, if available.
	 * @param  Property type
	 * @param property Property for which to obtain the value
	 * @return Property value, empty if current value is not available or the given property is not available in current
	 *         value
	 */
	private  Optional getCurrentPropertyValue(Property property) {
		if (value != null && value.contains(property)) {
			return Optional.ofNullable(value.getValue(property));
		}
		return Optional.empty();
	}

	// Builder

	/**
	 * {@link PropertyInputGroup} builder.
	 */
	static class InternalBuilder
			extends AbstractBuilder {

		public InternalBuilder() {
			super(new DefaultPropertyInputGroup());
		}

		@Override
		protected InternalBuilder builder() {
			return this;
		}

		@Override
		public DefaultPropertyInputGroup build() {
			instance.build();
			return instance;
		}

	}

	/**
	 * Default {@link PropertyInputGroupBuilder} implementation.
	 */
	public static class DefaultBuilder
			extends AbstractBuilder
			implements PropertyInputGroupBuilder {

		public DefaultBuilder() {
			super(new DefaultPropertyInputGroup());
		}

		@Override
		protected PropertyInputGroupBuilder builder() {
			return this;
		}

		@Override
		public PropertyInputGroup build() {
			instance.build();
			return instance;
		}

	}

	/**
	 * Abstract {@link Builder} implementation.
	 * @param  Actual {@link PropertyInputGroup} type
	 * @param  Concrete builder type
	 */
	public abstract static class AbstractBuilder>
			implements Builder {

		/**
		 * Instance to build
		 */
		protected final C instance;

		/**
		 * Constructor
		 * @param instance Instance to build
		 */
		public AbstractBuilder(C instance) {
			super();
			this.instance = instance;
		}

		/**
		 * Actual builder
		 * @return Builder
		 */
		protected abstract B builder();

		/*
		 * (non-Javadoc)
		 * @see
		 * com.holonplatform.vaadin.components.PropertyInputGroup.Builder#properties(com.holonplatform.core.property.
		 * Property[])
		 */
		@SuppressWarnings({ "rawtypes", "unchecked" })
		@Override
		public 

B properties(P... properties) { if (properties != null) { for (P property : properties) { instance.addProperty(property); } } return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#properties(java.lang.Iterable) */ @SuppressWarnings("rawtypes") @Override public

B properties(Iterable

properties) { ObjectUtils.argumentNotNull(properties, "Properties must be not null"); for (P property : properties) { instance.addProperty(property); } return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#readOnly(com.holonplatform.core.property. * Property) */ @Override public B readOnly(Property property) { ObjectUtils.argumentNotNull(property, "Property must be not null"); instance.getPropertyConfiguration(property).setReadOnly(true); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#required(com.holonplatform.core.property. * Property) */ @Override public B required(Property property) { ObjectUtils.argumentNotNull(property, "Property must be not null"); instance.getPropertyConfiguration(property).setRequired(true); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#required(com.holonplatform.core.property. * Property, com.holonplatform.core.Validator) */ @Override public B required(Property property, Validator validator) { ObjectUtils.argumentNotNull(property, "Property must be not null"); instance.getPropertyConfiguration(property).setRequired(true); instance.getPropertyConfiguration(property).setRequiredValidator(validator); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#required(com.holonplatform.core.property. * Property, com.holonplatform.core.i18n.Localizable) */ @Override public B required(Property property, Localizable message) { ObjectUtils.argumentNotNull(property, "Property must be not null"); instance.getPropertyConfiguration(property).setRequired(true); instance.getPropertyConfiguration(property).setRequiredMessage(message); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#hidden(com.holonplatform.core.property. * Property) */ @Override public B hidden(Property property) { ObjectUtils.argumentNotNull(property, "Property must be not null"); instance.getPropertyConfiguration(property).setHidden(true); return builder(); } /* * (non-Javadoc) * @see * com.holonplatform.vaadin.components.PropertyInputGroup.Builder#bind(com.holonplatform.core.property.Property, * com.holonplatform.core.property.PropertyRenderer) */ @Override public B bind(Property property, PropertyRenderer, T> renderer) { ObjectUtils.argumentNotNull(property, "Property must be not null"); ObjectUtils.argumentNotNull(renderer, "Renderer must be not null"); instance.getPropertyConfiguration(property).setRenderer(renderer); return builder(); } /* * (non-Javadoc) * @see * com.holonplatform.vaadin.components.PropertyInputGroup.Builder#defaultValue(com.holonplatform.core.property * .Property, com.holonplatform.vaadin.components.PropertyInputGroup.DefaultValueProvider) */ @Override public B defaultValue(Property property, DefaultValueProvider defaultValueProvider) { ObjectUtils.argumentNotNull(property, "Property must be not null"); ObjectUtils.argumentNotNull(defaultValueProvider, "DefaultValueProvider must be not null"); instance.getPropertyConfiguration(property).setDefaultValueProvider(defaultValueProvider); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#withValidator(com.holonplatform.core. * property.Property, com.holonplatform.core.Validator) */ @Override public B withValidator(Property property, Validator validator) { ObjectUtils.argumentNotNull(property, "Property must be not null"); ObjectUtils.argumentNotNull(validator, "Validator must be not null"); instance.getPropertyConfiguration(property).addValidator(validator); return builder(); } /* * (non-Javadoc) * @see * com.holonplatform.vaadin.components.PropertyInputGroup.Builder#validationStatusHandler(com.holonplatform.core * .property.Property, com.holonplatform.vaadin.components.ValidationStatusHandler) */ @Override public B validationStatusHandler(Property property, ValidationStatusHandler validationStatusHandler) { ObjectUtils.argumentNotNull(property, "Property must be not null"); ObjectUtils.argumentNotNull(validationStatusHandler, "ValidationStatusHandler must be not null"); instance.getPropertyConfiguration(property).setPropertyValidationStatusHandler(validationStatusHandler); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#propertiesValidationStatusHandler(com. * holonplatform.vaadin.components.ValidationStatusHandler) */ @Override public B propertiesValidationStatusHandler(ValidationStatusHandler validationStatusHandler) { ObjectUtils.argumentNotNull(validationStatusHandler, "ValidationStatusHandler must be not null"); instance.setPropertiesValidationStatusHandler(validationStatusHandler); return builder(); } /* * (non-Javadoc) * @see * com.holonplatform.vaadin.components.PropertyInputGroup.Builder#validationStatusHandler(com.holonplatform. * vaadin.components.ValidationStatusHandler) */ @Override public B validationStatusHandler(ValidationStatusHandler validationStatusHandler) { instance.setValidationStatusHandler(validationStatusHandler); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#withValidator(com.holonplatform.core. * Validator) */ @Override public B withValidator(Validator validator) { instance.addValidator(validator); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#validateOnValueChange(boolean) */ @Override public B validateOnValueChange(boolean validateOnValueChange) { instance.setValidateOnValueChange(validateOnValueChange); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#ignorePropertyValidation() */ @Override public B ignorePropertyValidation() { instance.setIgnorePropertyValidation(true); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#stopValidationAtFirstFailure(boolean) */ @Override public B stopValidationAtFirstFailure(boolean stopValidationAtFirstFailure) { instance.setStopValidationAtFirstFailure(stopValidationAtFirstFailure); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#setStopOverallValidationAtFirstFailure( * boolean) */ @Override public B stopOverallValidationAtFirstFailure(boolean stopOverallValidationAtFirstFailure) { instance.setStopOverallValidationAtFirstFailure(stopOverallValidationAtFirstFailure); return builder(); } /* * (non-Javadoc) * @see com.holonplatform.vaadin.components.PropertyInputGroup.Builder#ignoreMissingInputs(boolean) */ @Override public B ignoreMissingInputs(boolean ignoreMissingInputs) { instance.setIgnoreMissingInputs(ignoreMissingInputs); return builder(); } /* * (non-Javadoc) * @see * com.holonplatform.vaadin.components.PropertyInputGroup.Builder#withPostProcessor(com.holonplatform.vaadin. * components.PropertyBinding.PostProcessor) */ @Override public B withPostProcessor(PostProcessor> postProcessor) { ObjectUtils.argumentNotNull(postProcessor, "PostProcessor must be not null"); instance.addInputPostProcessor(postProcessor); return builder(); } } // Internal private class PropertyConfiguration { private final Property property; private boolean hidden; private boolean required; private boolean readOnly; private PropertyRenderer, T> renderer; private DefaultValueProvider defaultValueProvider; private List> propertyValidators; private Validator requiredValidator; private Localizable requiredMessage; private ValidationStatusHandler propertyValidationStatusHandler; private Input input; /** * Constructor * @param property The property (not null) */ public PropertyConfiguration(Property property) { super(); ObjectUtils.argumentNotNull(property, "Property must be not null"); this.property = property; } /** * Get the property * @return the property */ public Property getProperty() { return property; } /** * Get whether the property is hidden. * @return true if the property is hidden */ public boolean isHidden() { return hidden; } /** * Set the property as hidden. * @param hidden true to set the property as hidden */ public void setHidden(boolean hidden) { this.hidden = hidden; } /** * Get whether the property is required. * @return true if the property is required */ public boolean isRequired() { return required; } /** * Set the property as required. * @param required true to set the property as required */ public void setRequired(boolean required) { this.required = required; } /** * Get the {@link Validator} to use to check a required property value. * @return Optional {@link Validator} to use to check a required property valu */ public Optional> getRequiredValidator() { return Optional.ofNullable(requiredValidator); } /** * Set the {@link Validator} to use to check a required property value. * @param requiredValidator the required {@link Validator} to set */ public void setRequiredValidator(Validator requiredValidator) { this.requiredValidator = requiredValidator; } /** * Get the required validation message to show when using the default required property validator. * @return the optional required validation message */ public Optional getRequiredMessage() { return Optional.ofNullable(requiredMessage); } /** * Set the required validation message to show when using the default required property validator. * @param requiredMessage the required validation message to set */ public void setRequiredMessage(Localizable requiredMessage) { this.requiredMessage = requiredMessage; } /** * Get whether the property is read-only. * @return true if the property is read-only */ public boolean isReadOnly() { return property.isReadOnly() || readOnly; } /** * Set the property as read-only. * @param readOnly true to set the property as read-only */ public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } /** * Get the {@link PropertyRenderer} to use to create the bound {@link Input}. * @return the {@link PropertyRenderer} to use to create the bound {@link Input} */ public Optional, T>> getRenderer() { return Optional.ofNullable(renderer); } /** * Set the {@link PropertyRenderer} to use to create the bound {@link Input}. * @param renderer the {@link PropertyRenderer} to set */ public void setRenderer(PropertyRenderer, T> renderer) { this.renderer = renderer; } /** * Get the property default value provider. * @return Optional property default value provider */ public Optional> getDefaultValueProvider() { return Optional.ofNullable(defaultValueProvider); } /** * Set the property default value provider. * @param defaultValueProvider the {@link DefaultValueProvider} to set */ public void setDefaultValueProvider(DefaultValueProvider defaultValueProvider) { this.defaultValueProvider = defaultValueProvider; } /** * Get the registered property {@link Validator}s. * @return the property validators, empty if none */ public List> getValidators() { return (propertyValidators != null) ? propertyValidators : Collections.emptyList(); } /** * Add a property {@link Validator}s. * @param validator the {@link Validator} to add */ public void addValidator(Validator validator) { if (validator != null) { if (this.propertyValidators == null) { this.propertyValidators = new LinkedList<>(); } this.propertyValidators.add(validator); } } /** * Add a property {@link Validator}s as first. * @param validator the {@link Validator} to add */ public void addValidatorAsFirst(Validator validator) { if (validator != null) { if (this.propertyValidators == null) { this.propertyValidators = new LinkedList<>(); } this.propertyValidators.add(0, validator); } } /** * Get the property {@link ValidationStatusHandler}. * @return Optional property {@link ValidationStatusHandler} */ public Optional getPropertyValidationStatusHandler() { return Optional.ofNullable(propertyValidationStatusHandler); } /** * Set the property {@link ValidationStatusHandler}. * @param propertyValidationStatusHandler the property {@link ValidationStatusHandler} to set */ public void setPropertyValidationStatusHandler(ValidationStatusHandler propertyValidationStatusHandler) { this.propertyValidationStatusHandler = propertyValidationStatusHandler; } /** * Get the {@link Input} bound to the property, if available. * @return the optional property input */ public Optional> getInput() { return Optional.ofNullable(input); } /** * Set the {@link Input} bound to the property. * @param input the {@link Input} to set */ public void setInput(Input input) { this.input = input; } } }