Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ jobs:
build-n-test:
runs-on: ubuntu-latest

permissions:
contents: read
checks: write
pull-requests: write

steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -49,7 +54,7 @@ jobs:
env:
ACCESS_KEY: ${{ secrets.DEV_CA_PUBLIC_USE_INTEGRATION_TEST_ACCESS_KEY }}
SERVICE_PRINCIPAL_KEY: ${{ secrets.DEV_CA_PUBLIC_USE_TESTOAUTHSERVICEPRINCIPAL_SERVICE_PRINCIPAL_KEY }}
REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_2 }}
REPOSITORY_ID: ${{ secrets.DEV_CA_PUBLIC_USE_REPOSITORY_ID_1 }}
AUTHORIZATION_TYPE: ${{ secrets.AUTHORIZATION_TYPE }}
TEST_HEADER: ${{ secrets.TEST_HEADER }}
READONLY_TEST_FOLDER_ID: ${{ secrets.DEV_CA_READONLY_TEST_FOLDER_ID }}
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.0.4

### Chore & Maintenance

- Update `jackson-databind` dependency due to vulnerability

## 1.0.3

### Chore & Maintenance
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<artifactId>lf-repository-api-client-v2</artifactId>
<packaging>jar</packaging>
<name>Laserfiche Repository API Client V2</name>
<version>1.0.0</version>
<version>1.0.4</version>
<url>https://github.com/Laserfiche/lf-repository-api-client-java</url>
<description>The Java Laserfiche Repository API Client library for accessing the v2 Laserfiche Repository APIs.</description>
<scm>
Expand Down Expand Up @@ -309,7 +309,7 @@
<maven.compiler.target>${java.version}</maven.compiler.target>
<gson-fire-version>1.8.0</gson-fire-version>
<swagger-core-version>2.0.0</swagger-core-version>
<jackson-version>2.18.2</jackson-version>
<jackson-version>2.18.8</jackson-version>
<retrofit-version>2.9.0</retrofit-version>
<threetenbp-version>1.3.5</threetenbp-version>
<oltu-version>1.0.2</oltu-version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ public static <TResponse> TResponse sendRequestWithRetry(
HttpResponse<Object> httpResponse = null;
while (retryCount <= maxRetries && shouldRetry) {
try {
if (retryCount > 0) {
System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " starting for " + requestMethod + " " + url);
}
String requestUrl =
ApiClientUtils.beforeSend(url, headerParametersWithStringTypeValue, httpRequestHandler);
final HttpRequestWithBody httpRequestWithBody = httpClient.request(requestMethod, requestUrl);
Expand Down Expand Up @@ -212,10 +215,17 @@ public static <TResponse> TResponse sendRequestWithRetry(
int statusCode = httpResponse.getStatus();
shouldRetry = httpRequestHandler.afterSend(new ResponseImpl((short) statusCode))
|| ApiClientUtils.isRetryableStatusCode(statusCode, httpMethod);
if (statusCode == 401 || retryCount > 0 || shouldRetry) {
System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " " + requestMethod + " " + url
+ " -> status=" + statusCode + " shouldRetry=" + shouldRetry);
}
if (!shouldRetry) {
return parseResponse.apply(httpResponse);
}
} catch (Exception err) {
if (retryCount > 0 || err instanceof ApiException) {
System.out.println("[RETRY-DEBUG] attempt " + (retryCount + 1) + " threw " + err.getClass().getName() + ": " + err.getMessage());
}
if (err instanceof ApiException || retryCount >= maxRetries) {
throw err;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
package com.laserfiche.repository.api.integration;

import com.laserfiche.api.client.model.AccessKey;
import com.laserfiche.api.client.model.ApiException;
import com.laserfiche.api.client.model.ProblemDetails;
import com.laserfiche.repository.api.RepositoryApiClient;
import com.laserfiche.repository.api.RepositoryApiClientImpl;
import com.laserfiche.repository.api.clients.impl.model.*;
import com.laserfiche.repository.api.clients.params.ParametersForCreateEntry;
import com.laserfiche.repository.api.clients.params.ParametersForListAuditReasons;
import com.laserfiche.repository.api.clients.params.ParametersForListTasks;
import com.laserfiche.repository.api.clients.params.ParametersForStartDeleteEntry;
import io.github.cdimascio.dotenv.Dotenv;
Expand All @@ -24,6 +26,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

enum AuthorizationType {
CLOUD_ACCESS_KEY,
Expand Down Expand Up @@ -156,6 +159,11 @@ private static void waitUntilTaskEnds(String taskId, Duration interval) {
.setTaskIds(taskId));
TaskProgress progress = collectionResponse.getValue().get(0);
if (progress.getStatus() != TaskStatus.IN_PROGRESS) {
if (progress.getStatus() != TaskStatus.COMPLETED) {
throw new RuntimeException(String.format(
"Task %s ended with status %s instead of COMPLETED. Errors: %s",
taskId, progress.getStatus(), formatErrors(progress.getErrors())));
}
return;
}
try {
Expand All @@ -168,14 +176,68 @@ private static void waitUntilTaskEnds(String taskId, Duration interval) {
throw new RuntimeException("WaitUntilTaskEnds timeout");
}

// ProblemDetails doesn't override toString(), so printing the list directly just
// shows "ProblemDetails@<hash>" with none of the actual server-provided error info.
private static String formatErrors(List<ProblemDetails> errors) {
if (errors == null || errors.isEmpty()) {
return "none";
}
return errors.stream()
.map(e -> String.format(
"[title=%s, detail=%s, status=%s, errorCode=%s, errorSource=%s]",
e.getTitle(), e.getDetail(), e.getStatus(), e.getErrorCode(), e.getErrorSource()))
.collect(Collectors.joining(", "));
}

private static Integer deleteAuditReasonId;
private static boolean deleteAuditReasonResolved = false;

// This repository requires an audit reason for DeleteEntry (errorCode 216: "Need to
// provide correct audit reason for DeleteEntry"), unlike the old dedicated test repo.
// Resolved once and reused, mirroring ExportDocumentApiTest.findAuditReasonForExport.
protected static Integer getDeleteAuditReasonId() {
if (!deleteAuditReasonResolved) {
AuditReasonCollectionResponse auditReasons;
try {
auditReasons = repositoryApiClient
.getAuditReasonsClient()
.listAuditReasons(new ParametersForListAuditReasons().setRepositoryId(repositoryId));
} catch (ApiException e) {
// Don't cache "resolved" on failure — a transient hiccup here would otherwise
// permanently strand every later delete in the run with no audit reason, silently
// re-triggering "Need to provide correct audit reason for DeleteEntry" for the rest
// of the suite instead of just this one call.
throw new RuntimeException(String.format(
"listAuditReasons failed while resolving delete audit reason: statusCode=%d, headers=%s, problemDetails=%s",
e.getStatusCode(), e.getHeaders(), e.getProblemDetails()), e);
}
deleteAuditReasonId = auditReasons.getValue().stream()
.filter(reason -> reason.getAuditEventType() == AuditEventType.DELETE_ENTRY)
.map(AuditReason::getId)
.findFirst()
.orElse(null);
deleteAuditReasonResolved = true;
}
return deleteAuditReasonId;
}

protected static StartDeleteEntryRequest newDeleteEntryRequest() {
StartDeleteEntryRequest request = new StartDeleteEntryRequest();
Integer auditReasonId = getDeleteAuditReasonId();
if (auditReasonId != null) {
request.setAuditReasonId(auditReasonId);
}
return request;
}

public static void deleteEntry(int entryId) {
if (entryId != 0) {
StartTaskResponse startTaskResponse = repositoryApiClient
.getEntriesClient()
.startDeleteEntry(new ParametersForStartDeleteEntry()
.setRepositoryId(repositoryId)
.setEntryId(entryId)
.setRequestBody(new StartDeleteEntryRequest()));
.setRequestBody(newDeleteEntryRequest()));
waitUntilTaskEnds(startTaskResponse.getTaskId());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ void startDeleteEntryCanDeleteFolder() {
StartTaskResponse deleteEntryResponse = client.startDeleteEntry(new ParametersForStartDeleteEntry()
.setRepositoryId(repositoryId)
.setEntryId(entryToDelete.getId())
.setRequestBody(new StartDeleteEntryRequest()));
.setRequestBody(newDeleteEntryRequest()));
String taskId = deleteEntryResponse.getTaskId();
assertNotNull(taskId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ static void classCleanUp() {
deleteEntry(testEntryId);
}

// Wraps exportEntry so a failure's ApiException status code/headers/problem details
// (not just its message) show up in the surefire report, since we can't get the raw
// job log from this environment to see the actual HTTP response otherwise.
private ExportEntryResponse exportEntry(ParametersForExportEntry parameters) {
try {
return client.exportEntry(parameters);
} catch (ApiException e) {
throw new AssertionError(String.format(
"exportEntry failed: statusCode=%d, headers=%s, problemDetails=%s",
e.getStatusCode(), e.getHeaders(), e.getProblemDetails()), e);
}
}

@Test
void exportDocumentCanExportEDocPart() {
final String FILE_NAME = "exportDocument_temp_file.pdf";
Expand All @@ -116,7 +129,7 @@ void exportDocumentCanExportEDocPart() {
request.setAuditReasonId(auditReasonId);
request.setAuditReasonComment(auditReasonComment);
}
ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry()
ExportEntryResponse response = exportEntry(new ParametersForExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(testEntryId)
.setRequestBody(request));
Expand All @@ -137,7 +150,7 @@ void exportDocumentCanExportTextPart() {
request.setAuditReasonId(auditReasonId);
request.setAuditReasonComment(auditReasonComment);
}
ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry()
ExportEntryResponse response = exportEntry(new ParametersForExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(testEntryId)
.setRequestBody(request));
Expand Down Expand Up @@ -188,7 +201,7 @@ void exportDocumentCanExportPagesAsSinglePageTIFF() {
request.setAuditReasonId(auditReasonId);
request.setAuditReasonComment(auditReasonComment);
}
ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry()
ExportEntryResponse response = exportEntry(new ParametersForExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(testEntryId)
.setRequestBody(request));
Expand Down Expand Up @@ -217,7 +230,7 @@ void exportDocumentCanExportPagesAsMultiPageTIFF() {
request.setAuditReasonId(auditReasonId);
request.setAuditReasonComment(auditReasonComment);
}
ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry()
ExportEntryResponse response = exportEntry(new ParametersForExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(testEntryId)
.setRequestBody(request));
Expand All @@ -244,7 +257,7 @@ void exportDocumentCanExportPagesAsJPEG() {
request.setAuditReasonId(auditReasonId);
request.setAuditReasonComment(auditReasonComment);
}
ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry()
ExportEntryResponse response = exportEntry(new ParametersForExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(testEntryId)
.setRequestBody(request));
Expand All @@ -271,7 +284,7 @@ void exportDocumentCanExportPagesAsJPEGWithPageRange() {
request.setAuditReasonId(auditReasonId);
request.setAuditReasonComment(auditReasonComment);
}
ExportEntryResponse response = client.exportEntry(new ParametersForExportEntry()
ExportEntryResponse response = exportEntry(new ParametersForExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(testEntryId)
.setPageRange("1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.
package com.laserfiche.repository.api.integration;

import com.laserfiche.api.client.model.ApiException;
import com.laserfiche.repository.api.clients.EntriesClient;
import com.laserfiche.repository.api.clients.TasksClient;
import com.laserfiche.repository.api.clients.impl.model.*;
Expand Down Expand Up @@ -59,6 +60,19 @@ static void classCleanUp() {
deleteEntry(testClassParentFolder.getId());
}

// Surfaces the real HTTP status code/headers on failure instead of just the generic
// message, so an "Invalid or expired access token" error tells us whether it was
// actually a 401 (and thus in principle retryable) or something else entirely.
private static <T> T call(String callName, java.util.function.Supplier<T> apiCall) {
try {
return apiCall.get();
} catch (ApiException e) {
throw new AssertionError(String.format(
"%s failed: statusCode=%d, headers=%s, problemDetails=%s",
callName, e.getStatusCode(), e.getHeaders(), e.getProblemDetails()), e);
}
}

@Test
void createMultipartUploadUrlsCanBeCalledForSecondBatchOfURLs() {
String fileName = "Sample.pdf";
Expand Down Expand Up @@ -108,8 +122,8 @@ void startImportUploadedPartsCanImportLargeFileAndGeneratePages() {
requestBody.setMimeType(mimeType);
requestBody.setNumberOfParts(parts);

CreateMultipartUploadUrlsResponse response = client.createMultipartUploadUrls(new ParametersForCreateMultipartUploadUrls()
.setRepositoryId(repositoryId).setRequestBody(requestBody));
CreateMultipartUploadUrlsResponse response = call("createMultipartUploadUrls", () -> client.createMultipartUploadUrls(new ParametersForCreateMultipartUploadUrls()
.setRepositoryId(repositoryId).setRequestBody(requestBody)));

assertNotNull(response);
String uploadId = response.getUploadId();
Expand All @@ -130,17 +144,17 @@ void startImportUploadedPartsCanImportLargeFileAndGeneratePages() {
pdfOptions.setGeneratePages(true);
pdfOptions.setKeepPdfAfterImport(true);
requestBody2.setPdfOptions(pdfOptions);
StartTaskResponse response2 = client.startImportUploadedParts(new ParametersForStartImportUploadedParts()
StartTaskResponse response2 = call("startImportUploadedParts", () -> client.startImportUploadedParts(new ParametersForStartImportUploadedParts()
.setRepositoryId(repositoryId)
.setEntryId(testClassParentFolder.getId())
.setRequestBody(requestBody2));
.setRequestBody(requestBody2)));

assertNotNull(response2);
String taskId = response2.getTaskId();
assertNotNull(taskId);
waitUntilTaskEnds(taskId);

TaskCollectionResponse tasks = tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(taskId));
TaskCollectionResponse tasks = call("listTasks(import)", () -> tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(taskId)));
assertNotNull(tasks);
assertEquals(1, tasks.getValue().size());
TaskProgress taskProgress = tasks.getValue().get(0);
Expand All @@ -161,16 +175,16 @@ void startImportUploadedPartsCanImportLargeFileAndGeneratePages() {
exportRequestBody.setAuditReasonId(auditReasonId);
exportRequestBody.setAuditReasonComment(auditReasonComment);
}
StartTaskResponse exportResponse = client.startExportEntry(new ParametersForStartExportEntry()
StartTaskResponse exportResponse = call("startExportEntry", () -> client.startExportEntry(new ParametersForStartExportEntry()
.setRepositoryId(repositoryId)
.setEntryId(createdEntryId)
.setRequestBody(exportRequestBody));
.setRequestBody(exportRequestBody)));

assertNotNull(exportResponse);
taskId = exportResponse.getTaskId();
assertNotNull(taskId);
String exportTaskId = exportResponse.getTaskId();
assertNotNull(exportTaskId);

tasks = tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(taskId));
tasks = call("listTasks(export)", () -> tasksClient.listTasks(new ParametersForListTasks().setRepositoryId(repositoryId).setTaskIds(exportTaskId)));
assertNotNull(tasks);
assertEquals(1, tasks.getValue().size());
taskProgress = tasks.getValue().get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ void cancelTasksDoesNotReturnErrorWhenCancellingACompletedTask() {
.startDeleteEntry(new ParametersForStartDeleteEntry()
.setRepositoryId(repositoryId)
.setEntryId(deleteEntry.getId())
.setRequestBody(new StartDeleteEntryRequest()));
.setRequestBody(newDeleteEntryRequest()));
String taskId = result.getTaskId();
assertNotNull(taskId);

Expand Down Expand Up @@ -58,7 +58,7 @@ void listTasksWorksAndAcceptsMultipleTaskIdsAndCanBeCalledWithNoTaskIdsAndIgnore
.startDeleteEntry(new ParametersForStartDeleteEntry()
.setRepositoryId(repositoryId)
.setEntryId(entry.getId())
.setRequestBody(new StartDeleteEntryRequest()));
.setRequestBody(newDeleteEntryRequest()));
assertNotNull(startTaskResponse);
taskIds[i] = startTaskResponse.getTaskId();
}
Expand Down
Loading