From c71dc019283754d764ca5d327ec67ccb19c371e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?James=20Pether=20S=C3=B6rling?= Date: Thu, 2 Jan 2025 15:42:56 +0100 Subject: [PATCH] feat: Enhance Parliament Decision Flow visualization efficiency (#6929) * feat: Enhance Parliament Decision Flow visualization efficiency Improved legislative process transparency by optimizing committee decision flow charts and summaries. Dynamic year range generation ensures future-proof temporal analysis of parliamentary activities. Better UX for tracking policy development patterns across committees. Part of democratic oversight tooling. * fix locale * cleanup --- .../impl/DecisionFlowChartManagerImpl.java | 488 ++++++++++------ .../impl/DecisionDataFactoryImpl.java | 236 ++++---- ...DecisionFlowPageModContentFactoryImpl.java | 262 ++++++--- ...DecisionFlowPageModContentFactoryImpl.java | 251 ++++++-- .../external/esv/impl/EsvExcelReaderImpl.java | 470 ++++++++------- ...ernmentBodyOperationOutcomeReaderImpl.java | 549 +++++++++++------- .../main/resources/Myndighetsinformation.xls | Bin 1394688 -> 1441792 bytes .../external/esv/impl/EsvApiITest.java | 25 +- 8 files changed, 1413 insertions(+), 868 deletions(-) diff --git a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/chartfactory/impl/DecisionFlowChartManagerImpl.java b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/chartfactory/impl/DecisionFlowChartManagerImpl.java index 2f4c1db918c..bd10393ef61 100644 --- a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/chartfactory/impl/DecisionFlowChartManagerImpl.java +++ b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/chartfactory/impl/DecisionFlowChartManagerImpl.java @@ -21,10 +21,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -41,6 +40,7 @@ @Service public final class DecisionFlowChartManagerImpl implements DecisionFlowChartManager { + /** The decision data factory. */ @Autowired private DecisionDataFactory decisionDataFactory; @@ -51,256 +51,378 @@ public DecisionFlowChartManagerImpl() { super(); } + /** + * Creates the all decision flow. + * + * @param committeeMap the committee map + * @param rm the rm + * @return the sankey chart + */ @Override public SankeyChart createAllDecisionFlow(final Map> committeeMap, - final String rm) { - // Retrieve all proposals for the given 'rm' + final String rm) { + validateInput(committeeMap, rm); final List committeeSummaries = decisionDataFactory.createCommitteeSummary(rm); - - // Prepare the sankey chart - final SankeyChart chart = new SankeyChart(); - - // Group by organization - final Map> orgProposalMap = - committeeSummaries.stream() - .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getOrg)); - - // For each org, if we have a matching committee in 'committeeMap', add data rows - for (final Entry> orgEntry : orgProposalMap.entrySet()) { - final String orgKey = orgEntry.getKey(); - final List orgSummaries = orgEntry.getValue(); - if (committeeMap.containsKey(orgKey)) { - final Optional committeeOpt = - committeeMap.get(orgKey).stream().findFirst(); - if (committeeOpt.isPresent()) { - final ViewRiksdagenCommittee committee = committeeOpt.get(); - addDocTypeDataRows(chart, orgSummaries, committee); - addDecisionDataRows(chart, orgSummaries, committee); - } - } - } - - chart.drawChart(); - return chart; + return createAllDecisionFlowChart(committeeMap, committeeSummaries); } + /** + * Creates the committee decision flow. + * + * @param viewRiksdagenCommittee the view riksdagen committee + * @param committeeMap the committee map + * @param rm the rm + * @return the sankey chart + */ @Override public SankeyChart createCommitteeDecisionFlow(final ViewRiksdagenCommittee viewRiksdagenCommittee, - final Map> committeeMap, - final String rm) { + final Map> committeeMap, + final String rm) { + validateCommitteeInput(viewRiksdagenCommittee, committeeMap, rm); final List committeeSummaries = decisionDataFactory.createCommitteeSummary(rm); - - final SankeyChart chart = new SankeyChart(); - // Group by organization - final Map> orgProposalMap = - committeeSummaries.stream() - .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getOrg)); - - final String targetOrg = viewRiksdagenCommittee.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH); - for (final Entry> orgEntry : orgProposalMap.entrySet()) { - final String orgKeyUpper = orgEntry.getKey().toUpperCase(Locale.ENGLISH); - if (committeeMap.containsKey(orgKeyUpper) && targetOrg.equals(orgKeyUpper)) { - addDocTypeDecisionDataRows(chart, orgEntry.getValue()); - } - } - - chart.drawChart(); - return chart; + return createCommitteeSpecificChart(viewRiksdagenCommittee, committeeSummaries); } + /** + * Creates the committeee decision summary. + * + * @param committeeMap the committee map + * @param rm the rm + * @return the text area + */ @Override public TextArea createCommitteeeDecisionSummary(final Map> committeeMap, - final String rm) { - final TextArea area = new TextArea("Summary"); - final StringBuilder builder = new StringBuilder(); + final String rm) { + validateInput(committeeMap, rm); + final List committeeSummaries = decisionDataFactory.createCommitteeSummary(rm); + return createGeneralSummaryTextArea(committeeMap, committeeSummaries); + } + /** + * Creates the committeee decision summary. + * + * @param viewRiksdagenCommittee the view riksdagen committee + * @param rm the rm + * @return the text area + */ + @Override + public TextArea createCommitteeeDecisionSummary(final ViewRiksdagenCommittee viewRiksdagenCommittee, + final String rm) { + validateCommitteeInput(viewRiksdagenCommittee, null, rm); final List committeeSummaries = decisionDataFactory.createCommitteeSummary(rm); + return createSpecificCommitteeSummary(viewRiksdagenCommittee, committeeSummaries); + } - // Group by organization - final Map> orgProposalMap = - committeeSummaries.stream() - .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getOrg)); - - for (final Entry> orgEntry : orgProposalMap.entrySet()) { - final String orgKeyUpper = orgEntry.getKey().toUpperCase(Locale.ENGLISH); - if (committeeMap.containsKey(orgKeyUpper)) { - // Grab the first committee from the list - final ViewRiksdagenCommittee committee = - committeeMap.get(orgKeyUpper).stream().findFirst().orElse(null); - addCommiteeSummary(builder, orgEntry.getValue(), committee); - } + /** + * Validate input. + * + * @param committeeMap the committee map + * @param rm the rm + */ + private void validateInput(Map> committeeMap, String rm) { + if (committeeMap == null || committeeMap.isEmpty()) { + throw new IllegalArgumentException("Committee map cannot be null or empty"); } - area.setValue(builder.toString()); - return area; + validateRm(rm); } - @Override - public TextArea createCommitteeeDecisionSummary(final ViewRiksdagenCommittee viewRiksdagenCommittee, - final String rm) { - final TextArea area = new TextArea("Summary"); - final StringBuilder builder = new StringBuilder(); + /** + * Validate committee input. + * + * @param committee the committee + * @param committeeMap the committee map + * @param rm the rm + */ + private void validateCommitteeInput(ViewRiksdagenCommittee committee, + Map> committeeMap, + String rm) { + if (committee == null || committee.getEmbeddedId() == null) { + throw new IllegalArgumentException("Committee cannot be null"); + } + validateRm(rm); + } - final List committeeSummaries = decisionDataFactory.createCommitteeSummary(rm); + /** + * Validate rm. + * + * @param rm the rm + */ + private void validateRm(String rm) { + if (StringUtils.isBlank(rm)) { + throw new IllegalArgumentException("RM parameter cannot be blank"); + } + } - // Group by organization - final Map> orgProposalMap = - committeeSummaries.stream() - .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getOrg)); + /** + * Creates the all decision flow chart. + * + * @param committeeMap the committee map + * @param summaries the summaries + * @return the sankey chart + */ + private SankeyChart createAllDecisionFlowChart(Map> committeeMap, + List summaries) { + final SankeyChart chart = initializeChart("Parliamentary Decision Flow"); + + final Map> orgProposalMap = summaries.stream() + .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getOrg)); + + orgProposalMap.forEach((org, orgSummaries) -> { + if (committeeMap.containsKey(org) && !committeeMap.get(org).isEmpty()) { + final ViewRiksdagenCommittee committee = committeeMap.get(org).get(0); + addDocTypeDataRows(chart, orgSummaries, committee); + addDecisionDataRows(chart, orgSummaries, committee); + } + }); + chart.drawChart(); + return chart; + } - final String targetOrg = viewRiksdagenCommittee.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH); + /** + * Creates the committee specific chart. + * + * @param committee the committee + * @param summaries the summaries + * @return the sankey chart + */ + private SankeyChart createCommitteeSpecificChart(ViewRiksdagenCommittee committee, + List summaries) { + final SankeyChart chart = initializeChart("Committee Decision Flow: " + committee.getEmbeddedId().getDetail()); + final String targetOrg = committee.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH); - // Retrieve the relevant summaries for the single committee - final List list = orgProposalMap.get(targetOrg); - addCommiteeSummary(builder, list, viewRiksdagenCommittee); + final List committeeSummaries = summaries.stream() + .filter(summary -> targetOrg.equals(summary.getOrg().toUpperCase(Locale.ENGLISH))) + .collect(Collectors.toList()); - area.setValue(builder.toString()); - return area; + addDocTypeDecisionFlowData(chart, committeeSummaries); + chart.drawChart(); + return chart; } /** - * Helper to add doc type data rows to sankey. + * Adds the doc type data rows. * - * @param chart the chart - * @param proposals the summary list for the org + * @param chart the chart + * @param summaries the summaries * @param committee the committee */ - private static void addDocTypeDataRows(final SankeyChart chart, - final List proposals, - final ViewRiksdagenCommittee committee) { - // Group by doc type - final Map> docTypeMap = - proposals.stream().collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDocType)); + private void addDocTypeDataRows(SankeyChart chart, + List summaries, + ViewRiksdagenCommittee committee) { + final Map docTypeCounts = summaries.stream() + .collect(Collectors.groupingBy( + ProposalCommitteeeSummary::getDocType, + Collectors.counting() + )); final String committeeName = committee.getEmbeddedId().getDetail(); - for (final Entry> docEntry : docTypeMap.entrySet()) { - final String docType = docEntry.getKey(); - if (!docType.isEmpty()) { - chart.addDataRow(docType, committeeName, docEntry.getValue().size()); + docTypeCounts.forEach((docType, count) -> { + if (StringUtils.isNotEmpty(docType)) { + chart.addDataRow(docType, committeeName, count.intValue()); } - } + }); } /** - * Helper to add decision data rows to sankey. + * Adds the decision data rows. * - * @param chart the chart - * @param proposals the summary list for the org + * @param chart the chart + * @param summaries the summaries * @param committee the committee */ - private static void addDecisionDataRows(final SankeyChart chart, - final List proposals, - final ViewRiksdagenCommittee committee) { - // Group by decision - final Map> decisionMap = - proposals.stream().collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDecision)); + private void addDecisionDataRows(SankeyChart chart, + List summaries, + ViewRiksdagenCommittee committee) { + final Map decisionCounts = summaries.stream() + .collect(Collectors.groupingBy( + ProposalCommitteeeSummary::getDecision, + Collectors.counting() + )); final String committeeName = committee.getEmbeddedId().getDetail(); - for (final Entry> decisionEntry : decisionMap.entrySet()) { - final String decision = decisionEntry.getKey(); - if (!decision.isEmpty()) { - chart.addDataRow(committeeName, decision, decisionEntry.getValue().size()); + decisionCounts.forEach((decision, count) -> { + if (StringUtils.isNotEmpty(decision)) { + chart.addDataRow(committeeName, decision, count.intValue()); } - } + }); + } + + /** + * Adds the doc type decision flow data. + * + * @param chart the chart + * @param summaries the summaries + */ + private void addDocTypeDecisionFlowData(SankeyChart chart, + List summaries) { + summaries.stream() + .filter(s -> StringUtils.isNotEmpty(s.getDocType()) && StringUtils.isNotEmpty(s.getDecision())) + .collect(Collectors.groupingBy( + ProposalCommitteeeSummary::getDocType, + Collectors.groupingBy( + ProposalCommitteeeSummary::getDecision, + Collectors.counting() + ) + )) + .forEach((docType, decisionMap) -> + decisionMap.forEach((decision, count) -> + chart.addDataRow(docType, decision, count.intValue()) + ) + ); } /** - * Helper to add doc type -> decision data rows to sankey. + * Creates the general summary text area. * - * @param chart the chart - * @param proposals the summary list for the org + * @param committeeMap the committee map + * @param summaries the summaries + * @return the text area */ - private static void addDocTypeDecisionDataRows(final SankeyChart chart, - final List proposals) { - // Group by doc type - final Map> docTypeMap = - proposals.stream().collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDocType)); - - for (final Entry> docEntry : docTypeMap.entrySet()) { - final String docType = docEntry.getKey(); - if (!docType.isEmpty()) { - // Now group by decision - final Map> decisionMap = - docEntry.getValue().stream() - .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDecision)); - for (final Entry> decEntry : decisionMap.entrySet()) { - final String decision = decEntry.getKey(); - if (!decision.isEmpty()) { - chart.addDataRow(docType, decision, decEntry.getValue().size()); - } + private TextArea createGeneralSummaryTextArea(Map> committeeMap, + List summaries) { + final StringBuilder builder = new StringBuilder(); + + committeeMap.forEach((org, committees) -> { + if (!committees.isEmpty()) { + final List orgSummaries = summaries.stream() + .filter(s -> org.equals(s.getOrg())) + .collect(Collectors.toList()); + + if (!orgSummaries.isEmpty()) { + addCommitteeSummarySection(builder, orgSummaries, committees.get(0)); } } - } + }); + + return createFormattedTextArea("Summary", builder.toString()); } /** - * Helper to create a textual committee summary in a StringBuilder. + * Creates the specific committee summary. * - * @param builder the StringBuilder - * @param proposals the proposal summaries for this committee - * @param committee the committee + * @param committee the committee + * @param summaries the summaries + * @return the text area */ - private static void addCommiteeSummary(final StringBuilder builder, - final List proposals, - final ViewRiksdagenCommittee committee) { - // If no committee or proposals, bail out - if (committee == null || proposals == null || proposals.isEmpty()) { + private TextArea createSpecificCommitteeSummary(ViewRiksdagenCommittee committee, + List summaries) { + final StringBuilder builder = new StringBuilder(); + final String targetOrg = committee.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH); + + final List committeeSummaries = summaries.stream() + .filter(s -> targetOrg.equals(s.getOrg().toUpperCase(Locale.ENGLISH))) + .collect(Collectors.toList()); + + addCommitteeSummarySection(builder, committeeSummaries, committee); + return createFormattedTextArea("Committee Summary", builder.toString()); + } + + /** + * Adds the committee summary section. + * + * @param builder the builder + * @param summaries the summaries + * @param committee the committee + */ + private void addCommitteeSummarySection(StringBuilder builder, + List summaries, + ViewRiksdagenCommittee committee) { + if (committee == null || summaries.isEmpty()) { return; } - final String detail = committee.getEmbeddedId().getDetail(); - builder.append('\n').append(detail); + builder.append('\n') + .append(committee.getEmbeddedId().getDetail()); - // Group by doc type - final Map> docTypeMap = - proposals.stream().collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDocType)); + final Map> docTypeMap = summaries.stream() + .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDocType)); - for (final Entry> docEntry : docTypeMap.entrySet()) { - addEntry(builder, docEntry); - } + docTypeMap.forEach((docType, docTypeSummaries) -> + addDocTypeSummary(builder, docType, docTypeSummaries)); } /** - * Build a textual representation of doc type -> decision -> proposals. + * Adds the doc type summary. * * @param builder the builder - * @param docEntry the doc entry + * @param docType the doc type + * @param summaries the summaries */ - private static void addEntry(final StringBuilder builder, - final Entry> docEntry) { - final String docType = docEntry.getKey(); - final List docTypeList = docEntry.getValue(); + private void addDocTypeSummary(StringBuilder builder, + String docType, + List summaries) { + if (StringUtils.isEmpty(docType)) { + return; + } builder.append("\n ( ") - .append(docTypeList.size()) + .append(summaries.size()) .append(' ') .append(docType) .append(" -> "); - // Group by decision - final Map> decisionMap = - docTypeList.stream() - .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDecision)); - - for (final Entry> decisionEntry : decisionMap.entrySet()) { - final String decision = decisionEntry.getKey(); - if (!decision.isEmpty()) { - builder.append("\n ") - .append(decisionEntry.getValue().size()) - .append(' ') - .append(decision) - .append(' '); - - for (final ProposalCommitteeeSummary summary : decisionEntry.getValue()) { - builder.append("\n ") - .append(summary.getDecision()) - .append(':') - .append(summary.getWording()) - .append(' ') - .append(summary.getWording2()) - .append(' '); - } - } - } + final Map> decisionMap = summaries.stream() + .collect(Collectors.groupingBy(ProposalCommitteeeSummary::getDecision)); + + decisionMap.forEach((decision, decisionSummaries) -> + addDecisionSummary(builder, decision, decisionSummaries)); + builder.append(')'); } -} + + /** + * Adds the decision summary. + * + * @param builder the builder + * @param decision the decision + * @param summaries the summaries + */ + private void addDecisionSummary(StringBuilder builder, + String decision, + List summaries) { + if (StringUtils.isEmpty(decision)) { + return; + } + + builder.append("\n ") + .append(summaries.size()) + .append(' ') + .append(decision); + + summaries.forEach(summary -> + builder.append("\n ") + .append(decision) + .append(':') + .append(summary.getWording()) + .append(' ') + .append(summary.getWording2())); + } + + /** + * Initialize chart. + * + * @param title the title + * @return the sankey chart + */ + private SankeyChart initializeChart(String title) { + final SankeyChart chart = new SankeyChart(); + chart.setCaption(title); + chart.setWidth("100%"); + return chart; + } + + /** + * Creates the formatted text area. + * + * @param caption the caption + * @param content the content + * @return the text area + */ + private TextArea createFormattedTextArea(String caption, String content) { + final TextArea area = new TextArea(caption); + area.setValue(content); + area.setWidth("100%"); + area.setReadOnly(true); + return area; + } +} \ No newline at end of file diff --git a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/dataseriesfactory/impl/DecisionDataFactoryImpl.java b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/dataseriesfactory/impl/DecisionDataFactoryImpl.java index 32df2ff41c0..e2d1ea72b72 100644 --- a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/dataseriesfactory/impl/DecisionDataFactoryImpl.java +++ b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/common/dataseriesfactory/impl/DecisionDataFactoryImpl.java @@ -18,11 +18,13 @@ */ package com.hack23.cia.web.impl.ui.application.views.common.dataseriesfactory.impl; -import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; @@ -31,37 +33,40 @@ import com.hack23.cia.model.external.riksdagen.dokumentstatus.impl.DocumentProposalData; import com.hack23.cia.model.external.riksdagen.dokumentstatus.impl.DocumentStatusContainer; import com.hack23.cia.service.api.ApplicationManager; -import com.hack23.cia.service.api.DataContainer; import com.hack23.cia.web.impl.ui.application.views.common.dataseriesfactory.api.DecisionDataFactory; import com.hack23.cia.web.impl.ui.application.views.common.dataseriesfactory.api.ProposalCommitteeeSummary; /** - * The Class DecisionDataFactoryImpl. + * Implementation of DecisionDataFactory for processing parliamentary committee decisions. + * Handles document status data and creates summaries of committee proposals. */ @Service @Transactional(propagation = Propagation.REQUIRED, readOnly = true) public final class DecisionDataFactoryImpl implements DecisionDataFactory { - /** Common text constants for doc type. */ - private static final String PROP = "Proposition"; + /** Document type constants. */ + private static final String PROPOSITION = "Proposition"; /** The Constant MOTION. */ private static final String MOTION = "Motion"; - /** Known min and max length constraints from original code. */ + /** The Constant PROP_TYPE. */ + private static final String PROP_TYPE = "prop"; + + /** Chamber text length constraints for valid proposals. */ private static final int CHAMBER_MIN_LENGTH = "avslag".length(); // 6 /** The Constant CHAMBER_MAX_LENGTH. */ private static final int CHAMBER_MAX_LENGTH = "återförvisning till utskottet".length(); // 29 /** - * Pattern for cleaning up the 'chamber' string. Aggregates multiple .replace() calls - * into a single regex. This pattern will remove or replace these tokens: - * (UTSKOTTET), ( or ), UTBSKOTTET, UBTSKOTTET, UTKOTTET - * - * We'll do it case-insensitively, but remember we do an uppercase pass anyway. + * Pattern for standardizing committee decision text. + * Removes or replaces parliamentary specific tokens: + * - (UTSKOTTET) + * - Parentheses + * - Various committee spelling variants */ - private static final Pattern CLEANUP_PATTERN = Pattern.compile( + private static final Pattern COMMITTEE_TEXT_CLEANUP_PATTERN = Pattern.compile( "(\\(UTSKOTTET\\))|(\\()|(\\))|(UTBSKOTTET)|(UBTSKOTTET)|(UTKOTTET)", Pattern.CASE_INSENSITIVE ); @@ -71,134 +76,155 @@ public final class DecisionDataFactoryImpl implements DecisionDataFactory { private ApplicationManager applicationManager; /** - * Instantiates a new decision data factory impl. + * Creates committee summaries for a specific processing location. + * + * @param processedIn the location where proposals were processed + * @return List of committee summaries + * @throws IllegalArgumentException if processedIn is blank */ - public DecisionDataFactoryImpl() { - super(); + @Override + public List createCommitteeSummary(final String processedIn) { + validateProcessedIn(processedIn); + + return applicationManager.getDataContainer(DocumentStatusContainer.class) + .getAll() + .parallelStream() + .filter(doc -> isValidDocument(doc, processedIn)) + .map(doc -> createProposalSummary(doc)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); } /** - * Creates the committee summary. + * Validates the processedIn parameter. * - * @param processedIn the processed in - * @return the list + * @param processedIn location to validate + * @throws IllegalArgumentException if validation fails */ - @Override - public List createCommitteeSummary(final String processedIn) { - final List summary = new ArrayList<>(); - - // NOTE: Still loads ALL DocumentStatusContainer rows - final DataContainer dataContainer = - applicationManager.getDataContainer(DocumentStatusContainer.class); - - // For each doc, potentially add a summary - for (final DocumentStatusContainer document : dataContainer.getAll()) { - addProposalCommitteeeSummary(processedIn, summary, document); + private void validateProcessedIn(final String processedIn) { + if (StringUtils.isBlank(processedIn)) { + throw new IllegalArgumentException("ProcessedIn parameter cannot be blank"); } - return summary; } /** - * Check and build a summary if the document qualifies based on 'processedIn' and - * the proposal/chamber constraints. + * Validates if a document meets the criteria for processing. * - * @param processedIn the processed in - * @param summary the summary - * @param document the document + * @param document the document to validate + * @param processedIn the required processing location + * @return true if document is valid for processing */ - private static void addProposalCommitteeeSummary(final String processedIn, - final List summary, final DocumentStatusContainer document) { - - // Null checks on 'documentProposal' and 'proposal' - if (document.getDocumentProposal() == null || + private boolean isValidDocument(final DocumentStatusContainer document, final String processedIn) { + if (document == null || document.getDocumentProposal() == null || document.getDocumentProposal().getProposal() == null) { - return; + return false; } final DocumentProposalData proposal = document.getDocumentProposal().getProposal(); - final String chamber = proposal.getChamber(); - // Are we processed in the correct place? - if ((chamber == null) || proposal.getProcessedIn() == null || proposal.getProcessedIn().isEmpty()) { - return; - } - // Do we have a non-empty 'committee'? - // Does 'processedIn' match? - if (proposal.getCommittee() == null || proposal.getCommittee().isEmpty() || !proposal.getProcessedIn().contains(processedIn)) { - return; - } - // Is the chamber length within the specified min/max? - final int length = chamber.length(); - if (length < CHAMBER_MIN_LENGTH || length > CHAMBER_MAX_LENGTH) { - return; + return isValidProposal(proposal, processedIn); + } + + /** + * Validates proposal data for processing requirements. + * + * @param proposal the proposal to validate + * @param processedIn the required processing location + * @return true if proposal meets requirements + */ + private boolean isValidProposal(final DocumentProposalData proposal, final String processedIn) { + if (proposal == null || StringUtils.isBlank(proposal.getChamber()) || + StringUtils.isBlank(proposal.getProcessedIn()) || + StringUtils.isBlank(proposal.getCommittee())) { + return false; } - // If all checks pass, add the summary - summary.add(new ProposalCommitteeeSummary( - getCommitteeShortName(proposal), - getDocumentName(document), - cleanupDecision(chamber), - document.getDocument().getHangarId(), - proposal.getWording(), - proposal.getWording2(), - proposal.getDecisionType() - )); + final int chamberLength = proposal.getChamber().length(); + return chamberLength >= CHAMBER_MIN_LENGTH && + chamberLength <= CHAMBER_MAX_LENGTH && + proposal.getProcessedIn().contains(processedIn); + } + + /** + * Creates a ProposalCommitteeeSummary from a valid document. + * + * @param document source document + * @return ProposalCommitteeeSummary or null if invalid + */ + private ProposalCommitteeeSummary createProposalSummary(final DocumentStatusContainer document) { + try { + final DocumentProposalData proposal = document.getDocumentProposal().getProposal(); + return new ProposalCommitteeeSummary( + extractCommitteeShortName(proposal), + determineDocumentType(document), + standardizeDecisionText(proposal.getChamber()), + document.getDocument().getHangarId(), + proposal.getWording(), + proposal.getWording2(), + proposal.getDecisionType() + ); + } catch (final Exception e) { + // Log error if needed + return null; + } } /** - * Cleanup the 'chamber' string by uppercasing, removing tokens via regex, - * and normalizing "UTKOTTET" to "UTSKOTTET" if it appears alone. + * Standardizes the decision text by removing variations and normalizing format. * - * @param chamber the chamber - * @return the string + * @param chamber the original chamber text + * @return standardized decision text */ - private static String cleanupDecision(final String chamber) { - // Convert to uppercase English - String upper = chamber.toUpperCase(Locale.ENGLISH); + private static String standardizeDecisionText(final String chamber) { + if (StringUtils.isBlank(chamber)) { + return ""; + } - // Remove or replace known substrings with the pattern - upper = CLEANUP_PATTERN.matcher(upper).replaceAll(""); + String standardized = chamber.toUpperCase(Locale.ENGLISH); + standardized = COMMITTEE_TEXT_CLEANUP_PATTERN.matcher(standardized).replaceAll(""); - // If we find "UTKOTTET" alone, replace it with "UTSKOTTET" - // (We already removed parentheses above). - if (upper.contains("UTKOTTET")) { - upper = upper.replace("UTKOTTET", "UTSKOTTET"); + // Normalize committee spelling + if (standardized.contains("UTKOTTET")) { + standardized = standardized.replace("UTKOTTET", "UTSKOTTET"); } - return upper.trim(); + + return standardized.trim(); } /** - * Extract the committee short name from 'processedIn' by removing digits - * and certain punctuation, converting to uppercase, and truncating if comma found. + * Extracts the standardized committee short name. * - * @param proposal the proposal - * @return the committee short name + * @param proposal source proposal + * @return committee short name */ - private static String getCommitteeShortName(final DocumentProposalData proposal) { - // e.g. "UU12" => "UU" then uppercase => "UU" - final String upperCase = proposal.getProcessedIn() - .replaceAll("\\d", "") - .replace("/:", "") - .toUpperCase(Locale.ENGLISH); - // If there's a comma, only take up to that comma - final int commaIndex = upperCase.indexOf(','); - return (commaIndex >= 0) ? upperCase.substring(0, commaIndex) : upperCase; + private static String extractCommitteeShortName(final DocumentProposalData proposal) { + final String processedIn = proposal.getProcessedIn(); + if (StringUtils.isBlank(processedIn)) { + return ""; + } + + final String shortName = processedIn + .replaceAll("\\d", "") + .replace("/:", "") + .toUpperCase(Locale.ENGLISH); + + final int commaIndex = shortName.indexOf(','); + return (commaIndex >= 0) ? shortName.substring(0, commaIndex) : shortName; } /** - * Return a human-readable doc name: "Proposition" if docType="prop", - * or doc.getSubType() if subType is long enough, else "Motion". + * Determines the human-readable document type. * - * @param document the document - * @return the document name + * @param document source document + * @return document type string */ - private static String getDocumentName(final DocumentStatusContainer document) { - if ("prop".equalsIgnoreCase(document.getDocument().getDocumentType())) { - return PROP; - } else if (document.getDocument().getSubType() != null - && document.getDocument().getSubType().length() > MOTION.length()) { - return document.getDocument().getSubType(); - } else { - return MOTION; + private static String determineDocumentType(final DocumentStatusContainer document) { + if (PROP_TYPE.equalsIgnoreCase(document.getDocument().getDocumentType())) { + return PROPOSITION; } + + final String subType = document.getDocument().getSubType(); + return (subType != null && subType.length() > MOTION.length()) + ? subType + : MOTION; } -} +} \ No newline at end of file diff --git a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/committee/pagemode/CommitteeDecisionFlowPageModContentFactoryImpl.java b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/committee/pagemode/CommitteeDecisionFlowPageModContentFactoryImpl.java index 670d3d3fa7b..4393be2b9d7 100644 --- a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/committee/pagemode/CommitteeDecisionFlowPageModContentFactoryImpl.java +++ b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/committee/pagemode/CommitteeDecisionFlowPageModContentFactoryImpl.java @@ -18,12 +18,15 @@ */ package com.hack23.cia.web.impl.ui.application.views.user.committee.pagemode; -import java.util.Arrays; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -52,68 +55,195 @@ @Component public final class CommitteeDecisionFlowPageModContentFactoryImpl extends AbstractCommitteePageModContentFactoryImpl { - @Autowired - private DecisionFlowChartManager decisionFlowChartManager; - - /** - * Instantiates a new parliament decision flow page mod content factory - * impl. - */ - public CommitteeDecisionFlowPageModContentFactoryImpl() { - super(); - } - - @Secured({ "ROLE_ANONYMOUS", "ROLE_USER", "ROLE_ADMIN" }) - @Override - public Layout createContent(final String parameters, final MenuBar menuBar, final Panel panel) { - final VerticalLayout panelContent = createPanelContent(); - final String pageId = getPageId(parameters); - - final ViewRiksdagenCommittee viewRiksdagenCommittee = getItem(parameters); - getCommitteeMenuItemFactory().createCommitteeeMenuBar(menuBar, pageId); - - createPageHeader(panel, panelContent, "Committee Decision Flow " + viewRiksdagenCommittee.getEmbeddedId().getDetail(), "Decision Flow", "Analyze decision-making processes within committees."); - - String selectedYear = "2022/23"; - if (parameters != null && parameters.contains("[") && parameters.contains("]")) { - selectedYear = parameters.substring(parameters.indexOf('[') + 1, parameters.lastIndexOf(']')); - } - - final ComboBox comboBox = new ComboBox<>("Select year", Collections.unmodifiableList( - Arrays.asList("2024/25", "2023/24", "2022/23","2021/22","2020/21","2019/20","2018/19","2017/18", "2016/17", "2015/16", "2014/15", "2013/14", "2012/13", "2011/12", "2010/11"))); - panelContent.addComponent(comboBox); - panelContent.setExpandRatio(comboBox, ContentRatio.SMALL2); - comboBox.setSelectedItem(selectedYear); - - comboBox.addValueChangeListener(new DecisionFlowValueChangeListener(NAME,pageId)); - - final Map> committeeMap = getApplicationManager() - .getDataContainer(ViewRiksdagenCommittee.class).getAll().stream() - .collect(Collectors.groupingBy(c -> c.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH))); - - final SankeyChart chart = decisionFlowChartManager.createCommitteeDecisionFlow(viewRiksdagenCommittee, - committeeMap, comboBox.getSelectedItem().orElse(selectedYear)); - panelContent.addComponent(chart); - panelContent.setExpandRatio(chart, ContentRatio.LARGE); - - final TextArea textarea = decisionFlowChartManager.createCommitteeeDecisionSummary(viewRiksdagenCommittee,comboBox.getSelectedItem().orElse(selectedYear)); - textarea.setSizeFull(); - panelContent.addComponent(textarea); - panelContent.setExpandRatio(textarea, ContentRatio.SMALL_GRID); - - - - getPageActionEventHelper().createPageEvent(ViewAction.VISIT_COMMITTEE_VIEW, ApplicationEventGroup.USER, NAME, - parameters, pageId); - - return panelContent; - - } - - @Override - public boolean matches(final String page, final String parameters) { - return NAME.equals(page) && StringUtils.contains(parameters, PageMode.CHARTS.toString()) - && parameters.contains(ChartIndicators.DECISION_FLOW_CHART.toString()); - } - -} + /** The Constant DEFAULT_YEAR. */ + private static final String DEFAULT_YEAR = "2023/24"; + + /** The Constant YEAR_SELECTOR_LABEL. */ + private static final String YEAR_SELECTOR_LABEL = "Select year"; + + + /** The decision flow chart manager. */ + @Autowired + private DecisionFlowChartManager decisionFlowChartManager; + + /** + * Creates the content. + * + * @param parameters the parameters + * @param menuBar the menu bar + * @param panel the panel + * @return the layout + */ + @Secured({ "ROLE_ANONYMOUS", "ROLE_USER", "ROLE_ADMIN" }) + @Override + public Layout createContent(final String parameters, final MenuBar menuBar, final Panel panel) { + final VerticalLayout panelContent = createPanelContent(); + final String pageId = getPageId(parameters); + final ViewRiksdagenCommittee committee = getItem(parameters); + + setupMenuAndHeader(menuBar, panel, panelContent, pageId, committee); + final String selectedYear = extractSelectedYear(parameters); + final Map> committeeMap = loadCommitteeMap(); + + addYearSelector(panelContent, selectedYear, pageId); + addDecisionFlowChart(panelContent, committee, committeeMap, selectedYear); + addDecisionSummary(panelContent, committee, selectedYear); + + recordPageVisit(parameters, pageId); + + return panelContent; + } + + /** + * Creates list of available years for selection. + * Goes from 2010/11 up to (currentYear+1)/(currentYear+2). + * + * @return Unmodifiable list of year strings in format "YYYY/YY" + */ + private static List createAvailableYears() { + final int currentYear = LocalDate.now((ZoneOffset.UTC)).getYear(); + return IntStream.rangeClosed(2010, currentYear + 1) + .mapToObj(year -> String.format(Locale.ENGLISH,"%d/%02d", year, (year + 1) % 100)) + .sorted(Comparator.reverseOrder()) // Most recent years first + .collect(Collectors.collectingAndThen( + Collectors.toList(), + Collections::unmodifiableList + )); + } + + /** + * Setup menu and header. + * + * @param menuBar the menu bar + * @param panel the panel + * @param panelContent the panel content + * @param pageId the page id + * @param committee the committee + */ + private void setupMenuAndHeader(MenuBar menuBar, Panel panel, VerticalLayout panelContent, + String pageId, ViewRiksdagenCommittee committee) { + getCommitteeMenuItemFactory().createCommitteeeMenuBar(menuBar, pageId); + createPageHeader(panel, panelContent, + "Committee Decision Flow " + committee.getEmbeddedId().getDetail(), + "Decision Flow", + "Analyze decision-making processes within committees." + ); + } + + /** + * Extract selected year. + * + * @param parameters the parameters + * @return the string + */ + private String extractSelectedYear(String parameters) { + if (parameters != null && parameters.contains("[") && parameters.contains("]")) { + return parameters.substring( + parameters.indexOf('[') + 1, + parameters.lastIndexOf(']') + ); + } + return DEFAULT_YEAR; + } + + /** + * Load committee map. + * + * @return the map + */ + private Map> loadCommitteeMap() { + return getApplicationManager() + .getDataContainer(ViewRiksdagenCommittee.class) + .getAll() + .stream() + .collect(Collectors.groupingBy( + committee -> committee.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH) + )); + } + + /** + * Adds the year selector. + * + * @param panelContent the panel content + * @param selectedYear the selected year + * @param pageId the page id + */ + private void addYearSelector(VerticalLayout panelContent, String selectedYear, String pageId) { + final ComboBox yearSelector = new ComboBox<>(YEAR_SELECTOR_LABEL, createAvailableYears()); + yearSelector.setWidth("200px"); + yearSelector.setEmptySelectionAllowed(false); + yearSelector.setSelectedItem(selectedYear); + yearSelector.addValueChangeListener(new DecisionFlowValueChangeListener(NAME, pageId)); + + panelContent.addComponent(yearSelector); + panelContent.setExpandRatio(yearSelector, ContentRatio.SMALL2); + } + + /** + * Adds the decision flow chart. + * + * @param panelContent the panel content + * @param committee the committee + * @param committeeMap the committee map + * @param selectedYear the selected year + */ + private void addDecisionFlowChart(VerticalLayout panelContent, ViewRiksdagenCommittee committee, + Map> committeeMap, String selectedYear) { + final SankeyChart chart = decisionFlowChartManager.createCommitteeDecisionFlow( + committee, committeeMap, selectedYear + ); + chart.setWidth("100%"); + + panelContent.addComponent(chart); + panelContent.setExpandRatio(chart, ContentRatio.LARGE); + } + + /** + * Adds the decision summary. + * + * @param panelContent the panel content + * @param committee the committee + * @param selectedYear the selected year + */ + private void addDecisionSummary(VerticalLayout panelContent, + ViewRiksdagenCommittee committee, String selectedYear) { + final TextArea summaryArea = decisionFlowChartManager.createCommitteeeDecisionSummary( + committee, selectedYear + ); + summaryArea.setSizeFull(); + summaryArea.setReadOnly(true); + + panelContent.addComponent(summaryArea); + panelContent.setExpandRatio(summaryArea, ContentRatio.SMALL_GRID); + } + + /** + * Record page visit. + * + * @param parameters the parameters + * @param pageId the page id + */ + private void recordPageVisit(String parameters, String pageId) { + getPageActionEventHelper().createPageEvent( + ViewAction.VISIT_COMMITTEE_VIEW, + ApplicationEventGroup.USER, + NAME, + parameters, + pageId + ); + } + + /** + * Matches. + * + * @param page the page + * @param parameters the parameters + * @return true, if successful + */ + @Override + public boolean matches(final String page, final String parameters) { + return NAME.equals(page) + && StringUtils.contains(parameters, PageMode.CHARTS.toString()) + && parameters.contains(ChartIndicators.DECISION_FLOW_CHART.toString()); + } +} \ No newline at end of file diff --git a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/parliament/pagemode/ParliamentDecisionFlowPageModContentFactoryImpl.java b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/parliament/pagemode/ParliamentDecisionFlowPageModContentFactoryImpl.java index 7b6bbe92ced..e557964a7ae 100644 --- a/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/parliament/pagemode/ParliamentDecisionFlowPageModContentFactoryImpl.java +++ b/citizen-intelligence-agency/src/main/java/com/hack23/cia/web/impl/ui/application/views/user/parliament/pagemode/ParliamentDecisionFlowPageModContentFactoryImpl.java @@ -18,12 +18,15 @@ */ package com.hack23.cia.web.impl.ui.application.views.user.parliament.pagemode; -import java.util.Arrays; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -32,7 +35,6 @@ import com.hack23.cia.model.internal.application.data.committee.impl.ViewRiksdagenCommittee; import com.hack23.cia.model.internal.application.system.impl.ApplicationEventGroup; -import com.hack23.cia.service.api.DataContainer; import com.hack23.cia.web.impl.ui.application.action.ViewAction; import com.hack23.cia.web.impl.ui.application.views.common.chartfactory.api.DecisionFlowChartManager; import com.hack23.cia.web.impl.ui.application.views.common.sizing.ContentRatio; @@ -53,66 +55,189 @@ @Component public final class ParliamentDecisionFlowPageModContentFactoryImpl extends AbstractParliamentPageModContentFactoryImpl { + /** The Constant TITLE. */ + private static final String TITLE = "Parliament Decision Flow"; + + /** The Constant SUBTITLE. */ + private static final String SUBTITLE = "Legislative Pipelines"; + + /** The Constant DESCRIPTION. */ + private static final String DESCRIPTION = "Mapping legislative pipelines shaping national policies"; + + /** The Constant DEFAULT_YEAR. */ + private static final String DEFAULT_YEAR = "2023/24"; + + /** The Constant YEAR_SELECTOR_LABEL. */ + private static final String YEAR_SELECTOR_LABEL = "Select year"; + + /** The decision flow chart manager. */ + @Autowired + private DecisionFlowChartManager decisionFlowChartManager; + + /** + * Creates the content. + * + * @param parameters the parameters + * @param menuBar the menu bar + * @param panel the panel + * @return the layout + */ + @Secured({ "ROLE_ANONYMOUS", "ROLE_USER", "ROLE_ADMIN" }) + @Override + public Layout createContent(final String parameters, final MenuBar menuBar, final Panel panel) { + final VerticalLayout panelContent = createPanelContent(); + + setupMenuAndHeader(menuBar, panel, panelContent); + final String selectedYear = extractSelectedYear(parameters); + final Map> committeeMap = loadCommitteeMap(); + + addYearSelector(panelContent, selectedYear); + addDecisionFlowChart(panelContent, committeeMap, selectedYear); + addDecisionSummary(panelContent, committeeMap, selectedYear); + + recordPageVisit(parameters, selectedYear); + + return panelContent; + } + + /** + * Creates list of available years for selection. + * Goes from 2010/11 up to (currentYear+1)/(currentYear+2). + * + * @return Unmodifiable list of year strings in format "YYYY/YY" + */ + private static List createAvailableYears() { + final int currentYear = LocalDate.now((ZoneOffset.UTC)).getYear(); + return IntStream.rangeClosed(2010, currentYear + 1) + .mapToObj(year -> String.format(Locale.ENGLISH,"%d/%02d", year, (year + 1) % 100)) + .sorted(Comparator.reverseOrder()) // Most recent years first + .collect(Collectors.collectingAndThen( + Collectors.toList(), + Collections::unmodifiableList + )); + } + + + /** + * Setup menu and header. + * + * @param menuBar the menu bar + * @param panel the panel + * @param panelContent the panel content + */ + private void setupMenuAndHeader(MenuBar menuBar, Panel panel, VerticalLayout panelContent) { + getParliamentMenuItemFactory().createParliamentTopicMenu(menuBar); + createPageHeader(panel, panelContent, TITLE, SUBTITLE, DESCRIPTION); + } + + /** + * Extract selected year. + * + * @param parameters the parameters + * @return the string + */ + private String extractSelectedYear(String parameters) { + if (parameters != null && parameters.contains("[") && parameters.contains("]")) { + return parameters.substring( + parameters.indexOf('[') + 1, + parameters.lastIndexOf(']') + ); + } + return DEFAULT_YEAR; + } + + /** + * Load committee map. + * + * @return the map + */ + private Map> loadCommitteeMap() { + return getApplicationManager() + .getDataContainer(ViewRiksdagenCommittee.class) + .getAll() + .stream() + .collect(Collectors.groupingBy( + committee -> committee.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH) + )); + } + + /** + * Adds the year selector. + * + * @param panelContent the panel content + * @param selectedYear the selected year + */ + private void addYearSelector(VerticalLayout panelContent, String selectedYear) { + final ComboBox yearSelector = new ComboBox<>(YEAR_SELECTOR_LABEL, createAvailableYears()); + yearSelector.setWidth("200px"); + yearSelector.setEmptySelectionAllowed(false); + yearSelector.setSelectedItem(selectedYear); + yearSelector.addValueChangeListener(new DecisionFlowValueChangeListener(NAME, "")); + + panelContent.addComponent(yearSelector); + panelContent.setExpandRatio(yearSelector, ContentRatio.SMALL); + } + + /** + * Adds the decision flow chart. + * + * @param panelContent the panel content + * @param committeeMap the committee map + * @param selectedYear the selected year + */ + private void addDecisionFlowChart(VerticalLayout panelContent, + Map> committeeMap, String selectedYear) { + final SankeyChart chart = decisionFlowChartManager.createAllDecisionFlow(committeeMap, selectedYear); + chart.setWidth("100%"); + + panelContent.addComponent(chart); + panelContent.setExpandRatio(chart, ContentRatio.LARGE); +} - @Autowired - private DecisionFlowChartManager decisionFlowChartManager; - - /** - * Instantiates a new parliament decision flow page mod content factory impl. - */ - public ParliamentDecisionFlowPageModContentFactoryImpl() { - super(); - } - - @Secured({ "ROLE_ANONYMOUS", "ROLE_USER", "ROLE_ADMIN" }) - @Override - public Layout createContent(final String parameters, final MenuBar menuBar, final Panel panel) { - final VerticalLayout panelContent = createPanelContent(); - getParliamentMenuItemFactory().createParliamentTopicMenu(menuBar); - createPageHeader(panel, panelContent, - "Parliament - Decision Flow", - "Legislative Pipelines", - "Mapping legislative pipelines shaping national policies."); - - - String selectedYear = "2022/23"; - if (parameters != null && parameters.contains("[") && parameters.contains("]")) { - selectedYear = parameters.substring(parameters.indexOf('[') + 1, parameters.lastIndexOf(']')); - } - - final DataContainer dataContainer = getApplicationManager() - .getDataContainer(ViewRiksdagenCommittee.class); - final List allCommittess = dataContainer.getAll(); - - final Map> committeeMap = allCommittess.stream().collect(Collectors.groupingBy(c -> c.getEmbeddedId().getOrgCode().toUpperCase(Locale.ENGLISH))); - - final ComboBox comboBox = new ComboBox<>("Select year", Collections.unmodifiableList(Arrays.asList("2024/25","2023/24","2022/23","2021/22","2020/21","2019/20","2018/19","2017/18","2016/17","2015/16","2014/15","2013/14","2012/13","2011/12","2010/11"))); - panelContent.addComponent(comboBox); - panelContent.setExpandRatio(comboBox, ContentRatio.SMALL); - comboBox.setSelectedItem(selectedYear); - comboBox.addValueChangeListener(new DecisionFlowValueChangeListener(NAME,"")); - - final SankeyChart chart = decisionFlowChartManager.createAllDecisionFlow(committeeMap,comboBox.getSelectedItem().orElse(selectedYear)); - panelContent.addComponent(chart); - panelContent.setExpandRatio(chart, ContentRatio.LARGE); - - final TextArea textarea = decisionFlowChartManager.createCommitteeeDecisionSummary(committeeMap,comboBox.getSelectedItem().orElse(selectedYear)); - textarea.setSizeFull(); - panelContent.addComponent(textarea); - panelContent.setExpandRatio(textarea, ContentRatio.SMALL_GRID); - - - getPageActionEventHelper().createPageEvent(ViewAction.VISIT_PARLIAMENT_RANKING_VIEW, ApplicationEventGroup.USER, NAME, - parameters, selectedYear); - - return panelContent; - - } - - @Override - public boolean matches(final String page, final String parameters) { - return NAME.equals(page) && StringUtils.contains(parameters, PageMode.CHARTS.toString()) - && parameters.contains(ChartIndicators.DECISION_FLOW_CHART.toString()); - } - + /** + * Adds the decision summary. + * + * @param panelContent the panel content + * @param committeeMap the committee map + * @param selectedYear the selected year + */ + private void addDecisionSummary(VerticalLayout panelContent, + Map> committeeMap, String selectedYear) { + final TextArea summaryArea = decisionFlowChartManager.createCommitteeeDecisionSummary(committeeMap, selectedYear); + summaryArea.setSizeFull(); + summaryArea.setReadOnly(true); + + panelContent.addComponent(summaryArea); + panelContent.setExpandRatio(summaryArea, ContentRatio.SMALL_GRID); + } + + /** + * Record page visit. + * + * @param parameters the parameters + * @param selectedYear the selected year + */ + private void recordPageVisit(String parameters, String selectedYear) { + getPageActionEventHelper().createPageEvent( + ViewAction.VISIT_PARLIAMENT_RANKING_VIEW, + ApplicationEventGroup.USER, + NAME, + parameters, + selectedYear + ); + } + + /** + * Matches. + * + * @param page the page + * @param parameters the parameters + * @return true, if successful + */ + @Override + public boolean matches(final String page, final String parameters) { + return NAME.equals(page) + && StringUtils.contains(parameters, PageMode.CHARTS.toString()) + && parameters.contains(ChartIndicators.DECISION_FLOW_CHART.toString()); + } } diff --git a/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvExcelReaderImpl.java b/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvExcelReaderImpl.java index 3759aeb7c36..26d64e03011 100644 --- a/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvExcelReaderImpl.java +++ b/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvExcelReaderImpl.java @@ -19,18 +19,18 @@ package com.hack23.cia.service.external.esv.impl; import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; +import java.io.InputStream; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import com.hack23.cia.service.external.esv.api.GovernmentBodyAnnualSummary; @@ -41,238 +41,230 @@ @Component final class EsvExcelReaderImpl implements EsvExcelReader { - /** The Constant COMMENT_CELL. */ - private static final int COMMENT_CELL = 14; - - /** The Constant VAT_CELL. */ - private static final int VAT_CELL = 8; - - /** The Constant ANNUAL_HEADCOUNT_CELL. */ - private static final int ANNUAL_HEADCOUNT_CELL = 7; - - /** The Constant HEADCOUNT_CELL. */ - private static final int HEADCOUNT_CELL = 6; - - /** The Constant ORG_NUMBER_CELL. */ - private static final int ORG_NUMBER_CELL = 5; - - /** The Constant MINISTRY_CELL. */ - private static final int MINISTRY_CELL = 4; - - /** The Constant MCODE_CELL. */ - private static final int MCODE_CELL = 3; - - /** The Constant GOVERNMENT_BODY_ID_CELL. */ - private static final int GOVERNMENT_BODY_ID_CELL = 3; - - /** The Constant CONSECUTIVE_NUMBER_CELL. */ - private static final int CONSECUTIVE_NUMBER_CELL = 2; - - /** The Constant NAME_CELL. */ - private static final int NAME_CELL = 0; - - /** The Constant EXPECTED_COLUMN_LENGTH. */ - private static final int EXPECTED_COLUMN_LENGTH = 14; - - /** The Constant LOGGER. */ - private static final Logger LOGGER = LoggerFactory.getLogger(EsvExcelReaderImpl.class); - - /** - * Instantiates a new esv excel reader impl. - */ - public EsvExcelReaderImpl() { - super(); - } - - @Override - public Map> getDataPerMinistry(final String name) { - final Map> map = new TreeMap<>(); - try { - final HSSFWorkbook myWorkBook = createGovermentBodyWorkBook(); - - for (int sheetNr = 0; sheetNr < myWorkBook.getNumberOfSheets(); sheetNr++) { - addMinistryPerYearToMap(name, map, myWorkBook.getSheetAt(sheetNr)); - } - - myWorkBook.close(); - } catch (final IOException e) { - LOGGER.warn("Problem loading", e); - } - - return map; - } - - private static HSSFWorkbook createGovermentBodyWorkBook() throws IOException { - return new HSSFWorkbook(EsvExcelReaderImpl.class.getResourceAsStream("/Myndighetsinformation.xls")); - } - - /** - * Adds the ministry per year to map. - * - * @param name - * the name - * @param map - * the map - * @param mySheet - * the my sheet - */ - private static void addMinistryPerYearToMap(final String name, - final Map> map, final HSSFSheet mySheet) { - if (mySheet.getSheetName().chars().allMatch(Character::isDigit)) { - - final int year = Integer.parseInt(mySheet.getSheetName()); - - final List yearList = new ArrayList<>(); - final Iterator rowIterator = mySheet.iterator(); - - // Skip header row, ignore first - rowIterator.next(); - - while (rowIterator.hasNext()) { - addGovernmentBodyAnnualSummaryToList(name, year, yearList, rowIterator.next()); - } - map.put(year, yearList); - } - } - - /** - * Adds the government body annual summary to list. - * - * @param name - * the name - * @param year - * the year - * @param yearList - * the year list - * @param row - * the row - */ - private static void addGovernmentBodyAnnualSummaryToList(final String name, final int year, - final List yearList, final Row row) { - if (row.getLastCellNum() >= EXPECTED_COLUMN_LENGTH) { - - final GovernmentBodyAnnualSummary governmentBodyAnnualSummary = createGovernmentBodyAnnualSummaryFromRow( - year, row); - - if (name == null || name.equalsIgnoreCase(governmentBodyAnnualSummary.getMinistry())) { - yearList.add(governmentBodyAnnualSummary); - } - } - } - - /** - * Gets the integer. - * - * @param str - * the str - * @return the integer - */ - private static int getInteger(final String str) { - if (str == null || str.trim().length() == 0) { - return 0; - } else { - return Integer.parseInt(str); - } - } - - @Override - public Map getDataPerGovernmentBody(final String name) { - final Map map = new TreeMap<>(); - try { - final HSSFWorkbook myWorkBook = createGovermentBodyWorkBook(); - - for (int sheetNr = 0; sheetNr < myWorkBook.getNumberOfSheets(); sheetNr++) { - final HSSFSheet mySheet = myWorkBook.getSheetAt(sheetNr); - - addDataForYearToMap(name, map, mySheet); - } - myWorkBook.close(); - } catch ( - - final IOException e) { - LOGGER.warn("Problem loading", e); - } - - return map; - } - - /** - * Adds the data for year to map. - * - * @param name - * the name - * @param map - * the map - * @param mySheet - * the my sheet - */ - private static void addDataForYearToMap(final String name, final Map map, - final HSSFSheet mySheet) { - if (mySheet.getSheetName().chars().allMatch(Character::isDigit)) { - final int year = Integer.parseInt(mySheet.getSheetName()); - final Iterator rowIterator = mySheet.iterator(); - - rowIterator.next(); - - while (rowIterator.hasNext()) { - addGovernmentBodyAnnualSummaryToMap(name, map, year, rowIterator.next()); - } - } - } - - /** - * Adds the government body annual summary to map. - * - * @param name - * the name - * @param map - * the map - * @param year - * the year - * @param row - * the row - */ - private static void addGovernmentBodyAnnualSummaryToMap(final String name, final Map map, - final int year, final Row row) { - if (row.getLastCellNum() >= EXPECTED_COLUMN_LENGTH) { - - final GovernmentBodyAnnualSummary governmentBodyAnnualSummary = createGovernmentBodyAnnualSummaryFromRow( - year, row); - if (name == null || name.equalsIgnoreCase(governmentBodyAnnualSummary.getName())) { - map.put(year, governmentBodyAnnualSummary); - } - } - } - - /** - * Creates the government body annual summary from row. - * - * @param year - * the year - * @param row - * the row - * @return the government body annual summary - */ - private static GovernmentBodyAnnualSummary createGovernmentBodyAnnualSummaryFromRow(final int year, final Row row) { - return new GovernmentBodyAnnualSummary(year, defaultValueIfNull(row.getCell(NAME_CELL)), getInteger(defaultValueIfNull(row.getCell(CONSECUTIVE_NUMBER_CELL))), - defaultValueIfNull(row.getCell(GOVERNMENT_BODY_ID_CELL)), defaultValueIfNull(row.getCell(MCODE_CELL)), defaultValueIfNull(row.getCell(MINISTRY_CELL)), - defaultValueIfNull(row.getCell(ORG_NUMBER_CELL)), getInteger(defaultValueIfNull(row.getCell(HEADCOUNT_CELL))), getInteger(defaultValueIfNull(row.getCell(ANNUAL_HEADCOUNT_CELL))), - defaultValueIfNull(row.getCell(VAT_CELL)), defaultValueIfNull(row.getCell(COMMENT_CELL))); - } - - /** - * Default value if null. - * - * @param cell - * the cell - * @return the string - */ - private static String defaultValueIfNull(final Cell cell) { - if (cell != null) { - return cell.toString(); - } else { - return ""; - } - } - -} + /** The Constant GOVERNMENT_BODY_EXCEL. */ + private static final String GOVERNMENT_BODY_EXCEL = "/Myndighetsinformation.xls"; + + /** The Constant EXPECTED_COLUMN_LENGTH. */ + private static final int EXPECTED_COLUMN_LENGTH = 14; + + /** + * The Record ExcelColumn. + * + * @param name the name + * @param index the index + */ + private record ExcelColumn(String name, int index) { + + /** + * Gets the value. + * + * @param row the row + * @return the value + */ + public String getValue(Row row) { + final Cell cell = row.getCell(index); + return cell != null ? cell.toString() : ""; + } + + /** + * Gets the int value. + * + * @param row the row + * @return the int value + */ + public int getIntValue(Row row) { + final String value = getValue(row); + return value.trim().isEmpty() ? 0 : Integer.parseInt(value); + } + } + + /** The Constant COLUMNS. */ + private static final ExcelColumn[] COLUMNS = { + new ExcelColumn("NAME", 0), + new ExcelColumn("CONSECUTIVE_NUMBER", 2), + new ExcelColumn("GOVERNMENT_BODY_ID", 3), + new ExcelColumn("MCODE", 3), + new ExcelColumn("MINISTRY", 4), + new ExcelColumn("ORG_NUMBER", 5), + new ExcelColumn("HEADCOUNT", 6), + new ExcelColumn("ANNUAL_HEADCOUNT", 7), + new ExcelColumn("VAT", 8), + new ExcelColumn("COMMENT", 14) + }; + + /** + * Gets the data per ministry. + * + * @param name the name + * @return the data per ministry + */ + @Override + public Map> getDataPerMinistry(final String name) { + final Map> result = new TreeMap<>(); + + try (HSSFWorkbook workbook = loadWorkbook()) { + processSheets(workbook, (sheet, year) -> + result.put(year, processMinistrySheet(sheet, name))); + } catch (final IOException e) { + throw new EsvExcelReaderException("Failed to read ministry data", e); + } + + return result; + } + + /** + * Gets the data per government body. + * + * @param name the name + * @return the data per government body + */ + @Override + public Map getDataPerGovernmentBody(final String name) { + final Map result = new TreeMap<>(); + + try (HSSFWorkbook workbook = loadWorkbook()) { + processSheets(workbook, (sheet, year) -> { + final Optional summary = processGovernmentBodySheet(sheet, year, name); + summary.ifPresent(s -> result.put(year, s)); + }); + } catch (final IOException e) { + throw new EsvExcelReaderException("Failed to read government body data", e); + } + + return result; + } + + /** + * Load workbook. + * + * @return the HSSF workbook + * @throws IOException Signals that an I/O exception has occurred. + */ + private HSSFWorkbook loadWorkbook() throws IOException { + try (InputStream is = EsvExcelReaderImpl.class.getResourceAsStream(GOVERNMENT_BODY_EXCEL)) { + if (is == null) { + throw new IOException("Failed to load resource: " + GOVERNMENT_BODY_EXCEL); + } + return new HSSFWorkbook(is); + } + } + + /** + * The Interface SheetProcessor. + */ + @FunctionalInterface + private interface SheetProcessor { + + /** + * Process. + * + * @param sheet the sheet + * @param year the year + */ + void process(HSSFSheet sheet, int year); + } + + /** + * Process sheets. + * + * @param workbook the workbook + * @param processor the processor + */ + private void processSheets(HSSFWorkbook workbook, SheetProcessor processor) { + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + final HSSFSheet sheet = workbook.getSheetAt(i); + final String sheetName = sheet.getSheetName(); + + if (sheetName.chars().allMatch(Character::isDigit)) { + processor.process(sheet, Integer.parseInt(sheetName)); + } + } + } + + /** + * Process ministry sheet. + * + * @param sheet the sheet + * @param ministryName the ministry name + * @return the list + */ + private List processMinistrySheet(HSSFSheet sheet, String ministryName) { + return StreamSupport.stream(sheet.spliterator(), false) + .skip(1) // Skip header row + .filter(r -> r instanceof Row) + .map(r -> r) + .filter(row -> isValidRow(row)) + .map(row -> createSummary(row, Integer.parseInt(sheet.getSheetName()))) + .filter(summary -> ministryName == null || + ministryName.equalsIgnoreCase(summary.getMinistry())) + .collect(Collectors.toList()); + } + + /** + * Process government body sheet. + * + * @param sheet the sheet + * @param year the year + * @param bodyName the body name + * @return the optional + */ + private Optional processGovernmentBodySheet( + HSSFSheet sheet, int year, String bodyName) { + return StreamSupport.stream(sheet.spliterator(), false) + .skip(1) // Skip header row + .filter(row -> isValidRow(row)) + .map(row -> createSummary(row, year)) + .filter(summary -> bodyName == null || + bodyName.equalsIgnoreCase(summary.getName())) + .findFirst(); + } + + /** + * Checks if is valid row. + * + * @param row the row + * @return true, if is valid row + */ + private boolean isValidRow(Row row) { + return row != null && row.getLastCellNum() >= EXPECTED_COLUMN_LENGTH; + } + + /** + * Creates the summary. + * + * @param row the row + * @param year the year + * @return the government body annual summary + */ + private GovernmentBodyAnnualSummary createSummary(Row row, int year) { + return new GovernmentBodyAnnualSummary( + year, + COLUMNS[0].getValue(row), // NAME + COLUMNS[1].getIntValue(row), // CONSECUTIVE_NUMBER + COLUMNS[2].getValue(row), // GOVERNMENT_BODY_ID + COLUMNS[3].getValue(row), // MCODE + COLUMNS[4].getValue(row), // MINISTRY + COLUMNS[5].getValue(row), // ORG_NUMBER + COLUMNS[6].getIntValue(row), // HEADCOUNT + COLUMNS[7].getIntValue(row), // ANNUAL_HEADCOUNT + COLUMNS[8].getValue(row), // VAT + COLUMNS[9].getValue(row) // COMMENT + ); + } + + /** + * The Class EsvExcelReaderException. + */ + public static class EsvExcelReaderException extends RuntimeException { + + /** + * Instantiates a new esv excel reader exception. + * + * @param message the message + * @param cause the cause + */ + public EsvExcelReaderException(String message, Throwable cause) { + super(message, cause); + } + } +} \ No newline at end of file diff --git a/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvGovernmentBodyOperationOutcomeReaderImpl.java b/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvGovernmentBodyOperationOutcomeReaderImpl.java index 376d62d6356..63fcf5e5c1c 100644 --- a/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvGovernmentBodyOperationOutcomeReaderImpl.java +++ b/service.external.esv/src/main/java/com/hack23/cia/service/external/esv/impl/EsvGovernmentBodyOperationOutcomeReaderImpl.java @@ -25,14 +25,14 @@ import java.nio.charset.StandardCharsets; import java.time.Month; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; +import java.util.Objects; import java.util.stream.Collectors; -import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.csv.CSVFormat; @@ -51,207 +51,342 @@ @Component final class EsvGovernmentBodyOperationOutcomeReaderImpl implements EsvGovernmentBodyOperationOutcomeReader { - /** The Constant ORGANISATIONSNUMMER. */ - private static final String ORGANISATIONSNUMMER = "Organisationsnummer"; - - /** The Constant MYNDIGHET. */ - private static final String MYNDIGHET = "Myndighet"; - - /** The Constant ÅR. */ - private static final String YEAR = "År"; - - /** The Constant UTFALL_DECEMBER. */ - private static final String UTFALL_DECEMBER = "Utfall december"; - - /** The Constant UTFALL_NOVEMBER. */ - private static final String UTFALL_NOVEMBER = "Utfall november"; - - /** The Constant UTFALL_OKTOBER. */ - private static final String UTFALL_OKTOBER = "Utfall oktober"; - - /** The Constant UTFALL_SEPTEMBER. */ - private static final String UTFALL_SEPTEMBER = "Utfall september"; - - /** The Constant UTFALL_AUGUSTI. */ - private static final String UTFALL_AUGUSTI = "Utfall augusti"; - - /** The Constant UTFALL_JULI. */ - private static final String UTFALL_JULI = "Utfall juli"; - - /** The Constant UTFALL_JUNI. */ - private static final String UTFALL_JUNI = "Utfall juni"; - - /** The Constant UTFALL_MAJ. */ - private static final String UTFALL_MAJ = "Utfall maj"; - - /** The Constant UTFALL_APRIL. */ - private static final String UTFALL_APRIL = "Utfall april"; - - /** The Constant UTFALL_MARS. */ - private static final String UTFALL_MARS = "Utfall mars"; - - /** The Constant UTFALL_FEBRUARI. */ - private static final String UTFALL_FEBRUARI = "Utfall februari"; - - /** The Constant UTFALL_JANUARI. */ - private static final String UTFALL_JANUARI = "Utfall januari"; - - /** The Constant SPECIFIC_OUTGOING_FIELDS. */ - private static final String[] SPECIFIC_OUTGOING_FIELDS = { "Inkomsttyp", "Inkomsttypsnamn", "Inkomsthuvudgrupp", "Inkomsthuvudgruppsnamn", "Inkomsttitelgrupp", "Inkomsttitelgruppsnamn", "Inkomsttitel", "Inkomsttitelsnamn", "Inkomstundertitel", "Inkomstundertitelsnamn"}; - - /** The Constant SPECIFIC_INCOMING_FIELDS. */ - private static final String[] SPECIFIC_INCOMING_FIELDS = { "Utgiftsområde", "Utgiftsområdesnamn", "Anslag", "Anslagsnamn", "Anslagspost", "Anslagspostsnamn", "Anslagsdelpost", "Anslagsdelpostsnamn"}; - - /** The esv excel reader. */ - @Autowired - private EsvExcelReader esvExcelReader; - - private List incomeCsvValues; - - private List outGoingCsvValues; - - /** - * Instantiates a new esv government body operation outcome reader impl. - */ - public EsvGovernmentBodyOperationOutcomeReaderImpl() { - super(); - } - - @Override - public synchronized List readIncomeCsv() throws IOException { - if (incomeCsvValues == null) { - incomeCsvValues = readUsingZipInputStream(Request.Get( - "https://www.esv.se/OpenDataManadsUtfallPage/GetFile?documentType=Inkomst&fileType=Zip&fileName=M%C3%A5nadsutfall%20inkomster%20januari%202006%20-%20maj%202024,%20definitivt.zip&Year=2024&month=5&status=Definitiv") - .execute().returnContent().asStream(),SPECIFIC_OUTGOING_FIELDS); - } - return Collections.unmodifiableList(incomeCsvValues); - } - - @Override - public synchronized List readOutgoingCsv() throws IOException { - if (outGoingCsvValues == null) { - outGoingCsvValues = readUsingZipInputStream(Request.Get( - "https://www.esv.se/OpenDataManadsUtfallPage/GetFile?documentType=Utgift&fileType=Zip&fileName=M%C3%A5nadsutfall%20utgifter%20januari%202006%20-%20maj%202024,%20definitivt.zip&Year=2024&month=5&status=Definitiv") - .execute().returnContent().asStream(),SPECIFIC_INCOMING_FIELDS); - } - return Collections.unmodifiableList(outGoingCsvValues); - } - - /** - * Read using zip input stream. - * - * @param inputStream - * the input stream - * @param specificFields - * the specific fields - * @return the list - * @throws IOException - * Signals that an I/O exception has occurred. - */ - private List readUsingZipInputStream(final InputStream inputStream,final String[] specificFields) throws IOException { - final BufferedInputStream bis = new BufferedInputStream(inputStream); - final ZipInputStream is = new ZipInputStream(bis); - - final List list = new ArrayList<>(); - try { - ZipEntry entry; - while ((entry = is.getNextEntry()) != null) { - list.addAll(readCsvContent(is,specificFields)); - } - } finally { - is.close(); - } - return list; - } - - /** - * Read csv content. - * - * @param is - * the is - * @param specificFields - * the specific fields - * @return the list - * @throws IOException - * Signals that an I/O exception has occurred. - */ - private List readCsvContent(final InputStream is,final String[] specificFields) throws IOException { - final CSVParser parser = CSVParser.parse(new InputStreamReader(is,StandardCharsets.UTF_8), CSVFormat.EXCEL.builder().setHeader().setDelimiter(';').build()); - final List records = parser.getRecords(); - records.remove(0); - - final Map> orgMinistryMap = createOrgMinistryMap(esvExcelReader.getDataPerMinistry(null)); - - final List list = new ArrayList<>(); - - for (final CSVRecord csvRecord : records) { - - if (csvRecord.get(ORGANISATIONSNUMMER) != null) { - final GovernmentBodyAnnualOutcomeSummary governmentBodyAnnualOutcomeSummary = new GovernmentBodyAnnualOutcomeSummary(csvRecord.get(MYNDIGHET), csvRecord.get(ORGANISATIONSNUMMER), orgMinistryMap.get(Integer.valueOf(csvRecord.get(YEAR))).get(csvRecord.get(ORGANISATIONSNUMMER).replace("-", "")), Integer.parseInt(csvRecord.get(YEAR))); - - for (final String field : specificFields) { - governmentBodyAnnualOutcomeSummary.addDescriptionField(field,csvRecord.get(field)); - } - - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.JANUARY.getValue(),csvRecord.get(UTFALL_JANUARI)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.FEBRUARY.getValue(),csvRecord.get(UTFALL_FEBRUARI)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.MARCH.getValue(),csvRecord.get(UTFALL_MARS)); - - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.APRIL.getValue(),csvRecord.get(UTFALL_APRIL)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.MAY.getValue(),csvRecord.get(UTFALL_MAJ)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.JUNE.getValue(),csvRecord.get(UTFALL_JUNI)); - - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.JULY.getValue(),csvRecord.get(UTFALL_JULI)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.AUGUST.getValue(),csvRecord.get(UTFALL_AUGUSTI)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.SEPTEMBER.getValue(),csvRecord.get(UTFALL_SEPTEMBER)); - - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.OCTOBER.getValue(),csvRecord.get(UTFALL_OKTOBER)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.NOVEMBER.getValue(),csvRecord.get(UTFALL_NOVEMBER)); - addResultForMonth(governmentBodyAnnualOutcomeSummary,Month.DECEMBER.getValue(),csvRecord.get(UTFALL_DECEMBER)); - - list.add(governmentBodyAnnualOutcomeSummary); - } - } - - return list; - } - - /** - * Creates the org ministry map. - * - * @param data the data - * @return the map - */ - private static Map> createOrgMinistryMap( - final Map> data) { - final Map> orgMinistryMap = new HashMap<>(); - - final Set>> entrySet = data.entrySet(); - - for (final Entry> entry : entrySet) { - orgMinistryMap.put(entry.getKey(), entry.getValue().stream().collect(Collectors.groupingBy(t -> t.getOrgNumber().replace("-","") ,Collectors.collectingAndThen( - Collectors.toList(), - values -> values.get(0).getMinistry())))); - } - - return orgMinistryMap; - } - - /** - * Adds the result for month. - * - * @param governmentBodyAnnualOutcomeSummary - * the government body annual outcome summary - * @param month - * the month - * @param value - * the value - */ - private static void addResultForMonth(final GovernmentBodyAnnualOutcomeSummary governmentBodyAnnualOutcomeSummary, final int month, - final String value) { - if (value != null && value.length() >0 ) { - governmentBodyAnnualOutcomeSummary.addData(month,Double.valueOf(value.replace(",", "."))); - } - } - -} + /** The Constant ESV_BASE_URL. */ + private static final String ESV_BASE_URL = "https://www.esv.se/OpenDataManadsUtfallPage/GetFile"; + + /** The Constant CSV_DELIMITER. */ + private static final String CSV_DELIMITER = ";"; + + /** The Constant CONNECT_TIMEOUT. */ + private static final int CONNECT_TIMEOUT = 30_000; + + /** The Constant CHARSET. */ + private static final String CHARSET = StandardCharsets.UTF_8.name(); + + /** The Constant SWEDISH_MONTH_NAMES. */ + private static final Map SWEDISH_MONTH_NAMES = Map.ofEntries( + Map.entry(Month.JANUARY, "januari"), + Map.entry(Month.FEBRUARY, "februari"), + Map.entry(Month.MARCH, "mars"), + Map.entry(Month.APRIL, "april"), + Map.entry(Month.MAY, "maj"), + Map.entry(Month.JUNE, "juni"), + Map.entry(Month.JULY, "juli"), + Map.entry(Month.AUGUST, "augusti"), + Map.entry(Month.SEPTEMBER, "september"), + Map.entry(Month.OCTOBER, "oktober"), + Map.entry(Month.NOVEMBER, "november"), + Map.entry(Month.DECEMBER, "december") + ); + + /** + * The Record MonthColumn. + * + * @param month the month + * @param columnName the column name + */ + private record MonthColumn(Month month, String columnName) {} + + /** The Constant MONTH_COLUMNS. */ + private static final List MONTH_COLUMNS = Arrays.stream(Month.values()) + .map(month -> new MonthColumn(month, + String.format(Locale.ENGLISH,"Utfall %s", SWEDISH_MONTH_NAMES.get(month)))) + .collect(Collectors.toUnmodifiableList()); + + /** The Constant SPECIFIC_FIELDS. */ + private static final Map SPECIFIC_FIELDS = Map.of( + DataType.INCOME, new String[] { + "Inkomsttyp", "Inkomsttypsnamn", "Inkomsthuvudgrupp", + "Inkomsthuvudgruppsnamn", "Inkomsttitelgrupp", "Inkomsttitelgruppsnamn", + "Inkomsttitel", "Inkomsttitelsnamn", "Inkomstundertitel", "Inkomstundertitelsnamn" + }, + DataType.OUTGOING, new String[] { + "Utgiftsområde", "Utgiftsområdesnamn", "Anslag", "Anslagsnamn", + "Anslagspost", "Anslagspostsnamn", "Anslagsdelpost", "Anslagsdelpostsnamn" + } + ); + + /** The esv excel reader. */ + private final EsvExcelReader esvExcelReader; + + /** + * Instantiates a new esv government body operation outcome reader impl. + * + * @param esvExcelReader the esv excel reader + */ + @Autowired + public EsvGovernmentBodyOperationOutcomeReaderImpl(EsvExcelReader esvExcelReader) { + this.esvExcelReader = esvExcelReader; + } + + /** + * Read income csv. + * + * @return the list + * @throws IOException Signals that an I/O exception has occurred. + */ + @Override + public List readIncomeCsv() throws IOException { + return fetchData(DataType.INCOME); + } + + /** + * Read outgoing csv. + * + * @return the list + * @throws IOException Signals that an I/O exception has occurred. + */ + @Override + public List readOutgoingCsv() throws IOException { + return fetchData(DataType.OUTGOING); + } + + /** + * Fetch data. + * + * @param type the type + * @return the list + * @throws IOException Signals that an I/O exception has occurred. + */ + private List fetchData(DataType type) throws IOException { + final String url = buildUrl(type); + try (InputStream is = executeRequest(url)) { + return processZipStream(is, type); + } + } + + /** + * Builds the url. + * + * @param type the type + * @return the string + */ + private String buildUrl(DataType type) { + return String.format(Locale.ENGLISH,"%s?documentType=%s&fileType=Zip&fileName=M%%C3%%A5nadsutfall%%20%s%%20januari%%202006%%20-%%20november%%202024,%%20definitivt.zip&Year=2024&month=11&status=Definitiv", + ESV_BASE_URL, + type.getDocumentType(), + type.getUrlName()); + } + + /** + * Execute request. + * + * @param url the url + * @return the input stream + * @throws IOException Signals that an I/O exception has occurred. + */ + private InputStream executeRequest(String url) throws IOException { + return Request.Get(url) + .connectTimeout(CONNECT_TIMEOUT) + .socketTimeout(CONNECT_TIMEOUT) + .execute() + .returnContent() + .asStream(); + } + + /** + * Process zip stream. + * + * @param input the input + * @param type the type + * @return the list + * @throws IOException Signals that an I/O exception has occurred. + */ + private List processZipStream(InputStream input, DataType type) + throws IOException { + try (BufferedInputStream bis = new BufferedInputStream(input); + ZipInputStream zis = new ZipInputStream(bis)) { + + final List results = new ArrayList<>(); + while ((zis.getNextEntry()) != null) { + results.addAll(processCsvContent(zis, type)); + } + return Collections.unmodifiableList(results); + } + } + + /** + * Process csv content. + * + * @param is the is + * @param type the type + * @return the list + * @throws IOException Signals that an I/O exception has occurred. + */ + private List processCsvContent(InputStream is, DataType type) + throws IOException { + final CSVParser parser = CSVParser.parse( + new InputStreamReader(is, CHARSET), + CSVFormat.EXCEL.builder() + .setHeader() + .setDelimiter(CSV_DELIMITER) + .build() + ); + + final Map> ministryMap = createOrgMinistryMap( + esvExcelReader.getDataPerMinistry(null) + ); + + return parser.getRecords().stream() + .skip(1) // Skip header + .filter(record -> record.get("Organisationsnummer") != null) + .map(record -> createSummary(record, type, ministryMap)) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + } + + /** + * Creates the summary. + * + * @param record the record + * @param type the type + * @param ministryMap the ministry map + * @return the government body annual outcome summary + */ + private GovernmentBodyAnnualOutcomeSummary createSummary( + CSVRecord record, + DataType type, + Map> ministryMap) { + try { + final String orgNumber = record.get("Organisationsnummer"); + final int year = Integer.parseInt(record.get("År")); + + final GovernmentBodyAnnualOutcomeSummary summary = new GovernmentBodyAnnualOutcomeSummary( + record.get("Myndighet"), + orgNumber, + getMinistry(ministryMap, year, orgNumber), + year + ); + + addFields(summary, record, type); + addMonthlyData(summary, record); + + return summary; + } catch (final Exception e) { + return null; + } + } + + /** + * Adds the fields. + * + * @param summary the summary + * @param record the record + * @param type the type + */ + private void addFields( + GovernmentBodyAnnualOutcomeSummary summary, + CSVRecord record, + DataType type) { + for (final String field : SPECIFIC_FIELDS.get(type)) { + final String value = record.get(field); + if (value != null) { + summary.addDescriptionField(field, value); + } + } + } + + /** + * Adds the monthly data. + * + * @param summary the summary + * @param record the record + */ + private void addMonthlyData( + GovernmentBodyAnnualOutcomeSummary summary, + CSVRecord record) { + MONTH_COLUMNS.forEach(monthData -> { + final String value = record.get(monthData.columnName()); + if (value != null && !value.isEmpty()) { + try { + summary.addData( + monthData.month().getValue(), + Double.valueOf(value.replace(",", ".")) + ); + } catch (final NumberFormatException ignored) {} + } + }); + } + + /** + * Gets the ministry. + * + * @param ministryMap the ministry map + * @param year the year + * @param orgNumber the org number + * @return the ministry + */ + private String getMinistry(Map> ministryMap, + int year, + String orgNumber) { + final Map yearMap = ministryMap.get(year); + return yearMap != null ? yearMap.get(orgNumber.replace("-", "")) : null; + } + + /** + * The Enum DataType. + */ + private enum DataType { + + /** The income. */ + INCOME("Inkomst", "inkomster"), + + /** The outgoing. */ + OUTGOING("Utgift", "utgifter"); + + /** The document type. */ + private final String documentType; + + /** The url name. */ + private final String urlName; + + /** + * Instantiates a new data type. + * + * @param documentType the document type + * @param urlName the url name + */ + DataType(String documentType, String urlName) { + this.documentType = documentType; + this.urlName = urlName; + } + + /** + * Gets the document type. + * + * @return the document type + */ + public String getDocumentType() { + return documentType; + } + + /** + * Gets the url name. + * + * @return the url name + */ + public String getUrlName() { + return urlName; + } + } + + /** + * Creates the org ministry map. + * + * @param data the data + * @return the map + */ + private static Map> createOrgMinistryMap( + Map> data) { + return data.entrySet().stream() + .collect(Collectors.toMap( + Entry::getKey, + e -> e.getValue().stream() + .collect(Collectors.toMap( + t -> t.getOrgNumber().replace("-", ""), + GovernmentBodyAnnualSummary::getMinistry, + (v1, v2) -> v1 + )) + )); + } +} \ No newline at end of file diff --git a/service.external.esv/src/main/resources/Myndighetsinformation.xls b/service.external.esv/src/main/resources/Myndighetsinformation.xls index 281ec3c1e8f80a1f14b924981ea6aca27251e871..965f9c90c47dd953aec00dc56fe473b5a3134a4f 100644 GIT binary patch delta 7422 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 delta 4516 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 DIMl+B diff --git a/service.external.esv/src/test/java/com/hack23/cia/service/external/esv/impl/EsvApiITest.java b/service.external.esv/src/test/java/com/hack23/cia/service/external/esv/impl/EsvApiITest.java index 340019bf54d..74b3ed6b063 100644 --- a/service.external.esv/src/test/java/com/hack23/cia/service/external/esv/impl/EsvApiITest.java +++ b/service.external.esv/src/test/java/com/hack23/cia/service/external/esv/impl/EsvApiITest.java @@ -50,7 +50,7 @@ public void getDataDefenceMinistrySuccessTest() { final Map> governmentBodyAnnualSummaryData = esvApi .getDataPerMinistry("Försvarsdepartementet"); assertNotNull(governmentBodyAnnualSummaryData); - assertEquals(26, governmentBodyAnnualSummaryData.size()); + assertEquals(27, governmentBodyAnnualSummaryData.size()); for (final List list : governmentBodyAnnualSummaryData.values()) { for (final GovernmentBodyAnnualSummary governmentBodyAnnualSummary : list) { assertNotNull(governmentBodyAnnualSummary); @@ -122,6 +122,21 @@ public void getDataFinanceMinistry2017SuccessTest() { } } + /** + * Gets the data finance ministry 2025 success test. + * + * @return the data finance ministry 2025 success test + */ + @Test + public void getDataFinanceMinistry2025SuccessTest() { + final List list = esvApi.getDataPerMinistryAndYear("Finansdepartementet", 2025); + assertNotNull(list); + assertEquals(48, list.size()); + for (final GovernmentBodyAnnualSummary governmentBodyAnnualSummary : list) { + assertNotNull(governmentBodyAnnualSummary); + } + } + /** * Gets the data finance ministry success test. @@ -133,7 +148,7 @@ public void getDataFinanceMinistrySuccessTest() { final Map> governmentBodyAnnualSummaryData = esvApi .getDataPerMinistry("Finansdepartementet"); assertNotNull(governmentBodyAnnualSummaryData); - assertEquals(26, governmentBodyAnnualSummaryData.size()); + assertEquals(27, governmentBodyAnnualSummaryData.size()); for (final List list : governmentBodyAnnualSummaryData.values()) { for (final GovernmentBodyAnnualSummary governmentBodyAnnualSummary : list) { assertNotNull(governmentBodyAnnualSummary); @@ -151,7 +166,7 @@ public void getDataForeignMinistrySuccessTest() { final Map> governmentBodyAnnualSummaryData = esvApi .getDataPerMinistry("Utrikesdepartementet"); assertNotNull(governmentBodyAnnualSummaryData); - assertEquals(26, governmentBodyAnnualSummaryData.size()); + assertEquals(27, governmentBodyAnnualSummaryData.size()); for (final List list : governmentBodyAnnualSummaryData.values()) { for (final GovernmentBodyAnnualSummary governmentBodyAnnualSummary : list) { assertNotNull(governmentBodyAnnualSummary); @@ -169,7 +184,7 @@ public void getDataPerGovernmentBodySuccessTest() { final Map governmentBodyAnnualSummaryData = esvApi .getDataPerGovernmentBody("Exportkreditnämnden"); assertNotNull(governmentBodyAnnualSummaryData); - assertEquals(26, governmentBodyAnnualSummaryData.size()); + assertEquals(27, governmentBodyAnnualSummaryData.size()); for (final GovernmentBodyAnnualSummary governmentBodyAnnualSummary : governmentBodyAnnualSummaryData.values()) { assertNotNull(governmentBodyAnnualSummary); } @@ -184,7 +199,7 @@ public void getDataPerGovernmentBodySuccessTest() { public void getDataSuccessTest() { final Map> governmentBodyAnnualSummaryData = esvApi.getData(); assertNotNull(governmentBodyAnnualSummaryData); - assertEquals(26, governmentBodyAnnualSummaryData.size()); + assertEquals(27, governmentBodyAnnualSummaryData.size()); for (final List list : governmentBodyAnnualSummaryData.values()) { //assertTrue(list.size() > 200); for (final GovernmentBodyAnnualSummary governmentBodyAnnualSummary : list) {