001/* 002 * VM-Operator 003 * Copyright (C) 2023 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.vmoperator.util; 020 021import java.lang.reflect.Array; 022import java.lang.reflect.InvocationTargetException; 023import java.lang.reflect.Method; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.logging.Logger; 029 030/** 031 * Utility class that supports navigation through arbitrary data structures. 032 */ 033public final class DataPath { 034 035 private static final Logger logger 036 = Logger.getLogger(DataPath.class.getName()); 037 038 private DataPath() { 039 } 040 041 /** 042 * Apply the given selectors on the given object and return the 043 * value reached. 044 * 045 * Selectors can be if type {@link String} or {@link Number}. The 046 * former are used to access a property of an object, the latter to 047 * access an element in an array or a {@link List}. 048 * 049 * Depending on the object currently visited, a {@link String} can 050 * be the key of a {@link Map}, the property part of a getter method 051 * or the name of a method that has an empty parameter list. 052 * 053 * @param <T> the generic type 054 * @param from the from 055 * @param selectors the selectors 056 * @return the result 057 */ 058 public static <T> Optional<T> get(Object from, Object... selectors) { 059 Object cur = from; 060 for (var selector : selectors) { 061 if (cur == null) { 062 return Optional.empty(); 063 } 064 if (selector instanceof String && cur instanceof Map map) { 065 cur = map.get(selector); 066 continue; 067 } 068 if (selector instanceof Number index && cur instanceof List list) { 069 cur = list.get(index.intValue()); 070 continue; 071 } 072 if (selector instanceof String property) { 073 var retrieved = tryAccess(cur, property); 074 if (retrieved.isEmpty()) { 075 return Optional.empty(); 076 } 077 cur = retrieved.get(); 078 } 079 } 080 @SuppressWarnings("unchecked") 081 var result = Optional.ofNullable((T) cur); 082 return result; 083 } 084 085 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 086 private static Optional<Object> tryAccess(Object obj, String property) { 087 Method acc = null; 088 try { 089 // Try getter 090 acc = obj.getClass().getMethod("get" + property.substring(0, 1) 091 .toUpperCase() + property.substring(1)); 092 } catch (SecurityException e) { 093 return Optional.empty(); 094 } catch (NoSuchMethodException e) { // NOPMD 095 // Can happen... 096 } 097 if (acc == null) { 098 try { 099 // Try method 100 acc = obj.getClass().getMethod(property); 101 } catch (SecurityException | NoSuchMethodException e) { 102 return Optional.empty(); 103 } 104 } 105 if (acc != null) { 106 try { 107 return Optional.ofNullable(acc.invoke(obj)); 108 } catch (IllegalAccessException 109 | InvocationTargetException e) { 110 return Optional.empty(); 111 } 112 } 113 return Optional.empty(); 114 } 115 116 /** 117 * Attempts to make a as-deep-as-possible copy of the given 118 * container. New containers will be created for Maps, Lists and 119 * Arrays. The method is invoked recursively for the entries/items. 120 * 121 * If invoked with an object that is neither a map, list or array, 122 * the methods checks if the object implements {@link Cloneable} 123 * and if it does, invokes its {@link Object#clone()} method. 124 * Else the method return the object. 125 * 126 * @param <T> the generic type 127 * @param object the container 128 * @return the t 129 */ 130 @SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" }) 131 public static <T> T deepCopy(T object) { 132 if (object instanceof Map map) { 133 Map<Object, Object> copy; 134 try { 135 copy = (Map<Object, Object>) object.getClass().getConstructor() 136 .newInstance(); 137 } catch (InstantiationException | IllegalAccessException 138 | IllegalArgumentException | InvocationTargetException 139 | NoSuchMethodException | SecurityException e) { 140 logger.severe( 141 () -> "Cannot create new instance of " + object.getClass()); 142 return null; 143 } 144 for (var entry : ((Map<?, ?>) map).entrySet()) { 145 copy.put(entry.getKey(), 146 deepCopy(entry.getValue())); 147 } 148 return (T) copy; 149 } 150 if (object instanceof List list) { 151 List<Object> copy = new ArrayList<>(); 152 for (var item : list) { 153 copy.add(deepCopy(item)); 154 } 155 return (T) copy; 156 } 157 if (object.getClass().isArray()) { 158 var copy = Array.newInstance(object.getClass().getComponentType(), 159 Array.getLength(object)); 160 for (int i = 0; i < Array.getLength(object); i++) { 161 Array.set(copy, i, deepCopy(Array.get(object, i))); 162 } 163 return (T) copy; 164 } 165 if (object instanceof Cloneable) { 166 try { 167 return (T) object.getClass().getMethod("clone") 168 .invoke(object); 169 } catch (IllegalAccessException | InvocationTargetException 170 | NoSuchMethodException | SecurityException e) { 171 return object; 172 } 173 } 174 return object; 175 } 176}