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}