001/* 002 * VM-Operator 003 * Copyright (C) 2023 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.vmoperator.util; 020 021import com.google.gson.JsonArray; 022import com.google.gson.JsonElement; 023import com.google.gson.JsonObject; 024import com.google.gson.JsonPrimitive; 025import java.math.BigInteger; 026import java.util.Arrays; 027import java.util.Collections; 028import java.util.List; 029import java.util.Optional; 030import java.util.function.Supplier; 031 032/** 033 * Utility class for pointing to elements on a Gson (Json) tree. 034 */ 035@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", 036 "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" }) 037public class GsonPtr { 038 039 private final JsonElement position; 040 041 private GsonPtr(JsonElement root) { 042 this.position = root; 043 } 044 045 /** 046 * Create a new instance pointing to the given element. 047 * 048 * @param root the root 049 * @return the Gson pointer 050 */ 051 @SuppressWarnings("PMD.ShortMethodName") 052 public static GsonPtr to(JsonElement root) { 053 return new GsonPtr(root); 054 } 055 056 /** 057 * Create a new instance pointing to the {@link JsonElement} 058 * selected by the given selectors. If a selector of type 059 * {@link String} denotes a non-existant member of a 060 * {@link JsonObject}, a new member (of type {@link JsonObject} 061 * is added. 062 * 063 * @param selectors the selectors 064 * @return the Gson pointer 065 */ 066 @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace", 067 "PMD.AvoidDuplicateLiterals" }) 068 public GsonPtr to(Object... selectors) { 069 JsonElement element = position; 070 for (Object sel : selectors) { 071 if (element instanceof JsonObject obj 072 && sel instanceof String member) { 073 element = Optional.ofNullable(obj.get(member)).orElseGet(() -> { 074 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 075 var child = new JsonObject(); 076 obj.add(member, child); 077 return child; 078 }); 079 continue; 080 } 081 if (element instanceof JsonArray arr 082 && sel instanceof Integer index) { 083 try { 084 element = arr.get(index); 085 } catch (IndexOutOfBoundsException e) { 086 throw new IllegalStateException("Selected array index" 087 + " may not be empty."); 088 } 089 continue; 090 } 091 throw new IllegalStateException("Invalid selection"); 092 } 093 return new GsonPtr(element); 094 } 095 096 /** 097 * Create a new instance pointing to the {@link JsonElement} 098 * selected by the given selectors. If a selector of type 099 * {@link String} denotes a non-existant member of a 100 * {@link JsonObject} the result is empty. 101 * 102 * @param selectors the selectors 103 * @return the Gson pointer 104 */ 105 @SuppressWarnings({ "PMD.ShortMethodName", "PMD.PreserveStackTrace" }) 106 public Optional<GsonPtr> get(Object... selectors) { 107 JsonElement element = position; 108 for (Object sel : selectors) { 109 if (element instanceof JsonObject obj 110 && sel instanceof String member) { 111 element = obj.get(member); 112 if (element == null) { 113 return Optional.empty(); 114 } 115 continue; 116 } 117 if (element instanceof JsonArray arr 118 && sel instanceof Integer index) { 119 try { 120 element = arr.get(index); 121 } catch (IndexOutOfBoundsException e) { 122 throw new IllegalStateException("Selected array index" 123 + " may not be empty."); 124 } 125 continue; 126 } 127 throw new IllegalStateException("Invalid selection"); 128 } 129 return Optional.of(new GsonPtr(element)); 130 } 131 132 /** 133 * Returns {@link JsonElement} that the pointer points to. 134 * 135 * @return the result 136 */ 137 public JsonElement get() { 138 return position; 139 } 140 141 /** 142 * Returns {@link JsonElement} that the pointer points to, 143 * casted to the given type. 144 * 145 * @param <T> the generic type 146 * @param cls the cls 147 * @return the result 148 */ 149 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) 150 public <T extends JsonElement> T getAs(Class<T> cls) { 151 if (cls.isAssignableFrom(position.getClass())) { 152 return cls.cast(position); 153 } 154 throw new IllegalArgumentException("Not positioned at element" 155 + " of desired type."); 156 } 157 158 /** 159 * Returns the selected {@link JsonElement}, cast to the class 160 * specified. 161 * 162 * @param <T> the generic type 163 * @param cls the cls 164 * @param selectors the selectors 165 * @return the optional 166 */ 167 @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" }) 168 public <T extends JsonElement> Optional<T> 169 getAs(Class<T> cls, Object... selectors) { 170 JsonElement element = position; 171 for (Object sel : selectors) { 172 if (element instanceof JsonObject obj 173 && sel instanceof String member) { 174 element = obj.get(member); 175 if (element == null) { 176 return Optional.empty(); 177 } 178 continue; 179 } 180 if (element instanceof JsonArray arr 181 && sel instanceof Integer index) { 182 try { 183 element = arr.get(index); 184 } catch (IndexOutOfBoundsException e) { 185 return Optional.empty(); 186 } 187 continue; 188 } 189 return Optional.empty(); 190 } 191 if (cls.isAssignableFrom(element.getClass())) { 192 return Optional.of(cls.cast(element)); 193 } 194 return Optional.empty(); 195 } 196 197 /** 198 * Returns the String value of the selected {@link JsonPrimitive}. 199 * 200 * @param selectors the selectors 201 * @return the as string 202 */ 203 public Optional<String> getAsString(Object... selectors) { 204 return getAs(JsonPrimitive.class, selectors) 205 .map(JsonPrimitive::getAsString); 206 } 207 208 /** 209 * Returns the Integer value of the selected {@link JsonPrimitive}. 210 * 211 * @param selectors the selectors 212 * @return the as string 213 */ 214 public Optional<Integer> getAsInt(Object... selectors) { 215 return getAs(JsonPrimitive.class, selectors) 216 .map(JsonPrimitive::getAsInt); 217 } 218 219 /** 220 * Returns the Integer value of the selected {@link JsonPrimitive}. 221 * 222 * @param selectors the selectors 223 * @return the as string 224 */ 225 public Optional<BigInteger> getAsBigInteger(Object... selectors) { 226 return getAs(JsonPrimitive.class, selectors) 227 .map(JsonPrimitive::getAsBigInteger); 228 } 229 230 /** 231 * Returns the Long value of the selected {@link JsonPrimitive}. 232 * 233 * @param selectors the selectors 234 * @return the as string 235 */ 236 public Optional<Long> getAsLong(Object... selectors) { 237 return getAs(JsonPrimitive.class, selectors) 238 .map(JsonPrimitive::getAsLong); 239 } 240 241 /** 242 * Returns the boolean value of the selected {@link JsonPrimitive}. 243 * 244 * @param selectors the selectors 245 * @return the boolean 246 */ 247 public Optional<Boolean> getAsBoolean(Object... selectors) { 248 return getAs(JsonPrimitive.class, selectors) 249 .map(JsonPrimitive::getAsBoolean); 250 } 251 252 /** 253 * Returns the elements of the selected {@link JsonArray} as list. 254 * 255 * @param <T> the generic type 256 * @param cls the cls 257 * @param selectors the selectors 258 * @return the list 259 */ 260 @SuppressWarnings("unchecked") 261 public <T extends JsonElement> List<T> getAsListOf(Class<T> cls, 262 Object... selectors) { 263 return getAs(JsonArray.class, selectors).map(a -> (List<T>) a.asList()) 264 .orElse(Collections.emptyList()); 265 } 266 267 /** 268 * Sets the selected value. This pointer must point to a 269 * {@link JsonObject} or {@link JsonArray}. The selector must 270 * be a {@link String} or an integer respectively. 271 * 272 * @param selector the selector 273 * @param value the value 274 * @return the Gson pointer 275 */ 276 public GsonPtr set(Object selector, JsonElement value) { 277 if (position instanceof JsonObject obj 278 && selector instanceof String member) { 279 obj.add(member, value); 280 return this; 281 } 282 if (position instanceof JsonArray arr 283 && selector instanceof Integer index) { 284 if (index >= arr.size()) { 285 arr.add(value); 286 } else { 287 arr.set(index, value); 288 } 289 return this; 290 } 291 throw new IllegalStateException("Invalid selection"); 292 } 293 294 /** 295 * Short for `set(selector, new JsonPrimitive(value))`. 296 * 297 * @param selector the selector 298 * @param value the value 299 * @return the gson ptr 300 * @see #set(Object, JsonElement) 301 */ 302 public GsonPtr set(Object selector, String value) { 303 return set(selector, new JsonPrimitive(value)); 304 } 305 306 /** 307 * Short for `set(selector, new JsonPrimitive(value))`. 308 * 309 * @param selector the selector 310 * @param value the value 311 * @return the gson ptr 312 * @see #set(Object, JsonElement) 313 */ 314 public GsonPtr set(Object selector, Long value) { 315 return set(selector, new JsonPrimitive(value)); 316 } 317 318 /** 319 * Short for `set(selector, new JsonPrimitive(value))`. 320 * 321 * @param selector the selector 322 * @param value the value 323 * @return the gson ptr 324 * @see #set(Object, JsonElement) 325 */ 326 public GsonPtr set(Object selector, BigInteger value) { 327 return set(selector, new JsonPrimitive(value)); 328 } 329 330 /** 331 * Same as {@link #set(Object, JsonElement)}, but sets the value 332 * only if it doesn't exist yet, else returns the existing value. 333 * If this pointer points to a {@link JsonArray} and the selector 334 * if larger than or equal to the size of the array, the supplied 335 * value will be appended. 336 * 337 * @param <T> the generic type 338 * @param selector the selector 339 * @param supplier the supplier of the missing value 340 * @return the existing or supplied value 341 */ 342 @SuppressWarnings("unchecked") 343 public <T extends JsonElement> T 344 computeIfAbsent(Object selector, Supplier<T> supplier) { 345 if (position instanceof JsonObject obj 346 && selector instanceof String member) { 347 return Optional.ofNullable((T) obj.get(member)).orElseGet(() -> { 348 var res = supplier.get(); 349 obj.add(member, res); 350 return res; 351 }); 352 } 353 if (position instanceof JsonArray arr 354 && selector instanceof Integer index) { 355 if (index >= arr.size()) { 356 var res = supplier.get(); 357 arr.add(res); 358 return res; 359 } 360 return (T) arr.get(index); 361 } 362 throw new IllegalStateException("Invalid selection"); 363 } 364 365 /** 366 * Short for `computeIfAbsent(selector, () -> new JsonPrimitive(value))`. 367 * 368 * @param selector the selector 369 * @param value the value 370 * @return the Gson pointer 371 */ 372 public GsonPtr getOrSet(Object selector, String value) { 373 computeIfAbsent(selector, () -> new JsonPrimitive(value)); 374 return this; 375 } 376 377 /** 378 * Removes all properties except the specified ones. 379 * 380 * @param properties the properties 381 */ 382 public void removeExcept(String... properties) { 383 if (!position.isJsonObject()) { 384 return; 385 } 386 for (var itr = ((JsonObject) position).entrySet().iterator(); 387 itr.hasNext();) { 388 var entry = itr.next(); 389 if (Arrays.asList(properties).contains(entry.getKey())) { 390 continue; 391 } 392 itr.remove(); 393 } 394 } 395}