Skip to content

Commit 946deac

Browse files
christophstroblmp911de
authored andcommitted
Support generating JsonSchema for Polymorphic fields.
This commit introduces MergedJsonSchema and MergedJsonSchemaProperty that can be used to merge properties of multiple objects into one as long as the additions do not conflict with another (eg. due to usage of different types). To resolve previously mentioned errors it is required to provide a ConflictResolutionFunction. Closes #3870 Original pull request: #3986.
1 parent 02229f2 commit 946deac

File tree

10 files changed

+998
-10
lines changed

10 files changed

+998
-10
lines changed

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

+61-9
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.stream.Collectors;
2525

2626
import org.bson.Document;
27-
2827
import org.springframework.data.mapping.PersistentProperty;
2928
import org.springframework.data.mapping.context.MappingContext;
3029
import org.springframework.data.mongodb.core.convert.MongoConverter;
@@ -45,6 +44,7 @@
4544
import org.springframework.util.Assert;
4645
import org.springframework.util.ClassUtils;
4746
import org.springframework.util.CollectionUtils;
47+
import org.springframework.util.LinkedMultiValueMap;
4848
import org.springframework.util.ObjectUtils;
4949
import org.springframework.util.StringUtils;
5050

@@ -62,6 +62,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
6262
private final MongoConverter converter;
6363
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
6464
private final Predicate<JsonSchemaPropertyContext> filter;
65+
private final LinkedMultiValueMap<String, Class<?>> mergeProperties;
6566

6667
/**
6768
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
@@ -72,23 +73,51 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
7273
MappingMongoJsonSchemaCreator(MongoConverter converter) {
7374

7475
this(converter, (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter.getMappingContext(),
75-
(property) -> true);
76+
(property) -> true, new LinkedMultiValueMap<>());
7677
}
7778

7879
@SuppressWarnings("unchecked")
7980
MappingMongoJsonSchemaCreator(MongoConverter converter,
8081
MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
81-
Predicate<JsonSchemaPropertyContext> filter) {
82+
Predicate<JsonSchemaPropertyContext> filter, LinkedMultiValueMap<String, Class<?>> mergeProperties) {
8283

8384
Assert.notNull(converter, "Converter must not be null!");
8485
this.converter = converter;
8586
this.mappingContext = mappingContext;
8687
this.filter = filter;
88+
this.mergeProperties = mergeProperties;
8789
}
8890

8991
@Override
9092
public MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter) {
91-
return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter);
93+
return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter, mergeProperties);
94+
}
95+
96+
@Override
97+
public PropertySpecifier specify(String path) {
98+
return new PropertySpecifier() {
99+
@Override
100+
public MongoJsonSchemaCreator types(Class<?>... types) {
101+
return specifyTypesFor(path, types);
102+
}
103+
};
104+
}
105+
106+
/**
107+
* Specify additional types to be considered wehen rendering the schema for the given path.
108+
*
109+
* @param path path the path using {@literal dot '.'} notation.
110+
* @param types must not be {@literal null}.
111+
* @return new instance of {@link MongoJsonSchemaCreator}.
112+
* @since 3.4
113+
*/
114+
public MongoJsonSchemaCreator specifyTypesFor(String path, Class<?>... types) {
115+
116+
LinkedMultiValueMap<String, Class<?>> clone = mergeProperties.clone();
117+
for (Class<?> type : types) {
118+
clone.add(path, type);
119+
}
120+
return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter, clone);
92121
}
93122

94123
@Override
@@ -131,9 +160,12 @@ private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistent
131160

132161
List<MongoPersistentProperty> currentPath = new ArrayList<>(path);
133162

