Browse Source

Introduce Queryable annotation

issue/4185-light
Christoph Strobl 9 months ago
parent
commit
7472a3abd8
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
  2. 25
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
  3. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java
  4. 49
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java
  5. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java
  6. 41
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

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

@ -31,6 +31,7 @@ import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.Queryable;
import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
@ -296,7 +297,8 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
enc = enc.keys(property.getEncryptionKeyIds()); enc = enc.keys(property.getEncryptionKeyIds());
} }
if (!StringUtils.hasText(encrypted.queryType())) { Queryable queryable = property.findAnnotation(Queryable.class);
if (queryable == null || !StringUtils.hasText(queryable.queryType())) {
return enc; return enc;
} }
@ -304,7 +306,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
@Override @Override
public String queryType() { public String queryType() {
return encrypted.queryType(); return queryable.queryType();
} }
@Override @Override
@ -312,12 +314,11 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
Document options = QueryCharacteristic.super.toDocument(); Document options = QueryCharacteristic.super.toDocument();
RangeEncrypted rangeEncrypted = property.findAnnotation(RangeEncrypted.class); if (queryable.contentionFactor() >= 0) {
if (rangeEncrypted != null) { options.put("contention", queryable.contentionFactor());
options.put("contention", rangeEncrypted.contentionFactor());
} }
if (!encrypted.queryAttributes().isEmpty()) { if (!queryable.queryAttributes().isEmpty()) {
options.putAll(Document.parse(encrypted.queryAttributes())); options.putAll(Document.parse(queryable.queryAttributes()));
} }
return options; return options;

25
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java

@ -41,7 +41,7 @@ import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver;
import org.springframework.data.mongodb.core.encryption.EncryptionOptions; import org.springframework.data.mongodb.core.encryption.EncryptionOptions;
import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.mapping.Queryable;
import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -179,7 +179,8 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key, EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key,
getEQOptions(persistentProperty, fieldNameAndQueryOperator)); getEQOptions(persistentProperty, fieldNameAndQueryOperator));
if (fieldNameAndQueryOperator != null && encryptionOptions.queryableEncryptionOptions() != null) { if (fieldNameAndQueryOperator != null && encryptionOptions.queryableEncryptionOptions() != null
&& !encryptionOptions.queryableEncryptionOptions().getQueryType().equals("equality")) {
return encryptExpression(fieldNameAndQueryOperator, value, encryptionOptions); return encryptExpression(fieldNameAndQueryOperator, value, encryptionOptions);
} else { } else {
return encryptValue(value, context, persistentProperty, encryptionOptions); return encryptValue(value, context, persistentProperty, encryptionOptions);
@ -189,28 +190,28 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty, private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty,
String fieldNameAndQueryOperator) { String fieldNameAndQueryOperator) {
Encrypted encryptedAnnotation = persistentProperty.findAnnotation(Encrypted.class); Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class);
if (encryptedAnnotation != null && !StringUtils.hasText(encryptedAnnotation.queryType())) { if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) {
return null; return null;
} }
QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none(); QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none();
String queryAttributes = encryptedAnnotation.queryAttributes(); String queryAttributes = queryableAnnotation.queryAttributes();
if (!queryAttributes.isEmpty()) { if (!queryAttributes.isEmpty()) {
queryableEncryptionOptions = queryableEncryptionOptions.attributes(Document.parse(queryAttributes)); queryableEncryptionOptions = queryableEncryptionOptions.attributes(Document.parse(queryAttributes));
} }
RangeEncrypted rangeEncryptedAnnotation = persistentProperty.findAnnotation(RangeEncrypted.class); if (queryableAnnotation.contentionFactor() >= 0) {
if (rangeEncryptedAnnotation != null && rangeEncryptedAnnotation.contentionFactor() >= 0) { queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(queryableAnnotation.contentionFactor());
queryableEncryptionOptions = queryableEncryptionOptions
.contentionFactor(rangeEncryptedAnnotation.contentionFactor());
} }
boolean isPartOfARangeQuery = fieldNameAndQueryOperator != null; boolean isPartOfARangeQuery = fieldNameAndQueryOperator != null;
if (isPartOfARangeQuery || !encryptedAnnotation.queryType().isEmpty()) { if (isPartOfARangeQuery) {
queryableEncryptionOptions = queryableEncryptionOptions.queryType(encryptedAnnotation.queryType()); // should the type move to an extra annotation? queryableEncryptionOptions = queryableEncryptionOptions.queryType(queryableAnnotation.queryType()); // should the
queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(1l); // type move
// to an extra
// annotation?
} }
return queryableEncryptionOptions; return queryableEncryptionOptions;
} }

12
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java

