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.commons.functor.example;
018
019import static org.junit.Assert.assertEquals;
020import static org.junit.Assert.assertNull;
021import static org.junit.Assert.fail;
022
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.functor.BinaryFunction;
032import org.apache.commons.functor.BinaryProcedure;
033import org.apache.commons.functor.NullaryFunction;
034import org.apache.commons.functor.NullaryProcedure;
035import org.apache.commons.functor.Function;
036import org.apache.commons.functor.Procedure;
037import org.apache.commons.functor.adapter.IgnoreLeftFunction;
038import org.apache.commons.functor.core.Constant;
039import org.apache.commons.functor.core.Identity;
040import org.apache.commons.functor.core.IsInstance;
041import org.apache.commons.functor.core.IsNull;
042import org.apache.commons.functor.core.RightIdentity;
043import org.apache.commons.functor.core.composite.Conditional;
044import org.junit.Test;
045
046/*
047 * ----------------------------------------------------------------------------
048 * INTRODUCTION:
049 * ----------------------------------------------------------------------------
050 */
051
052/*
053 * In this example, we'll demonstrate how we can use "pluggable" functors
054 * to create specialized Map implementations via composition.
055 *
056 * All our specializations will use the same basic Map implementation.
057 * Once it is built, we'll only need to define the specialized behaviors.
058 */
059
060/**
061 * @version $Revision: 1541658 $ $Date: 2013-11-13 19:54:05 +0100 (Mi, 13 Nov 2013) $
062 */
063public class FlexiMapExample {
064
065    /*
066     * ---------------------------------------------------------------------------- UNIT TESTS:
067     * ----------------------------------------------------------------------------
068     */
069
070    /*
071     * In a "test first" style, let's first specify the Map behaviour we'd like to implement via unit tests.
072     */
073
074    /*
075     * First, let's review the basic Map functionality.
076     */
077
078    /*
079     * The basic Map interface lets one associate keys and values:
080     */
081    @Test
082    public void testBasicMap() {
083        /* (We'll define these make*Map functions below.) */
084        Map<Object, Object> map = makeBasicMap();
085        Object key = "key";
086        Object value = Integer.valueOf(3);
087        map.put(key, value);
088        assertEquals(value, map.get(key));
089    }
090
091    /*
092     * If there is no value associated with a key, the basic Map will return null for that key:
093     */
094    @Test
095    public void testBasicMapReturnsNullForMissingKey() {
096        Map<Object, Object> map = makeBasicMap();
097        assertNull(map.get("key"));
098    }
099
100    /*
101     * One can also explicitly store a null value for some key:
102     */
103    @Test
104    public void testBasicMapAllowsNull() {
105        Map<Object, Object> map = makeBasicMap();
106        Object key = "key";
107        Object value = null;
108        map.put(key, value);
109        assertNull(map.get(key));
110    }
111
112    /*
113     * The basic Map deals with Objects--it can store keys and values of multiple or differing types:
114     */
115    @Test
116    public void testBasicMapAllowsMultipleTypes() {
117        Map<Object, Object> map = makeBasicMap();
118        map.put("key-1", "value-1");
119        map.put(Integer.valueOf(2), "value-2");
120        map.put("key-3", Integer.valueOf(3));
121        map.put(Integer.valueOf(4), Integer.valueOf(4));
122
123        assertEquals("value-1", map.get("key-1"));
124        assertEquals("value-2", map.get(Integer.valueOf(2)));
125        assertEquals(Integer.valueOf(3), map.get("key-3"));
126        assertEquals(Integer.valueOf(4), map.get(Integer.valueOf(4)));
127    }
128
129    /*
130     * Finally, note that putting a second value for a given key will overwrite the first value--the basic Map only
131     * stores the most recently put value for each key:
132     */
133    @Test
134    public void testBasicMapStoresOnlyOneValuePerKey() {
135        Map<Object, Object> map = makeBasicMap();
136
137        assertNull(map.put("key", "value-1"));
138        assertEquals("value-1", map.get("key"));
139        assertEquals("value-1", map.put("key", "value-2"));
140        assertEquals("value-2", map.get("key"));
141    }
142
143    /*
144     * Now let's look at some specializations of the Map behavior.
145     */
146
147    /*
148     * One common specialization is to forbid null values, like our old friend Hashtable:
149     */
150    @Test
151    public void testForbidNull() {
152        Map<Object, Object> map = makeNullForbiddenMap();
153
154        map.put("key", "value");
155        map.put("key2", Integer.valueOf(2));
156        try {
157            map.put("key3", null);
158            fail("Expected NullPointerException");
159        } catch (NullPointerException e) {
160            // expected
161        }
162    }
163
164    /*
165     * Alternatively, we may want to provide a default value to return when null is associated with some key. (This
166     * might be useful, for example, when the Map contains a counter--when there's no count yet, we'll want to treat it
167     * as zero.):
168     */
169    @Test
170    public void testNullDefaultsToZero() {
171        Map<Object, Object> map = makeDefaultValueForNullMap(Integer.valueOf(0));
172        /*
173         * We expect 0 when no value has been associated with "key".
174         */
175        assertEquals(Integer.valueOf(0), map.get("key"));
176        /*
177         * We also expect 0 when a null value has been associated with "key".
178         */
179        map.put("key", null);
180        assertEquals(Integer.valueOf(0), map.get("key"));
181    }
182
183    /*
184     * Another common specialization is to constrain the type of values that may be stored in the Map:
185     */
186    @Test
187    public void testIntegerValuesOnly() {
188        Map<Object, Object> map = makeTypeConstrainedMap(Integer.class);
189        map.put("key", Integer.valueOf(2));
190        assertEquals(Integer.valueOf(2), map.get("key"));
191        try {
192            map.put("key2", "value");
193            fail("Expected ClassCastException");
194        } catch (ClassCastException e) {
195            // expected
196        }
197    }
198
199    /*
200     * A more interesting specialization is that used by the Apache Commons Collections MultiMap class, which allows one
201     * to associate multiple values with each key. The put function still accepts a single value, but the get function
202     * will return a Collection of values. Associating multiple values with a key adds to that collection, rather than
203     * overwriting the previous value:
204     */
205    @SuppressWarnings("unchecked")
206    @Test
207    public void testMultiMap() {
208        Map<Object, Object> map = makeMultiMap();
209
210        map.put("key", "value 1");
211
212        {
213            Collection<Object> result = (Collection<Object>) (map.get("key"));
214            assertEquals(1, result.size());
215            assertEquals("value 1", result.iterator().next());
216        }
217
218        map.put("key", "value 2");
219
220        {
221            Collection<Object> result = (Collection<Object>) (map.get("key"));
222            assertEquals(2, result.size());
223            Iterator<Object> iter = result.iterator();
224            assertEquals("value 1", iter.next());
225            assertEquals("value 2", iter.next());
226        }
227
228        map.put("key", "value 3");
229
230        {
231            Collection<Object> result = (Collection<Object>) (map.get("key"));
232            assertEquals(3, result.size());
233            Iterator<Object> iter = result.iterator();
234            assertEquals("value 1", iter.next());
235            assertEquals("value 2", iter.next());
236            assertEquals("value 3", iter.next());
237        }
238
239    }
240
241    /*
242     * Here's another variation on the MultiMap theme. Rather than adding elements to a Collection, let's concatenate
243     * String values together, delimited by commas. (Such a Map might be used by the Commons Collection's
244     * ExtendedProperties type.):
245     */
246    @Test
247    public void testStringConcatMap() {
248        Map<Object, Object> map = makeStringConcatMap();
249        map.put("key", "value 1");
250        assertEquals("value 1", map.get("key"));
251        map.put("key", "value 2");
252        assertEquals("value 1, value 2", map.get("key"));
253        map.put("key", "value 3");
254        assertEquals("value 1, value 2, value 3", map.get("key"));
255    }
256
257    /*
258     * ---------------------------------------------------------------------------- THE GENERIC MAP IMPLEMENTATION:
259     * ----------------------------------------------------------------------------
260     */
261
262    /*
263     * How can one Map implementation support all these behaviors? Using functors and composition, of course.
264     * 
265     * In order to keep our example small, we'll just consider the primary Map.put and Map.get methods here, although
266     * the remaining Map methods could be handled similiarly.
267     */
268    static class FlexiMap implements Map<Object, Object> {
269
270        /*
271         * Our FlexiMap will accept two BinaryFunctions, one that's used to transform objects being put into the Map,
272         * and one that's used to transforms objects being retrieved from the map.
273         */
274        public FlexiMap(BinaryFunction<Object, Object, Object> putfn, BinaryFunction<Object, Object, Object> getfn) {
275            onPut = null == putfn ? RightIdentity.function() : putfn;
276            onGet = null == getfn ? RightIdentity.function() : getfn;
277            proxiedMap = new HashMap<Object, Object>();
278        }
279
280        /*
281         * The arguments to our "onGet" function will be the key and the value associated with that key in the
282         * underlying Map. We'll return whatever the function returns.
283         */
284        public Object get(Object key) {
285            return onGet.evaluate(key, proxiedMap.get(key));
286        }
287
288        /*
289         * The arguments to our "onPut" function will be the value previously associated with that key (if any), as well
290         * as the new value being associated with that key.
291         * 
292         * Since put returns the previously associated value, we'll invoke onGet here as well.
293         */
294        public Object put(Object key, Object value) {
295            Object oldvalue = proxiedMap.get(key);
296            proxiedMap.put(key, onPut.evaluate(oldvalue, value));
297            return onGet.evaluate(key, oldvalue);
298        }
299
300        /*
301         * We'll skip the remaining Map methods for now.
302         */
303
304        public void clear() {
305            throw new UnsupportedOperationException("Left as an exercise for the reader.");
306        }
307
308        public boolean containsKey(Object key) {
309            throw new UnsupportedOperationException("Left as an exercise for the reader.");
310        }
311
312        public boolean containsValue(Object value) {
313            throw new UnsupportedOperationException("Left as an exercise for the reader.");
314        }
315
316        public Set<Map.Entry<Object, Object>> entrySet() {
317            throw new UnsupportedOperationException("Left as an exercise for the reader.");
318        }
319
320        public boolean isEmpty() {
321            throw new UnsupportedOperationException("Left as an exercise for the reader.");
322        }
323
324        public Set<Object> keySet() {
325            throw new UnsupportedOperationException("Left as an exercise for the reader.");
326        }
327
328        public void putAll(Map<?, ?> t) {
329            throw new UnsupportedOperationException("Left as an exercise for the reader.");
330        }
331
332        public Object remove(Object key) {
333            throw new UnsupportedOperationException("Left as an exercise for the reader.");
334        }
335
336        public int size() {
337            throw new UnsupportedOperationException("Left as an exercise for the reader.");
338        }
339
340        public Collection<Object> values() {
341            throw new UnsupportedOperationException("Left as an exercise for the reader.");
342        }
343
344        private BinaryFunction<Object, Object, Object> onPut = null;
345        private BinaryFunction<Object, Object, Object> onGet = null;
346        private Map<Object, Object> proxiedMap = null;
347    }
348
349    /*
350     * ---------------------------------------------------------------------------- MAP SPECIALIZATIONS:
351     * ----------------------------------------------------------------------------
352     */
353
354    /*
355     * For the "basic" Map, we'll simply create a HashMap. Note that using a RightIdentity for onPut and onGet would
356     * yield the same behavior.
357     */
358    private Map<Object, Object> makeBasicMap() {
359        return new HashMap<Object, Object>();
360    }
361
362    /*
363     * To prohibit null values, we'll only need to provide an onPut function.
364     */
365    private Map<Object, Object> makeNullForbiddenMap() {
366        return new FlexiMap(
367        /*
368         * We simply ignore the left-hand argument,
369         */
370        IgnoreLeftFunction.adapt(
371        /*
372         * and for the right-hand,
373         */
374        Conditional.function(
375        /*
376         * we'll test for null,
377         */
378        IsNull.instance(),
379        /*
380         * throwing a NullPointerException when the value is null,
381         */
382        throwNPE,
383        /*
384         * and passing through all non-null values.
385         */
386        Identity.instance())), null);
387    }
388
389    /*
390     * To provide a default for null values, we'll only need to provide an onGet function, simliar to the onPut method
391     * used above.
392     */
393    private Map<Object, Object> makeDefaultValueForNullMap(Object defaultValue) {
394        return new FlexiMap(null,
395        /*
396         * We ignore the left-hand argument,
397         */
398        IgnoreLeftFunction.adapt(
399        /*
400         * and for the right-hand,
401         */
402        Conditional.function(
403        /*
404         * we'll test for null,
405         */
406        IsNull.instance(),
407        /*
408         * returning our default when the value is otherwise null,
409         */
410        new Constant<Object>(defaultValue),
411        /*
412         * and passing through all non-null values.
413         */
414        Identity.instance())));
415    }
416
417    /*
418     * To constrain the value types, we'll provide an onPut function,
419     */
420    private Map<Object, Object> makeTypeConstrainedMap(Class<?> clazz) {
421        return new FlexiMap(
422        /*
423         * ignore the left-hand argument,
424         */
425        IgnoreLeftFunction.adapt(Conditional.function(
426        /*
427         * we'll test the type of the right-hand argument,
428         */
429        IsInstance.of(clazz),
430        /*
431         * and either pass the given value through,
432         */
433        Identity.instance(),
434        /*
435         * or throw a ClassCastException.
436         */
437        throwCCE)), null);
438    }
439
440    /*
441     * The MultiMap is a bit more interesting, since we'll need to consider both the old and new values during onPut:
442     */
443    private Map<Object, Object> makeMultiMap() {
444        return new FlexiMap(new BinaryFunction<Object, Object, Object>() {
445            @SuppressWarnings("unchecked")
446            public Object evaluate(Object oldval, Object newval) {
447                List<Object> list = null;
448                if (null == oldval) {
449                    list = new ArrayList<Object>();
450                } else {
451                    list = (List<Object>) oldval;
452                }
453                list.add(newval);
454                return list;
455            }
456        }, null);
457    }
458
459    /*
460     * The StringConcatMap is more interesting still.
461     */
462    private Map<Object, Object> makeStringConcatMap() {
463        return new FlexiMap(
464        /*
465         * The onPut function looks similiar to the MultiMap method:
466         */
467        new BinaryFunction<Object, Object, Object>() {
468            public Object evaluate(Object oldval, Object newval) {
469                StringBuilder buf = null;
470                if (null == oldval) {
471                    buf = new StringBuilder();
472                } else {
473                    buf = (StringBuilder) oldval;
474                    buf.append(", ");
475                }
476                buf.append(newval);
477                return buf;
478            }
479        },
480        /*
481         * but we'll also need an onGet functor to convert the StringBuilder to a String:
482         */
483        new BinaryFunction<Object, Object, Object>() {
484            public Object evaluate(Object key, Object val) {
485                if (null == val) {
486                    return null;
487                } else {
488                    return ((StringBuilder) val).toString();
489                }
490            }
491        });
492    }
493
494    /*
495     * (This "UniversalFunctor" type provides a functor that takes the same action regardless of the number of
496     * parameters. We used it above to throw Exceptions when needed.)
497     */
498
499    private abstract class UniversalFunctor implements NullaryProcedure, Procedure<Object>,
500        BinaryProcedure<Object, Object>, NullaryFunction<Object>, Function<Object, Object>,
501        BinaryFunction<Object, Object, Object> {
502        public abstract void run();
503
504        public void run(Object obj) {
505            run();
506        }
507
508        public void run(Object left, Object right) {
509            run();
510        }
511
512        public Object evaluate() {
513            run();
514            return null;
515        }
516
517        public Object evaluate(Object obj) {
518            run();
519            return null;
520        }
521
522        public Object evaluate(Object left, Object right) {
523            run();
524            return null;
525        }
526    }
527
528    private UniversalFunctor throwNPE = new UniversalFunctor() {
529        @Override
530        public void run() {
531            throw new NullPointerException();
532        }
533    };
534
535    private UniversalFunctor throwCCE = new UniversalFunctor() {
536        @Override
537        public void run() {
538            throw new ClassCastException();
539        }
540    };
541
542}