You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2024/06/13 18:36:23 UTC

(causeway) branch master updated: CAUSEWAY-3642: support for multiple CollectionSearchService(s)

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/causeway.git


The following commit(s) were added to refs/heads/master by this push:
     new ee18ab8f5d CAUSEWAY-3642: support for multiple CollectionSearchService(s)
ee18ab8f5d is described below

commit ee18ab8f5d58f09cda5fdba062c48627ea84455d
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Jun 13 20:36:18 2024 +0200

    CAUSEWAY-3642: support for multiple CollectionSearchService(s)
---
 .../applib/services/i18n/TranslationService.java   | 18 ++++++
 .../services/search/CollectionSearchService.java   | 35 +++++++++--
 .../services/i18n/TranslatableStringTest.java      | 47 ++++++++++++--
 .../tabular/interactive/DataTableInteractive.java  | 39 +++++-------
 .../tabular/interactive/_SearchUtils.java          | 72 ++++++++++++++++++++++
 5 files changed, 176 insertions(+), 35 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/i18n/TranslationService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/i18n/TranslationService.java
index 6c0eb839ea..1a114517ac 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/i18n/TranslationService.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/i18n/TranslationService.java
@@ -86,5 +86,23 @@ public interface TranslationService {
      * </p>
      */
     Mode getMode();
+        
+    /**
+     * Can be used as fallback in the absence of a {@link TranslationService} implementation.
+     * Acts as a text pass-through, not translating anything.
+     */
+    public static TranslationService identity() {
+        return new TranslationService() {
+            @Override public String translate(TranslationContext context, String text) {
+                return text;
+            }
+            @Override public String translate(TranslationContext context, String singularText, String pluralText, int num) {
+                return num==1 ? singularText : pluralText;
+            }
+            @Override public Mode getMode() { 
+                return Mode.READ; 
+            }
+        };
+    }
 
 }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/services/search/CollectionSearchService.java b/api/applib/src/main/java/org/apache/causeway/applib/services/search/CollectionSearchService.java
index c8fa29d3da..f63bf5586c 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/services/search/CollectionSearchService.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/services/search/CollectionSearchService.java
@@ -18,9 +18,11 @@
  */
 package org.apache.causeway.applib.services.search;
 
-import java.util.Optional;
 import java.util.function.BiPredicate;
 
+import org.apache.causeway.applib.services.i18n.TranslatableString;
+import org.apache.causeway.applib.services.i18n.TranslationContext;
+
 import lombok.NonNull;
 
 /**
@@ -36,7 +38,13 @@ import lombok.NonNull;
 public interface CollectionSearchService {
 
     /**
-     * Optionally returns a {@link BiPredicate} that filters collections
+     * Whether this service handles given type.
+     * @param domainType - entity or view-model type to be rendered as row in a table
+     */
+    boolean handles(@NonNull Class<?> domainType);
+
+    /**
+     * Returns a {@link BiPredicate} that filters collections
      * of given {@code domainType} by a nullable searchString,
      * based on whether the search is supported.
      * <p>
@@ -44,12 +52,29 @@ public interface CollectionSearchService {
      * domain object's title say.
      *
      * @param domainType - entity or view-model type to be rendered as row in a table
+     * @apiNote guarded by a call to {@link #handles(Class)}
      */
