diff --git a/pom.xml b/pom.xml
index 75a27c42..a54dc101 100644
--- a/pom.xml
+++ b/pom.xml
@@ -223,6 +223,11 @@
aws-java-sdk-sns
1.12.593
+
+ org.odftoolkit
+ odfdom-java
+ 0.10.0
+
diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java
index a4f895ed..b2774b93 100644
--- a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java
+++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java
@@ -9,10 +9,7 @@
import gov.cabinetoffice.gap.adminbackend.entities.GrantAdmin;
import gov.cabinetoffice.gap.adminbackend.enums.ApplicationStatusEnum;
import gov.cabinetoffice.gap.adminbackend.enums.EventType;
-import gov.cabinetoffice.gap.adminbackend.exceptions.ApplicationFormException;
-import gov.cabinetoffice.gap.adminbackend.exceptions.InvalidEventException;
-import gov.cabinetoffice.gap.adminbackend.exceptions.NotFoundException;
-import gov.cabinetoffice.gap.adminbackend.exceptions.UnauthorizedException;
+import gov.cabinetoffice.gap.adminbackend.exceptions.*;
import gov.cabinetoffice.gap.adminbackend.models.AdminSession;
import gov.cabinetoffice.gap.adminbackend.security.CheckSchemeOwnership;
import gov.cabinetoffice.gap.adminbackend.services.*;
@@ -25,7 +22,11 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.odftoolkit.odfdom.doc.OdfTextDocument;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.*;
@@ -55,7 +56,9 @@ public class ApplicationFormController {
private final EventLogService eventLogService;
private final UserService userService;
-
+
+ private final OdtService odtService;
+
@PostMapping
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Application form created successfully.",
@@ -264,6 +267,29 @@ public ResponseEntity getApplicationStatus(@PathVariable final Integer a
return ResponseEntity.ok(applicationStatus.toString());
}
+ @GetMapping("/{applicationId}/download-summary")
+ @CheckSchemeOwnership
+ public ResponseEntity exportApplication(
+ @PathVariable final Integer applicationId) {
+ try (OdfTextDocument odt = applicationFormService.getApplicationFormExport(applicationId)) {
+
+ ByteArrayResource odtResource = odtService.odtToResource(odt);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"application.odt\"");
+ headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
+
+ return ResponseEntity.ok().headers(headers).contentLength(odtResource.contentLength())
+ .contentType(MediaType.APPLICATION_OCTET_STREAM).body(odtResource);
+ } catch (RuntimeException e) {
+ log.error("Could not generate ODT for application " + applicationId + ". Exception: ", e);
+ throw new OdtException("Could not generate ODT for this application");
+ } catch (Exception e) {
+ log.error("Could not convert ODT to resource for application " + applicationId + ". Exception: ", e);
+ throw new OdtException("Could not download ODT for this application");
+ }
+ }
+
private void logApplicationEvent(EventType eventType, String sessionId, String applicationId) {
diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/exceptions/OdtException.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/exceptions/OdtException.java
new file mode 100644
index 00000000..37250718
--- /dev/null
+++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/exceptions/OdtException.java
@@ -0,0 +1,23 @@
+package gov.cabinetoffice.gap.adminbackend.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+public class OdtException extends RuntimeException {
+
+ public OdtException() {
+ }
+
+ public OdtException(String message) {
+ super(message);
+ }
+
+ public OdtException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public OdtException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java
index 29aa6469..36404f4a 100644
--- a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java
+++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java
@@ -27,6 +27,7 @@
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
+import org.odftoolkit.odfdom.doc.OdfTextDocument;
import javax.persistence.EntityNotFoundException;
import javax.servlet.http.HttpSession;
@@ -54,6 +55,8 @@ public class ApplicationFormService {
private final SessionsService sessionsService;
+ private final OdtService odtService;
+
private final Validator validator;
private final Clock clock;
@@ -418,4 +421,12 @@ public void removeAdminReferenceBySchemeId(GrantAdmin grantAdmin, Integer scheme
applicationFormRepository.save(application);
});
}
+
+ public OdfTextDocument getApplicationFormExport(Integer applicationId) {
+ final ApplicationFormEntity applicationForm = applicationFormRepository.findById(applicationId)
+ .orElseThrow(() -> new NotFoundException(String.format("No application with ID %s was found", applicationId)));
+ SchemeEntity scheme = this.schemeRepository.findById(applicationForm.getGrantSchemeId()).orElseThrow(EntityNotFoundException::new);
+
+ return odtService.generateSingleOdt(scheme, applicationForm);
+ }
}
diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/OdtService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/OdtService.java
new file mode 100644
index 00000000..f752e53f
--- /dev/null
+++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/OdtService.java
@@ -0,0 +1,421 @@
+package gov.cabinetoffice.gap.adminbackend.services;
+
+import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormQuestionDTO;
+import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormSectionDTO;
+import gov.cabinetoffice.gap.adminbackend.dtos.errors.GenericErrorDTO;
+import gov.cabinetoffice.gap.adminbackend.entities.ApplicationFormEntity;
+import gov.cabinetoffice.gap.adminbackend.entities.SchemeEntity;
+import gov.cabinetoffice.gap.adminbackend.enums.ApplicationStatusEnum;
+import gov.cabinetoffice.gap.adminbackend.enums.ResponseTypeEnum;
+import org.odftoolkit.odfdom.doc.OdfTextDocument;
+import org.odftoolkit.odfdom.doc.table.OdfTable;
+import org.odftoolkit.odfdom.dom.OdfContentDom;
+import org.odftoolkit.odfdom.dom.element.office.OfficeTextElement;
+import org.odftoolkit.odfdom.dom.element.style.*;
+import org.odftoolkit.odfdom.dom.element.table.TableTableElement;
+import org.odftoolkit.odfdom.dom.element.text.TextPElement;
+import org.odftoolkit.odfdom.dom.style.OdfStyleFamily;
+import org.odftoolkit.odfdom.dom.style.OdfStylePropertySet;
+import org.odftoolkit.odfdom.dom.style.props.OdfPageLayoutProperties;
+import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeAutomaticStyles;
+import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
+import org.odftoolkit.odfdom.incubator.doc.style.OdfStyle;
+import org.odftoolkit.odfdom.incubator.doc.style.OdfStylePageLayout;
+import org.odftoolkit.odfdom.incubator.doc.text.OdfTextHeading;
+import org.odftoolkit.odfdom.incubator.doc.text.OdfTextParagraph;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.EnumMap;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Service
+public class OdtService {
+
+ private static final Logger logger = LoggerFactory.getLogger(OdtService.class);
+ private static final String ELIGIBILITY_SECTION_ID = "ELIGIBILITY";
+ private static final String Heading_20_1 = "Heading_20_1";
+ private static final String Heading_20_2 = "Heading_20_2";
+ private static final String Heading_20_3 = "Heading_20_3";
+ private static final String Text_20_1 = "Text_20_1";
+ private static final String Text_20_2 = "Text_20_2";
+ private static final String Text_20_3 = "Text_20_3";
+ private static final String UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy 'at' HH:mm").withZone(ZoneOffset.UTC);
+
+ public OdfTextDocument generateSingleOdt(final SchemeEntity grantScheme, final ApplicationFormEntity applicationForm) {
+ try {
+ OdfStyleProcessor styleProcessor = new OdfStyleProcessor();
+ OdfTextDocument odt = OdfTextDocument.newTextDocument();
+ OdfOfficeStyles stylesOfficeStyles = odt.getOrCreateDocumentStyles();
+ OdfContentDom contentDom = odt.getContentDom();
+ OfficeTextElement documentText = odt.getContentRoot();
+
+ setOfficeStyles(odt, styleProcessor, stylesOfficeStyles);
+ OdfTextParagraph sectionBreak = new OdfTextParagraph(contentDom);
+
+ populateSchemeHeadingSection(grantScheme, documentText, contentDom, odt);
+
+ documentText.appendChild(sectionBreak);
+
+ populateApplicationHeadingSection(applicationForm, documentText, contentDom);
+
+ odt.getContentRoot().setTextUseSoftPageBreaksAttribute(true);
+
+ populateEligibilitySection(applicationForm, documentText, contentDom);
+
+ populateRequiredChecksSection(documentText, contentDom, odt);
+
+ AtomicInteger count = new AtomicInteger(3); //2 sections already added
+
+ if (applicationForm.getDefinition().getSections().stream().anyMatch(section -> section.getSectionId().matches(UUID_REGEX))) {
+ addPageBreak(contentDom, odt);
+ documentText.appendChild(new OdfTextParagraph(contentDom)
+ .addStyledContentWhitespace(Heading_20_2, "Custom sections"));
+ }
+ applicationForm.getDefinition().getSections().forEach(section -> {
+ if (section.getSectionId().matches(UUID_REGEX)) {
+ populateQuestionResponseTable(count, section, documentText, contentDom, odt);
+ }
+ });
+ logger.info("ODT file generated successfully");
+ return odt;
+ } catch (Exception e) {
+ logger.error("Could not generate ODT for given application", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void addPageBreak(OdfContentDom contentDocument, OdfTextDocument doc) throws Exception {
+ final OdfOfficeAutomaticStyles styles = contentDocument.getAutomaticStyles();
+ final OdfStyle style = styles.newStyle(OdfStyleFamily.Paragraph);
+ style.newStyleParagraphPropertiesElement().setFoBreakBeforeAttribute("page");
+ final TextPElement page = doc.getContentRoot().newTextPElement();
+ page.setStyleName(style.getStyleNameAttribute());
+ }
+
+ private static void populateSchemeHeadingSection(final SchemeEntity grantScheme,
+ final OfficeTextElement documentText,
+ final OdfContentDom contentDom,
+ final OdfTextDocument odt) {
+
+ OdfTextHeading h1 = new OdfTextHeading(contentDom);
+ OdfTextHeading h2 = new OdfTextHeading(contentDom);
+
+ h1.addStyledContentWhitespace(Heading_20_1, "Scheme details");
+ h2.addStyledContentWhitespace(Heading_20_2, grantScheme.getName());
+
+ OdfTable table;
+ table = OdfTable.newTable(odt, 2, 1);
+ table.getRowByIndex(0).getCellByIndex(0).setStringValue("GGIS ID");
+ table.getRowByIndex(0).getCellByIndex(1).setStringValue(grantScheme.getGgisIdentifier());
+ table.getRowByIndex(1).getCellByIndex(0).setStringValue("Contact email");
+ table.getRowByIndex(1).getCellByIndex(1).setStringValue(grantScheme.getEmail());
+
+ documentText.appendChild(h1);
+ documentText.appendChild(h2);
+ documentText.appendChild(table.getOdfElement());
+ }
+
+ private static void populateApplicationHeadingSection(final ApplicationFormEntity applicationForm,
+ final OfficeTextElement documentText,
+ final OdfContentDom contentDom) {
+
+ OdfTextHeading h1 = new OdfTextHeading(contentDom);
+ OdfTextHeading h2 = new OdfTextHeading(contentDom);
+ OdfTextParagraph p = new OdfTextParagraph(contentDom);
+
+ h1.addStyledContentWhitespace(Heading_20_1, "\nApplication details");
+ h2.addStyledContentWhitespace(Heading_20_2, applicationForm.getApplicationName());
+ p.addStyledContentWhitespace(Text_20_3, (
+ Objects.equals(ApplicationStatusEnum.PUBLISHED, applicationForm.getApplicationStatus())
+ ? ("Published on " + formatter.format(applicationForm.getLastPublished()))
+ : "Not published"
+ ) + "\n\n");
+ documentText.appendChild(h1);
+ documentText.appendChild(h2);
+ documentText.appendChild(p);
+ }
+
+ private static void populateRequiredChecksSection(final OfficeTextElement documentText,
+ final OdfContentDom contentDom,
+ final OdfTextDocument odt) {
+ OdfTextHeading requiredCheckHeading = new OdfTextHeading(contentDom);
+ OdfTextHeading requiredCheckSubHeading = new OdfTextHeading(contentDom);
+ OdfTextParagraph locationQuestion = new OdfTextParagraph(contentDom);
+ final String orgNameHeading = "Organisation details";
+
+ requiredCheckHeading.addStyledContentWhitespace(Heading_20_2, "Due diligence information");
+ requiredCheckSubHeading.addStyledContentWhitespace(Heading_20_3, orgNameHeading);
+
+ documentText.appendChild(requiredCheckHeading);
+ documentText.appendChild(requiredCheckSubHeading);
+ documentText.appendChild(generateEssentialTable(odt));
+ documentText.appendChild(new OdfTextParagraph(contentDom).addContentWhitespace(""));
+ locationQuestion.addStyledContent(Heading_20_3, "Funding");
+ OdfTable table = OdfTable.newTable(odt, 2, 1);
+
+ table.getRowByIndex(0).getCellByIndex(0).setStringValue("Amount applied for");
+ table.getRowByIndex(1).getCellByIndex(0).setStringValue("Where funding will be spent");
+
+ documentText.appendChild(locationQuestion);
+ documentText.appendChild(table.getOdfElement());
+ }
+
+ private static void populateEligibilitySection(final ApplicationFormEntity applicationForm,
+ final OfficeTextElement documentText,
+ final OdfContentDom contentDom) {
+ final ApplicationFormSectionDTO eligibilitySection = applicationForm.getDefinition().getSectionById(ELIGIBILITY_SECTION_ID);
+ OdfTextHeading eligibilityHeading = new OdfTextHeading(contentDom);
+ OdfTextParagraph eligibilityStatement = new OdfTextParagraph(contentDom);
+ OdfTextParagraph eligibilityResponse = new OdfTextParagraph(contentDom);
+
+ eligibilityHeading.addStyledContent(Heading_20_2, "Eligibility");
+ documentText.appendChild(eligibilityHeading);
+
+ OdfTextHeading eligibilitySubHeading = new OdfTextHeading(contentDom);
+ eligibilitySubHeading.addStyledContent(Heading_20_3, "Eligibility Statement");
+ documentText.appendChild(eligibilitySubHeading);
+
+ eligibilityResponse.addStyledContentWhitespace(Text_20_3, eligibilitySection
+ .getQuestionById(ELIGIBILITY_SECTION_ID).getDisplayText() + "\n");
+
+ documentText.appendChild(eligibilityResponse);
+
+ documentText.appendChild(new OdfTextHeading(contentDom).addContentWhitespace(""));
+ documentText.appendChild(eligibilityStatement);
+ }
+
+ private static void populateQuestionResponseTable(AtomicInteger count,
+ ApplicationFormSectionDTO section,
+ OfficeTextElement documentText,
+ OdfContentDom contentDom, OdfTextDocument odt) {
+ OdfTextHeading sectionHeading = new OdfTextHeading(contentDom);
+ sectionHeading.addStyledContentWhitespace(Heading_20_3, section.getSectionTitle());
+ documentText.appendChild(sectionHeading);
+
+ if (section.getQuestions().size() > 0) {
+ AtomicInteger questionIndex = new AtomicInteger(1);
+ OdfTable table = OdfTable.newTable(odt, section.getQuestions().size(), 3);
+ long columnWidth = table.getWidth() / 5;
+ table.getColumnByIndex(0).setWidth(columnWidth);
+ table.getColumnByIndex(1).setWidth(columnWidth);
+ table.getColumnByIndex(2).setWidth(columnWidth);
+ table.getColumnByIndex(3).setWidth(columnWidth);
+ table.getColumnByIndex(4).setWidth(columnWidth);
+ table.getRowByIndex(0).getCellByIndex(0).setStringValue("Question");
+ table.getRowByIndex(0).getCellByIndex(1).setStringValue("Hint text");
+ table.getRowByIndex(0).getCellByIndex(2).setStringValue("Question type");
+ table.getRowByIndex(0).getCellByIndex(3).setStringValue("Options / Max words");
+ table.getRowByIndex(0).getCellByIndex(4).setStringValue("Optional");
+ section.getQuestions().forEach(question -> {
+ populateDocumentFromQuestionResponse(question, documentText, questionIndex,
+ table);
+ questionIndex.incrementAndGet();
+ });
+ } else {
+ OdfTable table = OdfTable.newTable(odt, 1, 3);
+ long columnWidth = table.getWidth() / 5;
+ table.getColumnByIndex(0).setWidth(columnWidth);
+ table.getColumnByIndex(1).setWidth(columnWidth);
+ table.getColumnByIndex(2).setWidth(columnWidth);
+ table.getColumnByIndex(3).setWidth(columnWidth);
+ table.getColumnByIndex(4).setWidth(columnWidth);
+ table.getRowByIndex(0).getCellByIndex(0).setStringValue("Question");
+ table.getRowByIndex(0).getCellByIndex(1).setStringValue("Hint text");
+ table.getRowByIndex(0).getCellByIndex(2).setStringValue("Question type");
+ table.getRowByIndex(0).getCellByIndex(3).setStringValue("Options / Max words");
+ table.getRowByIndex(0).getCellByIndex(4).setStringValue("Optional");
+ documentText.appendChild(table.getOdfElement());
+ }
+ documentText.appendChild(new OdfTextParagraph(contentDom).addContentWhitespace(""));
+ count.getAndIncrement();
+ }
+
+ private static void populateDocumentFromQuestionResponse(ApplicationFormQuestionDTO question,
+ OfficeTextElement documentText,
+ AtomicInteger questionIndex,
+ OdfTable table) {
+ EnumMap responseTypeMap = new EnumMap<>(ResponseTypeEnum.class);
+ responseTypeMap.put(ResponseTypeEnum.YesNo, "Yes/No");
+ responseTypeMap.put(ResponseTypeEnum.SingleSelection, "Single selection");
+ responseTypeMap.put(ResponseTypeEnum.Dropdown, "Multiple choice");
+ responseTypeMap.put(ResponseTypeEnum.MultipleSelection, "Multiple select");
+ responseTypeMap.put(ResponseTypeEnum.ShortAnswer, "Short answer");
+ responseTypeMap.put(ResponseTypeEnum.LongAnswer, "Long answer");
+ responseTypeMap.put(ResponseTypeEnum.AddressInput, "Address input");
+ responseTypeMap.put(ResponseTypeEnum.Numeric, "Numeric");
+ responseTypeMap.put(ResponseTypeEnum.Date, "Date");
+ responseTypeMap.put(ResponseTypeEnum.SingleFileUpload, "Document upload");
+
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(0).setStringValue(question.getFieldTitle());
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(1).setStringValue(question.getHintText());
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(2).setStringValue(responseTypeMap.get(question.getResponseType()));
+ // Options / Max Words
+ boolean shouldDisplayOptions = question.getResponseType().equals(ResponseTypeEnum.MultipleSelection)
+ || question.getResponseType().equals(ResponseTypeEnum.SingleSelection)
+ || question.getResponseType().equals(ResponseTypeEnum.Dropdown);
+ boolean shouldDisplayMaxWords = question.getResponseType().equals(ResponseTypeEnum.LongAnswer)
+ || question.getResponseType().equals(ResponseTypeEnum.ShortAnswer);
+ if (shouldDisplayOptions) {
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(3).setStringValue(String.join(",\n", question.getOptions()));
+ } else if (shouldDisplayMaxWords) {
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(3).setStringValue(question.getValidation().get("maxWords").toString());
+ } else {
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(3).setStringValue("");
+ }
+ //Optional
+ table.getRowByIndex(questionIndex.get()).getCellByIndex(4).setStringValue(
+ Objects.equals(question.getValidation().get("mandatory"), true)
+ ? "No"
+ : "Yes"
+ );
+ documentText.appendChild(table.getOdfElement());
+ }
+
+ private static TableTableElement generateEssentialTable(OdfTextDocument doc) {
+ OdfTable odfTable = OdfTable.newTable(doc, 9, 1);
+
+ odfTable.getRowByIndex(0).getCellByIndex(0).setStringValue("Organisation name");
+ odfTable.getRowByIndex(1).getCellByIndex(0).setStringValue("Organisation type");
+
+ odfTable.getRowByIndex(2).getCellByIndex(0).setStringValue("Address line 1");
+ odfTable.getRowByIndex(3).getCellByIndex(0).setStringValue("Address line 2");
+ odfTable.getRowByIndex(4).getCellByIndex(0).setStringValue("Address city");
+ odfTable.getRowByIndex(5).getCellByIndex(0).setStringValue("Address county");
+ odfTable.getRowByIndex(6).getCellByIndex(0).setStringValue("Address postcode");
+
+ odfTable.getRowByIndex(7).getCellByIndex(0).setStringValue("Charities Commission number (if the organisation has one)");
+ odfTable.getRowByIndex(8).getCellByIndex(0).setStringValue("Companies House number (if the organisation has one)");
+
+ return odfTable.getOdfElement();
+ }
+
+ public ByteArrayResource odtToResource(OdfTextDocument odtDoc) throws Exception {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ odtDoc.save(outputStream);
+ byte[] zipBytes = outputStream.toByteArray();
+ return new ByteArrayResource(zipBytes);
+ }
+
+ private static void setOfficeStyles(OdfTextDocument outputDocument, OdfStyleProcessor styleProcessor, OdfOfficeStyles stylesOfficeStyles) {
+ // Set landscape layout
+ StyleMasterPageElement defaultPage = outputDocument.getOfficeMasterStyles().getMasterPage("Standard");
+ String pageLayoutName = defaultPage.getStylePageLayoutNameAttribute();
+ OdfStylePageLayout pageLayoutStyle = defaultPage.getAutomaticStyles().getPageLayout(pageLayoutName);
+ pageLayoutStyle.setProperty(OdfPageLayoutProperties.PrintOrientation, "Portrait");
+
+ styleProcessor.setStyle(stylesOfficeStyles.getDefaultStyle(OdfStyleFamily.Paragraph))
+ .margins("0cm", "0cm", "0cm", "0cm")
+ .fontFamily("Arial")
+ .fontSize("11pt")
+ .textAlign("normal");
+
+
+ // Title 1
+ styleProcessor.setStyle(stylesOfficeStyles.getStyle(Heading_20_1, OdfStyleFamily.Paragraph))
+ .margins("0cm", "0cm", "0cm", "0cm").
+ color("#000000")
+ .fontWeight("normal")
+ .fontSize("26pt");
+
+ // Title 2
+ styleProcessor.setStyle(stylesOfficeStyles.getStyle(Heading_20_2, OdfStyleFamily.Paragraph))
+ .fontStyle("normal")
+ .margins("0cm", "0cm", "0.2cm", "0cm")
+ .fontWeight("normal")
+ .fontSize("20pt")
+ .color("#000000");
+
+ // Title 3
+ styleProcessor.setStyle(stylesOfficeStyles.getStyle(Heading_20_3, OdfStyleFamily.Paragraph))
+ .margins("0cm", "0cm", "0.2cm", "0cm")
+ .fontWeight("normal")
+ .fontSize("16pt");
+
+ //test
+ styleProcessor.setStyle(stylesOfficeStyles.newStyle(Text_20_1, OdfStyleFamily.Text))
+ .margins("0cm", "0cm", "1cm", "0cm")
+ .fontFamily("Arial")
+ .fontSize("15pt")
+ .color("#000000");
+
+ styleProcessor.setStyle(stylesOfficeStyles.newStyle(Text_20_2, OdfStyleFamily.Text))
+ .margins("0cm", "0cm", "0cm", "0cm")
+ .fontFamily("Arial")
+ .fontSize("11pt")
+ .color("#000000");
+
+ styleProcessor.setStyle(stylesOfficeStyles.newStyle(Text_20_3, OdfStyleFamily.Text))
+ .margins("0cm", "0cm", "0cm", "0cm")
+ .fontFamily("Arial")
+ .fontSize("11pt")
+ .color("#000000")
+ .fontStyle("italic");
+ }
+
+ private static class OdfStyleProcessor {
+
+ private OdfStylePropertySet style;
+
+ public OdfStyleProcessor() {
+
+ }
+
+ public OdfStyleProcessor setStyle(OdfStylePropertySet style) {
+ this.style = style;
+ return this;
+ }
+
+ public OdfStyleProcessor fontFamily(String value) {
+ this.style.setProperty(StyleTextPropertiesElement.FontFamily, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontName, value);
+ return this;
+ }
+
+ public OdfStyleProcessor fontWeight(String value) {
+ this.style.setProperty(StyleTextPropertiesElement.FontWeight, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontWeightAsian, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontWeightComplex, value);
+ return this;
+ }
+
+ public OdfStyleProcessor fontStyle(String value) {
+ this.style.setProperty(StyleTextPropertiesElement.FontStyle, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontStyleAsian, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontStyleComplex, value);
+ return this;
+ }
+
+ public OdfStyleProcessor fontSize(String value) {
+ this.style.setProperty(StyleTextPropertiesElement.FontSize, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontSizeAsian, value);
+ this.style.setProperty(StyleTextPropertiesElement.FontSizeComplex, value);
+ return this;
+ }
+
+ public OdfStyleProcessor margins(String top, String right, String bottom, String left) {
+ this.style.setProperty(StyleParagraphPropertiesElement.MarginTop, top);
+ this.style.setProperty(StyleParagraphPropertiesElement.MarginRight, right);
+ this.style.setProperty(StyleParagraphPropertiesElement.MarginBottom, bottom);
+ this.style.setProperty(StyleParagraphPropertiesElement.MarginLeft, left);
+ return this;
+ }
+
+ public OdfStyleProcessor color(String value) {
+ this.style.setProperty(StyleTextPropertiesElement.Color, value);
+ return this;
+ }
+
+ public void textAlign(String value) {
+ this.style.setProperty(StyleParagraphPropertiesElement.TextAlign, value);
+ }
+
+ }
+}
diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java
index e0b57ea6..19621025 100644
--- a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java
+++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SubmissionsService.java
@@ -91,11 +91,6 @@ public ByteArrayOutputStream exportSpotlightChecks(Integer applicationId) {
+ " is unable to access application with id " + applicationId);
}
- // TODO GAP-1377 we need to limit the number of submissions we export
- // to 1000 rows in a file due to a restriction the Spotlight input
- // processing. So we will need to "page" the returned data and create
- // multiple files if there are more than 999 submissions.
-
final List submissionsByAppId = submissionRepository
.findByApplicationGrantApplicationIdAndStatus(applicationId, SubmissionStatus.SUBMITTED);
log.info("Found {} submissions in SUBMITTED state for application ID {}", submissionsByAppId.size(),
diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormControllerTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormControllerTest.java
index 6c3e4182..2631fb8e 100644
--- a/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormControllerTest.java
+++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormControllerTest.java
@@ -11,6 +11,7 @@
import gov.cabinetoffice.gap.adminbackend.enums.ApplicationStatusEnum;
import gov.cabinetoffice.gap.adminbackend.exceptions.ApplicationFormException;
import gov.cabinetoffice.gap.adminbackend.exceptions.NotFoundException;
+import gov.cabinetoffice.gap.adminbackend.exceptions.OdtException;
import gov.cabinetoffice.gap.adminbackend.mappers.ValidationErrorMapperImpl;
import gov.cabinetoffice.gap.adminbackend.repositories.ApplicationFormRepository;
import gov.cabinetoffice.gap.adminbackend.security.interceptors.AuthorizationHeaderInterceptor;
@@ -18,32 +19,37 @@
import gov.cabinetoffice.gap.adminbackend.utils.HelperUtils;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import org.odftoolkit.odfdom.doc.OdfTextDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
import java.time.Instant;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
import static gov.cabinetoffice.gap.adminbackend.testdata.ApplicationFormTestData.*;
import static gov.cabinetoffice.gap.adminbackend.testdata.generators.RandomApplicationFormGenerators.randomApplicationFormFound;
+import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ApplicationFormController.class)
@AutoConfigureMockMvc(addFilters = false)
@@ -82,6 +88,9 @@ class ApplicationFormControllerTest {
@MockBean
private UserService userService;
+ @MockBean
+ private OdtService odtService;
+
@Test
@WithAdminSession
void saveApplicationFormHappyPathTest() throws Exception {
@@ -472,4 +481,61 @@ void getApplicationStatusNotFoundException() throws Exception {
this.mockMvc.perform(get("/application-forms/1/status"))
.andExpect(status().isNotFound());
}
+
+
+ @Test
+ void testExportApplication() throws Exception {
+ Integer applicationId = 1;
+ OdfTextDocument odfTextDocument = OdfTextDocument.newTextDocument();
+ when(applicationFormService.getApplicationFormExport(applicationId)).thenReturn(odfTextDocument);
+ when(odtService.odtToResource(any())).thenReturn(new ByteArrayResource(new byte[]{1}));
+
+ this.mockMvc.perform(get("/application-forms/1/download-summary"))
+ .andExpect(status().isOk())
+ .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"application.odt\""))
+ .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE));
+ verify(applicationFormService, times(1)).getApplicationFormExport(any());
+ verify(odtService, times(1)).odtToResource(any());
+ }
+
+ @Test
+ void testExportApplicationOdtServiceThrowsIOException() throws Exception {
+ Integer applicationId = 1;
+ OdfTextDocument odfTextDocument = OdfTextDocument.newTextDocument();
+ when(applicationFormService.getApplicationFormExport(applicationId)).thenReturn(odfTextDocument);
+ when(odtService.odtToResource(Mockito.any())).thenThrow(new IOException());
+
+ MvcResult result = this.mockMvc.perform(get("/application-forms/" + applicationId + "/download-summary"))
+ .andExpect(status().isInternalServerError())
+ .andReturn();
+
+ Exception resolvedException = result.getResolvedException();
+ assertNotNull(resolvedException);
+ assertEquals(OdtException.class, resolvedException.getClass());
+ verify(applicationFormService, times(1)).getApplicationFormExport(any());
+ verify(odtService, times(1)).odtToResource(any());
+
+
+ UUID submissionId = UUID.randomUUID();
+ HttpServletRequest mockRequest = new MockHttpServletRequest();
+ Submission mockSubmission = mock(Submission.class);
+ }
+
+
+ @Test
+ void testExportApplicationThrowsRuntimeError() throws Exception {
+ when(applicationFormService.getApplicationFormExport(Mockito.any()))
+ .thenThrow(new RuntimeException());
+
+ MvcResult result = this.mockMvc.perform(get("/application-forms/1/download-summary"))
+ .andExpect(status().isInternalServerError())
+ .andReturn();
+
+ Exception resolvedException = result.getResolvedException();
+ assertNotNull(resolvedException);
+ assertEquals(OdtException.class, resolvedException.getClass());
+ verify(applicationFormService, times(1)).getApplicationFormExport(any());
+ verify(odtService, times(0)).odtToResource(any());
+ }
+
}
diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormServiceTest.java
index 381b5ee7..338a5b2a 100644
--- a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormServiceTest.java
+++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormServiceTest.java
@@ -6,6 +6,7 @@
import gov.cabinetoffice.gap.adminbackend.dtos.application.questions.QuestionGenericPatchDTO;
import gov.cabinetoffice.gap.adminbackend.dtos.schemes.SchemeDTO;
import gov.cabinetoffice.gap.adminbackend.entities.ApplicationFormEntity;
+import gov.cabinetoffice.gap.adminbackend.entities.SchemeEntity;
import gov.cabinetoffice.gap.adminbackend.enums.ApplicationStatusEnum;
import gov.cabinetoffice.gap.adminbackend.exceptions.ApplicationFormException;
import gov.cabinetoffice.gap.adminbackend.exceptions.ConflictException;
@@ -14,6 +15,7 @@
import gov.cabinetoffice.gap.adminbackend.mappers.ApplicationFormMapper;
import gov.cabinetoffice.gap.adminbackend.mappers.ApplicationFormMapperImpl;
import gov.cabinetoffice.gap.adminbackend.repositories.ApplicationFormRepository;
+import gov.cabinetoffice.gap.adminbackend.repositories.SchemeRepository;
import gov.cabinetoffice.gap.adminbackend.repositories.TemplateApplicationFormRepository;
import gov.cabinetoffice.gap.adminbackend.testdata.projectionimpls.TestApplicationFormsFoundView;
import gov.cabinetoffice.gap.adminbackend.utils.ApplicationFormUtils;
@@ -22,6 +24,7 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.*;
+import org.odftoolkit.odfdom.doc.OdfTextDocument;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -56,6 +59,12 @@ class ApplicationFormServiceTest {
@Mock
private ApplicationFormRepository applicationFormRepository;
+ @Mock
+ private SchemeRepository schemeRepository;
+
+ @Mock
+ private OdtService odtService;
+
@Mock
private SessionsService sessionsService;
@@ -925,5 +934,31 @@ void getApplicationStatusNotFoundException() {
}
+ @Nested
+ class getApplicationFormExport {
+ @Test
+ void getApplicationFormExportSuccessful() throws Exception {
+ final Integer applicationId = 1;
+ final Integer grantSchemeId = 1;
+ ApplicationFormEntity applicationForm = ApplicationFormEntity.builder()
+ .grantApplicationId(applicationId)
+ .grantSchemeId(grantSchemeId)
+ .build();
+ SchemeEntity scheme = SchemeEntity.builder()
+ .id(grantSchemeId)
+ .build();
+
+ when(applicationFormRepository.findById(anyInt())).thenReturn(Optional.of(applicationForm));
+ when(schemeRepository.findById(anyInt())).thenReturn(Optional.of(scheme));
+ OdfTextDocument odfTextDocument = OdfTextDocument.newTextDocument();
+ when(odtService.generateSingleOdt(scheme, applicationForm)).thenReturn(odfTextDocument);
+
+ final OdfTextDocument response = applicationFormService.getApplicationFormExport(applicationId);
+ verify(applicationFormRepository).findById(applicationId);
+ verify(schemeRepository).findById(applicationId);
+ assertThat(response).isInstanceOf(OdfTextDocument.class);
+
+ }
+ }
}
diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/OdtServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/OdtServiceTest.java
new file mode 100644
index 00000000..d8893b18
--- /dev/null
+++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/OdtServiceTest.java
@@ -0,0 +1,163 @@
+package gov.cabinetoffice.gap.adminbackend.services;
+
+import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationDefinitionDTO;
+import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormQuestionDTO;
+import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormSectionDTO;
+import gov.cabinetoffice.gap.adminbackend.entities.ApplicationFormEntity;
+import gov.cabinetoffice.gap.adminbackend.entities.SchemeEntity;
+import gov.cabinetoffice.gap.adminbackend.enums.ResponseTypeEnum;
+import gov.cabinetoffice.gap.adminbackend.testdata.generators.RandomSchemeGenerator;
+import lombok.RequiredArgsConstructor;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.odftoolkit.odfdom.doc.OdfDocument;
+import org.w3c.dom.Document;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+@RequiredArgsConstructor
+public class OdtServiceTest {
+ @InjectMocks
+ OdtService odtService;
+
+ @Test
+ void generateSingleOdt() throws Exception {
+ SchemeEntity mockScheme = RandomSchemeGenerator.randomSchemeEntity().version(2).build();
+
+ List sections = new ArrayList<>(
+ List.of(
+ ApplicationFormSectionDTO.builder()
+ .sectionId("ELIGIBILITY")
+ .questions(
+ new ArrayList<>(List.of(
+ ApplicationFormQuestionDTO.builder()
+ .questionId("ELIGIBILITY")
+ .displayText("This is the eligibility statement.")
+ .build()
+ ))
+ )
+ .build(),
+ ApplicationFormSectionDTO.builder()
+ .sectionId("REQUIRED_CHECKS")
+ .build(),
+ ApplicationFormSectionDTO.builder()
+ .sectionId("605d59ab-569a-4db9-bc96-58fb8a75ac94")
+ .sectionTitle("Custom Section 1")
+ .questions(
+ new ArrayList<>(List.of(
+ ApplicationFormQuestionDTO.builder()
+ .questionId("1ff22c1f-0065-4fe6-bc81-3038edd67ed5")
+ .fieldTitle("Custom Question 1")
+ .hintText("Question 1 hint text")
+ .responseType(ResponseTypeEnum.LongAnswer)
+ .validation(Map.ofEntries(
+ Map.entry("maxWords", 987),
+ Map.entry("mandatory", true)
+ ))
+ .build(),
+ ApplicationFormQuestionDTO.builder()
+ .questionId("9368923f-2264-4b2a-b84b-752d5cdeb574")
+ .fieldTitle("Custom Question 2")
+ .hintText("Question 2 hint text")
+ .responseType(ResponseTypeEnum.MultipleSelection)
+ .options(List.of("Option 1", "Option 2", "Option 3"))
+ .validation(Map.ofEntries(
+ Map.entry("mandatory", false)
+ ))
+ .build()
+ ))
+ )
+ .build(),
+ ApplicationFormSectionDTO.builder()
+ .sectionId("605d59ab-569a-4db9-bc96-58fb8a75ac94")
+ .sectionTitle("Custom Section 2")
+ .questions(List.of())
+ .build()
+ )
+ );
+ ApplicationDefinitionDTO definition = ApplicationDefinitionDTO.builder()
+ .sections(sections)
+ .build();
+ ApplicationFormEntity mockApplicationForm = ApplicationFormEntity.createFromTemplate(
+ mockScheme.getId(), "Application Name",
+ 1, definition, mockScheme.getVersion());
+
+ OdfDocument generatedDoc = odtService.generateSingleOdt(mockScheme, mockApplicationForm);
+ final String generatedContent = docToString(generatedDoc.getContentDom());
+
+ assertThat(generatedContent).contains(
+ "Scheme details",
+ "Sample Scheme",
+ "GGIS ID",
+ "GGIS12345",
+ "Contact email",
+ "contact@address.com"
+ );
+ assertThat(generatedContent).contains(
+ "Application details",
+ "Application Name",
+ "Not published"
+ );
+ assertThat(generatedContent).contains(
+ "Eligibility",
+ "This is the eligibility statement."
+ );
+ assertThat(generatedContent).contains(
+ "Due diligence information",
+ "Organisation details",
+ "Organisation name",
+ "Organisation type",
+ "Address line 1",
+ "Address line 2",
+ "Address city",
+ "Address county",
+ "Address postcode",
+ "Charities Commission number (if the organisation has one)",
+ "Companies House number (if the organisation has one)",
+ "Funding",
+ "Amount applied for",
+ "Where funding will be spent"
+ );
+ assertThat(generatedContent).contains(
+ "Custom Section 1",
+ "Question",
+ "Hint text",
+ "Question type",
+ "Options / Max words",
+ "Optional",
+ "Custom Question 1",
+ "Question 1 hint text",
+ "Long answer",
+ "987",
+ "No",
+ "Custom Question 2",
+ "Question 2 hint text",
+ "Multiple select",
+ "Option 1,",
+ "Option 2,",
+ "Option 3",
+ "Yes",
+ "Custom Section 2"
+ );
+ }
+
+ private String docToString(Document document) throws Exception {
+ TransformerFactory tf = TransformerFactory.newInstance();
+ Transformer transformer = tf.newTransformer();
+ StringWriter writer = new StringWriter();
+ transformer.transform(new DOMSource(document), new StreamResult(writer));
+ return writer.getBuffer().toString();
+ }
+}