All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sling.scripting.sightly.compiler.util.ObjectModel Maven / Gradle / Ivy

There is a newer version: 6.5.21
Show newest version
/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package org.apache.sling.scripting.sightly.compiler.util;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@code ObjectModel} class provides various static models for object conversion and object property resolution.
 */
public final class ObjectModel {

    private static final Logger LOGGER = LoggerFactory.getLogger(ObjectModel.class);
    private static final String EMPTY_STRING = "";

    /**
     * A {@link Set} that stores all the supported primitive classes.
     */
    public static final Set> PRIMITIVE_CLASSES;

    static {
        Set> primitivesBuilder = new HashSet<>();
        primitivesBuilder.add(Boolean.class);
        primitivesBuilder.add(Boolean.class);
        primitivesBuilder.add(Character.class);
        primitivesBuilder.add(Byte.class);
        primitivesBuilder.add(Short.class);
        primitivesBuilder.add(Integer.class);
        primitivesBuilder.add(Long.class);
        primitivesBuilder.add(Float.class);
        primitivesBuilder.add(Double.class);
        primitivesBuilder.add(Void.class);
        PRIMITIVE_CLASSES = Collections.unmodifiableSet(primitivesBuilder);
    }


    private static final String TO_STRING_METHOD = "toString";

    private ObjectModel() {}

    /**
     * Checks if the provided {@code object} is an instance of a primitive class.
     *
     * @param object the {@code Object} to check
     * @return {@code true} if the {@code object} is a primitive, {@code false} otherwise
     */
    public static boolean isPrimitive(Object object) {
        return PRIMITIVE_CLASSES.contains(object.getClass());
    }

