001/*
002 * Copyright (C) 2022-present 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     * @param supplier supplier
115     * @return an Optional containing the path Object or Optional created by the Supplier
116     */
117    public Optional<Object> getOrCreate(String path, Supplier<Object> supplier) {
118        if (path == null || path.trim().isEmpty()) {
119            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
120        }
121
122        path = validatePath(path);
123        if (path.equals("/")) {
124            return Optional.of(map);
125        }
126
127        if (supplier == null) {
128            throw new IllegalArgumentException("supplier is null");
129        }
130
131        String[] pathTokens = path.split(Pattern.quote("/"));
132        Object previous = map;
133        Object current = null;
134
135        for (int i = 1; i < pathTokens.length; i++) {
136            try {
137                current = resolve(pathTokens[i], previous);
138                if (current == null) {
139                    if ((i + 1) == pathTokens.length) {
140                        Object object = supplier.get();
141                        ((Map<String, Object>) previous).put(pathTokens[i], object);
142                        return Optional.of(object);
143                    } else {
144                        current = new LinkedHashMap<>();
145                        ((Map<String, Object>) previous).put(pathTokens[i], current);
146                    }
147                }
148                previous = current;
149            } catch (NullPointerException e) {
150                return Optional.empty();
151            } catch (ClassCastException e) {
152                if ((i + 1) == pathTokens.length) {
153                    throw new IllegalArgumentException(
154                            String.format("path [%s] isn't a Map", flatten(pathTokens, 1, i)));
155                }
156                return Optional.empty();
157            }
158        }
159
160        return Optional.ofNullable(current);
161    }
162
163    /**
164     * Method to get a path Object, throwing an RuntimeException created by the Supplier if the path
165     * doesn't exist
166     *
167     * @param path path
168     * @param supplier supplier
169     * @return an Optional containing the path Object
170     */
171    public Optional<Object> getOrThrow(String path, Supplier<? extends RuntimeException> supplier) {
172        if (path == null || path.trim().isEmpty()) {
173            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
174        }
175
176        if (supplier == null) {
177            throw new IllegalArgumentException("supplier is null");
178        }
179
180        path = validatePath(path);
181        if (path.equals("/")) {
182            return Optional.of(map);
183        }
184
185        String[] pathTokens = path.split(Pattern.quote("/"));
186        Object object = map;
187
188        for (int i = 1; i < pathTokens.length; i++) {
189            try {
190                object = resolve(pathTokens[i], object);
191            } catch (NullPointerException | ClassCastException e) {
192                throw supplier.get();
193            }
194
195            if (object == null) {
196                throw supplier.get();
197            }
198        }
199
200        return Optional.of(object);
201    }
202
203    /**
204     * Method to get a MapAccessor backed by an empty Map
205     *
206     * @return the return value
207     */
208    public static YamlMapAccessor empty() {
209        return new YamlMapAccessor(new LinkedHashMap<>());
210    }
211
212    /**
213     * Method to validate a path
214     *
215     * @param path path
216     * @return the return value
217     */
218    private String validatePath(String path) {
219        if (path == null) {
220            throw new IllegalArgumentException("path is null");
221        }
222
223        if (path.equals("/")) {
224            return path;
225        }
226
227        path = path.trim();
228
229        if (path.isEmpty()) {
230            throw new IllegalArgumentException("path is empty");
231        }
232
233        if (!path.startsWith("/")) {
234            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
235        }
236
237        if (path.endsWith("/")) {
238            throw new IllegalArgumentException(String.format("path [%s] is invalid", path));
239        }
240
241        return path;
242    }
243
244    /**
245     * Method to resolve a path token to an Object
246     *
247     * @param pathToken pathToken
248     * @param object object
249     * @return the return value
250     * @param <T> the return type
251     */
252    private <T> T resolve(String pathToken, Object object) {
253        return (T) ((Map<String, Object>) object).get(pathToken);
254    }
255
256    /**
257     * Method to flatten an array of path tokens to a path
258     *
259     * @param pathTokens pathTokens
260     * @param begin begin
261     * @param end end
262     * @return the return value
263     */
264    private String flatten(String[] pathTokens, int begin, int end) {
265        StringBuilder stringBuilder = new StringBuilder();
266        for (int i = begin; i < end; i++) {
267            stringBuilder.append("/").append(pathTokens[i]);
268        }
269
270        return stringBuilder.toString();
271    }
272}