1919import java .time .Instant ;
2020import java .util .Map ;
2121import java .util .Set ;
22+ import java .util .function .BiFunction ;
2223
23- import com .couchbase .client .core .annotation .Stability ;
2424import org .slf4j .Logger ;
2525import org .slf4j .LoggerFactory ;
2626import org .springframework .context .ApplicationContext ;
4040import org .springframework .data .mapping .model .ConvertingPropertyAccessor ;
4141import org .springframework .util .ClassUtils ;
4242
43+ import com .couchbase .client .core .annotation .Stability ;
4344import com .couchbase .client .core .error .CouchbaseException ;
4445
45-
4646/**
4747 * Base shared by Reactive and non-Reactive TemplateSupport
4848 *
@@ -71,82 +71,97 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv
7171
7272 public <T > T decodeEntityBase (Object id , String source , Long cas , Instant expiryTime , Class <T > entityClass ,
7373 String scope , String collection , Object txResultHolder , CouchbaseResourceHolder holder ) {
74- CouchbasePersistentEntity persistentEntity = couldBePersistentEntity (entityClass );
75-
76- if (persistentEntity == null ) {
77- final CouchbaseDocument converted = new CouchbaseDocument (id );
78- Set <Map .Entry <String , Object >> set = ((CouchbaseDocument ) translationService .decode (source , converted ))
79- .getContent ().entrySet ();
80- return (T ) set .iterator ().next ().getValue ();
81- }
82-
83- final CouchbaseDocument converted = prepareConvertedDocument (id , cas , persistentEntity );
84- T readEntity = converter .read (entityClass , (CouchbaseDocument ) translationService .decode (source , converted ));
85- return finalizeEntity (readEntity , id , cas , expiryTime , scope , collection , txResultHolder , holder );
74+ return decodeEntityBase (id , cas , expiryTime , entityClass , scope , collection , txResultHolder , holder ,
75+ (ts , converted ) -> (CouchbaseDocument ) ts .decode (source , converted ));
8676 }
8777
8878 public <T > T decodeEntityBase (Object id , byte [] source , Long cas , Instant expiryTime , Class <T > entityClass ,
8979 String scope , String collection , Object txResultHolder , CouchbaseResourceHolder holder ) {
80+ return decodeEntityBase (id , cas , expiryTime , entityClass , scope , collection , txResultHolder , holder ,
81+ (ts , converted ) -> (CouchbaseDocument ) ts .decode (source , converted ));
82+ }
83+
84+ private <T > T decodeEntityBase (Object id , Long cas , Instant expiryTime , Class <T > entityClass , String scope ,
85+ String collection , Object txResultHolder , CouchbaseResourceHolder holder ,
86+ BiFunction <TranslationService , CouchbaseDocument , CouchbaseDocument > translatorFn ) {
9087 CouchbasePersistentEntity persistentEntity = couldBePersistentEntity (entityClass );
9188
9289 if (persistentEntity == null ) {
9390 final CouchbaseDocument converted = new CouchbaseDocument (id );
94- Set <Map .Entry <String , Object >> set = (( CouchbaseDocument ) translationService . decode ( source , converted ))
95- .getContent (). entrySet ();
91+ Set <Map .Entry <String , Object >> set = translatorFn . apply ( translationService , converted ). getContent ( )
92+ .entrySet ();
9693 return (T ) set .iterator ().next ().getValue ();
9794 }
9895
9996 final CouchbaseDocument converted = prepareConvertedDocument (id , cas , persistentEntity );
100- T readEntity = converter .read (entityClass , ( CouchbaseDocument ) translationService . decode ( source , converted ));
97+ T readEntity = converter .read (entityClass , translatorFn . apply ( translationService , converted ));
10198 return finalizeEntity (readEntity , id , cas , expiryTime , scope , collection , txResultHolder , holder );
10299 }
103100
104- private CouchbaseDocument prepareConvertedDocument (Object id , Long cas , CouchbasePersistentEntity persistentEntity ) {
105- // this is the entity class defined for the repository. It may not be the class of the document that was read
106- // we will reset it after reading the document
101+ private CouchbaseDocument prepareConvertedDocument (Object id , Long cas ,
102+ CouchbasePersistentEntity persistentEntity ) {
103+ // persistentEntity is derived from the entityClass declared in the
104+ // repository definition. It may be an abstract class rather than the
105+ // concrete class of the document being read. The concrete type is only
106+ // known after converter.read() is called, therefore version/cas is set again
107+ // on the final entity in finalizeEntity().
107108 //
108- // This will fail for the case where:
109- // 1) The version is defined in the concrete class, but not in the abstract class; and
110- // 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in
111- // the source is null.
112- // We could expose from the MappingCouchbaseConverter determining the persistent entity from the source,
113- // but that is a lot of work to do every time just for this very rare and avoidable case.
109+ // Pre-populating the CAS/version into the source document (done in getDocument)
110+ // is a best-effort step to avoid a construction failure when the concrete
111+ // class constructor takes a primitive "long version" argument (null is not a
112+ // valid value for a primitive). If the version property is only declared on the
113+ // concrete subclass and not on the abstract base, pre-population is not
114+ // possible here and the issue can be avoided by using "Long" instead of "long".
115+ //
116+ // An alternative would be to resolve the actual concrete type from the source
117+ // document's type metadata before constructing it (see the comment
118+ // below), but that adds overhead for every decode to solve a rare and avoidable
119+ // case.
114120 // TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
115121
116122 if (id == null ) {
117- throw new CouchbaseException (TemplateUtils .SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project "
118- + TemplateUtils .SELECT_ID );
119- }
120-
121- final CouchbaseDocument converted = new CouchbaseDocument (id );
122-
123- // if possible, set the version property in the source so that if the constructor has a long version argument,
124- // it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure
125- // can be avoid by defining the argument as Long instead of long.
126- // persistentEntity is still the (possibly abstract) class specified in the repository definition
127- // it's possible that the abstract class does not have a version property, and this won't be able to set the version
128- if (persistentEntity .getVersionProperty () != null ) {
129- if (cas == null ) {
130- throw new CouchbaseException ("version/cas in the entity but " + TemplateUtils .SELECT_CAS
131- + " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils .SELECT_CAS );
132- }
133- if (cas != 0 ) {
134- converted .put (persistentEntity .getVersionProperty ().getName (), cas );
135- }
123+ throw new CouchbaseException (
124+ TemplateUtils .SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project "
125+ + TemplateUtils .SELECT_ID );
136126 }
137127
138- return converted ;
128+ return getDocument ( id , cas , persistentEntity ) ;
139129 }
140130
141- private <T > T finalizeEntity (T readEntity , Object id , Long cas , Instant expiryTime , String scope , String collection ,
131+ private static CouchbaseDocument getDocument (Object id , Long cas , CouchbasePersistentEntity persistentEntity ) {
132+ final CouchbaseDocument converted = new CouchbaseDocument (id );
133+
134+ // If possible, set the version property in the source so that if the
135+ // constructor has a long version argument, it will have a value and succeed,
136+ // as null is not a valid argument for a long. This failure can be avoided by
137+ // defining the argument as Long instead of long.
138+ // Note that persistentEntity is still the (possibly abstract) class specified
139+ // in the repository definition, so it's possible that the abstract class does
140+ // not have a version property, in which case this won't be able to set the version.
141+ if (persistentEntity .getVersionProperty () != null ) {
142+ if (cas == null ) {
143+ throw new CouchbaseException ("version/cas in the entity but " + TemplateUtils .SELECT_CAS
144+ + " was not in result. Either use #{#n1ql.selectEntity} or project "
145+ + TemplateUtils .SELECT_CAS );
146+ }
147+ if (cas != 0 ) {
148+ converted .put (persistentEntity .getVersionProperty ().getName (), cas );
149+ }
150+ }
151+ return converted ;
152+ }
153+
154+ private <T > T finalizeEntity (T readEntity , Object id , Long cas , Instant expiryTime , String scope , String collection ,
142155 Object txResultHolder , CouchbaseResourceHolder holder ) {
143156 final ConvertingPropertyAccessor <T > accessor = getPropertyAccessor (readEntity );
144157
145158 CouchbasePersistentEntity persistentEntity = couldBePersistentEntity (readEntity .getClass ());
146159
147- // if the constructor has an argument that is long version, then construction will fail if the 'version'
148- // is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this.
149- // (Version doesn't come from 'source', it comes from the cas argument to decodeEntity)
160+ // If the constructor has a long version argument, construction will fail if
161+ // 'version' is not available, as null is not a legal value for a long.
162+ // We therefore use the object-wrapped Long type.
163+ // (Version doesn't come from 'source', it comes from the cas argument to
164+ // decodeEntity.)
150165 if (cas != null && cas != 0 && persistentEntity .getVersionProperty () != null ) {
151166 accessor .setProperty (persistentEntity .getVersionProperty (), cas );
152167 }
@@ -155,7 +170,8 @@ private <T> T finalizeEntity(T readEntity, Object id, Long cas, Instant expiryTi
155170 accessor .setProperty (persistentEntity .getExpiryProperty (), expiryTime );
156171 }
157172
158- N1qlJoinResolver .handleProperties (persistentEntity , accessor , getReactiveTemplate (), id .toString (), scope , collection );
173+ N1qlJoinResolver .handleProperties (persistentEntity , accessor , getReactiveTemplate (), id .toString (), scope ,
174+ collection );
159175
160176 if (holder != null ) {
161177 holder .transactionResultHolder (txResultHolder , (T ) accessor .getBean ());
@@ -203,7 +219,8 @@ public <T> T applyResultBase(T entity, CouchbaseDocument converted, Object id, l
203219
204220 public Long getCas (final Object entity ) {
205221 final ConvertingPropertyAccessor <Object > accessor = getPropertyAccessor (entity );
206- final CouchbasePersistentEntity <?> persistentEntity = mappingContext .getRequiredPersistentEntity (entity .getClass ());
222+ final CouchbasePersistentEntity <?> persistentEntity = mappingContext
223+ .getRequiredPersistentEntity (entity .getClass ());
207224 final CouchbasePersistentProperty versionProperty = persistentEntity .getVersionProperty ();
208225 long cas = 0 ;
209226 if (versionProperty != null ) {
@@ -217,7 +234,8 @@ public Long getCas(final Object entity) {
217234
218235 public Object getId (final Object entity ) {
219236 final ConvertingPropertyAccessor <Object > accessor = getPropertyAccessor (entity );
220- final CouchbasePersistentEntity <?> persistentEntity = mappingContext .getRequiredPersistentEntity (entity .getClass ());
237+ final CouchbasePersistentEntity <?> persistentEntity = mappingContext
238+ .getRequiredPersistentEntity (entity .getClass ());
221239 final CouchbasePersistentProperty idProperty = persistentEntity .getIdProperty ();
222240 Object id = null ;
223241 if (idProperty != null ) {
0 commit comments