ids = entries.stream().map(PrintEntry::getId).toList();
+ storage.deleteEntries(ids);
}
}
}
diff --git a/src/main/java/org/folio/print/server/service/PdfService.java b/src/main/java/org/folio/print/server/service/PdfService.java
index b08f34e..dfa9d2e 100644
--- a/src/main/java/org/folio/print/server/service/PdfService.java
+++ b/src/main/java/org/folio/print/server/service/PdfService.java
@@ -1,12 +1,12 @@
package org.folio.print.server.service;
import com.lowagie.text.DocumentException;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.apache.pdfbox.io.RandomAccessReadBuffer;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@@ -30,8 +30,7 @@ public static byte[] createPdfFile(String htmlContent) {
if (htmlContent != null && !htmlContent.isBlank()) {
try (PDDocument document = new PDDocument();
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
- htmlContent = "" + htmlContent + "
";
- htmlContent = htmlContent.replace("
", "
");
+ htmlContent = cleanHtmlData(htmlContent);
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
ITextRenderer renderer = new ITextRenderer();
@@ -46,6 +45,24 @@ public static byte[] createPdfFile(String htmlContent) {
return new byte[0];
}
+ private static String cleanHtmlData(String htmlContent) {
+ return "" + htmlContent
+ .replace("
", "
")
+ .replace(" ", " ")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("&", "&")
+ .replace(""", """)
+ .replace("'", "'")
+ .replace("–", "–")
+ .replace("—", "—")
+ .replace("©", "©")
+ .replace("®", "®")
+ .replace(" ", " ")
+ .replace("™", "™")
+ + "
";
+ }
+
/**
* Combine single print entries in batch print file.
* @param entries Entries to combine
@@ -57,7 +74,11 @@ public static byte[] combinePdfFiles(List entries) {
PDFMergerUtility pdfMerger = new PDFMergerUtility();
entries.forEach(e -> {
if (e.getContent() != null && !e.getContent().isBlank()) {
- pdfMerger.addSource(new RandomAccessReadBuffer(Hex.decodeHex(e.getContent())));
+ try {
+ pdfMerger.addSource(new ByteArrayInputStream(Hex.decodeHex(e.getContent())));
+ } catch (IOException ex) {
+ LOGGER.error("Failed to merge entry: " + e.getId(), ex);
+ }
}
});
pdfMerger.setDestinationStream(mergedOutputStream);
diff --git a/src/main/java/org/folio/print/server/service/PrintService.java b/src/main/java/org/folio/print/server/service/PrintService.java
index d0807ea..8aa1fd9 100644
--- a/src/main/java/org/folio/print/server/service/PrintService.java
+++ b/src/main/java/org/folio/print/server/service/PrintService.java
@@ -13,6 +13,8 @@
import io.vertx.ext.web.validation.ValidationHandler;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.List;
import java.util.UUID;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -76,7 +78,12 @@ private void handlers(RouterBuilder routerBuilder) {
.onFailure(cause -> commonError(ctx, cause))
)
.failureHandler(this::failureHandler);
-
+ routerBuilder
+ .operation("deletePrintEntries")
+ .handler(ctx -> deletePrintEntries(ctx)
+ .onFailure(cause -> commonError(ctx, cause))
+ )
+ .failureHandler(this::failureHandler);
routerBuilder
.operation("postPrintEntry")
.handler(ctx -> postPrintEntry(ctx)
@@ -128,10 +135,13 @@ public static PrintStorage create(RoutingContext ctx) {
}
Future postPrintEntry(RoutingContext ctx) {
+ log.info("postPrintEntry:: create entry");
PrintStorage storage = create(ctx);
RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY);
RequestParameter body = params.body();
PrintEntry entry = body.getJsonObject().mapTo(PrintEntry.class);
+ log.info("postPrintEntry:: entry with type {}, sorting field{}",
+ entry.getType(), entry.getSortingField());
return storage.createEntry(entry)
.map(entity -> {
ctx.response().setStatusCode(204);
@@ -140,7 +150,26 @@ Future postPrintEntry(RoutingContext ctx) {
});
}
+ Future deletePrintEntries(RoutingContext ctx) {
+ PrintStorage storage = create(ctx);
+ RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY);
+ RequestParameter idsParameter = params.queryParameter("ids");
+ String ids = idsParameter != null ? idsParameter.getString() : "";
+ log.info("deletePrintEntries:: delete entries by ids: {}", ids);
+ List uuids = Arrays.stream(ids.split(","))
+ .filter(id -> id != null && !id.isBlank())
+ .map(UUID::fromString)
+ .toList();
+ return storage.deleteEntries(uuids)
+ .map(r -> {
+ ctx.response().setStatusCode(204);
+ ctx.response().end();
+ return null;
+ });
+ }
+
Future saveMail(RoutingContext ctx) {
+ log.info("saveMail:: create single entry");
final PrintStorage storage = create(ctx);
RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY);
RequestParameter body = params.body();
@@ -151,6 +180,8 @@ Future saveMail(RoutingContext ctx) {
entry.setCreated(ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC));
entry.setSortingField(message.getTo());
entry.setContent(Hex.getString(PdfService.createPdfFile(message.getBody())));
+ log.info("saveMail:: entry with type {}, sorting field{}",
+ entry.getType(), entry.getSortingField());
return storage.createEntry(entry)
.map(entity -> {
ctx.response().setStatusCode(HttpResponseStatus.OK.code());
@@ -163,6 +194,7 @@ Future getPrintEntry(RoutingContext ctx) {
PrintStorage storage = create(ctx);
RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY);
String id = params.pathParameter("id").getString();
+ log.info("getPrintEntry:: get single entry by id: {}", id);
return storage.getEntry(UUID.fromString(id))
.map(entity -> {
HttpResponse.responseJson(ctx, 200)
@@ -175,6 +207,7 @@ Future deletePrintEntry(RoutingContext ctx) {
PrintStorage printStorage = create(ctx);
RequestParameters params = ctx.get(ValidationHandler.REQUEST_CONTEXT_KEY);
String id = params.pathParameter("id").getString();
+ log.info("deletePrintEntry:: delete single entry by id: {}", id);
return printStorage.deleteEntry(UUID.fromString(id))
.map(res -> {
ctx.response().setStatusCode(204);
@@ -189,6 +222,7 @@ Future updatePrintEntry(RoutingContext ctx) {
RequestParameter body = params.body();
PrintEntry entry = body.getJsonObject().mapTo(PrintEntry.class);
UUID id = UUID.fromString(params.pathParameter("id").getString());
+ log.info("updatePrintEntry:: update single entry by id: {}", id);
if (!id.equals(entry.getId())) {
return Future.failedFuture(new EntryException("id mismatch"));
}
@@ -207,6 +241,8 @@ Future getPrintEntries(RoutingContext ctx) {
String query = queryParameter != null ? queryParameter.getString() : null;
int limit = params.queryParameter("limit").getInteger();
int offset = params.queryParameter("offset").getInteger();
+ log.info("getPrintEntries:: get entries by query: {}, limit {}, offset {}",
+ query, limit, offset);
return storage.getEntries(ctx.response(), query, offset, limit);
}
diff --git a/src/main/java/org/folio/print/server/storage/PrintStorage.java b/src/main/java/org/folio/print/server/storage/PrintStorage.java
index 1b0df2e..5b20537 100644
--- a/src/main/java/org/folio/print/server/storage/PrintStorage.java
+++ b/src/main/java/org/folio/print/server/storage/PrintStorage.java
@@ -39,6 +39,7 @@ public class PrintStorage {
private static final String CREATE_IF_NO_EXISTS = "CREATE TABLE IF NOT EXISTS ";
private static final String WHERE_BY_ID = " WHERE id = $1";
+ private static final String WHERE_BY_IDS = " WHERE id in (%s)";
private final TenantPgPool pool;
@@ -167,6 +168,29 @@ public Future deleteEntry(UUID id) {
});
}
+ /**
+ * Delete print entries by ID list.
+ *
+ * @param uuids entry identifiers
+ * @return async result; exception if not found or forbidden
+ */
+ public Future deleteEntries(List uuids) {
+ if (uuids.isEmpty()) {
+ return Future.succeededFuture();
+ }
+ Tuple tuple = Tuple.tuple();
+ StringBuilder replacement = new StringBuilder();
+ int counter = 1;
+ for (UUID id : uuids) {
+ tuple.addUUID(id);
+ replacement.append((replacement.isEmpty()) ? "$" + counter++ : ", $" + counter++);
+ }
+ return pool.preparedQuery(
+ "DELETE FROM " + printTable + String.format(WHERE_BY_IDS, replacement))
+ .execute(tuple)
+ .map(res -> null);
+ }
+
/**
* Update print entry.
*
diff --git a/src/main/resources/openapi/batchPrint.yaml b/src/main/resources/openapi/batchPrint.yaml
index 591de1e..e85dd5e 100644
--- a/src/main/resources/openapi/batchPrint.yaml
+++ b/src/main/resources/openapi/batchPrint.yaml
@@ -10,13 +10,14 @@ paths:
- $ref: headers/okapi-token.yaml
- $ref: headers/okapi-url.yaml
- $ref: headers/okapi-user.yaml
- - $ref: parameters/limit.yaml
- - $ref: parameters/offset.yaml
- - $ref: parameters/query.yaml
get:
description: >
Get batch printing entries with optional CQL query.
- X-Okapi-Permissions must include mod-batch-print.print.read
+ X-Okapi-Permissions must include batch-print.entries.collection.get
+ parameters:
+ - $ref: parameters/limit.yaml
+ - $ref: parameters/offset.yaml
+ - $ref: parameters/query.yaml
operationId: getPrintEntries
responses:
"200":
@@ -31,10 +32,24 @@ paths:
$ref: "#/components/responses/trait_404"
"500":
$ref: "#/components/responses/trait_500"
+ delete:
+ description: >
+ Delete batch printing entries by comma separated IDs.
+ X-Okapi-Permissions must include batch-print.entries.collection.delete
+ parameters:
+ - $ref: parameters/ids.yaml
+ operationId: deletePrintEntries
+ responses:
+ "204":
+ description: Print entries deleted
+ "400":
+ $ref: "#/components/responses/trait_400"
+ "500":
+ $ref: "#/components/responses/trait_500"
post:
description: >
Create print entry.
- X-Okapi-Permissions must include mod-batch-print.print.write
+ X-Okapi-Permissions must include batch-print.entries.item.post
operationId: postPrintEntry
requestBody:
content:
@@ -69,7 +84,7 @@ paths:
get:
description: >
Get print entry by id.
- X-Okapi-Permissions must include mod-batch-print.print.read
+ X-Okapi-Permissions must include batch-print.entries.item.get
operationId: getPrintEntry
responses:
"200":
@@ -89,7 +104,7 @@ paths:
delete:
description: >
Delete print entry.
- X-Okapi-Permissions must include mod-batch-print.print.write
+ X-Okapi-Permissions must include batch-print.entries.item.delete
operationId: deletePrintEntry
responses:
"204":
@@ -103,7 +118,7 @@ paths:
put:
description: >
Update print entry.
- X-Okapi-Permissions must include mod-batch-print.print.write
+ X-Okapi-Permissions must include batch-print.entries.item.put
operationId: updatePrintEntry
requestBody:
content:
@@ -129,7 +144,7 @@ paths:
post:
description: >
Send mail to create print entry.
- X-Okapi-Permissions must include mod-batch-print.print.write
+ X-Okapi-Permissions must include batch-print.entries.mail.post
operationId: saveMail
requestBody:
content:
@@ -159,7 +174,7 @@ paths:
post:
description: >
Send mail to create print entry.
- X-Okapi-Permissions must include mod-batch-print.print.write
+ X-Okapi-Permissions must include batch-print.print.write
operationId: createBatch
responses:
"204":
diff --git a/src/main/resources/openapi/examples/listResponse.sample b/src/main/resources/openapi/examples/listResponse.sample
new file mode 100644
index 0000000..21922a9
--- /dev/null
+++ b/src/main/resources/openapi/examples/listResponse.sample
@@ -0,0 +1,20 @@
+{
+ "items": [
+ {
+ "id": "47c62bf9-e225-4a4b-bb61-dc6fc11eed76",
+ "created": "2023-12-01T13:46:40.193455Z",
+ "type": "SINGLE",
+ "sortingField": "user@mail.com"
+ },
+ {
+ "id": "80ceb8f2-eae4-4a82-8a7e-23ea9b971480",
+ "created": "2023-12-12T18:59:35.347757Z",
+ "type": "SINGLE",
+ "sortingField": "Saldabols,Janis,janis@indexdata.com"
+ }
+ ],
+ "resultInfo": {
+ "totalRecords": 2,
+ "diagnostics": []
+ }
+}
diff --git a/src/main/resources/openapi/examples/printEntry.sample b/src/main/resources/openapi/examples/printEntry.sample
new file mode 100644
index 0000000..d7845ba
--- /dev/null
+++ b/src/main/resources/openapi/examples/printEntry.sample
@@ -0,0 +1,7 @@
+{
+ "id": "47c62bf9-e225-4a4b-bb61-dc6fc11eed76",
+ "created": "2023-12-01T13:46:40.193455Z",
+ "type": "SINGLE",
+ "sortingField": "user@mail.com",
+ "content": "AABB"
+}
diff --git a/src/main/resources/openapi/examples/tenantAttributes.sample b/src/main/resources/openapi/examples/tenantAttributes.sample
deleted file mode 100644
index 3b92024..0000000
--- a/src/main/resources/openapi/examples/tenantAttributes.sample
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "module_to": "module-1.1",
- "module_from": "module-1.0",
- "parameters": [
- {"ref": "core"}
- ]
-}
diff --git a/src/main/resources/openapi/parameters/ids.yaml b/src/main/resources/openapi/parameters/ids.yaml
new file mode 100644
index 0000000..3962e31
--- /dev/null
+++ b/src/main/resources/openapi/parameters/ids.yaml
@@ -0,0 +1,6 @@
+in: query
+name: ids
+description: Comma seperated IDs of items
+required: true
+schema:
+ type: string
diff --git a/src/test/java/org/folio/print/server/main/MainVerticleTest.java b/src/test/java/org/folio/print/server/main/MainVerticleTest.java
index 5a0808f..aae989a 100644
--- a/src/test/java/org/folio/print/server/main/MainVerticleTest.java
+++ b/src/test/java/org/folio/print/server/main/MainVerticleTest.java
@@ -10,6 +10,8 @@
import java.io.InputStream;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.folio.okapi.common.XOkapiHeaders;
@@ -259,6 +261,7 @@ public void testNotFound() {
@Test
public void testGetPrintEntries() {
+ List ids = new ArrayList<>();
PrintEntry entry = new PrintEntry();
entry.setCreated(ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC));
entry.setType(PrintEntryType.SINGLE);
@@ -268,6 +271,7 @@ public void testGetPrintEntries() {
entry.setSortingField("A" + (5 - i));
entry.setType(i % 2 == 0 ? PrintEntryType.SINGLE : PrintEntryType.BATCH);
JsonObject en = JsonObject.mapFrom(entry);
+ ids.add(entry.getId().toString());
RestAssured.given()
.header(XOkapiHeaders.TENANT, TENANT_1)
.header(XOkapiHeaders.PERMISSIONS, permWrite.encode())
@@ -321,7 +325,6 @@ public void testGetPrintEntries() {
.body("items", hasSize(1))
.body("resultInfo.totalRecords", is(1));
-
RestAssured.given()
.header(XOkapiHeaders.TENANT, TENANT_1)
.header(XOkapiHeaders.PERMISSIONS, permRead.encode())
@@ -334,7 +337,6 @@ public void testGetPrintEntries() {
.body("items", hasSize(greaterThanOrEqualTo(2)))
.body("resultInfo.totalRecords", is(greaterThanOrEqualTo(2)));
-
RestAssured.given()
.header(XOkapiHeaders.TENANT, TENANT_1)
.header(XOkapiHeaders.PERMISSIONS, permRead.encode())
@@ -349,6 +351,79 @@ public void testGetPrintEntries() {
.body("resultInfo.totalRecords", is(greaterThanOrEqualTo(2)));
}
+ @Test
+ public void testDeletePrintEntries() {
+ List ids = new ArrayList<>();
+ PrintEntry entry = new PrintEntry();
+ entry.setCreated(ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC));
+ entry.setType(PrintEntryType.SINGLE);
+ for (int i = 0; i < 3; i++) {
+ entry.setId(UUID.randomUUID());
+ entry.setContent("A" + i);
+ entry.setSortingField("A" + (5 - i));
+ entry.setType(i % 2 == 0 ? PrintEntryType.SINGLE : PrintEntryType.BATCH);
+ JsonObject en = JsonObject.mapFrom(entry);
+ ids.add(entry.getId().toString());
+ RestAssured.given()
+ .header(XOkapiHeaders.TENANT, TENANT_1)
+ .header(XOkapiHeaders.PERMISSIONS, permWrite.encode())
+ .contentType(ContentType.JSON)
+ .body(en.encode())
+ .post("/print/entries")
+ .then()
+ .statusCode(204);
+ }
+
+ int count = RestAssured.given()
+ .header(XOkapiHeaders.TENANT, TENANT_1)
+ .header(XOkapiHeaders.PERMISSIONS, permRead.encode())
+ .queryParam("limit", "100")
+ .get("/print/entries")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body("resultInfo.totalRecords", is(greaterThanOrEqualTo(3)))
+ .extract()
+ .jsonPath()
+ .getInt("resultInfo.totalRecords");
+
+ RestAssured.given()
+ .header(XOkapiHeaders.TENANT, TENANT_1)
+ .header(XOkapiHeaders.PERMISSIONS, permRead.encode())
+ .queryParam("ids", "")
+ .delete("/print/entries")
+ .then()
+ .statusCode(204);
+
+ RestAssured.given()
+ .header(XOkapiHeaders.TENANT, TENANT_1)
+ .header(XOkapiHeaders.PERMISSIONS, permRead.encode())
+ .queryParam("limit", "100")
+ .get("/print/entries")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body("resultInfo.totalRecords", is(greaterThanOrEqualTo(count)));
+
+ RestAssured.given()
+ .header(XOkapiHeaders.TENANT, TENANT_1)
+ .header(XOkapiHeaders.PERMISSIONS, permRead.encode())
+ .queryParam("ids", String.join(",", ids))
+ .delete("/print/entries")
+ .then()
+ .statusCode(204);
+
+ RestAssured.given()
+ .header(XOkapiHeaders.TENANT, TENANT_1)
+ .header(XOkapiHeaders.PERMISSIONS, permRead.encode())
+ .queryParam("limit", "100")
+ .get("/print/entries")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .body("resultInfo.totalRecords", is(greaterThanOrEqualTo(count - ids.size())));
+ }
+
@Test
public void testSaveMailMessage() throws IOException {
String message = getResourceAsString("mail/mail.json");
diff --git a/src/test/java/org/folio/print/server/service/PdfServiceTest.java b/src/test/java/org/folio/print/server/service/PdfServiceTest.java
index 32478f8..b5902dc 100644
--- a/src/test/java/org/folio/print/server/service/PdfServiceTest.java
+++ b/src/test/java/org/folio/print/server/service/PdfServiceTest.java
@@ -14,7 +14,8 @@ public class PdfServiceTest {
@Test
public void createPdfFile(){
- byte[] result = PdfService.createPdfFile("
Content
");
+ byte[] result = PdfService.createPdfFile("
Content < > "
+ + "& " ' – — © ® ™
");
assertTrue(result.length > 0);
}