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.InvocationTargetException; 022import java.lang.reflect.Method; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.logging.Logger; 028 029/** 030 * Utility class that supports navigation through arbitrary data structures. 031 */ 032public final class DataPath { 033 034 @SuppressWarnings("PMD.FieldNamingConventions") 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 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 059 public static <T> Optional<T> get(Object from, Object... selectors) { 060 Object cur = from; 061 for (var selector : selectors) { 062 if (cur == null) { 063 return Optional.empty(); 064 } 065 if (selector instanceof String && cur instanceof Map map) { 066 cur = map.get(selector); 067 continue; 068 } 069 if (selector instanceof Number index && cur instanceof List list) { 070 cur = list.get(index.intValue()); 071 continue; 072 } 073 if (selector instanceof String property) { 074 var retrieved = tryAccess(cur, property); 075 if (retrieved.isEmpty()) { 076 return Optional.empty(); 077 } 078 cur = retrieved.get(); 079 } 080 } 081 @SuppressWarnings("unchecked") 082 var result = Optional.ofNullable((T) cur); 083 return result; 084 } 085 086 @SuppressWarnings("PMD.UseLocaleWithCaseConversions") 087 private static Optional<Object> tryAccess(Object obj, String property) { 088 Method acc = null; 089 try { 090 // Try getter 091 acc = obj.getClass().getMethod("get" + property.substring(0, 1) 092 .toUpperCase() + property.substring(1)); 093 } catch (SecurityException e) { 094 return Optional.empty(); 095 } catch (NoSuchMethodException e) { // NOPMD 096 // Can happen... 097 } 098 if (acc == null) { 099 try { 100 // Try method 101 acc = obj.getClass().getMethod(property); 102 } catch (SecurityException | NoSuchMethodException e) { 103 return Optional.empty(); 104 } 105 } 106 if (acc != null) { 107 try { 108 return Optional.ofNullable(acc.invoke(obj)); 109 } catch (IllegalAccessException 110 | InvocationTargetException e) { 111 return Optional.empty(); 112 } 113 } 114 return Optional.empty(); 115 } 116 117 /** 118 * Attempts to make a as-deep-as-possible copy of the given 119 * container. New containers will be created for Maps, Lists and 120 * Arrays. The method is invoked recursively for the entries/items. 121 * 122 * If invoked with an object that is neither a map, list or array, 123 * the methods checks if the object implements {@link Cloneable} 124 * and if it does, invokes its {@link Object#clone()} method. 125 * Else the method return the object. 126 * 127 * @param <T> the generic type 128 * @param object the container 129 * @return the t 130 */ 131 @SuppressWarnings({ "PMD.CognitiveComplexity", "unchecked" }) 132 public static <T> T deepCopy(T object) { 133 if (object instanceof Map map) { 134 @SuppressWarnings("PMD.UseConcurrentHashMap") 135 Map<Object, Object> copy; 136 try { 137 copy = (Map<Object, Object>) object.getClass().getConstructor() 138 .newInstance(); 139 } catch (InstantiationException | IllegalAccessException 140 | IllegalArgumentException | InvocationTargetException 141 | NoSuchMethodException | SecurityException e) { 142 logger.severe( 143 () -> "Cannot create new instance of " + object.getClass()); 144 return null; 145 } 146 for (var entry : ((Map<?, ?>) map).entrySet()) { 147 copy.put(entry.getKey(), 148 deepCopy(entry.getValue())); 149 } 150 return (T) copy; 151 } 152 if (object instanceof List list) { 153 List<Object> copy = new ArrayList<>(); 154 for (var item : list) { 155 copy.add(deepCopy(item)); 156 } 157 return (T) copy; 158 } 159 if (object.getClass().isArray()) { 160 var copy = new ArrayList<>(); 161 for (var item : (Object[]) object) { 162 copy.add(deepCopy(item)); 163 } 164 return (T) copy.toArray(); 165 } 166 if (object instanceof Cloneable) { 167 try { 168 return (T) object.getClass().getMethod("clone") 169 .invoke(object); 170 } catch (IllegalAccessException | InvocationTargetException 171 | NoSuchMethodException | SecurityException e) { 172 return object; 173 } 174 } 175 return object; 176 } 177}