001/*
002 * Copyright (C) 2016 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect;
018
019import static com.google.common.base.Preconditions.checkNotNull;
020import static com.google.common.collect.NullnessCasts.uncheckedCastNullableTToT;
021import static java.util.Collections.emptyList;
022
023import com.google.common.annotations.GwtCompatible;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.NoSuchElementException;
027import java.util.Optional;
028import java.util.function.Supplier;
029import java.util.stream.Collector;
030import org.jspecify.annotations.NonNull;
031import org.jspecify.annotations.Nullable;
032
033/**
034 * Collectors not present in {@code java.util.stream.Collectors} that are not otherwise associated
035 * with a {@code com.google.common} type.
036 *
037 * @author Louis Wasserman
038 * @since 21.0
039 */
040@GwtCompatible
041public final class MoreCollectors {
042
043  /*
044   * TODO(lowasser): figure out if we can convert this to a concurrent AtomicReference-based
045   * collector without breaking j2cl?
046   */
047  private static final Collector<Object, ?, Optional<Object>> TO_OPTIONAL =
048      Collector.of(
049          () -> new ToOptionalState<>(null),
050          ToOptionalState::add,
051          ToOptionalState::combine,
052          ToOptionalState::getOptional,
053          Collector.Characteristics.UNORDERED);
054
055  /**
056   * A collector that converts a stream of zero or one elements to an {@code Optional}.
057   *
058   * @throws IllegalArgumentException if the stream consists of two or more elements.
059   * @throws NullPointerException if any element in the stream is {@code null}.
060   * @return {@code Optional.of(onlyElement)} if the stream has exactly one element (must not be
061   *     {@code null}) and returns {@code Optional.empty()} if it has none.
062   */
063  @SuppressWarnings("unchecked")
064  public static <T> Collector<T, ?, Optional<T>> toOptional() {
065    return (Collector) TO_OPTIONAL;
066  }
067
068  private static final Object NULL_PLACEHOLDER = new Object();
069
070  private static final Collector<@Nullable Object, ?, @Nullable Object> ONLY_ELEMENT =
071      Collector.<@Nullable Object, ToOptionalState<Object>, @Nullable Object>of(
072          () -> new ToOptionalState<>(null),
073          (state, o) -> state.add((o == null) ? NULL_PLACEHOLDER : o),
074          ToOptionalState::combine,
075          state -> {
076            Object result = state.getElement();
077            return (result == NULL_PLACEHOLDER) ? null : result;
078          },
079          Collector.Characteristics.UNORDERED);
080
081  /**
082   * A collector that takes a stream containing exactly one element and returns that element. The
083   * returned collector throws an {@code IllegalArgumentException} if the stream consists of two or
084   * more elements, and a {@code NoSuchElementException} if the stream is empty.
085   */
086  @SuppressWarnings("unchecked")
087  public static <T extends @Nullable Object> Collector<T, ?, T> onlyElement() {
088    return (Collector) ONLY_ELEMENT;
089  }
090
091  /**
092   * This atrocity is here to let us report several of the elements in the stream if there were more
093   * than one, not just two.
094   */
095  @SuppressWarnings("EmptyList") // ImmutableList doesn't support nullable element types
096  @IgnoreJRERequirement // see enclosing class (whose annotation Animal Sniffer ignores here...)
097  private static final class ToOptionalState<T extends @Nullable Object> {
098    static final int MAX_EXTRAS = 4;
099
100    @Nullable T element;
101    List<T> extras;
102    final @Nullable Supplier<? extends RuntimeException> exceptionSupplier;
103
104    ToOptionalState(@Nullable Supplier<? extends RuntimeException> exceptionSupplier) {
105      this.element = null;
106      this.extras = emptyList();
107      this.exceptionSupplier = exceptionSupplier;
108    }
109
110    RuntimeException multiples(boolean overflow) {
111      if (exceptionSupplier != null) {
112        throw exceptionSupplier.get();
113      }
114      StringBuilder sb =
115          new StringBuilder().append("expected one element but was: <").append(element);
116      for (Object o : extras) {
117        sb.append(", ").append(o);
118      }
119      if (overflow) {
120        sb.append(", ...");
121      }
122      sb.append('>');
123      throw new IllegalArgumentException(sb.toString());
124    }
125
126    void add(T o) {
127      checkNotNull(o);
128      if (element == null) {
129        this.element = o;
130      } else if (extras.isEmpty()) {
131        // Replace immutable empty list with mutable list.
132        extras = new ArrayList<>(MAX_EXTRAS);
133        extras.add(o);
134      } else if (extras.size() < MAX_EXTRAS) {
135        extras.add(o);
136      } else {
137        throw multiples(true);
138      }
139    }
140
141    ToOptionalState<T> combine(ToOptionalState<T> other) {
142      if (element == null) {
143        return other;
144      } else if (other.element == null) {
145        return this;
146      } else {
147        if (extras.isEmpty()) {
148          // Replace immutable empty list with mutable list.
149          extras = new ArrayList<>();
150        }
151        extras.add(other.element);
152        extras.addAll(other.extras);
153        if (extras.size() > MAX_EXTRAS) {
154          extras.subList(MAX_EXTRAS, extras.size()).clear();
155          throw multiples(true);
156        }
157        return this;
158      }
159    }
160
161    Optional<@NonNull T> getOptional() {
162      if (extras.isEmpty()) {
163        return Optional.ofNullable(element);
164      } else {
165        throw multiples(false);
166      }
167    }
168
169    T getElement() {
170      if (element == null) {
171        if (exceptionSupplier != null) {
172          throw exceptionSupplier.get();
173        } else {
174          throw new NoSuchElementException();
175        }
176      } else if (extras.isEmpty()) {
177        return uncheckedCastNullableTToT(element);
178      } else {
179        throw multiples(false);
180      }
181    }
182  }
183
184  private MoreCollectors() {}
185}