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}