-    <T> Optional<BiPredicate<T, String>> searchPredicate(
+    <T> BiPredicate<T, String> searchPredicate(
             @NonNull Class<T> domainType);
 
-    default String searchPromptPlaceholderText(final @NonNull Class<?> domainType) {
-        return "Search";
+    /**
+     *
+     * @param domainType - entity or view-model type to be rendered as row in a table
+     * @apiNote guarded by a call to {@link #handles(Class)}
+     */
+    default TranslatableString searchPromptPlaceholderText(
+            final @NonNull Class<?> domainType) {
+        return TranslatableString.tr("Search {domainType}", "domainType", domainType.getSimpleName());
+    }
+
+    /**
+     *
+     * @param domainType - entity or view-model type to be rendered as row in a table
+     * @apiNote guarded by a call to {@link #handles(Class)}
+     */
+    default TranslationContext translationContext(
+            final @NonNull Class<?> domainType) {
+        return TranslationContext.named("Search");
     }
 
 }
\ No newline at end of file
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/services/i18n/TranslatableStringTest.java b/api/applib/src/test/java/org/apache/causeway/applib/services/i18n/TranslatableStringTest.java
index 9647baf1d9..3bb2c9b07e 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/services/i18n/TranslatableStringTest.java
+++ b/api/applib/src/test/java/org/apache/causeway/applib/services/i18n/TranslatableStringTest.java
@@ -26,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 class TranslatableStringTest {
 
@@ -40,27 +41,62 @@ class TranslatableStringTest {
 
         @Test
         public void pluralFormOne() throws Exception {
-            final TranslatableString ts = TranslatableString.trn("You can't do that because there is a dependent object", "You can't do that because there are dependent objects", 1);
+            final TranslatableString ts = TranslatableString.trn(
+                    "You can't do that because there is a dependent object",
+                    "You can't do that because there are dependent objects", 1);
 
             assertThat(ts.getPattern(), is("You can't do that because there is a dependent object"));
         }
 
         @Test
         public void pluralFormTwo() throws Exception {
-            final TranslatableString ts = TranslatableString.trn("You can't do that because there is a dependent object", "You can't do that because there are dependent objects", 2);
+            final TranslatableString ts = TranslatableString.trn(
+                    "You can't do that because there is a dependent object",
+                    "You can't do that because there are dependent objects", 2);
 
             assertThat(ts.getPattern(), is("You can't do that because there are dependent objects"));
         }
 
     }
 
+    public static class Identity extends TranslatableStringTest {
+
+        final TranslationService tsIdentity = TranslationService.identity();
+
+        @Test
+        public void singularForm() throws Exception {
+            final TranslatableString ts = TranslatableString.tr(
+                    "My name is {lastName}, {firstName} {lastName}.",
+                    "lastName", "Bond", "firstName", "James");
+
+            assertEquals(
+                    "My name is Bond, James Bond.",
+                    ts.translate(tsIdentity, TranslationContext.named("someContext")));
+        }
+
+        @Test
+        public void pluralFormOne() throws Exception {
+            final TranslatableString ts = TranslatableString.trn(
+                    "My name is {lastName}, {firstName} {lastName}.",
+                    "My name is {firstName} {lastName}.",
+                    1,
+                    "lastName", "Bond", "firstName", "James");
+
+            assertThat(ts.translated("Ich heisse {lastName}, {firstName} {lastName}."),
+                    is("Ich heisse Bond, James Bond."));
+        }
+    }
+
     public static class Translated extends TranslatableStringTest {
 
         @Test
         public void singularForm() throws Exception {
-            final TranslatableString ts = TranslatableString.tr("My name is {lastName}, {firstName} {lastName}.", "lastName", "Bond", "firstName", "James");
+            final TranslatableString ts = TranslatableString.tr(
+                    "My name is {lastName}, {firstName} {lastName}.",
+                    "lastName", "Bond", "firstName", "James");
 
-            assertThat(ts.translated("Iche heisse {lastName}, {firstName} {lastName}."), is("Iche heisse Bond, James Bond."));
+            assertThat(ts.translated("Ich heisse {lastName}, {firstName} {lastName}."),
+                    is("Ich heisse Bond, James Bond."));
         }
 
         @Test
@@ -71,7 +107,8 @@ class TranslatableStringTest {
                     1,
                     "lastName", "Bond", "firstName", "James");
 
-            assertThat(ts.translated("Iche heisse {lastName}, {firstName} {lastName}."), is("Iche heisse Bond, James Bond."));
+            assertThat(ts.translated("Ich heisse {lastName}, {firstName} {lastName}."),
+                    is("Ich heisse Bond, James Bond."));
         }
     }
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/DataTableInteractive.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/DataTableInteractive.java
index 2d0773a2e8..e8592bcdff 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/DataTableInteractive.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/DataTableInteractive.java
@@ -23,7 +23,6 @@ import java.util.Comparator;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.BiPredicate;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -32,12 +31,9 @@ import org.springframework.lang.Nullable;
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.annotation.TableDecorator;
 import org.apache.causeway.applib.annotation.Where;
-import org.apache.causeway.applib.services.search.CollectionSearchService;
 import org.apache.causeway.commons.binding.Bindable;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.IndexedFunction;
-import org.apache.causeway.commons.internal.base._Casts;
-import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.binding._BindableAbstract;
 import org.apache.causeway.commons.internal.binding._Bindables;
 import org.apache.causeway.commons.internal.binding._Observables;
@@ -45,7 +41,6 @@ import org.apache.causeway.commons.internal.binding._Observables.LazyObservable;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.consent.InteractionResult;
-import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.interactions.InteractionHead;
 import org.apache.causeway.core.metamodel.interactions.InteractionUtils;
 import org.apache.causeway.core.metamodel.interactions.ObjectVisibilityContext;
@@ -62,6 +57,7 @@ import org.apache.causeway.core.metamodel.object.MmSortUtils;
 import org.apache.causeway.core.metamodel.object.PackedManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
+import org.apache.causeway.core.metamodel.tabular.interactive._SearchUtils.SearchHandler;
 import org.apache.causeway.core.metamodel.tabular.simple.DataTable;
 
 import lombok.AccessLevel;
@@ -125,8 +121,7 @@ implements MultiselectChoices {
     @Getter private final @NonNull LazyObservable<Can<DataColumn>> dataColumns;
     @Getter private final @NonNull LazyObservable<String> title;
 
-    private final @Nullable BiPredicate<Object, String> searchPredicate;
-    @Getter private final String searchPromptPlaceholderText;
+    private final Optional<SearchHandler> searchHandler;
 
     /**
      * On data row selection changes (originating from UI),
@@ -141,23 +136,12 @@ implements MultiselectChoices {
             final Where where,
             final Can<ManagedObject> elements) {
 
-        val mmc = MetaModelContext.instanceElseFail();
+        val elementType = managedMember.getElementType();
+        val mmc = elementType.getMetaModelContext();
 
         this.managedMember = managedMember;
         this.where = where;
-
-        { // search stuff
-            var collectionSearchServiceOpt = mmc.lookupService(CollectionSearchService.class);
-            var elementType = managedMember.getElementType().getCorrespondingClass();
-            this.searchPredicate = _Casts.uncheckedCast(
-                collectionSearchServiceOpt
-                    .flatMap(collectionSearchService->collectionSearchService.searchPredicate(elementType))
-                    .orElse(null));
-            this.searchPromptPlaceholderText = _Strings.nullToEmpty(
-                collectionSearchServiceOpt
-                    .map(collectionSearchService->collectionSearchService.searchPromptPlaceholderText(elementType))
-                    .orElse(null));
-        }
+        this.searchHandler = _SearchUtils.createSearchHandler(elementType);
 
         searchArgument = _Bindables.forValue("");
         columnSort = _Bindables.forValue(null);
@@ -216,7 +200,7 @@ implements MultiselectChoices {
     }
 
     public boolean isSearchSupported() {
-        return searchPredicate!=null;
+        return searchHandler.isPresent();
     }
 
     public int getPageSize(final int pageSizeDefault) {
@@ -255,10 +239,15 @@ implements MultiselectChoices {
 
     // -- SEARCH
 
+    public String getSearchPromptPlaceholderText() {
+        return searchHandler.map(handler->handler.searchPromptPlaceholderText)
+                .orElse("");
+    }
+
     private Predicate<DataRow> adaptSearchPredicate() {
-        return searchPredicate==null
+        return searchHandler.isEmpty()
                 ? dataRow->true
-                : dataRow->searchPredicate
+                : dataRow->searchHandler.get().searchPredicate
                     .test(dataRow.getRowElement().getPojo(), searchArgument.getValue());
     }
 
@@ -450,7 +439,7 @@ implements MultiselectChoices {
                         .getManagedCollection().orElseThrow()
                     : ActionInteraction.start(owner, memberId, where)
                         .getManagedActionElseFail();
-            
+
             var dataTableInteractive = new DataTableInteractive(managedMember, where,
                     dataTable.streamDataElements()
                     .peek(obj->{
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/_SearchUtils.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/_SearchUtils.java
new file mode 100644
index 0000000000..9b4470ee9d
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/tabular/interactive/_SearchUtils.java
@@ -0,0 +1,72 @@
+/*
+ *  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.causeway.core.metamodel.tabular.interactive;
+
+import java.util.Optional;
+import java.util.function.BiPredicate;
+
+import org.apache.causeway.applib.services.i18n.TranslationContext;
+import org.apache.causeway.applib.services.i18n.TranslationService;
+import org.apache.causeway.applib.services.search.CollectionSearchService;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+class _SearchUtils {
+
+    @AllArgsConstructor
+    static class SearchHandler {
+        @NonNull final BiPredicate<Object, String> searchPredicate;
+        @NonNull final String searchPromptPlaceholderText;
+    }
+
+    Optional<SearchHandler> createSearchHandler(final @NonNull ObjectSpecification elementType) {
+        var mmc = elementType.getMetaModelContext();
+        var collectionSearchServiceOpt = mmc.getServiceRegistry().select(CollectionSearchService.class).stream()
+                .filter(service->service.handles(elementType.getCorrespondingClass()))
+                .findFirst();
+        if(!collectionSearchServiceOpt.isPresent()) {
+            return Optional.empty();
+        }
+        var collectionSearchService = collectionSearchServiceOpt.get();
+        var searchPredicate = collectionSearchService.searchPredicate(elementType.getCorrespondingClass());
+        if(searchPredicate==null) {
+            return Optional.empty();
+        }
+
+        var translationService = mmc.lookupService(TranslationService.class)
+                .orElseGet(TranslationService::identity);
+        var translationContext = Optional.ofNullable(collectionSearchService.translationContext(elementType.getCorrespondingClass()))
+                .orElseGet(()->TranslationContext.named("Search"));
+        var translatableString = collectionSearchService.searchPromptPlaceholderText(elementType.getCorrespondingClass());
+
+        var searchPromptPlaceholderText = translatableString!=null
+                ? translatableString.translate(translationService, translationContext)
+                : "";
+
+        return Optional.of(new SearchHandler(
+                _Casts.uncheckedCast(searchPredicate),
+                searchPromptPlaceholderText));
+    }
+
+}