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 @SuppressWarnings("PMD.FieldNamingConventions") 036 private static final Logger logger 037 = Logger.getLogger(DataPath.class.getName()); 038 039 private DataPath() { 040 } 041 042 /** 043 * Apply the given selectors on the given object and return the 044 * value reached. 045 * 046 * Selectors can be if type {@link String} or {@link Number}. The 047 * former are used to access a property of an object, the latter to 048 * access an element in an array or a {@link List}. 049 * 050 * Depending on the object currently visited, a {@link String} can 051 * be the key of a {@link Map}, the property part of a getter method 052 * or the name of a method that has an empty parameter list. 053 * 054 * @param <T> the generic type 055 * @param from the from 056 * @param selectors the selectors 057 * @return the result 058 */ 059 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 060 public static <T> Optional<T> get(Object from, Object... selectors) { 061 Object cur = from; 062 for (var selector : selectors) { 063 if (cur == null) { 064 return Optional.empty(); 065 } 066 if (selector instanceof String && cur instanceof Map map) { 067 cur = map.get(selector); 068 continue; 069 } 070 if (selector instanceof Number index && cur instanceof List list) { 071 cur = list.get(index.intValue()); 072 continue; 073 } 074 if (selector instanceof String property) { 075 var retrieved = tryAccess(cur, property); 076 if (retrieved.isEmpty()) { 077 return Optional.empty(); 078 } 079 cur = retrieved.get(); 080 } 081 } 082 @SuppressWarnings("unchecked") 083 var result = Optional.ofNullable((T) cur); 084 return result; 085 } 086 087 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 088 private static Optional<Object> tryAccess(Object obj, String property) { 089 Method acc = null; 090 try { 091 // Try getter 092 acc = obj.getClass().getMethod("get" + property.substring(0, 1) 093 .toUpperCase() + property.substring(1)); 094 } catch (SecurityException e) { 095 return Optional.empty(); 096 } catch (NoSuchMethodException e) { // NOPMD 097 // Can happen... 098 } 099 if (acc == null) { 100 try { 101 // Try method 102 acc = obj.getClass().getMethod(property); 103 } catch (SecurityException | NoSuchMethodException e) { 104 return Optional.empty(); 105 } 106 } 107 if (acc != null) { 108 try { 109 return Optional.ofNullable(acc.invoke(obj)); 110 } catch (IllegalAccessException 111 | InvocationTargetException e) { 112 return Optional.empty(); 113 } 114 } 115 return Optional.empty(); 116 } 117 118 /** 119 * Attempts to make a as-deep-as-possible copy of the given 120 * container. New containers will be created for Maps, Lists and 121 * Arrays. The method is invoked recursively for the entries/items. 122 * 123 * If invoked with an object that is neither a map, list or array, 124 * the methods checks if the object implements {@link Cloneable} 125 * and if it does, invokes its {@link Object#clone()} method. 126 * Else the method return the object. 127 * 128 * @param <T> the generic type 129 * @param object the container 130 * @return the t 131 */ 132 @SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" }) 133 public static <T> T deepCopy(T object) { 134 if (object instanceof Map map) { 135 @SuppressWarnings("PMD.UseConcurrentHashMap") 136 Map<Object, Object> copy; 137 try { 138 copy = (Map<Object, Object>) object.getClass().getConstructor() 139 .newInstance(); 140 } catch (InstantiationException | IllegalAccessException 141 | IllegalArgumentException | InvocationTargetException 142 | NoSuchMethodException | SecurityException e) { 143 logger.severe( 144 () -> "Cannot create new instance of " + object.getClass()); 145 return null; 146 } 147 for (var entry : ((Map<?, ?>) map).entrySet()) { 148 copy.put(entry.getKey(), 149 deepCopy(entry.getValue())); 150 } 151 return (T) copy; 152 } 153 if (object instanceof List list) { 154 List<Object> copy = new ArrayList<>(); 155 for (var item : list) { 156 copy.add(deepCopy(item)); 157 } 158 return (T) copy; 159 } 160 if (object.getClass().isArray()) { 161 var copy = Array.newInstance(object.getClass().getComponentType(), 162 Array.getLength(object)); 163 for (int i = 0; i < Array.getLength(object); i++) { 164 Array.set(copy, i, deepCopy(Array.get(object, i))); 165 } 166 return (T) copy; 167 } 168 if (object instanceof Cloneable) { 169 try { 170 return (T) object.getClass().getMethod("clone") 171 .invoke(object); 172 } catch (IllegalAccessException | InvocationTargetException 173 | NoSuchMethodException | SecurityException e) { 174 return object; 175 } 176 } 177 return object; 178 } 179}