diff --git a/fineract-mix/dependencies.gradle b/fineract-mix/dependencies.gradle index 698ffa7e6fd..d705a0227a5 100644 --- a/fineract-mix/dependencies.gradle +++ b/fineract-mix/dependencies.gradle @@ -47,6 +47,8 @@ dependencies { 'org.mapstruct:mapstruct', 'io.github.resilience4j:resilience4j-spring-boot3', + + 'jakarta.xml.bind:jakarta.xml.bind-api' ) compileOnly 'com.github.spotbugs:spotbugs-annotations' compileOnly 'org.projectlombok:lombok' diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java index 89d48356606..c28ec08d848 100644 --- a/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java +++ b/fineract-mix/src/main/java/org/apache/fineract/mix/api/MixReportApiResource.java @@ -28,6 +28,7 @@ import java.sql.Date; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.annotation.AlternativeOperationId; +import org.apache.fineract.mix.data.MixReportXBRLDocument; import org.apache.fineract.mix.service.MixReportXBRLBuilder; import org.apache.fineract.mix.service.MixReportXBRLResultService; import org.springframework.stereotype.Component; @@ -46,12 +47,11 @@ public class MixReportApiResource { @Produces({ MediaType.APPLICATION_XML }) @Operation(summary = "Retrieve Mix XBRL report", operationId = "retrieveMixReport") @AlternativeOperationId("retrieveXBRLReport") - public String retrieveXBRLReport(@QueryParam("startDate") final Date startDate, @QueryParam("endDate") final Date endDate, - @QueryParam("currency") final String currency) { + public MixReportXBRLDocument retrieveXBRLReport(@QueryParam("startDate") final Date startDate, + @QueryParam("endDate") final Date endDate, @QueryParam("currency") final String currency) { final var data = xbrlResultService.getXBRLResult(startDate, endDate, currency); - // TODO: make this type safe? - return this.xbrlBuilder.build(data); + return this.xbrlBuilder.buildDocumentGraph(data); } } diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLDocument.java b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLDocument.java new file mode 100644 index 00000000000..b43c7657979 --- /dev/null +++ b/fineract-mix/src/main/java/org/apache/fineract/mix/data/MixReportXBRLDocument.java @@ -0,0 +1,160 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.mix.data; + +import jakarta.xml.bind.annotation.XmlAnyElement; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.w3c.dom.Element; + +@Getter +@Setter +@NoArgsConstructor +@XmlRootElement(name = "xbrl") +public class MixReportXBRLDocument implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private List taxonomyElements = new ArrayList<>(); + private List contexts = new ArrayList<>(); + private List units = new ArrayList<>(); + + @XmlElement(name = "schemaRef") + public SchemaRefDTO getSchemaRef() { + return new SchemaRefDTO(); + } + + @XmlAnyElement + public List getTaxonomyElements() { + return this.taxonomyElements; + } + + @XmlElement(name = "context") + public List getContexts() { + return this.contexts; + } + + @XmlElement(name = "unit") + public List getUnits() { + return this.units; + } + + public static class SchemaRefDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + public String getNamespaceUri() { + return "http://www.themix.org/sites/default/files/Taxonomy2010/dct/dc-all_2010-08-31.xsd"; + } + } + + @Getter + @Setter + @NoArgsConstructor + public static class XBRLContextDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String id; + private EntityDTO entity = new EntityDTO(); + private PeriodDTO period; + private ScenarioDTO scenario; + } + + @Getter + @Setter + @NoArgsConstructor + public static class EntityDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private IdentifierDTO identifier = new IdentifierDTO(); + } + + @Getter + @Setter + @NoArgsConstructor + public static class IdentifierDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String scheme = "http://www.themix.org"; + private String value = "000000"; + } + + @Getter + @Setter + @NoArgsConstructor + public static class PeriodDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String instant; + private String startDate; + private String endDate; + } + + @Getter + @Setter + @NoArgsConstructor + public static class ScenarioDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private ExplicitMemberDTO explicitMember; + } + + @Getter + @Setter + @NoArgsConstructor + public static class ExplicitMemberDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String dimension; + private String value; + } + + @Getter + @Setter + @NoArgsConstructor + public static class XBRLUnitDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String id; + private String measure; + } +} diff --git a/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java index 1d97f3210aa..4e704e1efbf 100644 --- a/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java +++ b/fineract-mix/src/main/java/org/apache/fineract/mix/service/MixReportXBRLBuilder.java @@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.mix.data.MixReportXBRLContextData; import org.apache.fineract.mix.data.MixReportXBRLData; +import org.apache.fineract.mix.data.MixReportXBRLDocument; import org.apache.fineract.mix.data.MixReportXBRLNamespaceData; import org.apache.fineract.mix.data.MixTaxonomyData; import org.apache.fineract.mix.exception.MixReportXBRLMappingInvalidException; @@ -205,4 +206,76 @@ private Integer getNumberOfDecimalPlaces(final BigDecimal bigDecimal) { final int index = string.indexOf("."); return index < 0 ? 0 : string.length() - index - 1; } + + public MixReportXBRLDocument buildDocumentGraph(final MixReportXBRLData xbrlData) { + MixReportXBRLDocument document = new MixReportXBRLDocument(); + + Integer instantScenarioCounter = 0; + Integer durationScenarioCounter = 0; + Map contextMap = new HashMap<>(); + + // 1. Build the dynamic taxonomy elements using standard W3C DOM elements + try { + // We reuse your existing logic but convert the output elements to the graph + for (final Map.Entry entry : xbrlData.getResultMap().entrySet()) { + final MixTaxonomyData taxonomy = entry.getKey(); + final BigDecimal value = entry.getValue(); + + // We keep using your existing addTaxonomy method helper + Element dom4jElement = addTaxonomy(DocumentHelper.createDocument().addElement("root"), taxonomy, value, + xbrlData.getStartDate(), xbrlData.getEndDate(), instantScenarioCounter, durationScenarioCounter, contextMap); + + // Convert dom4j Element to native W3C Element for JAXB compatibility + org.w3c.dom.Element w3cElement = new org.dom4j.io.DOMWriter().write(dom4jElement.getDocument()).getDocumentElement(); + document.getTaxonomyElements().add(w3cElement); + } + } catch (Exception e) { + log.error("Error mapping XBRL document elements to JAXB graph", e); + } + + // 2. Map Contexts into the structural DTO layout + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + + for (final Map.Entry entry : contextMap.entrySet()) { + final MixReportXBRLContextData context = entry.getKey(); + MixReportXBRLDocument.XBRLContextDTO contextDTO = new MixReportXBRLDocument.XBRLContextDTO(); + contextDTO.setId(entry.getValue()); + + MixReportXBRLDocument.PeriodDTO periodDTO = new MixReportXBRLDocument.PeriodDTO(); + if (context.getPeriodType() == 0) { + periodDTO.setInstant(format.format(xbrlData.getEndDate())); + } else { + periodDTO.setStartDate(format.format(xbrlData.getStartDate())); + periodDTO.setEndDate(format.format(xbrlData.getEndDate())); + } + contextDTO.setPeriod(periodDTO); + + final String dimension = context.getDimension(); + final String dimType = context.getDimensionType(); + if (dimType != null && dimension != null) { + MixReportXBRLDocument.ScenarioDTO scenarioDTO = new MixReportXBRLDocument.ScenarioDTO(); + MixReportXBRLDocument.ExplicitMemberDTO memberDTO = new MixReportXBRLDocument.ExplicitMemberDTO(); + memberDTO.setDimension(dimType); + memberDTO.setValue(dimension); + scenarioDTO.setExplicitMember(memberDTO); + contextDTO.setScenario(scenarioDTO); + } + + document.getContexts().add(contextDTO); + } + + // 3. Map Units into structured DTO layout + MixReportXBRLDocument.XBRLUnitDTO pureUnit = new MixReportXBRLDocument.XBRLUnitDTO(); + pureUnit.setId(UNITID_PURE); + pureUnit.setMeasure("xbrli:pure"); + document.getUnits().add(pureUnit); + + MixReportXBRLDocument.XBRLUnitDTO curUnit = new MixReportXBRLDocument.XBRLUnitDTO(); + curUnit.setId(UNITID_CUR); + curUnit.setMeasure("iso4217:" + xbrlData.getCurrency()); + document.getUnits().add(curUnit); + + return document; + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/XBRLIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/XBRLIntegrationTest.java index 61d023f638e..63e2ef356f9 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/XBRLIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/XBRLIntegrationTest.java @@ -64,4 +64,28 @@ private void verifyTaxonomyList(final ArrayList taxonomyList) { assertEquals("AdministrativeExpense", taxonomyList.get(0).get("name"), "Checking for the 1st taxonomy"); } + @Test + public void shouldSerializeMixXBRLReportToXMLNatively() throws Exception { + LOG.info("--------------------VERIFYING NATIVE JAXB XML SERIALIZATION CONTRACT--------------------------"); + + org.apache.fineract.mix.data.MixReportXBRLDocument mockDocument = new org.apache.fineract.mix.data.MixReportXBRLDocument(); + + jakarta.xml.bind.JAXBContext context = jakarta.xml.bind.JAXBContext + .newInstance(org.apache.fineract.mix.data.MixReportXBRLDocument.class); + jakarta.xml.bind.Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(jakarta.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + + java.io.StringWriter writer = new java.io.StringWriter(); + marshaller.marshal(mockDocument, writer); + String xmlResponse = writer.toString(); + + LOG.info("GENERATED NATIVE XBRL XML REPORT CONTENT:\n{}", xmlResponse); + + org.junit.jupiter.api.Assertions.assertNotNull(xmlResponse, "The serialized XML output should not be null"); + + org.junit.jupiter.api.Assertions.assertTrue(xmlResponse.toLowerCase().contains("xbrl"), + "Serialized XML must contain the xbrl root context schema identifier"); + + LOG.info("--------------------XML NATIVE SERIALIZATION VERIFIED SUCCESSFULLY--------------------------"); + } }