You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2021/11/02 13:47:37 UTC

[isis] branch master updated: ISIS-2741: intermediate - work on MaximumFractionDigits to be provided via meta meta-model

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 7325189  ISIS-2741: intermediate - work on MaximumFractionDigits to be provided via meta meta-model
7325189 is described below

commit 7325189d518b9282a6572ff75948bb45c0425bc2
Author: andi-huber <ah...@apache.org>
AuthorDate: Tue Nov 2 14:47:19 2021 +0100

    ISIS-2741: intermediate - work on MaximumFractionDigits to be provided
    via meta meta-model
    
    - not sure yet how to do this consistently
---
 .../applib/adapters/ValueSemanticsAbstract.java    |   4 +
 .../applib/adapters/ValueSemanticsProvider.java    |  10 ++
 .../_testing/InteractionService_forTesting.java    |  83 ++++++---
 .../scalars/ConverterBasedOnValueSemantics.java    |   6 +-
 .../jdkmath/BigDecimalConverter_roundtrip.java     | 195 +++++++++++++++++++++
 5 files changed, 267 insertions(+), 31 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsAbstract.java b/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsAbstract.java
index 22f1c96..fe09cc8 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsAbstract.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsAbstract.java
@@ -121,6 +121,9 @@ implements ValueSemanticsProvider<T> {
         }
         val format = getNumberFormat(context);
         format.setParseBigDecimal(true);
+        if(context.getMaximumFractionDigits()>-1) {
+            format.setMaximumFractionDigits(context.getMaximumFractionDigits());
+        }
         val position = new ParsePosition(0);
 
         try {
@@ -132,6 +135,7 @@ implements ValueSemanticsProvider<T> {
             }
             return number;
         } catch (final NumberFormatException | ParseException e) {
+            System.err.printf("suppressed message %s%n", e.getMessage());
             throw new TextEntryParseException("Not a decimal value " + input, e);
         }
 
