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/** 026 * Class to implement a MapAccessor to work with nested Maps / values 027 */ 028@SuppressWarnings("unchecked") 029public class YamlMapAccessor { 030 031 private final Map<Object, Object> map; 032 033 /** 034 * Constructor 035 * 036 * @param map map 037 */ 038 public YamlMapAccessor(Map<Object, Object> map) { 039 if (map == null) { 040 throw new IllegalArgumentException("Map is null"); 041 } 042 043 this.map = map; 044 } 045 046 /** 047 * Method to determine if a path exists 048 * 049 * @param path path 050 * @return true if the path exists (but could be null), false otherwise 051 */ 052 public boolean containsPath(String path) { 053 if (path == null || path.trim().isEmpty()) { 054 throw new IllegalArgumentException( 055 String.format("path [%s] is invalid", path)); 056 } 057 058 path = validatePath(path); 059 if (path.equals("/")) { 060 return true; 061 } 062 063 String[] pathTokens = path.split(Pattern.quote("/")); 064 Map<Object, Object> subMap = map; 065 066 for (int i = 1; i < pathTokens.length; i++) { 067 try { 068 if (subMap.containsKey(pathTokens[i])) { 069 subMap = (Map<Object, Object>) subMap.get(pathTokens[i]); 070 } else { 071 return false; 072 } 073 } catch (NullPointerException e) { 074 return false; 075 } catch (ClassCastException e) { 076 return false; 077 } 078 } 079 080 return true; 081 } 082 083 /** 084 * Method to get a path Object 085 * 086 * @param path path 087 * @return an Optional containing the path Object or an empty Optional if the path doesn't exist 088 */ 089 public Optional<Object> get(String path) { 090 if (path == null || path.trim().isEmpty()) { 091 throw new IllegalArgumentException( 092 String.format("path [%s] is invalid", path)); 093 } 094 095 path = validatePath(path); 096 if (path.equals("/")) { 097 return Optional.of(map); 098 } 099 100 String[] pathTokens = path.split(Pattern.quote("/")); 101 Object object = map; 102 103 for (int i = 1; i < pathTokens.length; i++) { 104 try { 105 object = resolve(pathTokens[i], object); 106 } catch (NullPointerException e) { 107 return Optional.ofNullable(null); 108 } catch (ClassCastException e) { 109 return Optional.ofNullable(null); 110 } 111 } 112 113 return Optional.ofNullable(object); 114 } 115 116 /** 117 * Method to get a path Object or create an Object using the Supplier 118 * <p> 119 * parent paths will be created if required 120 * 121 * @param path path 122 * @return an Optional containing the path Object or Optional created by the Supplier 123 */ 124 public Optional<Object> getOrCreate(String path, Supplier<Object> supplier) { 125 if (path == null || path.trim().isEmpty()) { 126 throw new IllegalArgumentException( 127 String.format("path [%s] is invalid", path)); 128 } 129 130 path = validatePath(path); 131 if (path.equals("/")) { 132 return Optional.of(map); 133 } 134 135 if (supplier == null) { 136 throw new IllegalArgumentException("supplier is null"); 137 } 138 139 String[] pathTokens = path.split(Pattern.quote("/")); 140 Object previous = map; 141 Object current = null; 142 143 for (int i = 1; i < pathTokens.length; i++) { 144 try { 145 current = resolve(pathTokens[i], previous); 146 if (current == null) { 147 if ((i + 1) == pathTokens.length) { 148 Object object = supplier.get(); 149 ((Map<String, Object>) previous).put(pathTokens[i], object); 150 return Optional.of(object); 151 } else { 152 current = new LinkedHashMap<String, Object>(); 153 ((Map<String, Object>) previous).put(pathTokens[i], current); 154 } 155 } 156 previous = current; 157 } catch (NullPointerException e) { 158 return Optional.ofNullable(null); 159 } catch (ClassCastException e) { 160 if ((i + 1) == pathTokens.length) { 161 throw new IllegalArgumentException( 162 String.format("path [%s] isn't a Map", flatten(pathTokens, 1, i))); 163 } 164 return Optional.ofNullable(null); 165 } 166 } 167 168 return Optional.ofNullable(current); 169 } 170 171 /** 172 * Method to get a path Object, throwing an RuntimeException created by the Supplier if the path doesn't exist 173 * 174 * @param path path 175 * @return an Optional containing the path Object 176 */ 177 public Optional<Object> getOrThrow(String path, Supplier<? extends RuntimeException> supplier) { 178 if (path == null || path.trim().isEmpty()) { 179 throw new IllegalArgumentException( 180 String.format("path [%s] is invalid", path)); 181 } 182 183 if (supplier == null) { 184 throw new IllegalArgumentException("supplier is null"); 185 } 186 187 path = validatePath(path); 188 if (path.equals("/")) { 189 return Optional.of(map); 190 } 191 192 String[] pathTokens = path.split(Pattern.quote("/")); 193 Object object = map; 194 195 for (int i = 1; i < pathTokens.length; i++) { 196 try { 197 object = resolve(pathTokens[i], object); 198 } catch (NullPointerException e) { 199 throw supplier.get(); 200 } catch (ClassCastException e) { 201 throw supplier.get(); 202 } 203 204 if (object == null && i < pathTokens.length) { 205 throw supplier.get(); 206 } 207 } 208 209 return Optional.ofNullable(object); 210 } 211 212 /** 213 * Method to get a MapAccessor backed by an empty Map 214 * 215 * @return the return value 216 */ 217 public static YamlMapAccessor empty() { 218 return new YamlMapAccessor(new LinkedHashMap<>()); 219 } 220 221 /** 222 * Method to validate a path 223 * 224 * @param path path 225 * @return the return value 226 */ 227 private String validatePath(String path) { 228 if (path == null) { 229 throw new IllegalArgumentException("path is null"); 230 } 231 232 if (path.equals("/")) { 233 return path; 234 } 235 236 path = path.trim(); 237 238 if (path.isEmpty()) { 239 throw new IllegalArgumentException("path is empty"); 240 } 241 242 if (!path.startsWith("/")) { 243 throw new IllegalArgumentException(String.format("path [%s] is invalid", path)); 244 } 245 246 if (path.endsWith("/")) { 247 throw new IllegalArgumentException(String.format("path [%s] is invalid", path)); 248 } 249 250 return path; 251 } 252 253 /** 254 * Method to resolve a path token to an Object 255 * 256 * @param pathToken pathToken 257 * @param object object 258 * @return the return value 259 * @param <T> the return type 260 */ 261 private <T> T resolve(String pathToken, Object object) { 262 return (T) ((Map<String, Object>) object).get(pathToken); 263 } 264 265 /** 266 * Method to flatten an array of path tokens to a path 267 * 268 * @param pathTokens pathTokens 269 * @param begin begin 270 * @param end end 271 * @return the return value 272 */ 273 private String flatten(String[] pathTokens, int begin, int end) { 274 StringBuilder stringBuilder = new StringBuilder(); 275 for (int i = begin; i < end; i++) { 276 stringBuilder.append("/").append(pathTokens[i]); 277 } 278 279 return stringBuilder.toString(); 280 } 281}