Skip to content

Commit f1cff3c

Browse files
christophstroblmp911de
authored andcommitted
Introduce AggregationVariable type.
This commit introduces a new AggregationVariable type that is intended to better identify variables within a pipeline to avoid mapping failures caused by invalid field names. Closes #4070 Original pull request: #4242
1 parent 0fd1273 commit f1cff3c

File tree

7 files changed

+295
-28
lines changed

7 files changed

+295
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import org.springframework.lang.Nullable;
19+
import org.springframework.util.Assert;
20+
import org.springframework.util.ObjectUtils;
21+
22+
/**
23+
* A special field that points to a variable {@code $$} expression.
24+
*
25+
* @author Christoph Strobl
26+
* @since 4.1
27+
*/
28+
public interface AggregationVariable extends Field {
29+
30+
String PREFIX = "$$";
31+
32+
/**
33+
* @return {@literal true} if the fields {@link #getName() name} does not match the defined {@link #getTarget()
34+
* target}.
35+
*/
36+
default boolean isAliased() {
37+
return !ObjectUtils.nullSafeEquals(getName(), getTarget());
38+
}
39+
40+
@Override
41+
default String getName() {
42+
return getTarget();
43+
}
44+
45+
default boolean isInternal() {
46+
return false;
47+
}
48+
49+
/**
50+
* Create a new {@link AggregationVariable} for the given name.
51+
* <p>
52+
* Variables start with {@code $$}. If not, the given value gets prefixed with {@code $$}.
53+
*
54+
* @param value must not be {@literal null}.
55+
* @return new instance of {@link AggregationVariable}.
56+
* @throws IllegalArgumentException if given value is {@literal null}.
57+
*/
58+
static AggregationVariable variable(String value) {
59+
60+
Assert.notNull(value, "Value must not be null");
61+
return new AggregationVariable() {
62+
63+
private final String val = AggregationVariable.prefixVariable(value);
64+
65+
@Override
66+
public String getTarget() {
67+
return val;
68+
}
69+
};
70+
}
71+
72+
/**
73+
* Create a new {@link #isInternal() local} {@link AggregationVariable} for the given name.
74+
* <p>
75+
* Variables start with {@code $$}. If not, the given value gets prefixed with {@code $$}.
76+
*
77+
* @param value must not be {@literal null}.
78+
* @return new instance of {@link AggregationVariable}.
79+
* @throws IllegalArgumentException if given value is {@literal null}.
80+
*/
81+
static AggregationVariable localVariable(String value) {
82+
83+
Assert.notNull(value, "Value must not be null");
84+
return new AggregationVariable() {
85+
86+
private final String val = AggregationVariable.prefixVariable(value);
87+
88+
@Override
89+
public String getTarget() {
90+
return val;
91+
}
92+
93+
@Override
94+
public boolean isInternal() {
95+
return true;
96+
}
97+
};
98+
}
99+
100+
/**
101+
* Check if the given field name reference may be variable.
102+
*
103+
* @param fieldRef can be {@literal null}.
104+
* @return true if given value matches the variable identification pattern.
105+
*/
106+
static boolean isVariable(@Nullable String fieldRef) {
107+
return fieldRef != null && fieldRef.stripLeading().matches("^\\$\\$\\w.*");
108+
}
109+
110+
/**
111+
* Check if the given field may be variable.
112+
*
113+
* @param field can be {@literal null}.
114+
* @return true if given {@link Field field} is an {@link AggregationVariable} or if its value is a
115+
* {@link #isVariable(String) variable}.
116+
*/
117+
static boolean isVariable(Field field) {
118+
119+
if (field instanceof AggregationVariable) {
120+
return true;
121+
}
122+
return isVariable(field.getTarget());
123+
}
124+
125+
private static String prefixVariable(String variable) {
126+
127+
var trimmed = variable.stripLeading();
128+
return trimmed.startsWith(PREFIX) ? trimmed : (PREFIX + trimmed);
129+
}
130+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java

+16-19
Original file line numberDiff line numberDiff line change
@@ -1515,52 +1515,39 @@ public interface AsBuilder {
15151515
}
15161516
}
15171517

