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
019import static org.apache.juneau.commons.utils.AssertionUtils.*;
020
021import java.util.*;
022import java.util.function.Predicate;
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&lt;String, String&gt; <jv>map</jv> = <jk>new</jk> FluentMap&lt;&gt;(<jk>new</jk> LinkedHashMap&lt;&gt;());
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&lt;String, String&gt; <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&lt;String, String&gt; <jv>config</jv> = <jk>new</jk> FluentMap&lt;&gt;(<jk>new</jk> LinkedHashMap&lt;&gt;())
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 &gt; juneau-commons</a>
083 * </ul>
084 *
085 * @param <K> The key type.
086 * @param <V> The value type.
087 */
088public class FluentMap<K,V> extends AbstractMap<K,V> {
089
090   private final Map<K,V> map;
091
092   /**
093    * Constructor.
094    *
095    * @param inner The underlying map to wrap. Must not be <jk>null</jk>.
096    */
097   public FluentMap(Map<K,V> inner) {
098      this.map = assertArgNotNull("inner", inner);
099   }
100
101   /**
102    * Adds a single key-value pair to this map.
103    *
104    * <p>
105    * This is a convenience method that calls {@link #put(Object, Object)} and returns <c>this</c>
106    * for method chaining.
107    *
108    * <h5 class='section'>Example:</h5>
109    * <p class='bjava'>
110    *    FluentMap&lt;String, String&gt; <jv>map</jv> = <jk>new</jk> FluentMap&lt;&gt;(<jk>new</jk> LinkedHashMap&lt;&gt;());
111    *    <jv>map</jv>.a(<js>"key1"</js>, <js>"value1"</js>).a(<js>"key2"</js>, <js>"value2"</js>);
112    * </p>
113    *
114    * @param key The key to add.
115    * @param value The value to add.
116    * @return This object for method chaining.
117    */
118   public FluentMap<K,V> a(K key, V value) {
119      map.put(key, value);
120      return this;
121   }
122
123   /**
124    * Adds all entries from the specified map to this map.
125    *
126    * <p>
127    * This is a convenience method that calls {@link #putAll(Map)} and returns <c>this</c>
128    * for method chaining. If the specified map is <jk>null</jk>, this is a no-op.
129    *
130    * <h5 class='section'>Example:</h5>
131    * <p class='bjava'>
132    *    FluentMap&lt;String, String&gt; <jv>map</jv> = <jk>new</jk> FluentMap&lt;&gt;(<jk>new</jk> LinkedHashMap&lt;&gt;());
133    *    Map&lt;String, String&gt; <jv>other</jv> = Map.of(<js>"key1"</js>, <js>"value1"</js>, <js>"key2"</js>, <js>"value2"</js>);
134    *    <jv>map</jv>.aa(<jv>other</jv>).a(<js>"key3"</js>, <js>"value3"</js>);
135    * </p>
136    *
137    * @param m The map whose entries are to be added. Can be <jk>null</jk> (no-op).
138    * @return This object for method chaining.
139    */
140   public FluentMap<K,V> aa(Map<? extends K, ? extends V> m) {
141      if (m != null)
142         map.putAll(m);
143      return this;
144   }
145
146   /**
147    * Adds a key-value pair to this map if the specified boolean condition is <jk>true</jk>.
148    *
149    * <p>
150    * This method is useful for conditionally adding entries based on runtime conditions.
151    * If the condition is <jk>false</jk>, the entry is not added and this method returns <c>this</c>
152    * without modifying the map.
153    *
154    * <h5 class='section'>Example:</h5>
155    * <p class='bjava'>
156    *    <jk>boolean</jk> <jv>includeDebug</jv> = <jk>true</jk>;
157    *    <jk>boolean</jk> <jv>includeTest</jv> = <jk>false</jk>;
158    *
159    *    FluentMap&lt;String, String&gt; <jv>map</jv> = <jk>new</jk> FluentMap&lt;&gt;(<jk>new</jk> LinkedHashMap&lt;&gt;())
160    *       .a(<js>"host"</js>, <js>"localhost"</js>)
161    *       .ai(<jv>includeDebug</jv>, <js>"debug"</js>, <js>"true"</js>)   <jc>// Added</jc>
162    *       .ai(<jv>includeTest</jv>, <js>"test"</js>, <js>"true"</js>);    <jc>// Not added</jc>
163    * </p>
164    *
165    * @param condition The condition to evaluate. If <jk>true</jk>, the entry is added; if <jk>false</jk>, it is not.
166    * @param key The key to add if the condition is <jk>true</jk>.
167    * @param value The value to add if the condition is <jk>true</jk>.
168    * @return This object for method chaining.
169    */
170   public FluentMap<K,V> ai(boolean condition, K key, V value) {
171      if (condition)
172         map.put(key, value);
173      return this;
174   }
175
176   /**
177    * Adds a key-value pair to this map if the specified predicate returns <jk>true</jk> when applied to the value.
178    *
179    * <p>
180    * This method is useful for conditionally adding entries based on the value itself.
181    * The predicate is applied to the value, and if it returns <jk>true</jk>, the entry is added.
182    * If the predicate returns <jk>false</jk>, the entry is not added and this method returns <c>this</c>
183    * without modifying the map.
184    *
185    * <h5 class='section'>Example:</h5>
186    * <p class='bjava'>
187    *    FluentMap&lt;String, String&gt; <jv>map</jv> = <jk>new</jk> FluentMap&lt;&gt;(<jk>new</jk> LinkedHashMap&lt;&gt;())
188    *       .a(<js>"host"</js>, <js>"localhost"</js>)
189    *       .ai(s -&gt; !s.isEmpty(), <js>"debug"</js>, <js>"true"</js>)   <jc>// Added (value is not empty)</jc>
190    *       .ai(s -&gt; !s.isEmpty(), <js>"test"</js>, <js>""</js>);       <jc>// Not added (value is empty)</jc>
191    * </p>
192    *
193    * @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.
194    * @param key The key to add if the predicate returns <jk>true</jk>.
195    * @param value The value to add if the predicate returns <jk>true</jk>.
196    * @return This object for method chaining.
197    */
198   public FluentMap<K,V> ai(Predicate<V> predicate, K key, V value) {
199      if (predicate.test(value))
200         map.put(key, value);
201      return this;
202   }
203
204   @Override
205   public Set<Entry<K,V>> entrySet() {
206      return map.entrySet();
207   }
208
209   @Override
210   public V get(Object key) {
211      return map.get(key);
212   }
213
214   @Override
215   public V put(K key, V value) {
216      return map.put(key, value);
217   }
218
219   @Override
220   public V remove(Object key) {
221      return map.remove(key);
222   }
223
224   @Override
225   public void putAll(Map<? extends K, ? extends V> m) {
226      map.putAll(m);
227   }
228
229   @Override
230   public void clear() {
231      map.clear();
232   }
233
234   @Override
235   public boolean containsKey(Object key) {
236      return map.containsKey(key);
237   }
238
239   @Override
240   public boolean containsValue(Object value) {
241      return map.containsValue(value);
242   }
243
244   @Override
245   public int size() {
246      return map.size();
247   }
248
249   @Override
250   public boolean isEmpty() {
251      return map.isEmpty();
252   }
253
254   @Override
255   public Set<K> keySet() {
256      return map.keySet();
257   }
258
259   @Override
260   public Collection<V> values() {
261      return map.values();
262   }
263
264   @Override
265   public String toString() {
266      return map.toString();
267   }
268
269   @Override
270   public boolean equals(Object o) {
271      return map.equals(o);
272   }
273
274   @Override
275   public int hashCode() {
276      return map.hashCode();
277   }
278}
279