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