001/*
002 * Copyright (C) 2022-2023 The Prometheus jmx_exporter Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package io.prometheus.jmx.common.yaml;
018
019import java.util.LinkedHashMap;
020import java.util.Map;
021import java.util.Optional;
022import java.util.function.Supplier;
023import java.util.regex.Pattern;
024
025/** Class to implement a MapAccessor to work with nested Maps / values */
026@SuppressWarnings("unchecked")
027public class YamlMapAccessor {
028
029    private final Map<Object, Object> map;
030
031    /**
032     * Constructor
033     *
034     * @param map map
035     */
036    public YamlMapAccessor(Map<Object, Object> map) {
037        if (map == null) {
038            throw new IllegalArgumentException("Map is null");
039        }
040
041        this.map = map;
042    }
043
044    /**
045     * Method to determine if a path exists
046     *
047     * @param path path
048     * @return true if the path exists (but could be null), false otherwise
049     */
050    public boolean containsPath(String path) {
051        if (path == null || path.trim().isEmpty()) {
052            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
053        }
054
055        path = validatePath(path);
056        if (path.equals("/")) {
057            return true;
058        }
059
060        String[] pathTokens = path.split(Pattern.quote("/"));
061        Map<Object, Object> subMap = map;
062
063        for (int i = 1; i < pathTokens.length; i++) {
064            try {
065                if (subMap.containsKey(pathTokens[i])) {
066                    subMap = (Map<Object, Object>) subMap.get(pathTokens[i]);
067                } else {
068                    return false;
069                }
070            } catch (NullPointerException | ClassCastException e) {
071                return false;
072            }
073        }
074
075        return true;
076    }
077
078    /**
079     * Method to get a path Object
080     *
081     * @param path path
082     * @return an Optional containing the path Object or an empty Optional if the path doesn't exist
083     */
084    public Optional<Object> get(String path) {
085        if (path == null || path.trim().isEmpty()) {
086            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
087        }
088
089        path = validatePath(path);
090        if (path.equals("/")) {
091            return Optional.of(map);
092        }
093
094        String[] pathTokens = path.split(Pattern.quote("/"));
095        Object object = map;
096
097        for (int i = 1; i < pathTokens.length; i++) {
098            try {
099                object = resolve(pathTokens[i], object);
100            } catch (NullPointerException | ClassCastException e) {
101                return Optional.empty();
102            }
103        }
104
105        return Optional.ofNullable(object);
106    }
107
108    /**
109     * Method to get a path Object or create an Object using the Supplier
110     *
111     * <p>parent paths will be created if required
112     *
113     * @param path path
114     * @return an Optional containing the path Object or Optional created by the Supplier
115     */
116    public Optional<Object> getOrCreate(String path, Supplier<Object> supplier) {
117        if (path == null || path.trim().isEmpty()) {
118            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
119        }
120
121        path = validatePath(path);
122        if (path.equals("/")) {
123            return Optional.of(map);
124        }
125
126        if (supplier == null) {
127            throw new IllegalArgumentException("supplier is null");
128        }
129
130        String[] pathTokens = path.split(Pattern.quote("/"));
131        Object previous = map;
132        Object current = null;
133
134        for (int i = 1; i < pathTokens.length; i++) {
135            try {
136                current = resolve(pathTokens[i], previous);
137                if (current == null) {
138                    if ((i + 1) == pathTokens.length) {
139                        Object object = supplier.get();
140                        ((Map<String, Object>) previous).put(pathTokens[i], object);
141                        return Optional.of(object);
142                    } else {
143                        current = new LinkedHashMap<>();
144                        ((Map<String, Object>) previous).put(pathTokens[i], current);
145                    }
146                }
147                previous = current;
148            } catch (NullPointerException e) {
149                return Optional.empty();
150            } catch (ClassCastException e) {
151                if ((i + 1) == pathTokens.length) {
152                    throw new IllegalArgumentException(
153                            String.format("path [%s] isn't a Map", flatten(pathTokens, 1, i)));
154                }
155                return Optional.empty();
156            }
157        }
158
159        return Optional.ofNullable(current);
160    }
161
162    /**
163     * Method to get a path Object, throwing an RuntimeException created by the Supplier if the path
164     * doesn't exist
165     *
166     * @param path path
167     * @return an Optional containing the path Object
168     */
169    public Optional<Object> getOrThrow(String path, Supplier<? extends RuntimeException> supplier) {
170        if (path == null || path.trim().isEmpty()) {
171            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
172        }
173
174        if (supplier == null) {
175            throw new IllegalArgumentException("supplier is null");
176        }
177
178        path = validatePath(path);
179        if (path.equals("/")) {
180            return Optional.of(map);
181        }
182
183        String[] pathTokens = path.split(Pattern.quote("/"));
184        Object object = map;
185
186        for (int i = 1; i < pathTokens.length; i++) {
187            try {
188                object = resolve(pathTokens[i], object);
189            } catch (NullPointerException | ClassCastException e) {
190                throw supplier.get();
191            }
192
193            if (object == null) {
194                throw supplier.get();
195            }
196        }
197
198        return Optional.of(object);
199    }
200
201    /**
202     * Method to get a MapAccessor backed by an empty Map
203     *
204     * @return the return value
205     */
206    public static YamlMapAccessor empty() {
207        return new YamlMapAccessor(new LinkedHashMap<>());
208    }
209
210    /**
211     * Method to validate a path
212     *
213     * @param path path
214     * @return the return value
215     */
216    private String validatePath(String path) {
217        if (path == null) {
218            throw new IllegalArgumentException("path is null");
219        }
220
221        if (path.equals("/")) {
222            return path;
223        }
224
225        path = path.trim();
226
227        if (path.isEmpty()) {
228            throw new IllegalArgumentException("path is empty");
229        }
230
231        if (!path.startsWith("/")) {
232            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
233        }
234
235        if (path.endsWith("/")) {
236            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
237        }
238
239        return path;
240    }
241
242    /**
243     * Method to resolve a path token to an Object
244     *
245     * @param pathToken pathToken
246     * @param object object
247     * @return the return value
248     * @param <T> the return type
249     */
250    private <T> T resolve(String pathToken, Object object) {
251        return (T) ((Map<String, Object>) object).get(pathToken);
252    }
253
254    /**
255     * Method to flatten an array of path tokens to a path
256     *
257     * @param pathTokens pathTokens
258     * @param begin begin
259     * @param end end
260     * @return the return value
261     */
262    private String flatten(String[] pathTokens, int begin, int end) {
263        StringBuilder stringBuilder = new StringBuilder();
264        for (int i = begin; i < end; i++) {
265            stringBuilder.append("/").append(pathTokens[i]);
266        }
267
268        return stringBuilder.toString();
269    }
270}