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