1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.functor.example;
18  
19  import static org.junit.Assert.assertEquals;
20  import static org.junit.Assert.assertNull;
21  import static org.junit.Assert.fail;
22  
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.apache.commons.functor.BinaryFunction;
32  import org.apache.commons.functor.BinaryProcedure;
33  import org.apache.commons.functor.Function;
34  import org.apache.commons.functor.Procedure;
35  import org.apache.commons.functor.UnaryFunction;
36  import org.apache.commons.functor.UnaryProcedure;
37  import org.apache.commons.functor.adapter.IgnoreLeftFunction;
38  import org.apache.commons.functor.core.Constant;
39  import org.apache.commons.functor.core.Identity;
40  import org.apache.commons.functor.core.IsInstance;
41  import org.apache.commons.functor.core.IsNull;
42  import org.apache.commons.functor.core.RightIdentity;
43  import org.apache.commons.functor.core.composite.Conditional;
44  import org.junit.Test;
45  
46  /*
47   * ----------------------------------------------------------------------------
48   * INTRODUCTION:
49   * ----------------------------------------------------------------------------
50   */
51  
52  /*
53   * In this example, we'll demonstrate how we can use "pluggable" functors
54   * to create specialized Map implementations via composition.
55   *
56   * All our specializations will use the same basic Map implementation.
57   * Once it is built, we'll only need to define the specialized behaviors.
58   */
59  
60  /**
61   * @version $Revision: 1171267 $ $Date: 2011-09-15 22:46:08 +0200 (Thu, 15 Sep 2011) $
62   * @author Rodney Waldhoff
63   */
64  @SuppressWarnings("unchecked")
65  public class FlexiMapExample {
66  
67      /*
68       * ----------------------------------------------------------------------------
69       * UNIT TESTS:
70       * ----------------------------------------------------------------------------
71       */
72  
73      /*
74       * In a "test first" style, let's first specify the Map behaviour we'd like
75       * to implement via unit tests.
76       */
77  
78      /*
79       * First, let's review the basic Map functionality.
80       */
81  
82      /*
83       * The basic Map interface lets one associate keys and values:
84       */
85      @Test
86      public void testBasicMap() {
87          /* (We'll define these make*Map functions below.) */
88          Map map = makeBasicMap();
89          Object key = "key";
90          Object value = new Integer(3);
91          map.put(key,value);
92          assertEquals(value, map.get(key) );
93      }
94  
95      /*
96       * If there is no value associated with a key,
97       * the basic Map will return null for that key:
98       */
99      @Test
100     public void testBasicMapReturnsNullForMissingKey() {
101         Map map = makeBasicMap();
102         assertNull( map.get("key") );
103     }
104 
105     /*
106      * One can also explicitly store a null value for
107      * some key:
108      */
109     @Test
110     public void testBasicMapAllowsNull() {
111         Map map = makeBasicMap();
112         Object key = "key";
113         Object value = null;
114         map.put(key,value);
115         assertNull( map.get(key) );
116     }
117 
118     /*
119      * The basic Map deals with Objects--it can store keys
120      * and values of multiple or differing types:
121      */
122     @Test
123     public void testBasicMapAllowsMultipleTypes() {
124         Map map = makeBasicMap();
125         map.put("key-1","value-1");
126         map.put(new Integer(2),"value-2");
127         map.put("key-3",new Integer(3));
128         map.put(new Integer(4),new Integer(4));
129 
130         assertEquals("value-1", map.get("key-1") );
131         assertEquals("value-2", map.get(new Integer(2)) );
132         assertEquals(new Integer(3), map.get("key-3") );
133         assertEquals(new Integer(4), map.get(new Integer(4)) );
134     }
135 
136     /*
137      * Finally, note that putting a second value for a given
138      * key will overwrite the first value--the basic Map only
139      * stores the most recently put value for each key:
140      */
141     @Test
142     public void testBasicMapStoresOnlyOneValuePerKey() {
143         Map map = makeBasicMap();
144 
145         assertNull( map.put("key","value-1") );
146         assertEquals("value-1", map.get("key") );
147         assertEquals("value-1", map.put("key","value-2"));
148         assertEquals("value-2", map.get("key") );
149     }
150 
151     /*
152      * Now let's look at some specializations of the Map behavior.
153      */
154 
155     /*
156      * One common specialization is to forbid null values,
157      * like our old friend Hashtable:
158      */
159     @Test
160     public void testForbidNull() {
161         Map map = makeNullForbiddenMap();
162 
163         map.put("key","value");
164         map.put("key2", new Integer(2) );
165         try {
166             map.put("key3",null);
167             fail("Expected NullPointerException");
168         } catch(NullPointerException e) {
169             // expected
170         }
171     }
172 
173     /*
174      * Alternatively, we may want to provide a default
175      * value to return when null is associated with some
176      * key. (This might be useful, for example, when the Map
177      * contains a counter--when there's no count yet, we'll
178      * want to treat it as zero.):
179      */
180     @Test
181     public void testNullDefaultsToZero() {
182         Map map = makeDefaultValueForNullMap(new Integer(0));
183         /*
184          * We expect 0 when no value has been associated with "key".
185          */
186         assertEquals( new Integer(0), map.get("key") );
187         /*
188          * We also expect 0 when a null value has been associated with "key".
189          */
190         map.put("key", null);
191         assertEquals( new Integer(0), map.get("key") );
192     }
193 
194     /*
195      * Another common specialization is to constrain the type of values
196      * that may be stored in the Map:
197      */
198     @Test
199     public void testIntegerValuesOnly() {
200         Map map = makeTypeConstrainedMap(Integer.class);
201         map.put("key", new Integer(2));
202         assertEquals( new Integer(2), map.get("key") );
203         try {
204             map.put("key2","value");
205             fail("Expected ClassCastException");
206         } catch(ClassCastException e) {
207             // expected
208         }
209     }
210 
211     /*
212      * A more interesting specialization is that used by the
213      * Apache Commons Collections MultiMap class, which allows
214      * one to associate multiple values with each key.  The put
215      * function still accepts a single value, but the get function
216      * will return a Collection of values.  Associating multiple values
217      * with a key adds to that collection, rather than overwriting the
218      * previous value:
219      */
220     @Test
221     public void testMultiMap() {
222         Map map = makeMultiMap();
223 
224         map.put("key", "value 1");
225 
226         {
227             Collection result = (Collection)(map.get("key"));
228             assertEquals(1,result.size());
229             assertEquals("value 1", result.iterator().next());
230         }
231 
232         map.put("key", "value 2");
233 
234         {
235             Collection result = (Collection)(map.get("key"));
236             assertEquals(2,result.size());
237             Iterator iter = result.iterator();
238             assertEquals("value 1", iter.next());
239             assertEquals("value 2", iter.next());
240         }
241 
242         map.put("key", "value 3");
243 
244         {
245             Collection result = (Collection)(map.get("key"));
246             assertEquals(3,result.size());
247             Iterator iter = result.iterator();
248             assertEquals("value 1", iter.next());
249             assertEquals("value 2", iter.next());
250             assertEquals("value 3", iter.next());
251         }
252 
253     }
254 
255     /*
256      * Here's another variation on the MultiMap theme.
257      * Rather than adding elements to a Collection, let's
258      * concatenate String values together, delimited by commas.
259      * (Such a Map might be used by the Commons Collection's
260      * ExtendedProperties type.):
261      */
262     @Test
263     public void testStringConcatMap() {
264         Map map = makeStringConcatMap();
265         map.put("key", "value 1");
266         assertEquals("value 1",map.get("key"));
267         map.put("key", "value 2");
268         assertEquals("value 1, value 2",map.get("key"));
269         map.put("key", "value 3");
270         assertEquals("value 1, value 2, value 3",map.get("key"));
271     }
272 
273     /*
274      * ----------------------------------------------------------------------------
275      * THE GENERIC MAP IMPLEMENTATION:
276      * ----------------------------------------------------------------------------
277      */
278 
279     /*
280      * How can one Map implementation support all these behaviors?
281      * Using functors and composition, of course.
282      *
283      * In order to keep our example small, we'll just consider the
284      * primary Map.put and Map.get methods here, although the remaining
285      * Map methods could be handled similiarly.
286      */
287     static class FlexiMap implements Map {
288 
289         /*
290          * Our FlexiMap will accept two BinaryFunctions, one
291          * that's used to transform objects being put into the Map,
292          * and one that's used to transforms objects being retrieved
293          * from the map.
294          */
295         public FlexiMap(BinaryFunction putfn, BinaryFunction getfn) {
296             onPut = null == putfn ? RightIdentity.function() : putfn;
297             onGet = null == getfn ? RightIdentity.function() : getfn;
298             proxiedMap = new HashMap();
299         }
300 
301 
302         /*
303          * The arguments to our "onGet" function will be the
304          * key and the value associated with that key in the
305          * underlying Map.  We'll return whatever the function
306          * returns.
307          */
308         public Object get(Object key) {
309             return onGet.evaluate( key, proxiedMap.get(key) );
310         }
311 
312         /*
313          * The arguments to our "onPut" function will be the
314          * value previously associated with that key (if any),
315          * as well as the new value being associated with that key.
316          *
317          * Since put returns the previously associated value,
318          * we'll invoke onGet here as well.
319          */
320         public Object put(Object key, Object value) {
321             Object oldvalue = proxiedMap.get(key);
322             proxiedMap.put(key, onPut.evaluate(oldvalue, value));
323             return onGet.evaluate(key,oldvalue);
324         }
325 
326        /*
327         * We'll skip the remaining Map methods for now.
328         */
329 
330         public void clear() {
331             throw new UnsupportedOperationException("Left as an exercise for the reader.");
332         }
333 
334         public boolean containsKey(Object key) {
335             throw new UnsupportedOperationException("Left as an exercise for the reader.");
336         }
337 
338         public boolean containsValue(Object value) {
339             throw new UnsupportedOperationException("Left as an exercise for the reader.");
340         }
341 
342         public Set entrySet() {
343             throw new UnsupportedOperationException("Left as an exercise for the reader.");
344         }
345 
346         public boolean isEmpty() {
347             throw new UnsupportedOperationException("Left as an exercise for the reader.");
348         }
349 
350         public Set keySet() {
351             throw new UnsupportedOperationException("Left as an exercise for the reader.");
352         }
353 
354         public void putAll(Map t) {
355             throw new UnsupportedOperationException("Left as an exercise for the reader.");
356         }
357 
358         public Object remove(Object key) {
359             throw new UnsupportedOperationException("Left as an exercise for the reader.");
360         }
361 
362         public int size() {
363             throw new UnsupportedOperationException("Left as an exercise for the reader.");
364         }
365 
366         public Collection values() {
367             throw new UnsupportedOperationException("Left as an exercise for the reader.");
368         }
369 
370         private BinaryFunction onPut = null;
371         private BinaryFunction onGet = null;
372         private Map proxiedMap = null;
373     }
374 
375     /*
376      * ----------------------------------------------------------------------------
377      * MAP SPECIALIZATIONS:
378      * ----------------------------------------------------------------------------
379      */
380 
381     /*
382      * For the "basic" Map, we'll simply create a HashMap.
383      * Note that using a RightIdentity for onPut and onGet
384      * would yield the same behavior.
385      */
386     private Map makeBasicMap() {
387         return new HashMap();
388     }
389 
390     /*
391      * To prohibit null values, we'll only need to
392      * provide an onPut function.
393      */
394     private Map makeNullForbiddenMap() {
395         return new FlexiMap(
396             /*
397              * We simply ignore the left-hand argument,
398              */
399             IgnoreLeftFunction.adapt(
400                 /*
401                  * and for the right-hand,
402                  */
403                 Conditional.function(
404                     /*
405                      * we'll test for null,
406                      */
407                     IsNull.instance(),
408                     /*
409                      * throwing a NullPointerException when the value is null,
410                      */
411                     throwNPE,
412                     /*
413                      * and passing through all non-null values.
414                      */
415                     Identity.instance()
416                 )
417             ),
418             null
419         );
420     }
421 
422     /*
423      * To provide a default for null values, we'll only need to
424      * provide an onGet function, simliar to the onPut method used
425      * above.
426      */
427     private Map makeDefaultValueForNullMap(Object defaultValue) {
428         return new FlexiMap(
429             null,
430             /*
431              * We ignore the left-hand argument,
432              */
433             IgnoreLeftFunction.adapt(
434                 /*
435                  * and for the right-hand,
436                  */
437                 Conditional.function(
438                     /*
439                      * we'll test for null,
440                      */
441                     IsNull.instance(),
442                     /*
443                      * returning our default when the value is otherwise null,
444                      */
445                     new Constant(defaultValue),
446                     /*
447                      * and passing through all non-null values.
448                      */
449                     Identity.instance()
450                 )
451             )
452         );
453     }
454 
455     /*
456      * To constrain the value types, we'll
457      * provide an onPut function,
458      */
459     private Map makeTypeConstrainedMap(Class clazz) {
460         return new FlexiMap(
461             /*
462              * ignore the left-hand argument,
463              */
464             IgnoreLeftFunction.adapt(
465                 Conditional.function(
466                     /*
467                      * we'll test the type of the right-hand argument,
468                      */
469                     IsInstance.of(clazz),
470                     /*
471                      * and either pass the given value through,
472                      */
473                     Identity.instance(),
474                     /*
475                      * or throw a ClassCastException.
476                      */
477                     throwCCE
478                 )
479             ),
480             null
481         );
482     }
483 
484     /*
485      * The MultiMap is a bit more interesting, since we'll
486      * need to consider both the old and new values during
487      * onPut:
488      */
489     private Map makeMultiMap() {
490         return new FlexiMap(
491             new BinaryFunction() {
492                 public Object evaluate(Object oldval, Object newval) {
493                     List list = null;
494                     if (null == oldval) {
495                         list = new ArrayList();
496                     } else {
497                         list = (List) oldval;
498                     }
499                     list.add(newval);
500                     return list;
501                 }
502             },
503             null
504         );
505     }
506 
507     /*
508      * The StringConcatMap is more interesting still.
509      */
510     private Map makeStringConcatMap() {
511         return new FlexiMap(
512             /*
513              * The onPut function looks similiar to the MultiMap
514              * method:
515              */
516             new BinaryFunction() {
517                 public Object evaluate(Object oldval, Object newval) {
518                     StringBuffer buf = null;
519                     if (null == oldval) {
520                         buf = new StringBuffer();
521                     } else {
522                         buf = (StringBuffer) oldval;
523                         buf.append(", ");
524                     }
525                     buf.append(newval);
526                     return buf;
527                 }
528             },
529             /*
530              * but we'll also need an onGet functor to convert
531              * the StringBuffer to a String:
532              */
533             new BinaryFunction() {
534                 public Object evaluate(Object key, Object val) {
535                     if (null == val) {
536                         return null;
537                     } else {
538                         return ((StringBuffer) val).toString();
539                     }
540                 }
541             }
542         );
543     }
544 
545     /*
546      * (This "UniversalFunctor" type provides a functor
547      * that takes the same action regardless of the number of
548      * parameters. We used it above to throw Exceptions when
549      * needed.)
550      */
551 
552     private abstract class UniversalFunctor implements
553         Procedure, UnaryProcedure, BinaryProcedure,
554         Function, UnaryFunction, BinaryFunction {
555         public abstract void run();
556 
557         public void run(Object obj) {
558             run();
559         }
560         public void run(Object left, Object right) {
561             run();
562         }
563         public Object evaluate() {
564             run();
565             return null;
566         }
567         public Object evaluate(Object obj) {
568             run();
569             return null;
570         }
571         public Object evaluate(Object left, Object right) {
572             run();
573             return null;
574         }
575     }
576 
577     private UniversalFunctor throwNPE = new UniversalFunctor() {
578         public void run() {
579             throw new NullPointerException();
580         }
581     };
582 
583     private UniversalFunctor throwCCE = new UniversalFunctor() {
584         public void run() {
585             throw new ClassCastException();
586         }
587     };
588 
589 }