This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git
The following commit(s) were added to refs/heads/master by this push:
new 7bcf0dc ISIS-2340: vaa: use converters when binding UI to backend
7bcf0dc is described below
commit 7bcf0dcf128f5817e2c3bb0eb9baa8ba39e98d70
Author: Andi Huber <[email protected]>
AuthorDate: Fri Aug 28 12:06:54 2020 +0200
ISIS-2340: vaa: use converters when binding UI to backend
---
.../interactions/managed/ManagedMember.java | 3 -
.../interactions/managed/ManagedParameter.java | 36 +++
.../viewer/vaadin/ui/binding/BindingsVaa.java | 348 +++++++++++----------
.../components/temporal/TemporalFieldFactory.java | 18 +-
.../ui/components/text/uuid/UuidFieldFactory.java | 32 +-
5 files changed, 239 insertions(+), 198 deletions(-)
diff --git
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedMember.java
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedMember.java
index 44402a6..74fdbc7 100644
---
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedMember.java
+++
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedMember.java
@@ -141,7 +141,6 @@ public abstract class ManagedMember implements
ManagedFeature {
/**
- * @param where
* @return non-empty if hidden
*/
public Optional<InteractionVeto> checkVisibility() {
@@ -164,7 +163,6 @@ public abstract class ManagedMember implements
ManagedFeature {
}
/**
- * @param where
* @return non-empty if not usable/editable (meaning if read-only)
*/
public Optional<InteractionVeto> checkUsability() {
@@ -187,7 +185,6 @@ public abstract class ManagedMember implements
ManagedFeature {
}
-
}
protected static <T extends ObjectMember> Optional<T> lookup(
diff --git
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedParameter.java
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedParameter.java
index 379fdc6..c5f8393 100644
---
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedParameter.java
+++
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/ManagedParameter.java
@@ -18,12 +18,48 @@
*/
package org.apache.isis.core.metamodel.interactions.managed;
+import java.util.Optional;
+
+import org.apache.isis.core.commons.collections.Can;
+import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.isis.core.metamodel.consent.Veto;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;
+import lombok.NonNull;
+import lombok.val;
+
public interface ManagedParameter extends ManagedValue, ManagedFeature {
int getParamNr();
ObjectActionParameter getMetaModel();
ParameterNegotiationModel getNegotiationModel();
+ /**
+ * @param params
+ * @return non-empty if not usable/editable (meaning if read-only)
+ */
+ default Optional<InteractionVeto> checkUsability(final @NonNull
Can<ManagedObject> params) {
+
+ try {
+ val head = getNegotiationModel().getHead();
+
+ val usabilityConsent =
+ getMetaModel()
+ .isUsable(head, params, InteractionInitiatedBy.USER);
+
+ return usabilityConsent.isVetoed()
+ ? Optional.of(InteractionVeto.readonly(usabilityConsent))
+ : Optional.empty();
+
+ } catch (final Exception ex) {
+
+ return Optional.of(InteractionVeto
+ .readonly(
+ new Veto(ex.getLocalizedMessage())));
+
+ }
+
+ }
+
}
diff --git
a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/binding/BindingsVaa.java
b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/binding/BindingsVaa.java
index 5e58354..c3c1674 100644
---
a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/binding/BindingsVaa.java
+++
b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/binding/BindingsVaa.java
@@ -18,20 +18,16 @@
*/
package org.apache.isis.incubator.viewer.vaadin.ui.binding;
-import java.time.LocalDate;
-import java.util.Objects;
-import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.Nullable;
import com.vaadin.flow.component.HasValidation;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.Binder.BindingBuilder;
-import com.vaadin.flow.data.binder.Result;
import com.vaadin.flow.data.binder.Setter;
-import com.vaadin.flow.data.binder.ValueContext;
import com.vaadin.flow.data.converter.Converter;
-import com.vaadin.flow.data.converter.DateToSqlDateConverter;
-import com.vaadin.flow.data.converter.LocalDateToDateConverter;
import com.vaadin.flow.function.ValueProvider;
import org.apache.isis.core.commons.binding.Bindable;
@@ -39,13 +35,11 @@ import org.apache.isis.core.commons.binding.Observable;
import org.apache.isis.core.commons.internal.base._Casts;
import org.apache.isis.core.commons.internal.base._Strings;
import org.apache.isis.core.commons.internal.exceptions._Exceptions;
-import org.apache.isis.core.metamodel.interactions.managed.InteractionVeto;
import org.apache.isis.core.metamodel.interactions.managed.ManagedFeature;
import org.apache.isis.core.metamodel.interactions.managed.ManagedParameter;
import org.apache.isis.core.metamodel.interactions.managed.ManagedProperty;
import org.apache.isis.core.metamodel.spec.ManagedObject;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
-import
org.apache.isis.viewer.common.model.components.UiComponentFactory.ComponentRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@@ -54,71 +48,175 @@ import lombok.experimental.UtilityClass;
@UtilityClass
public final class BindingsVaa {
+
+ // -- UNIDIRECTIONAL
/**
* Binds the uiField's (rendered) value to an {@link Observable}.
- * @param <V>
+ * @param <V> field/model/presentation value type
* @param uiField
- * @param value
+ * @param value - observable (backend)
+ * @param customizer - to customize the binding builder (ignored if null)
*/
public static <V> void bindValue(
final @NonNull HasValue<?, V> uiField,
- final @NonNull Observable<ManagedObject> value) {
+ final @NonNull Observable<ManagedObject> value,
+ @Nullable UnaryOperator<BindingBuilder<Observable<ManagedObject>,
V>> customizer) {
+
uiField.setReadOnly(true);
val binder = new Binder<Observable<ManagedObject>>();
val internalBinding = InternalUnidirBinding.<V>of();
- binder.forField(uiField)
+ if(customizer==null) {
+ customizer = UnaryOperator.identity();
+ }
+
+ customizer.apply(binder.forField(uiField))
.bind(
internalBinding,
- null
- );
+ null);
binder.setBean(value);
//TODO supposed to account for changes originating from backend side
//need to check whether this is possible with Vaadin
value.addListener((e, oldValue, newValue)->{
- uiField.setValue(_Casts.uncheckedCast(newValue.getPojo()));
+ uiField.setValue(_Casts.<V>uncheckedCast(newValue.getPojo()));
+ });
+
+ }
+
+ /**
+ * Binds the uiField's (rendered) value to an {@link Observable}.
+ * @param <P> field/presentation value type
+ * @param <M> model value type
+ * @param uiField
+ * @param value - observable (backend)
+ * @param converter - converts between model and presentation
+ * @param customizer - to customize the binding builder (ignored if null)
+ */
+ public static <P, M> void bindValue(
+ final @NonNull HasValue<?, P> uiField,
+ final @NonNull Observable<ManagedObject> value,
+ final @NonNull Converter<P, M> converter,
+ @Nullable UnaryOperator<BindingBuilder<Observable<ManagedObject>,
M>> customizer) {
+
+ uiField.setReadOnly(true);
+ val binder = new Binder<Observable<ManagedObject>>();
+ val internalBinding = InternalUnidirBinding.<M>of();
+
+ if(customizer==null) {
+ customizer = UnaryOperator.identity();
+ }
+
+ customizer.apply(
+ binder.forField(uiField)
+ .withConverter(converter))
+ .bind(
+ internalBinding,
+ null);
+
+ binder.setBean(value);
+
+ //TODO supposed to account for changes originating from backend side
+ //need to check whether this is possible with Vaadin
+ value.addListener((e, oldValue, newValue)->{
+ val newModelValue = _Casts.<M>uncheckedCast(newValue.getPojo());
+ P newFieldValue = converter.convertToPresentation(newModelValue,
null);
+ uiField.setValue(newFieldValue);
});
}
+
+ // -- BIDIRECTIONAL
/**
* Binds the uiField's (rendered) value to a {@link Bindable}.
- * @param <V>
+ * @param <V> field/model value type
* @param uiField
* @param value
+ * @param valueSpec
+ * @param customizer - to customize the binding builder (ignored if null)
*/
public static <V> void bindValueBidirectional(
final @NonNull HasValue<?, V> uiField,
final @NonNull Bindable<ManagedObject> value,
- final @NonNull ObjectSpecification valueSpec) {
+ final @NonNull ObjectSpecification valueSpec,
+ @Nullable UnaryOperator<BindingBuilder<Bindable<ManagedObject>,
V>> customizer) {
uiField.setReadOnly(false);
val binder = new Binder<Bindable<ManagedObject>>();
val internalBinding = InternalBidirBinding.<V>of(valueSpec);
+
+ if(customizer==null) {
+ customizer = UnaryOperator.identity();
+ }
- binder.forField(uiField)
+ customizer.apply(binder.forField(uiField))
.bind(
internalBinding::apply,
- internalBinding::accept
- );
+ internalBinding::accept);
binder.setBean(value);
//TODO supposed to account for changes originating from backend side
- //need to check whether this is possible with Vaadin
+ // not sure whether this works
value.addListener((e, oldValue, newValue)->{
uiField.setValue(_Casts.uncheckedCast(newValue.getPojo()));
});
}
+
+ /**
+ * Binds the uiField's (rendered) value to a {@link Bindable}.
+ * @param <P> field/presentation value type
+ * @param <M> model value type
+ * @param uiField
+ * @param value
+ * @param valueSpec
+ * @param converter - converts between model and presentation
+ * @param nullRepresentation - the model value to use instead of null
+ * @param customizer - to customize the binding builder (ignored if null)
+ */
+ public static <P, M> void bindValueBidirectional(
+ final @NonNull HasValue<?, P> uiField,
+ final @NonNull Bindable<ManagedObject> value,
+ final @NonNull ObjectSpecification valueSpec,
+ final @NonNull Converter<P, M> converter,
+ @Nullable UnaryOperator<BindingBuilder<Bindable<ManagedObject>,
M>> customizer) {
+
+ uiField.setReadOnly(false);
+ val binder = new Binder<Bindable<ManagedObject>>();
+ val internalBinding = InternalBidirBinding.<M>of(valueSpec);
+
+ if(customizer==null) {
+ customizer = UnaryOperator.identity();
+ }
+
+ customizer.apply(
+ binder.forField(uiField)
+ .withConverter(converter))
+ .bind(
+ internalBinding::apply,
+ internalBinding::accept);
+
+ binder.setBean(value);
+
+ //TODO supposed to account for changes originating from backend side
+ // not sure whether this works
+ value.addListener((e, oldValue, newValue)->{
+ val newModelValue = _Casts.<M>uncheckedCast(newValue.getPojo());
+ P newFieldValue = converter.convertToPresentation(newModelValue,
null);
+ uiField.setValue(newFieldValue);
+ });
+
+ }
+
+ // -- VALIDATION
/**
* Binds the uiField's (rendered) validation feedback to an {@link
Observable}.
- * @param <F>
* @param uiField
* @param validationFeedbackMessage
*/
@@ -126,26 +224,73 @@ public final class BindingsVaa {
final @NonNull HasValidation uiField,
final @NonNull Observable<String> validationFeedbackMessage) {
+ //TODO supposed to account for changes originating from backend side
+ // not sure whether this works
validationFeedbackMessage.addListener((e, oldValue, newValue)->{
uiField.setErrorMessage(newValue);
uiField.setInvalid(_Strings.isNotEmpty(newValue));
});
+
+ val initialValue = validationFeedbackMessage.getValue();
+ uiField.setErrorMessage(initialValue);
+ uiField.setInvalid(_Strings.isNotEmpty(initialValue));
+
}
+
+ // -- FEATURE (PARAMETER OR PROPERTY)
+ public static <P, F extends HasValue<?, P> & HasValidation>
+ void bindFeature(
+ final @NonNull F uiField,
+ final @NonNull ManagedFeature managedFeature) {
+ bindFeatureWithConverter(uiField, managedFeature, null, null);
+ }
+
- public static <V, F extends HasValue<?, V> & HasValidation>
- void bindFeature(F uiField, ManagedFeature managedFeature) {
+ /**
+ * @param <P> field/presentation value type
+ * @param <M> model value type
+ * @param uiField
+ * @param managedFeature
+ * @param converter - ignored if {@code null}, converts between model and
presentation
+ * @param nullRepresentation - (TODO remove) ignored if converter is
{@code null}
+ */
+ public static <M, P, F extends HasValue<?, P> & HasValidation>
+ void bindFeatureWithConverter(
+ final @NonNull F uiField,
+ final @NonNull ManagedFeature managedFeature,
+ final @Nullable Converter<P, M> converter,
+ final @Nullable M nullRepresentation) { // TODO remove, yet poorly
designed
val valueSpec = managedFeature.getSpecification();
if(managedFeature instanceof ManagedParameter) {
val managedParameter = (ManagedParameter)managedFeature;
- val isReadOnly = false; // TODO also handle case when parameters
are readonly
- uiField.setReadOnly(isReadOnly);
+
+ //TODO need a more advanced mechanism here:
+ // whether readonly or r/w depends (dynamically) on the state of
the
+ // Parameter Negotiation Model
+ val isReadOnly = managedParameter
+
.checkUsability(managedParameter.getNegotiationModel().getParamValues())
+ .isPresent();
+
+ if(isReadOnly) {
+ // readonly binding
+ if(converter!=null) {
+ bindValue(uiField, managedParameter.getValue(), converter,
bb->bb.withNullRepresentation(nullRepresentation));
+ } else {
+ bindValue(uiField, managedParameter.getValue(), null);
+ }
- // r/w binding
- bindValueBidirectional(uiField, managedParameter.getValue(),
valueSpec);
+ } else {
+ // r/w binding
+ if(converter!=null) {
+ bindValueBidirectional(uiField,
managedParameter.getValue(), valueSpec, converter,
bb->bb.withNullRepresentation(nullRepresentation));
+ } else {
+ bindValueBidirectional(uiField,
managedParameter.getValue(), valueSpec, null);
+ }
+ }
// bind parameter validation feedback
bindValidationFeedback(uiField,
managedParameter.getValidationMessage());
@@ -154,18 +299,22 @@ public final class BindingsVaa {
val managedProperty = (ManagedProperty)managedFeature;
val isReadOnly = managedProperty.checkUsability().isPresent();
- uiField.setReadOnly(isReadOnly);
if(isReadOnly) {
// readonly binding
- bindValue(uiField, managedProperty.getValue());
-
+ if(converter!=null) {
+ bindValue(uiField, managedProperty.getValue(), converter,
bb->bb.withNullRepresentation(nullRepresentation));
+ } else {
+ bindValue(uiField, managedProperty.getValue(), null);
+ }
} else {
-
//TODO allow property (inline) editing
- //using readonly as fallback for now
- uiField.setReadOnly(true);
- bindValue(uiField, managedProperty.getValue());
+ //render readonly for now (could use fallback dialog as an
intermediate step)
+ if(converter!=null) {
+ bindValue(uiField, managedProperty.getValue(), converter,
bb->bb.withNullRepresentation(nullRepresentation));
+ } else {
+ bindValue(uiField, managedProperty.getValue(), null);
+ }
}
} else {
@@ -174,133 +323,6 @@ public final class BindingsVaa {
}
- @Deprecated
- public static <P, M> Binder<ComponentRequest> requestBinder(
- final HasValue<?, P> uiField,
- final Class<M> modelValueType,
- final Function<BindingBuilder<ComponentRequest, P>,
BindingBuilder<ComponentRequest, M>> chain) {
-
- val binder = new Binder<ComponentRequest>();
- val propagator = new Propagator<M>(modelValueType);
-
- chain.apply(binder.forField(uiField))
- .withConverter(propagator)
- .bind(
- propagator::init,
- propagator::propagate
- );
- return binder;
- }
-
- /**
- *
- * @param <P> presentation type (field value type)
- * @param uiField
- * @param fieldValueType
- * @param nullRepresentation
- * @return
- */
- @Deprecated
- public static <P> Binder<ComponentRequest> requestBinder(
- final HasValue<?, P> uiField,
- final Class<P> fieldValueType) {
-
- val binder = new Binder<ComponentRequest>();
- val propagator = new Propagator<P>(fieldValueType);
-
- binder.forField(uiField)
- .withConverter(propagator)
- .bind(
- propagator::init,
- propagator::propagate
- );
- return binder;
- }
-
- /**
- *
- * @param <P> presentation type (field value type)
- * @param <M> model value type
- * @param uiField
- * @param modelValueType
- * @param converter
- * @return
- */
- @Deprecated
- public static <P, M> Binder<ComponentRequest> requestBinderWithConverter(
- final HasValue<?, P> uiField,
- final Class<M> modelValueType,
- final Converter<P, M> converter) {
-
- return requestBinder(uiField, modelValueType,
- binder->binder.withConverter(converter));
- }
-
- @RequiredArgsConstructor
- private static class Propagator<P> implements Converter<P, P> {
-
- private static final long serialVersionUID = 1L;
-
- private final Class<P> pojoType;
- private ComponentRequest request;
-
- @Override
- public Result<P> convertToModel(P newValue, ValueContext context) {
-
- // propagate new value down the domain model, and handle
validation feedback
-
- val validationMessage = request.setFeatureValue(newValue)
- .map(InteractionVeto::getReason)
- .orElse(null);
-
- return validationMessage==null
- ? Result.ok(newValue)
- : Result.error(validationMessage);
- }
-
- @Override
- public P convertToPresentation(P value, ValueContext context) {
- return value; // identity function
- }
-
- public P init(ComponentRequest request) {
- this.request = request;
- return request.getFeatureValue(pojoType).orElse(null);
- }
-
- public P propagate(ComponentRequest request, P newValue) {
- return newValue; // identity function
- }
-
- }
-
-
- // -- SHORTCUTS
-
- public static enum DateBinder {
-
- JAVA_TIME_LOCAL_DATE{
-
- @Override
- public Binder<ComponentRequest> bind(HasValue<?, LocalDate>
uiField) {
- return BindingsVaa.requestBinder(uiField, LocalDate.class);
- }
-
- },
- JAVA_SQL_DATE{
-
- @Override
- public Binder<ComponentRequest> bind(HasValue<?, LocalDate>
uiField) {
- return BindingsVaa.requestBinderWithConverter(uiField,
java.sql.Date.class,
- new LocalDateToDateConverter().chain(new
DateToSqlDateConverter()));
- }
-
- };
-
- public abstract Binder<ComponentRequest> bind(final HasValue<?,
LocalDate> uiField);
-
- }
-
// -- HELPER
@RequiredArgsConstructor(staticName = "of")
diff --git
a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/temporal/TemporalFieldFactory.java
b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/temporal/TemporalFieldFactory.java
index 1084526..85ab8bf 100644
---
a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/temporal/TemporalFieldFactory.java
+++
b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/temporal/TemporalFieldFactory.java
@@ -21,7 +21,8 @@ package
org.apache.isis.incubator.viewer.vaadin.ui.components.temporal;
import java.time.LocalDate;
import com.vaadin.flow.component.Component;
-import com.vaadin.flow.data.binder.Binder;
+import com.vaadin.flow.data.converter.DateToSqlDateConverter;
+import com.vaadin.flow.data.converter.LocalDateToDateConverter;
import org.springframework.core.annotation.Order;
@@ -61,21 +62,22 @@ public class TemporalFieldFactory implements
UiComponentHandlerVaa {
case DATE_ONLY:{
val uiField = new DateField(request.getDisplayLabel());
+ val managedFeature = request.getManagedFeature();
- final Binder<ComponentRequest> binder;
if(request.isFeatureTypeEqualTo(LocalDate.class)) {
- binder =
BindingsVaa.DateBinder.JAVA_TIME_LOCAL_DATE.bind(uiField);
+ BindingsVaa.bindFeature(uiField, managedFeature);
+
} else if(request.isFeatureTypeEqualTo(java.sql.Date.class)) {
- binder = BindingsVaa.DateBinder.JAVA_SQL_DATE.bind(uiField);
+
+ val converter = new LocalDateToDateConverter().chain(new
DateToSqlDateConverter());
+
+ BindingsVaa.bindFeatureWithConverter(uiField, managedFeature,
converter, null);
+
} else {
throw _Exceptions.unmatchedCase(request.getFeatureType());
}
- binder.setBean(request);
return uiField;
-
- //val uiField = DateField.bind(request);
-
}
case TIME_ONLY:{
diff --git
a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/text/uuid/UuidFieldFactory.java
b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/text/uuid/UuidFieldFactory.java
index 734122c..25bec6b 100644
---
a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/text/uuid/UuidFieldFactory.java
+++
b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/components/text/uuid/UuidFieldFactory.java
@@ -22,9 +22,7 @@ import java.util.UUID;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.textfield.TextField;
-import com.vaadin.flow.data.binder.Result;
-import com.vaadin.flow.data.binder.ValueContext;
-import com.vaadin.flow.data.converter.Converter;
+import com.vaadin.flow.data.converter.StringToUuidConverter;
import org.springframework.core.annotation.Order;
@@ -49,29 +47,15 @@ public class UuidFieldFactory implements
UiComponentHandlerVaa {
val uiField = new TextField(request.getDisplayLabel());
- val binder = BindingsVaa.requestBinderWithConverter(uiField,
UUID.class, new StringToUuidConverter());
- binder.setBean(request);
+ val managedFeature = request.getManagedFeature();
+ BindingsVaa.bindFeatureWithConverter(
+ uiField,
+ managedFeature,
+ new StringToUuidConverter("Unable to convert String to UUID"),
+ UUID.nameUUIDFromBytes(new byte[16]));
+
return uiField;
- }
-
- private static class StringToUuidConverter implements Converter<String,
UUID> {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public Result<UUID> convertToModel(String value, ValueContext context)
{
- try {
- return Result.ok(UUID.fromString(value));
- } catch (IllegalArgumentException e) {
- return Result.error(e.getMessage());
- }
- }
-
- @Override
- public String convertToPresentation(UUID value, ValueContext context) {
- return value.toString();
- }
}