1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function.library;
7   
8   import java.util.List;
9   import java.util.regex.Pattern;
10  import java.util.regex.PatternSyntaxException;
11  
12  import dev.metaschema.core.metapath.DynamicContext;
13  import dev.metaschema.core.metapath.MetapathConstants;
14  import dev.metaschema.core.metapath.function.FunctionUtils;
15  import dev.metaschema.core.metapath.function.IArgument;
16  import dev.metaschema.core.metapath.function.IFunction;
17  import dev.metaschema.core.metapath.function.regex.RegexUtil;
18  import dev.metaschema.core.metapath.function.regex.RegularExpressionMetapathException;
19  import dev.metaschema.core.metapath.item.IItem;
20  import dev.metaschema.core.metapath.item.ISequence;
21  import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
22  import dev.metaschema.core.metapath.item.atomic.IStringItem;
23  import dev.metaschema.core.util.ObjectUtils;
24  import edu.umd.cs.findbugs.annotations.NonNull;
25  import edu.umd.cs.findbugs.annotations.Nullable;
26  
27  /**
28   * Implements the XPath 3.1 <a href=
29   * "https://www.w3.org/TR/xpath-functions-31/#func-matches">fn:matches</a>
30   * functions.
31   */
32  public final class FnMatches {
33    @NonNull
34    private static final String NAME = "matches";
35    // CPD-OFF
36    @NonNull
37    static final IFunction SIGNATURE_TWO_ARG = IFunction.builder()
38        .name(NAME)
39        .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
40        .deterministic()
41        .contextIndependent()
42        .focusIndependent()
43        .argument(IArgument.builder()
44            .name("input")
45            .type(IStringItem.type())
46            .zeroOrOne()
47            .build())
48        .argument(IArgument.builder()
49            .name("pattern")
50            .type(IStringItem.type())
51            .one()
52            .build())
53        .returnType(IBooleanItem.type())
54        .returnOne()
55        .functionHandler(FnMatches::executeTwoArg)
56        .build();
57  
58    @NonNull
59    static final IFunction SIGNATURE_THREE_ARG = IFunction.builder()
60        .name(NAME)
61        .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
62        .deterministic()
63        .contextIndependent()
64        .focusIndependent()
65        .argument(IArgument.builder()
66            .name("input")
67            .type(IStringItem.type())
68            .zeroOrOne()
69            .build())
70        .argument(IArgument.builder()
71            .name("pattern")
72            .type(IStringItem.type())
73            .one()
74            .build())
75        .argument(IArgument.builder()
76            .name("flags")
77            .type(IStringItem.type())
78            .one()
79            .build())
80        .returnType(IBooleanItem.type())
81        .returnOne()
82        .functionHandler(FnMatches::executeThreeArg)
83        .build();
84    // CPD-ON
85  
86    @SuppressWarnings("unused")
87    @NonNull
88    private static ISequence<IBooleanItem> executeTwoArg(
89        @NonNull IFunction function,
90        @NonNull List<ISequence<?>> arguments,
91        @NonNull DynamicContext dynamicContext,
92        IItem focus) {
93      IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true));
94      IStringItem pattern = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true)));
95  
96      return execute(input, pattern, IStringItem.valueOf(""));
97    }
98  
99    @SuppressWarnings("unused")
100   @NonNull
101   private static ISequence<IBooleanItem> executeThreeArg(
102       @NonNull IFunction function,
103       @NonNull List<ISequence<?>> arguments,
104       @NonNull DynamicContext dynamicContext,
105       IItem focus) {
106     IStringItem input = FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true));
107     IStringItem pattern = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true)));
108     IStringItem flags = ObjectUtils.requireNonNull(FunctionUtils.asTypeOrNull(arguments.get(2).getFirstItem(true)));
109 
110     return execute(input, pattern, flags);
111   }
112 
113   @NonNull
114   private static ISequence<IBooleanItem> execute(
115       @Nullable IStringItem input,
116       @NonNull IStringItem pattern,
117       @NonNull IStringItem flags) {
118     return input == null
119         ? ISequence.empty()
120         : ISequence.of(
121             IBooleanItem.valueOf(
122                 fnMatches(input.asString(), pattern.asString(), flags.asString())));
123   }
124 
125   /**
126    * Implements <a href=
127    * "https://www.w3.org/TR/xpath-functions-31/#func-matches">fn:matches</a>.
128    *
129    * @param input
130    *          the string to match against
131    * @param pattern
132    *          the regular expression to use for matching
133    * @param flags
134    *          matching options
135    * @return {@code true} if the pattern matches or {@code false} otherwise
136    */
137   public static boolean fnMatches(@NonNull String input, @NonNull String pattern, @NonNull String flags) {
138     try {
139       return Pattern.compile(pattern, RegexUtil.parseFlags(flags))
140           .matcher(input).find();
141     } catch (PatternSyntaxException ex) {
142       throw new RegularExpressionMetapathException(
143           RegularExpressionMetapathException.INVALID_EXPRESSION,
144           "Invalid regular expression pattern: '" + pattern + "'",
145           ex);
146     } catch (IllegalArgumentException ex) {
147       throw new RegularExpressionMetapathException(
148           RegularExpressionMetapathException.INVALID_FLAG,
149           "Invalid regular expression flags: '" + flags + "'",
150           ex);
151     }
152   }
153 
154   private FnMatches() {
155     // disable construction
156   }
157 }