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}