From e3e2a27a8279eb5b88341c758f1d3d8c572b03c1 Mon Sep 17 00:00:00 2001 From: AKurakin Date: Thu, 30 Jun 2022 19:43:10 +0300 Subject: [PATCH] =?UTF-8?q?Storage.=20=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?.=20=D0=A3=D0=B6=D0=B5=20=D1=81=D0=BA=D0=BE=D0=BC=D0=BF=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clearing-classes/pom.xml | 37 +++ .../clearing/classes/ConstSerializable.java | 8 + .../classes/StaticData/Company/Company.java | 59 ++++ .../StaticData/Company/CompanyRoleSet.java | 30 ++ .../classes/StaticData/User/ClearingUser.java | 27 ++ .../classes/objects/BusinessEvent.java | 25 ++ .../classes/objects/BusinessObject.java | 31 ++ .../clearing/classes/objects/ObjectBase.java | 55 ++++ .../ru/clearing/common/interfaces/WithId.java | 5 + .../target/dfa-classes-DFA-1.0.0.0.jar | Bin 0 -> 39740 bytes .../target/maven-archiver/pom.properties | 4 + .../compile/default-compile/createdFiles.lst | 8 + .../compile/default-compile/inputFiles.lst | 8 + clearing-dictionary/.gitignore | 12 + clearing-dictionary/CHANGELOG.md | 3 + clearing-dictionary/pom.xml | 118 +++++++ .../dictionary/AbstractDictionary.java | 29 ++ .../ConstDictionarySerializable.java | 5 + .../platform/dictionary/Dictionary.java | 17 ++ .../dictionary/WorkflowStatusDictionary.java | 9 + .../dictionary/special/CountryCode.java | 47 +++ pom.xml | 5 + storage/pom.xml | 36 ++- .../storage/base/BusinessEventMapStore.java | 61 ++++ .../storage/base/BusinessObjectMapStore.java | 35 +++ .../storage/base/DictionaryMapStore.java | 91 ++++++ .../storage/base/LoggingMapStore.java | 94 ++++++ .../storage/base/ObjectBaseMapStore.java | 174 +++++++++++ .../storage/base/SimpleObjectMapStore.java | 200 ++++++++++++ .../config/ConfigurationRootElement.java | 48 +++ .../storage/config/DbConnectionConfig.java | 72 +++++ .../clearing/storage/config/DfaConfig.java | 34 +++ .../config/HazelcastConfiguration.java | 71 +++++ .../storage/config/PlatformStorageBeans.java | 9 + .../storage/config/PoolMapConfigs.java | 74 +++++ .../storage/config/ServicesElement.java | 34 +++ .../storage/config/SettingsElement.java | 29 ++ .../config/SettingsElementDatabase.java | 89 ++++++ .../WorkflowStatusDictionaryMapStore.java | 24 ++ .../error/ModuleInitializeException.java | 19 ++ .../object/CompanyRoleSetMapStore.java | 61 ++++ .../storage/utils/BigDecimalUtil.java | 111 +++++++ .../storage/utils/DbDefaultConfig.java | 11 + .../clearing/storage/utils/DbUtilsHelper.java | 51 ++++ .../storage/utils/IMDGDistributedNames.java | 10 + .../clearing/storage/utils/TimeUtil.java | 288 ++++++++++++++++++ 46 files changed, 2264 insertions(+), 4 deletions(-) create mode 100644 clearing-classes/pom.xml create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/ConstSerializable.java create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/Company.java create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/CompanyRoleSet.java create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/StaticData/User/ClearingUser.java create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessEvent.java create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessObject.java create mode 100644 clearing-classes/src/main/java/ru/clearing/classes/objects/ObjectBase.java create mode 100644 clearing-classes/src/main/java/ru/clearing/common/interfaces/WithId.java create mode 100644 clearing-classes/target/dfa-classes-DFA-1.0.0.0.jar create mode 100644 clearing-classes/target/maven-archiver/pom.properties create mode 100644 clearing-classes/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 clearing-classes/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 clearing-dictionary/.gitignore create mode 100644 clearing-dictionary/CHANGELOG.md create mode 100644 clearing-dictionary/pom.xml create mode 100644 clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/AbstractDictionary.java create mode 100644 clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/ConstDictionarySerializable.java create mode 100644 clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/Dictionary.java create mode 100644 clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/WorkflowStatusDictionary.java create mode 100644 clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/special/CountryCode.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/base/BusinessEventMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/base/BusinessObjectMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/base/DictionaryMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/base/LoggingMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/base/ObjectBaseMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/base/SimpleObjectMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/ConfigurationRootElement.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/DbConnectionConfig.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/DfaConfig.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/HazelcastConfiguration.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/PlatformStorageBeans.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/PoolMapConfigs.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/ServicesElement.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElement.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElementDatabase.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/dictionary/WorkflowStatusDictionaryMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/error/ModuleInitializeException.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/object/CompanyRoleSetMapStore.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/utils/BigDecimalUtil.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/utils/DbDefaultConfig.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/utils/DbUtilsHelper.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/utils/IMDGDistributedNames.java create mode 100644 storage/src/main/java/ru/spcex/clearing/storage/utils/TimeUtil.java diff --git a/clearing-classes/pom.xml b/clearing-classes/pom.xml new file mode 100644 index 000000000..402e26058 --- /dev/null +++ b/clearing-classes/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + clearing-classes + Clearing classes + Clearing classes module + jar + + + clearing + ru.spcex.clearing + SPCEX-1.0.0.0 + + + + 8 + 8 + + + + + + + + + + + + + + + ${project.artifactId}-${project.version} + + \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/classes/ConstSerializable.java b/clearing-classes/src/main/java/ru/clearing/classes/ConstSerializable.java new file mode 100644 index 000000000..8e607f379 --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/ConstSerializable.java @@ -0,0 +1,8 @@ +package ru.clearing.classes; + +/** + * В случае любых изменений модуля classes необходимо изменить serialVersionUID++ + */ +public interface ConstSerializable { + long serialVersionUID = 293236453420L; +} diff --git a/clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/Company.java b/clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/Company.java new file mode 100644 index 000000000..baaadd980 --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/Company.java @@ -0,0 +1,59 @@ +package ru.clearing.classes.StaticData.Company; + +import ru.clearing.classes.ConstSerializable; +import ru.clearing.classes.objects.BusinessObject; + +/** + * + */ +public class Company extends BusinessObject { + private static final long serialVersionUID = ConstSerializable.serialVersionUID; + + private String clearingCode; + private String exchangeCode; + private String fullName; +// private CompanyInfo profile; // todo impl profile + private Long statusId; // WorkflowStatusDictionary + + @Override + public String toString() { + return "Company{" + + "clearingCode='" + clearingCode + '\'' + + ", exchangeCode='" + exchangeCode + '\'' + + ", fullName='" + fullName + '\'' + + ", statusId=" + statusId + + '}'; + } + + public String getClearingCode() { + return clearingCode; + } + + public void setClearingCode(String clearingCode) { + this.clearingCode = clearingCode; + } + + public String getExchangeCode() { + return exchangeCode; + } + + public void setExchangeCode(String exchangeCode) { + this.exchangeCode = exchangeCode; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public Long getStatusId() { + return statusId; + } + + public void setStatusId(Long statusId) { + this.statusId = statusId; + } +} \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/CompanyRoleSet.java b/clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/CompanyRoleSet.java new file mode 100644 index 000000000..7304ab48b --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/StaticData/Company/CompanyRoleSet.java @@ -0,0 +1,30 @@ +package ru.clearing.classes.StaticData.Company; + +import ru.clearing.classes.ConstSerializable; +import ru.clearing.classes.objects.ObjectBase; + +/** + * DB: COMPANY_ROLE_SET + */ +public class CompanyRoleSet extends ObjectBase { + private static final long serialVersionUID = ConstSerializable.serialVersionUID; + + private Long roleId; + private Long companyId; + + public Long getRoleId() { + return roleId; + } + + public void setRoleId(Long roleId) { + this.roleId = roleId; + } + + public Long getCompanyId() { + return companyId; + } + + public void setCompanyId(Long companyId) { + this.companyId = companyId; + } +} \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/classes/StaticData/User/ClearingUser.java b/clearing-classes/src/main/java/ru/clearing/classes/StaticData/User/ClearingUser.java new file mode 100644 index 000000000..a0e4f93d6 --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/StaticData/User/ClearingUser.java @@ -0,0 +1,27 @@ +package ru.clearing.classes.StaticData.User; + + +import ru.clearing.classes.ConstSerializable; +import ru.clearing.classes.objects.BusinessObject; + +/** + * Database table: USER_CLEARING todo name for user ? + */ +public class ClearingUser extends BusinessObject { + private static final long serialVersionUID = ConstSerializable.serialVersionUID; + +// private String idpUuid; +// +// public ClearingUser() { +// super(); +// } +// +// public String toString() { +// return String.format("%s{id=%d, idpUuid='%s'}", getClass().getSimpleName(), getId(), getIdpUuid()); +// } +// +// public String getIdpUuid() { +// return idpUuid; +// } + +} \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessEvent.java b/clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessEvent.java new file mode 100644 index 000000000..d851b81ba --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessEvent.java @@ -0,0 +1,25 @@ +package ru.clearing.classes.objects; + +import ru.clearing.classes.ConstSerializable; + +import java.util.Date; + +/** + * + */ +public abstract class BusinessEvent extends ObjectBase { + static final long serialVersionUID = ConstSerializable.serialVersionUID; + + private Date eventTime; + + public BusinessEvent() { + } + + public Date getEventTime() { + return eventTime; + } + + public void setEventTime(Date eventTime) { + this.eventTime = eventTime; + } +} \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessObject.java b/clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessObject.java new file mode 100644 index 000000000..8c67d25a8 --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/objects/BusinessObject.java @@ -0,0 +1,31 @@ +package ru.clearing.classes.objects; + +import ru.clearing.classes.ConstSerializable; + +import java.time.Instant; + +/** + * UUID - Long based with field object + */ +public abstract class BusinessObject extends ObjectBase { + static final long serialVersionUID = ConstSerializable.serialVersionUID; + + private Instant created; + private Instant updated; + + public Instant getCreated() { + return created; + } + + public void setCreated(Instant created) { + this.created = created; + } + + public Instant getUpdated() { + return updated; + } + + public void setUpdated(Instant updated) { + this.updated = updated; + } +} \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/classes/objects/ObjectBase.java b/clearing-classes/src/main/java/ru/clearing/classes/objects/ObjectBase.java new file mode 100644 index 000000000..80e45d86e --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/classes/objects/ObjectBase.java @@ -0,0 +1,55 @@ +package ru.clearing.classes.objects; + +import ru.clearing.classes.ConstSerializable; +import ru.clearing.common.interfaces.WithId; + +import java.io.Serializable; + +/** + * Long ID - based primary key object + */ +public abstract class ObjectBase implements WithId, Serializable { + static final long serialVersionUID = ConstSerializable.serialVersionUID; + + protected Long id; + public ObjectBase(){ + } + + public ObjectBase(Long id) { + this.id = id; + } + + public static Long getIdOrNull(WithId o) { + return o == null ? null : o.getId(); + } + + @Override + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String toString() { + return String.format("%s{id=%d}", getClass().getSimpleName(), getId()); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ObjectBase otcObject = (ObjectBase) o; + + return id.equals(otcObject.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} \ No newline at end of file diff --git a/clearing-classes/src/main/java/ru/clearing/common/interfaces/WithId.java b/clearing-classes/src/main/java/ru/clearing/common/interfaces/WithId.java new file mode 100644 index 000000000..2db02001d --- /dev/null +++ b/clearing-classes/src/main/java/ru/clearing/common/interfaces/WithId.java @@ -0,0 +1,5 @@ +package ru.clearing.common.interfaces; + +public interface WithId { + Long getId(); +} diff --git a/clearing-classes/target/dfa-classes-DFA-1.0.0.0.jar b/clearing-classes/target/dfa-classes-DFA-1.0.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..1f3bc1eea11b0a247599779688428219f6512be4 GIT binary patch literal 39740 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 H3i + + 4.0.0 + + clearing-dictionary + PLATFORM dictionary + PLATFORM dictionary + jar + SPCEX-1.0.0.0 + + + clearing + ru.spcex.clearing + SPCEX-1.0.0.0 + + + + 5.4.2 + + + + + + + org.junit.jupiter + junit-jupiter + ${external_libraries.junit-jupiter.version} + test + + + junit + junit + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.apache.commons + commons-lang3 + 3.7 + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + ${project.java.version} + ${project.java.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + + true + true + + + ${built.by} + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + true + + + ${built.by} + + + + + + + + + + + + + \ No newline at end of file diff --git a/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/AbstractDictionary.java b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/AbstractDictionary.java new file mode 100644 index 000000000..f9ebdabe2 --- /dev/null +++ b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/AbstractDictionary.java @@ -0,0 +1,29 @@ +package ru.clearing.platform.dictionary; + +public abstract class AbstractDictionary implements Dictionary { + static final long serialVersionUID = ConstDictionarySerializable.serialVersionUID; + + protected Long id; + protected String name; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + +} \ No newline at end of file diff --git a/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/ConstDictionarySerializable.java b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/ConstDictionarySerializable.java new file mode 100644 index 000000000..7f7028c67 --- /dev/null +++ b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/ConstDictionarySerializable.java @@ -0,0 +1,5 @@ +package ru.clearing.platform.dictionary; + +public interface ConstDictionarySerializable { + long serialVersionUID = 7020097431335696640L; +} diff --git a/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/Dictionary.java b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/Dictionary.java new file mode 100644 index 000000000..a24bd2477 --- /dev/null +++ b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/Dictionary.java @@ -0,0 +1,17 @@ +package ru.clearing.platform.dictionary; + +import java.io.Serializable; + +/** + * Словарь. + */ +public interface Dictionary extends Serializable { + + Long getId(); + + void setId(Long id); + + String getName(); + + void setName(String id); +} diff --git a/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/WorkflowStatusDictionary.java b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/WorkflowStatusDictionary.java new file mode 100644 index 000000000..5864f07ee --- /dev/null +++ b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/WorkflowStatusDictionary.java @@ -0,0 +1,9 @@ +package ru.clearing.platform.dictionary; + +/** + * Database table: WORKFLOW_STATUS_DICTIONARY + */ +public class WorkflowStatusDictionary extends AbstractDictionary { + private static final long serialVersionUID = ConstDictionarySerializable.serialVersionUID; + +} \ No newline at end of file diff --git a/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/special/CountryCode.java b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/special/CountryCode.java new file mode 100644 index 000000000..5156a9120 --- /dev/null +++ b/clearing-dictionary/src/main/java/ru/clearing/platform/dictionary/special/CountryCode.java @@ -0,0 +1,47 @@ +package ru.clearing.platform.dictionary.special; + +import ru.clearing.platform.dictionary.ConstDictionarySerializable; +import ru.clearing.platform.dictionary.Dictionary; + +import java.io.Serializable; + +/** + * DB table: COUNTRY_CODE_DICTIONARY + * Dictionary + */ +public class CountryCode implements Serializable, Dictionary { + static final long serialVersionUID = ConstDictionarySerializable.serialVersionUID; + + private Long id; + private String name; + private String countryCode; + + @Override + public String toString() { + return String.format("CountryCode{id=%d, countryCode='%s'}", id, countryCode); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 72f449272..f8f24506b 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ frontendapi storage db-scripts + clearing-classes + clearing-dictionary @@ -27,6 +29,9 @@ SPCEX-1.0.0.0 2.3.12.RELEASE + + + 3.12.4 diff --git a/storage/pom.xml b/storage/pom.xml index 7066ccccb..20925b6be 100644 --- a/storage/pom.xml +++ b/storage/pom.xml @@ -17,10 +17,6 @@ - - org.springframework - spring-jdbc - org.springframework.boot spring-boot-starter @@ -29,6 +25,38 @@ org.springframework.boot spring-boot-autoconfigure + + ru.spcex.clearing + clearing-classes + SPCEX-1.0.0.0 + compile + + + ru.spcex.clearing + clearing-dictionary + SPCEX-1.0.0.0 + compile + + + + + + org.springframework + spring-jdbc + + + com.mchange + c3p0 + 0.9.5.2 + + + + + com.hazelcast + hazelcast-all + ${external_libraries.hazelcast.version} + + jar/${project.artifactId} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/base/BusinessEventMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/base/BusinessEventMapStore.java new file mode 100644 index 000000000..84b7d83c7 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/base/BusinessEventMapStore.java @@ -0,0 +1,61 @@ +package ru.spcex.clearing.storage.base; + +import org.springframework.jdbc.core.JdbcTemplate; +import ru.clearing.classes.objects.BusinessEvent; +import ru.clearing.classes.objects.BusinessObject; +import ru.spcex.clearing.storage.utils.TimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +public abstract class BusinessEventMapStore extends ObjectBaseMapStore { + + public BusinessEventMapStore(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + public final Iterable loadAllKeys() { + return Collections.emptyList(); + } + + @Override + public final T load(Long id) { + return null; + } + + @Override + public final Collection load(Collection keys) { + return Collections.emptyList(); + } + + @Override + public final Map loadAll(Collection keys) { + return Collections.emptyMap(); + } + + /** + * Заполняет поля:
+ * uuid
+ * eventTime
+ * ownerId
+ * eventTypeId + * + * @param businessEvent объект для заполнения + * @param rs resultSet для выгрузки + */ + public void fillBusinessEventFields(BusinessEvent businessEvent, ResultSet rs) throws SQLException { + businessEvent.setEventTime(TimeUtil.toUtilDate(rs.getDate("eventtime"))); + businessEvent.setId(getLong(rs, "id")); + } + + public void fillBusinessObjectFieds(BusinessObject businessObject, ResultSet rs) throws SQLException { + businessObject.setId(rs.getObject("orderbondid", Long.class)); + businessObject.setUpdated(TimeUtil.fromDate(rs.getDate("updated"))); + businessObject.setCreated(TimeUtil.fromDate(rs.getDate("created"))); + } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/base/BusinessObjectMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/base/BusinessObjectMapStore.java new file mode 100644 index 000000000..64e3093cf --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/base/BusinessObjectMapStore.java @@ -0,0 +1,35 @@ +package ru.spcex.clearing.storage.base; + +import org.springframework.jdbc.core.JdbcTemplate; +import ru.clearing.classes.objects.BusinessObject; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Шаблон для загрузки BusinessObject-ов. + */ +public abstract class BusinessObjectMapStore extends ObjectBaseMapStore { + + public BusinessObjectMapStore(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + /** + * Заполняет стандартные поля для BusinessObject из ResultSet + * + * @param businessObject дозаполнить стандартные поля этому объекту + * @param rs из ResultSet + * @throws SQLException - error + */ + protected void fillBusinessObjectFields(BusinessObject businessObject, ResultSet rs) throws SQLException { + fillBusinessObjectFields(businessObject, rs, "id"); + } + + protected void fillBusinessObjectFields(BusinessObject businessObject, ResultSet rs, String idName) throws SQLException { + businessObject.setUpdated(getInstantFromTimestamp(rs, "updated")); + businessObject.setCreated(getInstantFromTimestamp(rs, "created")); + businessObject.setId(getLong(rs, idName)); + } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/base/DictionaryMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/base/DictionaryMapStore.java new file mode 100644 index 000000000..2903d40e2 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/base/DictionaryMapStore.java @@ -0,0 +1,91 @@ +package ru.spcex.clearing.storage.base; + +import com.hazelcast.core.MapLoader; +import ru.clearing.platform.dictionary.Dictionary; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +import static ru.spcex.clearing.storage.base.ObjectBaseMapStore.MAX_IN_CLAUSE_SIZE; + +/** + * Store для словарей стандартных - из ID, NAME. + */ +public abstract class DictionaryMapStore implements MapLoader { + private static Logger log = LoggerFactory.getLogger(DictionaryMapStore.class); + + protected NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + protected final JdbcTemplate jdbcTemplate; + + protected DictionaryMapStore(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + } + + public abstract String getTableName(); + + public abstract T getDictionaryObject(); + + protected T mapRow(ResultSet rs, int rowNum) throws SQLException { + T dictionary = getDictionaryObject(); + dictionary.setId(rs.getLong("id")); + dictionary.setName(rs.getString("name")); + return dictionary; + } + + private Collection loadCollection(Collection keys) { + Map> paramMap = Collections.singletonMap("ids", keys); + return namedParameterJdbcTemplate.query("select * from " + getTableName() + " where id in (:ids)", paramMap, this::mapRow); + } + + @Override + public T load(Long id) { + Collection rows; + List list = Collections.singletonList(id); + try { + rows = loadCollection(list); + } catch (Throwable e) { // one retry + log.trace("Retry load after {}", e.toString()); + rows = loadCollection(list); + } + return DataAccessUtils.singleResult(rows); + } + + @Override + public Map loadAll(Collection keys) { + log.trace("loadAll from dictionary {} {} keys", getTableName(), keys.size()); + Map result = new HashMap<>(); + long start = System.currentTimeMillis(); + + // загрузить данные по ключам частями, чтобы не выйти за ограничения базы по кол-ву элементов в in clause + List keysSubList = new ArrayList<>(MAX_IN_CLAUSE_SIZE); + for (Iterator iterator = keys.iterator(); iterator.hasNext(); ) { + Long key = iterator.next(); + keysSubList.add(key); + if (keysSubList.size() == MAX_IN_CLAUSE_SIZE || !iterator.hasNext()) { + Collection rows = loadCollection(keysSubList); + for (T row : rows) { + result.put(row.getId(), row); + } + keysSubList.clear(); + } + } + + log.trace("loadAll from dictionary {} {} keys done in {} ms", getTableName(), keys.size(), (System.currentTimeMillis() - start)); + + return result; + } + + @Override + public Iterable loadAllKeys() { + log.debug("loadAllKeys from " + getTableName()); + return jdbcTemplate.query("select id from " + getTableName(), (rs, rowNum) -> rs.getLong("id")); + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/base/LoggingMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/base/LoggingMapStore.java new file mode 100644 index 000000000..622001a29 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/base/LoggingMapStore.java @@ -0,0 +1,94 @@ +package ru.spcex.clearing.storage.base; + +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.MapLoaderLifecycleSupport; +import com.hazelcast.core.MapStore; +//import com.opencsv.CSVWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.NonNull; +import ru.clearing.classes.objects.ObjectBase; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.function.Supplier; + +/** + * MapStore записывающий изменения в файл + * + * @param + */ +public abstract class LoggingMapStore implements MapStore, MapLoaderLifecycleSupport { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + +// private CSVWriter csvWriter; todo подключить OpenCSV и настройку про логирование данных добавить + +// private Supplier recoveryPath = () -> DfaConfig.get().getRoot().getSettings().getRecoveryLogPath(); + + @Override + public void init(HazelcastInstance hazelcastInstance, Properties properties, String mapName) { +// try { +// String recoveryLogPath = recoveryPath.get(); +// if (recoveryLogPath != null) { +// SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); +// File recoveryDir = new File(recoveryLogPath + File.separator + simpleDateFormat.format(new Date())); +// recoveryDir.mkdirs(); +// File recoveryFile = new File(recoveryDir, getTableName() + ".csv"); +// csvWriter = new CSVWriter(new BufferedWriter(new FileWriter(recoveryFile)), ';'); +// log.info("recovery log started {}", recoveryFile); +// } +// } catch (Exception e) { +// log.error("", e); +// } + } + +// public void setRecoveryPath(Supplier recoveryPath) { +// this.recoveryPath = recoveryPath; +// } + + @Override + public void destroy() { +// try { +// if (csvWriter != null) +// csvWriter.close(); +// } catch (IOException e) { +// log.error("", e); +// } + } + + public abstract String getTableName(); + + protected void logHeaders(@NonNull String[] headers) { + try { +// if (csvWriter != null) { +// csvWriter.writeNext(headers); +// csvWriter.flush(); +// } + } catch (Exception e) { + log.error("", e); + } + } + + protected void logValues(@NonNull List valuesList) { + try { +// if (csvWriter != null) { +// for (Object[] values : valuesList) { +// String[] nextLine = new String[values.length]; +// for (int i = 0; i < values.length; i++) { +// nextLine[i] = String.valueOf(values[i]); +// } +// csvWriter.writeNext(nextLine); +// } +// csvWriter.flush(); +// } + } catch (Exception e) { + log.error("", e); + } + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/base/ObjectBaseMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/base/ObjectBaseMapStore.java new file mode 100644 index 000000000..fa60ad33a --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/base/ObjectBaseMapStore.java @@ -0,0 +1,174 @@ +package ru.spcex.clearing.storage.base; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import ru.clearing.classes.objects.ObjectBase; +import ru.spcex.clearing.storage.utils.DbUtilsHelper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.*; + +public abstract class ObjectBaseMapStore extends SimpleObjectMapStore { + /** + * См. legalEntityProfileToSQLArgs() + */ + protected static final String LEGAL_ENTITY_PROFILE_STATEMENT_ARGUMENT = + "description, domicilecountrycodeid, isprofessional, organizationtypeid, ogrn, name," + + " fullname, nameeng, fullnameeng, kpp, contactphone, contactemail, tsedallowedid, edomail," + + " nsdallowedid, otcmonitorallowedid, otcmonitorobligedid"; + + protected static final String insertChargeDefinitionStatement = DbUtilsHelper.fillValuesBlock( + "UPDATE OR INSERT INTO CHARGEDEFINITION (id, chargeamount, chargedirectionid, " + + " chargesettlementamount, chargetypeid, netincluded, currencycodeid, moexexchangecommission, userid, partyid)" + + " values %values_block% matching (id);" + ); + public static final int MAX_IN_CLAUSE_SIZE = 1000; + + protected NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + public ObjectBaseMapStore(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + } + +// @Override +// public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { +// this.jdbcTemplate = jdbcTemplate; +// this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); +// } + + @Override + public T load(Long id) { + if (!isLoadable(id)) return null; + Collection rows; + List list = Collections.singletonList(id); + try { + rows = load(list); + } catch (Throwable e) { // one retry + rows = load(list); + } + T obj = DataAccessUtils.singleResult(rows); + if (obj != null && isLoadable(obj)) + return obj; + else + return null; + } + + protected boolean isLoadable(Long id) { + return true; + } + + protected boolean isLoadable(T obj) { + return true; + } + + @Override + public Map loadAll(Collection keys) { + log.debug("loadAll from " + getTableName() + " " + keys.size() + " keys"); + Map result = new HashMap<>(); + long start = System.currentTimeMillis(); + + // загрузить данные по ключам частями, чтобы не выйти за ограничения базы по кол-ву элементов в in clause + List keysSubList = new ArrayList<>(MAX_IN_CLAUSE_SIZE); + for (Iterator iterator = keys.iterator(); iterator.hasNext(); ) { + Long key = iterator.next(); + keysSubList.add(key); + if (keysSubList.size() == MAX_IN_CLAUSE_SIZE || !iterator.hasNext()) { + Collection rows = load(keysSubList); + for (T row : rows) { + result.put(row.getId(), row); + } + keysSubList.clear(); + } + } + + log.debug("loadAll from " + getTableName() + " " + keys.size() + " keys done in " + (System.currentTimeMillis() - start) + "ms"); + + return result; + } + + public abstract Collection load(Collection keys); + + protected Long getLong(ResultSet rs, String columnName) throws SQLException { + return rs.getObject(columnName, Long.class); + } + + protected Instant getInstantFromTimestamp(ResultSet rs, String column) throws SQLException { + Timestamp date = rs.getTimestamp(column); + return date != null ? date.toInstant() : null; + } + +/* todo реfакторинг: + protected LegalEntityProfile getLegalEntityProfile(Long id) { + if (id == null) + return null; + List results = jdbcTemplate.query( + "SELECT * FROM legalentityprofile WHERE id = ?", new Object[]{id}, + (rs, rowNum) -> { + LegalEntityProfile legalEntityProfile = new LegalEntityProfile(); + legalEntityProfile.setDescription(rs.getString("description")); + legalEntityProfile.setDomicileCountryCodeId(rs.getObject("domicilecountrycodeid", Long.class)); + legalEntityProfile.setProfessional(rs.getObject("isprofessional", Boolean.class)); + legalEntityProfile.setOrganizationTypeId(rs.getObject("organizationtypeid", Long.class)); + legalEntityProfile.setOgrn(rs.getString("ogrn")); + legalEntityProfile.setShortName(rs.getString("name")); + legalEntityProfile.setFullName(rs.getString("fullname")); + legalEntityProfile.setNameEng(rs.getString("nameeng")); + legalEntityProfile.setFullNameEng(rs.getString("fullnameeng")); + legalEntityProfile.setKpp(rs.getString("kpp")); + legalEntityProfile.setContactPhone(rs.getString("contactphone")); + legalEntityProfile.setContactEMail(rs.getString("contactemail")); + legalEntityProfile.setTsedAllowedId(rs.getObject("tsedallowedid", Long.class)); + legalEntityProfile.setEdoMail(rs.getString("edomail")); + legalEntityProfile.setNsdAllowedId(rs.getObject("nsdallowedid", Long.class)); + legalEntityProfile.setOtcMonitorAllowedId(rs.getObject("otcmonitorallowedid", Long.class)); + legalEntityProfile.setOtcMonitorObligedId(rs.getObject("otcmonitorobligedid", Long.class)); + legalEntityProfile.setId(getLong(rs, "id")); + return legalEntityProfile; + } + ); + LegalEntityProfile onceObject = DataAccessUtils.singleResult(results); + if (onceObject == null) { + log.warn("No row in table legalentityprofile where id={}. Create empty ", id); + onceObject = new LegalEntityProfile(); + onceObject.setId(id); // чтобы не потерять id + } + return onceObject; + } +*/ + +// /** +// * Без ID перечисление значений полей LegalEntityProfile. +// * См. LegalentityprofileStatementArgument +// * +// * @param legalEntityProfile +// * @return +// */ +// protected List legalEntityProfileToSQLArgs(@NonNull LegalEntityProfile legalEntityProfile) { +// List legalEntityArgs = new ArrayList<>(Arrays.asList( +//// legalEntityProfile.getId(), +// legalEntityProfile.getDescription(), +// legalEntityProfile.getDomicileCountryCodeId(), +// legalEntityProfile.getProfessional(), +// legalEntityProfile.getOrganizationTypeId(), +// legalEntityProfile.getOgrn(), +// legalEntityProfile.getShortName(), +// legalEntityProfile.getFullName(), +// legalEntityProfile.getNameEng(), +// legalEntityProfile.getFullNameEng(), +// legalEntityProfile.getKpp(), +// legalEntityProfile.getContactPhone(), +// legalEntityProfile.getContactEMail(), +// legalEntityProfile.getTsedAllowedId(), +// legalEntityProfile.getEdoMail(), +// legalEntityProfile.getNsdAllowedId(), +// legalEntityProfile.getOtcMonitorAllowedId(), +// legalEntityProfile.getOtcMonitorObligedId() +// )); +// return legalEntityArgs; +// } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/base/SimpleObjectMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/base/SimpleObjectMapStore.java new file mode 100644 index 000000000..8d45ff45c --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/base/SimpleObjectMapStore.java @@ -0,0 +1,200 @@ +package ru.spcex.clearing.storage.base; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.lang.NonNull; +import ru.clearing.classes.objects.ObjectBase; +import ru.spcex.clearing.storage.utils.BigDecimalUtil; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.*; + +/** + * MapStore для выгрузки обычных объектов + * + * @param + */ +public abstract class SimpleObjectMapStore extends LoggingMapStore +{ + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + protected SimpleDateFormat FIREBIRD_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd"); // не делать static. + protected final JdbcTemplate jdbcTemplate; + private final int bathSize = 1000; + + protected SimpleObjectMapStore(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + logHeaders(getFields()); + } + + public abstract String getTableName(); + + public abstract String[] getFields(); + + private boolean oidIsPresentInTheFields = Arrays.stream(getFields()).anyMatch(anotherString -> { + if (anotherString == null) return false; + return "oid".equalsIgnoreCase(anotherString.trim()); + }); + + public boolean deleteIsSupported() { + return !oidIsPresentInTheFields; + } + + @Override + public final void store(Long id, T o) { + log.debug("store {} {} {}", getTableName(), id, o); + Map map = new HashMap<>(); + map.put(id, o); + store(map); + } + + @Override + public final void storeAll(Map map) { + log.debug("storeAll {} {}", getTableName(), map); + store(map); + } + + public abstract void store(Map map); + + @Override + public void delete(Long id) { + if (deleteIsSupported()) { + defaultDelete(id); + } else { + throw new UnsupportedOperationException("delete not supported"); + } + } + + @Override + public void deleteAll(Collection collection) { + if (deleteIsSupported()) { + deleteAllShowNotDeleted(collection); + } else { + throw new UnsupportedOperationException("deleteAll not supported"); + } + } + + protected void defaultDelete(Long id) { + ArrayList list = new ArrayList<>(); + list.add(id); + deleteAll(list); + } + + protected void deleteAllShowNotDeleted(Collection collection) { + List listOfID = new ArrayList<>(collection.size()); + for (Long id : collection) + listOfID.add(new Object[]{id}); + try { + jdbcTemplate.batchUpdate("DELETE FROM " + getTableName() + " WHERE id=?", listOfID); + } catch (Exception e) { + log.error("SQL error at batch DELETE FROM {}\n{}", getTableName(), ExceptionUtils.getStackTrace(e)); + log.info("List of non deleted ID {}", collection); // показать список неудалённых UUID + throw e; + } + } + + @Override + public abstract T load(Long id); + + @Override + public Map loadAll(Collection keys) { + Map result = new HashMap<>(); + for (Long key : keys) { + result.put(key, load(key)); + } + return result; + } + + @Override + public Iterable loadAllKeys() { + log.debug("loadAllKeys from " + getTableName()); + List keys; + try { + keys = jdbcTemplate.query("select id from " + getTableName(), (resultSet, i) -> resultSet.getLong("id")); + } catch (Throwable e) { + log.error("{}", ExceptionUtils.getStackTrace(e)); + throw e; + } + return keys; + } + + /** + * Загружает ключи только за текущий день. + * where CAST(tradingday as DATE) = ? + * Осторожно со знаком: в некоторых случаях требуется за текущий и будущие дни, тогда этот метод не подходит. + * См. так же loadAllKeys(). + * + * @return список id + */ + protected List defaultLoadAllKeysOnTradingDay() { + log.debug("loadAllKeys from " + getTableName()); + return jdbcTemplate.query("select id from " + getTableName() + " where CAST(tradingday as DATE) = ?", + new Object[]{FIREBIRD_DATE_FORMATTER.format(new Date())}, + (resultSet, i) -> resultSet.getObject("id", Long.class)); + } + + + protected void batchInsertUpdate(@NonNull String insertStatement, @NonNull List args) { + try { + logValues(args); + int[][] ret = jdbcTemplate.batchUpdate(insertStatement, args, bathSize, (ps, params) -> { + for (int i = 0; i < params.length; i++) { + int type = ps.getParameterMetaData().getParameterType(i + 1); + Object value = params[i]; + if (value != null) { + if ((type == Types.VARCHAR || type == Types.CHAR) && value instanceof String) { + String stringValue = (String) value; + int length = ps.getParameterMetaData().getPrecision(i + 1); + if (stringValue.length() > length) { + value = stringValue.substring(0, length); + log.error("String value for insert is too large [ insertSql: {}, params: {}, incorrect value : {}, normalized value: {} ]", + insertStatement, Arrays.toString(params), stringValue, value); + } + } else if ((type == Types.DECIMAL || type == Types.NUMERIC) && value instanceof BigDecimal) { + BigDecimal bigDecimalValue = (BigDecimal) value; + int dbScalePartLength = ps.getParameterMetaData().getScale(i + 1); + int precision = ps.getParameterMetaData().getPrecision(i + 1); + if (!BigDecimalUtil.checkSize(bigDecimalValue, precision, dbScalePartLength)) { + value = BigDecimalUtil.genMaxValueForInsert(precision, dbScalePartLength); + log.error("BigDecimal value for insert is too large [ insertSql: {}, params: {}, incorrect value : {}, normalized value: {} ]", + insertStatement, Arrays.toString(params), bigDecimalValue, value); + } + } + } + //в batchUpdate(String sql, List batchArgs), как типа поля всегда передавался SqlTypeValue.TYPE_UNKNOWN, + //см. BatchUpdateUtils.setStatementParameters + try { + StatementCreatorUtils.setParameterValue(ps, i + 1, SqlTypeValue.TYPE_UNKNOWN, value); + } catch (SQLException e) { + throw new SQLException("batchInsertUpdate, at prepare argument " + i + " value = " + value + "; SQL=" + insertStatement, e); + } + } + }); + int batchSize = ret.length > 0 ? ret[0].length : ret.length; + log.debug("{} {} rows stored", getTableName(), batchSize); + } catch (Exception e) { + log.error("SQL error at batch UPDATE OR INSERT INTO {}\n{}", getTableName(), ExceptionUtils.getStackTrace(e)); + throw e; + } + } + + protected Instant getInstantFromTimestamp(ResultSet rs, String column) throws SQLException { + Timestamp date = rs.getTimestamp(column); + return date != null ? date.toInstant() : null; + } + + protected Timestamp timestampFromInstant(Instant instant) { + if (instant == null) return null; + return Timestamp.from(instant); + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/ConfigurationRootElement.java b/storage/src/main/java/ru/spcex/clearing/storage/config/ConfigurationRootElement.java new file mode 100644 index 000000000..75dc99b8a --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/ConfigurationRootElement.java @@ -0,0 +1,48 @@ +package ru.spcex.clearing.storage.config; + + +/** + * Корневой элемент конфигурации + */ + +@SuppressWarnings("DefaultAnnotationParam") +public class ConfigurationRootElement /*extends ConfigurationRootBaseElement*/ { + + /** + * Конфигурация БД + */ +// @JsonProperty(value = "Database", required = true) + private SettingsElementDatabase database = new SettingsElementDatabase(); + +// @JsonProperty(value = "Settings", required = false) + private SettingsElement settings = new SettingsElement(); + + /** + * Конфигурация Hazelcast + */ +// @JsonProperty(value = "HazelcastServer", required = true) +// private HazelcastServerElement hazelcast = new HazelcastServerElement(); + + /** + * Конфигурация сервисов Core + */ +// @JsonProperty(value = "Services") + private ServicesElement servicesElement = new ServicesElement(); + + public SettingsElementDatabase getDatabase() { + return database; + } + +// public HazelcastServerElement getHazelcast() { +// return hazelcast; +// } + + public SettingsElement getSettings() { + return settings; + } + + public ServicesElement getServicesElement() { + return servicesElement; + } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/DbConnectionConfig.java b/storage/src/main/java/ru/spcex/clearing/storage/config/DbConnectionConfig.java new file mode 100644 index 000000000..233157da2 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/DbConnectionConfig.java @@ -0,0 +1,72 @@ +package ru.spcex.clearing.storage.config; + +import com.mchange.v2.c3p0.ComboPooledDataSource; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import ru.spcex.clearing.storage.error.ModuleInitializeException; +import ru.spcex.clearing.storage.utils.DbDefaultConfig; + +import javax.sql.DataSource; +import java.sql.Connection; + +@SuppressWarnings("UnnecessaryLocalVariable") +@Configuration +public class DbConnectionConfig { + private Logger log = LoggerFactory.getLogger(this.getClass()); + + private ConfigurationRootElement configRoot = DfaConfig.get().getRoot(); + + @Bean + public DataSource dataSource() /*fixme why: throws PropertyVetoException*/ { + DataSource result; + String login = configRoot.getDatabase().getLogin(); + String password = configRoot.getDatabase().getPassword(); + String logTimeoutPart = ""; + String dbPath; + if (StringUtils.isEmpty(configRoot.getDatabase().getEmbeddedFilePath())) { + dbPath = configRoot.getDatabase().getJdbcConnectionString(); + int timeoutSec = configRoot.getDatabase().getConnectionAcquireTimeoutSeconds(); + ComboPooledDataSource cpds = new ComboPooledDataSource(); + try { + cpds.setDriverClass(configRoot.getDatabase().getDriver()); + } catch (Exception ue) { // fixme не компилится из-за PropertyVetoException + throw new RuntimeException(ue); + } + cpds.setJdbcUrl(dbPath); + cpds.setUser(login); + cpds.setPassword(password); + cpds.setInitialPoolSize(configRoot.getDatabase().getMinPoolSize()); + cpds.setMinPoolSize(configRoot.getDatabase().getMinPoolSize()); + cpds.setMaxPoolSize(configRoot.getDatabase().getMaxPoolSize()); + cpds.setNumHelperThreads(configRoot.getDatabase().getNumHelperThreads()); + cpds.setCheckoutTimeout(timeoutSec * 1000/*todo common 1000==ConstsCommon.SECOND*/); + logTimeoutPart = String.format(" (timeout=%ds)", timeoutSec); + result = cpds; + } else { + dbPath = configRoot.getDatabase().getEmbeddedFilePath(); + result = DbDefaultConfig.getEmbeddedDatabase(dbPath); + } + String OPERATION_DATABASE_CONNECTION_CHECK = String.format("Database [%s] connection check", dbPath); + try { + Connection conn = result.getConnection(); + conn.close(); + log.info("{}: success", OPERATION_DATABASE_CONNECTION_CHECK); + return result; + } catch (Throwable e) { + String msg = String.format("%s%s: failed: %s -> %s", + OPERATION_DATABASE_CONNECTION_CHECK, logTimeoutPart, e.getClass().getSimpleName(), e.getMessage()); + log.error(msg); + throw new ModuleInitializeException(msg, e); + } + } + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + return jdbcTemplate; + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/DfaConfig.java b/storage/src/main/java/ru/spcex/clearing/storage/config/DfaConfig.java new file mode 100644 index 000000000..dde133d1a --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/DfaConfig.java @@ -0,0 +1,34 @@ +package ru.spcex.clearing.storage.config; + + +public class DfaConfig { + protected static DfaConfig self; + + + ConfigurationRootElement root = new ConfigurationRootElement(); + + protected String defaultLogFileName() { + return "storage"; + } + + + public String appName() { + return "clearing-storage"; + } + + + protected Class parsedClazz() { + return ConfigurationRootElement.class; + } + + public static DfaConfig get() { + if (self == null) { + self = new DfaConfig(); + } + return self; + } + + public ConfigurationRootElement getRoot() { + return root; + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/HazelcastConfiguration.java b/storage/src/main/java/ru/spcex/clearing/storage/config/HazelcastConfiguration.java new file mode 100644 index 000000000..91995dd4a --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/HazelcastConfiguration.java @@ -0,0 +1,71 @@ +package ru.spcex.clearing.storage.config; + +import com.hazelcast.config.*; +import com.hazelcast.core.HazelcastInstance; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastInstanceFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HazelcastConfiguration { + + private ConfigurationRootElement configRoot = DfaConfig.get().getRoot(); + private final PoolMapConfigs poolMapConfigs; + + @Autowired + public HazelcastConfiguration(PoolMapConfigs poolMapConfigs) { + this.poolMapConfigs = poolMapConfigs; + } + + @Bean + public HazelcastInstance hazelcastServerInstance(Config config) { + return (new HazelcastInstanceFactory(config)).getHazelcastInstance(); + } + + @Bean + public Config hazelCastConfig() { + Config config = new Config(); + config.setInstanceName("instance"); + config.setGroupConfig(new GroupConfig() +// todo config .setName(configRoot.getHazelcast().getLogin()) +// .setPassword(configRoot.getHazelcast().getPassword() +// ) + ); +// if (StringUtils.isEmpty(configRoot.getHazelcast().getMancenterUrl())) { +// config.setManagementCenterConfig(new ManagementCenterConfig() +// .setEnabled(false) +// ); +// } else { +// config.setManagementCenterConfig(new ManagementCenterConfig() +// .setEnabled(true) +// .setUrl(configRoot.getHazelcast().getMancenterUrl()) +// ); +// } + config.setProperty("hazelcast.shutdownhook.enabled", "true"); + config.setProperty("hazelcast.logging.type", "slf4j"); + config.setProperty("hazelcast.operation.call.timeout.millis", "600000"); + config.setNetworkConfig(new NetworkConfig() +// .setPort(configRoot.getHazelcast().getListenPort()) + .setJoin(new JoinConfig() + .setMulticastConfig(new MulticastConfig() + .setEnabled(false)) + .setTcpIpConfig(new TcpIpConfig() + .setEnabled(true) +// .setMembers( +// DfaConfig.get() +// .getRoot().getHazelcast().getClusterMembersList() +// ) + ) + ) + ); + + config.addMapConfig(poolMapConfigs.map_WorkflowStatusDictionary()); + config.addMapConfig(poolMapConfigs.map_WorkflowStatusDictionary()); + + return config; + } + + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/PlatformStorageBeans.java b/storage/src/main/java/ru/spcex/clearing/storage/config/PlatformStorageBeans.java new file mode 100644 index 000000000..a43a90752 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/PlatformStorageBeans.java @@ -0,0 +1,9 @@ +package ru.spcex.clearing.storage.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan(basePackages = "com.moex.platform.storage") +public class PlatformStorageBeans { +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/PoolMapConfigs.java b/storage/src/main/java/ru/spcex/clearing/storage/config/PoolMapConfigs.java new file mode 100644 index 000000000..5202ef17d --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/PoolMapConfigs.java @@ -0,0 +1,74 @@ +package ru.spcex.clearing.storage.config; + +import com.hazelcast.config.*; +import com.hazelcast.core.MapLoader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import ru.spcex.clearing.storage.dictionary.WorkflowStatusDictionaryMapStore; +import ru.spcex.clearing.storage.object.CompanyRoleSetMapStore; +import ru.spcex.clearing.storage.utils.IMDGDistributedNames; + +@Configuration +public class PoolMapConfigs { + + private ConfigurationRootElement configRoot = DfaConfig.get().getRoot(); + + public PoolMapConfigs() { + } + + private MapStoreConfig makeDefaultMapStoreConfig(MapLoader mapBean) { + return new MapStoreConfig() + .setImplementation(mapBean) +;//todo config: .setWriteDelaySeconds(configRoot.getHazelcast().getDbSyncSeconds()); + } + + public ScheduledExecutorConfig makeDefaultScheduledExecutorConfig(String name) { + return new ScheduledExecutorConfig() + .setName(name) + .setPoolSize(16) + .setDurability(1) + .setCapacity(0); + } + + public MapConfig makeDefaultMapConfig(String mapName, MapLoader mapBean) { + return new MapConfig() + .setName(mapName) + .setInMemoryFormat(InMemoryFormat.OBJECT) + .setMapStoreConfig(makeDefaultMapStoreConfig(mapBean)) +//fixme config: .setBackupCount(configRoot.getHazelcast().getBackupCount()) + ; + } + + private NearCacheConfig makeDefaultNearCacheConfig() { + return new NearCacheConfig() + .setMaxIdleSeconds(3600) + .setInMemoryFormat(InMemoryFormat.OBJECT) + .setSerializeKeys(true) + ; + } + + private MapIndexConfig makeMapIndexConfig(String attributeName, boolean ordered) { + return new MapIndexConfig() + .setAttribute(attributeName) + .setOrdered(ordered); + } + + + + + @Autowired + private WorkflowStatusDictionaryMapStore workflowStatusDictionaryMapStore; + + public MapConfig map_WorkflowStatusDictionary() { + return makeDefaultMapConfig(IMDGDistributedNames.Map_WorkflowStatusDictionary, workflowStatusDictionaryMapStore) + .setNearCacheConfig(makeDefaultNearCacheConfig()); + } + + @Autowired + private CompanyRoleSetMapStore companyRoleSetMapStore; + + public MapConfig map_CompanyRoleSet() { + return makeDefaultMapConfig(IMDGDistributedNames.Map_CompanyRoleSet, companyRoleSetMapStore); + } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/ServicesElement.java b/storage/src/main/java/ru/spcex/clearing/storage/config/ServicesElement.java new file mode 100644 index 000000000..75e56a89a --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/ServicesElement.java @@ -0,0 +1,34 @@ +package ru.spcex.clearing.storage.config; + +//import com.fasterxml.jackson.annotation.JsonFormat; +//import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.Date; + +/** + * Настройка сервисов + */ +public class ServicesElement implements Serializable { + + /** + * Время, через которое выставленная заявка будет снята, в миллисекундах. + */ +// @JsonProperty(value = "ExpirationDelay") + private Integer ExpirationDelay = 5 * 60 * 1000; + + public Integer getExpirationDelay() { + return ExpirationDelay; + } + + /** + * Время окончания торговой сессии + */ +// @JsonProperty(value = "SessionEndTime") +// @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm:ss", timezone = "Europe/Moscow") + private Date SessionEndTime = null; + + public Date getSessionEndTime() { + return SessionEndTime; + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElement.java b/storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElement.java new file mode 100644 index 000000000..d2dbe8077 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElement.java @@ -0,0 +1,29 @@ +package ru.spcex.clearing.storage.config; + +//import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +@SuppressWarnings({"DefaultAnnotationParam", "unused", "FieldCanBeLocal"}) +public class SettingsElement implements Serializable { +// @JsonProperty(value = "InitHazelcastThreadMultiplier", required = false) + private int initHazelcastThreadMultiplier = 2; + +// @JsonProperty(value = "RecoveryLogPath", required = false) + private String recoveryLogPath; + +// @JsonProperty(value = "ExecutionLimitationPeriod", required = false) + private Integer executionLimitationPeriod = 10; + + public int getInitHazelcastThreadMultiplier() { + return initHazelcastThreadMultiplier; + } + + public Integer getExecutionLimitationPeriod() { + return executionLimitationPeriod; + } + + public String getRecoveryLogPath() { + return recoveryLogPath; + } +} \ No newline at end of file diff --git a/storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElementDatabase.java b/storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElementDatabase.java new file mode 100644 index 000000000..5f9a5855f --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/config/SettingsElementDatabase.java @@ -0,0 +1,89 @@ +package ru.spcex.clearing.storage.config; + +//import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +@SuppressWarnings({"FieldCanBeLocal", "unused", "DefaultAnnotationParam"}) +public class SettingsElementDatabase implements Serializable { + +// @JsonProperty(value = "Driver") + private String driver = "org.firebirdsql.jdbc.FBDriver"; + +// @JsonProperty(value = "JdbcConnectionString", required = true) + private String jdbcConnectionString; + +// @JsonProperty(value = "Login", required = true) + private String login; + +// @JsonProperty(value = "Password", required = true) + private String password; + +// @JsonProperty(value = "MaxPoolSize") + private int maxPoolSize = 30; + +// @JsonProperty(value = "MinPoolSize") + private int minPoolSize = 10; + +// @JsonProperty(value = "EmbeddedFilePath", required = false) + private String embeddedFilePath; + +// @JsonProperty(value = "NumHelperThreads", required = false) + private int numHelperThreads = Runtime.getRuntime().availableProcessors() * 2; + +// @JsonProperty(value = "ConnectionAcquireTimeoutSeconds", required = false) + private int connectionAcquireTimeoutSeconds = 30; + + public String getDriver() { + return driver; + } + + public String getJdbcConnectionString() { + return jdbcConnectionString; + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } + + public int getMaxPoolSize() { + return maxPoolSize; + } + + public int getMinPoolSize() { + return minPoolSize; + } + + public String getEmbeddedFilePath() { + return embeddedFilePath; + } + + public int getNumHelperThreads() { + return numHelperThreads; + } + + public int getConnectionAcquireTimeoutSeconds() { + return connectionAcquireTimeoutSeconds; + } + + // для поддержки интеграционных тестов otc-test: + public void setJdbcConnectionString(String jdbcConnectionString) { + this.jdbcConnectionString = jdbcConnectionString; + } + + public void setLogin(String login) { + this.login = login; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setEmbeddedFilePath(String embeddedFilePath) { + this.embeddedFilePath = embeddedFilePath; + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/dictionary/WorkflowStatusDictionaryMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/dictionary/WorkflowStatusDictionaryMapStore.java new file mode 100644 index 000000000..d807e1842 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/dictionary/WorkflowStatusDictionaryMapStore.java @@ -0,0 +1,24 @@ +package ru.spcex.clearing.storage.dictionary; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import ru.clearing.platform.dictionary.WorkflowStatusDictionary; +import ru.spcex.clearing.storage.base.DictionaryMapStore; + +@Component +public class WorkflowStatusDictionaryMapStore extends DictionaryMapStore { + + public WorkflowStatusDictionaryMapStore(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + public String getTableName() { + return "WORKFLOW_STATUS_DICTIONARY"; + } + + @Override + public WorkflowStatusDictionary getDictionaryObject() { + return new WorkflowStatusDictionary(); + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/error/ModuleInitializeException.java b/storage/src/main/java/ru/spcex/clearing/storage/error/ModuleInitializeException.java new file mode 100644 index 000000000..0e40d7936 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/error/ModuleInitializeException.java @@ -0,0 +1,19 @@ +package ru.spcex.clearing.storage.error; + +public class ModuleInitializeException extends RuntimeException { + public ModuleInitializeException() { + } + + public ModuleInitializeException(String message) { + super(message); + } + + public ModuleInitializeException(String message, Throwable cause) { + super(message, cause); + } + + public ModuleInitializeException(Throwable cause) { + super(cause); + } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/object/CompanyRoleSetMapStore.java b/storage/src/main/java/ru/spcex/clearing/storage/object/CompanyRoleSetMapStore.java new file mode 100644 index 000000000..43e397681 --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/object/CompanyRoleSetMapStore.java @@ -0,0 +1,61 @@ +package ru.spcex.clearing.storage.object; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import ru.clearing.classes.StaticData.Company.Company; +import ru.clearing.classes.StaticData.Company.CompanyRoleSet; +import ru.spcex.clearing.storage.base.ObjectBaseMapStore; +import ru.spcex.clearing.storage.utils.DbUtilsHelper; + +import java.util.*; + +@Component +public class CompanyRoleSetMapStore extends ObjectBaseMapStore { + + public CompanyRoleSetMapStore(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + @Override + public String getTableName() { + return "COMPANY_ROLE_SET"; + } + + @Override + public String[] getFields() { + return new String[]{"id", "roleid", "companyid"}; + } + + protected final String insertStatement = DbUtilsHelper.createUpdateOrInsert(getTableName(), getFields(), "id"); + + @Override + public Collection load(Collection keys) { + Map> paramMap = Collections.singletonMap("ids", keys); + return namedParameterJdbcTemplate.query("select * from " + getTableName() + " where id in (:ids)", paramMap, + (resultSet, i) -> { + CompanyRoleSet companyRoleSet = new CompanyRoleSet(); + companyRoleSet.setId(resultSet.getObject("id", Long.class)); + companyRoleSet.setRoleId(resultSet.getObject("roleid", Long.class)); + companyRoleSet.setCompanyId(resultSet.getObject("companyid", Long.class)); + return companyRoleSet; + }); + } + + @Override + public void store(Map map) { + List batchArgs = new ArrayList<>(); + + for (Map.Entry entry : map.entrySet()) { + CompanyRoleSet partnerList = entry.getValue(); + + Object[] args = new Object[]{ + partnerList.getId(), + partnerList.getRoleId(), + partnerList.getCompanyId(), + }; + batchArgs.add(args); + } + batchInsertUpdate(insertStatement, batchArgs); + } + +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/utils/BigDecimalUtil.java b/storage/src/main/java/ru/spcex/clearing/storage/utils/BigDecimalUtil.java new file mode 100644 index 000000000..4907c447d --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/utils/BigDecimalUtil.java @@ -0,0 +1,111 @@ +package ru.spcex.clearing.storage.utils; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; + +public class BigDecimalUtil { + + private static final DecimalFormat defaultDecimalFormat; + + static { + DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(); + decimalFormatSymbols.setDecimalSeparator('.'); + decimalFormatSymbols.setGroupingSeparator(' '); + defaultDecimalFormat = new DecimalFormat("#,###.00", decimalFormatSymbols); + } + + public static BigDecimal sum(BigDecimal num1, BigDecimal num2) { + BigDecimal sum = BigDecimal.ZERO; + if (num1 != null) + sum = sum.add(num1); + if (num2 != null) + sum = sum.add(num2); + return sum; + } + + /** + * @param value + * @param dbSize общее кол-во позиций + * @param dbScale кол-во позиций под дробную часть + * @return true, если размер целой части не превышает значения, указанные в параметрах + */ + public static boolean checkSize(BigDecimal value, int dbSize, int dbScale) { + if (value == null) + return true; + return value.precision() - value.scale() <= dbSize - dbScale; + } + + public static BigDecimal genMaxValueForInsert(int dbSize, int dbScale) { + int maxIntSizeForInsert = dbSize - dbScale; + long newLongValue = new BigDecimal(Math.pow(10, maxIntSizeForInsert)).longValue(); + return new BigDecimal(newLongValue - 1); + } + + public static BigDecimal chooseHighest(BigDecimal oldPrice, BigDecimal newPrice) { + if (newPrice != null && (oldPrice == null || isLess(oldPrice, newPrice))) { + return newPrice; + } else { + return oldPrice; + } + } + + public static BigDecimal chooseSmallest(BigDecimal oldPrice, BigDecimal newPrice) { + if (newPrice != null && (oldPrice == null || isLess(newPrice, oldPrice))) { + return newPrice; + } else { + return oldPrice; + } + } + + /** + * Если bestPrice меньше price, то true, иначе false + * + * @param bestPrice + * @param price + * @return + */ + public static boolean isLess(BigDecimal bestPrice, BigDecimal price) { + return price != null && bestPrice != null && bestPrice.compareTo(price) < 0; + } + + public static Integer nullSafeAdd(Integer a, Integer b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } else { + return a + b; + } + } + + public static BigDecimal nullSafeAdd(BigDecimal a, BigDecimal b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } else { + return a.add(b); + } + } + + public static Long nullSafeAdd(Long a, Long b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } else { + return a + b; + } + } + + public static String defaultFormatValue(BigDecimal val) { + DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(); + decimalFormatSymbols.setDecimalSeparator('.'); + decimalFormatSymbols.setGroupingSeparator(' '); + if (val == null) { + return ""; + } + return defaultDecimalFormat.format(val); + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/utils/DbDefaultConfig.java b/storage/src/main/java/ru/spcex/clearing/storage/utils/DbDefaultConfig.java new file mode 100644 index 000000000..644f937ee --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/utils/DbDefaultConfig.java @@ -0,0 +1,11 @@ +package ru.spcex.clearing.storage.utils; + + +import javax.sql.DataSource; +import java.nio.file.Paths; + +public class DbDefaultConfig { + public static DataSource getEmbeddedDatabase(String embeddedFilePath) { + throw new UnsupportedOperationException("Are PostgreSQL not supported embedded DB?");//todo PsotgreSQL + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/utils/DbUtilsHelper.java b/storage/src/main/java/ru/spcex/clearing/storage/utils/DbUtilsHelper.java new file mode 100644 index 000000000..b535bc64f --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/utils/DbUtilsHelper.java @@ -0,0 +1,51 @@ +package ru.spcex.clearing.storage.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class DbUtilsHelper { + + private static String generateValuesBlock(int querySignCount) { + return StringUtils.rightPad("(?", querySignCount * 3 - 1, ", ?") + ")"; + } + + private static Pattern pGetColumns = Pattern.compile("(?m)(?i)INTO\\s+[0-9a-z\" _$]+\\s+\\(\\s*([0-9a-z_\\- ,\"\\n+]*?)\\s*\\)"); + + public static String fillValuesBlock(String insertStatement) { + + Matcher m = pGetColumns.matcher(insertStatement); + if (m.find()) { + String g = m.group(1); + String block = generateValuesBlock(g.split(",").length); + return insertStatement.replaceAll("%values_block%", block); + } else { + return insertStatement; + } + } + + public static String createInsert(String tableName, String[] fields) { + return "INSERT INTO " + tableName + + " (" + Arrays.stream(fields).map(f -> "\"" + f.toUpperCase() + "\"").collect(Collectors.joining(", ")) + ") values (" + + // Arrays.stream(fields).map(f -> "\"" + f.toUpperCase() + "\"").collect(Collectors.joining(", ", ":", "")); + String.join(", ", Collections.nCopies(fields.length, "?")) + + ")"; + } + + public static String createUpdateOrInsert(String tableName, String[] fields, String matchingKey) { + String updateOrInsert = "INSERT INTO " + tableName + + " (" + Arrays.stream(fields).map(f -> "\"" + f.toUpperCase() + "\"").collect(Collectors.joining(", ")) + ") values (" + + // Arrays.stream(fields).map(f -> "\"" + f.toUpperCase() + "\"").collect(Collectors.joining(", ", ":", "")); + String.join(", ", Collections.nCopies(fields.length, "?")) + + ") WHERE "; + if (matchingKey != null) // PostgreSQL conflict_target fixme проверить мэтчинг + updateOrInsert += " " + matchingKey.toUpperCase(); + updateOrInsert += " CONFLICT DO UPDATE"; + + return updateOrInsert; + } +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/utils/IMDGDistributedNames.java b/storage/src/main/java/ru/spcex/clearing/storage/utils/IMDGDistributedNames.java new file mode 100644 index 000000000..0add1d9db --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/utils/IMDGDistributedNames.java @@ -0,0 +1,10 @@ +package ru.spcex.clearing.storage.utils; + +//fixme перенести в новый модуль IMDGDistributedNames +public final class IMDGDistributedNames { + private IMDGDistributedNames() { + } + + public static final String Map_WorkflowStatusDictionary = "Map_WorkflowStatusDictionary"; + public static final String Map_CompanyRoleSet = "Map_CompanyRoleSet"; +} diff --git a/storage/src/main/java/ru/spcex/clearing/storage/utils/TimeUtil.java b/storage/src/main/java/ru/spcex/clearing/storage/utils/TimeUtil.java new file mode 100644 index 000000000..3059f26ec --- /dev/null +++ b/storage/src/main/java/ru/spcex/clearing/storage/utils/TimeUtil.java @@ -0,0 +1,288 @@ +package ru.spcex.clearing.storage.utils; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; + +/** + * Для работы с java.util.Date + * И конвертация в новый формат + * todo копипаст юниттеста нужен. + */ +public class TimeUtil { + private static final Logger log = LoggerFactory.getLogger(TimeUtil.class); + public static final ZoneId zone = ZoneId.systemDefault(); + + /** + * Получить дату без времени (начало дня) + * + * @param date + * @return + */ + public static Date getDateOnStartOfDay(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * Получить начало следующего дня + * + * @param date + * @return + */ + public static Date getDateNextDay(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + calendar.add(Calendar.DAY_OF_YEAR, 1); + return calendar.getTime(); + } + + public static LocalDateTime toDateTime(Date fromDate) { + if (fromDate == null) + return null; + if (fromDate instanceof java.sql.Timestamp) + return ((java.sql.Timestamp) fromDate).toLocalDateTime(); + if (fromDate instanceof java.sql.Date || fromDate instanceof java.sql.Time) + log.debug("Warning at converting date/time: for class {} may not contain date or time. unixtime={}", fromDate.getClass(), fromDate.getTime()); + return toDateTime0(fromDate); + } + + private static LocalDateTime toDateTime0(Date fromDate) { + try { + if (fromDate instanceof java.sql.Date) { + Instant instant = Instant.now(); //can be LocalDateTime + ZoneId systemZone = ZoneId.systemDefault(); // my timezone + ZoneOffset currentOffsetForMyZone = systemZone.getRules().getOffset(instant); + return LocalDateTime.ofEpochSecond(fromDate.getTime() / 1000, 0, currentOffsetForMyZone); + } + return LocalDateTime.ofInstant(fromDate.toInstant(), ZoneId.systemDefault()); + } catch (UnsupportedOperationException ue) { + log.warn("Error(warn) convert type of date/time: {}({}) class: {}. Retry", fromDate, fromDate.getTime(), fromDate.getClass()); + try { + Instant instant = Instant.now(); //can be LocalDateTime + ZoneId systemZone = ZoneId.systemDefault(); // my timezone + ZoneOffset currentOffsetForMyZone = systemZone.getRules().getOffset(instant); + return LocalDateTime.ofEpochSecond(fromDate.getTime() / 1000, 0, currentOffsetForMyZone); + } catch (Exception e) { + log.error("Error convert type2 of date/time: {} class: {}\n{}", fromDate, fromDate.getClass(), ExceptionUtils.getStackTrace(ue)); + return null; + } + } +// Date out = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + } + + public static LocalDate toDate(Date fromDate) { + if (fromDate == null) + return null; + if (fromDate instanceof java.sql.Date) + return ((java.sql.Date) fromDate).toLocalDate(); + if (fromDate instanceof java.sql.Time) + log.debug("Warning at converting date/time: for class {} may not contain date. unixtime={}", fromDate.getClass(), fromDate.getTime()); + return toDateTime0(fromDate).toLocalDate(); + } + + public static LocalTime toTime(Date fromDate) { + if (fromDate == null) + return null; + if (fromDate instanceof java.sql.Time) + return ((java.sql.Time) fromDate).toLocalTime(); + if (fromDate instanceof java.sql.Date) + log.debug("Warning at converting date/time: for class {} may not contain time. unixtime={}", fromDate.getClass(), fromDate.getTime()); + return toDateTime0(fromDate).toLocalTime(); + } + + /** + * Последовательный ряд дат дней между двумя датами. + * + * @param from с даты, включительно + * @param to по дату, включительно. + * @return + */ + public static Date[] makeDateRange(Date from, Date to) { + ArrayList datesL = new ArrayList(); + LocalDate fromDate = toDate(from); + LocalDate toDate = toDate(to); + datesL.add(from); + for (LocalDate dateI = fromDate.plusDays(1); dateI.isBefore(toDate); dateI = dateI.plusDays(1)) { + Date date = Date.from(dateI.atStartOfDay(ZoneId.systemDefault()).toInstant()); + datesL.add(date); + } + if (!fromDate.equals(toDate)) + datesL.add(to); + Date[] dates = datesL.toArray(new Date[0]); + return dates; + } + + public static Date fromInstant(Instant instant) { + if (instant == null) return null; + return Date.from(instant); + } + + public static Date from(LocalDateTime localDateTime) { + return localDateTime != null ? Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()) : null; + } + + public static Date from(LocalDate localDate) { + return localDate != null ? Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) : null; + } + + /** + * иногда встречается java.sql.Date + * у них toInstant() не поддерживается + */ + public static Instant fromDate(Date dateToConvert) { + try { + if (dateToConvert == null) return null; + return dateToConvert.toInstant(); + } catch (UnsupportedOperationException e) { +// log.warn("java.sql.Date"); + if (dateToConvert instanceof java.sql.Date) { + return ((java.sql.Date) dateToConvert).toLocalDate().atStartOfDay(ZoneId.systemDefault()).toInstant(); + } else { + throw e; + } + } + } + + public static Instant fromDate(LocalDate dateToConvert) { + return dateToConvert == null ? null : dateToConvert.atStartOfDay(ZoneId.systemDefault()).toInstant(); + } + + public static LocalDateTime toDateTime(Instant instant) { + if (instant == null) { + return null; + } else { + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + } + + public static Date toUtilDate(java.sql.Date sqlDate) { + return sqlDate != null ? new Date(sqlDate.getTime()) : null; + } + + + public static boolean isCurrentTradingDay(Date date) { + Date tradingDay = getDateOnStartOfDay(date); + Date currentDay = getDateOnStartOfDay(new Date()); + return currentDay.equals(tradingDay); + } + + /** + * Начало дня по текущей временной зоне + */ + public static Instant startOfDayNow() { + ZonedDateTime zdtStart = ZonedDateTime.now(); + zdtStart = zdtStart.truncatedTo(ChronoUnit.DAYS); + return zdtStart.toInstant(); + } + + /** + * Начало дня по текущей временной зоне. + * + * @param from + * @return + */ + public static Instant startOfDay(Instant from) { + ZonedDateTime zdtStart = from.atZone(ZoneId.systemDefault()); + zdtStart = zdtStart.truncatedTo(ChronoUnit.DAYS); + return zdtStart.toInstant(); + } + + public static ZonedDateTime toZoned(Instant instant) { + if (instant == null) return null; + return instant.atZone(zone); + } + + public static LocalDate toDate(Instant instant) { + ZonedDateTime zoned = toZoned(instant); + if (zoned == null) return null; + return zoned.toLocalDate(); + } + + /** + * Сегодня (от начала дня) или позднее + * + * @param instant дата, может ыть null + * @return + */ + public static boolean afterToday(Instant instant) { + LocalDate localDate = toDate(instant); + return afterToday(localDate); + } + + /** + * Сегодня (от начала дня) или позднее + * + * @param date дата, может ыть null + * @return + */ + public static boolean afterToday(Date date) { + LocalDate localDate = toDate(date); + return afterToday(localDate); + } + + /** + * Сегодня (от начала дня) или позднее + * + * @param localDate дата, может ыть null + * @return + */ + public static boolean afterToday(LocalDate localDate) { + if (localDate == null) return false; + return !localDate.isBefore(LocalDate.now()); + } + + public static boolean isToday(Instant instant) { + LocalDate localDate = toDate(instant); + return isToday(localDate); + } + + public static boolean isToday(Date date) { + LocalDate localDate = toDate(date); + return isToday(localDate); + } + + public static boolean isToday(LocalDate localDate) { + if (localDate == null) return false; + return localDate.equals(LocalDate.now()); + } + + /** + * Если instant без даты (1970 год), то берёт из него время и добавляет текущую дату. + * + * @param time может быть null + * @return time, или если не указана дата, то текущая дата + время из time + */ + public static Instant addDateTodayToTimeIfNeeded(Instant time) { + if (time == null) + return null; + LocalDateTime ldt = TimeUtil.toDateTime(time); + if (ldt.getYear() == 1970 && ldt.getMonth() == Month.JANUARY && ldt.getDayOfMonth() == 1) { + // time without date + ldt = LocalDateTime.of(LocalDate.now(), ldt.toLocalTime()); + return ldt.atZone(ZoneId.systemDefault()).toInstant(); + } + return time; + } + + public static String formatInstantToString(DateTimeFormatter formatter, Instant date) { + if (formatter == null || date == null) return ""; + return formatter.format(date); + } +}