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 java.util.Collections.emptyList;
021
022import com.google.common.annotations.Beta;
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.stream.Collector;
029import javax.annotation.CheckForNull;
030import org.checkerframework.checker.nullness.qual.Nullable;
031
032/**
033 * Collectors not present in {@code java.util.stream.Collectors} that are not otherwise associated
034 * with a {@code com.google.common} type.
035 *
036 * @author Louis Wasserman
037 * @since 33.2.0 (available since 21.0 in guava-jre)
038 */
039@GwtCompatible
040@ElementTypesAreNonnullByDefault
041@SuppressWarnings({"AndroidJdkLibsChecker", "Java7ApiChecker"})
042@IgnoreJRERequirement // Users will use this only if they're already using streams.
043@Beta // TODO: b/288085449 - Remove.
044public final class MoreCollectors {
045
046  /*
047   * TODO(lowasser): figure out if we can convert this to a concurrent AtomicReference-based
048   * collector without breaking j2cl?
049   */
050  private static final Collector<Object, ?, Optional<Object>> TO_OPTIONAL =
051      Collector.of(
052          ToOptionalState::new,
053          ToOptionalState::add,
054          ToOptionalState::combine,
055          ToOptionalState::getOptional,
056          Collector.Characteristics.UNORDERED);
057
058  /**
059   * A collector that converts a stream of zero or one elements to an {@code Optional}.
060   *
061   * @throws IllegalArgumentException if the stream consists of two or more elements.
062   * @throws NullPointerException if any element in the stream is {@code null}.
063   * @return {@code Optional.of(onlyElement)} if the stream has exactly one element (must not be
064   *     {@code null}) and returns {@code Optional.empty()} if it has none.
065   */
066  @SuppressWarnings("unchecked")
067  public static <T> Collector<T, ?, Optional<T>> toOptional() {
068    return (Collector) TO_OPTIONAL;
069  }
070
071  private static final Object NULL_PLACEHOLDER = new Object();
072
073  private static final Collector<@Nullable Object, ?, @Nullable Object> ONLY_ELEMENT =
074      Collector.<@Nullable Object, ToOptionalState, @Nullable Object>of(
075          ToOptionalState::new,
076          (state, o) -> state.add((o == null) ? NULL_PLACEHOLDER : o),
077          ToOptionalState::combine,
078          state -> {
079            Object result = state.getElement();
080            return (result == NULL_PLACEHOLDER) ? null : result;
081          },
082          Collector.Characteristics.UNORDERED);
083
084  /**
085   * A collector that takes a stream containing exactly one element and returns that element. The
086   * returned collector throws an {@code IllegalArgumentException} if the stream consists of two or
087   * more elements, and a {@code NoSuchElementException} if the stream is empty.
088   */
089  @SuppressWarnings("unchecked")
090  public static <T extends @Nullable Object> Collector<T, ?, T> onlyElement() {
091    return (Collector) ONLY_ELEMENT;
092  }
093
094  /**
095   * This atrocity is here to let us report several of the elements in the stream if there were more
096   * than one, not just two.
097   */
098  private static final class ToOptionalState {
099    static final int MAX_EXTRAS = 4;
100
101    @CheckForNull Object element;
102    List<Object> extras;
103
104    ToOptionalState() {
105      element = null;
106      extras = emptyList();
107    }
108
109    IllegalArgumentException multiples(boolean overflow) {
110      StringBuilder sb =
111          new StringBuilder().append("expected one element but was: <").append(element);
112      for (Object o : extras) {
113        sb.append(", ").append(o);
114      }
115      if (overflow) {
116        sb.append(", ...");
117      }
118      sb.append('>');
119      throw new IllegalArgumentException(sb.toString());
120    }
121
122    void add(Object o) {
123      checkNotNull(o);
124      if (element == null) {
125        this.element = o;
126      } else if (extras.isEmpty()) {
127        // Replace immutable empty list with mutable list.
128        extras = new ArrayList<>(MAX_EXTRAS);
129        extras.add(o);
130      } else if (extras.size() < MAX_EXTRAS) {
131        extras.add(o);
132      } else {
133        throw multiples(true);
134      }
135    }
136
137    ToOptionalState combine(ToOptionalState other) {
138      if (element == null) {
139        return other;
140      } else if (other.element == null) {
141        return this;
142      } else {
143        if (extras.isEmpty()) {
144          // Replace immutable empty list with mutable list.
145          extras = new ArrayList<>();
146        }
147        extras.add(other.element);
148        extras.addAll(other.extras);
149        if (extras.size() > MAX_EXTRAS) {
150          extras.subList(MAX_EXTRAS, extras.size()).clear();
151          throw multiples(true);
152        }
153        return this;
154      }
155    }
156
157    @IgnoreJRERequirement // see enclosing class (whose annotation Animal Sniffer ignores here...)
158    Optional<Object> getOptional() {
159      if (extras.isEmpty()) {
160        return Optional.ofNullable(element);
161      } else {
162        throw multiples(false);
163      }
164    }
165
166    Object getElement() {
167      if (element == null) {
168        throw new NoSuchElementException();
169      } else if (extras.isEmpty()) {
170        return element;
171      } else {
172        throw multiples(false);
173      }
174    }
175  }
176
177  private MoreCollectors() {}
178}