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));
+ }
+
+}