diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6f9b3a5 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Copy this file to .env and update with your actual values +# DO NOT commit .env file to git - it's already in .gitignore +# +# These environment variables are used for database connection +# and correspond to the Spring Boot 3.5+ Couchbase properties: +# - spring.couchbase.connection-string +# - spring.couchbase.username +# - spring.couchbase.password + +DB_CONN_STR=couchbases://your-cluster-url.cloud.couchbase.com +DB_USERNAME=your-username +DB_PASSWORD=your-password \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 561be4c..0425b13 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -7,6 +7,7 @@ on: branches: [main] schedule: - cron: "10 9 * * *" + workflow_dispatch: jobs: run_tests: @@ -31,7 +32,7 @@ jobs: uses: actions/setup-java@v4 with: java-version: ${{ matrix.java-version }} - distribution: "adopt" + distribution: "temurin" cache: "maven" - name: Run Maven Tests diff --git a/.gitignore b/.gitignore index db3286e..c63b6f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iml .docker/ .vscode/ +.env # maven target directory target/ diff --git a/README.md b/README.md index ff32165..2102412 100644 --- a/README.md +++ b/README.md @@ -63,20 +63,24 @@ All configuration for communication with the database is read from the applicati ```properties spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER spring.couchbase.bucket.name=travel-sample -spring.couchbase.bootstrap-hosts=DB_CONN_STR -spring.couchbase.bucket.user=DB_USERNAME -spring.couchbase.bucket.password=DB_PASSWORD +spring.couchbase.connection-string=${DB_CONN_STR} +spring.couchbase.username=${DB_USERNAME} +spring.couchbase.password=${DB_PASSWORD} ``` -Instead of the DB_CONN_STR, DB_USERNAME and DB_PASSWORD, you need to add the values for the Couchbase connection. +The application uses environment variables for database configuration. You can set these in your system environment or create a `.env` file in the project root: -> Note: The connection string expects the `couchbases://` or `couchbase://` part. +```env +DB_CONN_STR=couchbases://your-cluster.cloud.couchbase.com +DB_USERNAME=your-username +DB_PASSWORD=your-password +``` -You can also use your system environment variables to set the properties. The properties are read from the environment variables if they are set. The properties are read from the `application.properties` file if the environment variables are not set. +> Note: The connection string expects the `couchbases://` or `couchbase://` part. ## Running The Application -You can add environment variables DB_CONN_STR, DB_USERNAME and DB_PASSWORD to your system environment variables or you can update the `application.properties` file in the `src/main/resources` folder. +The application will automatically load environment variables from a `.env` file if present, or use system environment variables. ### Directly on Machine @@ -100,8 +104,7 @@ Run the Docker image docker run -d --name springboot-container -p 9440:8080 java-springboot-quickstart -e DB_CONN_STR= -e DB_USERNAME= -e DB_PASSWORD= ``` -Note: The `application.properties` file has the connection information to connect to your Capella cluster. You can also pass the connection information as environment variables to the Docker container. -If you choose not to pass the environment variables, you can update the `application.properties` file in the `src/main/resources` folder. +Note: You can pass the connection information as environment variables to the Docker container or include a `.env` file in your Docker build context. ### Verifying the Application @@ -168,7 +171,7 @@ If you would like to add another entity to the APIs, these are the steps to foll If you are running this quickstart with a self-managed Couchbase cluster, you need to [load](https://docs.couchbase.com/server/current/manage/manage-settings/install-sample-buckets.html) the travel-sample data bucket in your cluster and generate the credentials for the bucket. -You need to update the connection string and the credentials in the `application.properties` file in the `src/main/resources` folder. +You need to set the connection string and credentials using environment variables or a `.env` file as described above. Note: Couchbase Server version 7 or higher must be installed and running before running the Spring Boot Java app. diff --git a/pom.xml b/pom.xml index 8faeeb8..d8d1b2f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.4.3 + 3.5.5 org.couchbase @@ -61,6 +61,12 @@ runtime true + + + io.github.cdimascio + dotenv-java + 3.0.2 + org.springframework.boot spring-boot-starter-validation @@ -128,11 +134,6 @@ org.apache.maven.plugins maven-surefire-plugin 3.5.2 - - - **/*IntegrationTest.java - - org.springframework.boot diff --git a/src/main/java/org/couchbase/quickstart/springboot/config/DotEnvConfiguration.java b/src/main/java/org/couchbase/quickstart/springboot/config/DotEnvConfiguration.java new file mode 100644 index 0000000..5161847 --- /dev/null +++ b/src/main/java/org/couchbase/quickstart/springboot/config/DotEnvConfiguration.java @@ -0,0 +1,52 @@ +package org.couchbase.quickstart.springboot.config; + +import io.github.cdimascio.dotenv.Dotenv; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +import java.util.HashMap; +import java.util.Map; + +public class DotEnvConfiguration implements ApplicationContextInitializer { + + private static final Logger log = LoggerFactory.getLogger(DotEnvConfiguration.class); + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + + try { + // Load .env file if it exists + Dotenv dotenv = Dotenv.configure() + .directory(".") + .ignoreIfMalformed() + .ignoreIfMissing() + .load(); + + // Create a property source from .env entries + Map envMap = new HashMap<>(); + dotenv.entries().forEach(entry -> { + String key = entry.getKey(); + String value = entry.getValue(); + + // Only add if not already set by system environment + if (System.getenv(key) == null) { + envMap.put(key, value); + log.debug("Loaded from .env: {}", key); + } + }); + + if (!envMap.isEmpty()) { + environment.getPropertySources().addFirst(new MapPropertySource("dotenv", envMap)); + log.info("Environment variables loaded from .env file: {}", envMap.keySet()); + } + + } catch (Exception e) { + log.error("Could not load .env file", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/couchbase/quickstart/springboot/configs/CouchbaseConfig.java b/src/main/java/org/couchbase/quickstart/springboot/configs/CouchbaseConfig.java index 44dadea..7fc1361 100644 --- a/src/main/java/org/couchbase/quickstart/springboot/configs/CouchbaseConfig.java +++ b/src/main/java/org/couchbase/quickstart/springboot/configs/CouchbaseConfig.java @@ -21,13 +21,13 @@ public class CouchbaseConfig { private static final Logger log = LoggerFactory.getLogger(CouchbaseConfig.class); - @Value("#{systemEnvironment['DB_CONN_STR'] ?: '${spring.couchbase.bootstrap-hosts:localhost}'}") + @Value("#{systemEnvironment['DB_CONN_STR'] ?: '${spring.couchbase.connection-string:localhost}'}") private String host; - @Value("#{systemEnvironment['DB_USERNAME'] ?: '${spring.couchbase.bucket.user:Administrator}'}") + @Value("#{systemEnvironment['DB_USERNAME'] ?: '${spring.couchbase.username:Administrator}'}") private String username; - @Value("#{systemEnvironment['DB_PASSWORD'] ?: '${spring.couchbase.bucket.password:password}'}") + @Value("#{systemEnvironment['DB_PASSWORD'] ?: '${spring.couchbase.password:password}'}") private String password; @Value("${spring.couchbase.bucket.name:travel-sample}") @@ -51,14 +51,15 @@ public class CouchbaseConfig { */ @Bean(destroyMethod = "disconnect") Cluster getCouchbaseCluster() { + Cluster cluster = null; try { log.debug("Connecting to Couchbase cluster at " + host); - Cluster cluster = Cluster.connect(host, username, password); - cluster.waitUntilReady(Duration.ofSeconds(15)); + cluster = Cluster.connect(host, username, password); + cluster.waitUntilReady(Duration.ofSeconds(30)); return cluster; } catch (UnambiguousTimeoutException e) { - log.error("Connection to Couchbase cluster at " + host + " timed out"); - throw e; + log.warn("Connection to Couchbase cluster at " + host + " timed out, but continuing with partial connectivity"); + return cluster; } catch (Exception e) { log.error(e.getClass().getName()); log.error("Could not connect to Couchbase cluster at " + host); @@ -75,7 +76,7 @@ Bucket getCouchbaseBucket(Cluster cluster) { throw new BucketNotFoundException("Bucket " + bucketName + " does not exist"); } Bucket bucket = cluster.bucket(bucketName); - bucket.waitUntilReady(Duration.ofSeconds(15)); + bucket.waitUntilReady(Duration.ofSeconds(30)); return bucket; } catch (UnambiguousTimeoutException e) { log.error("Connection to bucket " + bucketName + " timed out"); diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d864715 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.context.ApplicationContextInitializer=org.couchbase.quickstart.springboot.config.DotEnvConfiguration \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26b8195..1fbcf40 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,13 @@ +# Spring MVC configuration spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER + +# Modern Couchbase configuration (Spring Boot 3.5+) +spring.couchbase.connection-string=${DB_CONN_STR} +spring.couchbase.username=${DB_USERNAME} +spring.couchbase.password=${DB_PASSWORD} spring.couchbase.bucket.name=travel-sample -spring.couchbase.bucket.user=DB_USERNAME -spring.couchbase.bootstrap-hosts=DB_CONN_STR -spring.couchbase.bucket.password=DB_PASSWORD \ No newline at end of file + +# Couchbase connection optimizations +spring.couchbase.env.timeouts.query=30000ms +spring.couchbase.env.timeouts.key-value=5000ms +spring.couchbase.env.timeouts.connect=10000ms \ No newline at end of file diff --git a/src/test/java/org/couchbase/quickstart/springboot/controllers/AirlineIntegrationTest.java b/src/test/java/org/couchbase/quickstart/springboot/controllers/AirlineIntegrationTest.java index 44b480f..b630125 100644 --- a/src/test/java/org/couchbase/quickstart/springboot/controllers/AirlineIntegrationTest.java +++ b/src/test/java/org/couchbase/quickstart/springboot/controllers/AirlineIntegrationTest.java @@ -49,7 +49,7 @@ private void deleteAirline(String airlineId, String cleanupTiming) { } catch (DocumentNotFoundException | DataRetrievalFailureException | ResourceAccessException e) { log.warn("Document " + airlineId + " not present " + cleanupTiming); } catch (Exception e) { - log.error("Error deleting test data", e.getMessage()); + log.debug("Cleanup: Could not delete test airline {}: {} (this is expected during test cleanup)", airlineId, e.getMessage()); } } diff --git a/src/test/java/org/couchbase/quickstart/springboot/controllers/AirportIntegrationTest.java b/src/test/java/org/couchbase/quickstart/springboot/controllers/AirportIntegrationTest.java index 2bebba1..8814487 100644 --- a/src/test/java/org/couchbase/quickstart/springboot/controllers/AirportIntegrationTest.java +++ b/src/test/java/org/couchbase/quickstart/springboot/controllers/AirportIntegrationTest.java @@ -50,7 +50,7 @@ private void deleteAirport(String airportId, String cleanupTiming) { } catch (DocumentNotFoundException | DataRetrievalFailureException | ResourceAccessException e) { log.warn("Document " + airportId + " not present " + cleanupTiming); } catch (Exception e) { - log.error("Error deleting test data", e.getMessage()); + log.debug("Cleanup: Could not delete test airport {}: {} (this is expected during test cleanup)", airportId, e.getMessage()); } } diff --git a/src/test/java/org/couchbase/quickstart/springboot/controllers/RouteIntegrationTest.java b/src/test/java/org/couchbase/quickstart/springboot/controllers/RouteIntegrationTest.java index 1291528..e9973c5 100644 --- a/src/test/java/org/couchbase/quickstart/springboot/controllers/RouteIntegrationTest.java +++ b/src/test/java/org/couchbase/quickstart/springboot/controllers/RouteIntegrationTest.java @@ -47,7 +47,7 @@ private void deleteRoute(String routeId, String cleanupTiming) { } catch (DocumentNotFoundException | DataRetrievalFailureException | ResourceAccessException e) { log.warn("Document " + routeId + " not present " + cleanupTiming); } catch (Exception e) { - log.error("Error deleting test data", e.getMessage()); + log.debug("Cleanup: Could not delete test route {}: {} (this is expected during test cleanup)", routeId, e.getMessage()); } } diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..16fea80 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,48 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file