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}