@ -109,16 +109,4 @@ public @interface Encrypted {
* @see org.springframework.data.mongodb.core.EncryptionAlgorithms * @see org.springframework.data.mongodb.core.EncryptionAlgorithms
*/ */
String algorithm() default ""; String algorithm() default "";
/**
* @return empty {@link String} if not set.
* @since 4.5
*/
String queryType() default "";
/**
* @return empty {@link String} if not set.
* @since 4.5
*/
String queryAttributes() default "";
} }

49
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java

@ -0,0 +1,49 @@
/*
* Copyright 2025. 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
*
* http://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.mapping;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Christoph Strobl
* @since 4.5
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
public @interface Queryable {
/**
* @return empty {@link String} if not set.
* @since 4.5
*/
String queryType() default "";
/**
* @return empty {@link String} if not set.
* @since 4.5
*/
String queryAttributes() default "";
/**
* Set the contention factor
*
* @return the contention factor
*/
long contentionFactor() default -1;
}

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java

@ -29,7 +29,8 @@ import org.springframework.core.annotation.AliasFor;
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) @Target(ElementType.FIELD)
@Encrypted(algorithm = "Range", queryType = "range") @Encrypted(algorithm = "Range")
@Queryable(queryType = "range")
public @interface RangeEncrypted { public @interface RangeEncrypted {
/** /**
@ -37,6 +38,7 @@ public @interface RangeEncrypted {
* *
* @return the contention factor * @return the contention factor
*/ */
@AliasFor(annotation = Queryable.class, value = "contentionFactor")
long contentionFactor() default -1; long contentionFactor() default -1;
/** /**
@ -49,6 +51,6 @@ public @interface RangeEncrypted {
* *
* @return the json representation of range options * @return the json representation of range options
*/ */
@AliasFor(annotation = Encrypted.class, value = "queryAttributes") @AliasFor(annotation = Queryable.class, value = "queryAttributes")
String rangeOptions() default ""; String rangeOptions() default "";
} }

41
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

@ -51,6 +51,7 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.Mong
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.Queryable;
import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
@ -110,12 +111,22 @@ class RangeEncryptionTests {
.rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200))) .rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200)))
.keyId(keyHolder.getEncryptionKey("encryptedInt")).queryType("range"); .keyId(keyHolder.getEncryptionKey("encryptedInt")).queryType("range");
EncryptOptions stringEncOptions = new EncryptOptions("Range") /*
.contentionFactor(8L).rangeOptions(new RangeOptions()) @Encrypted(algorithm = "Indexed")
.keyId(keyHolder.getEncryptionKey("name")).queryType("equality"); @Queryable(queryType = "equality", contentionFactor = 0)
*/
EncryptOptions equalityEncOptions = new EncryptOptions("Indexed")
.contentionFactor(0L)
.keyId(keyHolder.getEncryptionKey("age"));;
EncryptOptions equalityEncOptionsString = new EncryptOptions("Indexed")
.contentionFactor(0L)
.keyId(keyHolder.getEncryptionKey("name"));;
Document source = new Document("_id", "id-1"); Document source = new Document("_id", "id-1");
source.put("name", clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), stringEncOptions));
source.put("name", clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), equalityEncOptionsString));
source.put("age", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), equalityEncOptions));
source.put("encryptedInt", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), encryptOptions)); source.put("encryptedInt", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), encryptOptions));
source.put("_class", Person.class.getName()); source.put("_class", Person.class.getName());
@ -143,6 +154,16 @@ class RangeEncryptionTests {
assertThat(loaded).isEqualTo(source); assertThat(loaded).isEqualTo(source);
} }
@Test
void eqQueryWorksOnEncryptedField() {
Person source = createPerson();
template.insert(source);
Person loaded = template.query(Person.class).matching(where("age").is(source.age)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test @Test
void canRangeMatchRangeEncryptedField() { void canRangeMatchRangeEncryptedField() {
@ -200,7 +221,8 @@ class RangeEncryptionTests {
private Person createPerson() { private Person createPerson() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
source.name = "it'se me mario!"; source.name = "it's a me mario!";
source.age = 42;
source.encryptedInt = 101; source.encryptedInt = 101;
source.encryptedLong = 1001L; source.encryptedLong = 1001L;
return source; return source;
@ -358,10 +380,17 @@ class RangeEncryptionTests {
static class Person { static class Person {
String id; String id;
@ValueConverter(MongoEncryptionConverter.class) @ValueConverter(MongoEncryptionConverter.class)
@Encrypted(algorithm = "Range", queryType = "equality") @Encrypted(algorithm = "Indexed")
@Queryable(queryType = "equality", contentionFactor = 0)
String name; String name;
@ValueConverter(MongoEncryptionConverter.class)
@Encrypted(algorithm = "Indexed")
@Queryable(queryType = "equality", contentionFactor = 0)
Integer age;
@ValueConverter(MongoEncryptionConverter.class) @ValueConverter(MongoEncryptionConverter.class)
@RangeEncrypted(contentionFactor = 0L, @RangeEncrypted(contentionFactor = 0L,
rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") // rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") //

Loading…
Cancel
Save