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}