/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed 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.keycloak.client.registration.cli.util;
import com.fasterxml.jackson.core.JsonParseException;
import org.keycloak.client.registration.cli.common.AttributeKey;
import org.keycloak.client.registration.cli.common.AttributeOperation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ReflectionUtil {
static Map<Class, Map<String, Field>> index = new HashMap<>();
static void populateAttributesIndex(Class type) {
// We are using fields rather than getters / setters
// because it seems like JSON mapping sometimes also uses fields as well
// This may have to be changed some day due to reliance on Field.setAccessible()
Map<String, Field> map = new HashMap<>();
Field [] fields = type.getDeclaredFields();
for (Field f: fields) {
// make sure to also have access to non-public fields
f.setAccessible(true);
map.put(f.getName(), f);
}
index.put(type, map);
}
public static Map<String, Field> getAttrFieldsForType(Type gtype) {
Class type;
if (gtype instanceof Class) {
type = (Class) gtype;
} else if (gtype instanceof ParameterizedType) {
type = (Class) ((ParameterizedType) gtype).getRawType();
} else {
throw new RuntimeException("Unexpected type: " + gtype);
}
if (isListType(type) || isMapType(type)) {
return Collections.emptyMap();
}
Map<String, Field> map = index.get(type);
if (map == null) {
populateAttributesIndex(type);
map = index.get(type);
}
return map;
}
public static boolean isListType(Class type) {
return List.class.isAssignableFrom(type) || type.isArray();
}
public static boolean isBasicType(Type type) {
return type == String.class || type == Boolean.class || type == boolean.class
|| type == Integer.class || type == int.class || type == Long.class || type == long.class
|| type == Float.class || type == float.class || type == Double.class || type == double.class;
}
public static boolean isMapType(Class type) {
return Map.class.isAssignableFrom(type);
}
public static Object convertValueToType(Object value, Class<?> type) throws IOException {
if (value == null) {
return null;
} else if (value instanceof String) {
if (type == String.class) {
return value;
} else if (type == Boolean.class) {
return Boolean.valueOf((String) value);
} else if (type == Integer.class) {
return Integer.valueOf((String) value);
} else if (type == Long.class) {
return Long.valueOf((String) value);
} else {
return JsonSerialization.readValue((String) value, type);
}
} else if (value instanceof Number) {
if (type == Integer.class) {
return ((Number) value).intValue();
} else if (type == Long.class) {
return ((Long) value).longValue();
} else if (type == String.class) {
return String.valueOf(value);
}
} else if (value instanceof Boolean) {
if (type == Boolean.class) {
return value;
} else if (type == String.class) {
return String.valueOf(value);
}
}
throw new RuntimeException("Unable to handle type [" + type + "]");
}
public static void setAttributes(Object client, List<AttributeOperation> attrs) {
for (AttributeOperation item: attrs) {
AttributeKey attr = item.getKey();
Object nested = client;
List<AttributeKey.Component> cs = attr.getComponents();
for (int i = 0; i < cs.size(); i++) {
AttributeKey.Component c = cs.get(i);
Class type = nested.getClass();
Field field = null;
if (!isMapType(type)) {
Map<String, Field> fields = getAttrFieldsForType(type);
if (fields == null) {
throw new AttributeException(attr.toString(), "Unexpected condition - unknown type: " + type);
}
field = fields.get(c.getName());
Class parent = type;
while (field == null) {
parent = parent.getSuperclass();
if (parent == Object.class) {
throw new AttributeException(attr.toString(), "Unknown attribute '" + c.getName() + "' on " + client.getClass());
}
fields = getAttrFieldsForType(parent);
field = fields.get(c.getName());
}
}
// if it's a 'basic' type we directly use setter
type = field == null ? type : field.getType();
if (isBasicType(type)) {
if (i < cs.size() - 1) {
throw new AttributeException(attr.toString(), "Attribute is of primitive type, and can't be nested further: " + c);
}
try {
Object val = convertValueToType(item.getValue(), type);
field.set(nested, val);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
}
} else if (isListType(type)) {
if (i < cs.size() -1) {
// not the target component
try {
nested = field.get(nested);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to get attribute \"" + c + "\" in " + attr, e);
}
if (c.getIndex() >= 0) {
// list item
// get idx-th item
List l = (List) nested;
if (c.getIndex() >= l.size()) {
throw new AttributeException(attr.toString(), "Array index out of bounds for \"" + c + "\" in " + attr);
}
nested = l.get(c.getIndex());
}
} else {
// target component
Class itype = type;
Type gtype = field.getGenericType();
if (gtype instanceof ParameterizedType) {
Type[] typeArgs = ((ParameterizedType) gtype).getActualTypeArguments();
if (typeArgs.length >= 1 && typeArgs[0] instanceof Class) {
itype = (Class) typeArgs[0];
} else {
itype = String.class;
}
}
if (c.getIndex() >= 0 || attr.isAppend()) {
// some list item
// get the list first
List target;
try {
target = (List) field.get(nested);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to get list attribute: " + attr, e);
}
// now replace or add idx-th item
if (target == null) {
target = createNewList(type);
try {
field.set(nested, target);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
}
}
if (c.getIndex() >= target.size()) {
throw new AttributeException(attr.toString(), "Array index out of bounds for \"" + c + "\" in " + attr);
}
if (attr.isAppend()) {
try {
Object value = convertValueToType(item.getValue(), itype);
if (c.getIndex() >= 0) {
target.add(c.getIndex(), value);
} else {
target.add(value);
}
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
}
} else {
if (item.getType() == AttributeOperation.Type.SET) {
try {
Object value = convertValueToType(item.getValue(), itype);
target.set(c.getIndex(), value);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
}
} else {
try {
target.remove(c.getIndex());
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to remove list attribute " + attr, e);
}
}
}
} else {
// set the whole list field itself
List value = createNewList(type);;
if (item.getType() == AttributeOperation.Type.SET) {
List converted = convertValueToList(item.getValue(), itype);
value.addAll(converted);
}
try {
field.set(nested, value);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set list attribute " + attr, e);
}
}
}
} else {
// object type
if (i < cs.size() -1) {
// not the target component
Object value;
if (field == null) {
if (isMapType(nested.getClass())) {
value = ((Map) nested).get(c.getName());
} else {
throw new RuntimeException("Unexpected condition while processing: " + attr);
}
} else {
try {
value = field.get(nested);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to get attribute \"" + c + "\" in " + attr, e);
}
}
if (value == null) {
// create the target attribute
if (isMapType(nested.getClass())) {
throw new RuntimeException("Creating nested object trees not supported");
} else {
try {
value = createNewObject(type);
field.set(nested, value);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
}
}
}
nested = value;
} else {
// target component
// todo implement map put
if (isMapType(nested.getClass())) {
try {
((Map) nested).put(c.getName(), item.getValue());
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set map key " + attr, e);
}
} else {
try {
Object value = convertValueToType(item.getValue(), type);
field.set(nested, value);
} catch (Exception e) {
throw new AttributeException(attr.toString(), "Failed to set attribute " + attr, e);
}
}
}
}
}
}
}
private static Object createNewObject(Class type) throws Exception {
return type.newInstance();
}
public static List createNewList(Class type) {
if (type == List.class) {
return new ArrayList();
} else if (type.isInterface()) {
throw new RuntimeException("Can't instantiate a list type: " + type);
}
try {
return (List) type.newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate a list type: " + type, e);
}
}
public static List convertValueToList(String value, Class itemType) {
try {
List result = new LinkedList();
if (!value.startsWith("[")) {
throw new RuntimeException("List attribute value has to start with '[' - '" + value + "'");
}
List parsed = JsonSerialization.readValue(value, List.class);
for (Object item: parsed) {
if (itemType.isAssignableFrom(item.getClass())) {
result.add(item);
} else {
result.add(convertValueToType(item, itemType));
}
}
return result;
} catch (JsonParseException e) {
throw new RuntimeException("Failed to parse list value: " + e.getMessage(), e);
} catch (IOException e) {
throw new RuntimeException("Failed to parse list value: " + value, e);
}
}
public static <T> void merge(T source, T dest) {
// Use existing index for type, then iterate over all attributes and
// use setter on dest, and getter on source to copy value over
Map<String, Field> fieldMap = getAttrFieldsForType(source.getClass());
try {
for (String attrName : fieldMap.keySet()) {
Field field = fieldMap.get(attrName);
Object localValue = field.get(source);
if (localValue != null) {
field.set(dest, localValue);
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to merge changes", e);
}
}
public static LinkedHashMap<String, String> getAttributeListWithJSonTypes(Class type, AttributeKey attr) {
LinkedHashMap<String, String> result = new LinkedHashMap<>();
attr = attr != null ? attr : new AttributeKey();
Map<String, Field> fields = getAttrFieldsForType(type);
for (AttributeKey.Component c: attr.getComponents()) {
Field f = fields.get(c.getName());
if (f == null) {
throw new AttributeException(attr.toString(), "No such attribute: " + attr);
}
type = f.getType();
if (isBasicType(type) || isListType(type) || isMapType(type)) {
return result;
} else {
fields = getAttrFieldsForType(type);
}
}
for (Map.Entry<String, Field> item : fields.entrySet()) {
String key = item.getKey();
Class clazz = item.getValue().getType();
String t = getTypeString(clazz, item.getValue());
result.put(key, t);
}
return result;
}
public static Field resolveField(Class type, AttributeKey attr) {
Field f = null;
Type gtype = type;
for (AttributeKey.Component c: attr.getComponents()) {
if (f != null) {
gtype = f.getGenericType();
if (gtype instanceof ParameterizedType) {
Type[] typeargs = ((ParameterizedType) gtype).getActualTypeArguments();
if (typeargs.length > 0) {
gtype = typeargs[typeargs.length-1];
}
}
}
Map<String, Field> fields = getAttrFieldsForType(gtype);
f = fields.get(c.getName());
if (f == null) {
throw new AttributeException(attr.toString(), "No such attribute: " + attr);
}
}
return f;
}
public static String getTypeString(Type type, Field field) {
Class clazz = null;
if (type == null) {
if (field == null) {
throw new IllegalArgumentException("type == null and field == null");
}
type = field.getGenericType();
}
if (type instanceof Class) {
clazz = (Class) type;
} else if (type instanceof ParameterizedType) {
StringBuilder sb = new StringBuilder();
String rtype = getTypeString(((ParameterizedType) type).getRawType(), null);
sb.append(rtype);
sb.append(" ").append("(");
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
for (int i = 0; i < typeArgs.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(getTypeString(typeArgs[i], null));
}
sb.append(")");
return sb.toString();
}
if (CharSequence.class.isAssignableFrom(clazz)) {
return "string";
} else if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) {
return "int";
} else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) {
return "long";
} else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) {
return "float";
} else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) {
return "double";
} else if (Number.class.isAssignableFrom(clazz)) {
return "number";
} else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) {
return "boolean";
} else if (isListType(clazz)) {
if (field != null) {
Type gtype = field.getGenericType();
if (gtype == clazz && clazz.isArray()) {
return "array (" + getTypeString(clazz.getComponentType(), null) + ")";
}
return getTypeString(gtype, null);
}
return "array";
} else if (isMapType(clazz)) {
if (field != null) {
Type gtype = field.getGenericType();
return getTypeString(gtype, null);
}
return "object";
} else {
return "object";
}
}
}