blob: 421550475cb0655de9054fc707942ad7b99340c8 [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.impl.compiler;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CompileTimeObjectModel {
private static final Logger LOGGER = LoggerFactory.getLogger(CompileTimeObjectModel.class);
/**
* A {@link Set} that stores all the supported primitive classes.
*/
public static final Set<Class<?>> PRIMITIVE_CLASSES = Collections.unmodifiableSet(new HashSet<Class<?>>() {{
add(Boolean.class);
add(Boolean.class);
add(Character.class);
add(Byte.class);
add(Short.class);
add(Integer.class);
add(Long.class);
add(Float.class);
add(Double.class);
add(Void.class);
}});
public static final String TO_STRING_METHOD = "toString";
public static boolean isPrimitive(Object obj) {
return PRIMITIVE_CLASSES.contains(obj.getClass());
}
/**
* Resolve a property of a target object and return its value. The property can
* be either an index or a name
*
* @param target the target object
* @param property the property to be resolved
* @return the value of the property
*/
public static Object resolveProperty(Object target, Object property) {
Object resolved;
if (property instanceof Number) {
resolved = getIndex(target, ((Number) property).intValue());
} else {
resolved = getProperty(target, property);
}
return resolved;
}
/**
* Convert the given object to a boolean value
*
* @param object the target object
* @return the boolean representation of that object
*/
public static boolean toBoolean(Object object) {
return toBooleanInternal(object);
}
/**
* Coerce the object to a numeric value
*
* @param object - the target object
* @return - the numeric representation
*/
public static Number toNumber(Object object) {
if (object instanceof Number) {
return (Number) object;
}
return 0;
}
/**
* Convert the given object to a string.
*
* @param target the target object
* @return the string representation of the object
*/
public static String toString(Object target) {
return objectToString(target);
}
/**
* Force the conversion of the object to a collection
*
* @param object the target object
* @return the collection representation of the object
*/
public static Collection<Object> toCollection(Object object) {
return obtainCollection(object);
}
private static String objectToString(Object obj) {
String output = "";
if (obj != null) {
if (obj instanceof String) {
output = (String) obj;
} else if (isPrimitive(obj)) {
output = obj.toString();
} else if (obj instanceof Enum) {
return ((Enum) obj).name();
} else {
Collection<?> col = obtainCollection(obj);
if (col != null) {
output = collectionToString(col);
}
}
}
return output;
}
private static Object getProperty(Object target, Object propertyObj) {
String property = toString(propertyObj);
if (StringUtils.isEmpty(property)) {
throw new IllegalArgumentException("Invalid property name");
}
if (target == null) {
return null;
}
Object result = null;
if (target instanceof Map) {
result = getMapProperty((Map) target, property);
}
if (result == null) {
result = getObjectProperty(target, property);
}
return result;
}
@SuppressWarnings("unchecked")
private static Collection<Object> obtainCollection(Object obj) {
if (obj == null) {
return Collections.emptyList();
}
if (obj instanceof Object[]) {
return Arrays.asList((Object[]) obj);
}
if (obj instanceof Collection) {
return (Collection<Object>) obj;
}
if (obj instanceof Map) {
return ((Map) obj).keySet();
}
if (obj instanceof Enumeration) {
return Collections.list((Enumeration<Object>) obj);
}
if (obj instanceof Iterator) {
return fromIterator((Iterator<Object>) obj);
}
if (obj instanceof Iterable) {
Iterable iterable = (Iterable) obj;
return fromIterator(iterable.iterator());
}
if (obj instanceof String || obj instanceof Number) {
Collection list = new ArrayList();
list.add(obj);
return list;
}
return Collections.emptyList();
}
private static String collectionToString(Collection<?> col) {
StringBuilder builder = new StringBuilder();
String prefix = "";
for (Object o : col) {
builder.append(prefix).append(objectToString(o));
prefix = ",";
}
return builder.toString();
}
private static Collection<Object> fromIterator(Iterator<Object> iterator) {
ArrayList<Object> result = new ArrayList<>();
while (iterator.hasNext()) {
result.add(iterator.next());
}
return result;
}
private static boolean toBooleanInternal(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof Number) {
Number number = (Number) obj;
//todo should we consider precision issues?
return !(number.doubleValue() == 0.0);
}
String s = obj.toString().trim();
if ("".equals(s)) {
return false;
} else if ("true".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s)) {
return Boolean.parseBoolean(s);
}
if (obj instanceof Collection) {
return ((Collection) obj).size() > 0;
}
if (obj instanceof Map) {
return ((Map) obj).size() > 0;
}
if (obj instanceof Iterable<?>) {
return ((Iterable<?>) obj).iterator().hasNext();
}
if (obj instanceof Iterator<?>) {
return ((Iterator<?>) obj).hasNext();
}
return !(obj instanceof Object[]) || ((Object[]) obj).length > 0;
}
private static Object getIndex(Object obj, int index) {
if (obj instanceof Map) {
Map map = (Map) obj;
if (map.containsKey(index)) {
return map.get(index);
}
}
Collection collection = toCollection(obj);
if (collection instanceof List) {
return getIndexSafe((List) collection, index);
}
return null;
}
private static Object getIndexSafe(List list, int index) {
if (index < 0 || index >= list.size()) {
return null;
}
return list.get(index);
}
private static Object getMapProperty(Map map, String property) {
return map.get(property);
}
private static Object getObjectProperty(Object obj, String property) {
Object result = getObjectNoArgMethod(obj, property);
if (result == null) {
result = getField(obj, property);
}
return result;
}
private static Object getField(Object obj, String property) {
Class<?> cls = obj.getClass();
if (cls.isArray() && "length".equals(property)) {
return Array.getLength(obj);
}
try {
Field field = cls.getDeclaredField(property);
return field.get(obj);
} catch (Exception e) {
return null;
}
}
private static Object getObjectNoArgMethod(Object obj, String property) {
Class<?> cls = obj.getClass();
Method method = findMethod(cls, property);
if (method != null) {
method = extractMethodInheritanceChain(cls, method);
try {
return method.invoke(obj);
} catch (Exception e) {
LOGGER.error("Cannot access method " + property + " on object " + obj.toString(), e);
}
}
return null;
}
private static Method findMethod(Class<?> cls, String baseName) {
Method[] publicMethods = cls.getMethods();
String capitalized = StringUtils.capitalize(baseName);
for (Method m : publicMethods) {
if (m.getParameterTypes().length == 0) {
String methodName = m.getName();
if (baseName.equals(methodName) || ("get" + capitalized).equals(methodName) || ("is" + capitalized).equals(methodName)) {
// this method is good, check whether allowed
if (isMethodAllowed(m)) {
return m;
}
// method would match but is not allowed, abort
break;
}
}
}
return null;
}
private static boolean isMethodAllowed(Method method) {
Class<?> declaringClass = method.getDeclaringClass();
//methods of the Object.class are forbidden (except toString, which is allowed)
return declaringClass != Object.class || TO_STRING_METHOD.equals(method.getName());
}
private static Method extractMethodInheritanceChain(Class type, Method m) {
if (m == null || Modifier.isPublic(type.getModifiers())) {
return m;
}
Class[] iFaces = type.getInterfaces();
Method mp;
for (Class<?> iFace : iFaces) {
mp = getClassMethod(iFace, m);
if (mp != null) {
return mp;
}
}
return getClassMethod(type.getSuperclass(), m);
}
private static Method getClassMethod(Class<?> clazz, Method m) {
Method mp;
try {
mp = clazz.getMethod(m.getName(), m.getParameterTypes());
mp = extractMethodInheritanceChain(mp.getDeclaringClass(), mp);
if (mp != null) {
return mp;
}
} catch (NoSuchMethodException e) {
// do nothing
}
return null;
}
}