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}