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.Collections;
027import java.util.List;
028import java.util.Optional;
029import java.util.function.Supplier;
030
031/**
032 * Utility class for pointing to elements on a Gson (Json) tree.
033 */
034@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
035    "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal", "PMD.GodClass" })
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    public GsonPtr to(Object... selectors) {
067        JsonElement element = position;
068        for (Object sel : selectors) {
069            if (element instanceof JsonObject obj
070                && sel instanceof String member) {
071                element = Optional.ofNullable(obj.get(member)).orElseGet(() -> {
072                    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
073                    var child = new JsonObject();
074                    obj.add(member, child);
075                    return child;
076                });
077                continue;
078            }
079            if (element instanceof JsonArray arr
080                && sel instanceof Integer index) {
081                try {
082                    element = arr.get(index);
083                } catch (IndexOutOfBoundsException e) {
084                    throw new IllegalStateException("Selected array index"
085                        + " may not be empty.");
086                }
087                continue;
088            }
089            throw new IllegalStateException("Invalid selection");
090        }
091        return new GsonPtr(element);
092    }
093
094    /**
095     * Returns {@link JsonElement} that the pointer points to.
096     *
097     * @return the result
098     */
099    public JsonElement get() {
100        return position;
101    }
102
103    /**
104     * Returns {@link JsonElement} that the pointer points to,
105     * casted to the given type.
106     *
107     * @param <T> the generic type
108     * @param cls the cls
109     * @return the result
110     */
111    @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
112    public <T extends JsonElement> T get(Class<T> cls) {
113        if (cls.isAssignableFrom(position.getClass())) {
114            return cls.cast(position);
115        }
116        throw new IllegalArgumentException("Not positioned at element"
117            + " of desired type.");
118    }
119
120    /**
121     * Returns the selected {@link JsonElement}, cast to the class
122     * specified.
123     *
124     * @param <T> the generic type
125     * @param cls the cls
126     * @param selectors the selectors
127     * @return the optional
128     */
129    @SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
130    public <T extends JsonElement> Optional<T>
131            get(Class<T> cls, Object... selectors) {
132        JsonElement element = position;
133        for (Object sel : selectors) {
134            if (element instanceof JsonObject obj
135                && sel instanceof String member) {
136                element = obj.get(member);
137                if (element == null) {
138                    return Optional.empty();
139                }
140                continue;
141            }
142            if (element instanceof JsonArray arr
143                && sel instanceof Integer index) {
144                try {
145                    element = arr.get(index);
146                } catch (IndexOutOfBoundsException e) {
147                    return Optional.empty();
148                }
149                continue;
150            }
151            return Optional.empty();
152        }
153        if (cls.isAssignableFrom(element.getClass())) {
154            return Optional.of(cls.cast(element));
155        }
156        return Optional.empty();
157    }
158
159    /**
160     * Returns the String value of the selected {@link JsonPrimitive}.
161     *
162     * @param selectors the selectors
163     * @return the as string
164     */
165    public Optional<String> getAsString(Object... selectors) {
166        return get(JsonPrimitive.class, selectors)
167            .map(JsonPrimitive::getAsString);
168    }
169
170    /**
171     * Returns the Integer value of the selected {@link JsonPrimitive}.
172     *
173     * @param selectors the selectors
174     * @return the as string
175     */
176    public Optional<Integer> getAsInt(Object... selectors) {
177        return get(JsonPrimitive.class, selectors)
178            .map(JsonPrimitive::getAsInt);
179    }
180
181    /**
182     * Returns the Integer value of the selected {@link JsonPrimitive}.
183     *
184     * @param selectors the selectors
185     * @return the as string
186     */
187    public Optional<BigInteger> getAsBigInteger(Object... selectors) {
188        return get(JsonPrimitive.class, selectors)
189            .map(JsonPrimitive::getAsBigInteger);
190    }
191
192    /**
193     * Returns the Long value of the selected {@link JsonPrimitive}.
194     *
195     * @param selectors the selectors
196     * @return the as string
197     */
198    public Optional<Long> getAsLong(Object... selectors) {
199        return get(JsonPrimitive.class, selectors)
200            .map(JsonPrimitive::getAsLong);
201    }
202
203    /**
204     * Returns the boolean value of the selected {@link JsonPrimitive}.
205     *
206     * @param selectors the selectors
207     * @return the boolean
208     */
209    public Optional<Boolean> getAsBoolean(Object... selectors) {
210        return get(JsonPrimitive.class, selectors)
211            .map(JsonPrimitive::getAsBoolean);
212    }
213
214    /**
215     * Returns the elements of the selected {@link JsonArray} as list.
216     *
217     * @param <T> the generic type
218     * @param cls the cls
219     * @param selectors the selectors
220     * @return the list
221     */
222    @SuppressWarnings("unchecked")
223    public <T extends JsonElement> List<T> getAsListOf(Class<T> cls,
224            Object... selectors) {
225        return get(JsonArray.class, selectors).map(a -> (List<T>) a.asList())
226            .orElse(Collections.emptyList());
227    }
228
229    /**
230     * Sets the selected value. This pointer must point to a
231     * {@link JsonObject} or {@link JsonArray}. The selector must
232     * be a {@link String} or an integer respectively.
233     *
234     * @param selector the selector
235     * @param value the value
236     * @return the Gson pointer
237     */
238    public GsonPtr set(Object selector, JsonElement value) {
239        if (position instanceof JsonObject obj
240            && selector instanceof String member) {
241            obj.add(member, value);
242            return this;
243        }
244        if (position instanceof JsonArray arr
245            && selector instanceof Integer index) {
246            if (index >= arr.size()) {
247                arr.add(value);
248            } else {
249                arr.set(index, value);
250            }
251            return this;
252        }
253        throw new IllegalStateException("Invalid selection");
254    }
255
256    /**
257     * Short for `set(selector, new JsonPrimitive(value))`.
258     *
259     * @param selector the selector
260     * @param value the value
261     * @return the gson ptr
262     * @see #set(Object, JsonElement)
263     */
264    public GsonPtr set(Object selector, String value) {
265        return set(selector, new JsonPrimitive(value));
266    }
267
268    /**
269     * Short for `set(selector, new JsonPrimitive(value))`.
270     *
271     * @param selector the selector
272     * @param value the value
273     * @return the gson ptr
274     * @see #set(Object, JsonElement)
275     */
276    public GsonPtr set(Object selector, Long value) {
277        return set(selector, new JsonPrimitive(value));
278    }
279
280    /**
281     * Short for `set(selector, new JsonPrimitive(value))`.
282     *
283     * @param selector the selector
284     * @param value the value
285     * @return the gson ptr
286     * @see #set(Object, JsonElement)
287     */
288    public GsonPtr set(Object selector, BigInteger value) {
289        return set(selector, new JsonPrimitive(value));
290    }
291
292    /**
293     * Same as {@link #set(Object, JsonElement)}, but sets the value
294     * only if it doesn't exist yet, else returns the existing value.
295     * If this pointer points to a {@link JsonArray} and the selector
296     * if larger than or equal to the size of the array, the supplied
297     * value will be appended.
298     *
299     * @param <T> the generic type
300     * @param selector the selector
301     * @param supplier the supplier of the missing value
302     * @return the existing or supplied value
303     */
304    @SuppressWarnings("unchecked")
305    public <T extends JsonElement> T
306            computeIfAbsent(Object selector, Supplier<T> supplier) {
307        if (position instanceof JsonObject obj
308            && selector instanceof String member) {
309            return Optional.ofNullable((T) obj.get(member)).orElseGet(() -> {
310                var res = supplier.get();
311                obj.add(member, res);
312                return res;
313            });
314        }
315        if (position instanceof JsonArray arr
316            && selector instanceof Integer index) {
317            if (index >= arr.size()) {
318                var res = supplier.get();
319                arr.add(res);
320                return res;
321            }
322            return (T) arr.get(index);
323        }
324        throw new IllegalStateException("Invalid selection");
325    }
326
327    /**
328     * Short for `computeIfAbsent(selector, () -> new JsonPrimitive(value))`.
329     *
330     * @param selector the selector
331     * @param value the value
332     * @return the Gson pointer
333     */
334    public GsonPtr getOrSet(Object selector, String value) {
335        computeIfAbsent(selector, () -> new JsonPrimitive(value));
336        return this;
337    }
338
339}