001/* 002 * MIT License 003 * 004 * Copyright (c) 2022 Michael Cowan 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in all 014 * copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024 025package io.blt.util.stream; 026 027import java.util.Optional; 028import java.util.Set; 029import java.util.function.BiConsumer; 030import java.util.function.BinaryOperator; 031import java.util.function.Function; 032import java.util.function.Supplier; 033import java.util.stream.Collector; 034 035import static java.util.Collections.emptySet; 036import static java.util.Objects.nonNull; 037 038/** 039 * Implementations of {@link Collector} that reduce to exactly one or zero elements. 040 * e.g., 041 * <pre>{@code 042 * var maybeActiveStep = sequentialSteps.stream() 043 * .filter(SequentialStep::isActive) 044 * .collect(SingletonCollectors.toOptional()); 045 * }</pre> 046 * <p> 047 * If more than one element is present, then {@code IllegalArgumentException} is thrown. 048 */ 049public final class SingletonCollectors { 050 051 private SingletonCollectors() { 052 throw new IllegalAccessError("Utility class should be accessed statically and never constructed"); 053 } 054 055 /** 056 * Returns a {@code Collector} that accumulates the only element, if any, into an {@code Optional}. 057 * 058 * @param <T> the type of the input elements 059 * @return a {@code Collector} that accumulates the only element, if any, into an {@code Optional}. 060 * @throws IllegalArgumentException if more than one element is present 061 */ 062 public static <T> SingletonCollector<T, Optional<T>> toOptional() { 063 return new SingletonCollector<>(Container::getOptional); 064 } 065 066 /** 067 * Returns a {@code Collector} that accumulates the only element, if any, into a nullable {@code Object}. 068 * 069 * @param <T> the type of the input elements 070 * @return a {@code Collector} that accumulates the only element, if any, into a nullable {@code Object}. 071 * @throws IllegalArgumentException if more than one element is present 072 */ 073 public static <T> SingletonCollector<T, T> toNullable() { 074 return new SingletonCollector<>(Container::getValue); 075 } 076 077 public static final class SingletonCollector<T, R> implements Collector<T, Container<T>, R> { 078 079 private final Function<Container<T>, R> finisher; 080 081 private SingletonCollector(Function<Container<T>, R> finisher) { 082 this.finisher = finisher; 083 } 084 085 @Override 086 public Supplier<Container<T>> supplier() { 087 return Container::new; 088 } 089 090 @Override 091 public BiConsumer<Container<T>, T> accumulator() { 092 return Container::setValue; 093 } 094 095 @Override 096 public BinaryOperator<Container<T>> combiner() { 097 return null; // Only used in a parallel stream, which this collector doesn't support 098 } 099 100 @Override 101 public Function<Container<T>, R> finisher() { 102 return finisher; 103 } 104 105 @Override 106 public Set<Characteristics> characteristics() { 107 return emptySet(); 108 } 109 } 110 111 public static final class Container<T> { 112 113 private T value; 114 115 private Container() {} 116 117 public T getValue() { 118 return value; 119 } 120 121 public void setValue(T value) { 122 if (nonNull(this.value)) { 123 throw new IllegalStateException("Expected stream to contain exactly 0 or 1 elements"); 124 } 125 this.value = value; 126 } 127 128 public Optional<T> getOptional() { 129 return Optional.ofNullable(getValue()); 130 } 131 } 132 133}