From 9fa03a75a2ed556069009efa2efec6cf9e40c392 Mon Sep 17 00:00:00 2001 From: Emilien Bevierre Date: Thu, 19 Feb 2026 12:46:33 +0000 Subject: [PATCH 1/7] Remove unnecessary intermediate String deserialization of document in favour of the raw bytes Signed-off-by: Emilien Bevierre --- .gitignore | 3 + .../core/AbstractTemplateSupport.java | 58 +++++++++++++------ .../core/CouchbaseTemplateSupport.java | 7 +++ .../core/NonReactiveSupportWrapper.java | 8 +++ .../ReactiveCouchbaseTemplateSupport.java | 9 +++ .../ReactiveFindByIdOperationSupport.java | 11 ++-- ...eFindFromReplicasByIdOperationSupport.java | 7 +-- .../ReactiveRangeScanOperationSupport.java | 10 +++- .../core/ReactiveTemplateSupport.java | 4 ++ .../data/couchbase/core/TemplateSupport.java | 4 ++ .../JacksonTranslationService.java | 32 +++++++++- .../translation/TranslationService.java | 15 +++++ .../JacksonTranslationServiceTests.java | 36 ++++++++++++ 13 files changed, 170 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 78662375b..5bf59fb65 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ target/ .project .settings +# Local test configuration (credentials, local server settings) +src/test/resources/integration.local.properties + *.iml .idea diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 23936f53d..eb07120e0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -46,6 +46,7 @@ /** * Base shared by Reactive and non-Reactive TemplateSupport * + * @author Emilien Bevierre * @author Michael Reiche */ @Stability.Internal @@ -70,7 +71,37 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv public T decodeEntityBase(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); + + if (persistentEntity == null) { + final CouchbaseDocument converted = new CouchbaseDocument(id); + Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) + .getContent().entrySet(); + return (T) set.iterator().next().getValue(); + } + + final CouchbaseDocument converted = prepareConvertedDocument(id, cas, persistentEntity); + T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + return finalizeEntity(readEntity, id, cas, expiryTime, scope, collection, txResultHolder, holder); + } + + public T decodeEntityBase(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, + String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); + + if (persistentEntity == null) { + final CouchbaseDocument converted = new CouchbaseDocument(id); + Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) + .getContent().entrySet(); + return (T) set.iterator().next().getValue(); + } + + final CouchbaseDocument converted = prepareConvertedDocument(id, cas, persistentEntity); + T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + return finalizeEntity(readEntity, id, cas, expiryTime, scope, collection, txResultHolder, holder); + } + private CouchbaseDocument prepareConvertedDocument(Object id, Long cas, CouchbasePersistentEntity persistentEntity) { // this is the entity class defined for the repository. It may not be the class of the document that was read // we will reset it after reading the document // @@ -82,19 +113,6 @@ public T decodeEntityBase(Object id, String source, Long cas, Instant expiry // but that is a lot of work to do every time just for this very rare and avoidable case. // TypeInformation typeToUse = typeMapper.readType(source, type); - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { // method could return a Long, Boolean, String etc. - // QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left - // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a - // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. - // if this is a Collection or array, only the first element will be returned. - final CouchbaseDocument converted = new CouchbaseDocument(id); - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - if (id == null) { throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_ID); @@ -117,14 +135,18 @@ public T decodeEntityBase(Object id, String source, Long cas, Instant expiry } } - // if the constructor has an argument that is long version, then construction will fail if the 'version' - // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. - // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + return converted; + } + + private T finalizeEntity(T readEntity, Object id, Long cas, Instant expiryTime, String scope, String collection, + Object txResultHolder, CouchbaseResourceHolder holder) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - persistentEntity = couldBePersistentEntity(readEntity.getClass()); + CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(readEntity.getClass()); + // if the constructor has an argument that is long version, then construction will fail if the 'version' + // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. + // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 7464abfb0..eaa917222 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -35,6 +35,7 @@ /** * Internal encode/decode support for CouchbaseTemplate. * + * @author Emilien Bevierre * @author Michael Nitschinger * @author Michael Reiche * @author Jorge Rodriguez Martin @@ -69,6 +70,12 @@ public T decodeEntity(Object id, String source, Long cas, Instant expiryTime return decodeEntityBase(id, source, cas, expiryTime, entityClass, scope, collection, txHolder, holder); } + @Override + public T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, + String scope, String collection, Object txHolder, CouchbaseResourceHolder holder) { + return decodeEntityBase(id, source, cas, expiryTime, entityClass, scope, collection, txHolder, holder); + } + @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, Object txResultHolder, CouchbaseResourceHolder holder) { diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index f104c8839..ab81437e2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -26,6 +26,7 @@ /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. * + * @author Emilien Bevierre * @author Carlos Espinaco * @author Michael Reiche * @since 4.2 @@ -50,6 +51,13 @@ public Mono decodeEntity(Object id, String source, Long cas, Instant expi txResultHolder, holder)); } + @Override + public Mono decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, + String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, expiryTime, entityClass, scope, collection, + txResultHolder, holder)); + } + @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, Object txResultHolder, CouchbaseResourceHolder holder) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 69a06c217..5dbfb0b84 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -38,6 +38,7 @@ /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. * + * @author Emilien Bevierre * @author Carlos Espinaco * @author Michael Reiche * @since 4.2 @@ -78,6 +79,14 @@ public Mono decodeEntity(Object id, String source, Long cas, Instant expi txResultHolder, holder)); } + @Override + public Mono decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, + String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + return Mono + .fromSupplier(() -> decodeEntityBase(id, source, cas, expiryTime, entityClass, scope, collection, + txResultHolder, holder)); + } + @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, Object txResultHolder, CouchbaseResourceHolder holder) { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index b8fbff560..dff7a788b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -22,7 +22,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.nio.charset.StandardCharsets; + import java.time.Duration; import java.util.Arrays; import java.util.Collection; @@ -46,6 +46,7 @@ /** * {@link ReactiveFindByIdOperation} implementations for Couchbase. * + * @author Emilien Bevierre * @author Michael Reiche * @author Tigran Babloyan */ @@ -111,26 +112,26 @@ public Mono one(final Object id) { return rc .getAndTouch(id.toString(), expiryToUse, buildOptions((GetAndTouchOptions) pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), + .flatMap(result -> support.decodeEntity(id, result.contentAsBytes(), result.cas(), result.expiryTime().orElse(null), domainType, pArgs.getScope(), pArgs.getCollection(), null, null)); } else if (pArgs.getOptions() instanceof GetAndLockOptions options) { return rc .getAndLock(id.toString(), Optional.of(lockDuration).orElse(Duration.ZERO), buildOptions((GetAndLockOptions) pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), + .flatMap(result -> support.decodeEntity(id, result.contentAsBytes(), result.cas(), result.expiryTime().orElse(null), domainType, pArgs.getScope(), pArgs.getCollection(), null, null)); } else { return rc.get(id.toString(), buildOptions((GetOptions) pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), + .flatMap(result -> support.decodeEntity(id, result.contentAsBytes(), result.cas(), result.expiryTime().orElse(null), domainType, pArgs.getScope(), pArgs.getCollection(), null, null)); } } else { rejectInvalidTransactionalOptions(); return ctxOpt.get().getCore().get(makeCollectionIdentifier(rc.async()), id.toString()) - .flatMap(result -> support.decodeEntity(id, new String(result.contentAsBytes(), StandardCharsets.UTF_8), + .flatMap(result -> support.decodeEntity(id, result.contentAsBytes(), result.cas(), null, domainType, pArgs.getScope(), pArgs.getCollection(), null, null)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 150a2d513..a21b1c001 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -27,12 +27,12 @@ import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; -import com.couchbase.client.java.codec.RawJsonTranscoder; import com.couchbase.client.java.kv.GetAnyReplicaOptions; /** * {@link ReactiveFindFromReplicasByIdOperation} implementations for Couchbase. * + * @author Emilien Bevierre * @author Michael Reiche */ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFindFromReplicasByIdOperation { @@ -75,9 +75,6 @@ static class ReactiveFindFromReplicasByIdSupport implements ReactiveFindFromR @Override public Mono any(final String id) { GetAnyReplicaOptions garOptions = options != null ? options : getAnyReplicaOptions(); - if (garOptions.build().transcoder() == null) { - garOptions.transcoder(RawJsonTranscoder.INSTANCE); - } PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType); if (LOG.isDebugEnabled()) { LOG.debug("getAnyReplica key={} {}", id, pArgs); @@ -85,7 +82,7 @@ public Mono any(final String id) { return TransactionalSupport.verifyNotInTransaction("findFromReplicasById").then(Mono.just(id)) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) - .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), + .flatMap(result -> support.decodeEntity(id, result.contentAsBytes(), result.cas(), result.expiryTime().orElse(null), returnType, pArgs.getScope(), pArgs.getCollection(), null, null)) .onErrorMap(throwable -> { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java index 8352f4404..8e01d2a5c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java @@ -17,8 +17,6 @@ import reactor.core.publisher.Flux; -import java.nio.charset.StandardCharsets; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.query.OptionsBuilder; @@ -30,6 +28,12 @@ import com.couchbase.client.java.kv.ScanTerm; import com.couchbase.client.java.kv.ScanType; +/** + * {@link ReactiveRangeScanOperationSupport} implementations for Couchbase. + * + * @author Emilien Bevierre + * @author Michael Reiche + */ public class ReactiveRangeScanOperationSupport implements ReactiveRangeScanOperation { private final ReactiveCouchbaseTemplate template; @@ -162,7 +166,7 @@ Flux rangeScan(String lower, String upper, boolean isSamplingScan, Long limit Flux reactiveEntities = TransactionalSupport.verifyNotInTransaction("rangeScan") .thenMany(rc.scan(scanType, buildScanOptions(pArgs.getOptions(), false)) .flatMap(result -> support.decodeEntity(result.id(), - new String(result.contentAsBytes(), StandardCharsets.UTF_8), result.cas(), + result.contentAsBytes(), result.cas(), result.expiryTime().orElse(null), domainType, pArgs.getScope(), pArgs.getCollection(), null, null))); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 05d725969..0ad9883cc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -26,6 +26,7 @@ /** * ReactiveTemplateSupport * + * @author Emilien Bevierre * @author Michael Reiche */ public interface ReactiveTemplateSupport { @@ -35,6 +36,9 @@ public interface ReactiveTemplateSupport { Mono decodeEntity(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); + Mono decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, + String collection, Object txResultHolder, CouchbaseResourceHolder holder); + Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, Object txResultHolder, CouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 1e002192a..d4b264c05 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -22,6 +22,7 @@ import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** + * @author Emilien Bevierre * @author Michael Reiche */ public interface TemplateSupport { @@ -31,6 +32,9 @@ public interface TemplateSupport { T decodeEntity(Object id, String source, Long cas, Instant expiryTIme, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); + T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, + String collection, Object txResultHolder, CouchbaseResourceHolder holder); + T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, Object txResultHolder, CouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java index 7a31ef6da..a22061ba7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -38,6 +38,7 @@ /** * A Jackson JSON Translator that implements the {@link TranslationService} contract. * + * @author Emilien Bevierre * @author Michael Nitschinger * @author Simon Baslé * @author Anastasiia Smirnova @@ -128,7 +129,33 @@ private boolean isEnumOrClass(final Class clazz) { @Override public final CouchbaseStorable decode(final String source, final CouchbaseStorable target) { try { - JsonParser parser = factory.createParser((String) source); + JsonParser parser = factory.createParser(source); + return decodeWithParser(parser, target); + } catch (IOException ex) { + throw new RuntimeException("Could not decode JSON", ex); + } + } + + /** + * Decode a JSON byte array into the {@link CouchbaseStorable} structure. + * This avoids the intermediate String allocation by parsing directly from bytes. + * + * @param source the source formatted document as bytes (UTF-8 encoded). + * @param target the target of the populated data. + * @return the decoded structure. + */ + @Override + public final CouchbaseStorable decode(final byte[] source, final CouchbaseStorable target) { + try { + JsonParser parser = factory.createParser(source); + return decodeWithParser(parser, target); + } catch (IOException ex) { + throw new RuntimeException("Could not decode JSON", ex); + } + } + + private CouchbaseStorable decodeWithParser(final JsonParser parser, final CouchbaseStorable target) throws IOException { + try { while (parser.nextToken() != null) { JsonToken currentToken = parser.getCurrentToken(); @@ -140,9 +167,8 @@ public final CouchbaseStorable decode(final String source, final CouchbaseStorab throw new MappingException("JSON to decode needs to start as array or object!"); } } + } finally { parser.close(); - } catch (IOException ex) { - throw new RuntimeException("Could not decode JSON", ex); } return target; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java index 5be23ffc0..1a4061947 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -16,12 +16,15 @@ package org.springframework.data.couchbase.core.convert.translation; +import java.nio.charset.StandardCharsets; + import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; /** * Defines a translation service to encode/decode responses into the {@link CouchbaseStorable} format. * + * @author Emilien Bevierre * @author Michael Nitschinger */ public interface TranslationService { @@ -43,6 +46,18 @@ public interface TranslationService { */ CouchbaseStorable decode(String source, CouchbaseStorable target); + /** + * Decodes the target format from a byte array into a {@link CouchbaseDocument}. + * This avoids the intermediate String allocation when the source is already available as bytes. + * + * @param source the source formatted document as bytes (UTF-8 encoded). + * @param target the target of the populated data. + * @return a properly populated document to work with. + */ + default CouchbaseStorable decode(byte[] source, CouchbaseStorable target) { + return decode(new String(source, StandardCharsets.UTF_8), target); + } + /** * Decodes an ad-hoc JSON object into a corresponding "case" class. * diff --git a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java index 5e136eea9..e0d36d376 100644 --- a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java @@ -18,6 +18,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.nio.charset.StandardCharsets; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; @@ -61,6 +63,40 @@ void shouldDecodeAdHocFragment() { assertEquals("french", f.language); } + @Test + void shouldDecodeFromBytes() { + String source = "{\"language\":\"english\",\"count\":42}"; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + + CouchbaseDocument targetFromString = new CouchbaseDocument(); + service.decode(source, targetFromString); + + CouchbaseDocument targetFromBytes = new CouchbaseDocument(); + service.decode(bytes, targetFromBytes); + + assertEquals(targetFromString.get("language"), targetFromBytes.get("language")); + assertEquals(targetFromString.get("count"), targetFromBytes.get("count")); + assertEquals("english", targetFromBytes.get("language")); + assertEquals(42, targetFromBytes.get("count")); + } + + @Test + void shouldDecodeNonASCIIFromBytes() { + String source = "{\"language\":\"русский\",\"greeting\":\"Привет мир\"}"; + byte[] bytes = source.getBytes(StandardCharsets.UTF_8); + + CouchbaseDocument targetFromString = new CouchbaseDocument(); + service.decode(source, targetFromString); + + CouchbaseDocument targetFromBytes = new CouchbaseDocument(); + service.decode(bytes, targetFromBytes); + + assertEquals(targetFromString.get("language"), targetFromBytes.get("language")); + assertEquals(targetFromString.get("greeting"), targetFromBytes.get("greeting")); + assertEquals("русский", targetFromBytes.get("language")); + assertEquals("Привет мир", targetFromBytes.get("greeting")); + } + static class LanguageFragment { public String language; } From 8885c005567bb27620938c5a277ed5c2776a3d78 Mon Sep 17 00:00:00 2001 From: Emilien Bevierre Date: Thu, 19 Feb 2026 12:47:53 +0000 Subject: [PATCH 2/7] Fix intermittent test failure where deleteById(id).block() is called immediately after subscribe(), before the document has potentially been saved. Signed-off-by: Emilien Bevierre --- .gitignore | 1 - .../convert/translation/JacksonTranslationServiceTests.java | 1 + ...iveCouchbaseRepositoryQueryCollectionIntegrationTests.java | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5bf59fb65..e6505b14f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target/ .project .settings -# Local test configuration (credentials, local server settings) src/test/resources/integration.local.properties *.iml diff --git a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java index e0d36d376..9d58f860e 100644 --- a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java @@ -27,6 +27,7 @@ /** * Verifies the functionality of a {@link JacksonTranslationService}. * + * @author Emilien Bevierre * @author Michael Nitschinger */ public class JacksonTranslationServiceTests { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java index 03607c6ed..1fd17c7cb 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -132,10 +132,10 @@ void testThreadLocal() throws InterruptedException { String id = UUID.randomUUID().toString(); Airport airport = new Airport(id, "testThreadLocal", "icao"); - Disposable s = reactiveAirportMustScopeRepository.withScope(scopeName).findById(airport.getId()).doOnNext(u -> { + reactiveAirportMustScopeRepository.withScope(scopeName).findById(airport.getId()).doOnNext(u -> { throw new RuntimeException("User already Exists! " + u); }).then(reactiveAirportMustScopeRepository.withScope(scopeName).save(airport)) - .subscribe(u -> LOGGER.info("User Persisted Successfully! {}", u)); + .block(); reactiveAirportMustScopeRepository.withScope(scopeName).deleteById(id).block(); } From 33354ce5b3265ccf79fa8e2b56068ccc4759836f Mon Sep 17 00:00:00 2001 From: Emilien Bevierre <44171454+emilienbev@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:55:42 +0000 Subject: [PATCH 3/7] Fix typo expiryTIme Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Emilien Bevierre <44171454+emilienbev@users.noreply.github.com> --- .../springframework/data/couchbase/core/TemplateSupport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index d4b264c05..f7f2d1385 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -29,7 +29,7 @@ public interface TemplateSupport { CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(Object id, String source, Long cas, Instant expiryTIme, Class entityClass, String scope, + T decodeEntity(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, From 349c095929ce27bcc7c8d22c2ea3927338401d22 Mon Sep 17 00:00:00 2001 From: Emilien Bevierre Date: Thu, 19 Feb 2026 17:09:48 +0000 Subject: [PATCH 4/7] Provide default fallback implementation of decodeEntity(Object id, byte[] ...) (back to String). Ensure that if both parsing and close throw, the close exception is suppressed rather than masking the original error. Signed-off-by: Emilien Bevierre --- .../core/ReactiveTemplateSupport.java | 8 ++++-- .../data/couchbase/core/TemplateSupport.java | 8 ++++-- .../JacksonTranslationService.java | 28 ++++++++----------- .../translation/TranslationService.java | 3 +- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 0ad9883cc..a1bbf75d7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -17,6 +17,7 @@ import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; import java.time.Instant; import org.springframework.data.couchbase.core.convert.translation.TranslationService; @@ -36,8 +37,11 @@ public interface ReactiveTemplateSupport { Mono decodeEntity(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); - Mono decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, - String collection, Object txResultHolder, CouchbaseResourceHolder holder); + default Mono decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, + String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + return decodeEntity(id, new String(source, StandardCharsets.UTF_8), cas, expiryTime, entityClass, scope, + collection, txResultHolder, holder); + } Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, Object txResultHolder, CouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index f7f2d1385..3b69f51a6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.couchbase.core; +import java.nio.charset.StandardCharsets; import java.time.Instant; import org.springframework.data.couchbase.core.convert.translation.TranslationService; @@ -32,8 +33,11 @@ public interface TemplateSupport { T decodeEntity(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); - T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, - String collection, Object txResultHolder, CouchbaseResourceHolder holder); + default T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, + String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + return decodeEntity(id, new String(source, StandardCharsets.UTF_8), cas, expiryTime, entityClass, scope, + collection, txResultHolder, holder); + } T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, Object txResultHolder, CouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java index a22061ba7..2caf652d9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -128,8 +128,7 @@ private boolean isEnumOrClass(final Class clazz) { */ @Override public final CouchbaseStorable decode(final String source, final CouchbaseStorable target) { - try { - JsonParser parser = factory.createParser(source); + try (JsonParser parser = factory.createParser(source)) { return decodeWithParser(parser, target); } catch (IOException ex) { throw new RuntimeException("Could not decode JSON", ex); @@ -146,8 +145,7 @@ public final CouchbaseStorable decode(final String source, final CouchbaseStorab */ @Override public final CouchbaseStorable decode(final byte[] source, final CouchbaseStorable target) { - try { - JsonParser parser = factory.createParser(source); + try (JsonParser parser = factory.createParser(source)) { return decodeWithParser(parser, target); } catch (IOException ex) { throw new RuntimeException("Could not decode JSON", ex); @@ -155,20 +153,16 @@ public final CouchbaseStorable decode(final byte[] source, final CouchbaseStorab } private CouchbaseStorable decodeWithParser(final JsonParser parser, final CouchbaseStorable target) throws IOException { - try { - while (parser.nextToken() != null) { - JsonToken currentToken = parser.getCurrentToken(); - - if (currentToken == JsonToken.START_OBJECT) { - return decodeObject(parser, (CouchbaseDocument) target); - } else if (currentToken == JsonToken.START_ARRAY) { - return decodeArray(parser, new CouchbaseList()); - } else { - throw new MappingException("JSON to decode needs to start as array or object!"); - } + while (parser.nextToken() != null) { + JsonToken currentToken = parser.getCurrentToken(); + + if (currentToken == JsonToken.START_OBJECT) { + return decodeObject(parser, (CouchbaseDocument) target); + } else if (currentToken == JsonToken.START_ARRAY) { + return decodeArray(parser, new CouchbaseList()); + } else { + throw new MappingException("JSON to decode needs to start as array or object!"); } - } finally { - parser.close(); } return target; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java index 1a4061947..58470b4dc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -48,7 +48,8 @@ public interface TranslationService { /** * Decodes the target format from a byte array into a {@link CouchbaseDocument}. - * This avoids the intermediate String allocation when the source is already available as bytes. + * The default implementation converts the bytes to a String via UTF-8 before decoding. + * Implementations may override this to parse bytes directly and avoid the intermediate String allocation. * * @param source the source formatted document as bytes (UTF-8 encoded). * @param target the target of the populated data. From a7d84f10ca8e8cb7b57ad45f3fc7040fefe8a5c4 Mon Sep 17 00:00:00 2001 From: Emilien Bevierre Date: Mon, 23 Feb 2026 12:39:38 +0000 Subject: [PATCH 5/7] Add ByteUtils utility static class for byte array/string conversions Re-order author tags correctly Remove excessive final modifiers Signed-off-by: Emilien Bevierre --- .../cache/CouchbaseCacheConfiguration.java | 38 +++++++++----- .../core/AbstractTemplateSupport.java | 2 +- .../core/CouchbaseTemplateSupport.java | 2 +- .../core/NonReactiveSupportWrapper.java | 2 +- .../ReactiveCouchbaseTemplateSupport.java | 2 +- .../ReactiveFindByIdOperationSupport.java | 2 +- ...eFindFromReplicasByIdOperationSupport.java | 2 +- .../ReactiveInsertByIdOperationSupport.java | 2 +- .../ReactiveRangeScanOperationSupport.java | 2 +- .../ReactiveRemoveByIdOperationSupport.java | 2 +- .../ReactiveReplaceByIdOperationSupport.java | 2 +- .../core/ReactiveTemplateSupport.java | 6 +-- .../data/couchbase/core/TemplateSupport.java | 9 ++-- .../core/convert/CryptoConverter.java | 9 ++-- .../core/convert/OtherConverters.java | 51 +++++++++++------- .../JacksonTranslationService.java | 34 ++++++------ .../translation/TranslationService.java | 23 ++++---- .../data/couchbase/core/util/ByteUtils.java | 52 +++++++++++++++++++ .../JacksonTranslationServiceTests.java | 2 +- 19 files changed, 165 insertions(+), 79 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/util/ByteUtils.java diff --git a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java index 03913ce1f..0f9d1e2ed 100644 --- a/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheConfiguration.java @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.cache; -import java.nio.charset.StandardCharsets; +import org.springframework.data.couchbase.core.util.ByteUtils; import java.time.Duration; import org.springframework.cache.Cache; @@ -60,7 +60,8 @@ public static CouchbaseCacheConfiguration defaultCacheConfig() { } /** - * Registers default cache key converters. The following converters get registered: + * Registers default cache key converters. The following converters get + * registered: *
    *
  • {@link String} to byte using UTF-8 encoding.
  • *
  • {@link SimpleKey} to {@link String}
  • @@ -70,12 +71,13 @@ public static CouchbaseCacheConfiguration defaultCacheConfig() { */ public static void registerDefaultConverters(final ConverterRegistry registry) { Assert.notNull(registry, "ConverterRegistry must not be null!"); - registry.addConverter(String.class, byte[].class, source -> source.getBytes(StandardCharsets.UTF_8)); + registry.addConverter(String.class, byte[].class, ByteUtils::getBytes); registry.addConverter(SimpleKey.class, String.class, SimpleKey::toString); } /** - * Set the expiry to apply for cache entries. Use {@link Duration#ZERO} to declare an eternal cache. + * Set the expiry to apply for cache entries. Use {@link Duration#ZERO} to + * declare an eternal cache. * * @param expiry must not be {@literal null}. * @return new {@link CouchbaseCacheConfiguration}. @@ -112,9 +114,13 @@ public CouchbaseCacheConfiguration valueTranscoder(final Transcoder valueTransco /** * Disable caching {@literal null} values.
    - * NOTE any {@link org.springframework.cache.Cache#put(Object, Object)} operation involving - * {@literal null} value will error. Nothing will be written to Couchbase, nothing will be removed. An already - * existing key will still be there afterwards with the very same value as before. + * NOTE any + * {@link org.springframework.cache.Cache#put(Object, Object)} operation + * involving + * {@literal null} value will error. Nothing will be written to Couchbase, + * nothing will be removed. An already + * existing key will still be there afterwards with the very same value as + * before. * * @return new {@link CouchbaseCacheConfiguration}. */ @@ -124,8 +130,10 @@ public CouchbaseCacheConfiguration disableCachingNullValues() { } /** - * Prefix the {@link CouchbaseCache#getName() cache name} with the given value.
    - * The generated cache key will be: {@code prefix + cache name + "::" + cache entry key}. + * Prefix the {@link CouchbaseCache#getName() cache name} with the given value. + *
    + * The generated cache key will be: + * {@code prefix + cache name + "::" + cache entry key}. * * @param prefix the prefix to prepend to the cache name. * @return this. @@ -137,7 +145,8 @@ public CouchbaseCacheConfiguration prefixCacheNameWith(final String prefix) { } /** - * Use the given {@link CacheKeyPrefix} to compute the prefix for the actual Couchbase {@literal key} given the + * Use the given {@link CacheKeyPrefix} to compute the prefix for the actual + * Couchbase {@literal key} given the * {@literal cache name} as function input. * * @param cacheKeyPrefix must not be {@literal null}. @@ -165,14 +174,16 @@ public boolean getAllowCacheNullValues() { } /** - * @return The {@link ConversionService} used for cache key to {@link String} conversion. Never {@literal null}. + * @return The {@link ConversionService} used for cache key to {@link String} + * conversion. Never {@literal null}. */ public ConversionService getConversionService() { return conversionService; } /** - * @return {@literal true} if cache keys need to be prefixed with the {@link #getKeyPrefixFor(String)} if present or + * @return {@literal true} if cache keys need to be prefixed with the + * {@link #getKeyPrefixFor(String)} if present or * the default which resolves to {@link Cache#getName()}. */ public boolean usePrefix() { @@ -197,7 +208,8 @@ public Transcoder getValueTranscoder() { } /** - * The name of the collection to use for this cache - if empty uses the default collection. + * The name of the collection to use for this cache - if empty uses the default + * collection. */ public String getCollectionName() { return collectionName; diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index eb07120e0..12866501a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -46,8 +46,8 @@ /** * Base shared by Reactive and non-Reactive TemplateSupport * - * @author Emilien Bevierre * @author Michael Reiche + * @author Emilien Bevierre */ @Stability.Internal public abstract class AbstractTemplateSupport { diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index eaa917222..d6971083c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -35,11 +35,11 @@ /** * Internal encode/decode support for CouchbaseTemplate. * - * @author Emilien Bevierre * @author Michael Nitschinger * @author Michael Reiche * @author Jorge Rodriguez Martin * @author Carlos Espinaco + * @author Emilien Bevierre * @since 3.0 */ class CouchbaseTemplateSupport extends AbstractTemplateSupport implements ApplicationContextAware, TemplateSupport { diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index ab81437e2..8ab1687c4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -26,9 +26,9 @@ /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. * - * @author Emilien Bevierre * @author Carlos Espinaco * @author Michael Reiche + * @author Emilien Bevierre * @since 4.2 */ public class NonReactiveSupportWrapper implements ReactiveTemplateSupport { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 5dbfb0b84..4acc91075 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -38,9 +38,9 @@ /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. * - * @author Emilien Bevierre * @author Carlos Espinaco * @author Michael Reiche + * @author Emilien Bevierre * @since 4.2 */ class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index dff7a788b..5b00a95fc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -46,9 +46,9 @@ /** * {@link ReactiveFindByIdOperation} implementations for Couchbase. * - * @author Emilien Bevierre * @author Michael Reiche * @author Tigran Babloyan + * @author Emilien Bevierre */ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index a21b1c001..364960616 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -32,8 +32,8 @@ /** * {@link ReactiveFindFromReplicasByIdOperation} implementations for Couchbase. * - * @author Emilien Bevierre * @author Michael Reiche + * @author Emilien Bevierre */ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFindFromReplicasByIdOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 4d8f78e21..4c5841bcd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -43,9 +43,9 @@ /** * {@link ReactiveInsertByIdOperation} implementations for Couchbase. * - * @author Emilien Bevierre * @author Michael Reiche * @author Tigran Babloyan + * @author Emilien Bevierre */ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java index 8e01d2a5c..9234dd081 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRangeScanOperationSupport.java @@ -31,8 +31,8 @@ /** * {@link ReactiveRangeScanOperationSupport} implementations for Couchbase. * - * @author Emilien Bevierre * @author Michael Reiche + * @author Emilien Bevierre */ public class ReactiveRangeScanOperationSupport implements ReactiveRangeScanOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 236ad2945..8f60a6601 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -44,9 +44,9 @@ /** * {@link ReactiveRemoveByIdOperation} implementations for Couchbase. * - * @author Emilien Bevierre * @author Michael Reiche * @author Tigran Babloyan + * @author Emilien Bevierre */ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index ec6c57271..5c5be3de4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -46,9 +46,9 @@ /** * {@link ReactiveReplaceByIdOperation} implementations for Couchbase. * - * @author Emilien Bevierre * @author Michael Reiche * @author Tigran Babloyan + * @author Emilien Bevierre */ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index a1bbf75d7..f5955b17c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -17,7 +17,7 @@ import reactor.core.publisher.Mono; -import java.nio.charset.StandardCharsets; +import org.springframework.data.couchbase.core.util.ByteUtils; import java.time.Instant; import org.springframework.data.couchbase.core.convert.translation.TranslationService; @@ -27,8 +27,8 @@ /** * ReactiveTemplateSupport * - * @author Emilien Bevierre * @author Michael Reiche + * @author Emilien Bevierre */ public interface ReactiveTemplateSupport { @@ -39,7 +39,7 @@ Mono decodeEntity(Object id, String source, Long cas, Instant expiryTime, default Mono decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { - return decodeEntity(id, new String(source, StandardCharsets.UTF_8), cas, expiryTime, entityClass, scope, + return decodeEntity(id, ByteUtils.getString(source), cas, expiryTime, entityClass, scope, collection, txResultHolder, holder); } diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 3b69f51a6..449168c14 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import java.nio.charset.StandardCharsets; +import org.springframework.data.couchbase.core.util.ByteUtils; import java.time.Instant; import org.springframework.data.couchbase.core.convert.translation.TranslationService; @@ -23,8 +23,8 @@ import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; /** - * @author Emilien Bevierre * @author Michael Reiche + * @author Emilien Bevierre */ public interface TemplateSupport { @@ -33,9 +33,10 @@ public interface TemplateSupport { T decodeEntity(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); - default T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, + default T decodeEntity(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, + String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { - return decodeEntity(id, new String(source, StandardCharsets.UTF_8), cas, expiryTime, entityClass, scope, + return decodeEntity(id, ByteUtils.getString(source), cas, expiryTime, entityClass, scope, collection, txResultHolder, holder); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java index 9f642ecc6..eaafd1b32 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core.convert; -import java.nio.charset.StandardCharsets; +import org.springframework.data.couchbase.core.util.ByteUtils; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -97,7 +97,8 @@ private Object coerceToValueRead(byte[] decrypted, CouchbaseConversionContext co String jsonString = "{\"" + property.getFieldName() + "\":" + decryptedString + "}"; try { CouchbaseDocument decryptedDoc = new CouchbaseDocument().setContent(JsonObject.fromJson(jsonString)); - return context.getConverter().getPotentiallyConvertedSimpleRead(decryptedDoc.get(property.getFieldName()), + return context.getConverter().getPotentiallyConvertedSimpleRead( + decryptedDoc.get(property.getFieldName()), property); } catch (InvalidArgumentException | ConverterNotFoundException | ConversionFailedException e) { throw new RuntimeException(decryptedString, e); @@ -114,7 +115,7 @@ private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, Converti Class targetType = cnvs.getCustomWriteTarget(property.getType()).orElse(null); Object value = context.getConverter().getPotentiallyConvertedSimpleWrite(property, accessor, false); if (value == null) { // null - plainText = "null".getBytes(StandardCharsets.UTF_8); + plainText = ByteUtils.getBytes("null"); } else if (value.getClass().isArray()) { // array JsonArray ja; if (value.getClass().getComponentType().isPrimitive()) { @@ -133,7 +134,7 @@ private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, Converti throw new RuntimeException(e); } } - plainText = plainString.getBytes(StandardCharsets.UTF_8); + plainText = ByteUtils.getBytes(plainString); } else { // an entity CouchbaseDocument doc = new CouchbaseDocument(); context.getConverter().writeInternalRoot(value, doc, property.getTypeInformation(), false, property, false); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java b/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java index a6bdba578..aa4a6c10e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java @@ -21,7 +21,7 @@ import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; -import java.nio.charset.StandardCharsets; +import org.springframework.data.couchbase.core.util.ByteUtils; import java.time.YearMonth; import java.util.ArrayList; import java.util.Base64; @@ -54,7 +54,8 @@ */ public final class OtherConverters { - private OtherConverters() {} + private OtherConverters() { + } /** * Returns all converters by this class that can be registered. @@ -82,9 +83,11 @@ private OtherConverters() {} converters.add(CouchbaseListToJsonArray.INSTANCE); converters.add(YearMonthToStringConverter.INSTANCE); converters.add(StringToYearMonthConverter.INSTANCE); - // EnumToObject, IntegerToEnumConverterFactory and StringToEnumConverterFactory are + // EnumToObject, IntegerToEnumConverterFactory and StringToEnumConverterFactory + // are // registered in - // {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( + // {@link + // org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( // CryptoManager)} as they require an ObjectMapper return converters; } @@ -109,7 +112,8 @@ public UUID convert(String source) { } } - // to support reading BigIntegers that were written as Strings (now discontinued) + // to support reading BigIntegers that were written as Strings (now + // discontinued) @ReadingConverter public enum StringToBigInteger implements Converter { INSTANCE; @@ -120,7 +124,8 @@ public BigInteger convert(String source) { } } - // to support reading BigDecimals that were written as Strings (now discontinued) + // to support reading BigDecimals that were written as Strings (now + // discontinued) @ReadingConverter public enum StringToBigDecimal implements Converter { INSTANCE; @@ -147,7 +152,7 @@ public enum StringToByteArray implements Converter { @Override public byte[] convert(String source) { - return source == null ? null : Base64.getDecoder().decode(source.getBytes(StandardCharsets.UTF_8)); + return source == null ? null : Base64.getDecoder().decode(ByteUtils.getBytes(source)); } } @@ -198,7 +203,8 @@ public Class convert(String source) { /** * Writing converter for Enums. This is registered in * {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( CryptoManager, ObjectMapper)}. - * The corresponding reading converters are in {@link IntegerToEnumConverterFactory} and + * The corresponding reading converters are in + * {@link IntegerToEnumConverterFactory} and * {@link StringToEnumConverterFactory} */ @@ -232,24 +238,29 @@ public Object convert(Enum source) { @WritingConverter public enum JsonNodeToMap implements Converter { INSTANCE; - static ObjectMapper mapper= new ObjectMapper().registerModule(new JsonValueModule()); + + static ObjectMapper mapper = new ObjectMapper().registerModule(new JsonValueModule()); + @Override public CouchbaseDocument convert(JsonNode source) { - if( source == null ){ + if (source == null) { return null; } - return new CouchbaseDocument().setContent((Map)mapper.convertValue(source, new TypeReference>(){})); + return new CouchbaseDocument() + .setContent((Map) mapper.convertValue(source, new TypeReference>() { + })); } } @ReadingConverter public enum MapToJsonNode implements Converter { INSTANCE; - static ObjectMapper mapper= new ObjectMapper().registerModule(new JsonValueModule()); + + static ObjectMapper mapper = new ObjectMapper().registerModule(new JsonValueModule()); @Override public JsonNode convert(CouchbaseDocument source) { - if( source == null ){ + if (source == null) { return null; } return mapper.valueToTree(source.export()); @@ -262,7 +273,7 @@ public enum JsonObjectToMap implements Converter @Override public CouchbaseDocument convert(JsonObject source) { - if( source == null ){ + if (source == null) { return null; } return new CouchbaseDocument().setContent(source); @@ -272,11 +283,12 @@ public CouchbaseDocument convert(JsonObject source) { @ReadingConverter public enum MapToJsonObject implements Converter { INSTANCE; - static ObjectMapper mapper= new ObjectMapper(); + + static ObjectMapper mapper = new ObjectMapper(); @Override public JsonObject convert(CouchbaseDocument source) { - if( source == null ){ + if (source == null) { return null; } return JsonObject.from(source.export()); @@ -289,7 +301,7 @@ public enum JsonArrayToCouchbaseList implements Converter { INSTANCE; - static ObjectMapper mapper= new ObjectMapper(); + + static ObjectMapper mapper = new ObjectMapper(); @Override public JsonArray convert(CouchbaseList source) { - if( source == null ){ + if (source == null) { return null; } return JsonArray.from(source.export()); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java index 2caf652d9..9cd8689e3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -36,13 +36,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** - * A Jackson JSON Translator that implements the {@link TranslationService} contract. + * A Jackson JSON Translator that implements the {@link TranslationService} + * contract. * - * @author Emilien Bevierre * @author Michael Nitschinger * @author Simon Baslé * @author Anastasiia Smirnova * @author Mark Paluch + * @author Emilien Bevierre */ public class JacksonTranslationService implements TranslationService, InitializingBean { @@ -68,7 +69,7 @@ public class JacksonTranslationService implements TranslationService, Initializi * @return the encoded JSON String. */ @Override - public final String encode(final CouchbaseStorable source) { + public final String encode(CouchbaseStorable source) { Writer writer = new StringWriter(); try { @@ -86,11 +87,11 @@ public final String encode(final CouchbaseStorable source) { /** * Recursively iterates through the sources and adds it to the JSON generator. * - * @param source the source document + * @param source the source document * @param generator the JSON generator. * @throws IOException */ - private void encodeRecursive(final CouchbaseStorable source, final JsonGenerator generator) throws IOException { + private void encodeRecursive(CouchbaseStorable source, JsonGenerator generator) throws IOException { generator.writeStartObject(); for (Map.Entry entry : ((CouchbaseDocument) source).export().entrySet()) { @@ -102,7 +103,7 @@ private void encodeRecursive(final CouchbaseStorable source, final JsonGenerator continue; } - final Class clazz = value.getClass(); + Class clazz = value.getClass(); if (simpleTypeHolder.isSimpleType(clazz) && !isEnumOrClass(clazz)) { generator.writeObject(value); @@ -115,7 +116,7 @@ private void encodeRecursive(final CouchbaseStorable source, final JsonGenerator generator.writeEndObject(); } - private boolean isEnumOrClass(final Class clazz) { + private boolean isEnumOrClass(Class clazz) { return Enum.class.isAssignableFrom(clazz) || Class.class.isAssignableFrom(clazz); } @@ -127,7 +128,7 @@ private boolean isEnumOrClass(final Class clazz) { * @return the decoded structure. */ @Override - public final CouchbaseStorable decode(final String source, final CouchbaseStorable target) { + public final CouchbaseStorable decode(String source, CouchbaseStorable target) { try (JsonParser parser = factory.createParser(source)) { return decodeWithParser(parser, target); } catch (IOException ex) { @@ -137,14 +138,15 @@ public final CouchbaseStorable decode(final String source, final CouchbaseStorab /** * Decode a JSON byte array into the {@link CouchbaseStorable} structure. - * This avoids the intermediate String allocation by parsing directly from bytes. + * This avoids the intermediate String allocation by parsing directly from + * bytes. * * @param source the source formatted document as bytes (UTF-8 encoded). * @param target the target of the populated data. * @return the decoded structure. */ @Override - public final CouchbaseStorable decode(final byte[] source, final CouchbaseStorable target) { + public final CouchbaseStorable decode(byte[] source, CouchbaseStorable target) { try (JsonParser parser = factory.createParser(source)) { return decodeWithParser(parser, target); } catch (IOException ex) { @@ -152,7 +154,7 @@ public final CouchbaseStorable decode(final byte[] source, final CouchbaseStorab } } - private CouchbaseStorable decodeWithParser(final JsonParser parser, final CouchbaseStorable target) throws IOException { + private CouchbaseStorable decodeWithParser(JsonParser parser, CouchbaseStorable target) throws IOException { while (parser.nextToken() != null) { JsonToken currentToken = parser.getCurrentToken(); @@ -175,7 +177,7 @@ private CouchbaseStorable decodeWithParser(final JsonParser parser, final Couchb * @throws IOException * @returns the decoded object. */ - private CouchbaseDocument decodeObject(final JsonParser parser, final CouchbaseDocument target) throws IOException { + private CouchbaseDocument decodeObject(JsonParser parser, CouchbaseDocument target) throws IOException { JsonToken currentToken = parser.nextToken(); String fieldName = ""; @@ -204,7 +206,7 @@ private CouchbaseDocument decodeObject(final JsonParser parser, final CouchbaseD * @throws IOException * @returns the decoded list. */ - private CouchbaseList decodeArray(final JsonParser parser, final CouchbaseList target) throws IOException { + private CouchbaseList decodeArray(JsonParser parser, CouchbaseList target) throws IOException { JsonToken currentToken = parser.nextToken(); while (currentToken != null && currentToken != JsonToken.END_ARRAY) { @@ -225,12 +227,12 @@ private CouchbaseList decodeArray(final JsonParser parser, final CouchbaseList t /** * Helper method to decode and assign a primitive. * - * @param token the type of token. + * @param token the type of token. * @param parser the parser with the content. * @return the decoded primitve. * @throws IOException */ - private Object decodePrimitive(final JsonToken token, final JsonParser parser) throws IOException { + private Object decodePrimitive(JsonToken token, JsonParser parser) throws IOException { switch (token) { case VALUE_TRUE: case VALUE_FALSE: @@ -257,7 +259,7 @@ public T decodeFragment(String source, Class target) { } } - public void setObjectMapper(final ObjectMapper objectMapper) { + public void setObjectMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java index 58470b4dc..a2f9e64b3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -16,16 +16,17 @@ package org.springframework.data.couchbase.core.convert.translation; -import java.nio.charset.StandardCharsets; +import org.springframework.data.couchbase.core.util.ByteUtils; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; /** - * Defines a translation service to encode/decode responses into the {@link CouchbaseStorable} format. + * Defines a translation service to encode/decode responses into the + * {@link CouchbaseStorable} format. * - * @author Emilien Bevierre * @author Michael Nitschinger + * @author Emilien Bevierre */ public interface TranslationService { @@ -48,24 +49,28 @@ public interface TranslationService { /** * Decodes the target format from a byte array into a {@link CouchbaseDocument}. - * The default implementation converts the bytes to a String via UTF-8 before decoding. - * Implementations may override this to parse bytes directly and avoid the intermediate String allocation. + * The default implementation converts the bytes to a String via UTF-8 before + * decoding. + * Implementations may override this to parse bytes directly and avoid the + * intermediate String allocation. * * @param source the source formatted document as bytes (UTF-8 encoded). * @param target the target of the populated data. * @return a properly populated document to work with. */ default CouchbaseStorable decode(byte[] source, CouchbaseStorable target) { - return decode(new String(source, StandardCharsets.UTF_8), target); + return decode(ByteUtils.getString(source), target); } /** * Decodes an ad-hoc JSON object into a corresponding "case" class. * - * @param source the JSON for the ad-hoc JSON object (from a N1QL query for instance). + * @param source the JSON for the ad-hoc JSON object (from a N1QL query for + * instance). * @param target the target class information. - * @param the target class. - * @return an ad-hoc instance of the decoded JSON into the corresponding "case" class. + * @param the target class. + * @return an ad-hoc instance of the decoded JSON into the corresponding "case" + * class. */ T decodeFragment(String source, Class target); } diff --git a/src/main/java/org/springframework/data/couchbase/core/util/ByteUtils.java b/src/main/java/org/springframework/data/couchbase/core/util/ByteUtils.java new file mode 100644 index 000000000..0a042fd0e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/util/ByteUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present 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.couchbase.core.util; + +import java.nio.charset.StandardCharsets; + +/** + * Utility class for common byte array and String conversions using UTF-8. + * + * @author Emilien Bevierre + */ +public abstract class ByteUtils { + + private ByteUtils() { + // Private constructor to prevent instantiation + } + + /** + * Decodes a given byte array into a String using UTF-8 charset. + * + * @param source the byte array to decode, can be null + * @return the decoded string, or null if source is null + */ + public static String getString(byte[] source) { + return source == null ? null : new String(source, StandardCharsets.UTF_8); + } + + /** + * Encodes a given String into a byte array using UTF-8 charset. + * + * @param source the string to encode, can be null + * @return the encoded byte array, or null if source is null + */ + public static byte[] getBytes(String source) { + return source == null ? null : source.getBytes(StandardCharsets.UTF_8); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java index 9d58f860e..c681791e8 100644 --- a/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java @@ -27,8 +27,8 @@ /** * Verifies the functionality of a {@link JacksonTranslationService}. * - * @author Emilien Bevierre * @author Michael Nitschinger + * @author Emilien Bevierre */ public class JacksonTranslationServiceTests { From a2a017ac92a76736952a6c4d41f73539adaa54b8 Mon Sep 17 00:00:00 2001 From: Emilien Bevierre Date: Mon, 23 Feb 2026 18:15:17 +0000 Subject: [PATCH 6/7] Remove decodeEntityBase logic duplication by using a shared function Adjust comments Signed-off-by: Emilien Bevierre --- .../core/AbstractTemplateSupport.java | 124 ++++++++++-------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 12866501a..a21298508 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -19,8 +19,8 @@ import java.time.Instant; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; -import com.couchbase.client.core.annotation.Stability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; @@ -40,9 +40,9 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.ClassUtils; +import com.couchbase.client.core.annotation.Stability; import com.couchbase.client.core.error.CouchbaseException; - /** * Base shared by Reactive and non-Reactive TemplateSupport * @@ -71,82 +71,97 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv public T decodeEntityBase(Object id, String source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { - CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); - - if (persistentEntity == null) { - final CouchbaseDocument converted = new CouchbaseDocument(id); - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); - return (T) set.iterator().next().getValue(); - } - - final CouchbaseDocument converted = prepareConvertedDocument(id, cas, persistentEntity); - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); - return finalizeEntity(readEntity, id, cas, expiryTime, scope, collection, txResultHolder, holder); + return decodeEntityBase(id, cas, expiryTime, entityClass, scope, collection, txResultHolder, holder, + (ts, converted) -> (CouchbaseDocument) ts.decode(source, converted)); } public T decodeEntityBase(Object id, byte[] source, Long cas, Instant expiryTime, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + return decodeEntityBase(id, cas, expiryTime, entityClass, scope, collection, txResultHolder, holder, + (ts, converted) -> (CouchbaseDocument) ts.decode(source, converted)); + } + + private T decodeEntityBase(Object id, Long cas, Instant expiryTime, Class entityClass, String scope, + String collection, Object txResultHolder, CouchbaseResourceHolder holder, + BiFunction translatorFn) { CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); if (persistentEntity == null) { final CouchbaseDocument converted = new CouchbaseDocument(id); - Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) - .getContent().entrySet(); + Set> set = translatorFn.apply(translationService, converted).getContent() + .entrySet(); return (T) set.iterator().next().getValue(); } final CouchbaseDocument converted = prepareConvertedDocument(id, cas, persistentEntity); - T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); + T readEntity = converter.read(entityClass, translatorFn.apply(translationService, converted)); return finalizeEntity(readEntity, id, cas, expiryTime, scope, collection, txResultHolder, holder); } - private CouchbaseDocument prepareConvertedDocument(Object id, Long cas, CouchbasePersistentEntity persistentEntity) { - // this is the entity class defined for the repository. It may not be the class of the document that was read - // we will reset it after reading the document + private CouchbaseDocument prepareConvertedDocument(Object id, Long cas, + CouchbasePersistentEntity persistentEntity) { + // persistentEntity is derived from the entityClass declared in the + // repository definition. It may be an abstract class rather than the + // concrete class of the document being read. The concrete type is only + // known after converter.read() is called, therefore version/cas is set again + // on the final entity in finalizeEntity(). // - // This will fail for the case where: - // 1) The version is defined in the concrete class, but not in the abstract class; and - // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in - // the source is null. - // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source, - // but that is a lot of work to do every time just for this very rare and avoidable case. + // Pre-populating the CAS/version into the source document (done in getDocument) + // is a best-effort step to avoid a construction failure when the concrete + // class constructor takes a primitive "long version" argument (null is not a + // valid value for a primitive). If the version property is only declared on the + // concrete subclass and not on the abstract base, pre-population is not + // possible here and the issue can be avoided by using "Long" instead of "long". + // + // An alternative would be to resolve the actual concrete type from the source + // document's type metadata before constructing it (see the comment + // below), but that adds overhead for every decode to solve a rare and avoidable + // case. // TypeInformation typeToUse = typeMapper.readType(source, type); if (id == null) { - throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID); - } - - final CouchbaseDocument converted = new CouchbaseDocument(id); - - // if possible, set the version property in the source so that if the constructor has a long version argument, - // it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure - // can be avoid by defining the argument as Long instead of long. - // persistentEntity is still the (possibly abstract) class specified in the repository definition - // it's possible that the abstract class does not have a version property, and this won't be able to set the version - if (persistentEntity.getVersionProperty() != null) { - if (cas == null) { - throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS - + " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS); - } - if (cas != 0) { - converted.put(persistentEntity.getVersionProperty().getName(), cas); - } + throw new CouchbaseException( + TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " + + TemplateUtils.SELECT_ID); } - return converted; + return getDocument(id, cas, persistentEntity); } - private T finalizeEntity(T readEntity, Object id, Long cas, Instant expiryTime, String scope, String collection, + private static CouchbaseDocument getDocument(Object id, Long cas, CouchbasePersistentEntity persistentEntity) { + final CouchbaseDocument converted = new CouchbaseDocument(id); + + // If possible, set the version property in the source so that if the + // constructor has a long version argument, it will have a value and succeed, + // as null is not a valid argument for a long. This failure can be avoided by + // defining the argument as Long instead of long. + // Note that persistentEntity is still the (possibly abstract) class specified + // in the repository definition, so it's possible that the abstract class does + // not have a version property, in which case this won't be able to set the version. + if (persistentEntity.getVersionProperty() != null) { + if (cas == null) { + throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS + + " was not in result. Either use #{#n1ql.selectEntity} or project " + + TemplateUtils.SELECT_CAS); + } + if (cas != 0) { + converted.put(persistentEntity.getVersionProperty().getName(), cas); + } + } + return converted; + } + + private T finalizeEntity(T readEntity, Object id, Long cas, Instant expiryTime, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(readEntity.getClass()); - // if the constructor has an argument that is long version, then construction will fail if the 'version' - // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this. - // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity) + // If the constructor has a long version argument, construction will fail if + // 'version' is not available, as null is not a legal value for a long. + // We therefore use the object-wrapped Long type. + // (Version doesn't come from 'source', it comes from the cas argument to + // decodeEntity.) if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } @@ -155,7 +170,8 @@ private T finalizeEntity(T readEntity, Object id, Long cas, Instant expiryTi accessor.setProperty(persistentEntity.getExpiryProperty(), expiryTime); } - N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id.toString(), scope, collection); + N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id.toString(), scope, + collection); if (holder != null) { holder.transactionResultHolder(txResultHolder, (T) accessor.getBean()); @@ -203,7 +219,8 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l public Long getCas(final Object entity) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); long cas = 0; if (versionProperty != null) { @@ -217,7 +234,8 @@ public Long getCas(final Object entity) { public Object getId(final Object entity) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); Object id = null; if (idProperty != null) { From 202f02b3ce7e166aa24723df180e30274d3bea9d Mon Sep 17 00:00:00 2001 From: Emilien Bevierre Date: Tue, 24 Feb 2026 14:20:55 +0000 Subject: [PATCH 7/7] Remove final modifiers in var declaration and method parameters Use String.formatted() for logging Signed-off-by: Emilien Bevierre --- .../core/AbstractTemplateSupport.java | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index a21298508..235368da1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -87,13 +87,13 @@ private T decodeEntityBase(Object id, Long cas, Instant expiryTime, Class CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass); if (persistentEntity == null) { - final CouchbaseDocument converted = new CouchbaseDocument(id); + CouchbaseDocument converted = new CouchbaseDocument(id); Set> set = translatorFn.apply(translationService, converted).getContent() .entrySet(); return (T) set.iterator().next().getValue(); } - final CouchbaseDocument converted = prepareConvertedDocument(id, cas, persistentEntity); + CouchbaseDocument converted = prepareConvertedDocument(id, cas, persistentEntity); T readEntity = converter.read(entityClass, translatorFn.apply(translationService, converted)); return finalizeEntity(readEntity, id, cas, expiryTime, scope, collection, txResultHolder, holder); } @@ -120,16 +120,15 @@ private CouchbaseDocument prepareConvertedDocument(Object id, Long cas, // TypeInformation typeToUse = typeMapper.readType(source, type); if (id == null) { - throw new CouchbaseException( - TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID); + throw new CouchbaseException("%s was null. Either use #{#n1ql.selectEntity} or project %s" + .formatted(TemplateUtils.SELECT_ID, TemplateUtils.SELECT_ID)); } return getDocument(id, cas, persistentEntity); } private static CouchbaseDocument getDocument(Object id, Long cas, CouchbasePersistentEntity persistentEntity) { - final CouchbaseDocument converted = new CouchbaseDocument(id); + CouchbaseDocument converted = new CouchbaseDocument(id); // If possible, set the version property in the source so that if the // constructor has a long version argument, it will have a value and succeed, @@ -140,9 +139,9 @@ private static CouchbaseDocument getDocument(Object id, Long cas, CouchbasePersi // not have a version property, in which case this won't be able to set the version. if (persistentEntity.getVersionProperty() != null) { if (cas == null) { - throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS - + " was not in result. Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_CAS); + throw new CouchbaseException( + "version/cas in the entity but %s was not in result. Either use #{#n1ql.selectEntity} or project %s" + .formatted(TemplateUtils.SELECT_CAS, TemplateUtils.SELECT_CAS)); } if (cas != 0) { converted.put(persistentEntity.getVersionProperty().getName(), cas); @@ -153,7 +152,7 @@ private static CouchbaseDocument getDocument(Object id, Long cas, CouchbasePersi private T finalizeEntity(T readEntity, Object id, Long cas, Instant expiryTime, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); + ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(readEntity.getClass()); @@ -196,15 +195,15 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l Object txResultHolder, CouchbaseResourceHolder holder) { ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = converter.getMappingContext() + CouchbasePersistentEntity persistentEntity = converter.getMappingContext() .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); if (idProperty != null) { accessor.setProperty(idProperty, id); } - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); if (versionProperty != null) { accessor.setProperty(versionProperty, cas); } @@ -217,11 +216,11 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l } - public Long getCas(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext + public Long getCas(Object entity) { + ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + CouchbasePersistentEntity persistentEntity = mappingContext .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); long cas = 0; if (versionProperty != null) { Object casObject = accessor.getProperty(versionProperty); @@ -232,11 +231,11 @@ public Long getCas(final Object entity) { return cas; } - public Object getId(final Object entity) { - final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext + public Object getId(Object entity) { + ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); + CouchbasePersistentEntity persistentEntity = mappingContext .getRequiredPersistentEntity(entity.getClass()); - final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); + CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); Object id = null; if (idProperty != null) { id = accessor.getProperty(idProperty); @@ -244,13 +243,13 @@ public Object getId(final Object entity) { return id; } - public String getJavaNameForEntity(final Class clazz) { - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); + public String getJavaNameForEntity(Class clazz) { + CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(clazz); MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); return info.getJavaType().getName(); } - ConvertingPropertyAccessor getPropertyAccessor(final T source) { + ConvertingPropertyAccessor getPropertyAccessor(T source) { CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass()); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService());