001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.collections; 018 019 020import java.util.*; 021import java.util.function.*; 022import static org.apache.juneau.commons.utils.Shorts.*; 023 024/** 025 * A fluent wrapper around an arbitrary map that provides convenient methods for adding entries. 026 * 027 * <p> 028 * This class wraps an underlying map and provides a fluent API for adding entries. All methods return 029 * <c>this</c> to allow method chaining. The underlying map can be any {@link Map} implementation. 030 * 031 * <h5 class='section'>Features:</h5> 032 * <ul class='spaced-list'> 033 * <li><b>Fluent API:</b> All methods return <c>this</c> for method chaining 034 * <li><b>Arbitrary Map Support:</b> Works with any map implementation 035 * <li><b>Conditional Adding:</b> Add entries conditionally based on boolean expressions 036 * <li><b>Transparent Interface:</b> Implements the full {@link Map} interface, so it can be used anywhere a map is expected 037 * </ul> 038 * 039 * <h5 class='section'>Usage:</h5> 040 * <p class='bjava'> 041 * <jc>// Create a FluentMap wrapping a LinkedHashMap</jc> 042 * FluentMap<String, String> <jv>map</jv> = <jk>new</jk> FluentMap<>(<jk>new</jk> LinkedHashMap<>()); 043 * 044 * <jv>map</jv> 045 * .a(<js>"key1"</js>, <js>"value1"</js>) 046 * .a(<js>"key2"</js>, <js>"value2"</js>) 047 * .ai(<jk>true</jk>, <js>"key3"</js>, <js>"value3"</js>) <jc>// Added</jc> 048 * .ai(<jk>false</jk>, <js>"key4"</js>, <js>"value4"</js>); <jc>// Not added</jc> 049 * 050 * <jc>// Add all entries from another map</jc> 051 * Map<String, String> <jv>other</jv> = Map.of(<js>"key5"</js>, <js>"value5"</js>, <js>"key6"</js>, <js>"value6"</js>); 052 * <jv>map</jv>.aa(<jv>other</jv>); 053 * </p> 054 * 055 * <h5 class='section'>Example - Conditional Building:</h5> 056 * <p class='bjava'> 057 * <jk>boolean</jk> <jv>includeDebug</jv> = <jk>true</jk>; 058 * <jk>boolean</jk> <jv>includeTest</jv> = <jk>false</jk>; 059 * 060 * FluentMap<String, String> <jv>config</jv> = <jk>new</jk> FluentMap<>(<jk>new</jk> LinkedHashMap<>()) 061 * .a(<js>"host"</js>, <js>"localhost"</js>) 062 * .a(<js>"port"</js>, <js>"8080"</js>) 063 * .ai(<jv>includeDebug</jv>, <js>"debug"</js>, <js>"true"</js>) <jc>// Added</jc> 064 * .ai(<jv>includeTest</jv>, <js>"test"</js>, <js>"true"</js>); <jc>// Not added</jc> 065 * </p> 066 * 067 * <h5 class='section'>Behavior Notes:</h5> 068 * <ul class='spaced-list'> 069 * <li>All map operations are delegated to the underlying map 070 * <li>The fluent methods ({@link #a(Object, Object)}, {@link #aa(Map)}, {@link #ai(boolean, Object, Object)}) return <c>this</c> for chaining 071 * <li>If a <jk>null</jk> map is passed to {@link #aa(Map)}, it is treated as a no-op 072 * <li>The underlying map is stored by reference (not copied), so modifications affect the original map 073 * </ul> 074 * 075 * <h5 class='section'>Thread Safety:</h5> 076 * <p> 077 * This class is not thread-safe unless the underlying map is thread-safe. If thread safety is required, 078 * use a thread-safe map type (e.g., {@link java.util.concurrent.ConcurrentHashMap}). 079 * 080 * <h5 class='section'>See Also:</h5> 081 * <ul> 082 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 083 * </ul> 084 * 085 * @param <K> The key type. 086 * @param <V> The value type. 087 */ 088@SuppressWarnings({ 089 "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants 090}) 091public class FluentMap<K,V> extends AbstractMap<K,V> { 092 093 private final Map<K,V> map; 094 095 /** 096 * Constructor. 097 * 098 * @param inner The underlying map to wrap. Must not be <jk>null</jk>. 099 */ 100 public FluentMap(Map<K,V> inner) { 101 this.map = reqnn("inner", inner); 102 } 103 104 /** 105 * Adds a single key-value pair to this map. 106 * 107 * <p> 108 * This is a convenience method that calls {@link #put(Object, Object)} and returns <c>this</c> 109 * for method chaining. 110 * 111 * <h5 class='section'>Example:</h5> 112 * <p class='bjava'> 113 * FluentMap<String, String> <jv>map</jv> = <jk>new</jk> FluentMap<>(<jk>new</jk> LinkedHashMap<>()); 114 * <jv>map</jv>.a(<js>"key1"</js>, <js>"value1"</js>).a(<js>"key2"</js>, <js>"value2"</js>); 115 * </p> 116 * 117 * @param key The key to add. 118 * @param value The value to add. 119 * @return This object for method chaining. 120 */ 121 public FluentMap<K,V> a(K key, V value) { 122 map.put(key, value); 123 return this; 124 } 125 126 /** 127 * Adds all entries from the specified map to this map. 128 * 129 * <p> 130 * This is a convenience method that calls {@link #putAll(Map)} and returns <c>this</c> 131 * for method chaining. If the specified map is <jk>null</jk>, this is a no-op. 132 * 133 * <h5 class='section'>Example:</h5> 134 * <p class='bjava'> 135 * FluentMap<String, String> <jv>map</jv> = <jk>new</jk> FluentMap<>(<jk>new</jk> LinkedHashMap<>()); 136 * Map<String, String> <jv>other</jv> = Map.of(<js>"key1"</js>, <js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>); 137 * <jv>map</jv>.aa(<jv>other</jv>).a(<js>"key3"</js>, <js>"value3"</js>); 138 * </p> 139 * 140 * @param m The map whose entries are to be added. Can be <jk>null</jk> (no-op). 141 * @return This object for method chaining. 142 */ 143 public FluentMap<K,V> aa(Map<? extends K,? extends V> m) { 144 if (m != null) 145 map.putAll(m); 146 return this; 147 } 148 149 /** 150 * Adds a key-value pair to this map if the specified boolean condition is <jk>true</jk>. 151 * 152 * <p> 153 * This method is useful for conditionally adding entries based on runtime conditions. 154 * If the condition is <jk>false</jk>, the entry is not added and this method returns <c>this</c> 155 * without modifying the map. 156 * 157 * <h5 class='section'>Example:</h5> 158 * <p class='bjava'> 159 * <jk>boolean</jk> <jv>includeDebug</jv> = <jk>true</jk>; 160 * <jk>boolean</jk> <jv>includeTest</jv> = <jk>false</jk>; 161 * 162 * FluentMap<String, String> <jv>map</jv> = <jk>new</jk> FluentMap<>(<jk>new</jk> LinkedHashMap<>()) 163 * .a(<js>"host"</js>, <js>"localhost"</js>) 164 * .ai(<jv>includeDebug</jv>, <js>"debug"</js>, <js>"true"</js>) <jc>// Added</jc> 165 * .ai(<jv>includeTest</jv>, <js>"test"</js>, <js>"true"</js>); <jc>// Not added</jc> 166 * </p> 167 * 168 * @param condition The condition to evaluate. If <jk>true</jk>, the entry is added; if <jk>false</jk>, it is not. 169 * @param key The key to add if the condition is <jk>true</jk>. 170 * @param value The value to add if the condition is <jk>true</jk>. 171 * @return This object for method chaining. 172 */ 173 public FluentMap<K,V> ai(boolean condition, K key, V value) { 174 if (condition) 175 map.put(key, value); 176 return this; 177 } 178 179 /** 180 * Adds a key-value pair to this map if the specified predicate returns <jk>true</jk> when applied to the value. 181 * 182 * <p> 183 * This method is useful for conditionally adding entries based on the value itself. 184 * The predicate is applied to the value, and if it returns <jk>true</jk>, the entry is added. 185 * If the predicate returns <jk>false</jk>, the entry is not added and this method returns <c>this</c> 186 * without modifying the map. 187 * 188 * <h5 class='section'>Example:</h5> 189 * <p class='bjava'> 190 * FluentMap<String, String> <jv>map</jv> = <jk>new</jk> FluentMap<>(<jk>new</jk> LinkedHashMap<>()) 191 * .a(<js>"host"</js>, <js>"localhost"</js>) 192 * .ai(s -> !s.isEmpty(), <js>"debug"</js>, <js>"true"</js>) <jc>// Added (value is not empty)</jc> 193 * .ai(s -> !s.isEmpty(), <js>"test"</js>, <js>""</js>); <jc>// Not added (value is empty)</jc> 194 * </p> 195 * 196 * @param predicate The predicate to test on the value. If it returns <jk>true</jk>, the entry is added; if <jk>false</jk>, it is not. 197 * @param key The key to add if the predicate returns <jk>true</jk>. 198 * @param value The value to add if the predicate returns <jk>true</jk>. 199 * @return This object for method chaining. 200 */ 201 public FluentMap<K,V> ai(Predicate<V> predicate, K key, V value) { 202 if (predicate.test(value)) 203 map.put(key, value); 204 return this; 205 } 206 207 @Override 208 public Set<Entry<K,V>> entrySet() { 209 return map.entrySet(); 210 } 211 212 @Override 213 public V get(Object key) { 214 return map.get(key); 215 } 216 217 @Override 218 public V put(K key, V value) { 219 return map.put(key, value); 220 } 221 222 @Override 223 public V remove(Object key) { 224 return map.remove(key); 225 } 226 227 @Override 228 public void putAll(Map<? extends K,? extends V> m) { 229 map.putAll(m); 230 } 231 232 @Override 233 public void clear() { 234 map.clear(); 235 } 236 237 @Override 238 public boolean containsKey(Object key) { 239 return map.containsKey(key); 240 } 241 242 @Override 243 public boolean containsValue(Object value) { 244 return map.containsValue(value); 245 } 246 247 @Override 248 public int size() { 249 return map.size(); 250 } 251 252 @Override 253 public boolean isEmpty() { 254 return map.isEmpty(); 255 } 256 257 @Override 258 public Set<K> keySet() { 259 return map.keySet(); 260 } 261 262 @Override 263 public Collection<V> values() { 264 return map.values(); 265 } 266 267 @Override 268 public String toString() { 269 return map.toString(); 270 } 271 272 @Override 273 public boolean equals(Object o) { 274 return map.equals(o); 275 } 276 277 @Override 278 public int hashCode() { 279 return map.hashCode(); 280 } 281} 282