Skip to content

Commit cd9d764

Browse files
DATAMONGO-1849 - Schema derivation from domain type.
JsonSchemaCreator extracts the MongoJsonSchema for a given Class by applying the following mapping rules: Required Properties: - All Constructor arguments annotated with Nullable. - Properties of primitive type. Ignored Properties: - All properties annotated with Transient. Property Type Mapping: - java.lang.Object -> { type : 'object' } - java.util.Arrays -> { type : 'array' } - java.util.Collection -> { type : 'array'} - java.util.Map -> { type : 'object'} - java.lang.Enum -> { type : 'string', enum : [ ... ] } - Simple Types -> { type : 'the corresponding bson type' } - Domain Types -> { type : 'object', properties : { ... } } _id properties using types that can be converted into ObjectId like String will be mapped to { type : 'object' } unless there is more specific information available via the MongoId annotation.
1 parent 1bcb22a commit cd9d764

File tree

13 files changed

+1109
-197
lines changed

13 files changed

+1109
-197
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2019 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;
17+
18+
import org.springframework.data.mongodb.core.convert.MongoConverter;
19+
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* {@link JsonSchemaCreator} extracts the {@link MongoJsonSchema} for a given {@link Class} by applying the following
24+
* mapping rules.
25+
* <p>
26+
* <strong>Required Properties</strong><br />
27+
* - All Constructor arguments annotated with {@link org.springframework.lang.Nullable}. <br />
28+
* - Properties of primitive type. <br />
29+
* </p>
30+
* <p>
31+
* <strong>Ignored Properties</strong><br />
32+
* - All properties annotated with {@link org.springframework.data.annotation.Transient}. <br />
33+
* </p>
34+
* <p>
35+
* <strong>Property Type Mapping</strong><br />
36+
* - {@link java.lang.Object} -> {@code type : 'object'} <br />
37+
* - {@link java.util.Arrays} -> {@code type : 'array'} <br />
38+
* - {@link java.util.Collection} -> {@code type : 'array'} <br />
39+
* - {@link java.util.Map} -> {@code type : 'object'} <br />
40+
* - {@link java.lang.Enum} -> {@code type : 'string', enum : [the enum values]} <br />
41+
* - Simple Types -> {@code type : 'the corresponding bson type' } <br />
42+
* - Domain Types -> {@code type : 'object', properties : &#123;the types properties&#125; } <br />
43+
* <br />
44+
* {@link org.springframework.data.annotation.Id _id} properties using types that can be converted into
45+
* {@link org.bson.types.ObjectId} like {@link String} will be mapped to {@code type : 'object'} unless there is more
46+
* specific information available via the {@link org.springframework.data.mongodb.core.mapping.MongoId} annotation.
47+
* </p>
48+
*
49+
* @author Christoph Strobl
50+
* @since 2.2
51+
*/
52+
public interface JsonSchemaCreator {
53+
54+
/**
55+
* Create the {@link MongoJsonSchema} for the given {@link Class type}.
56+
*
57+
* @param type must not be {@literal null}.
58+
* @return never {@literal null}.
59+
*/
60+
MongoJsonSchema createSchemaFor(Class<?> type);
61+
62+
/**
63+
* Creates a new {@link JsonSchemaCreator} that is aware of conversions applied by the given {@link MongoConverter}.
64+
*
65+
* @param mongoConverter must not be {@literal null}.
66+
* @return new instance of {@link JsonSchemaCreator}.
67+
*/
68+
default JsonSchemaCreator jsonSchemaCreator(MongoConverter mongoConverter) {
69+
70+
Assert.notNull(mongoConverter, "MongoConverter must not be null!");
71+
return new MappingJsonSchemaCreator(mongoConverter);
72+
}
73+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright 2019 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;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.EnumSet;
22+
import java.util.List;
23+
24+
import org.springframework.data.mapping.PersistentEntity;
25+
import org.springframework.data.mapping.PersistentProperty;
26+
import org.springframework.data.mapping.SimplePropertyHandler;
27+
import org.springframework.data.mapping.context.MappingContext;
28+
import org.springframework.data.mongodb.core.convert.MongoConverter;
29+
import org.springframework.data.mongodb.core.mapping.MongoId;
30+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
31+
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
32+
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
33+
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
34+
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
35+
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
36+
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
37+
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
38+
import org.springframework.lang.Nullable;
39+
import org.springframework.util.Assert;
40+
import org.springframework.util.ClassUtils;
41+
import org.springframework.util.CollectionUtils;
42+
import org.springframework.util.ObjectUtils;
43+
44+
/**
45+
* {@link JsonSchemaCreator} implementation using both {@link MongoConverter} and {@link MappingContext} to obtain
46+
* domain type meta information which considers {@link org.springframework.data.mongodb.core.mapping.Field field names}
47+
* and {@link org.springframework.data.mongodb.core.convert.MongoCustomConversions custom conversions}.
48+
*
49+
* @author Christoph Strobl
50+
* @since 2.2
51+
*/
52+
class MappingJsonSchemaCreator implements JsonSchemaCreator {
53+
54+
private MongoConverter converter;
55+
private MappingContext mappingContext;
56+
57+
/**
58+
* Create a new instance of {@link MappingJsonSchemaCreator}.
59+
*
60+
* @param converter must not be {@literal null}.
61+
*/
62+
MappingJsonSchemaCreator(MongoConverter converter) {
63+
64+
Assert.notNull(converter, "Converter must not be null!");
65+
this.converter = converter;
66+
this.mappingContext = converter.getMappingContext();
67+
68+
}
69+
70+
/*
71+
* (non-Javadoc)
72+
* org.springframework.data.mongodb.core.JsonSchemaCreator#createSchemaFor(java.lang.Class)
73+
*/
74+
@Override
75+
public MongoJsonSchema createSchemaFor(Class<?> type) {
76+
77+
PersistentEntity<?, ?> entity = mappingContext.getPersistentEntity(type);
78+
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
79+
80+
List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
81+
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
82+
83+
return schemaBuilder.build();
84+
85+
}
86+
87+
private List<JsonSchemaProperty> computePropertiesForEntity(List<PersistentProperty> path,
88+
PersistentEntity<?, ?> entity) {
89+
90+
List<JsonSchemaProperty> schemaProperties = new ArrayList<>();
91+
entity.doWithProperties((SimplePropertyHandler) nested -> {
92+
93+
ArrayList<PersistentProperty> currentPath = new ArrayList<>(path);
94+
95+
if (path.contains(nested)) { // cycle guard
96+
schemaProperties.add(createSchemaProperty(computePropertyFieldName(CollectionUtils.lastElement(currentPath)),
97+
Object.class, false));
98+
return;
99+
}
100+
101+
currentPath.add(nested);
102+
JsonSchemaProperty jsonSchemaProperty = computeSchemaForProperty(currentPath, entity);
103+
if (jsonSchemaProperty != null) {
104+
schemaProperties.add(jsonSchemaProperty);
105+
}
106+
});
107+
108+
return schemaProperties;
109+
}
110+
111+
private JsonSchemaProperty computeSchemaForProperty(List<PersistentProperty> path, PersistentEntity<?, ?> parent) {
112+
113+
PersistentProperty property = CollectionUtils.lastElement(path);
114+
115+
boolean required = isRequiredProperty(parent, property);
116+
Class<?> rawTargetType = computeTargetType(property); // target type before conversion
117+
Class<?> targetType = converter.computeWriteTarget(rawTargetType); // conversion target type
118+
119+
if (property.isEntity() && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
120+
return createObjectSchemaPropertyForEntity(path, property, required);
121+
}
122+
123+
String fieldName = computePropertyFieldName(property);
124+
125+
if (property.isCollectionLike()) {
126+
return createSchemaProperty(fieldName, targetType, required);
127+
} else if (property.isMap()) {
128+
return createSchemaProperty(fieldName, Type.objectType(), required);
129+
} else if (ClassUtils.isAssignable(Enum.class, targetType)) {
130+
return createEnumSchemaProperty(fieldName, targetType, required);
131+
}
132+
133+
return createSchemaProperty(fieldName, targetType, required);
134+
}
135+
136+
private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<PersistentProperty> path,
137+
PersistentProperty property, boolean required) {
138+
139+
ObjectJsonSchemaProperty target = JsonSchemaProperty.object(property.getName());
140+
List<JsonSchemaProperty> nestedProperties = computePropertiesForEntity(path,
141+
mappingContext.getPersistentEntity(property));
142+
143+
return createPotentiallyRequiredSchemaProperty(
144+
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required);
145+
}
146+
147+
private JsonSchemaProperty createEnumSchemaProperty(String fieldName, Class<?> targetType, boolean required) {
148+
149+
List<Object> possibleValues = new ArrayList<>();
150+
for (Object enumValue : EnumSet.allOf((Class) targetType)) {
151+
possibleValues.add(converter.convertToMongoType(enumValue));
152+
}
153+
154+
targetType = possibleValues.isEmpty() ? targetType : possibleValues.iterator().next().getClass();
155+
return createSchemaProperty(fieldName, targetType, required, possibleValues);
156+
}
157+
158+
JsonSchemaProperty createSchemaProperty(String fieldName, Object type, boolean required) {
159+
return createSchemaProperty(fieldName, type, required, Collections.emptyList());
160+
}
161+
162+
JsonSchemaProperty createSchemaProperty(String fieldName, Object type, boolean required,
163+
Collection<?> possibleValues) {
164+
165+
TypedJsonSchemaObject schemaObject = type instanceof Type ? JsonSchemaObject.of(Type.class.cast(type))
166+
: JsonSchemaObject.of(Class.class.cast(type));
167+
168+
if (!CollectionUtils.isEmpty(possibleValues)) {
169+
schemaObject = schemaObject.possibleValues(possibleValues);
170+
}
171+
172+
return createPotentiallyRequiredSchemaProperty(JsonSchemaProperty.named(fieldName).with(schemaObject), required);
173+
}
174+
175+
private String computePropertyFieldName(PersistentProperty property) {
176+
177+
return property instanceof MongoPersistentProperty ? ((MongoPersistentProperty) property).getFieldName()
178+
: property.getName();
179+
}
180+
181+
private boolean isRequiredProperty(PersistentEntity<?, ?> parent, PersistentProperty property) {
182+
183+
return (parent.isConstructorArgument(property) && !property.isAnnotationPresent(Nullable.class))
184+
|| property.getType().isPrimitive();
185+
}
186+
187+
private Class<?> computeTargetType(PersistentProperty<?> property) {
188+
189+
if (!(property instanceof MongoPersistentProperty)) {
190+
return property.getType();
191+
}
192+
193+
MongoPersistentProperty mongoProperty = (MongoPersistentProperty) property;
194+
if (!mongoProperty.isIdProperty()) {
195+
return mongoProperty.getFieldType();
196+
}
197+
198+
if (mongoProperty.isAnnotationPresent(MongoId.class)) {
199+
return mongoProperty.findAnnotation(MongoId.class).value().getJavaClass();
200+
}
201+
202+
return mongoProperty.getFieldType() != mongoProperty.getActualType() ? Object.class : mongoProperty.getFieldType();
203+
}
204+
205+
static JsonSchemaProperty createPotentiallyRequiredSchemaProperty(JsonSchemaProperty property, boolean required) {
206+
207+
if (!required) {
208+
return property;
209+
}
210+
211+
return JsonSchemaProperty.required(property);
212+
}
213+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ public ConversionService getConversionService() {
105105
return conversionService;
106106
}
107107

108+
/*
109+
* (non-Javadoc)
110+
* @see org.springframework.data.mongodb.core.convert.MongoConverter#computeWriteTarget(java.lang.class)
111+
*/
112+
@Override
113+
public Class<?> computeWriteTarget(Class<?> source) {
114+
return conversions.getCustomWriteTarget(source).orElse(source);
115+
}
116+
108117
/* (non-Javadoc)
109118
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
110119
*/

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,15 @@ default Object convertId(@Nullable Object id, Class<?> targetType) {
149149
return convertToMongoType(id, null);
150150
}
151151
}
152+
153+
/**
154+
* Compute the target type for a given source considering {@link org.springframework.data.convert.CustomConversions}.
155+
*
156+
* @param source the source type.
157+
* @return never {@literal null}.
158+
* @since 2.2
159+
*/
160+
default Class<?> computeWriteTarget(Class<?> source) {
161+
return source;
162+
}
152163
}

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

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ public DateJsonSchemaProperty description(String description) {
953953

954954
/**
955955
* @return new instance of {@link DateJsonSchemaProperty}.
956-
* @see DateJsonSchemaProperty#generateDescription()
956+
* @see DateJsonSchemaProperty#generatedDescription()
957957
*/
958958
public DateJsonSchemaProperty generatedDescription() {
959959
return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
@@ -983,10 +983,65 @@ public TimestampJsonSchemaProperty description(String description) {
983983

984984
/**
985985
* @return new instance of {@link TimestampJsonSchemaProperty}.
986-
* @see TimestampJsonSchemaProperty#generateDescription()
986+
* @see TimestampJsonSchemaProperty#generatedDescription()
987987
*/
988988
public TimestampJsonSchemaProperty generatedDescription() {
989989
return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
990990
}
991991
}
992+
993+
/**
994+
* Delegating {@link JsonSchemaProperty} implementation having a {@literal required} flag for evaluation during schema
995+
* creation process.
996+
*
997+
* @author Christoph Strobl
998+
* @since 2.2
999+
*/
1000+
public static class RequiredJsonSchemaProperty implements JsonSchemaProperty {
1001+
1002+
private final JsonSchemaProperty delegate;
1003+
private final boolean required;
1004+
1005+
RequiredJsonSchemaProperty(JsonSchemaProperty delegate, boolean required) {
1006+
1007+
this.delegate = delegate;
1008+
this.required = required;
1009+
}
1010+
1011+
/*
1012+
* (non-Javadoc)
1013+
* @see org.springframework.data.mongodb.core.schema.JsonSchemaProperty#getIdentifier()
1014+
*/
1015+
@Override
1016+
public String getIdentifier() {
1017+
return delegate.getIdentifier();
1018+
}
1019+
1020+
/*
1021+
* (non-Javadoc)
1022+
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes()
1023+
*/
1024+
@Override
1025+
public Set<Type> getTypes() {
1026+
return delegate.getTypes();
1027+
}
1028+
1029+
/*
1030+
* (non-Javadoc)
1031+
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()
1032+
*/
1033+
@Override
1034+
public Document toDocument() {
1035+
return delegate.toDocument();
1036+
}
1037+
1038+
/*
1039+
* (non-Javadoc)
1040+
* @see org.springframework.data.mongodb.core.schema.JsonSchemaProperty#isRequired()
1041+
*/
1042+
@Override
1043+
public boolean isRequired() {
1044+
return required;
1045+
}
1046+
}
9921047
}

0 commit comments

Comments
 (0)