Added zip export

This commit is contained in:
verboomp
2026-02-23 12:18:41 +01:00
parent 888136e76b
commit e0c6a7db5a
17 changed files with 757 additions and 75 deletions

View File

@@ -226,7 +226,7 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${version.commons-io}</version>
<version>2.21.0</version>
</dependency>
<!-- Apache POI -->

View File

@@ -20,14 +20,16 @@ import jakarta.persistence.*;
@Entity
@Table(name = "questionnaire_customer")
@NamedQuery(name = QuestionnaireCustomer.FIND_ALL, query = "select c from QuestionnaireCustomer c order by c.name")
@NamedQuery(name = QuestionnaireCustomer.FIND_BY_NUMBER, query = "select c from QuestionnaireCustomer c where c.customerNumber = :customerNumber")
public class QuestionnaireCustomer extends AbstractDateEntity {
private static final long serialVersionUID = 1L;
public static final String SEQUENCE = "questionnaire_customer_seq";
public static final String FIND_ALL = "QuestionnaireCustomer.findAll";
public static final String FIND_BY_NUMBER = "QuestionnaireCustomer.findByNumber";
public static final String PARAM_NUMBER = "customerNumber";
@Id
@Column(name = "customer_id", length = 22)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)

View File

@@ -33,6 +33,14 @@ public class QueryService {
@PersistenceContext
private EntityManager eManager;
public <T> T callNamedQueryList(String namedQuery, Param... objects) {
Query query = eManager.createNamedQuery(namedQuery);
for (Param param : objects) {
query.setParameter(param.name(), param.value());
}
return (T) query.getResultList();
}
public <T> Optional<T> callNamedQuerySingleResult(String namedQuery, Param... params) {
return singleResult(eManager.createNamedQuery(namedQuery), Arrays.asList(params));
}

View File

@@ -5,6 +5,7 @@ import java.util.*;
import org.apache.commons.lang3.StringUtils;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.EJB;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
@@ -12,8 +13,10 @@ import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.*;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
import marketing.heyday.hartmann.fotodocumentation.core.query.QueryService;
import marketing.heyday.hartmann.fotodocumentation.core.utils.CalendarUtil;
import marketing.heyday.hartmann.fotodocumentation.core.utils.ExcelUtils;
import marketing.heyday.hartmann.fotodocumentation.core.utils.ZipExportUtils;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerListValue;
import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomerValue;
@@ -32,6 +35,12 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnaireCustomer
@PermitAll
public class QuestionnaireCustomerService extends AbstractService {
@EJB
private QueryService queryService;
@Inject
private ZipExportUtils zipExportUtils;
@Inject
private ExcelUtils excelUtils;
@@ -100,11 +109,16 @@ public class QuestionnaireCustomerService extends AbstractService {
return QuestionnaireCustomerValue.builder(customer);
}
public Optional<byte[]> getExport() {
List<QuestionnaireCustomer> customers = queryService.callNamedQueryList(QuestionnaireCustomer.FIND_ALL);
return zipExportUtils.getExport(customers);
}
public Optional<byte[]> getExport(Long id, Long questionnaireId) {
QuestionnaireCustomer customer = entityManager.find(QuestionnaireCustomer.class, id);
if (customer == null) {
return Optional.empty();
}
}
List<Questionnaire> questionnaires = customer.getQuestionnaires().stream().sorted((x, y) -> x.getQuestionnaireDate().compareTo(y.getQuestionnaireDate())).toList();

View File

@@ -38,7 +38,6 @@ public class ExcelUtils {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); XSSFWorkbook workbook = new XSSFWorkbook()) {
for (var questionnaire : questionnaires) {
//TODO: set sheet name
writeSheet(workbook, customer, questionnaire);
}

View File

@@ -0,0 +1,55 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import jakarta.inject.Inject;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
public class ZipExportUtils {
private static final Log LOG = LogFactory.getLog(ZipExportUtils.class);
@Inject
private ExcelUtils excelUtils;
public Optional<byte[]> getExport(List<QuestionnaireCustomer> customers) {
if (customers.isEmpty()) {
return Optional.empty();
}
try (ZipUtils zipUtils = new ZipUtils()) {
boolean hasContent = false;
for (var customer : customers) {
List<Questionnaire> questionnaires = customer.getQuestionnaires().stream().sorted((x, y) -> x.getQuestionnaireDate().compareTo(y.getQuestionnaireDate())).toList();
var file = excelUtils.create(customer, questionnaires);
if (file.isPresent()) {
zipUtils.addFile(customer.getName() + ".xlsx", file.get());
hasContent = true;
}
}
if (!hasContent) {
return Optional.empty();
}
return Optional.of(zipUtils.create());
} catch (IOException ioe) {
LOG.error("Failed to create zip file " + ioe.getMessage(), ioe);
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,41 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
public class ZipUtils implements AutoCloseable {
private ByteArrayOutputStream baos = new ByteArrayOutputStream();
private ZipOutputStream zos = new ZipOutputStream(baos);
public void addFile(String name, byte[] file) throws IOException {
ZipEntry entry = new ZipEntry(name);
zos.putNextEntry(entry);
zos.write(file);
zos.closeEntry();
}
public byte[] create() throws IOException {
zos.flush();
zos.close();
return baos.toByteArray();
}
@Override
public void close() throws IOException {
zos.close();
baos.close();
}
}

View File

@@ -65,6 +65,28 @@ public class QuestionnaireCustomerResource {
var retVal = questionnaireCustomerService.get(id);
return Response.ok().entity(retVal).build();
}
@GZIP
@GET
@Path("exportall")
@Produces("application/zip")
@Operation(summary = "Get Export")
@ApiResponse(responseCode = "200", description = "Successfully retrieved export")
public Response doExport() {
LOG.debug("Create export for customer ");
Optional<byte[]> pdfOpt = questionnaireCustomerService.getExport();
if (pdfOpt.isEmpty()) {
return Response.status(Status.NOT_FOUND).build();
}
StreamingOutput streamingOutput = (OutputStream output) -> {
LOG.debug("Start writing content to OutputStream available bytes");
output.write(pdfOpt.get());
};
return Response.status(Status.OK).entity(streamingOutput).build();
}
@GZIP
@GET

View File

@@ -3,17 +3,12 @@ package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -32,8 +27,7 @@ import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCusto
*
* created: 19 Feb 2026
*/
class ExcelUtilsTest {
private static final Log LOG = LogFactory.getLog(ExcelUtilsTest.class);
class ExcelUtilsTest implements TestAble {
private ExcelUtils excelUtils;
@@ -103,7 +97,7 @@ class ExcelUtilsTest {
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertEquals(3, workbook.getNumberOfSheets());
}
writeToFile(bytes, "create_multipleQuestionnaires_createsSheetPerQuestionnaire.xlsx");
}
@@ -190,7 +184,6 @@ class ExcelUtilsTest {
assertNotNull(row.getCell(0));
}
}
// --- create: null handling ---
@@ -256,15 +249,4 @@ class ExcelUtilsTest {
.questions(QuestionnaireJsonParserTest.testJson1)
.build();
}
public void writeToFile(final byte[] content, final String fileName) {
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
IOUtils.write(content, out);
} catch (Exception e) {
LOG.error("Error saveing pdf file", e);
}
}
}

View File

@@ -139,7 +139,7 @@ public class QuestionnaireJsonParserTest {
[
{
"id": "answer1",
"answer": 47,
"answer": "47",
"selected": true
}
]
@@ -172,7 +172,7 @@ public class QuestionnaireJsonParserTest {
{
"id": "answer4",
"answer": "Ontex",
"selected": true
"selected": false
},
{
"id": "answer5",
@@ -205,7 +205,7 @@ public class QuestionnaireJsonParserTest {
{
"id": "answer1",
"answer": "HARTMANN",
"selected": true
"selected": false
},
{
"id": "answer2",
@@ -261,7 +261,7 @@ public class QuestionnaireJsonParserTest {
{
"id": "answer3",
"answer": "Ontex",
"selected": true
"selected": false
}
]
}

View File

@@ -0,0 +1,33 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import java.io.File;
import java.io.FileOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
public interface TestAble {
public static final Log LOG = LogFactory.getLog(ExcelUtilsTest.class);
default void writeToFile(final byte[] content, final String fileName) {
File file = new File("target/test/output/");
file.mkdirs();
try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) {
IOUtils.write(content, out);
} catch (Exception e) {
LOG.error("Error saveing pdf file", e);
}
}
}

View File

@@ -0,0 +1,224 @@
package marketing.heyday.hartmann.fotodocumentation.core.utils;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire;
import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer;
/**
*
* <p>Copyright: Copyright (c) 2024</p>
* <p>Company: heyday Marketing GmbH</p>
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
* @version 1.0
*
* created: 23 Feb 2026
*/
class ZipExportUtilsTest implements TestAble {
private ZipExportUtils zipExportUtils;
private ExcelUtils excelUtils;
@BeforeEach
void setUp() throws Exception {
zipExportUtils = new ZipExportUtils();
excelUtils = mock(ExcelUtils.class);
Field field = ZipExportUtils.class.getDeclaredField("excelUtils");
field.setAccessible(true);
field.set(zipExportUtils, excelUtils);
}
// --- getExport: empty input ---
@Test
void getExport_emptyList_returnsEmpty() {
Optional<byte[]> result = zipExportUtils.getExport(Collections.emptyList());
assertTrue(result.isEmpty());
}
// --- getExport: single customer ---
@Test
void getExport_singleCustomer_returnsPresent() {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Müller Apotheke", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1, 2, 3 }));
Optional<byte[]> result = zipExportUtils.getExport(List.of(customer));
assertTrue(result.isPresent());
}
@Test
void getExport_singleCustomer_returnsValidZip() throws IOException {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Müller Apotheke", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1, 2, 3 }));
byte[] bytes = zipExportUtils.getExport(List.of(customer)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals(1, entries.size());
}
@Test
void getExport_singleCustomer_zipEntryNamedAfterCustomer() throws IOException {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Müller Apotheke", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1, 2, 3 }));
byte[] bytes = zipExportUtils.getExport(List.of(customer)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals("Müller Apotheke.xlsx", entries.get(0));
}
@Test
void getExport_singleCustomer_zipEntryContainsExcelContent() throws IOException {
byte[] excelContent = new byte[] { 10, 20, 30, 40, 50 };
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Test", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(excelContent));
byte[] bytes = zipExportUtils.getExport(List.of(customer)).orElseThrow();
byte[] entryContent = getZipEntryContent(bytes, "Test.xlsx");
assertArrayEquals(excelContent, entryContent);
}
// --- getExport: multiple customers ---
@Test
void getExport_multipleCustomers_createsEntryPerCustomer() throws IOException {
QuestionnaireCustomer c1 = createCustomerWithQuestionnaires("Apotheke A", 1);
QuestionnaireCustomer c2 = createCustomerWithQuestionnaires("Apotheke B", 1);
QuestionnaireCustomer c3 = createCustomerWithQuestionnaires("Apotheke C", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1 }));
byte[] bytes = zipExportUtils.getExport(List.of(c1, c2, c3)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals(3, entries.size());
assertTrue(entries.contains("Apotheke A.xlsx"));
assertTrue(entries.contains("Apotheke B.xlsx"));
assertTrue(entries.contains("Apotheke C.xlsx"));
writeToFile(bytes, "getExport_multipleCustomers_createsEntryPerCustomer.zip");
}
// --- getExport: excel creation fails ---
@Test
void getExport_excelReturnsEmpty_returnsEmpty() {
QuestionnaireCustomer customer = createCustomerWithQuestionnaires("Test", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.empty());
Optional<byte[]> result = zipExportUtils.getExport(List.of(customer));
assertTrue(result.isEmpty());
}
@Test
void getExport_someExcelsFail_onlyIncludesSuccessful() throws IOException {
QuestionnaireCustomer c1 = createCustomerWithQuestionnaires("Success", 1);
QuestionnaireCustomer c2 = createCustomerWithQuestionnaires("Fail", 1);
when(excelUtils.create(eq(c1), anyList())).thenReturn(Optional.of(new byte[] { 1 }));
when(excelUtils.create(eq(c2), anyList())).thenReturn(Optional.empty());
byte[] bytes = zipExportUtils.getExport(List.of(c1, c2)).orElseThrow();
List<String> entries = getZipEntryNames(bytes);
assertEquals(1, entries.size());
assertEquals("Success.xlsx", entries.get(0));
}
// --- getExport: questionnaire sorting ---
@Test
void getExport_questionnairesPassedSortedByDate() {
QuestionnaireCustomer customer = new QuestionnaireCustomer.Builder()
.name("Test").customerNumber("C-001").build();
Questionnaire q1 = new Questionnaire.Builder().questionnaireDate(new Date(2000)).build();
Questionnaire q2 = new Questionnaire.Builder().questionnaireDate(new Date(1000)).build();
Questionnaire q3 = new Questionnaire.Builder().questionnaireDate(new Date(3000)).build();
customer.getQuestionnaires().addAll(Set.of(q1, q2, q3));
when(excelUtils.create(any(), anyList())).thenAnswer(invocation -> {
List<Questionnaire> questionnaires = invocation.getArgument(1);
for (int i = 0; i < questionnaires.size() - 1; i++) {
assertTrue(questionnaires.get(i).getQuestionnaireDate()
.compareTo(questionnaires.get(i + 1).getQuestionnaireDate()) <= 0,
"Questionnaires should be sorted by date");
}
return Optional.of(new byte[] { 1 });
});
zipExportUtils.getExport(List.of(customer));
verify(excelUtils).create(eq(customer), anyList());
}
// --- getExport: calls excelUtils correctly ---
@Test
void getExport_callsExcelUtilsForEachCustomer() {
QuestionnaireCustomer c1 = createCustomerWithQuestionnaires("A", 1);
QuestionnaireCustomer c2 = createCustomerWithQuestionnaires("B", 1);
when(excelUtils.create(any(), anyList())).thenReturn(Optional.of(new byte[] { 1 }));
zipExportUtils.getExport(List.of(c1, c2));
verify(excelUtils).create(eq(c1), anyList());
verify(excelUtils).create(eq(c2), anyList());
}
// --- helpers ---
private QuestionnaireCustomer createCustomerWithQuestionnaires(String name, int questionnaireCount) {
QuestionnaireCustomer customer = new QuestionnaireCustomer.Builder()
.name(name).customerNumber("C-" + name.hashCode()).build();
for (int i = 0; i < questionnaireCount; i++) {
Questionnaire q = new Questionnaire.Builder()
.questionnaireDate(new Date(1000L * (i + 1)))
.questions("[]")
.build();
customer.getQuestionnaires().add(q);
}
return customer;
}
private List<String> getZipEntryNames(byte[] zipBytes) throws IOException {
List<String> names = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
names.add(entry.getName());
}
}
return names;
}
private byte[] getZipEntryContent(byte[] zipBytes, String entryName) throws IOException {
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(entryName)) {
return zis.readAllBytes();
}
}
}
throw new IOException("Entry not found: " + entryName);
}
}