diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c67541e..941c487 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -2,57 +2,57 @@ name: Deploy to AKS Cluster
on:
push:
branches:
- - master
+ - master
pull_request:
branches:
- - master
+ - master
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@master
- - name: Decrypt large secret
- run: ./scripts/decrypt.sh
- env:
- APPLICATION_PROPERTIES_PASSPHRASE: ${{ secrets.APPLICATION_PROPERTIES_PASSPHRASE }}
-
- - name: Set up JDK 1.8
- uses: actions/setup-java@v1
- with:
- java-version: 1.8
-
- - name: Include local jar
- run: mvn install:install-file -Dfile="lib/PageSuccess-0.0.1-SNAPSHOT.jar" -DgroupId="ca.gc.tbs" -DartifactId="PageSuccess" -Dversion="0.0.1-SNAPSHOT" -Dpackaging=jar -DgeneratePom=true
-
- - name: Include local jar
- run: mvn install:install-file -Dfile="lib/airtable.java-0.2.0.jar" -DgroupId="com.sybit" -DartifactId="airtable.java" -Dversion="0.2.0" -Dpackaging=jar -DgeneratePom=true
-
- - name: Build with Maven
- run: mvn install --file pom.xml
-
- - uses: Azure/docker-login@v1
- with:
- login-server: tbsacr.azurecr.io
- username: ${{ secrets.ACR_USERNAME }}
- password: ${{ secrets.ACR_PASSWORD }}
-
- - run: |
- docker build -f ./docker/Dockerfile . -t tbsacr.azurecr.io/feedback-cj:${{ github.sha }}
- docker push tbsacr.azurecr.io/feedback-cj:${{ github.sha }}
-
- # Set the target AKS cluster.
- - uses: Azure/aks-set-context@v1
- with:
- creds: '${{ secrets.AZURE_CREDENTIALS }}'
- cluster-name: tbs-prod-aks
- resource-group: tbs-prod-rg
-
- - uses: Azure/k8s-deploy@v1
- with:
- manifests: |
- kubernetes/feedback-cronjob.yml
- images: |
- tbsacr.azurecr.io/feedback-cj:${{ github.sha }}
- namespace: |
- pagesuccess
+ - uses: actions/checkout@master
+ - name: Decrypt large secret
+ run: ./scripts/decrypt.sh
+ env:
+ APPLICATION_PROPERTIES_PASSPHRASE: ${{ secrets.APPLICATION_PROPERTIES_PASSPHRASE }}
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: "17"
+ distribution: "temurin"
+
+ - name: Include local jar
+ run: mvn install:install-file -Dfile="lib/PageSuccess-0.0.1-SNAPSHOT.jar" -DgroupId="ca.gc.tbs" -DartifactId="PageSuccess" -Dversion="0.0.1-SNAPSHOT" -Dpackaging=jar -DgeneratePom=true
+
+ - name: Include local jar
+ run: mvn install:install-file -Dfile="lib/airtable.java-0.2.0.jar" -DgroupId="com.sybit" -DartifactId="airtable.java" -Dversion="0.2.0" -Dpackaging=jar -DgeneratePom=true
+
+ - name: Build with Maven
+ run: mvn install --file pom.xml
+
+ - uses: Azure/docker-login@v1
+ with:
+ login-server: tbsacr.azurecr.io
+ username: ${{ secrets.ACR_USERNAME }}
+ password: ${{ secrets.ACR_PASSWORD }}
+
+ - run: |
+ docker build -f ./docker/Dockerfile . -t tbsacr.azurecr.io/feedback-cj:${{ github.sha }}
+ docker push tbsacr.azurecr.io/feedback-cj:${{ github.sha }}
+
+ # Set the target AKS cluster.
+ - uses: Azure/aks-set-context@v1
+ with:
+ creds: "${{ secrets.AZURE_CREDENTIALS }}"
+ cluster-name: tbs-prod-aks
+ resource-group: tbs-prod-rg
+
+ - uses: Azure/k8s-deploy@v1
+ with:
+ manifests: |
+ kubernetes/feedback-cronjob.yml
+ images: |
+ tbsacr.azurecr.io/feedback-cj:${{ github.sha }}
+ namespace: |
+ pagesuccess
diff --git a/.gitignore b/.gitignore
index 28f2b49..8af135f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,10 @@ hs_err_pid*
**/StoredCredential
.DS_Store
+# Google Service Account Keys (plaintext - encrypted versions OK)
+**/service-account.json
+**/service-account.p12
+
pagefeedback-cj.iml
feedback-cj.iml
Feedback Tool.iml
diff --git a/docker/Dockerfile b/docker/Dockerfile
index d06101f..4a23180 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,6 +1,4 @@
-FROM maven:3.8.6-openjdk-8-slim
-RUN apt-get clean
-RUN apt-get update
+FROM eclipse-temurin:17-jre-alpine
RUN mkdir -p /app
COPY target/pagefeedback-cj-1.0.0-SNAPSHOT.jar /app/app.jar
ENV JAVA_OPTS="-Xmx2g"
diff --git a/lib/PageSuccess-0.0.1-SNAPSHOT.jar b/lib/PageSuccess-0.0.1-SNAPSHOT.jar
index f87118f..b3fa8e3 100644
Binary files a/lib/PageSuccess-0.0.1-SNAPSHOT.jar and b/lib/PageSuccess-0.0.1-SNAPSHOT.jar differ
diff --git a/pom.xml b/pom.xml
index 7489d7a..d24559f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,9 +10,14 @@
org.springframework.boot
spring-boot-starter-parent
- 2.2.1.RELEASE
+ 3.2.5
+
+ 17
+ 17
+ 17
+
@@ -50,36 +55,38 @@
commons-io
commons-io
- 2.11.0
+ 2.15.1
- org.apache.httpcomponents
- httpclient
- 4.5.13
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.4
org.json
json
- 20160810
+ 20240303
org.springframework.boot
spring-boot-starter
- org.slf4j
- slf4j-api
- 2.0.4
+ org.apache.commons
+ commons-lang3
+ 3.14.0
+
com.mashape.unirest
unirest-java
1.4.9
+
- org.apache.commons
- commons-lang3
- 3.12.0
+ org.glassfish.jaxb
+ jaxb-runtime
+ 2.3.9
com.sybit
@@ -91,10 +98,17 @@
PageSuccess
0.0.1-SNAPSHOT
+
com.google.apis
google-api-services-sheets
- v4-rev20210629-1.32.1
+ v4-rev20240826-2.0.0
+
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ 1.24.1
org.springframework.data
@@ -108,12 +122,12 @@
org.apache.commons
commons-csv
- 1.9.0
+ 1.11.0
org.jsoup
jsoup
- 1.15.3
+ 1.18.3
uk.gov.service.notify
diff --git a/scripts/decrypt.sh b/scripts/decrypt.sh
index 7f8a078..18756b9 100755
--- a/scripts/decrypt.sh
+++ b/scripts/decrypt.sh
@@ -3,8 +3,10 @@ export GPG_TTY=$(tty)
ls ./src/main/resources
+# Decrypt application properties
gpg --quiet --batch --yes --passphrase="$APPLICATION_PROPERTIES_PASSPHRASE" --output ./src/main/resources/application.properties --decrypt ./src/main/resources/application.properties.gpg
-gpg --quiet --batch --yes --passphrase="$APPLICATION_PROPERTIES_PASSPHRASE" --output ./src/main/resources/service-account.p12 --decrypt ./src/main/resources/service-account.p12.gpg
+# Decrypt Google service account JSON key (modern format)
+gpg --quiet --batch --yes --passphrase="$APPLICATION_PROPERTIES_PASSPHRASE" --output ./src/main/resources/service-account.json --decrypt ./src/main/resources/service-account.json.gpg
ls ./src/main/resources
\ No newline at end of file
diff --git a/src/main/java/ca/gc/tbs/AirTableMLTag.java b/src/main/java/ca/gc/tbs/AirTableMLTag.java
deleted file mode 100644
index 5e58ef8..0000000
--- a/src/main/java/ca/gc/tbs/AirTableMLTag.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package ca.gc.tbs;
-
-import com.google.gson.annotations.SerializedName;
-
-public class AirTableMLTag {
- private String id;
-
- @SerializedName("ML tags")
- private String tag;
-
- public AirTableMLTag(String tag) {
- this.tag = tag;
- }
-
- public AirTableMLTag() {
-
- }
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getTag() {
- return tag;
- }
-
- public void setTag(String tag) {
- this.tag = tag;
- }
-}
diff --git a/src/main/java/ca/gc/tbs/AirTableProblem.java b/src/main/java/ca/gc/tbs/AirTableProblem.java
deleted file mode 100644
index a59a923..0000000
--- a/src/main/java/ca/gc/tbs/AirTableProblem.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package ca.gc.tbs;
-import com.google.gson.annotations.SerializedName;
-
-public class AirTableProblem {
- private String id;
-
- @SerializedName("Unique ID")
- private String uniqueID;
- @SerializedName("Date")
- private String date;
- @SerializedName("Time received")
- private String timeStamp;
- @SerializedName("URL")
- private String URL;
- @SerializedName("Name")
- private String URL_link;
- @SerializedName("Page title")
- private String pageTitle;
- @SerializedName("Lang")
- private String lang;
- @SerializedName("What's wrong")
- private String whatswrong;
- @SerializedName("Details")
- private String details;
- @SerializedName("Tags")
- private String tags;
- @SerializedName("Info exists")
- private String infoExists;
- @SerializedName("PII")
- private String PII;
-
- @SerializedName("PII Type")
- private String PIIType;
-
-
- @SerializedName("Topic - HC")
- private String topic;
- @SerializedName("Actionable")
- private Boolean actionable;
-
-
-
-
-
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- public String getTimeStamp() {
- return timeStamp;
- }
- public void setTimeStamp(String timeStamp) {
- this.timeStamp = timeStamp;
- }
- public String getDate() {
- return date;
- }
- public void setDate(String date) {
- this.date = date;
- }
- public String getURL() {
- return URL;
- }
- public void setURL(String uRL) {
- URL = uRL;
- }
- public String getPageTitle() {
- return pageTitle;
- }
- public void setPageTitle(String pageTitle) {
- this.pageTitle = pageTitle;
- }
- public String getLang() {
- return lang;
- }
- public void setLang(String lang) {
- this.lang = lang;
- }
- public String getWhatswrong() {
- return whatswrong;
- }
- public void setWhatswrong(String whatswrong) {
- this.whatswrong = whatswrong;
- }
- public String getDetails() {
- return details;
- }
- public void setDetails(String details) {
- this.details = details;
- }
- public String getTags() {
- return tags;
- }
- public void setTags(String tags) {
- this.tags = tags;
- }
- public String getInfoExists() {
- return infoExists;
- }
- public void setInfoExists(String infoExists) {
- this.infoExists = infoExists;
- }
- public String getPII() {
- return PII;
- }
- public void setPII(String pII) {
- PII = pII;
- }
- public String getTopic() {
- return topic;
- }
- public void setTopic(String topic) {
- this.topic = topic;
- }
- public String getURL_link() {
- return URL_link;
- }
- public void setURL_link(String uRL_link) {
- URL_link = uRL_link;
- }
- public String getUniqueID() {
- return uniqueID;
- }
- public void setUniqueID(String uniqueID) {
- this.uniqueID = uniqueID;
- }
- public String getPIIType() {
- return PIIType;
- }
- public void setPIIType(String pIIType) {
- PIIType = pIIType;
- }
-
-
-}
diff --git a/src/main/java/ca/gc/tbs/AirTableStat.java b/src/main/java/ca/gc/tbs/AirTableStat.java
deleted file mode 100644
index 8ad986e..0000000
--- a/src/main/java/ca/gc/tbs/AirTableStat.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package ca.gc.tbs;
-
-import com.google.gson.annotations.SerializedName;
-
-public class AirTableStat {
- private String id;
-
- @SerializedName("Page title")
- private String pageTitle;
-
- public AirTableStat(String title) {
- this.pageTitle = title;
- }
-
- public AirTableStat() {
-
- }
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getPageTitle() {
- return pageTitle;
- }
-
- public void setPageTitle(String pageTitle) {
- this.pageTitle = pageTitle;
- }
-}
diff --git a/src/main/java/ca/gc/tbs/AirTableURLLink.java b/src/main/java/ca/gc/tbs/AirTableURLLink.java
deleted file mode 100644
index 19023ba..0000000
--- a/src/main/java/ca/gc/tbs/AirTableURLLink.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package ca.gc.tbs;
-
-import com.google.gson.annotations.SerializedName;
-
-public class AirTableURLLink {
- private String id;
-
- @SerializedName("Name")
- private String URLlink;
-
-
- public AirTableURLLink(String urlLink) {
- this.URLlink = urlLink;
- }
-
- public AirTableURLLink() {
-
- }
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
- public String getURLlink() {
- return URLlink;
- }
-
- public void setURLlink(String uRLlink) {
- URLlink = uRLlink;
- }
-
-
-}
diff --git a/src/main/java/ca/gc/tbs/GoogleSheetsAPI.java b/src/main/java/ca/gc/tbs/GoogleSheetsAPI.java
index 20c4fd0..a4650ed 100644
--- a/src/main/java/ca/gc/tbs/GoogleSheetsAPI.java
+++ b/src/main/java/ca/gc/tbs/GoogleSheetsAPI.java
@@ -1,6 +1,5 @@
package ca.gc.tbs;
-import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
@@ -9,98 +8,195 @@
import com.google.api.services.sheets.v4.SheetsScopes;
import com.google.api.services.sheets.v4.model.AppendValuesResponse;
import com.google.api.services.sheets.v4.model.ValueRange;
+import com.google.auth.http.HttpCredentialsAdapter;
+import com.google.auth.oauth2.GoogleCredentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.io.InputStream;
import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+/**
+ * Google Sheets API client for appending feedback data.
+ * Uses modern GoogleCredentials with JSON key file and implements credential caching,
+ * retry logic, and thread-safe operations.
+ */
public class GoogleSheetsAPI {
+ private static final Logger logger = LoggerFactory.getLogger(GoogleSheetsAPI.class);
- static final String spreadsheetId = "1B16qEbfp7SFCfIsZ8fcj7DneCy1WkR0GPh4t9L9NRSg";
- static final String duplicateCommentsSpreadsheetId = "1cR2mih5sBwl3wUjniwdyVA0xZcqV2Wl9yhghJfMG5oM"; // Template ID to
- // be replaced
- static final String range = "A1:A50000";
- private static final String APPLICATION_NAME = "My Google Sheets Application";
+ // TODO: Externalize these to application.properties
+ static final String SPREADSHEET_ID = "1B16qEbfp7SFCfIsZ8fcj7DneCy1WkR0GPh4t9L9NRSg";
+ static final String DUPLICATE_COMMENTS_SPREADSHEET_ID = "1cR2mih5sBwl3wUjniwdyVA0xZcqV2Wl9yhghJfMG5oM";
+ static final String URL_RANGE = "A1:A50000";
+ static final String DUPLICATE_RANGE = "A1:D50000";
+
+ private static final String APPLICATION_NAME = "Page Feedback CronJob";
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
- private static final String SERVICE_ACCOUNT_EMAIL = "cronjob@feedback-cj.iam.gserviceaccount.com";
+ private static final String SERVICE_ACCOUNT_KEY_FILE = "service-account.json";
+
+ // Retry configuration
+ private static final int MAX_RETRY_ATTEMPTS = 3;
+ private static final long INITIAL_RETRY_DELAY_MS = 1000;
+
+ // Cached Sheets service instance (thread-safe lazy initialization)
+ private static volatile Sheets sheetsService;
+ private static final Object lock = new Object();
+
/**
- * Global instance of the HTTP transport.
+ * Gets or creates a cached Sheets service instance.
+ * Thread-safe singleton pattern with double-checked locking.
+ *
+ * @return Sheets service instance
+ * @throws IOException if service account key file cannot be read
+ * @throws GeneralSecurityException if HTTP transport cannot be created
*/
- private static NetHttpTransport HTTP_TRANSPORT;
+ private static Sheets getSheetsService() throws IOException, GeneralSecurityException {
+ if (sheetsService == null) {
+ synchronized (lock) {
+ if (sheetsService == null) {
+ logger.debug("Initializing Google Sheets service");
+ sheetsService = createSheetsService();
+ }
+ }
+ }
+ return sheetsService;
+ }
- public static void appendURL(String url) throws GeneralSecurityException, IOException {
- KeyStore keystore = KeyStore.getInstance("PKCS12");
- keystore.load(GoogleSheetsAPI.class.getClassLoader().getResourceAsStream("service-account.p12"),
- "notasecret".toCharArray());
- PrivateKey pk = (PrivateKey) keystore.getKey("privatekey", "notasecret".toCharArray());
+ /**
+ * Creates a new Sheets service instance with modern GoogleCredentials.
+ *
+ * @return configured Sheets service
+ * @throws IOException if service account key file cannot be read
+ * @throws GeneralSecurityException if HTTP transport cannot be created
+ */
+ private static Sheets createSheetsService() throws IOException, GeneralSecurityException {
+ NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
- final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
+ GoogleCredentials credentials;
+ try (InputStream keyStream = GoogleSheetsAPI.class.getClassLoader()
+ .getResourceAsStream(SERVICE_ACCOUNT_KEY_FILE)) {
- GoogleCredential credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
- .setJsonFactory(JSON_FACTORY)
- .setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
- .setServiceAccountScopes(Collections.singleton(SheetsScopes.SPREADSHEETS))
- .setServiceAccountPrivateKey(pk)
- .build();
+ if (keyStream == null) {
+ throw new IOException("Service account key file not found: " + SERVICE_ACCOUNT_KEY_FILE);
+ }
+
+ credentials = GoogleCredentials.fromStream(keyStream)
+ .createScoped(Collections.singleton(SheetsScopes.SPREADSHEETS));
+ }
- Sheets service = new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
+ return new Sheets.Builder(httpTransport, JSON_FACTORY, new HttpCredentialsAdapter(credentials))
.setApplicationName(APPLICATION_NAME)
.build();
+ }
- ValueRange appendBody = new ValueRange()
- .setValues(Arrays.asList(
- Arrays.asList(url)));
- try {
- AppendValuesResponse appendResult = service.spreadsheets().values()
- .append(spreadsheetId, range, appendBody)
- .setValueInputOption("USER_ENTERED")
- .setInsertDataOption("INSERT_ROWS")
- .setIncludeValuesInResponse(true)
- .execute();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ /**
+ * Appends a URL to the main feedback spreadsheet with retry logic.
+ *
+ * @param url the URL to append
+ * @throws IOException if all retry attempts fail
+ * @throws GeneralSecurityException if unable to create HTTP transport
+ */
+ public static void appendURL(String url) throws IOException, GeneralSecurityException {
+ logger.debug("Appending URL to spreadsheet: {}", url);
+ appendValues(SPREADSHEET_ID, URL_RANGE, Collections.singletonList(url));
}
+ /**
+ * Appends duplicate comment data to the duplicate comments spreadsheet with retry logic.
+ *
+ * @param date the date of the comment
+ * @param timestamp the timestamp of the comment
+ * @param url the URL associated with the comment
+ * @param comment the comment text
+ * @throws IOException if all retry attempts fail
+ * @throws GeneralSecurityException if unable to create HTTP transport
+ */
public static void appendDuplicateComment(String date, String timestamp, String url, String comment)
- throws GeneralSecurityException, IOException {
- KeyStore keystore = KeyStore.getInstance("PKCS12");
- keystore.load(GoogleSheetsAPI.class.getClassLoader().getResourceAsStream("service-account.p12"),
- "notasecret".toCharArray());
- PrivateKey pk = (PrivateKey) keystore.getKey("privatekey", "notasecret".toCharArray());
-
- final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
-
- GoogleCredential credential = new GoogleCredential.Builder().setTransport(HTTP_TRANSPORT)
- .setJsonFactory(JSON_FACTORY)
- .setServiceAccountId(SERVICE_ACCOUNT_EMAIL)
- .setServiceAccountScopes(Collections.singleton(SheetsScopes.SPREADSHEETS))
- .setServiceAccountPrivateKey(pk)
- .build();
+ throws IOException, GeneralSecurityException {
+ logger.debug("Appending duplicate comment - Date: {}, URL: {}", date, url);
+ appendValues(DUPLICATE_COMMENTS_SPREADSHEET_ID, DUPLICATE_RANGE,
+ Arrays.asList(date, timestamp, url, comment));
+ }
- Sheets service = new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
- .setApplicationName(APPLICATION_NAME)
- .build();
+ /**
+ * Generic method to append values to a spreadsheet with exponential backoff retry.
+ *
+ * @param spreadsheetId the ID of the target spreadsheet
+ * @param range the A1 notation range
+ * @param values the values to append
+ * @throws IOException if all retry attempts fail
+ * @throws GeneralSecurityException if unable to create HTTP transport
+ */
+ private static void appendValues(String spreadsheetId, String range, List