001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.util;
028
029import java.util.HashMap;
030import java.util.LinkedHashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.Objects;
034import java.util.function.BinaryOperator;
035import java.util.function.Function;
036import java.util.function.Supplier;
037import java.util.stream.Collector;
038import java.util.stream.Collectors;
039import java.util.stream.Stream;
040
041import edu.umd.cs.findbugs.annotations.NonNull;
042
043public final class CustomCollectors {
044  private CustomCollectors() {
045    // disable
046  }
047
048  @SuppressWarnings("null")
049  @NonNull
050  public static <T> Function<T, T> identity() {
051    return Function.identity();
052  }
053
054  public static Collector<CharSequence, ?, String> joiningWithOxfordComma(@NonNull String conjunction) {
055    return Collectors.collectingAndThen(Collectors.toList(), withOxfordComma(conjunction));
056  }
057
058  private static Function<List<CharSequence>, String> withOxfordComma(@NonNull String conjunction) {
059    return list -> {
060      int size = list.size();
061      if (size < 2) {
062        return String.join("", list);
063      }
064      if (size == 2) {
065        return String.join(" " + conjunction + " ", list);
066      }
067      // else there are 3 or more
068      int last = size - 1;
069      return String.join(", " + conjunction + " ",
070          String.join(", ", list.subList(0, last)),
071          list.get(last));
072    };
073  }
074
075  /**
076   * Produce a new stream with duplicates removed based on the provided
077   * {@code keyMapper}. When a duplicate key is encountered, the second item is
078   * used. The original sequencing is preserved if the input stream is sequential.
079   *
080   * @param <V>
081   *          the item value for the streams
082   * @param <K>
083   *          the key type
084   * @param stream
085   *          the stream to reduce
086   * @param keyMapper
087   *          the key function to use to find unique items
088   * @return a new stream
089   */
090  public static <V, K> Stream<V> distinctByKey(
091      @NonNull Stream<V> stream,
092      @NonNull Function<? super V, ? extends K> keyMapper) {
093    return distinctByKey(stream, keyMapper, (key, value1, value2) -> value2);
094  }
095
096  /**
097   * Produce a new stream with duplicates removed based on the provided
098   * {@code keyMapper}. When a duplicate key is encountered, the provided
099   * {@code duplicateHandler} is used to determine which item to keep. The
100   * original sequencing is preserved if the input stream is sequential.
101   *
102   * @param <V>
103   *          the item value for the streams
104   * @param <K>
105   *          the key type
106   * @param stream
107   *          the stream to reduce
108   * @param keyMapper
109   *          the key function to use to find unique items
110   * @param duplicateHander
111   *          used to determine which of two duplicates to keep
112   * @return a new stream
113   */
114  public static <V, K> Stream<V> distinctByKey(
115      @NonNull Stream<V> stream,
116      @NonNull Function<? super V, ? extends K> keyMapper,
117      @NonNull DuplicateHandler<K, V> duplicateHander) {
118    Map<K, V> uniqueRoles = stream
119        .collect(toMap(
120            keyMapper,
121            identity(),
122            duplicateHander,
123            LinkedHashMap::new));
124    return uniqueRoles.values().stream();
125  }
126
127  @NonNull
128  public static <T, K, V> Collector<T, ?, Map<K, V>> toMap(
129      @NonNull Function<? super T, ? extends K> keyMapper,
130      @NonNull Function<? super T, ? extends V> valueMapper,
131      @NonNull DuplicateHandler<K, V> duplicateHander) {
132    return toMap(keyMapper, valueMapper, duplicateHander, HashMap::new);
133  }
134
135  @NonNull
136  public static <T, K, V, M extends Map<K, V>> Collector<T, ?, M> toMap(
137      @NonNull Function<? super T, ? extends K> keyMapper,
138      @NonNull Function<? super T, ? extends V> valueMapper,
139      @NonNull DuplicateHandler<K, V> duplicateHander,
140      Supplier<M> supplier) {
141    return ObjectUtils.notNull(
142        Collector.of(
143            supplier,
144            (map, item) -> {
145              K key = keyMapper.apply(item);
146              V value = Objects.requireNonNull(valueMapper.apply(item));
147              V oldValue = map.get(key);
148              if (oldValue != null) {
149                value = duplicateHander.handle(key, oldValue, value);
150              }
151              map.put(key, value);
152            },
153            (map1, map2) -> {
154              map2.forEach((key, value) -> {
155                V oldValue = map1.get(key);
156                V newValue = value;
157                if (oldValue != null) {
158                  newValue = duplicateHander.handle(key, oldValue, value);
159                }
160                map1.put(key, newValue);
161              });
162              return map1;
163            }));
164  }
165
166  @FunctionalInterface
167  public interface DuplicateHandler<K, V> {
168    @NonNull
169    V handle(K key, @NonNull V value1, V value2);
170  }
171
172  @NonNull
173  public static <T> BinaryOperator<T> useFirstMapper() {
174    return (value1, value2) -> value1;
175  }
176
177  @NonNull
178  public static <T> BinaryOperator<T> useLastMapper() {
179    return (value1, value2) -> value2;
180  }
181}