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}