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