001/* 002 * MIT License 003 * 004 * Copyright (c) 2023 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; 026 027import io.blt.util.functional.ThrowingFunction; 028import io.blt.util.functional.ThrowingSupplier; 029import java.util.HashMap; 030import java.util.Map; 031 032import static io.blt.util.Obj.newInstanceOf; 033import static java.util.Objects.nonNull; 034 035/** 036 * Static utility methods for operating on implementations of {@code Collection} and {@code Map} i.e., Containers. 037 * <p> 038 * For methods that return a modification of a passed container, the result will be of the same type if possible. 039 * This is accomplished using {@link Obj#newInstanceOf(Object)} and its limitations apply. 040 * </p> 041 */ 042public final class Ctr { 043 044 private Ctr() { 045 throw new IllegalAccessError("Utility class should be accessed statically and never constructed"); 046 } 047 048 /** 049 * Returns a new {@link Map} containing the entries of {@code source} with {@code transform} applied to the values. 050 * <p> 051 * If possible, the result is of the same type as the passed {@code source} map. 052 * </p> 053 * e.g., 054 * <pre>{@code 055 * var scores = Map.of("Louis", 95, "Greg", 92, "Mike", 71, "Phil", 86); 056 * var grades = Ctr.transformValues(scores, score -> { 057 * if (score >= 90) { 058 * return "A"; 059 * } else if (score >= 80) { 060 * return "B"; 061 * } else if (score >= 70) { 062 * return "C"; 063 * } else if (score >= 60) { 064 * return "D"; 065 * } else { 066 * return "F"; 067 * } 068 * }); 069 * // grades = Map.of("Louis", "A", "Greg", "A", "Mike", "C", "Phil", "B") 070 * }</pre> 071 * 072 * @param source {@link Map} whose values should be transformed 073 * @param transform value transformation function 074 * @param <K> {@code map} key type 075 * @param <V> {@code map} value type 076 * @param <R> returned map value type 077 * @param <E> type of {@code transform} throwable 078 * @return a new {@link Map} containing the entries of {@code source} with {@code transform} applied to the values 079 * @see Obj#newInstanceOf(Object) 080 */ 081 @SuppressWarnings("unchecked") 082 public static <K, V, R, E extends Throwable> Map<K, R> transformValues( 083 Map<K, V> source, ThrowingFunction<? super V, R, E> transform) throws E { 084 var result = newInstanceOf((Map<K, R>) source).orElse(new DefaultMap<>()); 085 086 for (var entry : source.entrySet()) { 087 result.put(entry.getKey(), transform.apply(entry.getValue())); 088 } 089 090 return result instanceof DefaultMap ? Map.copyOf(result) : result; 091 } 092 093 /** 094 * For the specified {@code map}, if there is no value for the specified {@code key} then {@code compute} will be 095 * called and the result entered into the map. If a value is present, then it is returned. 096 * e.g., 097 * <pre>{@code 098 * private final Map<URL, String> cache = new HashMap<>(); 099 * 100 * public String fetch(URL url) throws IOException { 101 * return Ctr.computeIfAbsent(cache, url, this::get); 102 * } 103 * 104 * private String get(URL url) throws IOException { 105 * try (var stream = url.openStream()) { 106 * return new String(stream.readAllBytes(), StandardCharsets.UTF_8); 107 * } 108 * } 109 * }</pre> 110 * 111 * <p>If {@code compute} returns {@code null}, then the map is not modified and {@code null} is returned. 112 * 113 * <p>If {@code compute} throws, then map is not modified and the exception will bubble up. 114 * 115 * @param map {@link Map} whose value is returned and may be computed 116 * @param key key with which the specified value is to be associated 117 * @param compute the computation function to use if the value is absent 118 * @param <K> {@code map} key type 119 * @param <V> {@code map} value type 120 * @param <E> type of {@code compute} throwable 121 * @return the existing or computed value associated with the key, or null if the computed value is null 122 * @throws E if an exception is thrown by the computation function 123 * @implNote The implementation is equivalent to the following: 124 * <pre> {@code 125 * if (map.get(key) == null) { 126 * V value = compute.apply(key); 127 * if (value != null) { 128 * map.put(key, value); 129 * } 130 * } 131 * return map.get(key); 132 * }</pre> 133 * @see Ctr#computeIfAbsent(Map, Object, ThrowingSupplier) 134 */ 135 public static <K, V, E extends Throwable> V computeIfAbsent( 136 Map<K, V> map, K key, ThrowingFunction<? super K, ? extends V, E> compute) throws E { 137 return Obj.orElseGet(map.get(key), () -> { 138 var value = compute.apply(key); 139 if (nonNull(value)) { 140 map.put(key, value); 141 } 142 return value; 143 }); 144 } 145 146 /** 147 * For the specified {@code map}, if there is no value for the specified {@code key} then {@code compute} will be 148 * called and the result entered into the map. If a value is present, then it is returned. 149 * e.g., 150 * <pre>{@code 151 * private final Map<URL, String> cache = new HashMap<>(); 152 * 153 * public String fetch(URL url) throws IOException { 154 * return Ctr.computeIfAbsent(cache, url, () -> { 155 * try (var stream = url.openStream()) { 156 * return new String(stream.readAllBytes(), StandardCharsets.UTF_8); 157 * } 158 * }); 159 * } 160 * }</pre> 161 * 162 * <p>If {@code compute} returns {@code null}, then the map is not modified and {@code null} is returned. 163 * 164 * <p>If {@code compute} throws, then map is not modified and the exception will bubble up. 165 * 166 * @param map {@link Map} whose value is returned and may be computed 167 * @param key key with which the specified value is to be associated 168 * @param compute the computation supplier to use if the value is absent 169 * @param <K> {@code map} key type 170 * @param <V> {@code map} value type 171 * @param <E> type of {@code compute} throwable 172 * @return the existing or computed value associated with the key, or null if the computed value is null 173 * @throws E if an exception is thrown by the computation supplier 174 * @implNote The implementation is equivalent to the following: 175 * <pre> {@code 176 * if (map.get(key) == null) { 177 * V value = compute.apply(key); 178 * if (value != null) { 179 * map.put(key, value); 180 * } 181 * } 182 * return map.get(key); 183 * }</pre> 184 * @see Ctr#computeIfAbsent(Map, Object, ThrowingFunction) 185 */ 186 public static <K, V, E extends Throwable> V computeIfAbsent( 187 Map<K, V> map, K key, ThrowingSupplier<? extends V, E> compute) throws E { 188 return computeIfAbsent(map, key, unused -> compute.get()); 189 } 190 191 private static final class DefaultMap<K, V> extends HashMap<K, V> {} 192 193}