1 /*
2 * SPDX-FileCopyrightText: none
3 * SPDX-License-Identifier: CC0-1.0
4 */
5
6 package dev.metaschema.core.metapath.function;
7
8 import java.util.ArrayList;
9 import java.util.EnumSet;
10 import java.util.LinkedList;
11 import java.util.List;
12 import java.util.Objects;
13 import java.util.Set;
14 import java.util.stream.Collectors;
15
16 import dev.metaschema.core.metapath.DynamicContext;
17 import dev.metaschema.core.metapath.MetapathException;
18 import dev.metaschema.core.metapath.StaticContext;
19 import dev.metaschema.core.metapath.StaticMetapathException;
20 import dev.metaschema.core.metapath.item.ICollectionValue;
21 import dev.metaschema.core.metapath.item.IItem;
22 import dev.metaschema.core.metapath.item.ISequence;
23 import dev.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
24 import dev.metaschema.core.metapath.type.IItemType;
25 import dev.metaschema.core.metapath.type.ISequenceType;
26 import dev.metaschema.core.metapath.type.Occurrence;
27 import dev.metaschema.core.qname.IEnhancedQName;
28 import dev.metaschema.core.util.ObjectUtils;
29 import edu.umd.cs.findbugs.annotations.NonNull;
30
31 /**
32 * A common interface for all Metapath functions.
33 */
34 public interface IFunction extends IItem {
35 /**
36 * Details specific characteristics of a function.
37 */
38 enum FunctionProperty {
39 /**
40 * Indicates that the function will produce identical results for the same
41 * arguments (see XPath 3.1 <a href=
42 * "https://www.w3.org/TR/xpath-functions-31/#dt-deterministic">deterministic</a>).
43 * If not assigned to a function definition, a function call with the same
44 * arguments is not guaranteed to produce the same results in the same order for
45 * subsequent calls within the same execution context.
46 */
47 DETERMINISTIC,
48 /**
49 * Indicates that the result of the function depends on property values within
50 * the static or dynamic context and the provided arguments (see XPath 3.1
51 * <a href=
52 * "https://www.w3.org/TR/xpath-functions-31/#dt-context-dependent">context-dependent</a>).
53 * If not assigned to a function definition, a call will not be affected by the
54 * property values within the static or dynamic context and will not have any
55 * arguments.
56 */
57 CONTEXT_DEPENDENT,
58 /**
59 * Indicates that the result of the function depends on the current focus (see
60 * XPath 3.1 <a href=
61 * "https://www.w3.org/TR/xpath-functions-31/#dt-focus-independent">focus-dependent</a>).
62 * If not assigned to a function definition, a call will not be affected by the
63 * current focus.
64 */
65 FOCUS_DEPENDENT,
66 /**
67 * The function allows the last argument to be repeated any number of times.
68 */
69 UNBOUNDED_ARITY;
70 }
71
72 /**
73 * Get the type information for this item.
74 *
75 * @return the type information
76 */
77 @NonNull
78 static IItemType type() {
79 return IItemType.function();
80 }
81
82 @Override
83 default IItemType getType() {
84 // TODO: implement this based on the signature
85 return IItemType.function();
86 }
87
88 /**
89 * Retrieve the name of the function.
90 *
91 * @return the function's name
92 */
93 @NonNull
94 default String getName() {
95 return ObjectUtils.notNull(getQName().getLocalName());
96 }
97
98 /**
99 * Retrieve the namespace qualified name of the function.
100 *
101 * @return the namespace qualified name
102 */
103 @NonNull
104 IEnhancedQName getQName();
105
106 /**
107 * Retrieve the set of assigned function properties.
108 *
109 * @return the set of properties or an empty set
110 */
111 @NonNull
112 Set<FunctionProperty> getProperties();
113
114 /**
115 * Retrieve the list of function arguments.
116 *
117 * @return the function arguments or an empty list if there are none
118 */
119 @NonNull
120 List<IArgument> getArguments();
121
122 /**
123 * Determine the number of arguments the function has.
124 *
125 * @return the number of function arguments
126 */
127 int arity();
128
129 /**
130 * Determines if the result of the function call will produce identical results
131 * when provided the same implicit or explicit arguments.
132 *
133 * @return {@code true} if function is deterministic or {@code false} otherwise
134 * @see FunctionProperty#DETERMINISTIC
135 */
136 default boolean isDeterministic() {
137 return getProperties().contains(FunctionProperty.DETERMINISTIC);
138 }
139
140 /**
141 * Determines if the result of the function call depends on property values
142 * within the static or dynamic context and the provided arguments.
143 *
144 * @return {@code true} if function is context dependent or {@code false}
145 * otherwise
146 * @see FunctionProperty#CONTEXT_DEPENDENT
147 */
148 default boolean isContextDepenent() {
149 return getProperties().contains(FunctionProperty.CONTEXT_DEPENDENT);
150 }
151
152 /**
153 * Determines if the result of the function call depends on the current focus.
154 *
155 * @return {@code true} if function is focus dependent or {@code false}
156 * otherwise
157 * @see FunctionProperty#FOCUS_DEPENDENT
158 */
159 default boolean isFocusDependent() {
160 return getProperties().contains(FunctionProperty.FOCUS_DEPENDENT);
161 }
162
163 /**
164 * Determines if the final argument can be repeated.
165 *
166 * @return {@code true} if the final argument can be repeated or {@code false}
167 * otherwise
168 * @see FunctionProperty#UNBOUNDED_ARITY
169 */
170 default boolean isArityUnbounded() {
171 return getProperties().contains(FunctionProperty.UNBOUNDED_ARITY);
172 }
173
174 /**
175 * Retrieve the function result sequence type.
176 *
177 * @return the function result sequence type
178 */
179 @NonNull
180 ISequenceType getResult();
181
182 /**
183 * Determine if the function is a named function.
184 *
185 * @return {@code true} if the function is named or {@code false} if the
186 * function is anonymous
187 */
188 boolean isNamedFunction();
189
190 // /**
191 // * Determines by static analysis if the function supports the expression
192 // arguments provided.
193 // *
194 // * @param arguments
195 // * the expression arguments to evaluate
196 // * @return {@code true} if the arguments are supported or {@code false}
197 // otherwise
198 // */
199 // boolean isSupported(List<IExpression<?>> arguments);
200
201 @Override
202 default boolean deepEquals(ICollectionValue other, DynamicContext dynamicContext) {
203 // this is always the expected result
204 return false;
205 }
206
207 /**
208 * Execute the function with the provided {@code arguments}, using the provided
209 * {@code DynamicContext} and {@code focus}.
210 *
211 * @param arguments
212 * the function arguments or an empty list if there are no arguments
213 * @param dynamicContext
214 * the dynamic evaluation context
215 * @param focus
216 * the current focus or an empty sequence if there is no focus
217 * @return the function result
218 * @throws MetapathException
219 * if an error occurred while executing the function
220 */
221 @NonNull
222 ISequence<?> execute(
223 @NonNull List<? extends ISequence<?>> arguments,
224 @NonNull DynamicContext dynamicContext,
225 @NonNull ISequence<?> focus);
226
227 @Override
228 default IAnyAtomicItem toAtomicItem() {
229 throw new InvalidTypeFunctionException(InvalidTypeFunctionException.DATA_ITEM_IS_FUNCTION, this);
230 }
231
232 /**
233 * Get the signature of the function as a string.
234 *
235 * @return the signature
236 */
237 @Override
238 @NonNull
239 default String toSignature() {
240 return ObjectUtils.notNull(String.format("%s(%s) as %s",
241 getQName(),
242 getArguments().isEmpty() ? ""
243 : getArguments().stream().map(IArgument::toSignature).collect(Collectors.joining(","))
244 + (isArityUnbounded() ? ", ..." : ""),
245 getResult().toSignature()));
246 }
247
248 /**
249 * Construct a new function signature builder.
250 *
251 * @return the new builder instance
252 */
253 @NonNull
254 static Builder builder() {
255 return builder(StaticContext.instance());
256 }
257
258 /**
259 * Construct a new function signature builder.
260 *
261 * @param staticContext
262 * the static context used to lookup data types and function
263 * implementations
264 *
265 * @return the new builder instance
266 */
267 @NonNull
268 static Builder builder(@NonNull StaticContext staticContext) {
269 return new Builder(staticContext);
270 }
271
272 /**
273 * Used to create a function's signature using a builder pattern.
274 */
275 @SuppressWarnings("PMD.LooseCoupling")
276 final class Builder {
277 @NonNull
278 private final StaticContext staticContext;
279 private String name;
280 private String namespace;
281 @SuppressWarnings("null")
282 @NonNull
283 private final EnumSet<FunctionProperty> properties = EnumSet.noneOf(FunctionProperty.class);
284 @NonNull
285 private final List<IArgument> arguments = new LinkedList<>();
286 @NonNull
287 private IItemType returnType = IItem.type();
288 @NonNull
289 private Occurrence returnOccurrence = Occurrence.ONE;
290 private IFunctionExecutor functionHandler;
291
292 private Builder(@NonNull StaticContext staticContext) {
293 this.staticContext = staticContext;
294 }
295
296 private StaticContext getStaticContext() {
297 return staticContext;
298 }
299
300 /**
301 * Define the name of the function.
302 *
303 * @param name
304 * the function's name
305 * @return this builder
306 */
307 @NonNull
308 public Builder name(@NonNull String name) {
309 Objects.requireNonNull(name, "name");
310 if (name.isBlank()) {
311 throw new IllegalArgumentException("the name must be non-blank");
312 }
313 this.name = name.trim();
314 return this;
315 }
316
317 /**
318 * Define the namespace of the function.
319 *
320 * @param name
321 * the function's namespace URI as a string
322 * @return this builder
323 */
324 @NonNull
325 public Builder namespace(@NonNull String name) {
326 Objects.requireNonNull(name, "name");
327 if (name.isBlank()) {
328 throw new IllegalArgumentException("the name must be non-blank");
329 }
330 this.namespace = name.trim();
331 return this;
332 }
333
334 /**
335 * Mark the function as deterministic.
336 *
337 * @return this builder
338 * @see IFunction.FunctionProperty#DETERMINISTIC
339 */
340 @NonNull
341 public Builder deterministic() {
342 properties.add(FunctionProperty.DETERMINISTIC);
343 return this;
344 }
345
346 /**
347 * Mark the function as non-deterministic.
348 *
349 * @return this builder
350 * @see IFunction.FunctionProperty#DETERMINISTIC
351 */
352 @NonNull
353 public Builder nonDeterministic() {
354 properties.remove(FunctionProperty.DETERMINISTIC);
355 return this;
356 }
357
358 /**
359 * Mark the function as context dependent.
360 *
361 * @return this builder
362 * @see IFunction.FunctionProperty#CONTEXT_DEPENDENT
363 */
364 @NonNull
365 public Builder contextDependent() {
366 properties.add(FunctionProperty.CONTEXT_DEPENDENT);
367 return this;
368 }
369
370 /**
371 * Mark the function as context independent.
372 *
373 * @return this builder
374 * @see IFunction.FunctionProperty#CONTEXT_DEPENDENT
375 */
376 @NonNull
377 public Builder contextIndependent() {
378 properties.remove(FunctionProperty.CONTEXT_DEPENDENT);
379 return this;
380 }
381
382 /**
383 * Mark the function as focus dependent.
384 *
385 * @return this builder
386 * @see IFunction.FunctionProperty#FOCUS_DEPENDENT
387 */
388 @NonNull
389 public Builder focusDependent() {
390 properties.add(FunctionProperty.FOCUS_DEPENDENT);
391 return this;
392 }
393
394 /**
395 * Mark the function as focus independent.
396 *
397 * @return this builder
398 * @see IFunction.FunctionProperty#FOCUS_DEPENDENT
399 */
400 @NonNull
401 public Builder focusIndependent() {
402 properties.remove(FunctionProperty.FOCUS_DEPENDENT);
403 return this;
404 }
405
406 /**
407 * Indicate if the last argument can be repeated.
408 *
409 * @param allow
410 * if {@code true} then the the last argument can be repeated an
411 * unlimited number of times, or {@code false} otherwise
412 * @return this builder
413 */
414 @NonNull
415 public Builder allowUnboundedArity(boolean allow) {
416 if (allow) {
417 properties.add(FunctionProperty.UNBOUNDED_ARITY);
418 } else {
419 properties.remove(FunctionProperty.UNBOUNDED_ARITY);
420 }
421 return this;
422 }
423
424 /**
425 * Define the return sequence Java type of the function.
426 *
427 * @param name
428 * the extended qualified name of the function's return data type
429 * @return this builder
430 */
431 @NonNull
432 public Builder returnType(@NonNull String name) {
433 try {
434 this.returnType = getStaticContext().lookupAtomicType(name);
435 } catch (StaticMetapathException ex) {
436 throw new IllegalArgumentException(
437 String.format("No data type with the name '%s'.", name), ex);
438 }
439 return this;
440 }
441
442 /**
443 * Define the return sequence Java type of the function.
444 *
445 * @param name
446 * the qualified name of the function's return data type
447 * @return this builder
448 */
449 @NonNull
450 public Builder returnType(@NonNull IEnhancedQName name) {
451 try {
452 this.returnType = StaticContext.lookupAtomicType(name);
453 } catch (StaticMetapathException ex) {
454 throw new IllegalArgumentException(
455 String.format("No data type with the name '%s'.", name), ex);
456 }
457 return this;
458 }
459
460 /**
461 * Define the return sequence Java type of the function.
462 *
463 * @param type
464 * the function's return Java type
465 * @return this builder
466 */
467 @NonNull
468 public Builder returnType(@NonNull IItemType type) {
469 this.returnType = type;
470 return this;
471 }
472
473 /**
474 * Indicate the sequence returned will contain zero or one items.
475 *
476 * @return this builder
477 */
478 @NonNull
479 public Builder returnZeroOrOne() {
480 return returnOccurrence(Occurrence.ZERO_OR_ONE);
481 }
482
483 /**
484 * Indicate the sequence returned will contain one item.
485 *
486 * @return this builder
487 */
488 @NonNull
489 public Builder returnOne() {
490 return returnOccurrence(Occurrence.ONE);
491 }
492
493 /**
494 * Indicate the sequence returned will contain zero or more items.
495 *
496 * @return this builder
497 */
498 @NonNull
499 public Builder returnZeroOrMore() {
500 return returnOccurrence(Occurrence.ZERO_OR_MORE);
501 }
502
503 /**
504 * Indicate the sequence returned will contain one or more items.
505 *
506 * @return this builder
507 */
508 @NonNull
509 public Builder returnOneOrMore() {
510 return returnOccurrence(Occurrence.ONE_OR_MORE);
511 }
512
513 @NonNull
514 private Builder returnOccurrence(@NonNull Occurrence occurrence) {
515 Objects.requireNonNull(occurrence, "occurrence");
516 this.returnOccurrence = occurrence;
517 return this;
518 }
519
520 /**
521 * Add an argument based on the provided {@code builder}.
522 *
523 * @param builder
524 * the argument builder
525 * @return this builder
526 */
527 @NonNull
528 public Builder argument(@NonNull IArgument.Builder builder) {
529 return argument(builder.build());
530 }
531
532 /**
533 * Add an argument based on the provided {@code argument} signature.
534 *
535 * @param argument
536 * the argument
537 * @return this builder
538 */
539 @NonNull
540 public Builder argument(@NonNull IArgument argument) {
541 Objects.requireNonNull(argument, "argument");
542 this.arguments.add(argument);
543 return this;
544 }
545
546 /**
547 * Specify the static function to call when executing the function.
548 *
549 * @param handler
550 * a method implementing the {@link IFunctionExecutor} functional
551 * interface
552 * @return this builder
553 */
554 @NonNull
555 public Builder functionHandler(@NonNull IFunctionExecutor handler) {
556 Objects.requireNonNull(handler, "handler");
557 this.functionHandler = handler;
558 return this;
559 }
560
561 /**
562 * Builds the function's signature.
563 *
564 * @return the function's signature
565 */
566 @NonNull
567 public IFunction build() {
568 if (properties.contains(FunctionProperty.UNBOUNDED_ARITY) && arguments.isEmpty()) {
569 throw new IllegalStateException("to allow unbounded arity, at least one argument must be provided");
570 }
571
572 return new DefaultFunction(
573 ObjectUtils.requireNonNull(name, "the name must not be null"),
574 ObjectUtils.requireNonNull(namespace, "the namespace must not be null"),
575 properties,
576 new ArrayList<>(arguments),
577 // FIXME: Should return type be ISequenceType?
578 ISequenceType.of(returnType, returnOccurrence),
579 ObjectUtils.requireNonNull(functionHandler, "the function handler must not be null"));
580 }
581 }
582 }