Skip to content

DATAMONGO-1849 - Resolve JsonSchema from domain type. #733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.DATAMONGO-1849-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-benchmarks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.DATAMONGO-1849-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-mongodb-cross-store/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.DATAMONGO-1849-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down Expand Up @@ -50,7 +50,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.DATAMONGO-1849-SNAPSHOT</version>
</dependency>

<!-- reactive -->
Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.DATAMONGO-1849-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.DATAMONGO-1849-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;

import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

/**
* {@link MongoJsonSchemaCreator} implementation using both {@link MongoConverter} and {@link MappingContext} to obtain
* domain type meta information which considers {@link org.springframework.data.mongodb.core.mapping.Field field names}
* and {@link org.springframework.data.mongodb.core.convert.MongoCustomConversions custom conversions}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 2.2
*/
class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {

private final MongoConverter converter;
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;

/**
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
*
* @param converter must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter) {

Assert.notNull(converter, "Converter must not be null!");
this.converter = converter;
this.mappingContext = (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter
.getMappingContext();
}

/*
* (non-Javadoc)
* org.springframework.data.mongodb.core.MongoJsonSchemaCreator#createSchemaFor(java.lang.Class)
*/
@Override
public MongoJsonSchema createSchemaFor(Class<?> type) {

MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();

List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));

return schemaBuilder.build();

}

private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
MongoPersistentEntity<?> entity) {

List<JsonSchemaProperty> schemaProperties = new ArrayList<>();

for (MongoPersistentProperty nested : entity) {

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

if (path.contains(nested)) { // cycle guard
schemaProperties.add(createSchemaProperty(computePropertyFieldName(CollectionUtils.lastElement(currentPath)),
Object.class, false));
break;
}

currentPath.add(nested);
schemaProperties.add(computeSchemaForProperty(currentPath));
}

return schemaProperties;
}

private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty> path) {

MongoPersistentProperty property = CollectionUtils.lastElement(path);

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

if (property.isEntity() && ObjectUtils.nullSafeEquals(rawTargetType, targetType)) {
return createObjectSchemaPropertyForEntity(path, property, required);
}

String fieldName = computePropertyFieldName(property);

if (property.isCollectionLike()) {
return createSchemaProperty(fieldName, targetType, required);
} else if (property.isMap()) {
return createSchemaProperty(fieldName, Type.objectType(), required);
} else if (ClassUtils.isAssignable(Enum.class, targetType)) {
return createEnumSchemaProperty(fieldName, targetType, required);
}

return createSchemaProperty(fieldName, targetType, required);
}

private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,
MongoPersistentProperty property, boolean required) {

ObjectJsonSchemaProperty target = JsonSchemaProperty.object(property.getName());
List<JsonSchemaProperty> nestedProperties = computePropertiesForEntity(path,
mappingContext.getRequiredPersistentEntity(property));

return createPotentiallyRequiredSchemaProperty(
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required);
}

private JsonSchemaProperty createEnumSchemaProperty(String fieldName, Class<?> targetType, boolean required) {

List<Object> possibleValues = new ArrayList<>();

for (Object enumValue : EnumSet.allOf((Class) targetType)) {
possibleValues.add(converter.convertToMongoType(enumValue));
}

targetType = possibleValues.isEmpty() ? targetType : possibleValues.iterator().next().getClass();
return createSchemaProperty(fieldName, targetType, required, possibleValues);
}

JsonSchemaProperty createSchemaProperty(String fieldName, Object type, boolean required) {
return createSchemaProperty(fieldName, type, required, Collections.emptyList());
}

JsonSchemaProperty createSchemaProperty(String fieldName, Object type, boolean required,
Collection<?> possibleValues) {

TypedJsonSchemaObject schemaObject = type instanceof Type ? JsonSchemaObject.of(Type.class.cast(type))
: JsonSchemaObject.of(Class.class.cast(type));

if (!CollectionUtils.isEmpty(possibleValues)) {
schemaObject = schemaObject.possibleValues(possibleValues);
}

return createPotentiallyRequiredSchemaProperty(JsonSchemaProperty.named(fieldName).with(schemaObject), required);
}

private String computePropertyFieldName(PersistentProperty property) {

return property instanceof MongoPersistentProperty ? ((MongoPersistentProperty) property).getFieldName()
: property.getName();
}

private boolean isRequiredProperty(PersistentProperty property) {
return property.getType().isPrimitive();
}

private Class<?> computeTargetType(PersistentProperty<?> property) {

if (!(property instanceof MongoPersistentProperty)) {
return property.getType();
}

MongoPersistentProperty mongoProperty = (MongoPersistentProperty) property;
if (!mongoProperty.isIdProperty()) {
return mongoProperty.getFieldType();
}

if (mongoProperty.hasExplicitWriteTarget()) {
return mongoProperty.getRequiredAnnotation(Field.class).targetType().getJavaClass();
}

return mongoProperty.getFieldType() != mongoProperty.getActualType() ? Object.class : mongoProperty.getFieldType();
}

static JsonSchemaProperty createPotentiallyRequiredSchemaProperty(JsonSchemaProperty property, boolean required) {

if (!required) {
return property;
}

return JsonSchemaProperty.required(property);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;

import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;

/**
* {@link MongoJsonSchemaCreator} extracts the {@link MongoJsonSchema} for a given {@link Class} by applying the
* following mapping rules.
* <p>
* <strong>Required Properties</strong>
* <ul>
* <li>All constructor arguments annotated with {@link NonNull}</li>
* <li>{@link NonNull} properties</li>
* <li>Properties of primitive type</li>
* </ul>
* <strong>Ignored Properties</strong>
* <ul>
* <li>All properties annotated with {@link org.springframework.data.annotation.Transient}</li>
* </ul>
* <strong>Property Type Mapping</strong>
* <ul>
* <li>{@link java.lang.Object} -> {@code type : 'object'}</li>
* <li>{@link java.util.Arrays} -> {@code type : 'array'}</li>
* <li>{@link java.util.Collection} -> {@code type : 'array'}</li>
* <li>{@link java.util.Map} -> {@code type : 'object'}</li>
* <li>{@link java.lang.Enum} -> {@code type : 'string', enum : [the enum values]}</li>
* <li>Simple Types -> {@code type : 'the corresponding bson type' }</li>
* <li>Domain Types -> {@code type : 'object', properties : &#123;the types properties&#125; }</li>
* </ul>
* <br />
* {@link org.springframework.data.annotation.Id _id} properties using types that can be converted into
* {@link org.bson.types.ObjectId} like {@link String} will be mapped to {@code type : 'object'} unless there is more
* specific information available via the {@link org.springframework.data.mongodb.core.mapping.MongoId} annotation.
* </p>
*
* @author Christoph Strobl
* @since 2.2
*/
public interface MongoJsonSchemaCreator {

/**
* Create the {@link MongoJsonSchema} for the given {@link Class type}.
*
* @param type must not be {@literal null}.
* @return never {@literal null}.
*/
MongoJsonSchema createSchemaFor(Class<?> type);

/**
* Creates a new {@link MongoJsonSchemaCreator} that is aware of conversions applied by the given
* {@link MongoConverter}.
*
* @param mongoConverter must not be {@literal null}.
* @return new instance of {@link MongoJsonSchemaCreator}.
*/
static MongoJsonSchemaCreator create(MongoConverter mongoConverter) {

Assert.notNull(mongoConverter, "MongoConverter must not be null!");
return new MappingMongoJsonSchemaCreator(mongoConverter);
}
}
Loading