Issue with ExpiryExpression Annotation in Spring Data couchbase

Hi, So to give you a context , We have upgraded Spring version and hence Couchbase and com.couchbase.client:encryption:2.0.1 . We have a multibucket couchbase confiuration . We want to set expiry using ExpiryExpression paramter for Document annotation . So using @Document(expiry = 150005) works or setting TTL through getandTouch() works but @Document(expiryExpression = “${spring.compliance.couchbase.moalertreport.expiry.time.seconds}”) annotation does not work.
It gives an error

java.lang.IllegalArgumentException: Environment must be set to use ‘expiryExpression’

I dont understand this error . Environment has not been set.

This is our Couchbase Configuration.

public class CouchbaseConfiguration extends AbstractCouchbaseConfiguration{
@Value("${spring.core.couchbase.bucket.bucketName}")
private String bucketName;
@Value("${spring.core.couchbase.bucket.bucketPassword}")
private String bucketPassword;
@Value("${spring.core.couchbase.bucket.bootstrapHosts}")
private String bootstrapHosts;

@Value("${spring.compliance.couchbase.bucket.bucketName}")
private String complianceBucketName;
@Value("${spring.compliance.couchbase.bucket.bucketPassword}")
private String complianceBucketPassword;
@Value("${spring.compliance.couchbase.bucket.bootstrapHosts}")
private String complainceBootstrapHosts;
@Value("${spring.couchbase.sslEnabled}")
private boolean sslEnabled;
@Value("${spring.couchbase.env.sslKeyStorePath}")
private String sslKeyStorePath;
@Value("${spring.couchbase.sslKeyStorePassword}")
private String sslKeyStorePassword;

Cluster complianceCluster;

private long kvTimeout;
private long viewTimeout;
private long queryTimeout;
private long searchTimeout;
private long analyticsTimeout;
private long connectTimeout;
private long disconnectTimeout;
private long managementTimeout;
private boolean tcpNodelayEnabled;
private long configPollInterval;
public long documentExpiryTime;
public long idleHttpConnectionTimeout;
public long computationPoolSize;
public long minEndpoints;
public int maxEndpoints;


@Override
public String getConnectionString() {
	return this.bootstrapHosts;
}

@Override
public String getUserName() {
	return this.bucketName;
}

@Override
public String getPassword() {
	return this.bucketPassword;
}

@Override
public String getBucketName() {
	return this.bucketName;
}

private String getComplainceBucketUserName() {
	return this.complianceBucketName;
}

private String getComplianceBucketPassword() {
	return this.complianceBucketPassword;
}

private String getComplianceBucketName() {
	return this.complianceBucketName;
}


@Bean(name = { "defaultBucket" })
public Bucket defaultBucket() throws Exception {
	log.info("in default bucket");
	return couchbaseCluster(couchbaseClusterEnvironment()).bucket(getBucketName());
}

@Bean(name = { "complianceBucket" })
public Bucket complainceBucket() throws Exception {
	log.info("in default bucket");
	return complianceCluster.bucket(getComplianceBucketName());
}


@Bean
public Cluster complianceCluster() throws Exception {
	log.info("in compliance cluster");
	complianceCluster = Cluster.connect(this.getConnectionString(),
			ClusterOptions.clusterOptions(PasswordAuthenticator.create(this.getComplainceBucketUserName(),
					this.getComplianceBucketPassword())).environment(couchbaseClusterEnvironment()));
	return complianceCluster;
}



@Bean(name = "complianceDataTemplate")
public CouchbaseTemplate complianceDataTemplate() throws Exception {
	log.info("in compliance data template");
	List<Object> converters = new ArrayList<>();
	MappingCouchbaseConverter converter = new MappingCouchbaseConverter();
	converter.setCustomConversions(new CouchbaseCustomConversions(converters));
	CouchbaseClientFactory simpleCouchbaseClientFactory = new SimpleCouchbaseClientFactory(complianceCluster(), getComplianceBucketName(), this.getScopeName());
	CouchbaseTemplate template = new CouchbaseTemplate(simpleCouchbaseClientFactory, converter);
	//template.setDefaultConsistency(getDefaultConsistency());
	return template;
}


@Override
public void configureRepositoryOperationsMapping(RepositoryOperationsMapping baseMapping) {
	try {
		log.info("in config mapping");
		//baseMapping.mapEntity(AgentDetailsCBType.class,  coreTemplate);
		//baseMapping.mapEntity(MOConsumerProfileResolvedEntity.class, complianceDataTemplate());
		baseMapping.mapEntity(MOValidationAlertsReportData.class, complianceDataTemplate());
		baseMapping.mapEntity(MOConsumerProfileResolvedEntity.class, complianceDataTemplate());
		baseMapping.mapEntity(MOTransactionDetails.class, complianceDataTemplate());
	} catch (Exception e) {
		e.printStackTrace();
	}
}


@Override
public ClusterEnvironment couchbaseClusterEnvironment() {
	log.info("in couchbase cluster env ");
	ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
	this.configureEnvironment(builder);
	builder.timeoutConfig(TimeoutConfig.kvTimeout(Duration.ofMillis(kvTimeout)));
	builder.timeoutConfig(TimeoutConfig.viewTimeout(Duration.ofMillis(viewTimeout)));
	builder.timeoutConfig(TimeoutConfig.queryTimeout(Duration.ofMillis(queryTimeout)));
	builder.timeoutConfig(TimeoutConfig.searchTimeout(Duration.ofMillis(searchTimeout)));
	builder.timeoutConfig(TimeoutConfig.analyticsTimeout(Duration.ofMillis(analyticsTimeout)));
	builder.timeoutConfig(TimeoutConfig.connectTimeout(Duration.ofMillis(connectTimeout)));
	builder.timeoutConfig(TimeoutConfig.disconnectTimeout(Duration.ofMillis(disconnectTimeout)));
	builder.timeoutConfig(TimeoutConfig.managementTimeout(Duration.ofMillis(managementTimeout)));
	builder.ioConfig(IoConfig.configPollInterval(Duration.ofMillis(configPollInterval)));
	builder.ioConfig(IoConfig.enableTcpKeepAlives(tcpNodelayEnabled));
	builder.ioConfig(IoConfig.idleHttpConnectionTimeout(Duration.ofMillis(idleHttpConnectionTimeout)));
	if (sslEnabled) {
		builder.securityConfig(SecurityConfig.enableTls(true));
		builder.securityConfig(SecurityConfig.enableNativeTls(true));
		builder.securityConfig(SecurityConfig.trustStore(Paths.get(sslKeyStorePath), sslKeyStorePassword, Optional.empty()));
	}
	return builder.build();
}

@Override
public void  configureEnvironment(final ClusterEnvironment.Builder builder)  {
	builder.ioConfig()
			.maxHttpConnections(maxEndpoints)
			.idleHttpConnectionTimeout(Duration.ofSeconds(idleHttpConnectionTimeout));
	return ;

}

}

