001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.utils; 014 015import static org.apache.juneau.internal.StringUtils.*; 016import static java.lang.Character.*; 017 018import java.lang.reflect.*; 019import java.util.*; 020 021import org.apache.juneau.*; 022 023/** 024 * Allows arbitrary objects to be mapped to classes and methods base on class/method name keys. 025 * 026 * <p> 027 * The valid pattern matches are: 028 * <ul class='spaced-list'> 029 * <li>Classes: 030 * <ul> 031 * <li>Fully qualified: 032 * <ul> 033 * <li><js>"com.foo.MyClass"</js> 034 * </ul> 035 * <li>Fully qualified inner class: 036 * <ul> 037 * <li><js>"com.foo.MyClass$Inner1$Inner2"</js> 038 * </ul> 039 * <li>Simple: 040 * <ul> 041 * <li><js>"MyClass"</js> 042 * </ul> 043 * <li>Simple inner: 044 * <ul> 045 * <li><js>"MyClass$Inner1$Inner2"</js> 046 * <li><js>"Inner1$Inner2"</js> 047 * <li><js>"Inner2"</js> 048 * </ul> 049 * </ul> 050 * <li>Methods: 051 * <ul> 052 * <li>Fully qualified with args: 053 * <ul> 054 * <li><js>"com.foo.MyClass.myMethod(String,int)"</js> 055 * <li><js>"com.foo.MyClass.myMethod(java.lang.String,int)"</js> 056 * <li><js>"com.foo.MyClass.myMethod()"</js> 057 * </ul> 058 * <li>Fully qualified: 059 * <ul> 060 * <li><js>"com.foo.MyClass.myMethod"</js> 061 * </ul> 062 * <li>Simple with args: 063 * <ul> 064 * <li><js>"MyClass.myMethod(String,int)"</js> 065 * <li><js>"MyClass.myMethod(java.lang.String,int)"</js> 066 * <li><js>"MyClass.myMethod()"</js> 067 * </ul> 068 * <li>Simple: 069 * <ul> 070 * <li><js>"MyClass.myMethod"</js> 071 * </ul> 072 * <li>Simple inner class: 073 * <ul> 074 * <li><js>"MyClass$Inner1$Inner2.myMethod"</js> 075 * <li><js>"Inner1$Inner2.myMethod"</js> 076 * <li><js>"Inner2.myMethod"</js> 077 * </ul> 078 * </ul> 079 * <li>Fields: 080 * <ul> 081 * <li>Fully qualified: 082 * <ul> 083 * <li><js>"com.foo.MyClass.myField"</js> 084 * </ul> 085 * <li>Simple: 086 * <ul> 087 * <li><js>"MyClass.myField"</js> 088 * </ul> 089 * <li>Simple inner class: 090 * <ul> 091 * <li><js>"MyClass$Inner1$Inner2.myField"</js> 092 * <li><js>"Inner1$Inner2.myField"</js> 093 * <li><js>"Inner2.myField"</js> 094 * </ul> 095 * </ul> 096 * <li>Constructors: 097 * <ul> 098 * <li>Fully qualified with args: 099 * <ul> 100 * <li><js>"com.foo.MyClass(String,int)"</js> 101 * <li><js>"com.foo.MyClass(java.lang.String,int)"</js> 102 * <li><js>"com.foo.MyClass()"</js> 103 * </ul> 104 * <li>Simple with args: 105 * <ul> 106 * <li><js>"MyClass(String,int)"</js> 107 * <li><js>"MyClass(java.lang.String,int)"</js> 108 * <li><js>"MyClass()"</js> 109 * </ul> 110 * <li>Simple inner class: 111 * <ul> 112 * <li><js>"MyClass$Inner1$Inner2()"</js> 113 * <li><js>"Inner1$Inner2()"</js> 114 * <li><js>"Inner2()"</js> 115 * </ul> 116 * </ul> 117 * <li>A comma-delimited list of anything on this list. 118 * </ul> 119 * 120 * @param <V> The type of object in this map. 121 */ 122public class ReflectionMap<V> { 123 124 private final List<ClassEntry<V>> classEntries; 125 private final List<MethodEntry<V>> methodEntries; 126 private final List<FieldEntry<V>> fieldEntries; 127 private final List<ConstructorEntry<V>> constructorEntries; 128 final boolean noClassEntries, noMethodEntries, noFieldEntries, noConstructorEntries; 129 130 /** 131 * Constructor. 132 * 133 * @param b Initializer object. 134 */ 135 ReflectionMap(Builder<V> b) { 136 this.classEntries = Collections.unmodifiableList(new ArrayList<>(b.classEntries)); 137 this.methodEntries = Collections.unmodifiableList(new ArrayList<>(b.methodEntries)); 138 this.fieldEntries = Collections.unmodifiableList(new ArrayList<>(b.fieldEntries)); 139 this.constructorEntries = Collections.unmodifiableList(new ArrayList<>(b.constructorEntries)); 140 this.noClassEntries = classEntries.isEmpty(); 141 this.noMethodEntries = methodEntries.isEmpty(); 142 this.noFieldEntries = fieldEntries.isEmpty(); 143 this.noConstructorEntries = constructorEntries.isEmpty(); 144 } 145 146 /** 147 * Static builder creator. 148 * 149 * @param <V> The type of object in this map. 150 * @param c The type of object in this map. 151 * @return A new instance of this object. 152 */ 153 public static <V> ReflectionMap.Builder<V> create(Class<V> c) { 154 return new ReflectionMap.Builder<>(); 155 } 156 157 /** 158 * Creates a new builder object for {@link ReflectionMap} objects. 159 * 160 * @param <V> The type of object in this map. 161 */ 162 public static class Builder<V> { 163 List<ClassEntry<V>> classEntries = new ArrayList<>(); 164 List<MethodEntry<V>> methodEntries = new ArrayList<>(); 165 List<FieldEntry<V>> fieldEntries = new ArrayList<>(); 166 List<ConstructorEntry<V>> constructorEntries = new ArrayList<>(); 167 168 /** 169 * Adds a mapping to this builder. 170 * 171 * @param key 172 * The mapping key. 173 * <br>Can be any of the following: 174 * <ul> 175 * <li>Full class name (e.g. <js>"com.foo.MyClass"</js>). 176 * <li>Simple class name (e.g. <js>"MyClass"</js>). 177 * <li>Full method name (e.g. <js>"com.foo.MyClass.myMethod"</js>). 178 * <li>Simple method name (e.g. <js>"MyClass.myMethod"</js>). 179 * <li>A comma-delimited list of anything on this list. 180 * </ul> 181 * @param value The value for this mapping. 182 * @return This object (for method chaining). 183 */ 184 public Builder<V> append(String key, V value) { 185 if (isEmpty(key)) 186 throw new RuntimeException("Invalid reflection signature: [" + key + "]"); 187 try { 188 for (String k : splitNames(key)) { 189 if (k.endsWith(")")) { 190 int i = k.substring(0, k.indexOf('(')).lastIndexOf('.'); 191 if (i == -1 || isUpperCase(k.charAt(i+1))) { 192 constructorEntries.add(new ConstructorEntry<>(k, value)); 193 } else { 194 methodEntries.add(new MethodEntry<>(k, value)); 195 } 196 } else { 197 int i = k.lastIndexOf('.'); 198 if (i == -1) { 199 classEntries.add(new ClassEntry<>(k, value)); 200 } else if (isUpperCase(k.charAt(i+1))) { 201 classEntries.add(new ClassEntry<>(k, value)); 202 fieldEntries.add(new FieldEntry<>(k, value)); 203 } else { 204 methodEntries.add(new MethodEntry<>(k, value)); 205 fieldEntries.add(new FieldEntry<>(k, value)); 206 } 207 } 208 } 209 } catch (IndexOutOfBoundsException e) { 210 throw new RuntimeException("Invalid reflection signature: [" + key + "]"); 211 } 212 213 return this; 214 } 215 216 /** 217 * Create new instance of {@link ReflectionMap} based on the contents of this builder. 218 * 219 * @return A new {@link ReflectionMap} object. 220 */ 221 public ReflectionMap<V> build() { 222 return new ReflectionMap<>(this); 223 } 224 } 225 226 static List<String> splitNames(String key) { 227 if (key.indexOf(',') == -1) 228 return Collections.singletonList(key.trim()); 229 List<String> l = new ArrayList<>(); 230 231 int m = 0; 232 boolean escaped = false; 233 for (int i = 0; i < key.length(); i++) { 234 char c = key.charAt(i); 235 if (c == '(') 236 escaped = true; 237 else if (c == ')') 238 escaped = false; 239 else if (c == ',' && ! escaped) { 240 l.add(key.substring(m, i).trim()); 241 m = i+1; 242 } 243 } 244 l.add(key.substring(m).trim()); 245 246 return l; 247 } 248 249 /** 250 * Finds first value in this map that matches the specified class. 251 * 252 * @param c The class to test for. 253 * @param ofType Only return objects of the specified type. 254 * @return The matching object. Never <jk>null</jk>. 255 */ 256 public Optional<V> find(Class<?> c, Class<? extends V> ofType) { 257 if (! noClassEntries) 258 for (ClassEntry<V> e : classEntries) 259 if (e.matches(c)) 260 if (ofType == null || ofType.isInstance(e.value)) 261 return Optional.of(e.value); 262 return Optional.empty(); 263 } 264 265 /** 266 * Finds first value in this map that matches the specified method. 267 * 268 * @param m The method to test for. 269 * @param ofType Only return objects of the specified type. 270 * @return The matching object. Never <jk>null</jk>. 271 */ 272 public Optional<V> find(Method m, Class<? extends V> ofType) { 273 if (! noMethodEntries) 274 for (MethodEntry<V> e : methodEntries) 275 if (e.matches(m)) 276 if (ofType == null || ofType.isInstance(e.value)) 277 return Optional.of(e.value); 278 return Optional.empty(); 279 } 280 281 /** 282 * Finds first value in this map that matches the specified field. 283 * 284 * @param f The field to test for. 285 * @param ofType Only return objects of the specified type. 286 * @return The matching object. Never <jk>null</jk>. 287 */ 288 public Optional<V> find(Field f, Class<? extends V> ofType) { 289 if (! noFieldEntries) 290 for (FieldEntry<V> e : fieldEntries) 291 if (e.matches(f)) 292 if (ofType == null || ofType.isInstance(e.value)) 293 return Optional.of(e.value); 294 return Optional.empty(); 295 } 296 297 /** 298 * Finds first value in this map that matches the specified constructor. 299 * 300 * @param c The constructor to test for. 301 * @param ofType Only return objects of the specified type. 302 * @return The matching object. Never <jk>null</jk>. 303 */ 304 public Optional<V> find(Constructor<?> c, Class<? extends V> ofType) { 305 if (! noConstructorEntries) 306 for (ConstructorEntry<V> e : constructorEntries) 307 if (e.matches(c)) 308 if (ofType == null || ofType.isInstance(e.value)) 309 return Optional.of(e.value); 310 return Optional.empty(); 311 } 312 313 static class ClassEntry<V> { 314 final String simpleName, fullName; 315 final V value; 316 317 ClassEntry(String name, V value) { 318 this.simpleName = simpleClassName(name); 319 this.fullName = name; 320 this.value = value; 321 } 322 323 public boolean matches(Class<?> c) { 324 if (c == null) 325 return false; 326 return classMatches(simpleName, fullName, c); 327 } 328 329 public ObjectMap asMap() { 330 return new ObjectMap() 331 .append("simpleName", simpleName) 332 .append("fullName", fullName) 333 .append("value", value); 334 } 335 336 @Override 337 public String toString() { 338 return asMap().toString(); 339 } 340 } 341 342 static class MethodEntry<V> { 343 String simpleClassName, fullClassName, methodName, args[]; 344 V value; 345 346 MethodEntry(String name, V value) { 347 int i = name.indexOf('('); 348 this.args = i == -1 ? null : split(name.substring(i+1, name.length()-1)); 349 name = i == -1 ? name : name.substring(0, i); 350 i = name.lastIndexOf('.'); 351 String s1 = name.substring(0, i).trim(), s2 = name.substring(i+1).trim(); 352 this.simpleClassName = simpleClassName(s1); 353 this.fullClassName = s1; 354 this.methodName = s2; 355 this.value = value; 356 } 357 358 public boolean matches(Method m) { 359 if (m == null) 360 return false; 361 Class<?> c = m.getDeclaringClass(); 362 return 363 classMatches(simpleClassName, fullClassName, c) 364 && (isEquals(m.getName(), methodName)) 365 && (argsMatch(args, m.getParameterTypes())); 366 } 367 368 public ObjectMap asMap() { 369 return new ObjectMap() 370 .append("simpleClassName", simpleClassName) 371 .append("fullClassName", fullClassName) 372 .append("methodName", methodName) 373 .append("args", args) 374 .append("value", value); 375 } 376 377 @Override 378 public String toString() { 379 return asMap().toString(); 380 } 381 } 382 383 static class ConstructorEntry<V> { 384 String simpleClassName, fullClassName, args[]; 385 V value; 386 387 ConstructorEntry(String name, V value) { 388 int i = name.indexOf('('); 389 this.args = split(name.substring(i+1, name.length()-1)); 390 name = name.substring(0, i).trim(); 391 this.simpleClassName = simpleClassName(name); 392 this.fullClassName = name; 393 this.value = value; 394 } 395 396 public boolean matches(Constructor<?> m) { 397 if (m == null) 398 return false; 399 Class<?> c = m.getDeclaringClass(); 400 return 401 classMatches(simpleClassName, fullClassName, c) 402 && (argsMatch(args, m.getParameterTypes())); 403 } 404 405 public ObjectMap asMap() { 406 return new ObjectMap() 407 .append("simpleClassName", simpleClassName) 408 .append("fullClassName", fullClassName) 409 .append("args", args) 410 .append("value", value); 411 } 412 413 @Override 414 public String toString() { 415 return asMap().toString(); 416 } 417 } 418 419 static class FieldEntry<V> { 420 String simpleClassName, fullClassName, fieldName; 421 V value; 422 423 FieldEntry(String name, V value) { 424 int i = name.lastIndexOf('.'); 425 String s1 = name.substring(0, i), s2 = name.substring(i+1); 426 this.simpleClassName = simpleClassName(s1); 427 this.fullClassName = s1; 428 this.fieldName = s2; 429 this.value = value; 430 } 431 432 public boolean matches(Field f) { 433 if (f == null) 434 return false; 435 Class<?> c = f.getDeclaringClass(); 436 return 437 classMatches(simpleClassName, fullClassName, c) 438 && (isEquals(f.getName(), fieldName)); 439 } 440 441 public ObjectMap asMap() { 442 return new ObjectMap() 443 .append("simpleClassName", simpleClassName) 444 .append("fullClassName", fullClassName) 445 .append("fieldName", fieldName) 446 .append("value", value); 447 } 448 449 @Override 450 public String toString() { 451 return asMap().toString(); 452 } 453 } 454 455 static boolean argsMatch(String[] names, Class<?>[] args) { 456 if (names == null) 457 return true; 458 if (names.length != args.length) 459 return false; 460 for (int i = 0; i < args.length; i++) { 461 String n = names[i]; 462 Class<?> a = args[i]; 463 if (! (isEquals(n, a.getSimpleName()) || isEquals(n, a.getName()))) 464 return false; 465 } 466 return true; 467 } 468 469 static String simpleClassName(String name) { 470 int i = name.indexOf('.'); 471 if (i == -1) 472 return name; 473 return null; 474 } 475 476 static boolean classMatches(String simpleName, String fullName, Class<?> c) { 477 // For class org.apache.juneau.a.rttests.RountTripBeansWithBuilders$Ac$Builder 478 // c.getSimpleName() == "Builder" 479 // c.getFullName() == "org.apache.juneau.a.rttests.RountTripBeansWithBuilders$Ac$Builder" 480 // c.getPackage() == "org.apache.juneau.a.rttests" 481 String cSimple = c.getSimpleName(), cFull = c.getName(); 482 if (isEquals(simpleName, cSimple) || isEquals(fullName, cFull)) 483 return true; 484 if (cFull.indexOf('$') != -1) { 485 Package p = c.getPackage(); 486 if (p != null) 487 cFull = cFull.substring(p.getName().length() + 1); 488 if (isEquals(simpleName, cFull)) 489 return true; 490 int i = cFull.indexOf('$'); 491 while (i != -1) { 492 cFull = cFull.substring(i+1); 493 if (isEquals(simpleName, cFull)) 494 return true; 495 i = cFull.indexOf('$'); 496 } 497 } 498 return false; 499 } 500 501 @Override /* Object */ 502 public String toString() { 503 return new ObjectMap() 504 .append("classEntries", classEntries) 505 .append("methodEntries", methodEntries) 506 .append("fieldEntries", fieldEntries) 507 .append("constructorEntries", constructorEntries) 508 .toString(); 509 } 510}