1518-
public enum Variable implements Field {
1518+
public enum Variable implements AggregationVariable {
15191519

15201520
THIS {
1521-
@Override
1522-
public String getName() {
1523-
return "$$this";
1524-
}
15251521

15261522
@Override
15271523
public String getTarget() {
15281524
return "$$this";
15291525
}
15301526

1531-
@Override
1532-
public boolean isAliased() {
1533-
return false;
1534-
}
1535-
15361527
@Override
15371528
public String toString() {
15381529
return getName();
15391530
}
15401531
},
15411532

15421533
VALUE {
1543-
@Override
1544-
public String getName() {
1545-
return "$$value";
1546-
}
15471534

15481535
@Override
15491536
public String getTarget() {
15501537
return "$$value";
15511538
}
15521539

1553-
@Override
1554-
public boolean isAliased() {
1555-
return false;
1556-
}
1557-
15581540
@Override
15591541
public String toString() {
15601542
return getName();
15611543
}
15621544
};
15631545

1546+
@Override
1547+
public boolean isInternal() {
1548+
return true;
1549+
}
1550+
15641551
/**
15651552
* Create a {@link Field} reference to a given {@literal property} prefixed with the {@link Variable} identifier.
15661553
* eg. {@code $$value.product}
@@ -1592,6 +1579,16 @@ public String toString() {
15921579
}
15931580
};
15941581
}
1582+
1583+
public static boolean isVariable(Field field) {
1584+
1585+
for (Variable var : values()) {
1586+
if (field.getTarget().startsWith(var.getTarget())) {
1587+
return true;
1588+
}
1589+
}
1590+
return false;
1591+
}
15951592
}
15961593
}
15971594

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public AggregationField(String name, @Nullable String target) {
245245

246246
private static String cleanUp(String source) {
247247

248-
if (SystemVariable.isReferingToSystemVariable(source)) {
248+
if (AggregationVariable.isVariable(source)) {
249249
return source;
250250
}
251251

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java

+20-7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* @author Christoph Strobl
2525
* @see <a href="https://docs.mongodb.com/manual/reference/aggregation-variables">Aggregation Variables</a>.
2626
*/
27-
public enum SystemVariable {
27+
public enum SystemVariable implements AggregationVariable {
2828

2929
/**
3030
* Variable for the current datetime.
@@ -82,8 +82,6 @@ public enum SystemVariable {
8282
*/
8383
SEARCH_META;
8484

85-
private static final String PREFIX = "$$";
86-
8785
/**
8886
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
8987
* otherwise.
@@ -93,13 +91,12 @@ public enum SystemVariable {
9391
*/
9492
public static boolean isReferingToSystemVariable(@Nullable String fieldRef) {
9593

96-
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
94+
String candidate = variableNameFrom(fieldRef);
95+
if (candidate == null) {
9796
return false;
9897
}
9998

100-
int indexOfFirstDot = fieldRef.indexOf('.');
101-
String candidate = fieldRef.substring(2, indexOfFirstDot == -1 ? fieldRef.length() : indexOfFirstDot);
102-
99+
candidate = candidate.startsWith(PREFIX) ? candidate.substring(2) : candidate;
103100
for (SystemVariable value : values()) {
104101
if (value.name().equals(candidate)) {
105102
return true;
@@ -113,4 +110,20 @@ public static boolean isReferingToSystemVariable(@Nullable String fieldRef) {
113110
public String toString() {
114111
return PREFIX.concat(name());
115112
}
113+
114+
@Override
115+
public String getTarget() {
116+
return toString();
117+
}
118+
119+
@Nullable
120+
static String variableNameFrom(@Nullable String fieldRef) {
121+
122+
if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) {
123+
return null;
124+
}
125+
126+
int indexOfFirstDot = fieldRef.indexOf('.');
127+
return indexOfFirstDot == -1 ? fieldRef : fieldRef.substring(2, indexOfFirstDot);
128+
}
116129
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public AggregationOperationContext continueOnMissingFieldReference(Class<?> type
133133

134134
protected FieldReference getReferenceFor(Field field) {
135135

136-
if(entity.getNullable() == null) {
136+
if(entity.getNullable() == null || AggregationVariable.isVariable(field)) {
137137
return new DirectFieldReference(new ExposedField(field, true));
138138
}
139139

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
import org.mockito.Mockito;
22+
23+
/**
24+
* @author Christoph Strobl
25+
*/
26+
class AggregationVariableUnitTests {
27+
28+
@Test // GH-4070
29+
void variableErrorsOnNullValue() {
30+
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> AggregationVariable.variable(null));
31+
}
32+
33+
@Test // GH-4070
34+
void createsVariable() {
35+
36+
var variable = AggregationVariable.variable("$$now");
37+
38+
assertThat(variable.getTarget()).isEqualTo("$$now");
39+
assertThat(variable.isInternal()).isFalse();
40+
}
41+
42+
@Test // GH-4070
43+
void prefixesVariableIfNeeded() {
44+
45+
var variable = AggregationVariable.variable("this");
46+
47+
assertThat(variable.getTarget()).isEqualTo("$$this");
48+
}
49+
50+
@Test // GH-4070
51+
void localVariableErrorsOnNullValue() {
52+
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> AggregationVariable.localVariable(null));
53+
}
54+
55+
@Test // GH-4070
56+
void localVariable() {
57+
58+
var variable = AggregationVariable.localVariable("$$this");
59+
60+
assertThat(variable.getTarget()).isEqualTo("$$this");
61+
assertThat(variable.isInternal()).isTrue();
62+
}
63+
64+
@Test // GH-4070
65+
void prefixesLocalVariableIfNeeded() {
66+
67+
var variable = AggregationVariable.localVariable("this");
68+
69+
assertThat(variable.getTarget()).isEqualTo("$$this");
70+
}
71+
72+
@Test // GH-4070
73+
void isVariableReturnsTrueForAggregationVariableTypes() {
74+
75+
var variable = Mockito.mock(AggregationVariable.class);
76+
77+
assertThat(AggregationVariable.isVariable(variable)).isTrue();
78+
}
79+
80+
@Test // GH-4070
81+
void isVariableReturnsTrueForFieldThatTargetsVariable() {
82+
83+
var variable = Fields.field("value", "$$this");
84+
85+
assertThat(AggregationVariable.isVariable(variable)).isTrue();
86+
}
87+
88+
@Test // GH-4070
89+
void isVariableReturnsFalseForFieldThatDontTargetsVariable() {
90+
91+
var variable = Fields.field("value", "$this");
92+
93+
assertThat(AggregationVariable.isVariable(variable)).isFalse();
94+
}
95+
}

0 commit comments

Comments
 (0)