diff --git a/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsProvider.java b/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsProvider.java
index f62d12b..955a65e 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsProvider.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/adapters/ValueSemanticsProvider.java
@@ -50,8 +50,18 @@ public interface ValueSemanticsProvider<T> {
 
     @lombok.Value(staticConstructor = "of")
     class Context {
+
         Identifier identifier;
         InteractionContext interactionContext;
+        /**
+         * Optional. Used for for decimal values, when a fixed amount of fractional digits is specified, otherwise {@code -1}
+         */
+        public int getMaximumFractionDigits() {
+            // FIXME[ISIS-2741] provide some means to recover the meta-model member from feature-id,
+            // so we can evaluate facets that provide the MaximumFractionDigits
+            // - as an alternative move this to the Parser<T>
+            return -1;
+        }
     }
 
     Class<T> getCorrespondingClass();
diff --git a/core/security/src/main/java/org/apache/isis/core/security/_testing/InteractionService_forTesting.java b/core/security/src/main/java/org/apache/isis/core/security/_testing/InteractionService_forTesting.java
index 8210088..c9b1d93 100644
--- a/core/security/src/main/java/org/apache/isis/core/security/_testing/InteractionService_forTesting.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/_testing/InteractionService_forTesting.java
@@ -19,6 +19,7 @@
 package org.apache.isis.core.security._testing;
 
 import java.util.Optional;
+import java.util.Stack;
 import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.function.Function;
@@ -33,6 +34,7 @@ import org.apache.isis.applib.services.user.UserMemento;
 import org.apache.isis.commons.functional.ThrowingRunnable;
 
 import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 
 /**
@@ -42,39 +44,29 @@ import lombok.SneakyThrows;
 public class InteractionService_forTesting
 implements InteractionService {
 
-    private InteractionLayer interactionLayer = null;
+    private Stack<InteractionLayer> interactionLayers = new Stack<>();
 
     @Override
     public InteractionLayer openInteraction() {
         final UserMemento userMemento = UserMemento.system();
-        final UUID uuid = UUID.randomUUID();
-        final Interaction interaction = new Interaction() {
-            @Override public <T> T putAttribute(Class<? super T> type, T value) { return null; }
-            @Override public <T> T computeAttributeIfAbsent(Class<? super T> type, Function<Class<?>, ? extends T> mappingFunction) { return null; }
-            @Override public <T> T getAttribute(Class<T> type) { return null; }
-            @Override public void removeAttribute(Class<?> type) { }
-            @Override public UUID getInteractionId() { return uuid; }
-            @Override public Command getCommand() { return null; }
-            @Override public Execution<?, ?> getCurrentExecution() { return null; }
-            @Override public Execution<?, ?> getPriorExecution() { return null; }
-        };
-        interactionLayer = new InteractionLayer(interaction, InteractionContext.ofUserWithSystemDefaults(userMemento));
-        return null;
+        return openInteraction(InteractionContext.ofUserWithSystemDefaults(userMemento));
     }
 
     @Override
-    public InteractionLayer openInteraction(@NonNull InteractionContext interactionContext) {
-        return openInteraction();
+    public InteractionLayer openInteraction(@NonNull final InteractionContext interactionContext) {
+        final Interaction interaction = new Interaction_forTesting();
+        return interactionLayers.push(
+                new InteractionLayer(interaction, interactionContext));
     }
 
     @Override
     public void closeInteractionLayers() {
-        interactionLayer = null;
+        interactionLayers.clear();
     }
 
     @Override
     public boolean isInInteraction() {
-        return interactionLayer != null;
+        return interactionLayers.size()>0;
     }
 
     @Override public Optional<UUID> getInteractionId() {
@@ -84,32 +76,67 @@ implements InteractionService {
     }
 
     @Override public Optional<InteractionLayer> currentInteractionLayer() {
-        return Optional.ofNullable(this.interactionLayer);
+        return interactionLayers.isEmpty()
+                ? Optional.empty()
+                : Optional.of(interactionLayers.peek());
     }
 
     @Override public int getInteractionLayerCount() {
-        return isInInteraction() ? 1 : 0;
+        return interactionLayers.size();
     }
 
     @Override @SneakyThrows
-    public <R> R call(@NonNull InteractionContext interactionContext, @NonNull Callable<R> callable) {
-        return callable.call();
+    public <R> R call(@NonNull final InteractionContext interactionContext, @NonNull final Callable<R> callable) {
+        try {
+            openInteraction(interactionContext);
+            return callable.call();
+        } finally {
+            interactionLayers.pop();
+        }
     }
 
     @Override @SneakyThrows
-    public void run(@NonNull InteractionContext interactionContext, @NonNull ThrowingRunnable runnable) {
-        runnable.run();
+    public void run(@NonNull final InteractionContext interactionContext, @NonNull final ThrowingRunnable runnable) {
+        try {
+            openInteraction(interactionContext);
+            runnable.run();
+        } finally {
+            interactionLayers.pop();
+        }
     }
 
 
     @Override @SneakyThrows
-    public void runAnonymous(@NonNull ThrowingRunnable runnable) {
-        runnable.run();
+    public void runAnonymous(@NonNull final ThrowingRunnable runnable) {
+        try {
+            openInteraction();
+            runnable.run();
+        } finally {
+            interactionLayers.pop();
+        }
     }
 
     @Override @SneakyThrows
-    public <R> R callAnonymous(@NonNull Callable<R> callable) {
-        return callable.call();
+    public <R> R callAnonymous(@NonNull final Callable<R> callable) {
+        try {
+            openInteraction();
+            return callable.call();
+        } finally {
+            interactionLayers.pop();
+        }
     }
 
+    @RequiredArgsConstructor
+    static class Interaction_forTesting implements Interaction {
+        private final UUID uuid = UUID.randomUUID();
+        @Override public <T> T putAttribute(final Class<? super T> type, final T value) { return null; }
+        @Override public <T> T computeAttributeIfAbsent(final Class<? super T> type, final Function<Class<?>, ? extends T> mappingFunction) { return null; }
+        @Override public <T> T getAttribute(final Class<T> type) { return null; }
+        @Override public void removeAttribute(final Class<?> type) { }
+        @Override public UUID getInteractionId() { return uuid; }
+        @Override public Command getCommand() { return null; }
+        @Override public Execution<?, ?> getCurrentExecution() { return null; }
+        @Override public Execution<?, ?> getPriorExecution() { return null; }
+    };
+
 }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/scalars/ConverterBasedOnValueSemantics.java b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/scalars/ConverterBasedOnValueSemantics.java
index 8f0b61a..4c1bb5f 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/scalars/ConverterBasedOnValueSemantics.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/components/scalars/ConverterBasedOnValueSemantics.java
@@ -65,7 +65,7 @@ implements
      * @see IConverter#convertToObject(String, Locale)
      */
     @Override
-    public T convertToObject(final String text, final Locale locale) throws ConversionException {
+    public final T convertToObject(final String text, final Locale locale) throws ConversionException {
 
         val feature = feature();
         val valueFacet = valueFacet();
@@ -82,7 +82,7 @@ implements
      * @see IConverter#convertToString(String, Locale)
      */
     @Override
-    public String convertToString(final T value, final Locale locale) {
+    public final String convertToString(final T value, final Locale locale) {
 
         val feature = feature();
         val valueFacet = valueFacet();
@@ -126,7 +126,7 @@ implements
     // -- DEPENDENCIES
 
     @Override
-    public IsisAppCommonContext getCommonContext() {
+    public final IsisAppCommonContext getCommonContext() {
         return commonContext = CommonContextUtils.computeIfAbsent(commonContext);
     }
 
diff --git a/viewers/wicket/ui/src/test/java/org/apache/isis/viewer/wicket/ui/components/scalars/jdkmath/BigDecimalConverter_roundtrip.java b/viewers/wicket/ui/src/test/java/org/apache/isis/viewer/wicket/ui/components/scalars/jdkmath/BigDecimalConverter_roundtrip.java
new file mode 100644
index 0000000..8b70978
--- /dev/null
+++ b/viewers/wicket/ui/src/test/java/org/apache/isis/viewer/wicket/ui/components/scalars/jdkmath/BigDecimalConverter_roundtrip.java
@@ -0,0 +1,195 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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 org.apache.isis.viewer.wicket.ui.components.scalars.jdkmath;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+
+import javax.validation.constraints.Digits;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.exceptions.recoverable.TextEntryParseException;
+import org.apache.isis.applib.services.iactnlayer.InteractionContext;
+import org.apache.isis.applib.services.iactnlayer.InteractionService;
+import org.apache.isis.core.config.valuetypes.ValueSemanticsRegistry;
+import org.apache.isis.core.metamodel._testing.MetaModelContext_forTesting;
+import org.apache.isis.core.metamodel.context.MetaModelContext;
+import org.apache.isis.core.metamodel.spec.feature.ObjectFeature;
+import org.apache.isis.core.metamodel.valuesemantics.BigDecimalValueSemantics;
+import org.apache.isis.core.security._testing.InteractionService_forTesting;
+import org.apache.isis.viewer.wicket.ui.components.scalars.ConverterBasedOnValueSemantics;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.val;
+
+class BigDecimalConverter_roundtrip {
+
+    final BigDecimal bd_123_45_scale2 = new BigDecimal("123.45").setScale(2);
+    final BigDecimal bd_123_4500_scale2 = new BigDecimal("123.4500").setScale(2);
+
+    final BigDecimal bd_789123_45_scale2 = new BigDecimal("789123.45").setScale(2);
+
+    final BigDecimal bd_123_45_scale4 = new BigDecimal("123.45").setScale(4);
+    final BigDecimal bd_123_4500_scale4 = new BigDecimal("123.4500").setScale(4);
+
+    private InteractionService interactionService;
+    private MetaModelContext mmc;
+
+    private static class BigDecimalConverterForFeature
+    extends ConverterBasedOnValueSemantics<BigDecimal> {
+
+        private static final long serialVersionUID = 1L;
+
+        protected BigDecimalConverterForFeature(final ObjectFeature propOrParam) {
+            super(propOrParam);
+        }
+    }
+
+    @BeforeEach
+    void setUp() throws Exception {
+
+        mmc = MetaModelContext_forTesting.builder()
+                .valueSemantic(new BigDecimalValueSemantics())
+                .interactionProvider(interactionService = new InteractionService_forTesting())
+                .build();
+
+        // pre-requisites for testing
+        val reg = mmc.getServiceRegistry().lookupServiceElseFail(ValueSemanticsRegistry.class);
+        assertNotNull(reg.selectValueSemantics(BigDecimal.class));
+        assertTrue(reg.selectValueSemantics(BigDecimal.class).isNotEmpty());
+        assertNotNull(mmc.getServiceRegistry().lookupServiceElseFail(InteractionService.class));
+        assertNotNull(mmc.getInteractionProvider());
+    }
+
+    @Test
+    void scale2_english() {
+        assertRoundtrip(CustomerScale2.class, bd_123_45_scale2, "123.45", "123.45", Locale.ENGLISH);
+    }
+
+    @Test @Disabled //FIXME[ISIS-2741] scale not picked up yet
+    void scale4_english() {
+        assertRoundtrip(CustomerScale4.class, bd_123_45_scale4, "123.4500", "123.4500", Locale.ENGLISH);
+    }
+
+    @Test @Disabled //FIXME[ISIS-2741] scale not picked up yet
+    void scaleNull_english() {
+        assertRoundtrip(Customer.class, bd_123_45_scale2, "123.45", "123.45", Locale.ENGLISH);
+        assertRoundtrip(Customer.class, bd_123_45_scale4, "123.4500", "123.4500", Locale.ENGLISH);
+    }
+
+    @Test
+    void scale2_italian() {
+        assertRoundtrip(Customer.class, bd_123_45_scale2, "123,45", "123,45", Locale.ITALIAN);
+    }
+
+    @Test
+    void scale2_english_withThousandSeparators() {
+        assertRoundtrip(CustomerScale2.class, bd_789123_45_scale2, "789,123.45", "789,123.45", Locale.ENGLISH);
+    }
+
+    @Test
+    void scale2_english_withoutThousandSeparators() {
+        assertRoundtrip(CustomerScale2.class, bd_789123_45_scale2, "789123.45", "789,123.45", Locale.ENGLISH);
+    }
+
+    @Test @Disabled //FIXME[ISIS-2741] scale not picked up yet
+    void scale2_english_tooLargeScale() {
+        assertParseError(CustomerScale2.class, "123.454", Locale.ENGLISH,
+                "No more than 2 digits can be entered after the decimal place");
+    }
+
+    // -- HELPER
+
+    private void assertRoundtrip(final Class<?> type, final BigDecimal expected, final String valueAsText, final String expectedText, final Locale locale) {
+
+        val converter = newConverter(type);
+
+        interactionService.run(InteractionContext.builder()
+                .locale(locale)
+                .build(), ()->{
+
+                    // when
+                    final BigDecimal actual = converter.convertToObject(valueAsText, null);
+                    assertNumberEquals(expected, actual);
+
+                    // when
+                    final String actualStr = converter.convertToString(actual, null);
+                    assertEquals(expectedText, actualStr);
+
+                });
+    }
+
+    private void assertParseError(final Class<?> type, final String valueAsText, final Locale locale, final String expectedMessage) {
+
+        val converter = newConverter(type);
+
+        interactionService.run(InteractionContext.builder()
+                .locale(locale)
+                .build(), ()->{
+                    // when
+                    val ex = assertThrows(TextEntryParseException.class, ()->
+                        converter.convertToObject(valueAsText, null));
+                    assertEquals(expectedMessage, ex.getMessage());
+                });
+    }
+
+    private static void assertNumberEquals(final BigDecimal a, final BigDecimal b) {
+        assertEquals(a, b);
+    }
+
+    // -- SCENARIOS
+
+    @DomainObject
+    static class Customer {
+        @Property @Getter @Setter
+        private BigDecimal value;
+    }
+
+    @DomainObject
+    static class CustomerScale2 {
+        @Property @Getter @Setter
+        @Digits(fraction = 2, integer = 20)
+        private BigDecimal value;
+    }
+
+    @DomainObject
+    static class CustomerScale4 {
+        @Property @Getter @Setter
+        @Digits(fraction = 4, integer = 20)
+        private BigDecimal value;
+    }
+
+    private BigDecimalConverterForFeature newConverter(final Class<?> type) {
+        val customerSpec = mmc.getSpecificationLoader().specForTypeElseFail(type);
+        val prop = customerSpec.getPropertyElseFail("value");
+        return new BigDecimalConverterForFeature(prop);
+    }
+
+}