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}