blob: aa7bd8ff21632a7f45f767163c3bed500174cbf3 [file] [log] [blame]
/*******************************************************************************
* 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.
*
* @deprecated This class has been moved to {@link org.apache.sling.scripting.sightly.render.ObjectModel}.
*/
@Deprecated
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<Class<?>> PRIMITIVE_CLASSES;
static {
Set<Class<?>> primitivesBuilder = new HashSet<>();
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());
}
/**
* <p>
* Given the {@code target} object, this method attempts to resolve and return the value of the passed {@code property}.
* </p>
* <p>
* The property can be either an index or a name:
* </p>
* <ul>
* <li>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
* </li>
* <li>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)})</li>
* </ul>
*
* @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:
*
* <ul>
* <li>if the {@code object} is {@code null} the returned value is {@code false}</li>
* <li>if the {@code object} is a {@link Number} the method will return {@code false} only if the number's value is 0</li>
* <li>if the {@link String} representation of the {@code object} is equal irrespective of its casing to "true", the method will
* return {@code true}</li>
* <li>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</li>
* <li>if the object is an array, the method will return {@code true} only if the array is not empty</li>
* </ul>
*
* @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:
*
* <ul>
* <li>if the {@code object} is {@code null} an empty string will be returned</li>
* <li>if the {@code object} is an instance of a {@link String} the object itself will be returned</li>
* <li>if the object is a primitive (see {@link #isPrimitive(Object)}), its {@link String} representation will be returned</li>
* <li>if the object is an {@link Enum} its name will be returned (see {@link Enum#name()})</li>
* <li>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</li>
* </ul>
*
* @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:
*
* <ul>
* <li>if the {@code object} is {@code null} an empty collection will be returned</li>
* <li>if the {@code object} is an array a list transformation of the array will be returned</li>
* <li>if the {@code object} is a {@link Collection} the object itself will be returned</li>
* <li>if the {@code object} is an instance of a {@link Map} the map's key set will be returned (see {@link Map#keySet()})</li>
* <li>if the {@code object} is an instance of an {@link Enumeration} a list transformation will be returned</li>
* <li>if the {@code object} is an instance of an {@link Iterator} or {@link Iterable} the result of {@link #fromIterator(Iterator)}
* will be returned</li>
* <li>if the {@code object} is an instance of a {@link String} or {@link Number} a {@link Collection} containing only this
* object will be returned</li>
* <li>any other case not covered by the previous rules will result in an empty {@link Collection}</li>
* </ul>
*
* @param object the target object
* @return the collection representation of the object
*/
public static Collection<Object> 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<Object> 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>) object;
}
if (object instanceof Map) {
return ((Map) object).keySet();
}
if (object instanceof Enumeration) {
return Collections.list((Enumeration<Object>) object);
}
if (object instanceof Iterator) {
return fromIterator((Iterator<Object>) object);
}
if (object instanceof Iterable) {
Iterable<Object> iterable = (Iterable<Object>) object;
return fromIterator(iterable.iterator());
}
if (object instanceof String || object instanceof Number) {
Collection<Object> 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<Object> fromIterator(Iterator<Object> iterator) {
if (iterator == null) {
return Collections.EMPTY_LIST;
}
ArrayList<Object> 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:
* <ol>
* <li>{@code baseName}</li>
* <li>get + {@code BaseName}</li>
* <li>is + {@code BaseName}</li>
* </ol>
*
* @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;
}
}