Please help out.
Thank you.

@mreiche or @deniswsrosa can you help here ?

This is related to Custom converter for multibuckets · Issue #1141 · spring-projects/spring-data-couchbase · GitHub .
It would be better to use the mappingCouchbaseConverter bean instead of creating one.
If you are using a one that you created, it needs to be created like the framework creates it - complete with a ‘context’ (which has an environment).

	@Autowired
	ApplicationContext applicationContext;

	@Override
	@Bean(name = "couchbaseMappingConverter")
	public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext,
																														 CouchbaseCustomConversions couchbaseCustomConversions) {
		MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey());
		converter.setCustomConversions(couchbaseCustomConversions);
		converter.setApplicationContext(applicationContext);
		converter.afterPropertiesSet();
		return converter;
	}

If you are creating your own CouchbaseMappingContext, you must set the applicationContext on it after creating it :
mappingContext.setApplicationContext(applicationContext)

Hi @mreiche Thanks for the insight . After setting the following line

mappingContext.setApplicationContext(applicationContext);

The error is gone . However , The expiry now set to 0, not the configured value . Is this a spring issue or we are missing something ?

Im using the expressions like this :

@Document(expiryExpression = “${spring.compliance.couchbase.expiry.time}”)

I have even checked the value using following code:

        Document doc = MOValidationAlertsReportData.class.getAnnotation(Document.class);
        String expression = doc.expiryExpression();
        String expiryWithReplacedPlaceholders = environment.resolveRequiredPlaceholders(expression);
        log.info("exiry is {}",expiryWithReplacedPlaceholders);

It gives the correct expiry but the expiry set on the document is 0 (checking using metadata option on Couchbase dashboard).

Prior to December, expiryExpression was ignored.

So is this Bug fixed or not ?

Edit:
It was fixed in master(main) on December 2 and pulled into 4.1.x on Feb 16. If you have [edit] 4.1.7 or later, it will be fixed in that.
If you have 4.1.7 or later, and still have a problem with expiryExpression - then the issue is something else and needs further investigation.

Screen Shot 2021-06-07 at 9.12.13 AM

Hi I tried with 4.1.2 spring-data-couchbase , still the expiry is 0.Could it be thats its not working for Multi-bucket config ?

For some reason, the fix mentioned earlier was not pulled into main until 4.1.7. So 4.1.7 or later. Sorry.

Finally Working!! 4.1.7 is required to make it work !