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(); + } +}