content: /*
   *  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.applib.services.filter;
  
  import java.io.Serializable;
  import java.util.function.BiPredicate;
  import java.util.function.Function;
  
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.applib.services.i18n.TranslatableString;
  import org.apache.causeway.applib.services.i18n.TranslationContext;
  import org.apache.causeway.commons.internal.base._Strings;
  
  import org.jspecify.annotations.NonNull;
  
  /**
   * EXPERIMENTAL/DRAFT
   * <p>
   * If at least one {@link CollectionFilterService} is registered with Spring's context
   * that handles a given domainType,
   * viewer implementations (like Wicket Viewer) should show
   * a quick-search prompt, which is rendered on top of the UI table that
   * presents the collection in question.
   *
   * @since 2.1, 3.1 {@index}
   */
  public interface CollectionFilterService {
  
      public static interface Tokens extends Serializable {
          /**
           * @apiNote for convenience, the searchArg can be pre-processed by
           *      by the {@link CollectionFilterService#tokenFilter(Class)};
           *      The default implementation already handles the empty searchArg case.
           */
          boolean match(@Nullable String searchArg);
      }
  
      /**
       * 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);
  
      // -- TOKENIZER
  
      /**
       * Returns a function that for a given domainObject returns {@link Tokens} (words),
       * that are then matchable by {@link #tokenFilter(Class)}.
       * <p>
       * For example the domain object's title could be tokenized (parsed into tokens).
       *
       * @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> Function<T, Tokens> tokenizer(
              @NonNull Class<T> domainType);
  
      // -- MATCHER
  
      /**
       * For given {@code domainType} returns a {@link BiPredicate}
       * that matches {@link Tokens} against a nullable searchString.
       * <p>
       * For example, the searchString could be tokenized (parsed into tokens),
       * and then matched against the {@link Tokens} using either OR or AND semantics.
       * <p>
       * The default implementation sanitizes the user input (searchArg)
       * then strips any leading or trailing blanks and handles the empty searchArg case.
       *
       * @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 <T> BiPredicate<Tokens, String> tokenFilter(
              final @NonNull Class<T> domainType) {
          return (tokens, searchArg) -> {
              var sanitized = _Strings.blankToNullOrTrim(searchArg);
              return _Strings.isNotEmpty(sanitized)
                  ? tokens!=null
                      ? tokens.match(sanitized)
                      : false // if an element has no tokens, it cannot be found by any search
                  : true; // empty searchArg means 'unfiltered'
          };
      }
  
      // -- SEARCH ARG SANITIZER
  
      /**
       * Pre-process user input before passing it to the matcher.
       * <p>
       * Default behavior is to strip leading or trailing blanks (if any).
       */
      default String sanitzeSearchArgument(final @Nullable String searchArg) {
          return _Strings.blankToNullOrTrim(searchArg);
      }
  
      // -- SEARCH PROMT
  
      /**
       * Placeholder text for the quick-search prompt.
       * @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",
                  _Strings.asNaturalName.andThen(_Strings.asLowerCase)
                      .apply(domainType.getSimpleName()));
      }
  
      /**
       * Provides the {@link TranslationContext} for translating the {@link #searchPromptPlaceholderText(Class)}.
       * @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");
      }
  
  }