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}