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