FOSSredder 0.5.0
Loading...
Searching...
No Matches
ObjectExportExecutor.cpp
Go to the documentation of this file.
1
5
7
10#include "ExportConstants.h"
14
15#include <algorithm>
16#include <filesystem>
17#include <fstream>
18#include <iomanip>
19#include <map>
20#include <set>
21#include <sstream>
22#include <unordered_map>
23
24namespace {
25
31
32std::string safeFilePart(const std::string &text) {
33 std::string out;
34 out.reserve(text.size());
35 for (const char c : text) {
36 const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
37 (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.';
38 out.push_back(ok ? c : '_');
39 }
40 return out.empty() ? "entry" : out;
41}
42
43std::string extensionFor(AnalysisExportFormat format) {
44 switch (format) {
45 case AnalysisExportFormat::Csv:
46 return "csv";
47 case AnalysisExportFormat::Xlsx:
48 return "xlsx";
49 case AnalysisExportFormat::Jpg:
50 return "jpg";
51 case AnalysisExportFormat::Png:
52 return "png";
53 }
54 return "dat";
55}
56
57bool writeCsvTable(const std::filesystem::path &outputPath,
58 const std::vector<std::vector<std::string>> &rows) {
59 std::ofstream stream(outputPath, std::ios::binary);
60 if (!stream)
61 return false;
62 const unsigned char bom[] = {0xEF, 0xBB, 0xBF};
63 stream.write(reinterpret_cast<const char *>(bom), sizeof(bom));
64 for (const auto &row : rows) {
65 for (size_t i = 0; i < row.size(); ++i) {
66 if (i > 0)
67 stream << ';';
68 stream << row[i];
69 }
70 stream << '\n';
71 }
72 return !!stream;
73}
74
75std::vector<std::vector<std::string>>
76normalizedRowsForExport(const core::ports::analysis::AnalysisResult &result) {
77 std::vector<std::vector<std::string>> rows;
79 rows.push_back({"Category", "Value"});
80 for (const auto &row : result.table) {
81 if (row.empty())
82 continue;
83 rows.push_back(row.size() == 1
84 ? std::vector<std::string>{row[0], "0"}
85 : std::vector<std::string>{row[0], row[1]});
86 }
87 return rows;
88 }
90 rows.push_back({"Month", "Total"});
91 for (const auto &row : result.table) {
92 if (row.empty())
93 continue;
94 std::string month = row[0];
95 std::string totalValue = row.size() > 1 ? row[1] : "0";
96 rows.push_back({month, totalValue});
97 }
98 return rows;
99 }
100 return result.table;
101}
102
103std::unordered_map<std::string, std::string>
104propertyNameById(const core::domain::catalog::WorkspaceCatalog &state) {
105 std::unordered_map<std::string, std::string> out;
106 out.reserve(state.properties().size());
107 for (const auto &property : state.properties()) {
108 if (!property || property->id().empty())
109 continue;
110 out[property->id()] =
111 property->name().empty() ? property->id() : property->name();
112 }
113 return out;
114}
115
116std::vector<std::vector<std::string>>
117normalizedRowsForExport(const core::ports::analysis::AnalysisResult &result,
120 return normalizedRowsForExport(result);
121 }
122 const std::string unassigned =
124 std::set<std::string> contractTypes;
125 std::map<std::string, std::map<std::string, double>> amountsByProperty;
126 const auto nameById = propertyNameById(state);
127
128 for (const auto &tx : result.transactions) {
129 const std::string contractType =
130 !tx.contractType.empty() ? tx.contractType : unassigned;
131 contractTypes.insert(contractType);
132 if (tx.propertyIds.empty()) {
133 amountsByProperty[unassigned][contractType] += tx.amount;
134 continue;
135 }
136 bool hasMappedProperty = false;
137 for (size_t i = 0; i < tx.propertyIds.size(); ++i) {
138 const auto &propertyId = tx.propertyIds[i];
139 if (propertyId.empty())
140 continue;
141 std::string propertyName;
142 if (i < tx.propertyNames.size() && !tx.propertyNames[i].empty())
143 propertyName = tx.propertyNames[i];
144 else {
145 const auto it = nameById.find(propertyId);
146 propertyName = it != nameById.end() && !it->second.empty() ? it->second
147 : propertyId;
148 }
149 if (propertyName.empty())
150 continue;
151 amountsByProperty[propertyName][contractType] += tx.amount;
152 hasMappedProperty = true;
153 }
154 if (!hasMappedProperty)
155 amountsByProperty[unassigned][contractType] += tx.amount;
156 }
157
158 auto formatAmount = [](double value) {
159 std::ostringstream stream;
160 stream << std::fixed << std::setprecision(2) << value;
161 return stream.str();
162 };
163 std::vector<std::string> orderedContractTypes(contractTypes.begin(),
164 contractTypes.end());
165 std::stable_sort(orderedContractTypes.begin(), orderedContractTypes.end(),
166 [&](const std::string &lhs, const std::string &rhs) {
167 const bool lhsUnassigned = lhs == unassigned;
168 const bool rhsUnassigned = rhs == unassigned;
169 if (lhsUnassigned != rhsUnassigned)
170 return !lhsUnassigned;
171 return lhs < rhs;
172 });
173 std::vector<std::string> orderedProperties;
174 for (const auto &[propertyName, _] : amountsByProperty)
175 orderedProperties.push_back(propertyName);
176 std::stable_sort(orderedProperties.begin(), orderedProperties.end(),
177 [&](const std::string &lhs, const std::string &rhs) {
178 const bool lhsUnassigned = lhs == unassigned;
179 const bool rhsUnassigned = rhs == unassigned;
180 if (lhsUnassigned != rhsUnassigned)
181 return !lhsUnassigned;
182 return lhs < rhs;
183 });
184
185 std::vector<std::vector<std::string>> rows;
186 std::vector<std::string> header;
187 header.push_back(
189 for (const auto &contractType : orderedContractTypes)
190 header.push_back(contractType);
191 header.push_back(std::string(core::application::exporting::constants::labels::kTotal));
192 rows.push_back(std::move(header));
193
194 std::vector<double> totalsByContract(orderedContractTypes.size(), 0.0);
195 double grandTotal = 0.0;
196 for (const auto &propertyName : orderedProperties) {
197 const auto &byContract = amountsByProperty[propertyName];
198 std::vector<std::string> row;
199 row.push_back(propertyName);
200 double rowTotal = 0.0;
201 for (size_t i = 0; i < orderedContractTypes.size(); ++i) {
202 const auto it = byContract.find(orderedContractTypes[i]);
203 const double value = it != byContract.end() ? it->second : 0.0;
204 row.push_back(formatAmount(value));
205 rowTotal += value;
206 totalsByContract[i] += value;
207 }
208 row.push_back(formatAmount(rowTotal));
209 rows.push_back(std::move(row));
210 grandTotal += rowTotal;
211 }
212 std::vector<std::string> totalRow;
213 totalRow.push_back(std::string(core::application::exporting::constants::labels::kTotal));
214 for (const double value : totalsByContract)
215 totalRow.push_back(formatAmount(value));
216 totalRow.push_back(formatAmount(grandTotal));
217 rows.push_back(std::move(totalRow));
218 return rows;
219}
220
221std::unordered_map<std::string, std::string>
222annualFolderNames(const core::domain::catalog::WorkspaceCatalog &state) {
223 std::unordered_map<std::string, std::string> folderByAnnualId;
224 std::set<std::string> usedFolderNames;
225 for (const auto &annual : state.annuals()) {
226 if (!annual || annual->id().empty())
227 continue;
228 const std::string baseName =
229 safeFilePart(!annual->name().empty() ? annual->name() : annual->id());
230 std::string candidate = baseName;
231 int idx = 2;
232 while (usedFolderNames.find(candidate) != usedFolderNames.end()) {
233 candidate = baseName + "_" + std::to_string(idx++);
234 }
235 usedFolderNames.insert(candidate);
236 folderByAnnualId[annual->id()] = candidate;
237 }
238 return folderByAnnualId;
239}
240
241std::filesystem::path outputPathForItem(
242 const std::filesystem::path &baseOutput,
243 const std::unordered_map<std::string, std::string> &folderByAnnualId,
244 const ExportObjectRequest &item) {
245 const std::string fileStem =
246 safeFilePart(!item.name.empty() ? item.name : item.objectId);
247 const std::string ext = extensionFor(item.format);
248 if (item.annualId.empty())
249 return baseOutput / (fileStem + "." + ext);
250 const auto it = folderByAnnualId.find(item.annualId);
251 const std::string annualFolder =
252 it != folderByAnnualId.end() ? it->second : safeFilePart(item.annualId);
253 return baseOutput / annualFolder / (fileStem + "." + ext);
254}
255
256} // namespace
257
259
260namespace {
261
262bool shouldStop(const core::ports::exporting::ExportRequest &request) {
263 if (request.waitIfPaused) {
264 request.waitIfPaused();
265 }
266 return request.shouldCancel && request.shouldCancel();
267}
268
269core::ports::exporting::ExportResult canceledResult(
270 const core::ports::exporting::ExportRequest &request) {
271 core::ports::exporting::ExportResult result;
272 result.actualFormat = request.format;
273 result.resolvedOutputPath = request.outputPath;
275 result.message = "Canceled";
276 return result;
277}
278
279} // namespace
280
284 const std::shared_ptr<core::ports::archive::IArchive> &archive,
285 const std::shared_ptr<core::ports::xlsx_writer::IXlsxWriter> &xlsxWriter,
286 const std::shared_ptr<
288 &imageRenderer) {
290 result.actualFormat = request.format;
291 result.resolvedOutputPath = request.outputPath;
292
293 if (request.outputPath.empty()) {
294 result.status = ExportStatus::InvalidInput;
295 result.errorCode =
297 result.message =
299 return result;
300 }
301
302 try {
303 if (shouldStop(request)) {
304 return canceledResult(request);
305 }
306 std::filesystem::path baseOutput =
307 std::filesystem::path(request.outputPath);
308 if (baseOutput.has_extension())
309 baseOutput = baseOutput.parent_path() / baseOutput.stem();
310 if (!std::filesystem::exists(baseOutput))
311 std::filesystem::create_directories(baseOutput);
312
313 const auto folderByAnnualId = annualFolderNames(state);
315 std::vector<ExportObjectRequest> analysisItems;
316 for (const auto &item : request.objectRequests) {
317 if (item.type == ExportObjectType::Analysis && !item.objectId.empty())
318 analysisItems.push_back(item);
319 }
320
321 const std::size_t total = analysisItems.size();
322 for (size_t i = 0; i < total; ++i) {
323 if (shouldStop(request)) {
324 return canceledResult(request);
325 }
326 const auto &item = analysisItems[i];
327 if (request.progressCallback) {
328 request.progressCallback(
329 0.25 + (0.55 *
330 (static_cast<double>(i) / std::max<std::size_t>(1, total))),
331 "Exporting analysis " + std::to_string(i + 1) + "/" +
332 std::to_string(total));
333 }
334 const auto computed = analysisService.runAnalysisById(
335 state, item.objectId);
336 if (shouldStop(request)) {
337 return canceledResult(request);
338 }
339 if (!computed.found)
340 continue;
341 const std::filesystem::path outputFile =
342 outputPathForItem(baseOutput, folderByAnnualId, item);
343 std::filesystem::create_directories(outputFile.parent_path());
344
345 bool ok = false;
346 const auto tableRows =
347 normalizedRowsForExport(computed, state);
348 switch (item.format) {
349 case AnalysisExportFormat::Csv:
350 ok = writeCsvTable(outputFile, tableRows);
351 break;
352 case AnalysisExportFormat::Xlsx:
353 ok = xlsxWriter &&
354 xlsxWriter->writeTable(outputFile, tableRows, "Analysis");
355 break;
356 case AnalysisExportFormat::Jpg:
357 case AnalysisExportFormat::Png:
358 ok = imageRenderer &&
359 imageRenderer->renderToImage(
360 outputFile,
361 computed.type.empty() ? item.objectId : computed.type,
362 computed);
363 break;
364 }
365 if (!ok) {
366 result.status = ExportStatus::WriteFailed;
367 result.errorCode =
369 result.message = std::string(
371 return result;
372 }
373 }
374
375 if (request.packageFormat == PackageFormat::Zip) {
376 if (shouldStop(request)) {
377 return canceledResult(request);
378 }
379 if (request.progressCallback)
380 request.progressCallback(0.88, "Packaging export");
381 const std::filesystem::path archivePath =
382 baseOutput.string() +
384 if (!archive ||
385 !archive->create(baseOutput, archivePath, request.packageFormat)) {
386 result.status = ExportStatus::ArchiveFailed;
387 result.errorCode =
389 result.message =
391 return result;
392 }
393 result.resolvedOutputPath = archivePath.string();
394 }
395
396 if (request.progressCallback)
397 request.progressCallback(0.95, "Finalizing export");
398 if (shouldStop(request)) {
399 return canceledResult(request);
400 }
401 result.status = ExportStatus::Ok;
402 result.success = true;
403 return result;
404 } catch (...) {
405 result.status = ExportStatus::InternalError;
406 result.errorCode =
408 result.message =
410 return result;
411 }
412}
413
414} // namespace core::application::exporting
Defines stable analysis type, configuration, filter, and result keys.
Declares the application service that resolves and executes analyses.
Defines private constants shared by core export implementations.
Declares the analysis rendering port used by the export layer.
Declares the archive abstraction used by export packaging.
Declares the XLSX writing port used by the export layer.
Declares object export execution helpers.
Coordinates analysis lookup, execution, and transaction preview filtering.
core::ports::analysis::AnalysisResult runAnalysisById(const core::domain::catalog::WorkspaceCatalog &state, const std::string &analysisId, const std::string &filterSpec={}) const
Resolves an analysis by identifier and executes it.
const PropertyList & properties() const noexcept
Returns all properties in the catalog.
const AnnualList & annuals() const noexcept
Returns all annuals in the catalog.
Renders analysis results to image artifacts.
constexpr std::string_view kTypeTab
core::ports::exporting::ExportResult exportObjectRequests(const core::domain::catalog::WorkspaceCatalog &state, const core::ports::exporting::ExportRequest &request, const std::shared_ptr< core::ports::archive::IArchive > &archive, const std::shared_ptr< core::ports::xlsx_writer::IXlsxWriter > &xlsxWriter, const std::shared_ptr< core::ports::analysis_rendering::IAnalysisRenderer > &imageRenderer)
Export selected workspace objects according to an export request.
std::vector< AnalysisTransaction > transactions
std::vector< std::vector< std::string > > table
Describes one persisted analysis or annual export target.
Describes one export run against a workspace snapshot.
std::function< void(double, const std::string &)> progressCallback
std::function< void()> waitIfPaused
std::vector< ExportObjectRequest > objectRequests
std::function< bool()> shouldCancel