001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.svl.functions;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.svl.*;
024
025/**
026 * JSON-navigation functions for the {@code #{...}} script catalog.
027 *
028 * <p>
029 * All functions accept JSON strings as input and parse on each call via {@link MiniJson}. This
030 * is intentionally a per-resolve cost — for repeated lookups against the same JSON document the
031 * caller can pre-compile the surrounding template ({@link VarResolver#compile(String)}) so the
032 * tokenization is amortized; the JSON parse itself is not yet cached and is a known follow-on
033 * optimization opportunity.
034 */
035@SuppressWarnings({
036   "unchecked" // Cast is safe: type verified by caller context.
037})
038public final class JsonFunctions {
039
040   private JsonFunctions() {}
041
042   /** All function classes in this category. */
043   @SuppressWarnings({
044      "java:S2386" // ALL is an immutable compile-time registry; exposed as an array for the cross-package/varargs functions(...) API, so visibility cannot be reduced.
045   })
046   public static final Class<? extends VarFunction>[] ALL = new Class[] {
047      JsonPath.class, Get.class, Keys.class, Values.class, Size.class
048   };
049
050   /**
051    * {@code #{jsonPath(json, path)}} / {@code #{jsonPath(json, path, default)}} — extracts a
052    * value via a slash-rooted path (e.g. {@code "/data/0/name"}).
053    *
054    * <p>
055    * Returns the matched value as a String (objects/arrays serialize back to JSON). Missing
056    * path returns {@code ""} for the two-arg form, or {@code default} for the three-arg form.
057    */
058   public static class JsonPath extends TypedFunction {
059      @Override public String name() { return "jsonPath"; }
060      public String invoke(String json, String path) { return invoke(json, path, ""); }
061      public String invoke(String json, String path, String defaultValue) {
062         var root = MiniJson.parse(json);
063         var v = navigate(root, path);
064         if (v == null) return or(defaultValue, "");
065         return MiniJson.render(v);
066      }
067
068      @SuppressWarnings({
069         "java:S3776" // Cognitive complexity acceptable for JSON path navigation dispatch
070      })
071      private static Object navigate(Object root, String path) {
072         if (root == null || path == null) return null;
073         var p = path.startsWith("/") ? path.substring(1) : path;
074         if (p.isEmpty()) return root;
075         var current = root;
076         for (var seg : p.split("/")) {
077            if (current == null) return null;
078            if (current instanceof Map<?,?> current2) {
079               current = ((Map<String,Object>) current2).get(seg);
080            } else if (current instanceof List<?> current3) {
081               int idx;
082               try { idx = Integer.parseInt(seg); } catch (@SuppressWarnings("unused") NumberFormatException e) { return null; }
083               if (idx < 0 || idx >= current3.size()) return null;
084               current = current3.get(idx);
085            } else {
086               return null;
087            }
088         }
089         return current;
090      }
091   }
092
093   /**
094    * {@code #{get(json, key)}} — extracts {@code json[key]} (object) or {@code json[index]}
095    * (array, when {@code key} parses as int).
096    */
097   public static class Get extends TypedFunction {
098      @Override public String name() { return "get"; }
099      public String invoke(String json, String key) {
100         var v = MiniJson.parse(json);
101         if (v == null || key == null) return "";
102         if (v instanceof Map<?,?> v2) {
103            var x = ((Map<String,Object>) v2).get(key);
104            return x == null ? "" : MiniJson.render(x);
105         }
106         if (v instanceof List<?> v3) {
107            int idx;
108            try { idx = Integer.parseInt(key); } catch (@SuppressWarnings("unused") NumberFormatException e) { return ""; }
109            if (idx < 0 || idx >= v3.size()) return "";
110            var x = v3.get(idx);
111            return x == null ? "" : MiniJson.render(x);
112         }
113         return "";
114      }
115   }
116
117   /** {@code #{keys(json)}} — returns a JSON-array-shortcut string of an object's keys. */
118   public static class Keys extends TypedFunction {
119      @Override public String name() { return "keys"; }
120      public String invoke(String json) {
121         var v = MiniJson.parse(json);
122         if (!(v instanceof Map<?,?> v2)) return "[]";
123         return JsonShortcut.encodeArray(new ArrayList<>(((Map<String,Object>) v2).keySet()));
124      }
125   }
126
127   /** {@code #{values(json)}} — returns a JSON-array-shortcut string of an object's values (or array elements). */
128   public static class Values extends TypedFunction {
129      @Override public String name() { return "values"; }
130      public String invoke(String json) {
131         var v = MiniJson.parse(json);
132         if (v instanceof Map<?,?> v2) {
133            return JsonShortcut.encodeArray(((Map<String,Object>) v2).values().stream().map(MiniJson::render).toList());
134         }
135         if (v instanceof List<?> v3) {
136            return JsonShortcut.encodeArray(v3.stream().map(MiniJson::render).toList());
137         }
138         return "[]";
139      }
140   }
141
142   /**
143    * {@code #{size(json)}} — returns the count: object key count, array length, string
144    * length, or {@code 0} for null/primitives.
145    */
146   public static class Size extends TypedFunction {
147      @Override public String name() { return "size"; }
148      public String invoke(String json) {
149         var v = MiniJson.parse(json);
150         if (v == null) return "0";
151         if (v instanceof Map<?,?> v2) return String.valueOf(v2.size());
152         if (v instanceof List<?> v3) return String.valueOf(v3.size());
153         if (v instanceof String v4) return String.valueOf(v4.length());
154         return "0";
155      }
156   }
157}