Skip to content

Commit ebdf350

Browse files
authored
Add substring-before() implementation (#194)
* Add string-before impl and tests for #132
1 parent 1b54892 commit ebdf350

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java

+1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ public DefaultFunctionLibrary() { // NOPMD - intentional
166166
// P1: https://www.w3.org/TR/xpath-functions-31/#func-substring-after
167167
registerFunction(FnSubstringAfter.SIGNATURE_TWO_ARG);
168168
// P1: https://www.w3.org/TR/xpath-functions-31/#func-substring-before
169+
registerFunction(FnSubstringBefore.SIGNATURE_TWO_ARG);
169170
// https://www.w3.org/TR/xpath-functions-31/#func-sum
170171
registerFunction(FnSum.SIGNATURE_ONE_ARG);
171172
registerFunction(FnSum.SIGNATURE_TWO_ARG);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* SPDX-FileCopyrightText: none
3+
* SPDX-License-Identifier: CC0-1.0
4+
*/
5+
6+
package gov.nist.secauto.metaschema.core.metapath.function.library;
7+
8+
import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
9+
import gov.nist.secauto.metaschema.core.metapath.ISequence;
10+
import gov.nist.secauto.metaschema.core.metapath.MetapathConstants;
11+
import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils;
12+
import gov.nist.secauto.metaschema.core.metapath.function.IArgument;
13+
import gov.nist.secauto.metaschema.core.metapath.function.IFunction;
14+
import gov.nist.secauto.metaschema.core.metapath.item.IItem;
15+
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDecimalItem;
16+
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
17+
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
18+
19+
import java.util.List;
20+
21+
import edu.umd.cs.findbugs.annotations.NonNull;
22+
23+
import org.apache.commons.lang3.StringUtils;
24+
25+
/**
26+
* Implements <a href=
27+
* "https://www.w3.org/TR/xpath-functions-31/#func-substring-before">fn:substring-before</a>.
28+
*/
29+
public final class FnSubstringBefore {
30+
private static final String NAME = "substring-before";
31+
@NonNull
32+
static final IFunction SIGNATURE_TWO_ARG = IFunction.builder()
33+
.name(NAME)
34+
.namespace(MetapathConstants.NS_METAPATH_FUNCTIONS)
35+
.deterministic()
36+
.contextIndependent()
37+
.focusIndependent()
38+
.argument(IArgument.builder()
39+
.name("arg1")
40+
.type(IStringItem.class)
41+
.zeroOrOne()
42+
.build())
43+
.argument(IArgument.builder()
44+
.name("arg2")
45+
.type(IStringItem.class)
46+
.zeroOrOne()
47+
.build())
48+
.returnType(IStringItem.class)
49+
.returnOne()
50+
.functionHandler(FnSubstringBefore::executeTwoArg)
51+
.build();
52+
53+
private FnSubstringBefore() {
54+
// disable construction
55+
}
56+
57+
@SuppressWarnings({ "unused", "PMD.OnlyOneReturn" })
58+
@NonNull
59+
private static ISequence<IStringItem> executeTwoArg(
60+
@NonNull IFunction function,
61+
@NonNull List<ISequence<?>> arguments,
62+
@NonNull DynamicContext dynamicContext,
63+
IItem focus) {
64+
65+
// From the XPath 3.1 specification:
66+
// If the value of $arg1 or $arg2 is the empty sequence, or contains only
67+
// ignorable collation units, it is interpreted as the zero-length string.
68+
IStringItem arg1 = arguments.get(0).isEmpty() ? IStringItem.valueOf("") : FunctionUtils.asTypeOrNull(arguments.get(0).getFirstItem(true));
69+
IStringItem arg2 = arguments.get(1).isEmpty() ? IStringItem.valueOf("") : FunctionUtils.asTypeOrNull(arguments.get(1).getFirstItem(true));
70+
71+
return ISequence.of(IStringItem.valueOf(fnSubstringBefore(arg1.asString(), arg2.asString())));
72+
}
73+
74+
/**
75+
* An implementation of XPath 3.1 <a href=
76+
* "https://www.w3.org/TR/xpath-functions-31/#func-substring-before">fn:substring-before</a>.
77+
*
78+
* @param arg1
79+
* the source string to get a substring from
80+
* @param arg2
81+
* the substring to match and find the substring to return before the match
82+
* @return the substring
83+
*/
84+
@NonNull
85+
public static String fnSubstringBefore(
86+
@NonNull String arg1,
87+
@NonNull String arg2) {
88+
return StringUtils.substringBefore(arg1,arg2);
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* SPDX-FileCopyrightText: none
3+
* SPDX-License-Identifier: CC0-1.0
4+
*/
5+
6+
package gov.nist.secauto.metaschema.core.metapath.function.library;
7+
8+
import static gov.nist.secauto.metaschema.core.metapath.TestUtils.string;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase;
12+
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
13+
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
14+
15+
import org.junit.jupiter.params.ParameterizedTest;
16+
import org.junit.jupiter.params.provider.Arguments;
17+
import org.junit.jupiter.params.provider.MethodSource;
18+
19+
import java.util.stream.Stream;
20+
21+
import edu.umd.cs.findbugs.annotations.NonNull;
22+
23+
class FnSubstringBeforeTest
24+
extends ExpressionTestBase {
25+
private static Stream<Arguments> provideValues() { // NOPMD - false positive
26+
return Stream.of(
27+
Arguments.of(
28+
string("t"),
29+
"substring-before('tattoo', 'attoo')"),
30+
Arguments.of(
31+
string(""),
32+
"substring-before('tattoo', 'tatto')"),
33+
Arguments.of(
34+
string(""),
35+
"substring-before((), ())")
36+
);
37+
}
38+
39+
@ParameterizedTest
40+
@MethodSource("provideValues")
41+
void testExpression(@NonNull IStringItem expected, @NonNull String metapath) {
42+
assertEquals(
43+
expected,
44+
MetapathExpression.compile(metapath)
45+
.evaluateAs(null, MetapathExpression.ResultType.ITEM, newDynamicContext()));
46+
}
47+
48+
}

0 commit comments

Comments
 (0)