Skip to content

Commit 47bb7a7

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 eee8fc4 commit 47bb7a7

File tree

7 files changed

+663
-8
lines changed

7 files changed

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

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

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

108+
@Override
109+
public Class<?> computeWriteTarget(Class<?> source) {
110+
return conversions.getCustomWriteTarget(source).orElse(source);
111+
}
112+
108113
/* (non-Javadoc)
109114
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
110115
*/

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,6 @@ default Object convertId(@Nullable Object id, Class<?> targetType) {
149149
return convertToMongoType(id, null);
150150
}
151151
}
152+
153+
Class<?> computeWriteTarget(Class<?> source);
152154
}

0 commit comments

Comments
 (0)