001/*
002 * This file is copyright Bitronix Software.
003 *
004 * Bitronix Transaction Manager
005 *
006 * https://github.com/brettwooldridge/bitronix-hp/blob/master/btm/src/main/java/bitronix/tm/utils/PropertyUtils.java
007 *
008 * Copyright (c) 2010, Bitronix Software.
009 *
010 * This copyrighted material is made available to anyone wishing to use, modify,
011 * copy, or redistribute it subject to the terms and conditions of the GNU
012 * Lesser General Public License, as published by the Free Software Foundation.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
017 * for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public License
020 * along with this distribution; if not, write to:
021 * Free Software Foundation, Inc.
022 * 51 Franklin Street, Fifth Floor
023 * Boston, MA 02110-1301 USA
024 */
025
026package io.stallion.reflection;
027
028import io.stallion.dataAccess.MappedModel;
029import io.stallion.plugins.javascript.BaseJavascriptModel;
030import io.stallion.utils.GeneralUtils;
031import org.apache.commons.lang3.StringUtils;
032
033import static io.stallion.utils.Literals.*;
034
035import java.lang.annotation.Annotation;
036import java.lang.reflect.InvocationTargetException;
037import java.lang.reflect.Method;
038import java.lang.reflect.Modifier;
039import java.time.Instant;
040import java.time.ZonedDateTime;
041import java.util.*;
042
043/**
044 * Utility class for getting and setting bean properties via reflection.
045 *
046 */
047public final class PropertyUtils {
048
049    private static HashMap<String, Object> lookupCache = new HashMap<>();
050
051    private PropertyUtils() {
052    }
053
054    public static  void resetCache() {
055        lookupCache = new HashMap<>();
056    }
057
058    /**
059     * Set a direct or indirect property (dotted property: prop1.prop2.prop3) on the target object. This method tries
060     * to be smart in the way that intermediate properties currently set to null are set if it is possible to create
061     * and set an object. Conversions from propertyValue to the proper destination type are performed when possible.
062     * @param target the target object on which to set the property.
063     * @param propertyName the name of the property to set.
064     * @param propertyValue the value of the property to set.
065     * @throws PropertyException if an error happened while trying to set the property.
066     */
067    public static void setProperty(Object target, String propertyName, Object propertyValue) throws PropertyException {
068        String[] propertyNames = propertyName.split("\\.");
069
070        StringBuffer visitedPropertyName = new StringBuffer();
071        Object currentTarget = target;
072        int i = 0;
073        while (i < propertyNames.length -1) {
074            String name = propertyNames[i];
075            Object result = callGetter(currentTarget, name);
076            if (result == null) {
077                // try to instanciate the object & set it in place
078                Class propertyType = getPropertyType(target, name);
079                try {
080                    result = propertyType.newInstance();
081                } catch (InstantiationException ex) {
082                    throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex);
083                } catch (IllegalAccessException ex) {
084                    throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex);
085                }
086                callSetter(currentTarget, name, result);
087            }
088
089            currentTarget = result;
090            visitedPropertyName.append(name);
091            visitedPropertyName.append('.');
092            i++;
093
094            // if it's a Properties object -> the non-visited part of the key should be used
095            // as this Properties' object key so stop iterating over the dotted properties.
096            if (currentTarget instanceof Properties)
097                break;
098        }
099
100        String lastPropertyName = propertyName.substring(visitedPropertyName.length(), propertyName.length());
101        if (currentTarget instanceof Properties) {
102            Properties p = (Properties) currentTarget;
103            p.setProperty(lastPropertyName, propertyValue.toString());
104
105        } else {
106            setDirectProperty(currentTarget, lastPropertyName, propertyValue);
107        }
108    }
109
110    /**
111     * Build a map of direct javabeans properties of the target object. Only read/write properties (ie: those who have
112     * both a getter and a setter) are returned.
113     * @param target the target object from which to get properties names.
114     * @return a Map of String with properties names as key and their values
115     * @throws PropertyException if an error happened while trying to get a property.
116     */
117    public static Map<String, Object> getProperties(Object target, Class<? extends Annotation>...excludeAnnotations) throws PropertyException {
118        Map<String, Object> properties = new HashMap<String, Object>();
119        Class clazz = target.getClass();
120        Method[] methods = clazz.getMethods();
121        for (int i = 0; i < methods.length; i++) {
122            Method method = methods[i];
123            String name = method.getName();
124            Boolean hasExcludeAnno = false;
125            if (excludeAnnotations.length > 0) {
126                for(Class<? extends Annotation> anno: excludeAnnotations) {
127                    if (method.isAnnotationPresent(anno)) {
128                        hasExcludeAnno = true;
129                    }
130                }
131            }
132            if (hasExcludeAnno) {
133                continue;
134            }
135            if (method.getModifiers() == Modifier.PUBLIC && method.getParameterTypes().length == 0 && (name.startsWith("get") || name.startsWith("is"))
136                    && containsSetterForGetter(clazz, method)) {
137                String propertyName;
138                if (name.startsWith("get"))
139                    propertyName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
140                else if (name.startsWith("is"))
141                    propertyName = Character.toLowerCase(name.charAt(2)) + name.substring(3);
142                else
143                    throw new PropertyException("method '" + name + "' is not a getter, thereof no setter can be found");
144
145                try {
146                    Object propertyValue = method.invoke(target, (Object[]) null);  // casting to (Object[]) b/c of javac 1.5 warning
147                    if (propertyValue != null && propertyValue instanceof Properties) {
148                        Map propertiesContent = getNestedProperties(propertyName, (Properties) propertyValue);
149                        properties.putAll(propertiesContent);
150                    }
151                    else {
152                        properties.put(propertyName, propertyValue);
153                    }
154                } catch (IllegalAccessException ex) {
155                    throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex);
156                } catch (InvocationTargetException ex) {
157                    throw new PropertyException("cannot set property '" + propertyName + "' - '" + name + "' is null and cannot be auto-filled", ex);
158                }
159
160            } // if
161        } // for
162
163        return properties;
164    }
165
166    public static List<String> getPropertyNames(Class clazz) throws PropertyException {
167        Method[] methods = clazz.getMethods();
168        List<String> names = list();
169        for (int i = 0; i < methods.length; i++) {
170            Method method = methods[i];
171            String name = method.getName();
172            if (method.getModifiers() == Modifier.PUBLIC && method.getParameterTypes().length == 0 && (name.startsWith("get") || name.startsWith("is"))
173                    && containsSetterForGetter(clazz, method)) {
174                String propertyName;
175                if (name.startsWith("get"))
176                    propertyName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
177                else
178                    propertyName = Character.toLowerCase(name.charAt(2)) + name.substring(3);
179                names.add(propertyName);
180            }
181        }
182        return names;
183    }
184
185    public static boolean propertyHasAnnotation(Class cls, String name, Class<? extends Annotation> anno) {
186        return getAnnotationForProperty(cls, name, anno) != null;
187    }
188
189    public static <T extends Annotation> T getAnnotationForProperty(Class cls, String name, Class<T> anno) {
190        String postFix = name.toUpperCase();
191        if (name.length() > 1) {
192            postFix = name.substring(0, 1).toUpperCase() + name.substring(1);
193        }
194        Method method = null;
195        try {
196            method = cls.getMethod("get" + postFix);
197        } catch (NoSuchMethodException e) {
198
199        }
200        if (method == null) {
201            try {
202                method = cls.getMethod("is" + postFix);
203            } catch (NoSuchMethodException e) {
204
205            }
206        }
207        if (method == null) {
208            return null;
209        }
210        if (method.getModifiers() != Modifier.PUBLIC) {
211            return null;
212        }
213        if (method.isAnnotationPresent(anno)) {
214            return method.getDeclaredAnnotation(anno);
215        }
216        return null;
217    }
218
219
220
221
222
223    public static Boolean isReadable(Object obj, String name) {
224        String cacheKey = obj.getClass().getCanonicalName() + "|" + name;
225        if (lookupCache.containsKey(cacheKey)) {
226            return (Boolean)lookupCache.get(cacheKey);
227        }
228
229        String postFix = name.toUpperCase();
230        if (name.length() > 1) {
231            postFix = name.substring(0, 1).toUpperCase() + name.substring(1);
232        }
233        Method method = null;
234        try {
235            method = obj.getClass().getMethod("get" + postFix, (Class<?>[]) null);
236        } catch (NoSuchMethodException e) {
237
238        }
239        if (method == null) {
240            try {
241                method = obj.getClass().getMethod("is" + postFix, (Class<?>[]) null);
242            } catch (NoSuchMethodException e) {
243
244            }
245        }
246        if (method == null) {
247            lookupCache.put(cacheKey, false);
248            return false;
249        }
250        if (method.getModifiers() == Modifier.PUBLIC) {
251            lookupCache.put(cacheKey, true);
252            return true;
253        }
254        lookupCache.put(cacheKey, false);
255        return false;
256    }
257
258    public static Boolean isWriteable(Object target, String propertyName) {
259        if (propertyName == null || "".equals(propertyName))
260            throw new PropertyException("encountered invalid null or empty property name");
261        String setterName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
262        Method[] methods = target.getClass().getMethods();
263        for (int i = 0; i < methods.length; i++) {
264            Method method = methods[i];
265            if (method.getName().equals(setterName)  &&  method.getParameterTypes().length == 1) {
266                return true;
267            }
268        }
269        return false;
270    }
271
272    private static boolean containsSetterForGetter(Class clazz, Method method) {
273        String methodName = method.getName();
274        String setterName;
275
276        if (methodName.startsWith("get"))
277            setterName = "set" + methodName.substring(3);
278        else if (methodName.startsWith("is"))
279            setterName = "set" + methodName.substring(2);
280        else
281            throw new PropertyException("method '" + methodName + "' is not a getter, thereof no setter can be found");
282
283        Method[] methods = clazz.getMethods();
284        for (int i = 0; i < methods.length; i++) {
285            Method method1 = methods[i];
286            if (method1.getName().equals(setterName))
287                return true;
288        }
289        return false;
290    }
291
292    /**
293     * Get a direct or indirect property (dotted property: prop1.prop2.prop3) on the target object.
294     *
295     * @param target the target object from which to get the property.
296     * @param propertyName the name of the property to get.
297     * @return the value of the specified property.
298     * @throws PropertyException if an error happened while trying to get the property.
299     */
300    public static Object getProperty(Object target, String propertyName) throws PropertyException {
301        String[] propertyNames = propertyName.split("\\.");
302        Object currentTarget = target;
303
304        for (int i = 0; i < propertyNames.length; i++) {
305            String name = propertyNames[i];
306            Object result = callGetter(currentTarget, name);
307
308            if (result == null && i < propertyNames.length -1)
309                throw new PropertyException("cannot get property '" + propertyName + "' - '" + name + "' is null");
310            currentTarget = result;
311        }
312
313        return currentTarget;
314    }
315
316    public static Object getDotProperty(Object target, String propertyName) throws PropertyException {
317        if (!propertyName.contains(".")) {
318            return getProperty(target, propertyName);
319        }
320        String[] parts = StringUtils.split(propertyName, '.');
321        Object thing = target;
322        for (String part: parts) {
323            Object oldThing = thing;
324            if (PropertyUtils.isReadable(thing, part)) {
325                thing = PropertyUtils.getProperty(thing, part);
326            } else if (thing instanceof Map) {
327                thing = ((Map)thing).get(part);
328            } else {
329                throw new PropertyException("Cannot read property " + part + " of object " + thing + " of class " + thing.getClass().getName());
330            }
331            if (thing == null) {
332                return null;
333            }
334        }
335        return thing;
336    }
337
338    public static Object getPropertyOrMappedValue(Object target, String propertyName) throws PropertyException {
339        if (isReadable(target, propertyName)) {
340            return getProperty(target, propertyName);
341        } else if (target instanceof MappedModel) {
342            MappedModel model = (MappedModel)target;
343            return model.get(propertyName);
344        } else {
345            throw new PropertyException("Cannot read property " + propertyName + " of object " + target + " of class " + target.getClass().getName());
346        }
347    }
348
349    /**
350     * Set a {@link Map} of direct or indirect properties on the target object.
351     * @param target the target object on which to set the properties.
352     * @param properties a {@link Map} of String/Object pairs.
353     *
354     * @throws PropertyException if an error happened while trying to set a property.
355     */
356    public static void setProperties(Object target, Map properties) throws PropertyException {
357        Iterator it = properties.entrySet().iterator();
358        while (it.hasNext()) {
359            Map.Entry entry = (Map.Entry) it.next();
360            String name = (String) entry.getKey();
361            Object value = entry.getValue();
362            setProperty(target, name, value);
363        }
364    }
365
366    /**
367     * Return a comma-separated String of r/w properties of the specified object.
368     * @param obj the object to introspect.
369     * @return a a comma-separated String of r/w properties.
370     */
371    public static String propertiesToString(Object obj) {
372        StringBuffer sb = new StringBuffer();
373        Map properties = new TreeMap(getProperties(obj));
374        Iterator it = properties.keySet().iterator();
375        while (it.hasNext()) {
376            String property = (String) it.next();
377            Object val = getProperty(obj, property);
378            sb.append(property);
379            sb.append("=");
380            sb.append(val);
381            if (it.hasNext())
382                sb.append(", ");
383        }
384        return sb.toString();
385    }
386
387    /**
388     * Set a direct property on the target object. Conversions from propertyValue to the proper destination type
389     * are performed whenever possible.
390     *
391     * @param target the target object on which to set the property.
392     * @param propertyName the name of the property to set.
393     * @param propertyValue the value of the property to set.
394     * @throws PropertyException if an error happened while trying to set the property.
395     */
396    private static void setDirectProperty(Object target, String propertyName, Object propertyValue) throws PropertyException {
397        Method setter = getSetter(target, propertyName);
398        if (setter == null && !(target instanceof Map)) {
399            throw new PropertyException("no writable setter for '" + propertyName + "' in class '" + target.getClass().getName() + "'");
400        }
401        Object transformedPropertyValue = propertyValue;
402        if (setter != null && propertyValue != null) {
403            Class parameterType = setter.getParameterTypes()[0];
404            if (parameterType != null) {
405                transformedPropertyValue = transform(propertyValue, parameterType);
406            }
407        }
408        try {
409            if (propertyValue != null) {
410                if (setter == null) {
411                    ((Map)target).put(propertyName, transformedPropertyValue);
412                } else {
413                    setter.invoke(target, new Object[]{transformedPropertyValue});
414                }
415            } else {
416                if (setter == null) {
417                    ((Map)target).put(propertyName, transformedPropertyValue);
418                }   else {
419                    setter.invoke(target, new Object[]{null});
420                }
421            }
422        } catch (IllegalAccessException ex) {
423            throw new PropertyException("property '" + propertyName + "' is not accessible", ex);
424        } catch (InvocationTargetException ex) {
425            throw new PropertyException("property '" + propertyName + "' access threw an exception", ex);
426        } catch (Exception ex) {
427            String msg = "Error setting property " + target.getClass().getSimpleName() + "." + propertyName + " to value " + transformedPropertyValue;
428            throw new PropertyException(msg, ex);
429        }
430    }
431
432    private static Map getNestedProperties(String prefix, Properties properties) {
433        Map result = new HashMap();
434        Iterator it = properties.entrySet().iterator();
435        while (it.hasNext()) {
436            Map.Entry entry = (Map.Entry) it.next();
437            String name = (String) entry.getKey();
438            String value = (String) entry.getValue();
439            result.put(prefix + '.' + name, value);
440        }
441        return result;
442    }
443
444    /**
445     * Try to transform the passed in value into the destinationClass, via a applying a boatload of
446     * heuristics.
447     *
448     * @param value
449     * @param destinationClass
450     * @return
451     */
452    public static Object transform(Object value, Class destinationClass) {
453        if (value == null) {
454            return null;
455        }
456        if (value.getClass() == destinationClass)
457            return value;
458        if (destinationClass.isInstance(value)) {
459            return value;
460        }
461
462        // If target type is Date and json was a long, convert the long to a date
463        if (destinationClass == Date.class && (value.getClass() == long.class || value.getClass() == Long.class)) {
464            return new Date((long)value);
465        }
466        // Convert integers to longs, if target type is long
467        if ((destinationClass == Long.class || destinationClass == long.class) && (value.getClass() == int.class || value.getClass() == Integer.class)) {
468            return new Long((int)value);
469        }
470        // Convert ints and longs to ZonedDateTime, if ZonedDateTime was a long
471        if (destinationClass == ZonedDateTime.class && (
472                value.getClass() == long.class ||
473                value.getClass() == Long.class ||
474                value.getClass() == int.class ||
475                value.getClass() == Integer.class
476                )
477            ) {
478            return ZonedDateTime.ofInstant(Instant.ofEpochMilli((long) value), GeneralUtils.UTC);
479        }
480        if (destinationClass == ZonedDateTime.class && (value.getClass() == double.class || value.getClass() == Double.class)) {
481            return ZonedDateTime.ofInstant(Instant.ofEpochMilli(Math.round((Double)value)), GeneralUtils.UTC);
482        }
483
484        // Convert Strings to Enums, if target type was an enum
485        if (destinationClass.isEnum()) {
486            return Enum.valueOf(destinationClass, value.toString());
487        }
488
489        if ((destinationClass == boolean.class || destinationClass == Boolean.class)) {
490            if (value instanceof String) {
491                return Boolean.valueOf((String) value);
492            } else if (value instanceof Integer) {
493                return (Integer)value > 0;
494            } else if (value instanceof Long) {
495                return (Long)value > 0;
496            }
497        }
498
499        if ((destinationClass == byte.class || destinationClass == Byte.class)  &&  value.getClass() == String.class) {
500            return new Byte((String) value);
501        }
502        if ((destinationClass == short.class || destinationClass == Short.class)  &&  value.getClass() == String.class) {
503            return new Short((String) value);
504        }
505        if ((destinationClass == int.class || destinationClass == Integer.class)  &&  value.getClass() == String.class) {
506            return new Integer((String) value);
507        }
508        if ((destinationClass == long.class || destinationClass == Long.class)  &&  value.getClass() == String.class) {
509            return new Long((String) value);
510        }
511        if ((destinationClass == float.class || destinationClass == Float.class)  &&  value.getClass() == String.class) {
512            return new Float((String) value);
513        }
514        if ((destinationClass == float.class || destinationClass == Float.class)  &&  value.getClass() == Integer.class) {
515            return ((Integer)value).floatValue();
516        }
517        if ((destinationClass == float.class || destinationClass == Float.class)  &&  value.getClass() == Long.class) {
518            return ((Long)value).floatValue();
519        }
520        if ((destinationClass == float.class || destinationClass == Float.class)  &&  value.getClass() == Double.class) {
521            return ((Double)value).floatValue();
522        }
523
524
525        if ((destinationClass == double.class || destinationClass == Double.class)  &&  value.getClass() == Long.class) {
526            return ((Long)value).floatValue();
527        }
528
529
530        if ((destinationClass == float.class || destinationClass == Float.class)  &&  value.getClass() == String.class) {
531            return new Float((String) value);
532        }
533
534        if ((destinationClass == double.class || destinationClass == Double.class)  &&  value.getClass() == String.class) {
535            return new Double((String) value);
536        }
537
538        // If the type mis-match is due to boxing, just return the value
539        if (    value.getClass() == boolean.class || value.getClass() == Boolean.class ||
540                value.getClass() == byte.class || value.getClass() == Byte.class ||
541                value.getClass() == short.class || value.getClass() == Short.class ||
542                value.getClass() == int.class || value.getClass() == Integer.class ||
543                value.getClass() == long.class || value.getClass() == Long.class ||
544                value.getClass() == float.class || value.getClass() == Float.class ||
545                value.getClass() == double.class || value.getClass() == Double.class
546                )
547            return value;
548
549        throw new PropertyException("cannot convert values of type '" + value.getClass().getName() + "' into type '" + destinationClass + "'");
550    }
551
552    private static void callSetter(Object target, String propertyName, Object parameter) throws PropertyException {
553        Method setter = getSetter(target, propertyName);
554        try {
555            setter.invoke(target, new Object[] {parameter});
556        } catch (IllegalAccessException ex) {
557            throw new PropertyException("property '" + propertyName + "' is not accessible", ex);
558        } catch (InvocationTargetException ex) {
559            throw new PropertyException("property '" + propertyName + "' access threw an exception", ex);
560        }
561    }
562
563    private static Object callGetter(Object target, String propertyName) throws PropertyException {
564        Method getter = getGetter(target, propertyName);
565        try {
566            return getter.invoke(target, (Object[]) null); // casting to (Object[]) b/c of javac 1.5 warning
567        } catch (IllegalAccessException ex) {
568            throw new PropertyException("property '" + propertyName + "' is not accessible", ex);
569        } catch (InvocationTargetException ex) {
570            throw new PropertyException("property '" + propertyName + "' access threw an exception", ex);
571        }
572    }
573
574    private static Method getSetter(Object target, String propertyName) {
575        if (propertyName == null || "".equals(propertyName))
576            throw new PropertyException("encountered invalid null or empty property name");
577        String setterName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
578        Method[] methods = target.getClass().getMethods();
579        for (int i = 0; i < methods.length; i++) {
580            Method method = methods[i];
581            if (method.getName().equals(setterName)  &&  method.getParameterTypes().length == 1) {
582                return method;
583            }
584        }
585        return null;
586    }
587
588    public static Method getGetter(Object target, String propertyName) {
589        String cacheKey = "getGetter" + "|" + target.getClass().getCanonicalName() + "|" + propertyName;
590        if (target instanceof BaseJavascriptModel) {
591            cacheKey = "getGetter" + "|jsModel" + ((BaseJavascriptModel) target).getBucketName() + "|" + propertyName;
592        }
593        if (lookupCache.containsKey(cacheKey)) {
594            return (Method)lookupCache.get(cacheKey);
595        }
596        String getterName = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
597        String getterIsName = "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
598        Method[] methods = target.getClass().getMethods();
599        for (int i = 0; i < methods.length; i++) {
600            Method method = methods[i];
601            if ((method.getName().equals(getterName) || method.getName().equals(getterIsName))  &&  !method.getReturnType().equals(void.class)  &&  method.getParameterTypes().length == 0) {
602                lookupCache.put(cacheKey, method);
603                return method;
604            }
605        }
606        lookupCache.put(cacheKey, null);
607        throw new PropertyException("no readable property '" + propertyName + "' in class '" + target.getClass().getName() + "'");
608    }
609
610    private static Class getPropertyType(Object target, String propertyName) {
611        String getterName = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
612        String getterIsName = "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
613        Method[] methods = target.getClass().getMethods();
614        for (int i = 0; i < methods.length; i++) {
615            Method method = methods[i];
616            if ((method.getName().equals(getterName) || method.getName().equals(getterIsName))  &&  !method.getReturnType().equals(void.class)  &&  method.getParameterTypes().length == 0) {
617                return method.getReturnType();
618            }
619        }
620        throw new PropertyException("no property '" + propertyName + "' in class '" + target.getClass().getName() + "'");
621    }
622
623}