134-
if (!filter.test(new PropertyContext(
135-
currentPath.stream().map(PersistentProperty::getName).collect(Collectors.joining(".")), nested))) {
136-
continue;
163+
String stringPath = currentPath.stream().map(PersistentProperty::getName).collect(Collectors.joining("."));
164+
stringPath = StringUtils.hasText(stringPath) ? (stringPath + "." + nested.getName()) : nested.getName();
165+
if (!filter.test(new PropertyContext(stringPath, nested))) {
166+
if (!mergeProperties.containsKey(stringPath)) {
167+
continue;
168+
}
137169
}
138170

139171
if (path.contains(nested)) { // cycle guard
@@ -151,14 +183,34 @@ private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistent
151183

152184
private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty> path) {
153185

186+
String stringPath = path.stream().map(MongoPersistentProperty::getName).collect(Collectors.joining("."));
154187
MongoPersistentProperty property = CollectionUtils.lastElement(path);
155188

156189
boolean required = isRequiredProperty(property);
157190
Class<?> rawTargetType = computeTargetType(property); // target type before conversion
158191
Class<?> targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type
159192

160-
if (!isCollection(property) && property.isEntity() && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
161-
return createObjectSchemaPropertyForEntity(path, property, required);
193+
if (!isCollection(property) && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
194+
if (property.isEntity() || mergeProperties.containsKey(stringPath)) {
195+
List<JsonSchemaProperty> targetProperties = new ArrayList<>();
196+
197+
if (property.isEntity()) {
198+
targetProperties.add(createObjectSchemaPropertyForEntity(path, property, required));
199+
}
200+
if (mergeProperties.containsKey(stringPath)) {
201+
for (Class<?> theType : mergeProperties.get(stringPath)) {
202+
203+
ObjectJsonSchemaProperty target = JsonSchemaProperty.object(property.getName());
204+
List<JsonSchemaProperty> nestedProperties = computePropertiesForEntity(path,
205+
mappingContext.getRequiredPersistentEntity(theType));
206+
207+
targetProperties.add(createPotentiallyRequiredSchemaProperty(
208+
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required));
209+
}
210+
}
211+
return targetProperties.size() == 1 ? targetProperties.iterator().next()
212+
: JsonSchemaProperty.combined(targetProperties);
213+
}
162214
}
163215

164216
String fieldName = computePropertyFieldName(property);

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

+40-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18+
import java.util.Arrays;
1819
import java.util.HashSet;
1920
import java.util.Set;
2021
import java.util.function.Predicate;
@@ -62,7 +63,6 @@
6263
* {@link org.springframework.data.annotation.Id _id} properties using types that can be converted into
6364
* {@link org.bson.types.ObjectId} like {@link String} will be mapped to {@code type : 'object'} unless there is more
6465
* specific information available via the {@link org.springframework.data.mongodb.core.mapping.MongoId} annotation.
65-
6666
* {@link Encrypted} properties will contain {@literal encrypt} information.
6767
*
6868
* @author Christoph Strobl
@@ -78,6 +78,20 @@ public interface MongoJsonSchemaCreator {
7878
*/
7979
MongoJsonSchema createSchemaFor(Class<?> type);
8080

81+
/**
82+
* Create a combined {@link MongoJsonSchema} out of the individual schemas of the given types by combining their
83+
* properties into one large {@link MongoJsonSchema schema}.
84+
*
85+
* @param types must not be {@literal null} nor contain {@literal null}.
86+
* @return new instance of {@link MongoJsonSchema}.
87+
* @since 3.4
88+
*/
89+
default MongoJsonSchema combineSchemaFor(Class<?>... types) {
90+
91+
MongoJsonSchema[] schemas = Arrays.stream(types).map(this::createSchemaFor).toArray(MongoJsonSchema[]::new);
92+
return MongoJsonSchema.combined(schemas);
93+
}
94+
8195
/**
8296
* Filter matching {@link JsonSchemaProperty properties}.
8397
*
@@ -87,6 +101,15 @@ public interface MongoJsonSchemaCreator {
87101
*/
88102
MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter);
89103

104+
/**
105+
* Entry point to specify additional behavior for a given path.
106+
*
107+
* @param path the path using {@literal dot '.'} notation.
108+
* @return new instance of {@link PropertySpecifier}.
109+
* @since 3.4
110+
*/
111+
PropertySpecifier specify(String path);
112+
90113
/**
91114
* The context in which a specific {@link #getProperty()} is encountered during schema creation.
92115
*
@@ -209,4 +232,20 @@ static MongoJsonSchemaCreator create() {
209232

210233
return create(converter);
211234
}
235+
236+
/**
237+
* @since 3.4
238+
* @author Christoph Strobl
239+
* @since 3.4
240+
*/
241+
interface PropertySpecifier {
242+
243+
/**
244+
* Set additional type parameters for polymorphic ones.
245+
*
246+
* @param types must not be {@literal null}.
247+
* @return the source
248+
*/
249+
MongoJsonSchemaCreator types(Class<?>... types);
250+
}
212251
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.schema;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.function.BiFunction;
23+
24+
import org.bson.Document;
25+
26+
/**
27+
* {@link MongoJsonSchema} implementation that is capable of combining properties of different schemas into one.
28+
*
29+
* @author Christoph Strobl
30+
* @since 3.4
31+
*/
32+
class CombinedJsonSchema implements MongoJsonSchema {
33+
34+
private final List<MongoJsonSchema> schemaList;
35+
private final BiFunction<Map<String, Object>, Map<String, Object>, Document> mergeFunction;
36+
37+
CombinedJsonSchema(List<MongoJsonSchema> schemaList, ConflictResolutionFunction conflictResolutionFunction) {
38+
this(schemaList, new TypeUnifyingMergeFunction(conflictResolutionFunction));
39+
}
40+
41+
CombinedJsonSchema(List<MongoJsonSchema> schemaList,
42+
BiFunction<Map<String, Object>, Map<String, Object>, Document> mergeFunction) {
43+
44+
this.schemaList = new ArrayList<>(schemaList);
45+
this.mergeFunction = mergeFunction;
46+
}
47+
48+
@Override
49+
public MongoJsonSchema combineWith(Collection<MongoJsonSchema> sources) {
50+
51+
schemaList.addAll(sources);
52+
return this;
53+
}
54+
55+
@Override
56+
public Document schemaDocument() {
57+
58+
Document targetSchema = new Document();
59+
for (MongoJsonSchema schema : schemaList) {
60+
targetSchema = mergeFunction.apply(targetSchema, schema.schemaDocument());
61+
}
62+
63+
return targetSchema;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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.schema;
17+
18+
import java.util.Collections;
19+
import java.util.Map;
20+
import java.util.Set;
21+
import java.util.function.BiFunction;
22+
23+
import org.bson.Document;
24+
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction;
25+
26+
/**
27+
* {@link JsonSchemaProperty} implementation that is capable of combining multiple properties with different values into
28+
* a single one.
29+
*
30+
* @author Christoph Strobl
31+
* @since 3.4
32+
*/
33+
class CombinedJsonSchemaProperty implements JsonSchemaProperty {
34+
35+
private final Iterable<JsonSchemaProperty> properties;
36+
private final BiFunction<Map<String, Object>, Map<String, Object>, Document> mergeFunction;
37+
38+
CombinedJsonSchemaProperty(Iterable<JsonSchemaProperty> properties) {
39+
this(properties, (k, a, b) -> {
40+
throw new IllegalStateException(
41+
String.format("Error resolving conflict for %s. No conflict resolution function defined.", k));
42+
});
43+
}
44+
45+
CombinedJsonSchemaProperty(Iterable<JsonSchemaProperty> properties,
46+
ConflictResolutionFunction conflictResolutionFunction) {
47+
this(properties, new TypeUnifyingMergeFunction(conflictResolutionFunction));
48+
}
49+
50+
CombinedJsonSchemaProperty(Iterable<JsonSchemaProperty> properties,
51+
BiFunction<Map<String, Object>, Map<String, Object>, Document> mergeFunction) {
52+
53+
this.properties = properties;
54+
this.mergeFunction = mergeFunction;
55+
}
56+
57+
@Override
58+
public Set<Type> getTypes() {
59+
return Collections.emptySet();
60+
}
61+
62+
@Override
63+
public Document toDocument() {
64+
65+
Document document = new Document();
66+
67+
for (JsonSchemaProperty property : properties) {
68+
document = mergeFunction.apply(document, property.toDocument());
69+
}
70+
return document;
71+
}
72+
73+
@Override
74+
public String getIdentifier() {
75+
return properties.iterator().next().getIdentifier();
76+
}
77+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java

+13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.core.schema;
1717

18+
import java.util.Collection;
19+
1820
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
1921
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
2022
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.*;
@@ -233,6 +235,17 @@ static JsonSchemaProperty required(JsonSchemaProperty property) {
233235
return new RequiredJsonSchemaProperty(property, true);
234236
}
235237

238+
/**
239+
* Combines multiple {@link JsonSchemaProperty} with potentially different attributes into one.
240+
*
241+
* @param properties must not be {@literal null}.
242+
* @return new instance of {@link JsonSchemaProperty}.
243+
* @since 3.4
244+
*/
245+
static JsonSchemaProperty combined(Collection<JsonSchemaProperty> properties) {
246+
return new CombinedJsonSchemaProperty(properties);
247+
}
248+
236249
/**
237250
* Builder for {@link IdentifiableJsonSchemaProperty}.
238251
*/

0 commit comments

Comments
 (0)