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}