    /**
     * 

* Given the {@code target} object, this method attempts to resolve and return the value of the passed {@code property}. *

*

* The property can be either an index or a name: *

*
    *
  • index: the property is considered an index if its value is an integer number and in this case the {@code target} * will be assumed to be either an array or it will be converted to a {@link Collection}; a fallback to {@link Map} will be * made in case the previous two attempts failed *
  • *
  • name: the {@code property} will be converted to a {@link String} (see {@link #toString(Object)}); the {@code target} * will be assumed to be either a {@link Map} or an object; if the {@link Map} attempt fails, the {@code property} will be * used to check if the {@code target} has a publicly accessible field with this name or a publicly accessible method with no * parameters with this name or a combination of the "get" or "is" prefixes plus the capitalised name (see * {@link #invokeBeanMethod(Object, String)})
  • *
* * @param target the target object * @param property the property to be resolved * @return the value of the property or {@code null} */ public static Object resolveProperty(Object target, Object property) { if (target == null || property == null) { return null; } Object resolved = null; if (property instanceof Number) { resolved = getIndex(target, ((Number) property).intValue()); } if (resolved == null) { String propertyName = toString(property); if (StringUtils.isNotEmpty(propertyName)) { if (target instanceof Map) { resolved = ((Map) target).get(property); } if (resolved == null) { resolved = getField(target, propertyName); } if (resolved == null) { resolved = invokeBeanMethod(target, propertyName); } } } return resolved; } /** * Converts the given {@code object} to a boolean value, applying the following rules: * *
    *
  • if the {@code object} is {@code null} the returned value is {@code false}
  • *
  • if the {@code object} is a {@link Number} the method will return {@code false} only if the number's value is 0
  • *
  • if the {@link String} representation of the {@code object} is equal irrespective of its casing to "true", the method will * return {@code true}
  • *
  • if the {@code object} is a {@link Collection} or a {@link Map}, the method will return {@code true} only if the collection / * map is not empty
  • *
  • if the object is an array, the method will return {@code true} only if the array is not empty
  • *
* * @param object the target object * @return the boolean representation of the {@code object} according to the conversion rules */ public static boolean toBoolean(Object object) { if (object == null) { return false; } if (object instanceof Number) { Number number = (Number) object; return !(number.doubleValue() == 0.0); } String s = object.toString().trim(); if (EMPTY_STRING.equals(s)) { return false; } else if ("true".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s)) { return Boolean.parseBoolean(s); } if (object instanceof Collection) { return ((Collection) object).size() > 0; } if (object instanceof Map) { return ((Map) object).size() > 0; } if (object instanceof Iterable) { return ((Iterable) object).iterator().hasNext(); } if (object instanceof Iterator) { return ((Iterator) object).hasNext(); } return !(object instanceof Object[]) || ((Object[]) object).length > 0; } /** * Coerces the passed {@code object} to a numeric value. If the passed value is a {@link String} the conversion rules are those of * {@link NumberUtils#createNumber(String)}. * * @param object the target object * @return the numeric representation if one can be determined or {@code null} * @see NumberUtils#createNumber(String) */ public static Number toNumber(Object object) { if (object == null) { return null; } if (object instanceof Number) { return (Number) object; } String stringValue = toString(object); try { return NumberUtils.createNumber(stringValue); } catch (NumberFormatException e) { return null; } } /** * Converts the passed {@code object} to a {@link String}. The following rules apply: * *
    *
  • if the {@code object} is {@code null} an empty string will be returned
  • *
  • if the {@code object} is an instance of a {@link String} the object itself will be returned
  • *
  • if the object is a primitive (see {@link #isPrimitive(Object)}), its {@link String} representation will be returned
  • *
  • if the object is an {@link Enum} its name will be returned (see {@link Enum#name()})
  • *
  • otherwise an attempt to convert the object to a {@link Collection} will be made and then the output of * {@link #collectionToString(Collection)} will be returned
  • *
* * @param object the target object * @return the string representation of the object or an empty string */ public static String toString(Object object) { String output = EMPTY_STRING; if (object != null) { if (object instanceof String) { output = (String) object; } else if (isPrimitive(object)) { output = object.toString(); } else if (object instanceof Enum) { return ((Enum) object).name(); } else { Collection col = toCollection(object); output = collectionToString(col); } } return output; } /** * Forces the conversion of the passed {@code object} to a collection, according to the following rules: * *
    *
  • if the {@code object} is {@code null} an empty collection will be returned
  • *
  • if the {@code object} is an array a list transformation of the array will be returned
  • *
  • if the {@code object} is a {@link Collection} the object itself will be returned
  • *
  • if the {@code object} is an instance of a {@link Map} the map's key set will be returned (see {@link Map#keySet()})
  • *
  • if the {@code object} is an instance of an {@link Enumeration} a list transformation will be returned
  • *
  • if the {@code object} is an instance of an {@link Iterator} or {@link Iterable} the result of {@link #fromIterator(Iterator)} * will be returned
  • *
  • if the {@code object} is an instance of a {@link String} or {@link Number} a {@link Collection} containing only this * object will be returned
  • *
  • any other case not covered by the previous rules will result in an empty {@link Collection}
  • *
* * @param object the target object * @return the collection representation of the object */ public static Collection toCollection(Object object) { if (object == null) { return Collections.emptyList(); } if (object instanceof Object[]) { return Arrays.asList((Object[]) object); } if (object.getClass().isArray()) { int length = Array.getLength(object); Collection list = new ArrayList<>(); for (int i = 0; i < length; i++) { list.add(Array.get(object, i)); } return list; } if (object instanceof Collection) { return (Collection) object; } if (object instanceof Map) { return ((Map) object).keySet(); } if (object instanceof Enumeration) { return Collections.list((Enumeration) object); } if (object instanceof Iterator) { return fromIterator((Iterator) object); } if (object instanceof Iterable) { Iterable iterable = (Iterable) object; return fromIterator(iterable.iterator()); } if (object instanceof String || object instanceof Number) { Collection list = new ArrayList<>(); list.add(object); return list; } return Collections.emptyList(); } /** * Converts the passed {@code collection} to a comma separated values {@link String} representation. * * @param collection the collection to be converted to CSV * @return the CSV; if the {@code collection} is empty then an empty string will be returned */ public static String collectionToString(Collection collection) { if (collection == null) { return EMPTY_STRING; } StringBuilder builder = new StringBuilder(); String prefix = EMPTY_STRING; for (Object o : collection) { builder.append(prefix).append(toString(o)); prefix = ","; } return builder.toString(); } /** * Given an {@code iterator}, this method will return a {@link Collection}. * * @param iterator the iterator to be transformed into a {@code collection} * @return a collection with the iterator's elements */ public static Collection fromIterator(Iterator iterator) { if (iterator == null) { return Collections.EMPTY_LIST; } ArrayList result = new ArrayList<>(); while (iterator.hasNext()) { result.add(iterator.next()); } return result; } /** * Given an indexable {@code object} (i.e. an array or a collection), this method will return the value available at the {@code * index} position. * * @param object the indexable object * @param index the index * @return the value stored at the {@code index} or {@code null} */ public static Object getIndex(Object object, int index) { if (object == null) { return null; } Class cls = object.getClass(); if (cls.isArray() && index >= 0 && index < Array.getLength(object)) { return Array.get(object, index); } Collection collection = toCollection(object); if (collection instanceof List && index >= 0 && index < collection.size()) { return ((List) collection).get(index); } return null; } /** * Given an {@code object}, this method will return the value of the public field identified by {@code fieldName}. * * @param object the target object * @param fieldName the name of the field * @return the value of the field or {@code null} if the field was not found */ public static Object getField(Object object, String fieldName) { if (object == null || StringUtils.isEmpty(fieldName)) { return null; } Class cls = object.getClass(); if (cls.isArray() && "length".equals(fieldName)) { return Array.getLength(object); } try { Field field = cls.getField(fieldName); return field.get(object); } catch (Exception e) { return null; } } /** * Given a bean {@code object}, this method will invoke the public method without parameters identified by {@code methodName} and * return the invocation's result. * * @param object the target object * @param methodName the name of the public method without parameters to invoke * @return the invocation's result or {@code null} if such a method cannot be found */ public static Object invokeBeanMethod(Object object, String methodName) { if (object == null || StringUtils.isEmpty(methodName)) { return null; } Class cls = object.getClass(); Method method = findBeanMethod(cls, methodName); if (method != null) { try { method = extractMethodInheritanceChain(cls, method); return method.invoke(object); } catch (Exception e) { LOGGER.error("Cannot access method " + methodName + " on object " + object.toString(), e); } } return null; } /** * Given a bean class and a base method name, this method will try to find a public method without parameters that is named: *
    *
  1. {@code baseName}
  2. *
  3. get + {@code BaseName}
  4. *
  5. is + {@code BaseName}
  6. *
* * @param cls the class into which to search for the method * @param baseName the base method name * @return a method that matches the criteria or {@code null} */ public static Method findBeanMethod(Class cls, String baseName) { if (cls == null || StringUtils.isEmpty(baseName)) { return null; } Method[] publicMethods = cls.getMethods(); String capitalized = StringUtils.capitalize(baseName); for (Method method : publicMethods) { if (method.getParameterTypes().length == 0) { String methodName = method.getName(); if (baseName.equals(methodName) || ("get" + capitalized).equals(methodName) || ("is" + capitalized).equals(methodName)) { if (isMethodAllowed(method)) { return method; } break; } } } return null; } /** * Returns {@code true} if the method is not one of the {@link Object}'s class declared methods, with the exception of * {@link Object#toString()}. * * @param method the method to check * @return {@code true} if the method is not one of the {@link Object}'s class declared methods, with the exception of * {@link Object#toString()}, {@code false} otherwise */ public static boolean isMethodAllowed(Method method) { if (method == null) { return false; } Class declaringClass = method.getDeclaringClass(); return declaringClass != Object.class || TO_STRING_METHOD.equals(method.getName()); } private static Method extractMethodInheritanceChain(Class type, Method method) { if (method == null || Modifier.isPublic(type.getModifiers())) { return method; } Class[] interfaces = type.getInterfaces(); Method parentMethod; for (Class iface : interfaces) { parentMethod = getClassMethod(iface, method); if (parentMethod != null) { return parentMethod; } } return getClassMethod(type.getSuperclass(), method); } private static Method getClassMethod(Class type, Method method) { try { Method parentMethod = type.getMethod(method.getName(), method.getParameterTypes()); parentMethod = extractMethodInheritanceChain(parentMethod.getDeclaringClass(), parentMethod); if (parentMethod != null) { return parentMethod; } } catch (NoSuchMethodException e) { // ignore - maybe we don't have access to that method or the method does not belong to the current type } return null; } }