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.Beta;
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.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@Beta
038@GwtCompatible
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}. The returned
055   * collector throws an {@code IllegalArgumentException} if the stream consists of two or more
056   * elements, and a {@code NullPointerException} if the stream consists of exactly one element,
057   * which is null.
058   */
059  @SuppressWarnings("unchecked")
060  public static <T> Collector<T, ?, Optional<T>> toOptional() {
061    return (Collector) TO_OPTIONAL;
062  }
063  
064  private static final Object NULL_PLACEHOLDER = new Object();
065
066  private static final Collector<Object, ?, Object> ONLY_ELEMENT =
067      Collector.of(
068          ToOptionalState::new,
069          (state, o) -> state.add((o == null) ? NULL_PLACEHOLDER : o),
070          ToOptionalState::combine,
071          state -> {
072            Object result = state.getElement();
073            return (result == NULL_PLACEHOLDER) ? null : result;
074          },
075          Collector.Characteristics.UNORDERED);
076
077  /**
078   * A collector that takes a stream containing exactly one element and returns that element. The
079   * returned collector throws an {@code IllegalArgumentException} if the stream consists of two or
080   * more elements, and a {@code NoSuchElementException} if the stream is empty.
081   */
082  @SuppressWarnings("unchecked")
083  public static <T> Collector<T, ?, T> onlyElement() {
084    return (Collector) ONLY_ELEMENT;
085  }
086
087  /**
088   * This atrocity is here to let us report several of the elements in the stream if there were more
089   * than one, not just two.
090   */
091  private static final class ToOptionalState {
092    static final int MAX_EXTRAS = 4;
093
094    @Nullable Object element;
095    @Nullable List<Object> extras;
096
097    ToOptionalState() {
098      element = null;
099      extras = null;
100    }
101
102    IllegalArgumentException multiples(boolean overflow) {
103      StringBuilder sb =
104          new StringBuilder().append("expected one element but was: <").append(element);
105      for (Object o : extras) {
106        sb.append(", ").append(o);
107      }
108      if (overflow) {
109        sb.append(", ...");
110      }
111      sb.append('>');
112      throw new IllegalArgumentException(sb.toString());
113    }
114
115    void add(Object o) {
116      checkNotNull(o);
117      if (element == null) {
118        this.element = o;
119      } else if (extras == null) {
120        extras = new ArrayList<>(MAX_EXTRAS);
121        extras.add(o);
122      } else if (extras.size() < MAX_EXTRAS) {
123        extras.add(o);
124      } else {
125        throw multiples(true);
126      }
127    }
128
129    ToOptionalState combine(ToOptionalState other) {
130      if (element == null) {
131        return other;
132      } else if (other.element == null) {
133        return this;
134      } else {
135        if (extras == null) {
136          extras = new ArrayList<>();
137        }
138        extras.add(other.element);
139        if (other.extras != null) {
140          this.extras.addAll(other.extras);
141        }
142        if (extras.size() > MAX_EXTRAS) {
143          extras.subList(MAX_EXTRAS, extras.size()).clear();
144          throw multiples(true);
145        }
146        return this;
147      }
148    }
149
150    Optional<Object> getOptional() {
151      if (extras == null) {
152        return Optional.ofNullable(element);
153      } else {
154        throw multiples(false);
155      }
156    }
157
158    Object getElement() {
159      if (element == null) {
160        throw new NoSuchElementException();
161      } else if (extras == null) {
162        return element;
163      } else {
164        throw multiples(false);
165      }
166    }
167  }
168
169  private MoreCollectors() {}
170}