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.StringUtils.*;
020
021import java.util.regex.*;
022
023import org.apache.juneau.commons.collections.*;
024import org.apache.juneau.commons.svl.*;
025
026/**
027 * Conditional and routing functions for the {@code #{...}} script catalog.
028 *
029 * <p>
030 * These four functions replaced the legacy {@code IfVar}, {@code SwitchVar}, {@code CoalesceVar},
031 * and {@code NotEmptyVar} classes when the {@code #{...}} script syntax landed in 10.0.0.
032 */
033public final class ConditionalFunctions {
034
035   private ConditionalFunctions() {}
036
037   /** All function classes in this category. */
038   @SuppressWarnings({
039      "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.
040      "unchecked" // Cast is safe: type verified by caller context.
041   })
042   public static final Class<? extends VarFunction>[] ALL = new Class[] {
043      If.class, Switch.class, Coalesce.class, NotEmpty.class
044   };
045
046   /**
047    * {@code #{if(cond, then, else)}} — replacement for the legacy {@code $IF{...}}.
048    *
049    * <p>
050    * Returns {@code then} if {@code cond} is truthy, {@code else} otherwise. Truthiness is
051    * explicit (no implicit truthiness).
052    */
053   public static class If extends TypedFunction {
054      @Override public String name() { return "if"; }
055      public String invoke(boolean cond, String thenVal, String elseVal) {
056         var then = thenVal == null ? "" : thenVal;
057         var els = elseVal == null ? "" : elseVal;
058         return cond ? then : els;
059      }
060   }
061
062   /**
063    * {@code #{switch(value, case1, val1, case2, val2, ..., default)}} — replacement for the
064    * legacy {@code $SW{...}}.
065    *
066    * <p>
067    * Compares {@code value} against each {@code caseN} <i>glob pattern</i> and returns the
068    * matching {@code valN} for the first match. {@code caseN} patterns may contain {@code *}
069    * (any sequence of characters) and {@code ?} (any single character) wildcards — same
070    * semantics as the legacy {@code SwitchVar}. Plain literals (no wildcards) match by
071    * full-string equality.
072    *
073    * <p>
074    * The last unmatched arg is the default; if the arg count is even (no trailing default),
075    * an empty string is returned for unmatched values.
076    *
077    * <h5 class='section'>Examples:</h5>
078    * <ul>
079    *    <li>{@code #{switch(${os}, *win*, Windows, *nix*, Linux, *)}} — glob-routes by substring.
080    *    <li>{@code #{switch(${env}, prod, A, dev, B, default)}} — exact equality, three branches.
081    * </ul>
082    */
083   public static class Switch extends TypedFunction {
084
085      // Compiled glob-derived regexes are cached and reused.  Bounded (entire cache cleared past maxSize) to guard
086      // against unbounded growth from arbitrary user-supplied glob patterns.  Pattern is immutable/thread-safe.
087      private static final Cache<String,Pattern> PATTERN_CACHE = Cache.of(String.class, Pattern.class).maxSize(1000).supplier(Pattern::compile).build();
088
089      @Override public String name() { return "switch"; }
090      public String invoke(String[] args) {
091         if (args.length == 0) return "";
092         var value = args[0] == null ? "" : args[0];
093         var i = 1;
094         while (i + 1 < args.length) {
095            if (globMatches(value, args[i])) return args[i + 1] == null ? "" : args[i + 1];
096            i += 2;
097         }
098         // Trailing default if arity is even (value + N pairs + default).
099         if (i < args.length) return args[i] == null ? "" : args[i];
100         return "";
101      }
102
103      /** Full-string glob match supporting {@code *} and {@code ?} wildcards. */
104      private static boolean globMatches(String value, String pattern) {
105         if (pattern == null) return value == null;
106         // Compile glob to regex once per call. Translation: '*' -> '.*', '?' -> '.',
107         // every other regex metacharacter is quoted.
108         var sb = new StringBuilder(pattern.length() + 4);
109         for (var i = 0; i < pattern.length(); i++) {
110            var c = pattern.charAt(i);
111            switch (c) {
112               case '*' -> sb.append(".*");
113               case '?' -> sb.append('.');
114               case '\\', '.', '[', ']', '(', ')', '{', '}', '|', '+', '^', '$' -> sb.append('\\').append(c);
115               default -> sb.append(c);
116            }
117         }
118         return PATTERN_CACHE.get(sb.toString()).matcher(value).matches();
119      }
120   }
121
122   /**
123    * {@code #{coalesce(a, b, ...)}} — replacement for the legacy {@code $CO{...}}.
124    *
125    * <p>
126    * Returns the first non-empty arg, or {@code ""} if all args are empty.
127    */
128   public static class Coalesce extends TypedFunction {
129      @Override public String name() { return "coalesce"; }
130      public String invoke(String[] args) {
131         for (var a : args)
132            if (!isEmpty(a)) return a;
133         return "";
134      }
135   }
136
137   /**
138    * {@code #{notEmpty(s)}} — replacement for the legacy {@code $NE{...}}.
139    *
140    * <p>
141    * Returns {@code "true"} if {@code s} is non-{@code null} and non-empty, {@code "false"}
142    * otherwise. Whitespace-only strings count as non-empty (matches {@code String.isEmpty()}).
143    */
144   public static class NotEmpty extends TypedFunction {
145      @Override public String name() { return "notEmpty"; }
146      public String invoke(String s) { return String.valueOf(!isEmpty(s)); }
147   }
148}