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}