IntelligenceExportServiceImpl.java
/*
* Copyright 2010-2025 James Pether Sörling
*
* Licensed 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.
*
* $Id$
* $HeadURL$
*/
package com.hack23.cia.service.data.impl;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.hack23.cia.model.internal.application.data.impl.ViewDecisionTemporalTrends;
import com.hack23.cia.model.internal.application.data.party.impl.ViewRiksdagenCoalitionAlignmentMatrix;
import com.hack23.cia.model.internal.application.data.rules.impl.RuleViolation;
import com.hack23.cia.service.data.api.IntelligenceExportService;
import com.hack23.cia.service.data.api.RuleViolationDAO;
import com.hack23.cia.service.data.api.ViewDecisionTemporalTrendsDAO;
import com.hack23.cia.service.data.api.ViewRiksdagenCoalitionAlignmentMatrixDAO;
import com.hack23.cia.service.data.impl.export.CoalitionAlignmentExportDTO;
import com.hack23.cia.service.data.impl.export.CoalitionAlignmentExportDTO.PartyAlignment;
import com.hack23.cia.service.data.impl.export.ExportMetadata;
import com.hack23.cia.service.data.impl.export.RiskAssessmentExportDTO;
import com.hack23.cia.service.data.impl.export.RiskAssessmentExportDTO.RiskViolation;
import com.hack23.cia.service.data.impl.export.TemporalTrendsExportDTO;
import com.hack23.cia.service.data.impl.export.TemporalTrendsExportDTO.TrendDataPoint;
/**
* Implementation of IntelligenceExportService.
*
* @author intelligence-operative
* @since v1.36
*/
@Service
@Transactional(readOnly = true)
final class IntelligenceExportServiceImpl implements IntelligenceExportService {
@Autowired
private RuleViolationDAO ruleViolationDAO;
@Autowired
private ViewRiksdagenCoalitionAlignmentMatrixDAO coalitionAlignmentDAO;
@Autowired
private ViewDecisionTemporalTrendsDAO temporalTrendsDAO;
private final ObjectMapper objectMapper;
public IntelligenceExportServiceImpl() {
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
this.objectMapper.setDateFormat(new com.fasterxml.jackson.databind.util.StdDateFormat().withColonInTimeZone(true));
}
@Override
public String exportRiskAssessments() throws IOException {
final List<RuleViolation> violations = ruleViolationDAO.getAll();
final RiskAssessmentExportDTO dto = new RiskAssessmentExportDTO();
final ExportMetadata metadata = new ExportMetadata();
metadata.setSchema("intelligence-schema");
metadata.setRecordCount(violations.size());
metadata.setDataDate(new Date());
dto.setMetadata(metadata);
final List<RiskViolation> riskViolations = new ArrayList<>();
for (final RuleViolation violation : violations) {
final RiskViolation riskViolation = new RiskViolation();
riskViolation.setId(violation.getId());
riskViolation.setDetectedDate(violation.getDetectedDate());
riskViolation.setReferenceId(violation.getReferenceId());
riskViolation.setName(violation.getName());
riskViolation.setResourceType(violation.getResourceType() != null ? violation.getResourceType().toString() : null);
riskViolation.setRuleName(violation.getRuleName());
riskViolation.setRuleDescription(violation.getRuleDescription());
riskViolation.setRuleGroup(violation.getRuleGroup());
riskViolation.setStatus(violation.getStatus() != null ? violation.getStatus().toString() : null);
riskViolation.setPositive(violation.getPositive());
riskViolations.add(riskViolation);
}
dto.setViolations(riskViolations);
return objectMapper.writeValueAsString(dto);
}
@Override
public String exportCoalitionAlignment() throws IOException {
final List<ViewRiksdagenCoalitionAlignmentMatrix> alignments = coalitionAlignmentDAO.getAll();
final CoalitionAlignmentExportDTO dto = new CoalitionAlignmentExportDTO();
final ExportMetadata metadata = new ExportMetadata();
metadata.setSchema("intelligence-schema");
metadata.setRecordCount(alignments.size());
metadata.setDataDate(new Date());
dto.setMetadata(metadata);
final List<PartyAlignment> partyAlignments = new ArrayList<>();
for (final ViewRiksdagenCoalitionAlignmentMatrix alignment : alignments) {
final PartyAlignment partyAlignment = new PartyAlignment();
if (alignment.getEmbeddedId() != null) {
partyAlignment.setParty1(alignment.getEmbeddedId().getParty1());
partyAlignment.setParty2(alignment.getEmbeddedId().getParty2());
}
partyAlignment.setAlignmentRate(alignment.getAlignmentRate());
partyAlignment.setSharedVotes(alignment.getSharedVotes());
partyAlignment.setAlignedVotes(alignment.getAlignedVotes());
partyAlignments.add(partyAlignment);
}
dto.setAlignments(partyAlignments);
return objectMapper.writeValueAsString(dto);
}
@Override
public String exportTemporalTrends() throws IOException {
final List<ViewDecisionTemporalTrends> trends = temporalTrendsDAO.getAll();
final TemporalTrendsExportDTO dto = new TemporalTrendsExportDTO();
final ExportMetadata metadata = new ExportMetadata();
metadata.setSchema("intelligence-schema");
metadata.setRecordCount(trends.size());
metadata.setDataDate(new Date());
dto.setMetadata(metadata);
final List<TrendDataPoint> dataPoints = new ArrayList<>();
for (final ViewDecisionTemporalTrends trend : trends) {
final TrendDataPoint dataPoint = new TrendDataPoint();
dataPoint.setDecisionDay(trend.getDecisionDay());
dataPoint.setDailyDecisions(trend.getDailyDecisions());
dataPoint.setDailyApprovalRate(trend.getDailyApprovalRate());
dataPoint.setApprovedDecisions(trend.getApprovedDecisions());
dataPoint.setRejectedDecisions(trend.getRejectedDecisions());
dataPoint.setReferredBackDecisions(trend.getReferredBackDecisions());
dataPoint.setCommitteeReferralDecisions(trend.getCommitteeReferralDecisions());
dataPoint.setMa7dayDecisions(trend.getMa7dayDecisions());
dataPoint.setMa30dayDecisions(trend.getMa30dayDecisions());
dataPoint.setMa90dayDecisions(trend.getMa90dayDecisions());
dataPoint.setMa30dayApprovalRate(trend.getMa30dayApprovalRate());
dataPoint.setDecisionsLastYear(trend.getDecisionsLastYear());
dataPoint.setYoyDecisionsChange(trend.getYoyDecisionsChange());
dataPoint.setYoyDecisionsChangePct(trend.getYoyDecisionsChangePct());
dataPoint.setDecisionYear(trend.getDecisionYear());
dataPoint.setDecisionMonth(trend.getDecisionMonth());
dataPoint.setDecisionWeek(trend.getDecisionWeek());
dataPoint.setDayOfWeek(trend.getDayOfWeek());
dataPoints.add(dataPoint);
}
dto.setTrends(dataPoints);
return objectMapper.writeValueAsString(dto);
}
@Override
public void writeJsonToFile(final String jsonContent, final String fileName, final String outputDirectory)
throws IOException {
// Validate fileName doesn't contain path separators (subdirectories not supported)
if (fileName.contains("/") || fileName.contains("\\")) {
throw new IllegalArgumentException("Invalid file name: subdirectories not supported, must be a flat file name");
}
// Validate against path traversal by ensuring resolved path stays within target directory
final Path targetDir = Paths.get(outputDirectory).toAbsolutePath().normalize();
final Path resolvedPath = targetDir.resolve(fileName).normalize();
if (!resolvedPath.startsWith(targetDir)) {
throw new IllegalArgumentException("Invalid file name: path traversal detected");
}
final File directory = new File(outputDirectory);
if (!directory.exists() && !directory.mkdirs()) {
throw new IOException("Failed to create directory: " + outputDirectory);
}
Files.write(resolvedPath, jsonContent.getBytes(StandardCharsets.UTF_8));
}
}