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}