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