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 java.util.regex.*;
020
021import org.apache.juneau.commons.collections.*;
022import org.apache.juneau.commons.svl.*;
023
024/**
025 * Regex functions for the {@code #{...}} script catalog.
026 *
027 * <p>
028 * Replacements for {@code PatternMatchVar} ({@code $PM}), {@code PatternExtractVar}
029 * ({@code $PE}), and {@code PatternReplaceVar} ({@code $PR}) per the migration inventory.
030 *
031 * <p>
032 * Note that {@link StringFunctions.Replace} is the literal-string replace; {@link ReplaceRegex}
033 * is the regex form.
034 */
035public final class RegexFunctions {
036
037   private RegexFunctions() {}
038
039   // User-supplied regexes are compiled once and reused.  Bounded (entire cache cleared past maxSize) to guard
040   // against unbounded growth from arbitrary user input.  Pattern is immutable/thread-safe; a fresh Matcher is
041   // created per invocation, so behavior is identical to compiling on every call.
042   private static final Cache<String,Pattern> PATTERN_CACHE = Cache.of(String.class, Pattern.class).maxSize(1000).supplier(Pattern::compile).build();
043
044   /** All function classes in this category. */
045   @SuppressWarnings({
046      "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.
047      "unchecked" // Cast is safe: type verified by caller context.
048   })
049   public static final Class<? extends VarFunction>[] ALL = new Class[] {
050      Match.class, Extract.class, ReplaceRegex.class
051   };
052
053   /** {@code #{match(s, regex)}} — replacement for the legacy {@code $PM{...}}. Returns {@code "true"} or {@code "false"}. */
054   public static class Match extends TypedFunction {
055      @Override public String name() { return "match"; }
056      public String invoke(String s, String regex) {
057         if (s == null || regex == null) return "false";
058         return String.valueOf(PATTERN_CACHE.get(regex).matcher(s).find());
059      }
060   }
061
062   /**
063    * {@code #{extract(s, regex)}} / {@code #{extract(s, regex, group)}} — replacement for the
064    * legacy {@code $PE{...}}. Returns the first match; if {@code group} is supplied, returns
065    * that capture group from the first match.
066    */
067   public static class Extract extends TypedFunction {
068      @Override public String name() { return "extract"; }
069      public String invoke(String s, String regex) { return invoke(s, regex, 0); }
070      public String invoke(String s, String regex, int group) {
071         if (s == null || regex == null) return "";
072         var m = PATTERN_CACHE.get(regex).matcher(s);
073         if (!m.find()) return "";
074         if (group < 0 || group > m.groupCount()) return "";
075         var v = m.group(group);
076         return v == null ? "" : v;
077      }
078   }
079
080   /** {@code #{replaceRegex(s, regex, replacement)}} — replacement for the legacy {@code $PR{...}}. */
081   public static class ReplaceRegex extends TypedFunction {
082      @Override public String name() { return "replaceRegex"; }
083      public String invoke(String s, String regex, String replacement) {
084         if (s == null) return "";
085         if (regex == null) return s;
086         return PATTERN_CACHE.get(regex).matcher(s).replaceAll(replacement == null ? "" : replacement);
087      }
088   }
089}