/* * 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.admin.cli.util; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import org.keycloak.client.admin.cli.common.AttributeKey; import org.keycloak.client.admin.cli.common.AttributeOperation; import java.util.Iterator; import java.util.List; import java.util.Map; import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET; import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER; /** * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> */ public class ReflectionUtil { public static void setAttributes(JsonNode client, List<AttributeOperation> attrs) { for (AttributeOperation item: attrs) { AttributeKey attr = item.getKey(); JsonNode nested = client; List<AttributeKey.Component> cs = attr.getComponents(); for (int i = 0; i < cs.size(); i++) { AttributeKey.Component c = cs.get(i); // if this is the last component of the name, // then if SET we need to set value on nested: // if value already set on nested, then overwrite, maybe remove node + add new node // if DELETE we need to remove or nullify value (if isArray) // else get child and // if exist set nested to child // else if SET create new empty object or array - depending on c.isArray() // // if this is the last component of the name if (i == cs.size() - 1) { String val = item.getValue(); ObjectNode obj = (ObjectNode) nested; if (SET == item.getType()) { JsonNode valNode = valueToJsonNode(val); if (c.isArray() || attr.isAppend()) { JsonNode list = obj.get(c.getName()); // child expected to be an array if ( ! (list instanceof ArrayNode)) { // replace with new array list = MAPPER.createArrayNode(); obj.set(c.getName(), list); } setArrayItem((ArrayNode) list, c.getIndex(), valNode); } else { ((ObjectNode) nested).set(c.getName(), valNode); } } else { // type == DELETE if (c.isArray()) { JsonNode list = obj.get(c.getName()); // child expected to be an array if (list instanceof ArrayNode) { removeArrayItem((ArrayNode) list, c.getIndex()); } } else { obj.remove(c.getName()); } } } else { // get child and // if exist set nested to child // else create new empty object or array - depending on c.isArray() JsonNode node = nested.get(c.getName()); if (node == null) { if (c.isArray()) { node = MAPPER.createArrayNode(); } else { node = MAPPER.createObjectNode(); } ((ObjectNode) nested).set(c.getName(), node); } nested = node; } } } } private static void setArrayItem(ArrayNode list, int index, JsonNode valNode) { if (index == -1) { // append to end of array list.add(valNode); return; } // make sure items up to index exist for (int i = list.size(); i < index+1; i++) { list.add(NullNode.instance); } list.set(index, valNode); } private static void removeArrayItem(ArrayNode list, int index) { if (index == -1) { throw new IllegalArgumentException("Internal error - should never be called with index == -1"); } list.remove(index); } private static JsonNode valueToJsonNode(String val) { // try get value as JSON object try { return MAPPER.readValue(val, ObjectNode.class); } catch (Exception ignored) { } // try get value as JSON array try { return MAPPER.readValue(val, ArrayNode.class); } catch (Exception ignored) { } if (isBoolean(val)) { return BooleanNode.valueOf(Boolean.valueOf(val)); } else if (isInteger(val)) { return LongNode.valueOf(Long.valueOf(val)); } else if (isNumber(val)) { return DoubleNode.valueOf(Double.valueOf(val)); } else if (isQuoted(val)) { return TextNode.valueOf(unquote(val)); } return TextNode.valueOf(val); } private static boolean isInteger(String val) { try { Long.valueOf(val); return true; } catch (Exception ignored) { return false; } } private static boolean isNumber(String val) { try { Double.valueOf(val); return true; } catch (Exception ignored) { return false; } } private static boolean isBoolean(String val) { return "false".equals(val) || "true".equals(val); } private static boolean isQuoted(String val) { return val.startsWith("'") || val.startsWith("\""); } private static String unquote(String val) { if (!(val.startsWith("'") || val.startsWith("\"")) || !(val.endsWith("'") || val.endsWith("\""))) { throw new RuntimeException("Invalid string value: " + val); } return val.substring(1, val.length()-1); } public static void merge(JsonNode source, ObjectNode dest) { // Iterate over source // For each child check if exists on the destination // if it does go deep // otherwise copy over // if it's last component, set it on destination if (!source.isObject()) { throw new RuntimeException("Not a JSON object: " + source); } Iterator<Map.Entry<String, JsonNode>> it = ((ObjectNode) source).fields(); while (it.hasNext()) { Map.Entry<String, JsonNode> item = it.next(); String name = item.getKey(); JsonNode node = item.getValue(); JsonNode destNode = dest.get(name); if (destNode != null) { if (destNode.isObject()) { if (node.isObject()) { merge(node, (ObjectNode) destNode); } else { throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node); } } else if (destNode.isArray()) { if (node.isArray()) { dest.set(name, node); } else { throw new RuntimeException("Attribute is of incompatible type - " + name + ": " + node); } } else { dest.set(name, node); } } else { dest.set(name, node); } } } }