diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/NotificationBuilder.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/NotificationBuilder.java new file mode 100644 index 000000000..cc212b236 --- /dev/null +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/NotificationBuilder.java @@ -0,0 +1,43 @@ +package ru.spcex.clearing.reports.notifications; + +import ru.spcex.clearing.reports.exceptions.ConfigException; +import ru.spcex.clearing.reports.services.DOCXService; + +import java.io.File; +import java.net.URL; +import java.time.format.DateTimeFormatter; + +public abstract class NotificationBuilder { + protected final DateTimeFormatter filenameDateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy'T'HH:mm:ss"); + protected final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); + protected final DOCXService docxService; + + protected NotificationBuilder(String keyPattern) { + this.docxService = new DOCXService(); + if (!docxService.init(getTemplateFile(), keyPattern)) + throw new ConfigException("Can't create NotificationBuilder: docxService not initialized"); + } + + abstract public File buildNotification(Long consumerId); + + protected File getTemplateFile() { + File templateFile; + String templateResourceName = getTemplateResourceName(); + try { + URL resource = getClass().getClassLoader().getResource(templateResourceName); + if (resource == null) throw new Exception(); + templateFile = new File(resource.toURI()); + } catch (Exception e) { + throw new ConfigException("Can't read template resource " + templateResourceName, e); + } + return templateFile; + } + + protected String normalizeFilenameForOS(String filename) { + if (System.getProperty("os.name").toLowerCase().contains("windows")) + return filename.replace(":", "_"); + return filename; + } + + abstract protected String getTemplateResourceName(); +} diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/REXNotificationBuilder.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/REXNotificationBuilder.java new file mode 100644 index 000000000..e53754c09 --- /dev/null +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/REXNotificationBuilder.java @@ -0,0 +1,106 @@ +package ru.spcex.clearing.reports.notifications; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import ru.clearing.classes.statics.data.company.Company; +import ru.clearing.classes.statics.data.profile.ProfileDocument; +import ru.spcex.clearing.imdg.IMDGDistributedNames; +import ru.spcex.platform.imdg.api.Imdg; +import ru.spcex.platform.imdg.api.ImdgProvider; + +import java.io.File; +import java.time.LocalDate; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Component +public class REXNotificationBuilder extends NotificationBuilder { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final Imdg profileDocumentImdg; + private final Imdg companyImdg; + + public REXNotificationBuilder(ImdgProvider imdgProvider) { + super(null); + profileDocumentImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_ProfileDocument, ProfileDocument.class); + companyImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Company, Company.class); + } + + @Override + public File buildNotification(Long consumerId) { + String sql = "companyId = %d AND documentType = 'XCNT'".formatted(consumerId); + Collection profileDocuments = profileDocumentImdg.getCollectionObjectsBySQL(sql); + ProfileDocument currentProfileDocument = null; + if (profileDocuments.size() >= 1) { + Optional profileDocumentOptional = profileDocuments.stream().max((o1, o2) -> { + LocalDate o1ValidFromDate = o1.getValidFromDate(); + LocalDate o2ValidFromDate = o2.getValidFromDate(); + o1ValidFromDate = o1ValidFromDate == null ? LocalDate.MIN : o1ValidFromDate; + o2ValidFromDate = o2ValidFromDate == null ? LocalDate.MIN : o2ValidFromDate; + return o1ValidFromDate.compareTo(o2ValidFromDate); + }); + currentProfileDocument = profileDocumentOptional.get(); + } else { + log.warn("Profile_document with sql {} not found", sql); + return null; + } + + Long notificationId = currentProfileDocument.getId(); + LocalDate validFromDate = currentProfileDocument.getValidFromDate(); + if (validFromDate == null) { + log.error("validFromDate is null for profile_document {}", currentProfileDocument); + return null; + } + Long companyId = currentProfileDocument.getCompanyId(); + if (companyId == null) { + log.error("companyId is null for profile_document {}", currentProfileDocument); + return null; + } + Company company = companyImdg.getSingleObjectByID(companyId); + if (company == null) { + log.error("Company for companyId={} is null", companyId); + return null; + } + String companyClearingCode = company.getClearingCode(); + if (StringUtils.isBlank(companyClearingCode)) { + log.error("Company.clearing_code for companyId={} is null", companyId); + return null; + } + + LocalDate notificationDate = LocalDate.now(); + int day = notificationDate.getDayOfMonth(); + int month = notificationDate.getMonthValue(); + int year = notificationDate.getYear(); + String dateStr = "%02d%02d%04d".formatted(day, month, year); + + Map valuesForTemplate = new HashMap<>(); + valuesForTemplate.put("${dateStr}", dateStr); + valuesForTemplate.put("${count}", String.valueOf(notificationId)); + valuesForTemplate.put("${currDay}", "%02d".formatted(day)); + valuesForTemplate.put("${currMonth}", "%02d".formatted(month)); + valuesForTemplate.put("${currYear}", "%04d".formatted(year % 100)); + valuesForTemplate.put("${validFromDate}", "%02d.%02d.%04d".formatted(day, month, year)); + + String outputFilename = "REX.%s.%s.docx".formatted(companyClearingCode, filenameDateTimeFormatter.format(notificationDate)); + outputFilename = normalizeFilenameForOS(outputFilename); + + File outputFile = new File(outputFilename); + try { + docxService.insertValues(outputFile, valuesForTemplate, true); + } catch (Exception e) { + log.error("Can't insert values into template.", e); + return null; + } + + return outputFile; + } + + @Override + protected String getTemplateResourceName() { + return "templates/REX_TEMPLATE.docx"; + } + +} diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/RRPCNotificationBuilder.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/RRPCNotificationBuilder.java new file mode 100644 index 000000000..46ff76f60 --- /dev/null +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/notifications/RRPCNotificationBuilder.java @@ -0,0 +1,112 @@ +package ru.spcex.clearing.reports.notifications; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import ru.clearing.classes.objects.BusinessObject; +import ru.clearing.classes.statics.data.company.Company; +import ru.clearing.classes.statics.data.company.CompanySymbols; +import ru.clearing.classes.statics.data.company.relation.Relation; +import ru.spcex.clearing.imdg.IMDGDistributedNames; +import ru.spcex.platform.imdg.api.Imdg; +import ru.spcex.platform.imdg.api.ImdgProvider; + +import java.io.File; +import java.time.Instant; +import java.time.LocalDate; +import java.util.*; + +@Component +public class RRPCNotificationBuilder extends NotificationBuilder { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final Imdg companyImdg; + private final Imdg relationImdg; + private final Imdg companySymbolsImdg; + + public RRPCNotificationBuilder(ImdgProvider imdgProvider) { + super(null); + companyImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Company, Company.class); + relationImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Relation, Relation.class); + companySymbolsImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_CompanySymbols, CompanySymbols.class); + } + + @Override + public File buildNotification(Long consumerId) { + Company company = companyImdg.getSingleObjectByID(consumerId); + if (company == null) { + log.error("Company for companyId={} is null", consumerId); + return null; + } + String companyClearingCode = company.getClearingCode(); + if (StringUtils.isBlank(companyClearingCode)) { + log.error("Company.clearing_code for companyId={} is null", consumerId); + return null; + } + + String relationsSql = "consumerId = %d".formatted(consumerId); + Relation relation = null; + Collection relations = relationImdg.getCollectionObjectsBySQL(relationsSql); + if (relations.size() >= 1) { + Optional relationOptional = relations.stream().max(Comparator.comparing(BusinessObject::getUpdated)); + relation = relationOptional.get(); + } else { + log.warn("Relation with sql {} not found", relationsSql); + return null; + } + Long notificationId = relation.getId(); + Instant updatedAt = relation.getUpdated(); + if (updatedAt == null) { + log.error("updated_at is null for relation {}", relation); + return null; + } + + String companySymbolsSql = "companyId = %d AND companySymbol = INN"; + CompanySymbols companySymbols = null; + Collection companySymbolsCollection = companySymbolsImdg.getCollectionObjectsBySQL(companySymbolsSql); + if (companySymbolsCollection.size() >= 1) { + companySymbols = companySymbolsCollection.iterator().next(); + } else { + log.warn("CompanySymbols with sql {} not found", companySymbolsSql); + return null; + } + + LocalDate notificationDate = LocalDate.now(); + int day = notificationDate.getDayOfMonth(); + int month = notificationDate.getMonthValue(); + int year = notificationDate.getYear(); + String dateStr = "%02d%02d%04d".formatted(day, month, year); + + Map valuesForTemplate = new HashMap<>(); + valuesForTemplate.put("${dateStr}", dateStr); + valuesForTemplate.put("${count}", String.valueOf(notificationId)); + valuesForTemplate.put("${currDay}", "%02d".formatted(day)); + valuesForTemplate.put("${currMonth}", "%02d".formatted(month)); + valuesForTemplate.put("${currYear}", "%04d".formatted(year % 100)); + + valuesForTemplate.put("${relationUpdatedAt}", dateFormatter.format(updatedAt)); + valuesForTemplate.put("${companyFullName}", company.getFullName()); + valuesForTemplate.put("${companyShortName}", company.getShortName()); + valuesForTemplate.put("${companySymbolsINN}", companySymbols.getCompanySymbolValue()); + valuesForTemplate.put("${companyClearingCode}", companyClearingCode); + + String outputFilename = "RRPC.%s.%s.docx".formatted(companyClearingCode, filenameDateTimeFormatter.format(notificationDate)); + outputFilename = normalizeFilenameForOS(outputFilename); + + File outputFile = new File(outputFilename); + try { + docxService.insertValues(outputFile, valuesForTemplate, true); + } catch (Exception e) { + log.error("Can't insert values into template.", e); + return null; + } + + return outputFile; + } + + @Override + protected String getTemplateResourceName() { + return "templates/RRPC_TEMPLATE.docx"; + } + +} diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java index 86483a38b..02e1813eb 100644 --- a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/DOCXService.java @@ -2,6 +2,7 @@ package ru.spcex.clearing.reports.services; import org.apache.poi.xwpf.usermodel.*; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.*; import java.util.LinkedList; @@ -10,6 +11,7 @@ import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; /** @@ -22,58 +24,86 @@ import java.util.stream.Collectors; * идентификатору потом можно установить стиль, который будет применён к вставляемой строке. */ public class DOCXService { + private final Logger log = LoggerFactory.getLogger(getClass()); /** * При инициализации сохраняет все имеющиеся ключи (ищет по маске keyPattern), найденные в текстовых элементах документа */ private List keyPositions = new LinkedList<>(); + /** + * Паттерн поиска ключей в файле по умолчанию + */ + private final Pattern DEFAULT_KEY_PATTERN = Pattern.compile("\\$\\{.*?}"); + /** * Паттерн поиска ключей в файле */ - private Pattern KEY_PATTERN = Pattern.compile("\\$\\{.*?}"); + private Pattern keyPattern = DEFAULT_KEY_PATTERN; + + /** + * Файл шаблона + */ + private File templateFile; + + /** + * Флаг инициализации + */ + private boolean initialized = false; /** * (ре)Инициализация сервиса */ - public void init(String keyPattern) { - KEY_PATTERN = Pattern.compile(keyPattern); + public boolean init(File templateFile, String keyPattern) { + initialized = false; + if (templateFile == null || !templateFile.exists() || !templateFile.isFile()) { + log.warn("Invalid templateFile"); + return false; + } + this.templateFile = templateFile; + if (keyPattern != null) { + try { + this.keyPattern = Pattern.compile(keyPattern); + } catch (PatternSyntaxException e) { + log.warn("Invalid keyPattern"); + return false; + } + } else { + this.keyPattern = DEFAULT_KEY_PATTERN; + } keyPositions = new LinkedList<>(); + initialized = true; + return true; } /** * Вставка значений в шаблон. Шаблон может содержать любое количество ключей, при этом эти ключи должны содержаться в * valuesMap. * - * @param templateFile Файл шаблона * @param outputFile Выходной файл. Должен быть либо файлом (будет перезаписан), либо отсутствовать * @param valuesMap Мапа значений. В файле шаблона будут искаться ключи этой мапы и заменяться на соответствующие значения. - * @param log Если будет передан null, то метод будет выбрасывать исключения. В противном случае будет записан лог и возвращено false * @param checkKeys True - строгий режим, где будет проверено наличие всех ключей из файла в valuesMap. * False - мягкий режим, отсутствующие ключи будут пропущены * * @return true - если вставка прошла успешно */ - public boolean insertValues(File templateFile, - File outputFile, + public boolean insertValues(File outputFile, Map valuesMap, - Logger log, boolean checkKeys) throws Exception { - assert templateFile != null && templateFile.exists() && templateFile.isFile(); + if (!initialized) { + log.warn("DOCXService not initialized"); + return false; + } if (outputFile == null || outputFile.isDirectory()) { - String logMsg = "Can't open output file. Output file: %s".formatted(outputFile == null ? "null" : outputFile.getAbsolutePath()); - if (log == null) throw new Exception(logMsg); - log.error(logMsg); + log.error("Can't open output file. Output file: {}", outputFile == null ? "null" : outputFile.getAbsolutePath()); return false; } if (outputFile.exists()) { - if (log != null) log.warn("Output file {} exist and will be overwritten", outputFile.getAbsolutePath()); + log.warn("Output file {} exist and will be overwritten", outputFile.getAbsolutePath()); boolean deleteOk = outputFile.delete(); if (!deleteOk) { - String logMsg = "Can't delete file %s".formatted(outputFile.getAbsolutePath()); - if (log == null) throw new Exception(logMsg); - log.error(logMsg); + log.error("Can't delete file {}", outputFile.getAbsolutePath()); return false; } } @@ -94,11 +124,9 @@ public class DOCXService { List notPresentInMap = new LinkedList<>(); boolean checkOk = checkIntersection(valuesMap, notPresentInMap); if (!checkOk) { - String logMsg = "Can't insert values in file %s. Keys not in map: %s." - .formatted(templateFile.getAbsolutePath(), - String.join(", ", notPresentInMap)); - if (log == null) throw new Exception(logMsg); - log.error(logMsg); + log.error("Can't insert values in file {}. Keys not in map: {}.", + templateFile.getAbsolutePath(), + String.join(", ", notPresentInMap)); return false; } } @@ -178,7 +206,7 @@ public class DOCXService { for (int currPos = 0; currPos < MAX_POSITION; currPos++) { String currText = run.getText(currPos); if (currText == null) break; - Matcher matcher = KEY_PATTERN.matcher(currText); + Matcher matcher = keyPattern.matcher(currText); while (matcher.find()) { String key = matcher.group(); keyPositions.add(new KeyPositionInDoc(key, run, currPos)); diff --git a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/QCommandExecutor.java b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/QCommandExecutor.java index 90b95ed39..3d5e55548 100644 --- a/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/QCommandExecutor.java +++ b/clearing-parent/reports-service/src/main/java/ru/spcex/clearing/reports/services/QCommandExecutor.java @@ -23,6 +23,7 @@ import java.util.Map; public class QCommandExecutor extends QueueConsumer implements InitializingBean { private final Logger log = LoggerFactory.getLogger(getClass()); private final Map collectorForReport = new HashMap<>(); + // private final ImdgId idGenerator; // private final KafkaSender kafkaReqProducer; @@ -71,4 +72,5 @@ public class QCommandExecutor extends QueueConsumer implements InitializingBean } log.debug("successfully processed"); } + } diff --git a/clearing-parent/reports-service/src/main/resources/templates/REX_TEMPLATE.docx b/clearing-parent/reports-service/src/main/resources/templates/REX_TEMPLATE.docx new file mode 100644 index 000000000..363dc2568 Binary files /dev/null and b/clearing-parent/reports-service/src/main/resources/templates/REX_TEMPLATE.docx differ diff --git a/clearing-parent/reports-service/src/main/resources/templates/RRPC_TEMPLATE.docx b/clearing-parent/reports-service/src/main/resources/templates/RRPC_TEMPLATE.docx new file mode 100644 index 000000000..a5ecabd12 Binary files /dev/null and b/clearing-parent/reports-service/src/main/resources/templates/RRPC_TEMPLATE.docx differ diff --git a/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java b/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java index 528d64be2..ffd640744 100644 --- a/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java +++ b/clearing-parent/reports-service/src/test/java/ru/spcex/clearing/reports/services/DOCXServiceTest.java @@ -19,10 +19,9 @@ class DOCXServiceTest { valuesMap.put("${test3}", "======== TEST 3 ========="); DOCXService service = new DOCXService(); - service.insertValues(new File(getClass().getClassLoader().getResource("REC.ACTV_TEST.docx").getPath()), - new File("output.docx"), + service.init(new File(getClass().getClassLoader().getResource("REC.ACTV_TEST.docx").getPath()), null); + service.insertValues(new File("output.docx"), valuesMap, - null, false); File file = new File("output.docx"); diff --git a/platform-parent/platform-messaging/src/main/java/ru/spcex/clearing/platform/messaging/domain/cud/reports/NotificationRequest.java b/platform-parent/platform-messaging/src/main/java/ru/spcex/clearing/platform/messaging/domain/cud/reports/NotificationRequest.java new file mode 100644 index 000000000..f09103e1d --- /dev/null +++ b/platform-parent/platform-messaging/src/main/java/ru/spcex/clearing/platform/messaging/domain/cud/reports/NotificationRequest.java @@ -0,0 +1,23 @@ +package ru.spcex.clearing.platform.messaging.domain.cud.reports; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Запрос на формирование уведомления + */ +public class NotificationRequest { + /** + * Идентификатор участника клиринга + */ + @JsonProperty + protected String consumerId; + + + public String getConsumerId() { + return consumerId; + } + + public void setConsumerId(String consumerId) { + this.consumerId = consumerId; + } +}