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}