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.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.util.*; 023 024import org.apache.juneau.commons.svl.*; 025 026/** 027 * String-manipulation functions for the {@code #{...}} script catalog. 028 * 029 * <p> 030 * Each public static nested class is a {@link TypedFunction} registered under the function name 031 * shown in its {@link VarFunction#name()} method. The {@code Replaces} annotation in each 032 * nested-class Javadoc names the legacy {@code Var} class that was retired in 10.0.0 in favor of 033 * the function form. 034 */ 035public final class StringFunctions { 036 037 private StringFunctions() {} 038 039 /** All function classes in this category, in registration order. */ 040 @SuppressWarnings({ 041 "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. 042 "unchecked" // Array initializer with parameterized element type; class literals are always erased at runtime. 043 }) 044 public static final Class<? extends VarFunction>[] ALL = new Class[] { 045 Substring.class, Upper.class, Lower.class, Trim.class, StripLeading.class, 046 StripTrailing.class, StripSlashes.class, PathToken.class, Len.class, Replace.class, 047 Contains.class, StartsWith.class, EndsWith.class, Concat.class, Repeat.class, 048 Reverse.class, Format.class, Split.class, Join.class 049 }; 050 051 /** 052 * {@code #{substring(s, start)}} / {@code #{substring(s, start, end)}} — replacement for the 053 * legacy {@code $ST{...}} {@code SubstringVar}. 054 * 055 * <p> 056 * Both endpoints are clamped to {@code [0, len(s)]} so out-of-range arguments don't throw — 057 * matches the legacy {@code SubstringVar} behavior. 058 */ 059 public static class Substring extends TypedFunction { 060 @Override public String name() { return "substring"; } 061 public String invoke(String s, int start) { 062 if (s == null) return ""; 063 var len = s.length(); 064 var a = Math.max(0, Math.min(start, len)); 065 return s.substring(a); 066 } 067 public String invoke(String s, int start, int end) { 068 if (s == null) return ""; 069 var len = s.length(); 070 var a = Math.max(0, Math.min(start, len)); 071 var b = Math.max(a, Math.min(end, len)); 072 return s.substring(a, b); 073 } 074 } 075 076 /** {@code #{upper(s)}} — replacement for the legacy {@code $UC{...}}. */ 077 public static class Upper extends TypedFunction { 078 @Override public String name() { return "upper"; } 079 public String invoke(String s) { return s == null ? "" : s.toUpperCase(); } 080 } 081 082 /** {@code #{lower(s)}} — replacement for the legacy {@code $LC{...}}. */ 083 public static class Lower extends TypedFunction { 084 @Override public String name() { return "lower"; } 085 public String invoke(String s) { return s == null ? "" : s.toLowerCase(); } 086 } 087 088 /** {@code #{trim(s)}} — strips leading and trailing whitespace. */ 089 public static class Trim extends TypedFunction { 090 @Override public String name() { return "trim"; } 091 public String invoke(String s) { return s == null ? "" : s.trim(); } 092 } 093 094 /** {@code #{stripLeading(s)}} — strips leading whitespace. */ 095 public static class StripLeading extends TypedFunction { 096 @Override public String name() { return "stripLeading"; } 097 public String invoke(String s) { return s == null ? "" : s.stripLeading(); } 098 } 099 100 /** {@code #{stripTrailing(s)}} — strips trailing whitespace. */ 101 public static class StripTrailing extends TypedFunction { 102 @Override public String name() { return "stripTrailing"; } 103 public String invoke(String s) { return s == null ? "" : s.stripTrailing(); } 104 } 105 106 /** {@code #{stripSlashes(s)}} — strips leading and trailing {@code /} characters. */ 107 public static class StripSlashes extends TypedFunction { 108 @Override public String name() { return "stripSlashes"; } 109 public String invoke(String s) { 110 if (s == null) return ""; 111 var start = 0; 112 var end = s.length(); 113 while (start < end && s.charAt(start) == '/') start++; 114 while (end > start && s.charAt(end - 1) == '/') end--; 115 return s.substring(start, end); 116 } 117 } 118 119 /** 120 * {@code #{pathToken(s)}} — normalize a user-supplied path token for safe interpolation 121 * into a {@code path=} template. 122 * 123 * <p> 124 * Strips leading and trailing {@code /}, plus a trailing {@code /*} (or {@code *} that 125 * sits immediately after a {@code /}). Multi-segment paths are preserved. 126 * 127 * <h5 class='section'>Edge cases:</h5> 128 * <ul> 129 * <li>{@code ""} → {@code ""} 130 * <li>{@code "/"} → {@code ""} 131 * <li>{@code "/*"} → {@code ""} 132 * <li>{@code "jsp"} → {@code "jsp"} 133 * <li>{@code "/jsp"} → {@code "jsp"} 134 * <li>{@code "/jsp/"} → {@code "jsp"} 135 * <li>{@code "/jsp/*"} → {@code "jsp"} 136 * <li>{@code "jsp/*"} → {@code "jsp"} 137 * <li>{@code "/api/v1/*"} → {@code "api/v1"} 138 * </ul> 139 * 140 * <p> 141 * Used in SVL-configurable mixin paths so users can supply 142 * {@code "jsp"}, {@code "/jsp"}, {@code "/jsp/"}, {@code "/jsp/*"}, or {@code "jsp/*"} and 143 * the template {@code /#{pathToken(${juneau.jsp.path:jsp})}/*} always resolves to a clean 144 * {@code /jsp/*}. 145 */ 146 public static class PathToken extends TypedFunction { 147 @Override public String name() { return "pathToken"; } 148 public String invoke(String s) { 149 if (isEmpty(s)) return ""; 150 var t = s; 151 // Strip a trailing /* or trailing * (when sitting right after a /). 152 if (t.endsWith("/*")) 153 t = t.substring(0, t.length() - 2); 154 else if (t.endsWith("*") && t.length() >= 2 && t.charAt(t.length() - 2) == '/') 155 t = t.substring(0, t.length() - 1); 156 // Strip leading and trailing slashes. 157 var start = 0; 158 var end = t.length(); 159 while (start < end && t.charAt(start) == '/') start++; 160 while (end > start && t.charAt(end - 1) == '/') end--; 161 return t.substring(start, end); 162 } 163 } 164 165 /** 166 * {@code #{len(s)}} / {@code #{len(s, delimiter)}} — replacement for the legacy 167 * {@code $LN{...}}. 168 * 169 * <p> 170 * Single-arg form returns the character count. Two-arg form splits on the literal 171 * {@code delimiter} (after trimming leading/trailing whitespace, matching the legacy 172 * {@code LenVar} behavior) and returns the part count. 173 */ 174 public static class Len extends TypedFunction { 175 @Override public String name() { return "len"; } 176 public String invoke(String s) { return s == null ? "0" : String.valueOf(s.length()); } 177 public String invoke(String s, String delimiter) { 178 if (s == null) return "0"; 179 if (ie(delimiter)) return String.valueOf(s.length()); 180 return String.valueOf(s.trim().split(java.util.regex.Pattern.quote(delimiter)).length); 181 } 182 } 183 184 /** 185 * {@code #{replace(s, target, replacement)}} — literal (non-regex) string replacement. 186 * 187 * <p> 188 * For regex replacement use {@code #{replaceRegex(s, regex, replacement)}}. 189 */ 190 public static class Replace extends TypedFunction { 191 @Override public String name() { return "replace"; } 192 public String invoke(String s, String target, String replacement) { 193 if (s == null) return ""; 194 return s.replace(target, replacement == null ? "" : replacement); 195 } 196 } 197 198 /** {@code #{contains(s, substr)}} — returns {@code "true"} or {@code "false"}. */ 199 public static class Contains extends TypedFunction { 200 @Override public String name() { return "contains"; } 201 public String invoke(String s, String substr) { 202 return String.valueOf(s != null && substr != null && s.contains(substr)); 203 } 204 } 205 206 /** {@code #{startsWith(s, prefix)}} — returns {@code "true"} or {@code "false"}. */ 207 public static class StartsWith extends TypedFunction { 208 @Override public String name() { return "startsWith"; } 209 public String invoke(String s, String prefix) { 210 return String.valueOf(s != null && prefix != null && s.startsWith(prefix)); 211 } 212 } 213 214 /** {@code #{endsWith(s, suffix)}} — returns {@code "true"} or {@code "false"}. */ 215 public static class EndsWith extends TypedFunction { 216 @Override public String name() { return "endsWith"; } 217 public String invoke(String s, String suffix) { 218 return String.valueOf(s != null && suffix != null && s.endsWith(suffix)); 219 } 220 } 221 222 /** {@code #{concat(s1, s2, ...)}} — variadic string concatenation. */ 223 public static class Concat extends TypedFunction { 224 @Override public String name() { return "concat"; } 225 public String invoke(String[] parts) { 226 var sb = new StringBuilder(); 227 for (var p : parts) 228 if (p != null) sb.append(p); 229 return sb.toString(); 230 } 231 } 232 233 /** {@code #{repeat(s, n)}} — repeats {@code s} {@code n} times. */ 234 public static class Repeat extends TypedFunction { 235 @Override public String name() { return "repeat"; } 236 public String invoke(String s, int n) { 237 if (s == null || n <= 0) return ""; 238 return s.repeat(n); 239 } 240 } 241 242 /** {@code #{reverse(s)}} — reverses the characters of {@code s}. */ 243 public static class Reverse extends TypedFunction { 244 @Override public String name() { return "reverse"; } 245 public String invoke(String s) { 246 return s == null ? "" : new StringBuilder(s).reverse().toString(); 247 } 248 } 249 250 /** 251 * {@code #{format(pattern, args...)}} — {@link String#format Java formatter-style 252 * formatting}. Pattern uses Java {@link Formatter} syntax (e.g. {@code %s}, 253 * {@code %d}, {@code %.2f}). Variadic args; coerced per Java {@code Formatter} rules at 254 * invocation. 255 * 256 * <h5 class='section'>Example:</h5> 257 * <p> 258 * {@code #{format("%s-%03d", ${prefix}, ${counter})}} 259 * 260 * <p> 261 * All args are passed as raw {@link String} values; format specifiers like {@code %d} will 262 * convert numeric strings via {@link Integer#parseInt} / {@link Long#parseLong} as needed. 263 * {@code %s} accepts any string verbatim. 264 */ 265 public static class Format extends TypedFunction { 266 @Override public String name() { return "format"; } 267 public String invoke(String pattern, String[] args) { 268 if (pattern == null) return ""; 269 var coerced = new Object[args.length]; 270 for (var i = 0; i < args.length; i++) 271 coerced[i] = coerceArgForFormat(args[i]); 272 return String.format(pattern, coerced); 273 } 274 275 /** 276 * Best-effort numeric coercion for {@code %d}/{@code %f}-style specifiers — strings 277 * that parse as long/double are passed as the typed value, otherwise as the raw 278 * string. This keeps {@code %s} as the safe default while letting numeric specifiers 279 * work without a separate {@code numericFormat(...)} entry point. 280 */ 281 @SuppressWarnings({ 282 "java:S1166" // Exception swallowed — coercion is best-effort. 283 }) 284 private static Object coerceArgForFormat(String s) { 285 if (s == null) return ""; 286 var t = s.trim(); 287 try { return Long.parseLong(t); } catch (@SuppressWarnings("unused") NumberFormatException e) { /* fall through */ } 288 try { return Double.parseDouble(t); } catch (@SuppressWarnings("unused") NumberFormatException e) { /* fall through */ } 289 return s; 290 } 291 } 292 293 /** 294 * {@code #{split(s, separator)}} — splits {@code s} on the literal {@code separator} 295 * substring. Returns a JSON-array-shortcut string (e.g. {@code ["a","b","c"]}) so the 296 * result composes with other functions that accept {@code String[]}. Trailing empty fields 297 * dropped. 298 */ 299 public static class Split extends TypedFunction { 300 @Override public String name() { return "split"; } 301 public String invoke(String s, String separator) { 302 if (isEmpty(s)) return "[]"; 303 var parts = new ArrayList<String>(); 304 if (isEmpty(separator)) { 305 parts.add(s); 306 } else { 307 var i = 0; 308 while (true) { 309 var hit = s.indexOf(separator, i); 310 if (hit < 0) { parts.add(s.substring(i)); break; } 311 parts.add(s.substring(i, hit)); 312 i = hit + separator.length(); 313 } 314 } 315 // Drop trailing empties. 316 while (!parts.isEmpty() && parts.get(parts.size() - 1).isEmpty()) 317 parts.remove(parts.size() - 1); 318 return JsonShortcut.encodeArray(parts); 319 } 320 } 321 322 /** 323 * {@code #{join(separator, parts...)}} — concatenates {@code parts} with {@code separator} 324 * between each pair. 325 * 326 * <h5 class='section'>Example:</h5> 327 * <p> 328 * {@code #{join("/", ${tenant}, ${id})}} → {@code acme/42} 329 */ 330 public static class Join extends TypedFunction { 331 @Override public String name() { return "join"; } 332 public String invoke(String separator, String[] parts) { 333 var sep = separator == null ? "" : separator; 334 var sb = new StringBuilder(); 335 for (var i = 0; i < parts.length; i++) { 336 if (i > 0) sb.append(sep); 337 sb.append(parts[i] == null ? "" : parts[i]); 338 } 339 return sb.toString(); 340 } 341 } 342}