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}