diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..9b0c1078 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,47 @@ +name: Unit Tests + +on: + workflow_dispatch: + pull_request: + branches: + - 'master' + - 'develop' + push: + branches: + - 'master' + - 'develop' + +jobs: + tests: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '22' + distribution: 'liberica' + cache: maven + + - name: Maven Tests + run: mvn --batch-mode clean test + + - name: Test Coverage + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: SonarCloud Analyze + run: > + mvn --batch-mode sonar:sonar + -Dsonar.projectKey=spacious-team_investbook + -Dsonar.organization=spacious-team + -Dsonar.host.url=https://sonarcloud.io + -Dsonar.login=$SONAR_TOKEN + -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/README.md b/README.md index 48c62cc5..32e1d2bd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![spring-boot-version](https://img.shields.io/badge/spring--boot-3.3.4-brightgreen?style=flat-square)](/~https://github.com/spring-projects/spring-boot/releases) [![hits-of-code](https://img.shields.io/badge/dynamic/json?style=flat-square&color=lightblue&label=hits-of-code&url=https://hitsofcode.com/github/spacious-team/investbook/json?branch=develop&query=$.count)](https://hitsofcode.com/github/spacious-team/investbook/view?branch=develop) [![github-closed-pull-requests](https://img.shields.io/github/issues-pr-closed/spacious-team/investbook?style=flat-square&color=brightgreen)](/~https://github.com/spacious-team/investbook/pulls?q=is%3Apr+is%3Aclosed) +[![Unit tests](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fspacious-team%2Finvestbook%2Fbadge%3Fref%3Ddevelop&style=flat-square&label=test&logo=none)]( +/~https://github.com/spacious-team/investbook/actions/workflows/unit-tests.yml) [![github-workflow-status](https://img.shields.io/github/actions/workflow/status/spacious-team/investbook/publish-docker.yml?style=flat-square&branch=master)](/~https://github.com/spacious-team/investbook/actions/workflows/publish-docker.yml) [![github-all-releases](https://img.shields.io/github/downloads/spacious-team/investbook/total?style=flat-square&logo=github&color=lightblue)](/~https://github.com/spacious-team/investbook/releases/latest) [![docker-pulls](https://img.shields.io/docker/pulls/spaciousteam/investbook?style=flat-square&logo=docker&color=lightblue&logoColor=white)](https://hub.docker.com/r/spaciousteam/investbook) diff --git a/checkerframework.astub b/checkerframework.astub new file mode 100644 index 00000000..c0720267 --- /dev/null +++ b/checkerframework.astub @@ -0,0 +1,76 @@ +/* + * InvestBook + * Copyright (C) 2024 Spacious Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; + +package java.util; +public class Objects { + + @EnsuresNonNull("#1") + static T requireNonNull(@Nullable T obj); + + @EnsuresNonNull("#1") + static T requireNonNull(@Nullable T obj, String message); + + @EnsuresNonNull("#1") + static T requireNonNull(@Nullable T obj, Supplier messageSupplier); + + @EnsuresNonNullIf(expression="#1", result=true) + static boolean nonNull(@Nullable Object obj); + + @EnsuresNonNullIf(expression="#1", result=false) + static boolean isNull(@Nullable Object obj); +} + + +package org.springframework.util; +class StringUtils { + + @EnsuresNonNullIf(expression="#1", result=true) + static boolean hasText(@Nullable CharSequence str); + + @EnsuresNonNullIf(expression="#1", result=true) + static boolean hasText(@Nullable String str); + + @EnsuresNonNullIf(expression="#1", result=true) + static boolean hasLength(@Nullable CharSequence str); + + @EnsuresNonNullIf(expression="#1", result=true) + static boolean hasLength(@Nullable String str); +} + + +package org.springframework.util; +class CollectionUtils { + + @EnsuresNonNullIf(expression="#1", result=false) + static boolean isEmpty(@Nullable Collection collection); + + @EnsuresNonNullIf(expression="#1", result=false) + static boolean isEmpty(@Nullable Map map); +} + + +package org.slf4j; +interface Logger { + + void warn(String format, @Nullable Object arg1, @Nullable Object arg2); + + void warn(String format, @Nullable Object... arguments); +} diff --git a/pom.xml b/pom.xml index bc5b42a7..44081493 100644 --- a/pom.xml +++ b/pom.xml @@ -62,8 +62,10 @@ - 24.2 + 24.2 22 + 1.18.34 + 3.48.1 @@ -86,9 +88,15 @@ org.glassfish.jaxb jaxb-runtime - + compile + + + com.github.spacious-team + table-wrapper-api + f2341ce264 + @@ -96,7 +104,7 @@ com.github.spacious-team broker-report-parser-api - 2024.1 + 109c37e4a9 com.github.spacious-team @@ -164,10 +172,6 @@ spring-boot-devtools true - - org.hibernate.orm - hibernate-jpamodelgen - com.h2database h2 @@ -209,6 +213,7 @@ org.projectlombok lombok + ${lombok.version} provided true @@ -222,10 +227,95 @@ poi-scratchpad 5.2.5 + + org.checkerframework + checker-qual + ${checkerframework.version} + provided + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-junit-jupiter + test + + + nl.jqno.equalsverifier + equalsverifier + 3.17.1 + test + + + + + maven-surefire-plugin + 3.5.1 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + true + true + + + org.projectlombok + lombok + ${lombok.version} + + + org.hibernate + hibernate-jpamodelgen + ${hibernate.version} + + + org.checkerframework + checker + ${checkerframework.version} + + + + + lombok.launch.AnnotationProcessorHider$AnnotationProcessor + + + org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor + + + org.checkerframework.checker.nullness.NullnessChecker + + + + + + + -AskipDefs=(Test$|V2022_1_0_1|Entity(Pk)?_$|generated.ValCurs) + + -Astubs=permit-nullness-assertion-exception.astub${path.separator}${project.basedir}/checkerframework.astub + -AsuppressWarnings=methodref,type.arguments.not.inferred,lambda.param,expression.unparsable + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + + + org.springframework.boot spring-boot-maven-plugin @@ -287,6 +377,26 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + diff --git a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java index fc5580f5..22e91599 100644 --- a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java +++ b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java @@ -22,6 +22,7 @@ import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Session; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -93,7 +94,7 @@ public boolean insert(Pojo object) { log.error("Can't INSERT by optimized deprecated Hibernate method save(): {}", object, e); } } - Boolean result = transactionTemplateRequired.execute(_ -> createIfAbsent(object)); + @Nullable Boolean result = transactionTemplateRequired.execute(_ -> createIfAbsent(object)); return Boolean.TRUE.equals(result); } @@ -123,14 +124,14 @@ public CreateResult createIfAbsentAndGet(Pojo object) { * @implSpec Should be called in transaction */ private Optional createIfAbsentInternal(Pojo object) { - Entity entity = null; + @Nullable Entity entity = null; if (repository instanceof ConstraintAwareRepository caRepository) { entity = converter.toEntity(object); if (caRepository.exists(entity)) { return Optional.empty(); } } else { - ID id = getId(object); + @Nullable ID id = getId(object); if (id != null && existsById(id)) { return Optional.empty(); } @@ -153,13 +154,13 @@ private Optional createIfAbsentInternal(Pojo object) { * @implSpec Should be called in transaction */ private CreateResult createIfAbsentAndGetInternal(Pojo object) { - Entity entity = null; + @Nullable Entity entity = null; Optional selectedEntity; if (repository instanceof ConstraintAwareRepository caRepository) { entity = converter.toEntity(object); selectedEntity = caRepository.findBy(entity); } else { - ID id = getId(object); + @Nullable ID id = getId(object); selectedEntity = Optional.ofNullable(id) .flatMap(repository::findById); } diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java index 03d99d12..4a52aad0 100644 --- a/src/main/java/ru/investbook/api/AbstractRestController.java +++ b/src/main/java/ru/investbook/api/AbstractRestController.java @@ -20,6 +20,7 @@ import jakarta.persistence.GeneratedValue; import lombok.SneakyThrows; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -32,9 +33,12 @@ import java.net.URI; import java.util.Objects; +import java.util.Optional; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.isNull; import static java.util.Objects.nonNull; +import static org.springframework.http.HttpStatus.CREATED; public abstract class AbstractRestController extends AbstractEntityRepositoryService { @@ -73,7 +77,7 @@ protected ResponseEntity post(Pojo object) { CreateResult result = createIfAbsentAndGet(object); Pojo savedObject = result.object(); if (result.created()) { - return createResponseWithLocationHeader(savedObject); + return createResponseWithOptionalLocationHeader(savedObject); } else { return createConflictResponse(savedObject); } @@ -85,10 +89,8 @@ protected ResponseEntity post(Pojo object) { @NonNull private ResponseEntity createConflictResponse(Pojo object) { ResponseEntity.BodyBuilder response = ResponseEntity.status(HttpStatus.CONFLICT); - if (getId(object) != null) { - URI locationURI = getLocationURI(object); - response.location(locationURI); - } + getLocationURI(object) + .ifPresent(response::location); return response.build(); } @@ -106,14 +108,14 @@ private ResponseEntity createConflictResponse(Pojo object) { @Transactional public ResponseEntity put(ID id, Pojo object) { try { - ID objectId = getId(object); + @Nullable ID objectId = getId(object); if (nonNull(objectId) && !Objects.equals(id, objectId)) { throw new BadRequestException("Идентификатор объекта, переданный в URI [" + id + "] и в теле " + "запроса [" + objectId + "] не совпадают"); } Pojo objectWithId = nonNull(objectId) ? object : updateId(id, object); return createAndGetIfAbsent(objectWithId) - .map(this::createResponseWithLocationHeader) + .map(this::createResponseWithOptionalLocationHeader) .orElseGet(() -> { createOrUpdate(objectWithId); return ResponseEntity.noContent().build(); @@ -134,16 +136,21 @@ public ResponseEntity delete(ID id) { /** * @return response entity with http CREATE status, Location http header and body */ - private ResponseEntity createResponseWithLocationHeader(Pojo object) { - URI locationURI = getLocationURI(object); - return ResponseEntity - .created(locationURI) + private ResponseEntity createResponseWithOptionalLocationHeader(Pojo object) { + return getLocationURI(object) + .map(ResponseEntity::created) + .orElseGet(() -> ResponseEntity.status(CREATED)) .build(); } @SneakyThrows - protected URI getLocationURI(Pojo object) { - return new URI(UriUtils.encodePath(getLocation() + "/" + getId(object), UTF_8)); + protected Optional getLocationURI(Pojo object) { + @Nullable ID id = getId(object); + if (isNull(id)) { + return Optional.empty(); + } + URI uri = new URI(UriUtils.encodePath(getLocation() + "/" + id, UTF_8)); + return Optional.of(uri); } protected abstract String getLocation(); diff --git a/src/main/java/ru/investbook/api/EntityRepositoryService.java b/src/main/java/ru/investbook/api/EntityRepositoryService.java index 1a802d5c..7b42b5ea 100644 --- a/src/main/java/ru/investbook/api/EntityRepositoryService.java +++ b/src/main/java/ru/investbook/api/EntityRepositoryService.java @@ -18,6 +18,7 @@ package ru.investbook.api; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -25,6 +26,7 @@ public interface EntityRepositoryService { + @Nullable ID getId(Pojo object); boolean existsById(ID id); diff --git a/src/main/java/ru/investbook/api/EventCashFlowRestController.java b/src/main/java/ru/investbook/api/EventCashFlowRestController.java index f8477e66..3de35821 100644 --- a/src/main/java/ru/investbook/api/EventCashFlowRestController.java +++ b/src/main/java/ru/investbook/api/EventCashFlowRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.EventCashFlow; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -84,7 +85,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody EventCashFlow event) { + public ResponseEntity post(@RequestBody @Valid EventCashFlow event) { return super.post(event); } @@ -97,8 +98,8 @@ public ResponseEntity post(@Valid @RequestBody EventCashFlow event) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Номер события") Integer id, - @Valid @RequestBody + @Valid EventCashFlow event) { return super.put(id, event); } @@ -115,7 +116,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(EventCashFlow object) { + public @Nullable Integer getId(EventCashFlow object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java index 4b5d371c..605f9e5a 100644 --- a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java +++ b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java @@ -49,6 +49,7 @@ import java.net.URI; import java.time.LocalDate; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static org.springframework.http.HttpHeaders.LOCATION; @@ -120,7 +121,7 @@ protected ResponseEntity get(@PathVariable("currency-pair") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody ForeignExchangeRate object) { + public ResponseEntity post(@RequestBody @Valid ForeignExchangeRate object) { foreignExchangeRateService.invalidateCache(); return super.post(object); } @@ -141,8 +142,8 @@ public ResponseEntity put(@PathVariable("currency-pair") @Parameter(description = "Дата", example = "2021-01-23") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date, - @Valid @RequestBody + @Valid ForeignExchangeRate object) { foreignExchangeRateService.invalidateCache(); return super.put(getId(currencyPair, date), object); @@ -189,8 +190,9 @@ protected ForeignExchangeRate updateId(ForeignExchangeRateEntityPk id, ForeignEx @Override @SneakyThrows - protected URI getLocationURI(ForeignExchangeRate object) { - return new URI(getLocation() + "/currency-pairs/" + object.getCurrencyPair() + "/dates/" + object.getDate()); + protected Optional getLocationURI(ForeignExchangeRate object) { + URI uri = new URI(getLocation() + "/currency-pairs/" + object.getCurrencyPair() + "/dates/" + object.getDate()); + return Optional.of(uri); } @Override diff --git a/src/main/java/ru/investbook/api/IssuerRestController.java b/src/main/java/ru/investbook/api/IssuerRestController.java index 2b4fc112..f6121f76 100644 --- a/src/main/java/ru/investbook/api/IssuerRestController.java +++ b/src/main/java/ru/investbook/api/IssuerRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Issuer; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -83,7 +84,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody Issuer issuer) { + public ResponseEntity post(@RequestBody @Valid Issuer issuer) { return super.post(issuer); } @@ -96,8 +97,8 @@ public ResponseEntity post(@Valid @RequestBody Issuer issuer) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Внутренний идентификатор эмитента") Integer id, - @Valid @RequestBody + @Valid Issuer issuer) { return super.put(id, issuer); } @@ -114,7 +115,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(Issuer object) { + public @Nullable Integer getId(Issuer object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/PortfolioCashRestController.java b/src/main/java/ru/investbook/api/PortfolioCashRestController.java index 96d2996b..9862465e 100644 --- a/src/main/java/ru/investbook/api/PortfolioCashRestController.java +++ b/src/main/java/ru/investbook/api/PortfolioCashRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioCash; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -82,7 +83,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody PortfolioCash property) { + public ResponseEntity post(@RequestBody @Valid PortfolioCash property) { return super.post(property); } @@ -95,8 +96,8 @@ public ResponseEntity post(@Valid @RequestBody PortfolioCash property) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Внутренний идентификатор записи") Integer id, - @Valid @RequestBody + @Valid PortfolioCash property) { return super.put(id, property); } @@ -113,7 +114,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(PortfolioCash object) { + public @Nullable Integer getId(PortfolioCash object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java index 11fd8051..595c2aea 100644 --- a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java +++ b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioProperty; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -85,7 +86,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody PortfolioProperty property) { + public ResponseEntity post(@RequestBody @Valid PortfolioProperty property) { return super.post(property); } @@ -99,8 +100,8 @@ public ResponseEntity post(@Valid @RequestBody PortfolioProperty property) public ResponseEntity put(@PathVariable("id") @Parameter(description = "Внутренний идентификатор записи") Integer id, - @Valid @RequestBody + @Valid PortfolioProperty property) { return super.put(id, property); } @@ -118,7 +119,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(PortfolioProperty object) { + public @Nullable Integer getId(PortfolioProperty object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/PortfolioRestController.java b/src/main/java/ru/investbook/api/PortfolioRestController.java index 429dd659..d86da423 100644 --- a/src/main/java/ru/investbook/api/PortfolioRestController.java +++ b/src/main/java/ru/investbook/api/PortfolioRestController.java @@ -48,11 +48,9 @@ @Tag(name = "Счета") @RequestMapping("/api/v1/portfolios") public class PortfolioRestController extends AbstractRestController { - private final PortfolioRepository repository; public PortfolioRestController(PortfolioRepository repository, PortfolioConverter converter) { super(repository, converter); - this.repository = repository; } @Override @@ -83,7 +81,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody Portfolio object) { + public ResponseEntity post(@RequestBody @Valid Portfolio object) { return super.post(object); } @@ -96,8 +94,8 @@ public ResponseEntity post(@Valid @RequestBody Portfolio object) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Номер счета") String id, - @Valid @RequestBody + @Valid Portfolio object) { return super.put(id, object); } diff --git a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java index e52563c1..546b1faf 100644 --- a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java +++ b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java @@ -48,11 +48,9 @@ @Tag(name = "Информация по инструментам", description = "Сектор экономики, эмитент") @RequestMapping("/api/v1/security-descriptions") public class SecurityDescriptionRestController extends AbstractRestController { - private final SecurityDescriptionRepository repository; public SecurityDescriptionRestController(SecurityDescriptionRepository repository, SecurityDescriptionConverter converter) { super(repository, converter); - this.repository = repository; } @Override @@ -89,7 +87,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody SecurityDescription security) { + public ResponseEntity post(@RequestBody @Valid SecurityDescription security) { return super.post(security); } @@ -103,8 +101,8 @@ public ResponseEntity post(@Valid @RequestBody SecurityDescription securit public ResponseEntity put(@PathVariable("id") @Parameter(description = "Идентификатор", example = "123", required = true) Integer id, - @Valid @RequestBody + @Valid SecurityDescription security) { return super.put(id, security); } diff --git a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java index 764d784d..ece6c652 100644 --- a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java +++ b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -47,9 +48,7 @@ import static org.springframework.http.HttpHeaders.LOCATION; @RestController -@Tag(name = "События по бумаге", description = """ - Дивиденды, купоны, амортизации, вариационная маржа, комиссии, налоги - """) +@Tag(name = "События по бумаге", description = "Дивиденды, купоны, амортизации, вариационная маржа, комиссии, налоги") @RequestMapping("/api/v1/security-event-cash-flows") public class SecurityEventCashFlowRestController extends AbstractRestController { private final FifoPositionsFactory positionsFactory; @@ -89,7 +88,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody SecurityEventCashFlow event) { + public ResponseEntity post(@RequestBody @Valid SecurityEventCashFlow event) { if (event.getEventType() == REDEMPTION) positionsFactory.invalidateCache(); return super.post(event); } @@ -103,8 +102,8 @@ public ResponseEntity post(@Valid @RequestBody SecurityEventCashFlow event public ResponseEntity put(@PathVariable("id") @Parameter(description = "Внутренний идентификатор выплаты") Integer id, - @Valid @RequestBody + @Valid SecurityEventCashFlow event) { if (event.getEventType() == REDEMPTION) positionsFactory.invalidateCache(); return super.put(id, event); @@ -123,7 +122,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(SecurityEventCashFlow object) { + public @Nullable Integer getId(SecurityEventCashFlow object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java index e3623562..d1c4c0fd 100644 --- a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java +++ b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -85,7 +86,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody SecurityQuote quote) { + public ResponseEntity post(@RequestBody @Valid SecurityQuote quote) { return super.post(quote); } @@ -98,8 +99,8 @@ public ResponseEntity post(@Valid @RequestBody SecurityQuote quote) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Номер записи о котировке") Integer id, - @Valid @RequestBody + @Valid SecurityQuote quote) { return super.put(id, quote); } @@ -116,7 +117,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(SecurityQuote object) { + public @Nullable Integer getId(SecurityQuote object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/SecurityRestController.java b/src/main/java/ru/investbook/api/SecurityRestController.java index 2ac8fe49..da6203d7 100644 --- a/src/main/java/ru/investbook/api/SecurityRestController.java +++ b/src/main/java/ru/investbook/api/SecurityRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -48,11 +49,9 @@ @Tag(name = "Инструменты", description = "Акции, облигации, деривативы и валютные пары") @RequestMapping("/api/v1/securities") public class SecurityRestController extends AbstractRestController { - private final SecurityRepository repository; public SecurityRestController(SecurityRepository repository, SecurityConverter converter) { super(repository, converter); - this.repository = repository; } @Override @@ -88,7 +87,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody Security security) { + public ResponseEntity post(@RequestBody @Valid Security security) { return super.post(security); } @@ -102,8 +101,8 @@ public ResponseEntity post(@Valid @RequestBody Security security) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Идентификатор", example = "123", required = true) Integer id, - @Valid @RequestBody + @Valid Security security) { return super.put(id, security); } @@ -121,7 +120,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(Security object) { + public @Nullable Integer getId(Security object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java index 869b880d..ab7e9f13 100644 --- a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java +++ b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Transaction; import org.spacious_team.broker.pojo.TransactionCashFlow; @@ -47,7 +48,9 @@ import ru.investbook.repository.TransactionCashFlowRepository; import java.util.List; +import java.util.stream.Stream; +import static java.util.Objects.isNull; import static org.springframework.http.HttpHeaders.LOCATION; @RestController @@ -76,12 +79,15 @@ public TransactionCashFlowRestController(TransactionCashFlowRepository repositor protected Page get( @RequestParam(value = "portfolio", required = false) @Parameter(description = "Номер счета") + @Nullable String portfolio, @RequestParam(value = "trade-id", required = false) @Parameter(description = "Номер сделки в системе учета брокера") + @Nullable String tradeId, @RequestParam(value = "event-type", required = false) @Parameter(description = "Тип (стоимость/комиссия/НКД)", example = "Смотреть API \"Типы событий\"") + @Nullable Integer eventType, @Parameter(hidden = true) Pageable pageable @@ -95,17 +101,27 @@ protected Page get( } private Page filterByEventType(Page transactions, - Integer eventType) { + @Nullable Integer eventType) { + List transactionCashFlows = transactions .stream() - .flatMap(transaction -> eventType == null ? - repository.findByTransactionId(transaction.getId()).stream() : - repository.findByTransactionIdAndCashFlowType(transaction.getId(), CashFlowType.valueOf(eventType)).stream()) + .flatMap(transaction -> findTransactionCashFlow(transaction, eventType)) .map(converter::fromEntity) .toList(); return new PageImpl<>(transactionCashFlows); } + private Stream findTransactionCashFlow(Transaction transaction, + @Nullable Integer eventType) { + @Nullable Integer id = transaction.getId(); + if (isNull(id)) { + return Stream.empty(); + } + return isNull(eventType) ? + repository.findByTransactionId(id).stream() : + repository.findByTransactionIdAndCashFlowType(id, CashFlowType.valueOf(eventType)).stream(); + } + @Override @GetMapping("{id}") @Operation(summary = "Отобразить одну", description = "Отобразить информацию о конкретной сделке", @@ -125,7 +141,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody TransactionCashFlow object) { + public ResponseEntity post(@RequestBody @Valid TransactionCashFlow object) { return super.post(object); } @@ -142,8 +158,8 @@ public ResponseEntity post(@Valid @RequestBody TransactionCashFlow object) public ResponseEntity put(@PathVariable("id") @Parameter(description = "Внутренний идентификатор сделки") Integer id, - @Valid @RequestBody + @Valid TransactionCashFlow object) { return super.put(id, object); } @@ -166,7 +182,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(TransactionCashFlow object) { + public @Nullable Integer getId(TransactionCashFlow object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/api/TransactionRestController.java b/src/main/java/ru/investbook/api/TransactionRestController.java index 8020eefb..fbfce778 100644 --- a/src/main/java/ru/investbook/api/TransactionRestController.java +++ b/src/main/java/ru/investbook/api/TransactionRestController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Transaction; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -74,9 +75,11 @@ public TransactionRestController(TransactionRepository repository, @ApiResponse(responseCode = "500", content = @Content)}) public Page get(@RequestParam(value = "portfolio", required = false) @Parameter(description = "Идентификатор счета брокера") + @Nullable String portfolio, @RequestParam(value = "trade-id", required = false) @Parameter(description = "Номер сделки в системе учета брокера") + @Nullable String tradeId, @Parameter(hidden = true) Pageable pageable) { @@ -131,7 +134,7 @@ public ResponseEntity get(@PathVariable("id") @ApiResponse(responseCode = "201", headers = @Header(name = LOCATION)), @ApiResponse(responseCode = "409"), @ApiResponse(responseCode = "500", content = @Content)}) - public ResponseEntity post(@Valid @RequestBody Transaction object) { + public ResponseEntity post(@RequestBody @Valid Transaction object) { positionsFactory.invalidateCache(); return super.post(object); } @@ -149,8 +152,8 @@ public ResponseEntity post(@Valid @RequestBody Transaction object) { public ResponseEntity put(@PathVariable("id") @Parameter(description = "Внутренний идентификатор сделки") Integer id, - @Valid @RequestBody + @Valid Transaction object) { positionsFactory.invalidateCache(); return super.put(id, object); @@ -173,7 +176,7 @@ public ResponseEntity delete(@PathVariable("id") } @Override - public Integer getId(Transaction object) { + public @Nullable Integer getId(Transaction object) { return object.getId(); } diff --git a/src/main/java/ru/investbook/converter/EventCashFlowConverter.java b/src/main/java/ru/investbook/converter/EventCashFlowConverter.java index b0639a67..bdfb5973 100644 --- a/src/main/java/ru/investbook/converter/EventCashFlowConverter.java +++ b/src/main/java/ru/investbook/converter/EventCashFlowConverter.java @@ -34,6 +34,7 @@ public class EventCashFlowConverter implements EntityConverter { + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public IssuerEntity toEntity(Issuer issuer) { IssuerEntity entity = new IssuerEntity(); diff --git a/src/main/java/ru/investbook/converter/PortfolioCashConverter.java b/src/main/java/ru/investbook/converter/PortfolioCashConverter.java index dacc2a62..df67e4be 100644 --- a/src/main/java/ru/investbook/converter/PortfolioCashConverter.java +++ b/src/main/java/ru/investbook/converter/PortfolioCashConverter.java @@ -25,6 +25,7 @@ @Component public class PortfolioCashConverter implements EntityConverter { + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public PortfolioCashEntity toEntity(PortfolioCash cash) { PortfolioCashEntity entity = new PortfolioCashEntity(); diff --git a/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java b/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java index 8ab06baa..91bce8ec 100644 --- a/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java +++ b/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java @@ -31,6 +31,7 @@ public class PortfolioPropertyConverter implements EntityConverter { private final PortfolioRepository portfolioRepository; + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public PortfolioPropertyEntity toEntity(PortfolioProperty property) { PortfolioEntity portfolioEntity = portfolioRepository.getReferenceById(property.getPortfolio()); @@ -38,7 +39,7 @@ public PortfolioPropertyEntity toEntity(PortfolioProperty property) { PortfolioPropertyEntity entity = new PortfolioPropertyEntity(); entity.setId(property.getId()); entity.setPortfolio(portfolioEntity); - entity.setTimestamp(property.getTimestamp()); + entity.setTimestamp(property.getTimestamp()); // when is null, default value is set entity.setProperty(property.getProperty().name()); entity.setValue(property.getValue()); return entity; diff --git a/src/main/java/ru/investbook/converter/SecurityConverter.java b/src/main/java/ru/investbook/converter/SecurityConverter.java index 8e0dc5cd..1ce5f547 100644 --- a/src/main/java/ru/investbook/converter/SecurityConverter.java +++ b/src/main/java/ru/investbook/converter/SecurityConverter.java @@ -27,6 +27,7 @@ @RequiredArgsConstructor public class SecurityConverter implements EntityConverter { + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public SecurityEntity toEntity(Security security) { SecurityEntity entity = new SecurityEntity(); diff --git a/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java b/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java index e660e6e8..3046a98a 100644 --- a/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java +++ b/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java @@ -19,6 +19,7 @@ package ru.investbook.converter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityDescription; import org.springframework.stereotype.Component; import ru.investbook.entity.IssuerEntity; @@ -33,9 +34,10 @@ public class SecurityDescriptionConverter implements EntityConverter { private final SecurityRepository securityRepository; + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public SecurityQuoteEntity toEntity(SecurityQuote quote) { SecurityEntity securityEntity = securityRepository.getReferenceById(quote.getSecurity()); diff --git a/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java b/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java index 65ed1e09..b03dc758 100644 --- a/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java +++ b/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java @@ -25,14 +25,13 @@ import ru.investbook.entity.CashFlowTypeEntity; import ru.investbook.entity.TransactionCashFlowEntity; import ru.investbook.repository.CashFlowTypeRepository; -import ru.investbook.repository.TransactionRepository; @Component @RequiredArgsConstructor public class TransactionCashFlowConverter implements EntityConverter { - private final TransactionRepository transactionRepository; private final CashFlowTypeRepository cashFlowTypeRepository; + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public TransactionCashFlowEntity toEntity(TransactionCashFlow cash) { CashFlowTypeEntity cashFlowTypeEntity = cashFlowTypeRepository.getReferenceById(cash.getEventType().getId()); @@ -42,6 +41,7 @@ public TransactionCashFlowEntity toEntity(TransactionCashFlow cash) { entity.setTransactionId(cash.getTransactionId()); entity.setCashFlowType(cashFlowTypeEntity); entity.setValue(cash.getValue()); + //noinspection ConstantValue if (cash.getCurrency() != null) entity.setCurrency(cash.getCurrency()); return entity; } diff --git a/src/main/java/ru/investbook/converter/TransactionConverter.java b/src/main/java/ru/investbook/converter/TransactionConverter.java index e502c906..269ecbe3 100644 --- a/src/main/java/ru/investbook/converter/TransactionConverter.java +++ b/src/main/java/ru/investbook/converter/TransactionConverter.java @@ -30,6 +30,7 @@ public class TransactionConverter implements EntityConverter { private final SecurityRepository securityRepository; + @SuppressWarnings({"nullness", "DataFlowIssue"}) @Override public TransactionEntity toEntity(Transaction transaction) { SecurityEntity securityEntity = securityRepository.getReferenceById(transaction.getSecurity()); diff --git a/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java b/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java index fd2d1545..852d47bb 100644 --- a/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java +++ b/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java @@ -41,6 +41,7 @@ * returns not null ID, when this ID will be stored to DB. In another case, the DBMS must generate ID itself * based on the strategy implemented by {@link OnExecutionGenerator}. */ +@SuppressWarnings("removal") class BeforeOrOnExecutionGenerator implements BeforeExecutionGenerator, OnExecutionGenerator { private final BeforeExecutionGenerator beforeExecutionGenerator; @@ -73,6 +74,7 @@ public boolean generatedOnExecution() { public boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) { try { EventType eventType = beforeExecutionGenerator.getEventTypes().iterator().next(); + @SuppressWarnings("argument") Object id = beforeExecutionGenerator.generate(session, entity, null, eventType); return isNull(id); } catch (Exception e) { diff --git a/src/main/java/ru/investbook/entity/EventCashFlowEntity.java b/src/main/java/ru/investbook/entity/EventCashFlowEntity.java index 5301285d..7136634b 100644 --- a/src/main/java/ru/investbook/entity/EventCashFlowEntity.java +++ b/src/main/java/ru/investbook/entity/EventCashFlowEntity.java @@ -29,6 +29,8 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.annotations.GenericGenerator; import java.math.BigDecimal; import java.time.Instant; @@ -68,5 +70,5 @@ public class EventCashFlowEntity { @Basic @Column(name = "description") - private String description; + private @Nullable String description; } diff --git a/src/main/java/ru/investbook/entity/IssuerEntity.java b/src/main/java/ru/investbook/entity/IssuerEntity.java index 646cb621..69c9aded 100644 --- a/src/main/java/ru/investbook/entity/IssuerEntity.java +++ b/src/main/java/ru/investbook/entity/IssuerEntity.java @@ -23,6 +23,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; @Entity @Table(name = "issuer") @@ -34,7 +35,7 @@ public class IssuerEntity { private Integer id; @Column(name = "taxpayer_id") - private String taxpayerId; + private @Nullable String taxpayerId; @Column(name = "name", nullable = false) private String name; diff --git a/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java b/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java index 4fbd73dd..10a840ca 100644 --- a/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java +++ b/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java @@ -28,6 +28,7 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +import org.checkerframework.checker.nullness.qual.Nullable; @Entity @Table(name = "security_description") @@ -39,10 +40,10 @@ public class SecurityDescriptionEntity { private int security; @Column(name = "sector") - private String sector; + private @Nullable String sector; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "issuer", referencedColumnName = "id") @JsonIgnoreProperties({"hibernateLazyInitializer"}) - private IssuerEntity issuer; + private @Nullable IssuerEntity issuer; } diff --git a/src/main/java/ru/investbook/entity/SecurityEntity.java b/src/main/java/ru/investbook/entity/SecurityEntity.java index e130459d..6ce5564d 100644 --- a/src/main/java/ru/investbook/entity/SecurityEntity.java +++ b/src/main/java/ru/investbook/entity/SecurityEntity.java @@ -26,6 +26,8 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.annotations.GenericGenerator; import org.spacious_team.broker.pojo.SecurityType; import java.util.regex.Pattern; @@ -48,11 +50,11 @@ public class SecurityEntity { private SecurityType type; @Column(name = "isin") - private String isin; + private @Nullable String isin; @Column(name = "ticker") - private String ticker; + private @Nullable String ticker; @Column(name = "name") - private String name; + private @Nullable String name; } diff --git a/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java b/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java index 1e4038e3..7588b7ab 100644 --- a/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java +++ b/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java @@ -28,6 +28,8 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.annotations.GenericGenerator; import java.math.BigDecimal; import java.time.Instant; @@ -57,13 +59,13 @@ public class SecurityQuoteEntity { @Basic @Column(name = "price") - private BigDecimal price; + private @Nullable BigDecimal price; @Basic @Column(name = "accrued_interest") - private BigDecimal accruedInterest; + private @Nullable BigDecimal accruedInterest; @Basic @Column(name = "currency") - private String currency; + private @Nullable String currency; } diff --git a/src/main/java/ru/investbook/loadingpage/LoadingPageServer.java b/src/main/java/ru/investbook/loadingpage/LoadingPageServer.java index 9bd3c20b..d4d934c2 100644 --- a/src/main/java/ru/investbook/loadingpage/LoadingPageServer.java +++ b/src/main/java/ru/investbook/loadingpage/LoadingPageServer.java @@ -22,6 +22,7 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import ru.investbook.BrowserHomePageOpener; import java.io.ByteArrayOutputStream; @@ -30,23 +31,24 @@ import java.io.OutputStream; import java.net.InetSocketAddress; -import static java.nio.charset.StandardCharsets.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; @Slf4j public class LoadingPageServer implements AutoCloseable{ public static final int SERVER_PORT = 2031; public static final int DEFAULT_CLOSE_DELAY_SEC = 120; - private HttpServer server; + private volatile @Nullable HttpServer server = null; public void start() { try { - server = HttpServer.create(new InetSocketAddress(SERVER_PORT), 0); + HttpServer server = HttpServer.create(new InetSocketAddress(SERVER_PORT), 0); server.createContext("/", new LoadingPageHandler()); server.createContext("/main-app-port", new PortHandler()); - server.start(); - + this.server = server; String loadingPageUrl = "http://localhost:" + SERVER_PORT + "/loading"; BrowserHomePageOpener.open(loadingPageUrl); } catch (IOException e) { @@ -54,27 +56,20 @@ public void start() { } } - public void stopAfter(int delayInSec) { - if (isRunning()) { - server.stop(delayInSec); - server = null; - } - } - - public boolean isRunning() { - return server != null; - } - @Override public void close() { - stopAfter(DEFAULT_CLOSE_DELAY_SEC); + if (nonNull(server)) { + //noinspection DataFlowIssue + server.stop(DEFAULT_CLOSE_DELAY_SEC); + server = null; + } } static class LoadingPageHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { byte[] data; - try (InputStream in = getClass().getResourceAsStream("/templates/loading.html")) { + try (InputStream in = requireNonNull(getClass().getResourceAsStream("/templates/loading.html"))) { ByteArrayOutputStream out = new ByteArrayOutputStream(); in.transferTo(out); data = out.toByteArray(); diff --git a/src/main/java/ru/investbook/loadingpage/LoadingPageServerUtils.java b/src/main/java/ru/investbook/loadingpage/LoadingPageServerUtils.java index f787f684..82ee9e98 100644 --- a/src/main/java/ru/investbook/loadingpage/LoadingPageServerUtils.java +++ b/src/main/java/ru/investbook/loadingpage/LoadingPageServerUtils.java @@ -30,6 +30,7 @@ import java.util.Properties; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; @Slf4j @UtilityClass @@ -42,7 +43,7 @@ public static int getMainAppPort() { Properties properties = loadProperties(); String value = properties.getProperty("server.port", "2030"); return Integer.parseInt(value); - } catch (IOException e) { + } catch (Exception e) { log.warn("Can't find 'server.port' property, fallback to default value: 2030"); return 2030; } @@ -53,7 +54,7 @@ public static boolean shouldOpenHomePageAfterStart() { Properties properties = loadProperties(); String value = properties.getProperty("investbook.open-home-page-after-start", "true"); return Boolean.parseBoolean(value); - } catch (IOException e) { + } catch (Exception e) { log.warn("Can't find 'investbook.open-home-page-after-start' fallback to default value: true"); return true; } @@ -66,7 +67,7 @@ private static Properties loadProperties() throws IOException { properties.load(reader); } catch (Exception e) { // Properties file is not found in app installation path, read default file from class path - try (InputStream in = LoadingPageServerUtils.class.getClassLoader().getResourceAsStream(CONF_PROPERTIES); + try (InputStream in = requireNonNull(LoadingPageServerUtils.class.getResourceAsStream(CONF_PROPERTIES)); Reader reader = new InputStreamReader(in, UTF_8)) { properties.load(reader); } diff --git a/src/main/java/ru/investbook/openformat/OpenFormatHelper.java b/src/main/java/ru/investbook/openformat/OpenFormatHelper.java index 8d864d98..24378c67 100644 --- a/src/main/java/ru/investbook/openformat/OpenFormatHelper.java +++ b/src/main/java/ru/investbook/openformat/OpenFormatHelper.java @@ -18,22 +18,26 @@ package ru.investbook.openformat; -import org.springframework.util.StringUtils; +import org.checkerframework.checker.nullness.qual.PolyNull; import java.util.Objects; +import static org.springframework.util.StringUtils.hasLength; import static ru.investbook.entity.SecurityEntity.isinPattern; public class OpenFormatHelper { - public static String getValidCurrencyOrNull(String currency) { - if (currency == null) return null; + public static @PolyNull String getValidCurrencyOrNull(@PolyNull String currency) { + if (currency == null) { + return null; + } currency = currency.toUpperCase(); return Objects.equals(currency, "RUR") ? "RUB" : currency; } - public static String getValidIsinOrNull(String isin) { - return StringUtils.hasLength(isin) && isinPattern.matcher(isin).matches() ? + @SuppressWarnings("return") + public static @PolyNull String getValidIsinOrNull(@PolyNull String isin) { + return hasLength(isin) && isinPattern.matcher(isin).matches() ? isin : null; } diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/AccountPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/AccountPof.java index 854f8a99..68fcc202 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/AccountPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/AccountPof.java @@ -27,11 +27,11 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.PortfolioProperty; import org.spacious_team.broker.pojo.PortfolioProperty.PortfolioPropertyBuilder; import org.spacious_team.broker.pojo.PortfolioPropertyType; -import org.springframework.lang.Nullable; import ru.investbook.entity.PortfolioEntity; import java.math.BigDecimal; @@ -54,27 +54,30 @@ @Slf4j public class AccountPof { + @SuppressWarnings("type.argument") private static final ThreadLocal idGenerator = ThreadLocal.withInitial(AtomicInteger::new); + @SuppressWarnings("type.argument") private static final ThreadLocal> accountNumberToIdMap = ThreadLocal.withInitial(HashMap::new); - @NotNull @JsonProperty("id") + @NotNull int id; - @Nullable + @JsonProperty("account-number") + @Nullable String accountNumber; - @NotNull @JsonProperty("type") + @NotNull AccountTypePof type; - @NotNull @JsonProperty("valuation") + @NotNull BigDecimal valuation; - @NotEmpty @JsonProperty("valuation-currency") + @NotEmpty String valuationCurrency; static void resetAccountIdGenerator() { diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/AssetPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/AssetPof.java index f96a18ae..028b31ce 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/AssetPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/AssetPof.java @@ -26,9 +26,9 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityType; -import org.springframework.lang.Nullable; import ru.investbook.entity.SecurityEntity; import java.util.Optional; @@ -43,24 +43,24 @@ @Slf4j class AssetPof { - @NotNull @JsonProperty("id") + @NotNull int id; - @NotNull @JsonProperty("type") + @NotNull String type; - @Nullable @JsonProperty("symbol") + @Nullable String symbol; - @Nullable @JsonProperty("name") + @Nullable String name; - @Nullable @JsonProperty("isin") + @Nullable String isin; diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/CashBalancesPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/CashBalancesPof.java index 132956ff..c556b3c5 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/CashBalancesPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/CashBalancesPof.java @@ -48,24 +48,24 @@ @Slf4j public class CashBalancesPof { - @NotNull @JsonProperty("account") + @NotNull int account; - @NotNull @Builder.Default @JsonProperty("cash") + @NotNull Collection cash = Collections.emptyList(); @Value @JsonInclude(JsonInclude.Include.NON_NULL) private static class CashPof { - @NotNull @JsonProperty("value") + @NotNull BigDecimal value; - @NotEmpty @JsonProperty("currency") + @NotEmpty String currency; } diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/CashFlowPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/CashFlowPof.java index fc66cc3a..4bc1b668 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/CashFlowPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/CashFlowPof.java @@ -27,9 +27,9 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; -import org.springframework.lang.Nullable; import ru.investbook.entity.EventCashFlowEntity; import java.math.BigDecimal; @@ -48,36 +48,36 @@ @Slf4j public class CashFlowPof { - @NotNull @JsonProperty("id") + @NotNull int id; - @Nullable @JsonProperty("flow-id") + @Nullable String flowId; - @NotNull @JsonProperty("account") + @NotNull int account; - @NotNull @JsonProperty("timestamp") + @NotNull long timestamp; - @NotNull @JsonProperty("amount") + @NotNull BigDecimal amount; - @NotEmpty @JsonProperty("currency") + @NotEmpty String currency; - @NotNull @JsonProperty("type") + @NotNull PaymentTypePof type; - @Nullable @JsonProperty("description") + @Nullable String description; static CashFlowPof of(EventCashFlowEntity cashFlow) { diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/PaymentPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/PaymentPof.java index c0d2274e..488c04c9 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/PaymentPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/PaymentPof.java @@ -29,10 +29,10 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.pojo.SecurityType; -import org.springframework.lang.Nullable; import ru.investbook.entity.SecurityEventCashFlowEntity; import ru.investbook.openformat.OpenFormatHelper; @@ -55,56 +55,56 @@ @Slf4j public class PaymentPof { - @NotNull @JsonProperty("id") + @NotNull int id; - @Nullable @JsonProperty("payment-id") + @Nullable String paymentId; - @NotNull @JsonProperty("account") + @NotNull int account; - @NotNull @Getter(AccessLevel.NONE) @JsonProperty("asset") + @NotNull int asset; - @NotNull @JsonProperty("type") + @NotNull PaymentTypePof type; /** * Поддерживаются дробные акции */ - @NotNull @JsonProperty("count") + @NotNull BigDecimal count; - @NotNull @JsonProperty("timestamp") + @NotNull long timestamp; - @NotNull @JsonProperty("amount") + @NotNull BigDecimal amount; - @NotEmpty @JsonProperty("currency") + @NotEmpty String currency; - @Nullable @JsonProperty("tax") + @Nullable BigDecimal tax; - @Nullable @JsonProperty("tax-currency") + @Nullable String taxCurrency; - @Nullable @JsonProperty("description") + @Nullable String description; static PaymentPof of(SecurityEventCashFlowEntity cashFlow, Optional tax) { @@ -142,7 +142,7 @@ Collection getSecurityEventCashFlow(Map .value(amount) .currency(getValidCurrencyOrNull(currency)) .build(); - if (tax != null && Math.abs(tax.floatValue()) > 0.0001) { + if (tax != null && taxCurrency != null && Math.abs(tax.floatValue()) > 0.0001) { return Set.of(cashFlow, cashFlow.toBuilder() .eventType(CashFlowType.TAX) diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatPersister.java b/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatPersister.java index 23e9082d..20c27e0a 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatPersister.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatPersister.java @@ -21,11 +21,11 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityDescription; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityType; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import ru.investbook.parser.InvestbookApiClient; import ru.investbook.parser.SecurityRegistrar; @@ -43,6 +43,7 @@ import static java.util.concurrent.Executors.newWorkStealingPool; import static java.util.stream.Collectors.toMap; +import static org.springframework.util.StringUtils.hasText; import static ru.investbook.openformat.v1_1_0.PortfolioOpenFormatV1_1_0.GENERATED_BY_INVESTBOOK; @Service @@ -107,7 +108,7 @@ public void persist(PortfolioOpenFormatV1_1_0 object) { .flatMap(Optional::stream) .forEach(api::addEventCashFlow)); - VndInvestbookPof vndInvestbook = object.getVndInvestbook(); + @Nullable VndInvestbookPof vndInvestbook = object.getVndInvestbook(); if (vndInvestbook != null) { tasks.add(() -> vndInvestbook.getPortfolioCash().forEach(api::addPortfolioCash)); tasks.add(() -> vndInvestbook.getPortfolioProperties().forEach(api::addPortfolioProperty)); @@ -126,14 +127,12 @@ public void persist(PortfolioOpenFormatV1_1_0 object) { @SneakyThrows private void runTasks(Collection tasks) { - ExecutorService executorService = newWorkStealingPool(4 * Runtime.getRuntime().availableProcessors()); - try { + try (ExecutorService executorService = newWorkStealingPool(4 * Runtime.getRuntime().availableProcessors())) { + @SuppressWarnings("assignment") Collection> callables = tasks.stream() .map(Executors::callable) .toList(); executorService.invokeAll(callables); - } finally { - executorService.shutdown(); } } @@ -151,7 +150,7 @@ private Collection getTradesWithUniqTradeId(Collection trade Collection tradesWithUniqId = new ArrayList<>(trades.size()); Set tradeIds = new HashSet<>(trades.size()); for (TradePof t : trades) { - String tradeId = StringUtils.hasText(t.getTradeId()) ? + String tradeId = hasText(t.getTradeId()) ? t.getTradeId() : t.getSettlementOrTimestamp() + ":" + t.getSecurityId(assetToSecurityId) + ":" + t.getAccount(); String tid = getUniqId(tradeId, tradeIds); @@ -168,7 +167,8 @@ private Collection getTransfersWithUniqTransferId(Collection transfersWithUniqId = new ArrayList<>(transfers.size()); Set transferIds = new HashSet<>(transfers.size()); for (TransferPof t : transfers) { - String transferId = StringUtils.hasText(t.getTransferId()) ? + @SuppressWarnings("assignment") + String transferId = hasText(t.getTransferId()) ? t.getTransferId() : t.getTimestamp() + ":" + t.getSecurityId(assetToSecurityId) + ":" + t.getAccount(); String tid = getUniqId(transferId, transferIds); diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatV1_1_0.java b/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatV1_1_0.java index 66b7c788..92caf62e 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatV1_1_0.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/PortfolioOpenFormatV1_1_0.java @@ -25,10 +25,11 @@ import lombok.Builder; import lombok.Value; import lombok.extern.jackson.Jacksonized; -import org.springframework.lang.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collection; -import java.util.Collections; + +import static java.util.Collections.emptySet; @Jacksonized @Builder @@ -38,65 +39,65 @@ public class PortfolioOpenFormatV1_1_0 { public static String GENERATED_BY_INVESTBOOK = "investbook"; - @NotNull @Builder.Default @JsonProperty("version") + @NotNull String version = "1.1.0"; - @NotNull @Builder.Default @JsonProperty("generated-by") + @NotNull String generatedBy = GENERATED_BY_INVESTBOOK; - @NotNull @Builder.Default @JsonProperty("generated") + @NotNull long generated = System.currentTimeMillis() / 1000; - @NotNull @JsonProperty("end") + @NotNull long end; - @Nullable @JsonProperty("start") + @Nullable Long start; - @NotNull @Builder.Default @JsonProperty("accounts") - Collection accounts = Collections.emptySet(); - @NotNull + Collection accounts = emptySet(); + @Builder.Default @JsonProperty("cash-balances") - Collection cashBalances = Collections.emptySet(); - @NotNull + Collection cashBalances = emptySet(); + @Builder.Default @JsonProperty("assets") - Collection assets = Collections.emptySet(); - @NotNull + Collection assets = emptySet(); + @Builder.Default @JsonProperty("trades") - Collection trades = Collections.emptySet(); - @NotNull + Collection trades = emptySet(); + @Builder.Default @JsonProperty("transfers") - Collection transfer = Collections.emptySet(); - @NotNull + Collection transfer = emptySet(); + @Builder.Default @JsonProperty("payments") - Collection payments = Collections.emptySet(); - @NotNull + Collection payments = emptySet(); + @Builder.Default @JsonProperty("cash-flows") - Collection cashFlows = Collections.emptySet(); + @NotNull + Collection cashFlows = emptySet(); - @Nullable @JsonProperty("vnd-investbook") + @Nullable VndInvestbookPof vndInvestbook; } diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/TradePof.java b/src/main/java/ru/investbook/openformat/v1_1_0/TradePof.java index 4d8f5074..721884c2 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/TradePof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/TradePof.java @@ -29,12 +29,12 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityType; import org.spacious_team.broker.report_parser.api.AbstractTransaction; import org.spacious_team.broker.report_parser.api.DerivativeTransaction; import org.spacious_team.broker.report_parser.api.ForeignExchangeTransaction; import org.spacious_team.broker.report_parser.api.SecurityTransaction; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import ru.investbook.entity.SecurityEventCashFlowEntity; import ru.investbook.entity.TransactionCashFlowEntity; @@ -60,80 +60,87 @@ @Slf4j public class TradePof { - @NotNull @JsonProperty("id") + @NotNull int id; - @NotEmpty @JsonProperty("trade-id") + @NotEmpty String tradeId; /** + * Дата и время сделки. * Если значение null, то значение поля settlement обязательно */ - @Nullable @JsonProperty("timestamp") + @Nullable Long timestamp; /** + * Дата и время поставки. * Если значение null, то значение поля timestamp обязательно */ - @Nullable @JsonProperty("settlement") + @Nullable Long settlement; - @NotNull @JsonProperty("account") + @NotNull int account; - @NotNull @Getter(AccessLevel.NONE) @JsonProperty("asset") + @NotNull int asset; /** * Если '+', то это покупка, если '-', то это продажа. * Поддерживаются дробные акции */ - @NotNull @JsonProperty("count") + @NotNull BigDecimal count; /** - * В валюте сделки, для облигации - без НКД, для деривативов в валюте - опционально + * Цена бумаги/контракта (за единицу) в валюте сделки, + * для облигации - без учета НКД, для деривативов поле может отсутствовать */ - @Nullable @JsonProperty("price") + @Nullable BigDecimal price; - @Nullable @JsonProperty("accrued-interest") + @Nullable BigDecimal accruedInterest; /** - * Котировка. Для облигации в процентах, для деривативов в пунктах + * Котировка. Для облигации в процентах, для деривативов в пунктах. + * Для деривативов - обязательное поле, для облигаций - опциональное. */ - @Nullable @JsonProperty("quote") + @Nullable BigDecimal quote; - @Nullable + /** + * Может отсутствовать, если поле 'price' и 'accrued-interest' отсутствуют + */ @JsonProperty("currency") + @Nullable String currency; /** * Комиссия. Если отрицательное значение, значит возврат комиссии */ - @NotNull @JsonProperty("fee") + @NotNull BigDecimal fee; - @NotEmpty @JsonProperty("fee-currency") + @NotEmpty String feeCurrency; - @Nullable @JsonProperty("description") + @Nullable String description; static TradePof of(TransactionEntity transaction, @@ -165,8 +172,8 @@ static TradePof of(TransactionEntity transaction, .filter(e -> e.getCashFlowType().getId() == FEE.getId()) .findAny() .ifPresentOrElse(e -> builder - .fee(e.getValue().negate()) - .feeCurrency(getValidCurrencyOrNull(e.getCurrency())), + .fee(e.getValue().negate()) + .feeCurrency(requireNonNull(getValidCurrencyOrNull(e.getCurrency()))), () -> builder.fee(BigDecimal.ZERO).feeCurrency("RUB")); return builder.build(); } @@ -204,18 +211,19 @@ Optional toTransaction(Map accountToPortfo Map assetTypes) { try { SecurityType securityType = requireNonNull(assetTypes.get(asset)); + @Nullable BigDecimal value = (price == null) ? null : price.multiply(count).negate(); AbstractTransaction.AbstractTransactionBuilder builder = switch (securityType) { case STOCK, BOND, STOCK_OR_BOND, ASSET -> SecurityTransaction.builder() - .value(requireNonNull(price).multiply(count).negate()) + .value(requireNonNull(value)) .accruedInterest((accruedInterest == null) ? null : accruedInterest.multiply(count).negate()) - .valueCurrency(getValidCurrencyOrNull(requireNonNull(currency))); + .valueCurrency(requireNonNull(getValidCurrencyOrNull(currency))); case DERIVATIVE -> DerivativeTransaction.builder() - .valueInPoints(requireNonNull(quote).multiply(count).negate()) - .value((price == null) ? null : price.multiply(count).negate()) // для деривативов - опциональное - .valueCurrency(getValidCurrencyOrNull(currency)); // для деривативов - опциональное + .valueInPoints(requireNonNull(quote).multiply(count).negate()) + .value(value) // для деривативов - опциональное + .valueCurrency(getValidCurrencyOrNull(currency)); // для деривативов - опциональное case CURRENCY_PAIR -> ForeignExchangeTransaction.builder() - .value(requireNonNull(price).multiply(count).negate()) - .valueCurrency(getValidCurrencyOrNull(requireNonNull(currency))); + .value(requireNonNull(value)) + .valueCurrency(requireNonNull(getValidCurrencyOrNull(currency))); }; long ts = requireNonNull(getSettlementOrTimestamp()); @@ -225,8 +233,8 @@ Optional toTransaction(Map accountToPortfo .security(getSecurityId(assetToSecurityId)) .count(count.intValueExact()) .timestamp(Instant.ofEpochSecond(ts)) - .fee((fee == null) ? null : fee.negate()) - .feeCurrency(getValidCurrencyOrNull(feeCurrency)) + .fee(fee.negate()) + .feeCurrency(requireNonNull(getValidCurrencyOrNull(feeCurrency))) .build()); } catch (Exception e) { log.error("Не могу распарсить {}", this, e); diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/TransferPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/TransferPof.java index 887989fd..e0fde563 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/TransferPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/TransferPof.java @@ -28,9 +28,9 @@ import lombok.Value; import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.pojo.Transaction; -import org.springframework.lang.Nullable; import ru.investbook.entity.TransactionEntity; import java.math.BigDecimal; @@ -38,10 +38,10 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.CashFlowType.FEE; import static ru.investbook.openformat.OpenFormatHelper.getValidCurrencyOrNull; @@ -52,52 +52,52 @@ @JsonIgnoreProperties(ignoreUnknown = true) @Slf4j public class TransferPof { - @NotNull @JsonProperty("id") + @NotNull int id; /** * Если описывается сплит акций, то может иметь одинаковое значение для нескольких объектов, которое разрешается * {@link PortfolioOpenFormatPersister} */ - @Nullable @JsonProperty("transfer-id") + @Nullable String transferId; - @NotNull @JsonProperty("account") + @NotNull int account; - @NotNull @JsonProperty("timestamp") + @NotNull long timestamp; - @NotNull @JsonProperty("asset") @Getter(AccessLevel.NONE) + @NotNull int asset; /** * Поддерживаются дробные акции */ - @NotNull @JsonProperty("count") + @NotNull BigDecimal count; - @Nullable @JsonProperty("fee-account") + @Nullable Integer feeAccount; - @Nullable @JsonProperty("fee") + @Nullable BigDecimal fee; - @Nullable @JsonProperty("fee-currency") + @Nullable String feeCurrency; - @Nullable @JsonProperty("description") + @Nullable String description; static TransferPof of(TransactionEntity transaction) { @@ -115,8 +115,8 @@ Optional toTransaction(Map accountToPortfolioId, Map assetToSecurityId) { try { return Optional.of(Transaction.builder() - .tradeId(transferId) - .portfolio(Objects.requireNonNull(accountToPortfolioId.get(account))) + .tradeId(requireNonNull(transferId)) + .portfolio(requireNonNull(accountToPortfolioId.get(account))) .timestamp(Instant.ofEpochSecond(timestamp)) .security(getSecurityId(assetToSecurityId)) .count(count.intValueExact()) @@ -130,10 +130,10 @@ Optional toTransaction(Map accountToPortfolioId, Collection getSecurityEventCashFlow(Map accountToPortfolioId, Map assetToSecurityId) { try { - if (fee != null) { + if (fee != null && feeCurrency != null) { return Set.of( SecurityEventCashFlow.builder() - .portfolio(Objects.requireNonNull(accountToPortfolioId.get(account))) + .portfolio(requireNonNull(accountToPortfolioId.get(account))) .timestamp(Instant.ofEpochSecond(timestamp)) .security(getSecurityId(assetToSecurityId)) .count(count.intValueExact()) @@ -151,6 +151,6 @@ Collection getSecurityEventCashFlow(Map } int getSecurityId(Map assetToSecurityId) { - return Objects.requireNonNull(assetToSecurityId.get(asset)); + return requireNonNull(assetToSecurityId.get(asset)); } } diff --git a/src/main/java/ru/investbook/openformat/v1_1_0/VndInvestbookPof.java b/src/main/java/ru/investbook/openformat/v1_1_0/VndInvestbookPof.java index 10721de3..259b0438 100644 --- a/src/main/java/ru/investbook/openformat/v1_1_0/VndInvestbookPof.java +++ b/src/main/java/ru/investbook/openformat/v1_1_0/VndInvestbookPof.java @@ -40,31 +40,31 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class VndInvestbookPof { - @NotNull @JsonProperty("version") + @NotNull String version; - @NotNull @Builder.Default @JsonIgnoreProperties(value = {"id"}) @JsonProperty("portfolio-cash") + @NotNull Collection portfolioCash = Collections.emptySet(); - @NotNull @Builder.Default @JsonIgnoreProperties(value = {"id"}) @JsonProperty("portfolio-properties") + @NotNull Collection portfolioProperties = Collections.emptySet(); - @NotNull @Builder.Default @JsonIgnoreProperties(value = {"issuer"}) @JsonProperty("security-descriptions") + @NotNull Collection securityDescriptions = Collections.emptySet(); - @NotNull @Builder.Default @JsonIgnoreProperties(value = {"id"}) @JsonProperty("security-quotes") + @NotNull Collection securityQuotes = Collections.emptySet(); } diff --git a/src/main/java/ru/investbook/package-info.java b/src/main/java/ru/investbook/package-info.java new file mode 100644 index 00000000..fd48bdf0 --- /dev/null +++ b/src/main/java/ru/investbook/package-info.java @@ -0,0 +1,23 @@ +/* + * InvestBook + * Copyright (C) 2024 Spacious Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +@DefaultQualifier(NonNull.class) +package ru.investbook; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; diff --git a/src/main/java/ru/investbook/parser/AbstractBrokerReport.java b/src/main/java/ru/investbook/parser/AbstractBrokerReport.java index 885eec7d..45ccc431 100644 --- a/src/main/java/ru/investbook/parser/AbstractBrokerReport.java +++ b/src/main/java/ru/investbook/parser/AbstractBrokerReport.java @@ -18,14 +18,12 @@ package ru.investbook.parser; -import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; +import lombok.ToString; import org.spacious_team.table_wrapper.api.ReportPage; -import java.nio.file.Path; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -33,32 +31,49 @@ import java.time.format.DateTimeFormatter; @RequiredArgsConstructor -@EqualsAndHashCode(of = "path") +@ToString(of = "reportName") +@EqualsAndHashCode(of = "reportName") public abstract class AbstractBrokerReport implements SingleBrokerReport { protected static final int LAST_TRADE_HOUR = 19; - @Getter - private final SecurityRegistrar securityRegistrar; - @Setter(AccessLevel.PROTECTED) - private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); - @Setter(AccessLevel.PROTECTED) - private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + protected static final ZoneId MOSCOW_ZONEID = ZoneId.of("Europe/Moscow"); + private static final DateTimeFormatter RUSSIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy"); + private static final DateTimeFormatter RUSSIAN_DATETIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); - @Setter(AccessLevel.PROTECTED) - private Path path; + private final DateTimeFormatter dateFormatter = RUSSIAN_DATE_FORMATTER; + private final DateTimeFormatter dateTimeFormatter = RUSSIAN_DATETIME_FORMATTER; + @Getter + private final ZoneId reportZoneId = MOSCOW_ZONEID; @Getter - @Setter(AccessLevel.PROTECTED) - private String portfolio; + private final ReportPage reportPage; + private final String reportName; @Getter - @Setter(AccessLevel.PROTECTED) - private ReportPage reportPage; + private final Instant reportEndDateTime; @Getter - @Setter(AccessLevel.PROTECTED) - private Instant reportEndDateTime; + private final String portfolio; @Getter - private final ZoneId reportZoneId = ZoneId.of("Europe/Moscow"); + private final SecurityRegistrar securityRegistrar; + + public AbstractBrokerReport(Attributes attributes, SecurityRegistrar securityRegistrar) { + this.reportPage = attributes.reportPage(); + this.reportName = attributes.reportName(); + this.reportEndDateTime = attributes.reportEndDateTime(); + this.portfolio = attributes.portfolio(); + this.securityRegistrar = securityRegistrar; + } public Instant convertToInstant(String value) { + return convertToInstant(value, dateFormatter, dateTimeFormatter, reportZoneId); + } + + protected static Instant convertToInstantWithRussianFormatAndMoscowZoneId(String value) { + return convertToInstant(value, RUSSIAN_DATE_FORMATTER, RUSSIAN_DATETIME_FORMATTER, MOSCOW_ZONEID); + } + + protected static Instant convertToInstant(String value, + DateTimeFormatter dateFormatter, + DateTimeFormatter dateTimeFormatter, + ZoneId reportZoneId) { value = value.trim(); if (value.contains(":")) { return LocalDateTime.parse(value, dateTimeFormatter).atZone(reportZoneId).toInstant(); @@ -67,8 +82,9 @@ public Instant convertToInstant(String value) { } } - @Override - public String toString() { - return path.getFileName().toString(); + public record Attributes(ReportPage reportPage, + String reportName, + Instant reportEndDateTime, + String portfolio) { } } diff --git a/src/main/java/ru/investbook/parser/AbstractExcelBrokerReport.java b/src/main/java/ru/investbook/parser/AbstractExcelBrokerReport.java index c04fe3a7..3d468994 100644 --- a/src/main/java/ru/investbook/parser/AbstractExcelBrokerReport.java +++ b/src/main/java/ru/investbook/parser/AbstractExcelBrokerReport.java @@ -28,8 +28,11 @@ public abstract class AbstractExcelBrokerReport extends AbstractBrokerReport { - public AbstractExcelBrokerReport(SecurityRegistrar securityRegistrar) { - super(securityRegistrar); + private final Workbook workbook; + + public AbstractExcelBrokerReport(ExcelAttributes attributes, SecurityRegistrar securityRegistrar) { + super(attributes.attributes(), securityRegistrar); + this.workbook = attributes.workbook(); } public static Workbook getWorkBook(String excelFileName, InputStream is) { @@ -45,4 +48,11 @@ public static Workbook getWorkBook(String excelFileName, InputStream is) { } } + @Override + public void close() throws IOException { + workbook.close(); + } + + public record ExcelAttributes(Workbook workbook, Attributes attributes) { + } } diff --git a/src/main/java/ru/investbook/parser/InvestbookApiClient.java b/src/main/java/ru/investbook/parser/InvestbookApiClient.java index 2034caa5..498d2c2c 100644 --- a/src/main/java/ru/investbook/parser/InvestbookApiClient.java +++ b/src/main/java/ru/investbook/parser/InvestbookApiClient.java @@ -193,7 +193,7 @@ private boolean saveWithoutUpdate(T object, Consumer persistFunction, Str persistFunction.accept(object); return true; } catch (ConstraintViolationException e) { // jakarta.validation, not SQL constraint - log.warn("{} {}: {}", errorMsg, object, e.getMessage()); + log.warn("{}, {}: {}", errorMsg, e.getMessage(), object); return false; } catch (Exception e) { if (isUniqIndexViolationException(e)) { @@ -219,7 +219,7 @@ private Optional saveWithoutUpdateAndGet(T object, Function ofNullable(security.getName()) .map(name -> declareSecurityByName(name, security.getType(), security::toBuilder))) .orElseThrow(); - case DERIVATIVE, CURRENCY_PAIR -> declareContractByTicker(security.getTicker(), security.getType()); - case ASSET -> declareAsset(security.getName(), security::toBuilder); + case DERIVATIVE, CURRENCY_PAIR -> Optional.ofNullable(security.getTicker()) + .map(tiker -> declareContractByTicker(tiker, security.getType())) + .orElseThrow(); + case ASSET -> Optional.ofNullable(security.getName()) + .map(name -> declareAsset(name, security::toBuilder)) + .orElseThrow(); }; } @@ -160,6 +164,7 @@ private Integer declareContractByTicker(String contract, SecurityType contractTy private Security buildSecurity(SecurityBuilder builder, SecurityType defaultType) { Security security = builder.build(); + //noinspection ConstantValue if (security.getType() == null) { security = builder.type(defaultType).build(); } diff --git a/src/main/java/ru/investbook/parser/SingleAbstractReportTable.java b/src/main/java/ru/investbook/parser/SingleAbstractReportTable.java index c6e50522..55f9267d 100644 --- a/src/main/java/ru/investbook/parser/SingleAbstractReportTable.java +++ b/src/main/java/ru/investbook/parser/SingleAbstractReportTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.report_parser.api.AbstractReportTable; import org.spacious_team.table_wrapper.api.TableHeaderColumn; @@ -32,7 +33,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable & TableHeaderColumn> SingleAbstractReportTable(SingleBrokerReport report, String tableName, - String tableFooter, + @Nullable String tableFooter, Class headerDescription) { super(report, tableName, tableFooter, headerDescription); } @@ -40,7 +41,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable & TableHeaderColumn> SingleAbstractReportTable(SingleBrokerReport report, String tableName, - String tableFooter, + @Nullable String tableFooter, Class headerDescription, int headersRowCount) { super(report, tableName, tableFooter, headerDescription, headersRowCount); @@ -49,7 +50,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable & TableHeaderColumn> SingleAbstractReportTable(SingleBrokerReport report, Predicate tableNameFinder, - Predicate tableFooterFinder, + @Nullable Predicate tableFooterFinder, Class headerDescription) { super(report, tableNameFinder, tableFooterFinder, headerDescription); } @@ -57,7 +58,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable & TableHeaderColumn> SingleAbstractReportTable(SingleBrokerReport report, Predicate tableNameFinder, - Predicate tableFooterFinder, + @Nullable Predicate tableFooterFinder, Class headerDescription, int headersRowCount) { super(report, tableNameFinder, tableFooterFinder, headerDescription, headersRowCount); @@ -67,7 +68,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable headerDescription) { super(report, providedTableName, namelessTableFirstLine, tableFooter, headerDescription); } @@ -76,7 +77,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable headerDescription, int headersRowCount) { super(report, providedTableName, namelessTableFirstLine, tableFooter, headerDescription, headersRowCount); @@ -86,7 +87,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable namelessTableFirstLineFinder, - Predicate tableFooterFinder, + @Nullable Predicate tableFooterFinder, Class headerDescription) { super(report, providedTableName, namelessTableFirstLineFinder, tableFooterFinder, headerDescription); } @@ -95,7 +96,7 @@ public abstract class SingleAbstractReportTable extends AbstractReportTable namelessTableFirstLineFinder, - Predicate tableFooterFinder, + @Nullable Predicate tableFooterFinder, Class headerDescription, int headersRowCount) { super(report, providedTableName, namelessTableFirstLineFinder, tableFooterFinder, headerDescription, headersRowCount); diff --git a/src/main/java/ru/investbook/parser/TableFactoryRegistryService.java b/src/main/java/ru/investbook/parser/TableFactoryRegistryService.java index 26859630..4ae5699d 100644 --- a/src/main/java/ru/investbook/parser/TableFactoryRegistryService.java +++ b/src/main/java/ru/investbook/parser/TableFactoryRegistryService.java @@ -19,6 +19,7 @@ package ru.investbook.parser; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.TableFactory; import org.spacious_team.table_wrapper.api.TableFactoryRegistry; import org.springframework.beans.factory.config.BeanDefinition; @@ -31,7 +32,6 @@ import java.time.Duration; import java.util.Collection; import java.util.Objects; -import java.util.stream.Collectors; import static java.lang.System.nanoTime; @@ -52,9 +52,10 @@ public TableFactoryRegistryService(InvestbookProperties properties) { Duration.ofNanos(nanoTime() - t0), factories.stream() .map(TableFactory::getClass) - .collect(Collectors.toList())); + .toList()); } + @SuppressWarnings("return") private static Collection findTableFactories(String basePackage) { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); scanner.addIncludeFilter(new AssignableTypeFilter(TableFactory.class)); @@ -63,10 +64,10 @@ private static Collection findTableFactories(String basePackage) { .map(BeanDefinition::getBeanClassName) .map(TableFactoryRegistryService::getInstance) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .toList(); } - private static TableFactory getInstance(String className) { + private static @Nullable TableFactory getInstance(String className) { try { Class clazz = Class.forName(className); Constructor constructor = clazz.getConstructor(); diff --git a/src/main/java/ru/investbook/parser/TransactionValueAndFeeParser.java b/src/main/java/ru/investbook/parser/TransactionValueAndFeeParser.java index f3bd6e86..794f6600 100644 --- a/src/main/java/ru/investbook/parser/TransactionValueAndFeeParser.java +++ b/src/main/java/ru/investbook/parser/TransactionValueAndFeeParser.java @@ -18,14 +18,13 @@ package ru.investbook.parser; -import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.RequiredArgsConstructor; import lombok.Value; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.TableHeaderColumn; import org.spacious_team.table_wrapper.api.TableRow; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import ru.investbook.report.ForeignExchangeRateService; @@ -60,10 +59,10 @@ public Arguments.ArgumentsBuilder argumentsBuilder() { */ public Result parse(Arguments arg) { - BigDecimal brokerFee = getFee(arg.row, arg.brokerFeeColumn).orElse(null); - BigDecimal marketFee = getFee(arg.row, arg.marketFeeColumn).orElse(null); - BigDecimal clearingFee = getFee(arg.row, arg.clearingFeeColumn).orElse(null); - BigDecimal stampDuty = getFee(arg.row, arg.stampDutyColumn).orElse(null); + @Nullable BigDecimal brokerFee = getFee(arg.row, arg.brokerFeeColumn).orElse(null); + @Nullable BigDecimal marketFee = getFee(arg.row, arg.marketFeeColumn).orElse(null); + @Nullable BigDecimal clearingFee = getFee(arg.row, arg.clearingFeeColumn).orElse(null); + @Nullable BigDecimal stampDuty = getFee(arg.row, arg.stampDutyColumn).orElse(null); BigDecimal value = arg.value; @@ -102,15 +101,18 @@ public Result parse(Arguments arg) { return new Result(value, valueCurrency, totalFee, totalFeeCurrency); } - private Optional getFee(TableRow row, TableHeaderColumn feeColumn) { + private Optional getFee(TableRow row, @Nullable TableHeaderColumn feeColumn) { + //noinspection DataFlowIssue,ReturnOfNull return Optional.ofNullable(feeColumn) .map(col -> row.getBigDecimalCellValueOrDefault(col, null)) .map(v -> Math.abs(v.floatValue()) > 1e-3 ? v : null); } private String getTotalFeeCurrency(Arguments arg, String valueCurrency, - BigDecimal brokerFee, BigDecimal marketFee, BigDecimal clearingFee, - BigDecimal stampDuty) { + @Nullable BigDecimal brokerFee, + @Nullable BigDecimal marketFee, + @Nullable BigDecimal clearingFee, + @Nullable BigDecimal stampDuty) { //noinspection ConstantConditions List feeCurrencies = Stream.of( @@ -127,13 +129,13 @@ private String getTotalFeeCurrency(Arguments arg, String valueCurrency, if (feeCurrencies.isEmpty()) { return valueCurrency; } else if (feeCurrencies.size() == 1) { - return feeCurrencies.get(0); + return feeCurrencies.getFirst(); } List currencies = feeCurrencies.stream() .filter(currency -> !Objects.equals(currency, valueCurrency)) .toList(); if (currencies.size() == 1) { - return currencies.get(0); + return currencies.getFirst(); } throw new IllegalArgumentException("Три разные валюты при проведении сделки не поддерживаются"); } @@ -146,9 +148,9 @@ private String filterCurrency(String currency) { * @throws NoSuchElementException если обменный курс не известен */ private BigDecimal addToTotalFee(BigDecimal totalFee, String totalFeeCurrency, - BigDecimal fee, TableHeaderColumn feeCurrencyColumn, + @Nullable BigDecimal fee, @Nullable TableHeaderColumn feeCurrencyColumn, Arguments arg) { - if (fee != null) { + if (fee != null && feeCurrencyColumn != null) { String feeCurrency = filterCurrency(arg.row.getStringCellValue(feeCurrencyColumn)); BigDecimal exchangeRate = arg.exchangeRateProvider .getExchangeRate(feeCurrency, totalFeeCurrency, arg.transactionInstant); @@ -158,9 +160,9 @@ private BigDecimal addToTotalFee(BigDecimal totalFee, String totalFeeCurrency, } private BigDecimal subtractFromValue(BigDecimal value, String valueCurrency, - BigDecimal fee, TableHeaderColumn feeCurrencyColumn, + @Nullable BigDecimal fee, @Nullable TableHeaderColumn feeCurrencyColumn, Arguments arg) { - if (fee == null) { + if (fee == null || feeCurrencyColumn == null) { return value; } String feeCurrency = filterCurrency(arg.row.getStringCellValue(feeCurrencyColumn)); @@ -184,20 +186,14 @@ public record Result(BigDecimal value, String valueCurrency, BigDecimal fee, Str @Value @Builder - private static class Arguments { - @NotNull + @SuppressWarnings("type.anno.before.modifier") + public static class Arguments { TableRow row; - @NotNull String portfolio; - @NotNull String tradeId; - @NotNull Instant transactionInstant; - @NotNull ExchangeRateProvider exchangeRateProvider; - @NotNull BigDecimal value; // отрицательное - для покупки, положительное - для продажи - @NotNull TableHeaderColumn valueCurrencyColumn; @Nullable TableHeaderColumn brokerFeeColumn; // положительное значение для списания комиссии diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookBrokerReport.java b/src/main/java/ru/investbook/parser/investbook/InvestbookBrokerReport.java index cb570125..7e61210f 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookBrokerReport.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookBrokerReport.java @@ -29,6 +29,7 @@ import org.spacious_team.table_wrapper.api.ReportPage; import org.spacious_team.table_wrapper.csv.CsvReportPage; import org.spacious_team.table_wrapper.excel.ExcelSheet; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import java.io.IOException; @@ -41,7 +42,7 @@ public class InvestbookBrokerReport implements BrokerReport { @Getter private final ReportPage reportPage; - private Workbook workbook; + private @Nullable Workbook workbook; @SneakyThrows public InvestbookBrokerReport(String fileName, InputStream is) { @@ -55,8 +56,9 @@ public InvestbookBrokerReport(String fileName, InputStream is) { } else { this.workbook = getWorkBook(fileName, is); this.reportPage = new ExcelSheet(workbook.getSheetAt(0)); - Assert.isTrue(reportPage.getRow(0).getCell(0).getStringValue().toLowerCase().contains("событие"), - "Не отчет в формате Investbook"); + @SuppressWarnings({"nullable", "DataFlowIssue"}) + String string = reportPage.getRow(0).getCell(0).getStringValue(); + Assert.isTrue(string.toLowerCase().contains("событие"), "Не отчет в формате Investbook"); } } diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookCashFlowTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookCashFlowTable.java index e3640e00..34849bf5 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookCashFlowTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.investbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.report_parser.api.BrokerReport; @@ -37,7 +38,7 @@ protected InvestbookCashFlowTable(BrokerReport report) { } @Override - protected EventCashFlow parseRow(TableRow row) { + protected @Nullable EventCashFlow parseRow(TableRow row) { String operation = row.getStringCellValue(OPERATION).toLowerCase(); CashFlowType type; boolean negate; @@ -56,10 +57,12 @@ protected EventCashFlow parseRow(TableRow row) { } else { return null; } - BigDecimal value = Optional.ofNullable(row.getBigDecimalCellValueOrDefault(PRICE, null)) + @SuppressWarnings("DataFlowIssue") + @Nullable BigDecimal value = Optional.ofNullable(row.getBigDecimalCellValueOrDefault(PRICE, null)) .orElseGet(() -> row.getBigDecimalCellValue(FEE)) .abs(); if (negate) value = value.negate(); + @SuppressWarnings("DataFlowIssue") String currency = Optional.ofNullable(row.getStringCellValueOrDefault(CURRENCY, null)) .orElseGet(() -> row.getStringCellValue(FEE_CURRENCY)); return EventCashFlow.builder() diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioCashTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioCashTable.java index 5fca9a49..173a7dee 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioCashTable.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioCashTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.investbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioCash; import org.spacious_team.broker.report_parser.api.BrokerReport; import org.spacious_team.table_wrapper.api.TableRow; @@ -31,7 +32,7 @@ protected InvestbookPortfolioCashTable(BrokerReport report) { } @Override - protected PortfolioCash parseRow(TableRow row) { + protected @Nullable PortfolioCash parseRow(TableRow row) { String operation = row.getStringCellValue(OPERATION).toLowerCase(); if (!operation.contains("остаток")) { // Остаток денежных средств return null; diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioPropertyTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioPropertyTable.java index 63df3933..1639be31 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioPropertyTable.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookPortfolioPropertyTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.investbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioProperty; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.spacious_team.broker.report_parser.api.BrokerReport; @@ -32,7 +33,7 @@ protected InvestbookPortfolioPropertyTable(BrokerReport report) { } @Override - protected PortfolioProperty parseRow(TableRow row) { + protected @Nullable PortfolioProperty parseRow(TableRow row) { String operation = row.getStringCellValue(OPERATION).toLowerCase(); if (!operation.contains("актив")) { // Оценка стоимости активов return null; diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java index b49700d1..e8c937d9 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.investbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityType; import org.spacious_team.broker.report_parser.api.BrokerReport; @@ -42,7 +43,7 @@ protected InvestbookSecurityDepositAndWithdrawalTable(BrokerReport report, } @Override - protected SecurityTransaction parseRow(TableRow row) { + protected @Nullable SecurityTransaction parseRow(TableRow row) { boolean negate; String operation = row.getStringCellValue(OPERATION).toLowerCase(); if (operation.contains("зачисление")) { // Зачисление ЦБ diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityEventCashFowTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityEventCashFowTable.java index adfa6cc5..15312a53 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityEventCashFowTable.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityEventCashFowTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.investbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.pojo.SecurityType; @@ -77,7 +78,7 @@ protected Collection parseRowToCollection(TableRow row) { .value(row.getBigDecimalCellValue(PRICE)) .currency(row.getStringCellValue(CURRENCY)); result.add(builder.build()); - BigDecimal tax = row.getBigDecimalCellValueOrDefault(FEE, null); + @Nullable BigDecimal tax = row.getBigDecimalCellValueOrDefault(FEE, null); if (tax != null && Math.abs(tax.floatValue()) > 0.001) { result.add(builder .eventType(CashFlowType.TAX) diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookTransactionTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookTransactionTable.java index 1b3d9d06..897d1319 100644 --- a/src/main/java/ru/investbook/parser/investbook/InvestbookTransactionTable.java +++ b/src/main/java/ru/investbook/parser/investbook/InvestbookTransactionTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.investbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityType; import org.spacious_team.broker.report_parser.api.AbstractTransaction; import org.spacious_team.broker.report_parser.api.AbstractTransaction.AbstractTransactionBuilder; @@ -45,7 +46,7 @@ protected InvestbookTransactionTable(BrokerReport report, } @Override - protected AbstractTransaction parseRow(TableRow row) { + protected @Nullable AbstractTransaction parseRow(TableRow row) { String operation = row.getStringCellValue(OPERATION).toLowerCase(); boolean isBuy; if (operation.contains("покупка")) { @@ -58,7 +59,7 @@ protected AbstractTransaction parseRow(TableRow row) { BigDecimal price = row.getBigDecimalCellValue(PRICE).abs(); int count = Math.abs(row.getIntCellValue(COUNT)); - BigDecimal value = getOptionalAmount(price, count, isBuy); + @Nullable BigDecimal value = getOptionalAmount(price, count, isBuy); String securityTickerNameOrIsin = row.getStringCellValue(TICKER_NAME_ISIN); SecurityType securityType = getSecurityTypeForTransaction(row, securityTickerNameOrIsin); @@ -97,7 +98,7 @@ protected AbstractTransaction parseRow(TableRow row) { .build(); } - private BigDecimal getOptionalAmount(BigDecimal price, int count, boolean isBuy) { + private @Nullable BigDecimal getOptionalAmount(@Nullable BigDecimal price, int count, boolean isBuy) { if (price == null) return null; BigDecimal value = price.multiply(BigDecimal.valueOf(count)); return isBuy ? value.negate() : value; @@ -105,7 +106,7 @@ private BigDecimal getOptionalAmount(BigDecimal price, int count, boolean isBuy) private SecurityType getSecurityTypeForTransaction(TableRow row, String securityTickerNameOrIsin) { SecurityType securityType; - String derivativePrice = row.getStringCellValueOrDefault(DERIVATIVE_PRICE_IN_CURRENCY, null); + @Nullable String derivativePrice = row.getStringCellValueOrDefault(DERIVATIVE_PRICE_IN_CURRENCY, null); if (derivativePrice != null) { securityType = SecurityType.DERIVATIVE; } else if (isCurrencyPair(securityTickerNameOrIsin)) { diff --git a/src/main/java/ru/investbook/parser/psb/CashFlowTable.java b/src/main/java/ru/investbook/parser/psb/CashFlowTable.java index bb631b44..57cdb380 100644 --- a/src/main/java/ru/investbook/parser/psb/CashFlowTable.java +++ b/src/main/java/ru/investbook/parser/psb/CashFlowTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.table_wrapper.api.PatternTableColumn; @@ -44,9 +45,10 @@ public CashFlowTable(PsbBrokerReport report) { } @Override - protected EventCashFlow parseRow(TableRow row) { - String action = row.getStringCellValue(OPERATION); - action = String.valueOf(action).toLowerCase().trim(); + protected @Nullable EventCashFlow parseRow(TableRow row) { + String action = row.getStringCellValue(OPERATION) + .toLowerCase() + .trim(); CashFlowType type = CashFlowType.CASH; boolean isPositive; switch (action) { @@ -66,7 +68,7 @@ protected EventCashFlow parseRow(TableRow row) { if (type == CashFlowType.CASH && !row.getStringCellValue(DESCRIPTION).isEmpty()) { return null; // cash in/out records has no description } - String description = row.getStringCellValueOrDefault(DESCRIPTION, null); + @Nullable String description = row.getStringCellValueOrDefault(DESCRIPTION, null); BigDecimal value = row.getBigDecimalCellValue(VALUE); return EventCashFlow.builder() .portfolio(getReport().getPortfolio()) diff --git a/src/main/java/ru/investbook/parser/psb/CashTable.java b/src/main/java/ru/investbook/parser/psb/CashTable.java index 958f0802..087f9b15 100644 --- a/src/main/java/ru/investbook/parser/psb/CashTable.java +++ b/src/main/java/ru/investbook/parser/psb/CashTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioCash; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -41,7 +42,7 @@ public CashTable(PsbBrokerReport report) { } @Override - protected PortfolioCash parseRow(TableRow row) { + protected @Nullable PortfolioCash parseRow(TableRow row) { return row.rowContains(INVALID_TEXT) ? null : PortfolioCash.builder() .portfolio(getReport().getPortfolio()) @@ -52,14 +53,15 @@ protected PortfolioCash parseRow(TableRow row) { .build(); } + @Getter enum CashTableHeader implements TableHeaderColumn { SECTION("сектор"), VALUE("плановый исходящий остаток"), CURRENCY("валюта"); - @Getter private final TableColumn column; - CashTableHeader(String ... words) { + + CashTableHeader(String... words) { this.column = PatternTableColumn.of(words); } } diff --git a/src/main/java/ru/investbook/parser/psb/DerivativeCashFlowTable.java b/src/main/java/ru/investbook/parser/psb/DerivativeCashFlowTable.java index 0b1f803d..2f7bb9e4 100644 --- a/src/main/java/ru/investbook/parser/psb/DerivativeCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/psb/DerivativeCashFlowTable.java @@ -21,6 +21,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.table_wrapper.api.PatternTableColumn; @@ -80,7 +81,7 @@ private static AbstractMap.SimpleEntry getCount(TableRow row) { } @Override - protected SecurityEventCashFlow parseRow(TableRow row) { + protected @Nullable SecurityEventCashFlow parseRow(TableRow row) { BigDecimal value = row.getBigDecimalCellValue(DerivativeCashFlowTableHeader.INCOMING) .subtract(row.getBigDecimalCellValue(DerivativeCashFlowTableHeader.OUTGOING)); SecurityEventCashFlow.SecurityEventCashFlowBuilder builder = SecurityEventCashFlow.builder() @@ -93,7 +94,7 @@ protected SecurityEventCashFlow parseRow(TableRow row) { case "вариационная маржа": String contract = row.getStringCellValue(DerivativeCashFlowTableHeader.CONTRACT) .split("/")[1].trim(); - Integer count = getContractCount().get(contract); + @Nullable Integer count = getContractCount().get(contract); if (count == null) { throw new IllegalArgumentException("Открытых контрактов не найдено"); } @@ -112,6 +113,7 @@ protected SecurityEventCashFlow parseRow(TableRow row) { } } + @Getter enum ContractCountTableHeader implements TableHeaderColumn { CONTRACT("^контракт$"), INCOMING("входящий остаток"), @@ -122,7 +124,6 @@ enum ContractCountTableHeader implements TableHeaderColumn { PRICE_TICK("шаг цены"), PRICE_TICK_VALUE("стоимость шага цены"); - @Getter private final TableColumn column; ContractCountTableHeader(String... words) { @@ -130,6 +131,7 @@ enum ContractCountTableHeader implements TableHeaderColumn { } } + @Getter enum DerivativeCashFlowTableHeader implements TableHeaderColumn { DATE("дата"), CONTRACT("№", "контракт"), @@ -137,7 +139,6 @@ enum DerivativeCashFlowTableHeader implements TableHeaderColumn { INCOMING("зачислено"), OUTGOING("списано"); - @Getter private final TableColumn column; DerivativeCashFlowTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/psb/DerivativeQuoteTable.java b/src/main/java/ru/investbook/parser/psb/DerivativeQuoteTable.java index e1ced79e..916a76d6 100644 --- a/src/main/java/ru/investbook/parser/psb/DerivativeQuoteTable.java +++ b/src/main/java/ru/investbook/parser/psb/DerivativeQuoteTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.psb; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.table_wrapper.api.TableRow; import ru.investbook.parser.SingleAbstractReportTable; @@ -37,7 +38,7 @@ public DerivativeQuoteTable(PsbBrokerReport report) { } @Override - protected SecurityQuote parseRow(TableRow row) { + protected @Nullable SecurityQuote parseRow(TableRow row) { BigDecimal price = row.getBigDecimalCellValue(PRICE); BigDecimal tickValue = row.getBigDecimalCellValue(PRICE_TICK_VALUE); if (price.compareTo(minValue) < 0 || tickValue.compareTo(minValue) < 0) { diff --git a/src/main/java/ru/investbook/parser/psb/ForeignExchangeRateTable.java b/src/main/java/ru/investbook/parser/psb/ForeignExchangeRateTable.java index b8bb2ae4..fbcbcf1a 100644 --- a/src/main/java/ru/investbook/parser/psb/ForeignExchangeRateTable.java +++ b/src/main/java/ru/investbook/parser/psb/ForeignExchangeRateTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.psb; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.ForeignExchangeRate; import org.spacious_team.table_wrapper.api.Table; import org.spacious_team.table_wrapper.api.TableRow; @@ -52,7 +53,7 @@ protected Collection parseTable() { protected Collection getExchangeRate(Table table) { try { - TableRow row = table.findRowByPrefix(EXCHANGE_RATE_ROW); + @Nullable TableRow row = table.findRowByPrefix(EXCHANGE_RATE_ROW); if (row == null) { return emptyList(); } diff --git a/src/main/java/ru/investbook/parser/psb/PortfolioPropertyTable.java b/src/main/java/ru/investbook/parser/psb/PortfolioPropertyTable.java index 6885556f..46d56cd3 100644 --- a/src/main/java/ru/investbook/parser/psb/PortfolioPropertyTable.java +++ b/src/main/java/ru/investbook/parser/psb/PortfolioPropertyTable.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioProperty; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.spacious_team.broker.report_parser.api.BrokerReport; @@ -71,7 +72,7 @@ public static Table getSummaryTable(BrokerReport report, String tableFooterStrin protected Collection getTotalAssets(Table table) { try { - TableRow row = table.findRowByPrefix(ASSETS); + @Nullable TableRow row = table.findRowByPrefix(ASSETS); if (row == null) { return emptyList(); } @@ -87,6 +88,7 @@ protected Collection getTotalAssets(Table table) { } } + @Getter @RequiredArgsConstructor public enum SummaryTableHeader implements TableHeaderColumn { DESCRIPTION(1), @@ -98,7 +100,6 @@ public enum SummaryTableHeader implements TableHeaderColumn { GBP(OptionalTableColumn.of(PatternTableColumn.of("GBP"))), CHF(OptionalTableColumn.of(PatternTableColumn.of("CHF"))); - @Getter private final TableColumn column; SummaryTableHeader(int columnIndex) { this.column = ConstantPositionTableColumn.of(columnIndex); diff --git a/src/main/java/ru/investbook/parser/psb/PsbBrokerReport.java b/src/main/java/ru/investbook/parser/psb/PsbBrokerReport.java index e5abfa5c..55817c72 100644 --- a/src/main/java/ru/investbook/parser/psb/PsbBrokerReport.java +++ b/src/main/java/ru/investbook/parser/psb/PsbBrokerReport.java @@ -32,7 +32,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; -import java.time.temporal.ChronoUnit; + +import static java.time.temporal.ChronoUnit.HOURS; @EqualsAndHashCode(callSuper = true) public class PsbBrokerReport extends AbstractExcelBrokerReport { @@ -40,25 +41,33 @@ public class PsbBrokerReport extends AbstractExcelBrokerReport { private static final String PORTFOLIO_MARKER = "Договор №:"; private static final String REPORT_DATE_MARKER = "ОТЧЕТ БРОКЕРА"; - private final Workbook book; - public PsbBrokerReport(String excelFileName, SecurityRegistrar securityRegistrar) throws IOException { this(Paths.get(excelFileName), securityRegistrar); } public PsbBrokerReport(Path report, SecurityRegistrar securityRegistrar) throws IOException { - this(report.getFileName().toString(), Files.newInputStream(report), securityRegistrar); + this(getFileName(report), Files.newInputStream(report), securityRegistrar); } public PsbBrokerReport(String excelFileName, InputStream is, SecurityRegistrar securityRegistrar) { - super(securityRegistrar); - this.book = getWorkBook(excelFileName, is); - ReportPage reportPage = new ExcelSheet(book.getSheetAt(0)); + super(getBrokerReportAttributes(excelFileName, is), securityRegistrar); + } + + @SuppressWarnings("nullness") + private static String getFileName(Path path) { + return path.getFileName().toString(); + } + + private static ExcelAttributes getBrokerReportAttributes(String excelFileName, InputStream is) { + Workbook workbook = getWorkBook(excelFileName, is); + ReportPage reportPage = new ExcelSheet(workbook.getSheetAt(0)); checkReportFormat(excelFileName, reportPage); - setPath(Paths.get(excelFileName)); - setReportPage(reportPage); - setPortfolio(getPortfolio(reportPage)); - setReportEndDateTime(getReportEndDateTime(reportPage)); + Attributes attributes = new Attributes( + reportPage, + excelFileName, + getReportEndDateTime(reportPage), + getPortfolio(reportPage)); + return new ExcelAttributes(workbook, attributes); } public static void checkReportFormat(String excelFileName, ReportPage reportPage) { @@ -77,19 +86,15 @@ private static String getPortfolio(ReportPage reportPage) { } } - private Instant getReportEndDateTime(ReportPage reportPage) { + private static Instant getReportEndDateTime(ReportPage reportPage) { try { String value = String.valueOf(reportPage.getNextColumnValue(REPORT_DATE_MARKER)); - return convertToInstant(value.split(" ")[3]) - .plus(LAST_TRADE_HOUR, ChronoUnit.HOURS); + String date = value.split(" ")[3]; + return convertToInstantWithRussianFormatAndMoscowZoneId(date) + .plus(LAST_TRADE_HOUR, HOURS); } catch (Exception e) { throw new IllegalArgumentException( "Не найдена дата отчета по заданному шаблону '" + REPORT_DATE_MARKER + " XXX'"); } } - - @Override - public void close() throws IOException { - this.book.close(); - } } diff --git a/src/main/java/ru/investbook/parser/psb/SecuritiesTable.java b/src/main/java/ru/investbook/parser/psb/SecuritiesTable.java index 09b5486a..517bcbfd 100644 --- a/src/main/java/ru/investbook/parser/psb/SecuritiesTable.java +++ b/src/main/java/ru/investbook/parser/psb/SecuritiesTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.Security.SecurityBuilder; import org.spacious_team.broker.pojo.SecurityType; @@ -43,7 +44,7 @@ public SecuritiesTable(PsbBrokerReport report) { } @Override - protected Security parseRow(TableRow row) { + protected @Nullable Security parseRow(TableRow row) { if (row.rowContains(INVALID_TEXT)) { return null; } @@ -56,6 +57,7 @@ protected Security parseRow(TableRow row) { return security.id(securityId).build(); } + @Getter enum SecuritiesTableHeader implements TableHeaderColumn { NAME("наименование"), ISIN("isin"), @@ -68,7 +70,6 @@ enum SecuritiesTableHeader implements TableHeaderColumn { FACEUNIT("валюта бумаги"), CURRENCY("валюта цены"); - @Getter private final TableColumn column; SecuritiesTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/psb/SecurityQuoteTable.java b/src/main/java/ru/investbook/parser/psb/SecurityQuoteTable.java index ec5f5136..8c2acbbc 100644 --- a/src/main/java/ru/investbook/parser/psb/SecurityQuoteTable.java +++ b/src/main/java/ru/investbook/parser/psb/SecurityQuoteTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.psb; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.table_wrapper.api.TableRow; @@ -38,7 +39,7 @@ public SecurityQuoteTable(PsbBrokerReport report) { } @Override - protected SecurityQuote parseRow(TableRow row) { + protected @Nullable SecurityQuote parseRow(TableRow row) { if (row.rowContains(SecuritiesTable.INVALID_TEXT)) { return null; } @@ -48,9 +49,9 @@ protected SecurityQuote parseRow(TableRow row) { } String isin = row.getStringCellValue(ISIN); BigDecimal amount = row.getBigDecimalCellValue(AMOUNT); - BigDecimal price = amount.divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP); + @Nullable BigDecimal price = amount.divide(BigDecimal.valueOf(count), 4, RoundingMode.HALF_UP); BigDecimal quote = row.getBigDecimalCellValue(QUOTE); - BigDecimal accruedInterest = row.getBigDecimalCellValue(ACCRUED_INTEREST) + @Nullable BigDecimal accruedInterest = row.getBigDecimalCellValue(ACCRUED_INTEREST) .divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP); String currency = row.getStringCellValue(CURRENCY); if (accruedInterest.compareTo(minValue) < 0 && price.subtract(quote).abs().compareTo(minValue) < 0) { diff --git a/src/main/java/ru/investbook/parser/psb/SecurityTransactionTable.java b/src/main/java/ru/investbook/parser/psb/SecurityTransactionTable.java index c282ae66..ce4b048a 100644 --- a/src/main/java/ru/investbook/parser/psb/SecurityTransactionTable.java +++ b/src/main/java/ru/investbook/parser/psb/SecurityTransactionTable.java @@ -45,7 +45,7 @@ @Slf4j public class SecurityTransactionTable extends SingleInitializableReportTable { - // Использовать в том числе таблицы "... рассчитанные в отчетном периоде", + // Использовать, в том числе таблицы "... рассчитанные в отчетном периоде", // иначе в расчет не возьмутся позиции, закрытые в течении T+2 (до исполнения), // в годовых же отчетах сделка уже не встретится в таблице нерассчитанных сделок. private static final String[] TABLE_NAMES = { @@ -92,11 +92,13 @@ private SecurityTransaction getTransaction(TableRow row) { .add(row.getBigDecimalCellValue(ITS_COMMISSION)) .negate(); Security security = getSecurity(row); + @SuppressWarnings({"nullable", "DataFlowIssue"}) + int securityId = security.getId(); return SecurityTransaction.builder() .timestamp(getReport().convertToInstant(row.getStringCellValue(DATE_TIME))) .tradeId(String.valueOf(row.getLongCellValue(TRADE_ID))) // may be double numbers in future .portfolio(getReport().getPortfolio()) - .security(security.getId()) + .security(securityId) .count((isBuy ? 1 : -1) * row.getIntCellValue(COUNT)) .value(value) .accruedInterest((accruedInterest.abs().compareTo(minValue) >= 0) ? accruedInterest : BigDecimal.ZERO) @@ -123,6 +125,7 @@ public Set getSecurities() { return Set.copyOf(securities); } + @Getter enum TransactionTableHeader implements TableHeaderColumn { DATE_TIME(PatternTableColumn.of("дата", "исполнения"), PatternTableColumn.of("дата и время")), TRADE_ID("номер сделки"), @@ -139,7 +142,6 @@ enum TransactionTableHeader implements TableHeaderColumn { BROKER_COMMISSION("ком", "брокера"), COMMISSION_CURRENCY("валюта", "брок", "комиссии"); - @Getter private final TableColumn column; TransactionTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashFlowTable.java b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashFlowTable.java index 732fc6c0..86da4e5d 100644 --- a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashFlowTable.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.table_wrapper.api.OptionalTableColumn; @@ -34,6 +35,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Optional; import static java.util.Collections.emptyList; @@ -62,14 +64,14 @@ protected Collection parseTable(Table table) { @Override protected Collection parseRowToCollection(TableRow row) { String action = row.getStringCellValue(OPERATION); - action = String.valueOf(action).toLowerCase().trim(); + action = action.toLowerCase().trim(); boolean isPositive; switch (action) { case "ввод дс" -> isPositive = true; case "вывод дс" -> isPositive = false; default -> { log.debug("Не известный тип операции '{}' в таблице '{}'", action, row.getTable()); - return null; + return List.of(); } } EventCashFlow.EventCashFlowBuilder builder = EventCashFlow.builder() @@ -96,6 +98,7 @@ protected Collection parseRowToCollection(TableRow row) { } private Optional getBigDecimal(TableRow row, FxCashFlowTableHeader column, boolean isPositive) { + //noinspection DataFlowIssue return Optional.ofNullable(row.getBigDecimalCellValueOrDefault(column, null)) .filter(value -> value.compareTo(min) > 0) .map(value -> isPositive ? value : value.negate()); @@ -103,7 +106,7 @@ private Optional getBigDecimal(TableRow row, FxCashFlowTableHeader c private Collection getDailyBrokerCommission() { try { - Object value = getReport().getReportPage().getNextColumnValue(BROKER_FEE); + @Nullable Object value = getReport().getReportPage().getNextColumnValue(BROKER_FEE); double doubleValue = Double.parseDouble(String.valueOf(value)); if (doubleValue > 0.01) { BigDecimal brokerFee = BigDecimal.valueOf(doubleValue).negate(); @@ -131,6 +134,7 @@ protected Collection mergeDuplicates(EventCashFlow old, EventCash return EventCashFlow.mergeDuplicates(old, nw); } + @Getter @RequiredArgsConstructor enum FxCashFlowTableHeader implements TableHeaderColumn { DATE("дата"), @@ -141,9 +145,9 @@ enum FxCashFlowTableHeader implements TableHeaderColumn { VALUE(OptionalTableColumn.of(PatternTableColumn.of("Сумма"))), // new format CURRENCY(OptionalTableColumn.of(PatternTableColumn.of("Валюта"))); // new format - @Getter private final TableColumn column; - FxCashFlowTableHeader(String ... words) { + + FxCashFlowTableHeader(String... words) { this.column = PatternTableColumn.of(words); } } diff --git a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashTable.java b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashTable.java index e2d5c981..93e0beeb 100644 --- a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashTable.java +++ b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeCashTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.psb.foreignmarket; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioCash; import org.spacious_team.table_wrapper.api.Table; import org.spacious_team.table_wrapper.api.TableRow; @@ -47,13 +48,13 @@ public ForeignExchangeCashTable(SingleBrokerReport report) { @Override protected Collection parseTable() { Table table = getSummaryTable(); - TableRow row = table.findRowByPrefix(ASSETS); + @Nullable TableRow row = table.findRowByPrefix(ASSETS); if (row == null) { return emptyList(); } Collection cashes = new ArrayList<>(); for (PortfolioPropertyTable.SummaryTableHeader currency : CURRENCIES) { - BigDecimal cash = row.getBigDecimalCellValueOrDefault(currency, null); + @Nullable BigDecimal cash = row.getBigDecimalCellValueOrDefault(currency, null); if (cash != null) { cashes.add(PortfolioCash.builder() .portfolio(getReport().getPortfolio()) diff --git a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangePortfolioPropertyTable.java b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangePortfolioPropertyTable.java index e90e473e..ec647f6a 100644 --- a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangePortfolioPropertyTable.java +++ b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangePortfolioPropertyTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.psb.foreignmarket; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioProperty; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.spacious_team.table_wrapper.api.Table; @@ -50,8 +51,8 @@ protected Table getSummaryTable() { @Override protected Collection getTotalAssets(Table table) { try { - TableRow assetsRow = table.findRowByPrefix(ASSETS); - TableRow exchangeRateRow = table.findRowByPrefix(ForeignExchangeRateTable.EXCHANGE_RATE_ROW); + @Nullable TableRow assetsRow = table.findRowByPrefix(ASSETS); + @Nullable TableRow exchangeRateRow = table.findRowByPrefix(ForeignExchangeRateTable.EXCHANGE_RATE_ROW); if (assetsRow == null || exchangeRateRow == null) { return emptyList(); } diff --git a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeTransactionTable.java b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeTransactionTable.java index f79f1281..2fd0766e 100644 --- a/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeTransactionTable.java +++ b/src/main/java/ru/investbook/parser/psb/foreignmarket/ForeignExchangeTransactionTable.java @@ -68,6 +68,7 @@ protected ForeignExchangeTransaction parseRow(TableRow row) { .build(); } + @Getter enum FxTransactionTableHeader implements TableHeaderColumn { TRADE_ID("номер сделки"), DATE_TIME("дата", "заключения сделки"), // учет по дате сделки, а не дате исполнения, чтобы учесть неисполненные сделки @@ -78,7 +79,6 @@ enum FxTransactionTableHeader implements TableHeaderColumn { MARKET_COMMISSION("комиссия", "биржи", "руб"), POSITION_SWAP("перенос", "позиции"); - @Getter private final TableColumn column; FxTransactionTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java b/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java index c9de2950..2615ef37 100644 --- a/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java +++ b/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java @@ -31,12 +31,14 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Paths; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; +import java.util.function.Function; + +import static java.time.temporal.ChronoUnit.HOURS; @EqualsAndHashCode(callSuper = true) public class PsbBrokerForeignMarketReport extends AbstractBrokerReport { @@ -47,14 +49,18 @@ public class PsbBrokerForeignMarketReport extends AbstractBrokerReport { private static final String REPORT_DATE_MARKER = "ОТЧЕТ БРОКЕРА"; public PsbBrokerForeignMarketReport(String excelFileName, InputStream is, SecurityRegistrar securityRegistrar) { - super(securityRegistrar); + super(getBrokerReportAttributes(excelFileName, is), securityRegistrar); + } + + private static Attributes getBrokerReportAttributes(String excelFileName, InputStream is) { Workbook book = getWorkbook(is); ReportPage reportPage = new XmlReportPage(book.getWorksheetAt(0)); PsbBrokerReport.checkReportFormat(excelFileName, reportPage); - setPath(Paths.get(excelFileName)); - setReportPage(reportPage); - setPortfolio(getPortfolio(reportPage)); - setReportEndDateTime(getReportEndDateTime(reportPage)); + return new Attributes( + reportPage, + excelFileName, + getReportEndDateTime(reportPage), + getPortfolio(reportPage)); } private static Workbook getWorkbook(InputStream is) { @@ -88,11 +94,12 @@ private static String getPortfolio(ReportPage reportPage) { } } - private Instant getReportEndDateTime(ReportPage reportPage) { + private static Instant getReportEndDateTime(ReportPage reportPage) { try { String value = String.valueOf(reportPage.getNextColumnValue(REPORT_DATE_MARKER)); - return convertToInstant(value.split(" ")[3]) - .plus(LAST_TRADE_HOUR, ChronoUnit.HOURS); + String date = value.split(" ")[3]; + return convertToInstantWithRussianFormatAndMoscowZoneId(date) + .plus(LAST_TRADE_HOUR, HOURS); } catch (Exception e) { throw new IllegalArgumentException( "Не найдена дата отчета по заданному шаблону '" + REPORT_DATE_MARKER + " XXX'"); @@ -101,14 +108,26 @@ private Instant getReportEndDateTime(ReportPage reportPage) { @Override public Instant convertToInstant(String value) { + return convertXmlDateTimeToInstant(value, getReportZoneId(), super::convertToInstant); + } + + protected static Instant convertToInstantWithRussianFormatAndMoscowZoneId(String value) { + return convertXmlDateTimeToInstant(value, + MOSCOW_ZONEID, + AbstractBrokerReport::convertToInstantWithRussianFormatAndMoscowZoneId); + } + + private static Instant convertXmlDateTimeToInstant(String value, + ZoneId zoneId, + Function defaultConverter) { value = value.trim(); if (value.contains("/")) { return LocalDate.parse(value, PsbBrokerForeignMarketReport.dateFormatterWithSlash) - .atStartOfDay(getReportZoneId()).toInstant(); + .atStartOfDay(zoneId).toInstant(); } else if (value.contains(":") && value.length() == 16) { - return LocalDateTime.parse(value, dateTimeFormatter).atZone(getReportZoneId()).toInstant(); + return LocalDateTime.parse(value, dateTimeFormatter).atZone(zoneId).toInstant(); } else { - return super.convertToInstant(value); + return defaultConverter.apply(value); } } diff --git a/src/main/java/ru/investbook/parser/sber/SecurityHelper.java b/src/main/java/ru/investbook/parser/sber/SecurityHelper.java index 7592dd0e..9cc54516 100644 --- a/src/main/java/ru/investbook/parser/sber/SecurityHelper.java +++ b/src/main/java/ru/investbook/parser/sber/SecurityHelper.java @@ -19,44 +19,57 @@ package ru.investbook.parser.sber; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityType; -import org.springframework.util.StringUtils; import ru.investbook.parser.SecurityRegistrar; +import java.util.Optional; + +import static org.springframework.util.StringUtils.hasLength; import static ru.investbook.entity.SecurityEntity.isinPattern; @Slf4j public class SecurityHelper { - public static Security getSecurity(String code, String securityName, String section, String type, SecurityRegistrar securityRegistrar) { - String securityId = getSecurityId(code, section); + public static Security getSecurity(String code, @Nullable String securityName, String section, @Nullable String type, + SecurityRegistrar securityRegistrar) { + @Nullable String securityId = getSecurityId(code, section); SecurityType securityType = getSecurityType(section, type); Security.SecurityBuilder security = Security.builder().type(securityType); - int id = -1; - switch (securityType) { + int id = switch (securityType) { case STOCK, BOND, STOCK_OR_BOND -> { security .isin(securityId) .name(securityName); - id = (securityId == null) ? - securityRegistrar.declareStockOrBondByName(securityName, () -> security) : - securityRegistrar.declareStockOrBondByIsin(securityId, () -> security); + if (securityId == null) { + yield Optional.ofNullable(securityName) + .map(name -> securityRegistrar.declareStockOrBondByName(name, () -> security)) + .orElseThrow(); + } else { + yield securityRegistrar.declareStockOrBondByIsin(securityId, () -> security); + } } case DERIVATIVE -> { security.ticker(securityId); - id = securityRegistrar.declareDerivative(securityId); + yield Optional.ofNullable(securityId) + .map(securityRegistrar::declareDerivative) + .orElseThrow(); } case CURRENCY_PAIR -> { security.ticker(securityId); - id = securityRegistrar.declareCurrencyPair(securityId); + yield Optional.ofNullable(securityId) + .map(securityRegistrar::declareCurrencyPair) + .orElseThrow(); } case ASSET -> { security.name(securityId); - id = securityRegistrar.declareAsset(securityId, () -> security); + yield Optional.ofNullable(securityId) + .map(asset -> securityRegistrar.declareAsset(asset, () -> security)) + .orElseThrow(); } - } + }; return security.id(id).build(); } @@ -64,7 +77,7 @@ public static Security getSecurity(String code, String securityName, String sect * @param nameAndIsin in format "\s*()" * @return isin for stock or bond, nameAndIsin for others */ - private static String getSecurityId(String nameAndIsin, String section) { + private static @Nullable String getSecurityId(String nameAndIsin, String section) { int start = nameAndIsin.indexOf('(') + 1; int end = nameAndIsin.indexOf(')'); String id = nameAndIsin.substring(start, (end == -1) ? nameAndIsin.length() : end); @@ -74,8 +87,8 @@ private static String getSecurityId(String nameAndIsin, String section) { return id; } - private static String getValidIsinOrNull(String isin) { - return StringUtils.hasLength(isin) && isinPattern.matcher(isin).matches() ? + private static @Nullable String getValidIsinOrNull(String isin) { + return hasLength(isin) && isinPattern.matcher(isin).matches() ? isin : null; } @@ -85,7 +98,7 @@ public static String getSecurityName(String nameAndIsin) { return (start == -1) ? nameAndIsin : nameAndIsin.substring(0, start).trim(); } - public static SecurityType getSecurityType(String section, String securityType) { + public static SecurityType getSecurityType(String section, @Nullable String securityType) { if ("Фондовый рынок".equalsIgnoreCase(section)) { if (securityType == null) return SecurityType.STOCK_OR_BOND; return switch (securityType.toLowerCase()) { diff --git a/src/main/java/ru/investbook/parser/sber/cash_security/SberCashBrokerReport.java b/src/main/java/ru/investbook/parser/sber/cash_security/SberCashBrokerReport.java index 4c81134f..7a1ec796 100644 --- a/src/main/java/ru/investbook/parser/sber/cash_security/SberCashBrokerReport.java +++ b/src/main/java/ru/investbook/parser/sber/cash_security/SberCashBrokerReport.java @@ -42,9 +42,12 @@ public SberCashBrokerReport(String excelFileName, Workbook book) { public static void checkReportFormat(String excelFileName, ExcelSheet reportPage) { Sheet sheet = reportPage.getSheet(); - if (sheet.getSheetName().equals("Движение ДС") && - reportPage.getRow(0).getCell(0).getStringValue().equals("Номер договора")) { - return; + if (sheet.getSheetName().equals("Движение ДС")) { + @SuppressWarnings({"nullable", "DataFlowIssue"}) + String string = reportPage.getRow(0).getCell(0).getStringValue(); + if (string.equals("Номер договора")) { + return; + } } throw new RuntimeException("В файле " + excelFileName + " не содержится отчета движения ДС брокера Сбербанк"); } diff --git a/src/main/java/ru/investbook/parser/sber/cash_security/SberCashFlowTable.java b/src/main/java/ru/investbook/parser/sber/cash_security/SberCashFlowTable.java index 448fb5ac..3534f1f2 100644 --- a/src/main/java/ru/investbook/parser/sber/cash_security/SberCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/sber/cash_security/SberCashFlowTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.report_parser.api.AbstractReportTable; @@ -43,7 +44,7 @@ protected SberCashFlowTable(SberCashBrokerReport report) { } @Override - protected EventCashFlow parseRow(TableRow row) { + protected @Nullable EventCashFlow parseRow(TableRow row) { if (!"Исполнено".equalsIgnoreCase(row.getStringCellValueOrDefault(STATUS, null))) { return null; } @@ -88,6 +89,7 @@ protected EventCashFlow parseRow(TableRow row) { .build(); } + @Getter enum SberCashFlowTableHeader implements TableHeaderColumn { PORTFOLIO("Номер договора"), DATE_TIME("Дата исполнения поручения"), @@ -99,8 +101,8 @@ enum SberCashFlowTableHeader implements TableHeaderColumn { DESCRIPTION("Содержание операции"), STATUS("Статус"); - @Getter private final TableColumn column; + SberCashFlowTableHeader(String words) { this.column = PatternTableColumn.of(words); } diff --git a/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepositBrokerReport.java b/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepositBrokerReport.java index 4125c6db..96269345 100644 --- a/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepositBrokerReport.java +++ b/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepositBrokerReport.java @@ -48,9 +48,12 @@ public SberSecurityDepositBrokerReport(String excelFileName, Workbook book, Secu public static void checkReportFormat(String excelFileName, ExcelSheet reportPage) { Sheet sheet = reportPage.getSheet(); - if (sheet.getSheetName().equals("Движение ЦБ") && - reportPage.getRow(0).getCell(0).getStringValue().equals("Номер договора")) { - return; + if (sheet.getSheetName().equals("Движение ЦБ")) { + @SuppressWarnings({"nullable", "DataFlowIssue"}) + String string = reportPage.getRow(0).getCell(0).getStringValue(); + if (string.equals("Номер договора")) { + return; + } } throw new RuntimeException("В файле " + excelFileName + " не содержится отчета движения ЦБ брокера Сбербанк"); } diff --git a/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepsitAndWithdrawalTable.java b/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepsitAndWithdrawalTable.java index fc3ff293..5ab737f4 100644 --- a/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepsitAndWithdrawalTable.java +++ b/src/main/java/ru/investbook/parser/sber/cash_security/SberSecurityDepsitAndWithdrawalTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.report_parser.api.AbstractReportTable; import org.spacious_team.broker.report_parser.api.AbstractTransaction; @@ -49,7 +50,7 @@ protected SberSecurityDepsitAndWithdrawalTable(SberSecurityDepositBrokerReport r } @Override - protected SecurityTransaction parseRow(TableRow row) { + protected @Nullable SecurityTransaction parseRow(TableRow row) { if (!"Исполнено".equalsIgnoreCase(row.getStringCellValueOrDefault(STATUS, null))) { return null; } else if ("Погашение ценной бумаги".equalsIgnoreCase(row.getStringCellValueOrDefault(DESCRIPTION, null))) { @@ -74,20 +75,23 @@ protected SecurityTransaction parseRow(TableRow row) { null, report.getSecurityRegistrar()); + @SuppressWarnings({"nullable", "DataFlowIssue"}) + int securityId = security.getId(); return SecurityTransaction.builder() - .tradeId(generateTradeId(portfolio, instant, security.getId())) + .tradeId(generateTradeId(portfolio, instant, securityId)) .timestamp(instant) .portfolio(portfolio) - .security(security.getId()) + .security(securityId) .count(count) .build(); } - private static String generateTradeId(String portfolio, Instant instant, Integer securityId) { + private static String generateTradeId(String portfolio, Instant instant, int securityId) { String id = instant.getEpochSecond() + securityId + portfolio; return id.substring(0, Math.min(32, id.length())); } + @Getter enum SberSecurityDepositAndWithdrawalTableHeader implements TableHeaderColumn { PORTFOLIO("Номер договора"), DATE_TIME("Дата исполнения поручения"), @@ -99,8 +103,8 @@ enum SberSecurityDepositAndWithdrawalTableHeader implements TableHeaderColumn { DESCRIPTION("Содержание операции"), STATUS("Статус"); - @Getter private final TableColumn column; + SberSecurityDepositAndWithdrawalTableHeader(String words) { this.column = PatternTableColumn.of(words); } diff --git a/src/main/java/ru/investbook/parser/sber/transaction/SberTrBrokerReport.java b/src/main/java/ru/investbook/parser/sber/transaction/SberTrBrokerReport.java index 0eac651b..26a9570d 100644 --- a/src/main/java/ru/investbook/parser/sber/transaction/SberTrBrokerReport.java +++ b/src/main/java/ru/investbook/parser/sber/transaction/SberTrBrokerReport.java @@ -53,9 +53,12 @@ public SberTrBrokerReport(String excelFileName, InputStream is, SecurityRegistra } public static void checkReportFormat(String excelFileName, ExcelSheet reportPage) { - if (reportPage.getSheet().getSheetName().equals("Сделки") && - reportPage.getRow(0).getCell(0).getStringValue().equals("Номер договора")) { - return; + if (reportPage.getSheet().getSheetName().equals("Сделки")) { + @SuppressWarnings({"nullable", "DataFlowIssue"}) + String string = reportPage.getRow(0).getCell(0).getStringValue(); + if (string.equals("Номер договора")) { + return; + } } throw new RuntimeException("В файле " + excelFileName + " не содержится отчета сделок брокера Сбербанк"); } diff --git a/src/main/java/ru/investbook/parser/sber/transaction/SberTrSecurityTransactionTable.java b/src/main/java/ru/investbook/parser/sber/transaction/SberTrSecurityTransactionTable.java index 0a6d2127..20708087 100644 --- a/src/main/java/ru/investbook/parser/sber/transaction/SberTrSecurityTransactionTable.java +++ b/src/main/java/ru/investbook/parser/sber/transaction/SberTrSecurityTransactionTable.java @@ -64,11 +64,13 @@ protected SecurityTransaction parseRow(TableRow row) { row.getStringCellValue(SECURITY_TYPE), report.getSecurityRegistrar()); + @SuppressWarnings({"nullable", "DataFlowIssue"}) + int securityId = security.getId(); return SecurityTransaction.builder() .portfolio(row.getStringCellValue(PORTFOLIO)) .timestamp(row.getInstantCellValue(DATE_TIME)) .tradeId(String.valueOf(row.getLongCellValue(TRADE_ID))) // may be double numbers in future - .security(security.getId()) + .security(securityId) .count(row.getIntCellValue(COUNT) * (isBuy ? 1 : -1)) .value(value) .accruedInterest(accruedInterest) @@ -80,6 +82,7 @@ protected SecurityTransaction parseRow(TableRow row) { .build(); } + @Getter public enum SberTransactionTableHeader implements TableHeaderColumn { PORTFOLIO("Номер договора"), TRADE_ID("Номер сделки"), @@ -96,7 +99,6 @@ public enum SberTransactionTableHeader implements TableHeaderColumn { CURRENCY("Валюта"), EXCHANGE_RATE("Курс"); // обменный курс валюты? "1" для CURRENCY = RUB - @Getter private final TableColumn column; SberTransactionTableHeader(String words) { diff --git a/src/main/java/ru/investbook/parser/tinkoff/SecurityCodeAndIsinTable.java b/src/main/java/ru/investbook/parser/tinkoff/SecurityCodeAndIsinTable.java index f424d65d..985f753a 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/SecurityCodeAndIsinTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/SecurityCodeAndIsinTable.java @@ -18,9 +18,9 @@ package ru.investbook.parser.tinkoff; -import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityType; import org.spacious_team.broker.report_parser.api.AbstractReportTable; import org.spacious_team.broker.report_parser.api.BrokerReport; @@ -54,8 +54,8 @@ protected SecurityCodeAndIsinTable(BrokerReport report) { } @Override - protected Void parseRow(TableRow row) { - String code = row.getStringCellValueOrDefault(CODE, null); + protected @Nullable Void parseRow(TableRow row) { + @Nullable String code = row.getStringCellValueOrDefault(CODE, null); if (hasLength(code) && !code.contains("Код актива")) { // exclude table's empty row // если колонка ISIN отсутствует, то ISIN используется в отчете вместо кода (SBERP) String isin = row.getStringCellValueOrDefault(ISIN, code); @@ -72,12 +72,12 @@ protected Void parseRow(TableRow row) { } codeToType.put(code, securityType); - BigDecimal faceValue = row.getBigDecimalCellValueOrDefault(FACE_VALUE, null); + @Nullable BigDecimal faceValue = row.getBigDecimalCellValueOrDefault(FACE_VALUE, null); if (faceValue != null) { codeToFaceValue.put(code, faceValue); } - String shortName = row.getStringCellValueOrDefault(SHORT_NAME, null); + String shortName = row.getStringCellValueOrDefault(SHORT_NAME, ""); if (hasLength(shortName)) { shortNameToCode.put(shortName, code); } @@ -85,45 +85,42 @@ protected Void parseRow(TableRow row) { return null; } - @NotNull public String getIsin(String code, String shortName) { initializeIfNeed(); - String isin = codeToIsin.get(code); + @Nullable String isin = codeToIsin.get(code); if (isin == null) { - code = shortNameToCode.get(shortName); - isin = codeToIsin.get(code); + String codeFromName = shortNameToCode.getOrDefault(shortName, ""); + isin = codeToIsin.get(codeFromName); } return requireNonNull(isin, "Не найден ISIN"); } - @NotNull public SecurityType getSecurityType(String code, String shortName) { initializeIfNeed(); - SecurityType type = codeToType.get(code); + @Nullable SecurityType type = codeToType.get(code); if (type == null) { - code = shortNameToCode.get(shortName); - type = codeToType.get(code); + String codeFromName = shortNameToCode.getOrDefault(shortName, ""); + type = codeToType.get(codeFromName); } return requireNonNull(type, "Не найден тип ценной бумаги"); } - @NotNull public BigDecimal getFaceValue(String code, String shortName) { initializeIfNeed(); - BigDecimal faceValue = codeToFaceValue.get(code); + @Nullable BigDecimal faceValue = codeToFaceValue.get(code); if (faceValue == null) { - code = shortNameToCode.get(shortName); - faceValue = codeToFaceValue.get(code); + String codeFromName = shortNameToCode.getOrDefault(shortName, ""); + faceValue = codeToFaceValue.get(codeFromName); } return requireNonNull(faceValue, "Не найдена номинальная стоимость облигации"); } - @NotNull public String getCode(String shortName) { initializeIfNeed(); return requireNonNull(shortNameToCode.get(shortName), "Не найден код бумаги"); } + @Getter @RequiredArgsConstructor protected enum SecurityAndCodeTableHeader implements TableHeaderColumn { SHORT_NAME("Сокращенное", "наименование"), @@ -132,7 +129,6 @@ protected enum SecurityAndCodeTableHeader implements TableHeaderColumn { TYPE("^Тип$"), FACE_VALUE(optional("Номинал")); - @Getter private final TableColumn column; SecurityAndCodeTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReport.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReport.java index 5079a47e..a4fae10f 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReport.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReport.java @@ -20,20 +20,19 @@ import lombok.EqualsAndHashCode; import org.apache.poi.ss.usermodel.Workbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.ReportPage; import org.spacious_team.table_wrapper.api.TableCellAddress; import org.spacious_team.table_wrapper.excel.ExcelSheet; import ru.investbook.parser.AbstractExcelBrokerReport; import ru.investbook.parser.SecurityRegistrar; -import java.io.IOException; import java.io.InputStream; -import java.nio.file.Paths; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.function.Predicate; import java.util.regex.Pattern; +import static java.time.temporal.ChronoUnit.HOURS; import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; import static ru.investbook.parser.tinkoff.TinkoffBrokerReportHelper.removePageNumRows; @@ -41,25 +40,28 @@ public class TinkoffBrokerReport extends AbstractExcelBrokerReport { static final Pattern tablesLastRowPattern = Pattern.compile("^[0-9]+\\.[0-9]+\\s+\\b", UNICODE_CHARACTER_CLASS); private static final String PORTFOLIO_MARKER = "Инвестор:"; - private static final Predicate tinkoffReportPredicate = cell -> + private static final Predicate<@Nullable Object> tinkoffReportPredicate = cell -> (cell instanceof String) && ((String) cell).contains("Тинькофф"); - private static final Predicate tbankReportPredicate = cell -> + private static final Predicate<@Nullable Object> tbankReportPredicate = cell -> (cell instanceof String) && ((String) cell).contains("ТБанк"); - private static final Predicate dateMarkerPredicate = cell -> + private static final Predicate<@Nullable Object> dateMarkerPredicate = cell -> (cell instanceof String) && ((String) cell).contains("за период"); - private final Workbook book; - public TinkoffBrokerReport(String excelFileName, InputStream is, SecurityRegistrar securityRegistrar) { - super(securityRegistrar); - this.book = getWorkBook(excelFileName, is); - ExcelSheet reportPage = new ExcelSheet(book.getSheetAt(0)); + super(getBrokerReportAttributes(excelFileName, is), securityRegistrar); + } + + private static ExcelAttributes getBrokerReportAttributes(String excelFileName, InputStream is) { + Workbook workbook = getWorkBook(excelFileName, is); + ExcelSheet reportPage = new ExcelSheet(workbook.getSheetAt(0)); checkReportFormat(excelFileName, reportPage); - setPath(Paths.get(excelFileName)); - setReportPage(reportPage); - setPortfolio(getPortfolio(reportPage)); - setReportEndDateTime(getReportEndDateTime(reportPage)); removePageNumRows(reportPage); + Attributes attributes = new Attributes( + reportPage, + excelFileName, + getReportEndDateTime(reportPage), + getPortfolio(reportPage)); + return new ExcelAttributes(workbook, attributes); } public static void checkReportFormat(String excelFileName, ReportPage reportPage) { @@ -69,34 +71,32 @@ public static void checkReportFormat(String excelFileName, ReportPage reportPage } } + @SuppressWarnings({"nullness", "DataFlowIssue"}) private static String getPortfolio(ReportPage reportPage) { try { return reportPage.getCell(reportPage.findByPrefix(PORTFOLIO_MARKER)) .getStringValue() .split("/")[1] .trim() - .split("\s+")[0]; + .split("\\s+")[0]; } catch (Exception e) { throw new IllegalArgumentException( "В отчете не найден номер договора по заданному шаблону '" + PORTFOLIO_MARKER); } } - private Instant getReportEndDateTime(ReportPage reportPage) { + private static Instant getReportEndDateTime(ReportPage reportPage) { try { TableCellAddress address = reportPage.find(0, 10, dateMarkerPredicate); + @SuppressWarnings({"nullness", "DataFlowIssue"}) String[] words = reportPage.getCell(address) .getStringValue() .split("-"); String value = words[words.length - 1].trim(); - return convertToInstant(value).plus(LAST_TRADE_HOUR, ChronoUnit.HOURS); + return convertToInstantWithRussianFormatAndMoscowZoneId(value) + .plus(LAST_TRADE_HOUR, HOURS); } catch (Exception e) { throw new IllegalArgumentException("Не найдена дата отчета по заданному шаблону"); } } - - @Override - public void close() throws IOException { - this.book.close(); - } } diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReportHelper.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReportHelper.java index 9d4ab5f6..13a3b18b 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReportHelper.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffBrokerReportHelper.java @@ -21,6 +21,7 @@ import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.util.CellRangeAddress; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.ReportPage; import org.spacious_team.table_wrapper.excel.ExcelSheet; import org.springframework.util.StringUtils; @@ -66,7 +67,7 @@ private static boolean isTableHeader(ReportPage reportPage, int rowNum) { TinkoffBrokerReportHelper::isCellContainsTableName) != NOT_FOUND; } - private static boolean isCellContainsTableName(Object cell) { + private static boolean isCellContainsTableName(@Nullable Object cell) { return (cell instanceof String) && !cell.toString().isEmpty() && tableNamePattern.matcher(cell.toString()).lookingAt(); diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTable.java index 9f3e75e5..08d94e25 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.table_wrapper.api.PatternTableColumn; @@ -36,7 +37,7 @@ public class TinkoffCashFlowTable extends SingleAbstractReportTable { - private String currency = null; + private String currency = ""; protected TinkoffCashFlowTable(SingleBrokerReport report) { super(report, @@ -48,9 +49,9 @@ protected TinkoffCashFlowTable(SingleBrokerReport report) { } @Override - protected EventCashFlow parseRow(TableRow row) { + protected @Nullable EventCashFlow parseRow(TableRow row) { currency = TinkoffCashFlowTableHelper.getCurrency(row, currency); - if (currency == null) { + if (!hasLength(currency)) { return null; } String operationOriginal = row.getStringCellValueOrDefault(OPERATION, ""); @@ -86,7 +87,7 @@ protected EventCashFlow parseRow(TableRow row) { } private EventCashFlow.EventCashFlowBuilder getBuilder(TableRow row, String currency) { - String description = row.getStringCellValueOrDefault(DESCRIPTION, null); + @Nullable String description = row.getStringCellValueOrDefault(DESCRIPTION, null); return EventCashFlow.builder() .portfolio(getReport().getPortfolio()) .timestamp(getReport().convertToInstant(row.getStringCellValue(DATE))) diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTableHelper.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTableHelper.java index 23759af0..f755af8a 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTableHelper.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashFlowTableHelper.java @@ -29,7 +29,7 @@ class TinkoffCashFlowTableHelper { static String getCurrency(TableRow row, String defaultCurrency) { boolean isCurrencyHeader = !hasLength(row.getStringCellValueOrDefault(DATE, null)); if (isCurrencyHeader) { - String currency = row.getStringCellValueOrDefault(CURRENCY, null); + String currency = row.getStringCellValueOrDefault(CURRENCY, ""); if (hasLength(currency) && currency.length() == 3) { // RUB, USD, ... (ISO format) return currency.toUpperCase(); } diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashTable.java index 592a8fe5..a4408449 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffCashTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioCash; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -40,12 +41,12 @@ protected TinkoffCashTable(SingleBrokerReport report) { } @Override - protected PortfolioCash parseRow(TableRow row) { - BigDecimal value = row.getBigDecimalCellValueOrDefault(CashTableHeader.VALUE, null); + protected @Nullable PortfolioCash parseRow(TableRow row) { + @Nullable BigDecimal value = row.getBigDecimalCellValueOrDefault(CashTableHeader.VALUE, null); if (value == null) { return null; } - String currency = row.getStringCellValue(CashTableHeader.CURRENCY); + @Nullable String currency = row.getStringCellValue(CashTableHeader.CURRENCY); if (currency == null || currency.length() != 3) { return null; // неизвестный контракт GLD_MOEX указывается в качестве валюты } @@ -58,12 +59,12 @@ protected PortfolioCash parseRow(TableRow row) { .build(); } + @Getter @RequiredArgsConstructor protected enum CashTableHeader implements TableHeaderColumn { CURRENCY("Валюта"), VALUE("Исходящий", "остаток", "на конец", "периода"); - @Getter private final TableColumn column; CashTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffDepositAndWithdrawalTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffDepositAndWithdrawalTable.java index 4c510a42..24061978 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffDepositAndWithdrawalTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffDepositAndWithdrawalTable.java @@ -18,7 +18,6 @@ package ru.investbook.parser.tinkoff; -import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.spacious_team.broker.pojo.Security; @@ -64,7 +63,6 @@ protected TinkoffDepositAndWithdrawalTable(SingleBrokerReport report, this.transactionWithdrawal = getCounter(transactions, t -> t.getCount() < 0); } - @NotNull private static Map getCounter(ReportTable transactions, Predicate filter) { return transactions.getData() diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffDerivativeCashFlowTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffDerivativeCashFlowTable.java index 6a8da35c..e2c184ce 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffDerivativeCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffDerivativeCashFlowTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.table_wrapper.api.PatternTableColumn; @@ -42,8 +43,8 @@ protected TinkoffDerivativeCashFlowTable(SingleBrokerReport report) { } @Override - protected SecurityEventCashFlow parseRow(TableRow row) { - String contract = row.getStringCellValueOrDefault(CONTRACT, null); + protected @Nullable SecurityEventCashFlow parseRow(TableRow row) { + @Nullable String contract = row.getStringCellValueOrDefault(CONTRACT, null); if (!hasLength(contract) || contract.contains("Наименование")) { return null; } @@ -61,6 +62,7 @@ protected SecurityEventCashFlow parseRow(TableRow row) { .build(); } + @Getter @RequiredArgsConstructor protected enum DerivativeCashFlowTableHeader implements TableHeaderColumn { DATE("Дата"), @@ -69,7 +71,6 @@ protected enum DerivativeCashFlowTableHeader implements TableHeaderColumn { OUTGOING_COUNT("Позиция", "на", "конец", "дня"), VARIATION_MARGIN("Вар", "маржа", "на", "конец", "дня"); - @Getter private final TableColumn column; DerivativeCashFlowTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityEventCashFlowTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityEventCashFlowTable.java index eaf8a1b0..7a30304d 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityEventCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityEventCashFlowTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.tinkoff; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityEventCashFlow; @@ -38,7 +39,7 @@ public class TinkoffSecurityEventCashFlowTable extends SingleAbstractReportTable { private final SecurityCodeAndIsinTable codeAndIsin; - private String currency = null; + private String currency = ""; protected TinkoffSecurityEventCashFlowTable(SingleBrokerReport report, SecurityCodeAndIsinTable codeAndIsin) { super(report, @@ -51,9 +52,9 @@ protected TinkoffSecurityEventCashFlowTable(SingleBrokerReport report, SecurityC } @Override - protected SecurityEventCashFlow parseRow(TableRow row) { + protected @Nullable SecurityEventCashFlow parseRow(TableRow row) { currency = TinkoffCashFlowTableHelper.getCurrency(row, currency); - if (currency == null) { + if (!hasLength(currency)) { return null; } String operation = row.getStringCellValueOrDefault(OPERATION, "").toLowerCase(); @@ -135,7 +136,7 @@ private int getCount(TableRow row) { Assert.isTrue(slashPos != -1 && description.endsWith(" шт."), "Ожидается количество бумаг в формате '<...>/ 10 шт.'"); String text = description.substring(slashPos + 1).trim(); - return Integer.parseInt(text.split("\s+")[0]); + return Integer.parseInt(text.split("\\s+")[0]); } @Override diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityQuoteTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityQuoteTable.java index 0ddd048e..be0927b8 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityQuoteTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityQuoteTable.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityType; @@ -29,7 +30,6 @@ import org.spacious_team.table_wrapper.api.TableColumn; import org.spacious_team.table_wrapper.api.TableHeaderColumn; import org.spacious_team.table_wrapper.api.TableRow; -import org.springframework.util.StringUtils; import ru.investbook.parser.SingleAbstractReportTable; import ru.investbook.parser.SingleBrokerReport; import ru.investbook.report.ForeignExchangeRateService; @@ -39,6 +39,9 @@ import java.util.Optional; import static java.math.RoundingMode.HALF_UP; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; +import static org.springframework.util.StringUtils.hasLength; import static ru.investbook.parser.tinkoff.TinkoffSecurityQuoteTable.SecurityQuoteTableHeader.*; import static ru.investbook.parser.tinkoff.TinkoffSecurityTransactionTableHelper.declareSecurity; @@ -76,11 +79,13 @@ public TinkoffSecurityQuoteTable(String tableNamePrefix, } @Override - protected SecurityQuote parseRow(TableRow row) { + protected @Nullable SecurityQuote parseRow(TableRow row) { adjustSecuritiesValueEstimate(row); - BigDecimal price = row.getBigDecimalCellValueOrDefault(PRICE, null); - if (price == null) { + @SuppressWarnings("DataFlowIssue") + @Nullable BigDecimal price = row.getBigDecimalCellValueOrDefault(PRICE, null); + //noinspection ConstantValue + if (isNull(price)) { return null; } @@ -116,8 +121,10 @@ protected SecurityQuote parseRow(TableRow row) { private void adjustSecuritiesValueEstimate(TableRow row) { try { - BigDecimal value = row.getBigDecimalCellValueOrDefault(VALUE, null); - if (value != null) { + @SuppressWarnings("DataFlowIssue") + @Nullable BigDecimal value = row.getBigDecimalCellValueOrDefault(VALUE, null); + //noinspection ConstantValue + if (nonNull(value)) { String currency = row.getStringCellValue(CURRENCY); if (currency.equalsIgnoreCase("RUB")) { rubSecuritiesTotalValue = rubSecuritiesTotalValue.add(value); @@ -138,8 +145,9 @@ private void adjustSecuritiesValueEstimate(TableRow row) { private Optional getSecurityId(TableRow row) { try { - String code = row.getStringCellValueOrDefault(CODE, null); - if (StringUtils.hasLength(code)) { + @SuppressWarnings("DataFlowIssue") + @Nullable String code = row.getStringCellValueOrDefault(CODE, null); + if (hasLength(code)) { BigDecimal accruedInterest = row.getBigDecimalCellValueOrDefault(ACCRUED_INTEREST, BigDecimal.ZERO); boolean isBond = accruedInterest.floatValue() > 1e-3; Security security = TinkoffSecurityTransactionTableHelper.getSecurity( @@ -164,6 +172,7 @@ public BigDecimal getUsdSecuritiesTotalValue() { return usdSecuritiesTotalValue; } + @Getter @RequiredArgsConstructor protected enum SecurityQuoteTableHeader implements TableHeaderColumn { SHORT_NAME("Сокращенное", "наименование", "актива"), @@ -174,7 +183,6 @@ protected enum SecurityQuoteTableHeader implements TableHeaderColumn { CURRENCY(optional("Валюта", "цены")), VALUE(optional("Рыночная", "стои", "мость")); // на все бумаги исходящего остатка с учетом НКД - @Getter private final TableColumn column; SecurityQuoteTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTable.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTable.java index 962b6d59..db355061 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTable.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTable.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityType; import org.spacious_team.broker.report_parser.api.AbstractTransaction; import org.spacious_team.broker.report_parser.api.DerivativeTransaction; @@ -77,7 +78,7 @@ private TinkoffSecurityTransactionTable(String tableNamePrefix, } @Override - protected AbstractTransaction parseRow(TableRow row) { + protected @Nullable AbstractTransaction parseRow(TableRow row) { String tradeId = row.getStringCellValueOrDefault(TRADE_ID, ""); if (!hasLength(tradeId)) return null; String operation = row.getStringCellValue(OPERATION).toLowerCase(); @@ -97,7 +98,7 @@ protected AbstractTransaction parseRow(TableRow row) { amount = isBuy ? amount.negate() : amount; count = isBuy ? count : -count; - Instant timestamp = null; + Instant timestamp; SecurityType securityType = transactionTableHelper.getSecurityType(row); AbstractTransaction.AbstractTransactionBuilder builder = switch (securityType) { case STOCK -> SecurityTransaction.builder() diff --git a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTableHelper.java b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTableHelper.java index ad7d4ff1..ffd54dd1 100644 --- a/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTableHelper.java +++ b/src/main/java/ru/investbook/parser/tinkoff/TinkoffSecurityTransactionTableHelper.java @@ -19,6 +19,7 @@ package ru.investbook.parser.tinkoff; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityType; import org.spacious_team.table_wrapper.api.TableRow; @@ -29,6 +30,7 @@ import java.math.BigDecimal; import java.util.Objects; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.tinkoff.TinkoffSecurityTransactionTable.TransactionTableHeader.*; @Component @@ -82,15 +84,16 @@ static Security getSecurity(String code, SecurityCodeAndIsinTable codeAndIsin, String shortName, SecurityType securityType) { - String isin = switch (securityType) { + @SuppressWarnings("switch.expression") + @Nullable String isin = switch (securityType) { case STOCK, BOND, STOCK_OR_BOND -> codeAndIsin.getIsin(code, shortName); - default -> null; + case DERIVATIVE, CURRENCY_PAIR, ASSET -> null; }; return getSecurity(code, isin, shortName, securityType); } static Security getSecurity(String code, - String isin, + @Nullable String isin, String shortName, SecurityType securityType) { return switch (securityType) { @@ -113,11 +116,11 @@ static Security getSecurity(String code, static int declareSecurity(Security security, SecurityRegistrar registrar) { return switch (security.getType()) { - case STOCK -> registrar.declareStockByIsin(security.getIsin(), security::toBuilder); - case BOND -> registrar.declareBondByIsin(security.getIsin(), security::toBuilder); - case STOCK_OR_BOND -> registrar.declareStockOrBondByIsin(security.getIsin(), security::toBuilder); - case DERIVATIVE -> registrar.declareDerivative(security.getTicker()); - case CURRENCY_PAIR -> registrar.declareCurrencyPair(security.getTicker()); + case STOCK -> registrar.declareStockByIsin(requireNonNull(security.getIsin()), security::toBuilder); + case BOND -> registrar.declareBondByIsin(requireNonNull(security.getIsin()), security::toBuilder); + case STOCK_OR_BOND -> registrar.declareStockOrBondByIsin(requireNonNull(security.getIsin()), security::toBuilder); + case DERIVATIVE -> registrar.declareDerivative(requireNonNull(security.getTicker())); + case CURRENCY_PAIR -> registrar.declareCurrencyPair(requireNonNull(security.getTicker())); case ASSET -> throw new IllegalArgumentException("Тип ASSET не поддерживается"); }; } diff --git a/src/main/java/ru/investbook/parser/uralsib/AssetsTable.java b/src/main/java/ru/investbook/parser/uralsib/AssetsTable.java index 02a4e19f..36ca3b33 100644 --- a/src/main/java/ru/investbook/parser/uralsib/AssetsTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/AssetsTable.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioProperty; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.spacious_team.table_wrapper.api.AnyOfTableColumn; @@ -34,9 +35,11 @@ import ru.investbook.parser.SingleInitializableReportTable; import java.util.Collection; +import java.util.Objects; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.AssetsTable.SummaryTableHeader.RUB; /** @@ -69,8 +72,9 @@ protected Collection parseTable() { return emptyList(); } - TableRow row = table.stream() - .filter(tableRow -> tableRow.rowContains(ASSETS)) + @Nullable TableRow row = table.stream() + .filter(Objects::nonNull) + .filter(tableRow -> requireNonNull(tableRow).rowContains(ASSETS)) .findAny() .orElse(null); diff --git a/src/main/java/ru/investbook/parser/uralsib/CashFlowTable.java b/src/main/java/ru/investbook/parser/uralsib/CashFlowTable.java index adf67f4c..6612e858 100644 --- a/src/main/java/ru/investbook/parser/uralsib/CashFlowTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/CashFlowTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.uralsib; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.table_wrapper.api.TableRow; @@ -29,6 +30,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.PaymentsTable.PaymentsTableHeader.*; @Slf4j @@ -43,9 +45,10 @@ public CashFlowTable(UralsibBrokerReport report) { } @Override - protected EventCashFlow parseRow(TableRow row) { - String action = row.getStringCellValue(OPERATION); - action = String.valueOf(action).toLowerCase().trim(); + protected @Nullable EventCashFlow parseRow(TableRow row) { + String action = row.getStringCellValue(OPERATION) + .toLowerCase() + .trim(); String description = row.getStringCellValueOrDefault(DESCRIPTION, ""); CashFlowType type; switch (action) { @@ -57,8 +60,8 @@ protected EventCashFlow parseRow(TableRow row) { Matcher matcherFrom = moneyTransferFromDescriptionPattern.matcher(description); Matcher matcherTo = moneyTransferToDescriptionPattern.matcher(description); if (matcherFrom.find() && matcherTo.find()) { - String to = matcherTo.group(1); - String from = matcherFrom.group(1); + String to = requireNonNull(matcherTo.group(1)); + String from = requireNonNull(matcherFrom.group(1)); if (isCurrentPortfolioAccount(to) != isCurrentPortfolioAccount(from)) { if (isExternalAccount(from) && isExternalAccount(to)) { type = CashFlowType.CASH; @@ -111,8 +114,10 @@ private boolean isExternalAccount(String account) { private Integer getClientCode(String account) { try { Matcher matcher = clientCodePattern.matcher(account); + //noinspection ResultOfMethodCallIgnored matcher.find(); - return Integer.parseInt(matcher.group(1)); + String value = requireNonNull(matcher.group(1)); + return Integer.parseInt(value); } catch (Exception e) { throw new RuntimeException("Не могу найти код клиента для субсчета " + account); } diff --git a/src/main/java/ru/investbook/parser/uralsib/CashTable.java b/src/main/java/ru/investbook/parser/uralsib/CashTable.java index 7645c88c..a3f16e5e 100644 --- a/src/main/java/ru/investbook/parser/uralsib/CashTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/CashTable.java @@ -50,13 +50,14 @@ protected PortfolioCash parseRow(TableRow row) { .build(); } + @Getter enum CashTableHeader implements TableHeaderColumn { VALUE("исходящий остаток"), CURRENCY("код валюты"); - @Getter private final TableColumn column; - CashTableHeader(String ... words) { + + CashTableHeader(String... words) { this.column = PatternTableColumn.of(words); } } diff --git a/src/main/java/ru/investbook/parser/uralsib/CouponAmortizationRedemptionTable.java b/src/main/java/ru/investbook/parser/uralsib/CouponAmortizationRedemptionTable.java index 9110f88e..36ab83a8 100644 --- a/src/main/java/ru/investbook/parser/uralsib/CouponAmortizationRedemptionTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/CouponAmortizationRedemptionTable.java @@ -19,12 +19,12 @@ package ru.investbook.parser.uralsib; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.report_parser.api.ReportTable; import org.spacious_team.broker.report_parser.api.SecurityTransaction; -import org.spacious_team.table_wrapper.api.Table; import org.spacious_team.table_wrapper.api.TableRow; import java.math.BigDecimal; @@ -36,6 +36,8 @@ import java.util.Map; import static java.util.Collections.emptyList; +import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.PaymentsTable.PaymentsTableHeader.*; @Slf4j @@ -52,9 +54,9 @@ public CouponAmortizationRedemptionTable(UralsibBrokerReport report, } protected Collection parseRowToCollection(TableRow row) { - CashFlowType event; + @Nullable CashFlowType event; String action = row.getStringCellValue(OPERATION); - action = String.valueOf(action).toLowerCase().trim(); + action = action.toLowerCase().trim(); if (action.equalsIgnoreCase("погашение купона")) { event = CashFlowType.COUPON; } else if (action.equalsIgnoreCase("погашение номинала")) { // и амортизация и погашение @@ -63,12 +65,12 @@ protected Collection parseRowToCollection(TableRow row) { return emptyList(); } - Security security = getSecurity(row, CashFlowType.AMORTIZATION); + @Nullable Security security = getSecurity(row, CashFlowType.AMORTIZATION); if (security == null) return emptyList(); Instant timestamp = convertToInstant(row.getStringCellValue(DATE)); if (event == null) { - if (isRedemption(security.getName(), timestamp)) { + if (isRedemption(requireNonNull(security.getName()), timestamp)) { event = CashFlowType.REDEMPTION; } else { event = CashFlowType.AMORTIZATION; @@ -79,7 +81,7 @@ protected Collection parseRowToCollection(TableRow row) { BigDecimal value = row.getBigDecimalCellValue(VALUE) .add(tax.abs()); SecurityEventCashFlow.SecurityEventCashFlowBuilder builder = SecurityEventCashFlow.builder() - .security(security.getId()) + .security(requireNonNull(security.getId())) .portfolio(getReport().getPortfolio()) .count(getSecurityCount(security, timestamp)) .eventType(event) @@ -99,16 +101,17 @@ protected Collection parseRowToCollection(TableRow row) { } private boolean isRedemption(String securityName, Instant amortizationDay) { - LocalDate redemptionDate = redemptionDates.stream() + @Nullable LocalDate redemptionDate = redemptionDates.stream() .filter(e -> securityName.equalsIgnoreCase(e.getKey())) .map(Map.Entry::getValue) .map(instant -> LocalDate.ofInstant(instant, getReport().getReportZoneId())) .findAny() .orElse(null); - return (redemptionDate != null) && redemptionDate.equals(LocalDate.ofInstant(amortizationDay, getReport().getReportZoneId())); + return nonNull(redemptionDate) && redemptionDate.equals(LocalDate.ofInstant(amortizationDay, getReport().getReportZoneId())); } - protected BigDecimal getTax(Table table, TableRow row) { + @Override + protected BigDecimal getTax(TableRow row) { // информация о налоге по купонам облигаций не выводится в отчет брокера return BigDecimal.ZERO; } diff --git a/src/main/java/ru/investbook/parser/uralsib/DerivativeCashFlowTable.java b/src/main/java/ru/investbook/parser/uralsib/DerivativeCashFlowTable.java index 2be3f1fa..71c2b41b 100644 --- a/src/main/java/ru/investbook/parser/uralsib/DerivativeCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/DerivativeCashFlowTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.uralsib; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.table_wrapper.api.TableRow; @@ -28,6 +29,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.PaymentsTable.PaymentsTableHeader.*; @Slf4j @@ -39,9 +41,10 @@ public DerivativeCashFlowTable(UralsibBrokerReport report) { super(report, PaymentsTable.TABLE_NAME, "", PaymentsTable.PaymentsTableHeader.class); } - protected SecurityEventCashFlow parseRow(TableRow row) { - String action = row.getStringCellValue(OPERATION); - action = String.valueOf(action).toLowerCase().trim(); + protected @Nullable SecurityEventCashFlow parseRow(TableRow row) { + String action = row.getStringCellValue(OPERATION) + .toLowerCase() + .trim(); if (!action.equalsIgnoreCase("вариационная маржа")) { return null; } @@ -56,10 +59,12 @@ protected SecurityEventCashFlow parseRow(TableRow row) { } private int getSecurityId(TableRow row) { + @SuppressWarnings("DataFlowIssue") String description = row.getStringCellValueOrDefault(DESCRIPTION, ""); + @SuppressWarnings("DataFlowIssue") Matcher matcher = contractPattern.matcher(description); if (matcher.find()) { - String code = matcher.group(1); + String code = requireNonNull(matcher.group(1)); return getReport().getSecurityRegistrar().declareDerivative(code); } throw new RuntimeException("Не могу найти наименование контракта в отчете брокера по событию:" + description); diff --git a/src/main/java/ru/investbook/parser/uralsib/DerivativeQuoteTable.java b/src/main/java/ru/investbook/parser/uralsib/DerivativeQuoteTable.java index 4a079c44..c715a1da 100644 --- a/src/main/java/ru/investbook/parser/uralsib/DerivativeQuoteTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/DerivativeQuoteTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.uralsib; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -42,8 +43,8 @@ protected DerivativeQuoteTable(UralsibBrokerReport report) { } @Override - protected SecurityQuote parseRow(TableRow row) { - BigDecimal quote = row.getBigDecimalCellValueOrDefault(QUOTE, null); + protected @Nullable SecurityQuote parseRow(TableRow row) { + @Nullable BigDecimal quote = row.getBigDecimalCellValueOrDefault(QUOTE, null); if (quote == null || quote.compareTo(minValue) < 0) { return null; } @@ -56,6 +57,7 @@ protected SecurityQuote parseRow(TableRow row) { .build(); } + @Getter enum ContractCountTableHeader implements TableHeaderColumn { CONTRACT("наименование контракта"), INCOMING("входящий остаток"), @@ -64,8 +66,8 @@ enum ContractCountTableHeader implements TableHeaderColumn { CELL("списано"), QUOTE("расчетная цена"); - @Getter private final TableColumn column; + ContractCountTableHeader(String... words) { this.column = PatternTableColumn.of(words); } diff --git a/src/main/java/ru/investbook/parser/uralsib/DerivativeTransactionTable.java b/src/main/java/ru/investbook/parser/uralsib/DerivativeTransactionTable.java index 48a33b1d..c97ccb66 100644 --- a/src/main/java/ru/investbook/parser/uralsib/DerivativeTransactionTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/DerivativeTransactionTable.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.report_parser.api.DerivativeTransaction; import org.spacious_team.table_wrapper.api.AnyOfTableColumn; import org.spacious_team.table_wrapper.api.OptionalTableColumn; @@ -31,6 +32,7 @@ import ru.investbook.parser.SingleAbstractReportTable; import java.math.BigDecimal; +import java.util.Objects; import java.util.regex.Pattern; import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; @@ -39,7 +41,7 @@ @Slf4j public class DerivativeTransactionTable extends SingleAbstractReportTable { private static final String TABLE_NAME = "СДЕЛКИ С ФЬЮЧЕРСАМИ И ОПЦИОНАМИ"; - private static final Pattern tableEndPredicate = Pattern.compile("^[А-Я\s]+$", UNICODE_CHARACTER_CLASS); + private static final Pattern tableEndPredicate = Pattern.compile("^[А-Я\\s]+$", UNICODE_CHARACTER_CLASS); private boolean expirationTableReached = false; public DerivativeTransactionTable(UralsibBrokerReport report) { @@ -55,11 +57,13 @@ protected DerivativeTransactionTable(UralsibBrokerReport report, String tableNam } @Override - protected DerivativeTransaction parseRow(TableRow row) { + protected @Nullable DerivativeTransaction parseRow(TableRow row) { if (expirationTableReached) return null; - String tradeId = SecurityTransactionTable.getTradeId(row, TRANSACTION); + @Nullable String tradeId = SecurityTransactionTable.getTradeId(row, TRANSACTION); if (tradeId == null) { - if (DerivativeExpirationTable.TABLE_NAME.equals(row.getStringCellValueOrDefault(TRANSACTION, null))) { + if (Objects.equals( + row.getStringCellValueOrDefault(TRANSACTION, null), + DerivativeExpirationTable.TABLE_NAME)) { expirationTableReached = true; } return null; @@ -96,6 +100,7 @@ protected DerivativeTransaction parseRow(TableRow row) { .build(); } + @Getter @RequiredArgsConstructor enum FortsTableHeader implements TableHeaderColumn { DATE_TIME(AnyOfTableColumn.of( @@ -119,7 +124,6 @@ enum FortsTableHeader implements TableHeaderColumn { CLEARING_COMMISSION(OptionalTableColumn.of( PatternTableColumn.of("клиринговая комиссия"))); // only for Expiration table - @Getter private final TableColumn column; FortsTableHeader(String ... words) { this.column = PatternTableColumn.of(words); diff --git a/src/main/java/ru/investbook/parser/uralsib/DividendTable.java b/src/main/java/ru/investbook/parser/uralsib/DividendTable.java index a7462525..ba32d804 100644 --- a/src/main/java/ru/investbook/parser/uralsib/DividendTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/DividendTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.uralsib; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityEventCashFlow; @@ -32,6 +33,7 @@ import java.util.Collection; import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.PaymentsTable.PaymentsTableHeader.*; @Slf4j @@ -55,7 +57,7 @@ protected Collection parseRowToCollection(TableRow row) { return emptyList(); } - Security security = getSecurity(row, CashFlowType.DIVIDEND); + @Nullable Security security = getSecurity(row, CashFlowType.DIVIDEND); if (security == null) return emptyList(); Instant timestamp = convertToInstant(row.getStringCellValue(DATE)); @@ -63,7 +65,7 @@ protected Collection parseRowToCollection(TableRow row) { BigDecimal value = row.getBigDecimalCellValue(VALUE) .add(tax.abs()); SecurityEventCashFlow.SecurityEventCashFlowBuilder builder = SecurityEventCashFlow.builder() - .security(security.getId()) + .security(requireNonNull(security.getId())) .portfolio(getReport().getPortfolio()) .count(getSecurityCount(security, timestamp)) .eventType(CashFlowType.DIVIDEND) diff --git a/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeRateTable.java b/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeRateTable.java index ac541156..1656361b 100644 --- a/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeRateTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeRateTable.java @@ -56,7 +56,9 @@ protected Collection parseTable() { return emptyList(); } List exchangeRates = new ArrayList<>(); + @SuppressWarnings({"nullness", "DataFlowIssue"}) TableCell cell = report.getReportPage().getRow(address.getRow() + 1).getCell(0); + @SuppressWarnings({"nullness", "DataFlowIssue"}) String text = cell.getStringValue(); String[] words = text.split(" "); for (int i = 0; i < words.length; i++) { diff --git a/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeTransactionTable.java b/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeTransactionTable.java index cf6f136f..874e4091 100644 --- a/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeTransactionTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/ForeignExchangeTransactionTable.java @@ -20,6 +20,8 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.report_parser.api.ForeignExchangeTransaction; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -29,22 +31,24 @@ import java.math.BigDecimal; +import static java.util.Objects.requireNonNull; +import static org.springframework.util.StringUtils.hasLength; import static ru.investbook.parser.uralsib.ForeignExchangeTransactionTable.FxTransactionTableHeader.*; @Slf4j public class ForeignExchangeTransactionTable extends SingleAbstractReportTable { private static final String TABLE_NAME = "Биржевые валютные сделки, совершенные в отчетном периоде"; private static final String CONTRACT_PREFIX = "Инструмент:"; - private String instrument = null; + private @MonotonicNonNull String instrument = null; public ForeignExchangeTransactionTable(UralsibBrokerReport report) { super(report, TABLE_NAME, "", FxTransactionTableHeader.class, 2); } @Override - protected ForeignExchangeTransaction parseRow(TableRow row) { + protected @Nullable ForeignExchangeTransaction parseRow(TableRow row) { String tradeId; - Object cellValue = row.getCellValue(TRADE_ID); + @Nullable Object cellValue = row.getCellValue(TRADE_ID); if (cellValue instanceof String) { String stringValue = cellValue.toString(); try { @@ -62,7 +66,7 @@ protected ForeignExchangeTransaction parseRow(TableRow row) { } else { return null; } - if (instrument == null || instrument.isEmpty()) { + if (!hasLength(instrument)) { return null; } @@ -75,7 +79,7 @@ protected ForeignExchangeTransaction parseRow(TableRow row) { .add(row.getBigDecimalCellValue(BROKER_COMMISSION)) .negate(); - int securityId = getReport().getSecurityRegistrar().declareCurrencyPair(instrument); + int securityId = getReport().getSecurityRegistrar().declareCurrencyPair(requireNonNull(instrument)); return ForeignExchangeTransaction.builder() .timestamp(convertToInstant(row.getStringCellValue(DATE_TIME))) .tradeId(tradeId) diff --git a/src/main/java/ru/investbook/parser/uralsib/PaymentsTable.java b/src/main/java/ru/investbook/parser/uralsib/PaymentsTable.java index fe69018d..f7c65c84 100644 --- a/src/main/java/ru/investbook/parser/uralsib/PaymentsTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/PaymentsTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.pojo.Security; @@ -48,6 +49,7 @@ import static java.lang.Character.isLetterOrDigit; import static java.lang.Double.parseDouble; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.PaymentsTable.PaymentsTableHeader.*; import static ru.investbook.parser.uralsib.UralsibBrokerReport.convertToCurrency; @@ -61,7 +63,7 @@ abstract class PaymentsTable extends SingleAbstractReportTable securityTransactions; private final Collection eventCashFlows = new ArrayList<>(); private final Pattern taxInformationPattern = Pattern.compile("налог в размере ([0-9.]+) удержан"); - private String currentRowDescription = ""; + private @Nullable String currentRowDescription = ""; public PaymentsTable(UralsibBrokerReport report, SecuritiesTable securitiesTable, @@ -86,7 +88,7 @@ private Collection getRowAndSaveDescription(TableRow row) /** * @return security if found, null otherwise */ - protected Security getSecurity(TableRow row, CashFlowType cashEventIfSecurityNotFound) { + protected @Nullable Security getSecurity(TableRow row, CashFlowType cashEventIfSecurityNotFound) { try { return getSecurityIfCan(row); } catch (Exception e) { @@ -132,7 +134,7 @@ protected Security getSecurityIfCan(TableRow row) { throw new RuntimeException("Не могу найти ISIN ценной бумаги в отчете брокера по событию:" + description); } - private boolean contains(String description, String securityParameter) { + private boolean contains(String description, @Nullable String securityParameter) { if (securityParameter == null) { return false; } @@ -151,7 +153,8 @@ protected BigDecimal getTax(TableRow row) { Matcher matcher = taxInformationPattern.matcher(description.toLowerCase()); if (matcher.find()) { try { - return BigDecimal.valueOf(parseDouble(matcher.group(1))); + String value = requireNonNull(matcher.group(1)); + return BigDecimal.valueOf(parseDouble(value)); } catch (Exception e) { log.info("Не смогу выделить сумму налога из описания: {}", description); } @@ -161,7 +164,7 @@ protected BigDecimal getTax(TableRow row) { protected Integer getSecurityCount(Security security, Instant atInstant) { int count = securitiesIncomingCount.stream() - .filter(i -> i.getSecurity().getId().equals(security.getId())) + .filter(i -> Objects.equals(i.getSecurity().getId(), security.getId())) .map(ReportSecurityInformation::getIncomingCount) .findAny() .orElseThrow(() -> new RuntimeException("Не найдено количество на начало периода отчета для ЦБ " + security)); diff --git a/src/main/java/ru/investbook/parser/uralsib/SecuritiesTable.java b/src/main/java/ru/investbook/parser/uralsib/SecuritiesTable.java index f4efa028..f4e8ccec 100644 --- a/src/main/java/ru/investbook/parser/uralsib/SecuritiesTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/SecuritiesTable.java @@ -67,6 +67,7 @@ public Security getSecurityByCfi(String cfi) { .orElseThrow(() -> new RuntimeException("ЦБ с номером гос. регистрации/CFI не найден: " + cfi)); } + @Getter enum SecuritiesTableHeader implements TableHeaderColumn { NAME("наименование"), ISIN("isin"), @@ -77,7 +78,6 @@ enum SecuritiesTableHeader implements TableHeaderColumn { AMOUNT("Стоимость позиции по цене закрытия"), // в рублях для СПБ биржи ACCRUED_INTEREST("^нкд$"); // в валюте для валютных облигаций - @Getter private final TableColumn column; SecuritiesTableHeader(String... words) { @@ -89,7 +89,7 @@ enum SecuritiesTableHeader implements TableHeaderColumn { @ToString @Builder(toBuilder = true) @EqualsAndHashCode - static class ReportSecurityInformation { + public static class ReportSecurityInformation { private final Security security; private final String cfi; private final int incomingCount; diff --git a/src/main/java/ru/investbook/parser/uralsib/SecurityDepositAndWithdrawalTable.java b/src/main/java/ru/investbook/parser/uralsib/SecurityDepositAndWithdrawalTable.java index cba9cf14..cbf5fa7e 100644 --- a/src/main/java/ru/investbook/parser/uralsib/SecurityDepositAndWithdrawalTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/SecurityDepositAndWithdrawalTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.uralsib; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.report_parser.api.SecurityTransaction; import org.spacious_team.table_wrapper.api.TableRow; @@ -25,6 +26,7 @@ import java.math.BigDecimal; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.SecurityRedemptionTable.SecurityFlowTableHeader.*; /** @@ -42,7 +44,7 @@ public SecurityDepositAndWithdrawalTable(UralsibBrokerReport report, SecuritiesT } @Override - protected SecurityTransaction parseRow(TableRow row) { + protected @Nullable SecurityTransaction parseRow(TableRow row) { String operation = row.getStringCellValue(OPERATION); if (!operation.equalsIgnoreCase(IN_DESCRIPTION) && !operation.equalsIgnoreCase(OUT_DESCRIPTION)) { return null; @@ -51,7 +53,7 @@ protected SecurityTransaction parseRow(TableRow row) { .timestamp(convertToInstant(row.getStringCellValue(DATE))) .tradeId(row.getStringCellValue(ID)) .portfolio(getReport().getPortfolio()) - .security(getSecurity(row).getId()) + .security(requireNonNull(getSecurity(row).getId())) .count(row.getIntCellValue(COUNT)) .value(BigDecimal.ZERO) .accruedInterest(BigDecimal.ZERO) diff --git a/src/main/java/ru/investbook/parser/uralsib/SecurityQuoteTable.java b/src/main/java/ru/investbook/parser/uralsib/SecurityQuoteTable.java index a81f0eb7..e9748c5a 100644 --- a/src/main/java/ru/investbook/parser/uralsib/SecurityQuoteTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/SecurityQuoteTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.uralsib; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityQuote.SecurityQuoteBuilder; import org.spacious_team.table_wrapper.api.TableRow; @@ -52,8 +53,8 @@ protected SecurityQuoteTable(UralsibBrokerReport report, ForeignExchangeRateTabl } @Override - protected SecurityQuote parseRow(TableRow row) { - BigDecimal amountInRub = row.getBigDecimalCellValueOrDefault(AMOUNT, null); + protected @Nullable SecurityQuote parseRow(TableRow row) { + @Nullable BigDecimal amountInRub = row.getBigDecimalCellValueOrDefault(AMOUNT, null); if (amountInRub == null || amountInRub.compareTo(minValue) < 0) { return null; } diff --git a/src/main/java/ru/investbook/parser/uralsib/SecurityRedemptionTable.java b/src/main/java/ru/investbook/parser/uralsib/SecurityRedemptionTable.java index 60872e65..dbdf7d6d 100644 --- a/src/main/java/ru/investbook/parser/uralsib/SecurityRedemptionTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/SecurityRedemptionTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.uralsib; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; import org.spacious_team.table_wrapper.api.TableHeaderColumn; @@ -43,7 +44,7 @@ public SecurityRedemptionTable(UralsibBrokerReport report) { } @Override - protected Entry parseRow(TableRow row) { + protected @Nullable Entry parseRow(TableRow row) { return row.getStringCellValue(OPERATION).equalsIgnoreCase(REDEMPTION_DESCRIPTION) ? new AbstractMap.SimpleEntry<>( row.getStringCellValue(NAME), @@ -51,6 +52,7 @@ protected Entry parseRow(TableRow row) { null; } + @Getter enum SecurityFlowTableHeader implements TableHeaderColumn { ID("№", "операции"), OPERATION("тип операции"), @@ -59,7 +61,6 @@ enum SecurityFlowTableHeader implements TableHeaderColumn { CFI("номер гос. регистрации"), COUNT("количество цб"); - @Getter private final TableColumn column; SecurityFlowTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/uralsib/SecurityRegistryHelper.java b/src/main/java/ru/investbook/parser/uralsib/SecurityRegistryHelper.java index 8b059fa6..6a6e1ec9 100644 --- a/src/main/java/ru/investbook/parser/uralsib/SecurityRegistryHelper.java +++ b/src/main/java/ru/investbook/parser/uralsib/SecurityRegistryHelper.java @@ -18,10 +18,12 @@ package ru.investbook.parser.uralsib; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityType; import ru.investbook.parser.SecurityRegistrar; +import static java.util.Objects.requireNonNull; import static ru.investbook.entity.SecurityEntity.isinPattern; class SecurityRegistryHelper { @@ -33,18 +35,19 @@ static int declareStockOrBond(String isin, String name, SecurityRegistrar regist static int declareStockOrBond(Security security, SecurityRegistrar registrar) { return (security.getTicker() != null) ? - registrar.declareStockOrBondByTicker(security.getTicker(), security::toBuilder) : - registrar.declareStockOrBondByIsin(security.getIsin(), security::toBuilder); + registrar.declareStockOrBondByTicker(requireNonNull(security.getTicker()), security::toBuilder) : + registrar.declareStockOrBondByIsin(requireNonNull(security.getIsin()), security::toBuilder); } static Security getStockOrBond(String isin, String name) { - String ticker = null; + @Nullable String securityIsin = isin; + @Nullable String ticker = null; if (!isinPattern.matcher(isin).matches()) { ticker = isin; - isin = null; // отчет иногда вместо ISIN содержит непонятный идентификаторБ например TRAK + securityIsin = null; // отчет иногда вместо ISIN содержит непонятный идентификаторБ например TRAK } return Security.builder() - .isin(isin) + .isin(securityIsin) .ticker(ticker) .name(name) .type(SecurityType.STOCK_OR_BOND) diff --git a/src/main/java/ru/investbook/parser/uralsib/SecurityTransactionTable.java b/src/main/java/ru/investbook/parser/uralsib/SecurityTransactionTable.java index 0016df6b..754240cc 100644 --- a/src/main/java/ru/investbook/parser/uralsib/SecurityTransactionTable.java +++ b/src/main/java/ru/investbook/parser/uralsib/SecurityTransactionTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.report_parser.api.SecurityTransaction; import org.spacious_team.table_wrapper.api.AnyOfTableColumn; @@ -29,15 +30,14 @@ import org.spacious_team.table_wrapper.api.TableColumn; import org.spacious_team.table_wrapper.api.TableHeaderColumn; import org.spacious_team.table_wrapper.api.TableRow; -import org.springframework.lang.Nullable; import ru.investbook.parser.SingleAbstractReportTable; import ru.investbook.parser.TransactionValueAndFeeParser; import java.math.BigDecimal; import java.time.Instant; -import java.util.Objects; import java.util.Optional; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.uralsib.SecurityTransactionTable.TransactionTableHeader.*; @Slf4j @@ -67,16 +67,15 @@ protected SecurityTransactionTable(UralsibBrokerReport report, this.transactionValueAndFeeParser = transactionValueAndFeeParser; } - @Nullable @Override - protected SecurityTransaction parseRow(TableRow row) { - String tradeId = getTradeId(row, TRADE_ID); + protected @Nullable SecurityTransaction parseRow(TableRow row) { + @Nullable String tradeId = getTradeId(row, TRADE_ID); if (tradeId == null) { - security = getSecurity(row); + this.security = getSecurity(row); return null; } - Objects.requireNonNull(security, "Не известная ЦБ"); + Security security = requireNonNull(this.security, "Не известная ЦБ"); boolean isBuy = row.getStringCellValue(DIRECTION).equalsIgnoreCase("покупка"); BigDecimal value = row.getBigDecimalCellValue(VALUE); BigDecimal accruedInterest = row.getBigDecimalCellValue(ACCRUED_INTEREST); @@ -105,7 +104,7 @@ protected SecurityTransaction parseRow(TableRow row) { .timestamp(timestamp) .tradeId(tradeId) .portfolio(getReport().getPortfolio()) - .security(security.getId()) + .security(requireNonNull(security.getId())) .count((isBuy ? 1 : -1) * row.getIntCellValue(COUNT)) .value(valueAndFee.value()) .accruedInterest((accruedInterest.abs().compareTo(minValue) >= 0) ? accruedInterest : BigDecimal.ZERO) @@ -115,8 +114,7 @@ protected SecurityTransaction parseRow(TableRow row) { .build(); } - @Nullable - static String getTradeId(TableRow row, TableHeaderColumn column) { + static @Nullable String getTradeId(TableRow row, TableHeaderColumn column) { try { // some numbers (doubles) represented by string type cells return String.valueOf(row.getLongCellValue(column)); @@ -125,8 +123,8 @@ static String getTradeId(TableRow row, TableHeaderColumn column) { } } - @Nullable - private Security getSecurity(TableRow row) { + private @Nullable Security getSecurity(TableRow row) { + //noinspection DataFlowIssue return Optional.ofNullable(row.getStringCellValueOrDefault(TRADE_ID, null)) .filter(securityDescription -> !securityDescription.startsWith("Итого")) .map(securityDescription -> securityDescription.split(" ")) @@ -135,6 +133,7 @@ private Security getSecurity(TableRow row) { .orElse(null); } + @Getter enum TransactionTableHeader implements TableHeaderColumn { DATE_TIME( AnyOfTableColumn.of( @@ -175,7 +174,6 @@ enum TransactionTableHeader implements TableHeaderColumn { PatternTableColumn.of("валюта списания")), VALUE_CURRENCY.getColumn())); // new report (fallback to value currency) - @Getter private final TableColumn column; TransactionTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/uralsib/UralsibBrokerReport.java b/src/main/java/ru/investbook/parser/uralsib/UralsibBrokerReport.java index 68f347b7..f7b64658 100644 --- a/src/main/java/ru/investbook/parser/uralsib/UralsibBrokerReport.java +++ b/src/main/java/ru/investbook/parser/uralsib/UralsibBrokerReport.java @@ -20,23 +20,21 @@ import lombok.EqualsAndHashCode; import org.apache.poi.ss.usermodel.Workbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.ReportPage; +import org.spacious_team.table_wrapper.api.ReportPageRow; import org.spacious_team.table_wrapper.api.TableCell; import org.spacious_team.table_wrapper.api.TableCellAddress; import org.spacious_team.table_wrapper.excel.ExcelSheet; import ru.investbook.parser.AbstractExcelBrokerReport; import ru.investbook.parser.SecurityRegistrar; -import java.io.IOException; import java.io.InputStream; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.function.Predicate; -import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import static java.time.temporal.ChronoUnit.HOURS; import static java.util.Objects.requireNonNull; import static org.spacious_team.table_wrapper.api.TableCellAddress.NOT_FOUND; @@ -44,53 +42,54 @@ public class UralsibBrokerReport extends AbstractExcelBrokerReport { // "УРАЛСИБ Брокер" или "УРАЛСИБ Кэпитал - Финансовые услуги" (старый формат 2018 г) private static final String PORTFOLIO_MARKER = "Номер счета Клиента:"; - private final Predicate uralsibReportPredicate = cell -> + private static final Predicate<@Nullable Object> uralsibReportPredicate = cell -> (cell instanceof String value) && (value.contains("Твой Брокер") || value.contains("УРАЛСИБ")); - private final Predicate dateMarkerPredicate = cell -> + private static final Predicate<@Nullable Object> dateMarkerPredicate = cell -> (cell instanceof String value) && value.contains("за период"); - private final Workbook book; public UralsibBrokerReport(ZipInputStream zis, SecurityRegistrar securityRegistrar) { - super(securityRegistrar); + this(getFileName(zis), zis, securityRegistrar); + } + + public UralsibBrokerReport(String excelFileName, InputStream is, SecurityRegistrar securityRegistrar) { + super(getBrokerReportAttributes(excelFileName, is), securityRegistrar); + } + + @SuppressWarnings("DataFlowIssue") + private static String getFileName(ZipInputStream zis) { try { - ZipEntry zipEntry = requireNonNull(zis.getNextEntry()); - Path path = Paths.get(zipEntry.getName()); - this.book = getWorkBook(path.getFileName().toString(), zis); - ReportPage reportPage = new ExcelSheet(book.getSheetAt(0)); - checkReportFormat(path, reportPage); - setPath(path); - setReportPage(reportPage); - setPortfolio(getPortfolio(reportPage)); - setReportEndDateTime(getReportEndDateTime(reportPage)); + return zis.getNextEntry() + .getName(); } catch (Exception e) { throw new RuntimeException("Не смог открыть excel файл", e); } } - public UralsibBrokerReport(String excelFileName, InputStream is, SecurityRegistrar securityRegistrar) { - super(securityRegistrar); - this.book = getWorkBook(excelFileName, is); - ReportPage reportPage = new ExcelSheet(book.getSheetAt(0)); - Path path = Paths.get(excelFileName); - checkReportFormat(path, reportPage); - setPath(path); - setReportPage(reportPage); - setPortfolio(getPortfolio(reportPage)); - setReportEndDateTime(getReportEndDateTime(reportPage)); + private static ExcelAttributes getBrokerReportAttributes(String excelFileName, InputStream is) { + Workbook workbook = getWorkBook(excelFileName, is); + ReportPage reportPage = new ExcelSheet(workbook.getSheetAt(0)); + checkReportFormat(excelFileName, reportPage); + Attributes attributes = new Attributes( + reportPage, + excelFileName, + getReportEndDateTime(reportPage), + getPortfolio(reportPage)); + return new ExcelAttributes(workbook, attributes); } - private void checkReportFormat(Path path, ReportPage reportPage) { + private static void checkReportFormat(String excelFileName, ReportPage reportPage) { if (reportPage.find(0, 1, uralsibReportPredicate) == NOT_FOUND) { - throw new RuntimeException("В файле " + path + " не содержится отчет брокера Твой Брокер (Уралсиб)"); + throw new RuntimeException("В файле " + excelFileName + " не содержится отчет брокера Твой Брокер (Уралсиб)"); } } private static String getPortfolio(ReportPage reportPage) { try { TableCellAddress address = reportPage.findByPrefix(PORTFOLIO_MARKER); - for (TableCell cell : reportPage.getRow(address.getRow())) { + ReportPageRow row = requireNonNull(reportPage.getRow(address.getRow())); + for (@Nullable TableCell cell : row) { if (cell != null && cell.getColumnIndex() > address.getColumn()) { - Object value = cell.getValue(); + @Nullable Object value = cell.getValue(); if (value instanceof String) { return value.toString() .replace("_invest", "") @@ -107,14 +106,16 @@ private static String getPortfolio(ReportPage reportPage) { } } - private Instant getReportEndDateTime(ReportPage reportPage) { + private static Instant getReportEndDateTime(ReportPage reportPage) { try { TableCellAddress address = reportPage.find(0, dateMarkerPredicate); + @SuppressWarnings({"nullness", "DataFlowIssue"}) String[] words = reportPage.getCell(address) .getStringValue() .split(" "); - return convertToInstant(words[words.length - 1]) - .plus(LAST_TRADE_HOUR, ChronoUnit.HOURS); + String date = words[words.length - 1]; + return convertToInstantWithRussianFormatAndMoscowZoneId(date) + .plus(LAST_TRADE_HOUR, HOURS); } catch (Exception e) { throw new RuntimeException("Ошибка поиска даты отчета"); } @@ -123,9 +124,4 @@ private Instant getReportEndDateTime(ReportPage reportPage) { public static String convertToCurrency(String value) { return value.replace("RUR", "RUB"); // uralsib uses RUR (used till 1998) code in reports } - - @Override - public void close() throws IOException { - this.book.close(); - } } diff --git a/src/main/java/ru/investbook/parser/vtb/AbstractVtbCashFlowTable.java b/src/main/java/ru/investbook/parser/vtb/AbstractVtbCashFlowTable.java index b0292d70..7303397e 100644 --- a/src/main/java/ru/investbook/parser/vtb/AbstractVtbCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/vtb/AbstractVtbCashFlowTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.vtb; +import org.checkerframework.checker.nullness.qual.Nullable; import ru.investbook.parser.SingleInitializableReportTable; import java.util.ArrayList; @@ -34,8 +35,8 @@ public abstract class AbstractVtbCashFlowTable extends SingleInitializableReportTable { private final Collection events; - private final BiPredicate equalityChecker; - private final BiFunction> duplicatesMerger; + private final @Nullable BiPredicate equalityChecker; + private final @Nullable BiFunction> duplicatesMerger; public AbstractVtbCashFlowTable(CashFlowEventTable cashFlowEventTable) { super(cashFlowEventTable.getReport()); diff --git a/src/main/java/ru/investbook/parser/vtb/CashFlowEventTable.java b/src/main/java/ru/investbook/parser/vtb/CashFlowEventTable.java index 74d9000f..3a101c44 100644 --- a/src/main/java/ru/investbook/parser/vtb/CashFlowEventTable.java +++ b/src/main/java/ru/investbook/parser/vtb/CashFlowEventTable.java @@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -48,8 +49,8 @@ public CashFlowEventTable(SingleBrokerReport report) { } @Override - protected CashFlowEvent parseRow(TableRow row) { - String operation = row.getStringCellValueOrDefault(OPERATION, null); + protected @Nullable CashFlowEvent parseRow(TableRow row) { + @Nullable String operation = row.getStringCellValueOrDefault(OPERATION, null); if (operation == null) { return null; } @@ -85,7 +86,7 @@ public List getData() { @Slf4j @Builder @EqualsAndHashCode - static class CashFlowEvent { + public static class CashFlowEvent { private static final String duplicateOperation = "Перераспределение дохода между субсчетами / торговыми площадками"; private final Instant date; private final BigDecimal value; @@ -111,6 +112,7 @@ String getLowercaseDescription() { return lowercaseDescription; } + @Nullable CashFlowType getEventType() { String lowercaseDescription = getLowercaseDescription(); return switch (operation) { diff --git a/src/main/java/ru/investbook/parser/vtb/SecurityRegNumberRegistrarImpl.java b/src/main/java/ru/investbook/parser/vtb/SecurityRegNumberRegistrarImpl.java index b8f9f3ac..fa2e1824 100644 --- a/src/main/java/ru/investbook/parser/vtb/SecurityRegNumberRegistrarImpl.java +++ b/src/main/java/ru/investbook/parser/vtb/SecurityRegNumberRegistrarImpl.java @@ -30,6 +30,7 @@ public class SecurityRegNumberRegistrarImpl implements SecurityRegNumberRegistra private final VtbSecuritiesTable vtbSecuritiesTable; private final VtbSecurityFlowTable vtbSecurityFlowTable; + @SuppressWarnings("return") @Override public Optional getSecurityByRegistrationNumber(String registrationNumber) { return ofNullable(registrationNumber) diff --git a/src/main/java/ru/investbook/parser/vtb/VtbBrokerReport.java b/src/main/java/ru/investbook/parser/vtb/VtbBrokerReport.java index ca83715b..2837c0a6 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbBrokerReport.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbBrokerReport.java @@ -20,19 +20,18 @@ import lombok.EqualsAndHashCode; import org.apache.poi.ss.usermodel.Workbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.ReportPage; import org.spacious_team.table_wrapper.api.TableCellAddress; import org.spacious_team.table_wrapper.excel.ExcelSheet; import ru.investbook.parser.AbstractExcelBrokerReport; import ru.investbook.parser.SecurityRegistrar; -import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Objects; @EqualsAndHashCode(callSuper = true) public class VtbBrokerReport extends AbstractExcelBrokerReport { @@ -42,33 +41,39 @@ public class VtbBrokerReport extends AbstractExcelBrokerReport { private static final String REPORT_DATE_MARKER = "Отчет Банка ВТБ"; static final BigDecimal minValue = BigDecimal.valueOf(0.01); - private final Workbook book; - public VtbBrokerReport(String excelFileName, InputStream is, SecurityRegistrar securityRegistrar) { - super(securityRegistrar); - this.book = getWorkBook(excelFileName, is); - ReportPage reportPage = new ExcelSheet(book.getSheetAt(0)); - Path path = Paths.get(excelFileName); - checkReportFormat(path, reportPage); - setPath(path); - setReportPage(reportPage); - setPortfolio(getPortfolio(reportPage)); - setReportEndDateTime(getReportEndDateTime(reportPage)); + super(getBrokerReportAttributes(excelFileName, is), securityRegistrar); + } + + private static ExcelAttributes getBrokerReportAttributes(String excelFileName, InputStream is) { + Workbook workbook = getWorkBook(excelFileName, is); + ReportPage reportPage = new ExcelSheet(workbook.getSheetAt(0)); + checkReportFormat(excelFileName, reportPage); + Attributes attributes = new Attributes( + reportPage, + excelFileName, + getReportEndDateTime(reportPage), + getPortfolio(reportPage)); + return new ExcelAttributes(workbook, attributes); } - private static void checkReportFormat(Path path, ReportPage reportPage) { + private static void checkReportFormat(String excelFileName, ReportPage reportPage) { if (reportPage.findByPrefix(UNIQ_TEXT, 1, 2) == TableCellAddress.NOT_FOUND) { - throw new RuntimeException("В файле " + path + " не содержится отчет брокера ВТБ"); + throw new RuntimeException("В файле " + excelFileName + " не содержится отчет брокера ВТБ"); } } private static String getPortfolio(ReportPage reportPage) { try { - Object account = reportPage.getNextColumnValue(ACCOUNT_MARKER); - Object subAccount = reportPage.getNextColumnValue(SUBACCOUNT_MARKER); - if (account instanceof Number) account = ((Number) account).intValue(); - if (subAccount instanceof Number) subAccount = ((Number) subAccount).intValue(); - if (account.equals(subAccount)) { + @Nullable Object account = reportPage.getNextColumnValue(ACCOUNT_MARKER); + @Nullable Object subAccount = reportPage.getNextColumnValue(SUBACCOUNT_MARKER); + if (account instanceof Number) { + account = ((Number) account).intValue(); + } + if (subAccount instanceof Number) { + subAccount = ((Number) subAccount).intValue(); + } + if (Objects.equals(account, subAccount)) { return String.valueOf(account); } else { return account + "-" + subAccount; @@ -78,13 +83,14 @@ private static String getPortfolio(ReportPage reportPage) { } } - private Instant getReportEndDateTime(ReportPage reportPage) { + private static Instant getReportEndDateTime(ReportPage reportPage) { try { TableCellAddress address = reportPage.findByPrefix(REPORT_DATE_MARKER, 1, 2); + @SuppressWarnings({"nullness", "DataFlowIssue"}) String value = reportPage.getCell(address) .getStringValue() .split(" ")[9]; - return convertToInstant(value) + return convertToInstantWithRussianFormatAndMoscowZoneId(value) .plus(LAST_TRADE_HOUR, ChronoUnit.HOURS); } catch (Exception e) { throw new IllegalArgumentException( @@ -95,9 +101,4 @@ private Instant getReportEndDateTime(ReportPage reportPage) { public static String convertToCurrency(String value) { return value.replace("RUR", "RUB"); // vtb uses RUR (used till 1998) code in reports } - - @Override - public void close() throws IOException { - this.book.close(); - } } diff --git a/src/main/java/ru/investbook/parser/vtb/VtbCashFlowTable.java b/src/main/java/ru/investbook/parser/vtb/VtbCashFlowTable.java index 8cfe95a0..410ea6ce 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbCashFlowTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.vtb; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.springframework.util.StringUtils; @@ -37,7 +38,7 @@ public VtbCashFlowTable(CashFlowEventTable cashFlowEventTable) { @Override protected Collection getRow(CashFlowEventTable.CashFlowEvent event) { - CashFlowType type = event.getEventType(); + @Nullable CashFlowType type = event.getEventType(); if (type != CASH && type != TAX) { return Collections.emptyList(); } diff --git a/src/main/java/ru/investbook/parser/vtb/VtbCouponAmortizationRedemptionTable.java b/src/main/java/ru/investbook/parser/vtb/VtbCouponAmortizationRedemptionTable.java index a95f9113..61e74197 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbCouponAmortizationRedemptionTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbCouponAmortizationRedemptionTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.vtb; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.pojo.Security; @@ -36,6 +37,7 @@ import java.util.regex.Pattern; import static java.lang.Double.parseDouble; +import static java.util.Objects.requireNonNull; import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; import static org.spacious_team.broker.pojo.CashFlowType.*; import static ru.investbook.parser.vtb.VtbBrokerReport.minValue; @@ -64,7 +66,7 @@ protected VtbCouponAmortizationRedemptionTable(CashFlowEventTable cashFlowEventT @Override protected Collection getRow(CashFlowEventTable.CashFlowEvent event) { - CashFlowType eventType = event.getEventType(); + @Nullable CashFlowType eventType = event.getEventType(); if (eventType != COUPON && eventType != AMORTIZATION && eventType != REDEMPTION) { return Collections.emptyList(); } @@ -83,7 +85,7 @@ protected Collection getRow(CashFlowEventTable.CashFlowEv .orElseThrow(() -> new IllegalArgumentException("Не удалось определить количество погашенных облигаций " + security.getIsin())); default -> throw new UnsupportedOperationException(); }; - int securityId = getReport().getSecurityRegistrar().declareBondByIsin(security.getIsin(), security::toBuilder); + int securityId = getReport().getSecurityRegistrar().declareBondByIsin(requireNonNull(security.getIsin()), security::toBuilder); Instant instant = event.getDate(); if (eventType != COUPON) { // gh-510: чтобы отличать налог на купон, событие налога и купона сдвигаем по времени от амортизации (погашения) @@ -117,7 +119,7 @@ private Security getSecurity(String description) { for (Pattern pattern : registrationNumberPatterns) { Matcher matcher = pattern.matcher(description); if (matcher.find()) { - String regNumber = matcher.group(1); + String regNumber = requireNonNull(matcher.group(1)); Optional security = securityRegNumberRegistrar.getSecurityByRegistrationNumber(regNumber); if (security.isPresent()) { return security.get(); @@ -130,7 +132,8 @@ private Security getSecurity(String description) { private static BigDecimal getCouponPerOneBond(String description) { Matcher matcher = couponPerOneBondPattern.matcher(description); if (matcher.find()) { - return BigDecimal.valueOf(parseDouble(matcher.group(1))); + String value = requireNonNull(matcher.group(1)); + return BigDecimal.valueOf(parseDouble(value)); } throw new IllegalArgumentException("Не смогу выделить размер купона на одну облигацию из описания: " + description); } @@ -138,7 +141,8 @@ private static BigDecimal getCouponPerOneBond(String description) { private static BigDecimal getAmortizationPerOneBond(String description) { Matcher matcher = amortizationPerOneBondPattern.matcher(description); if (matcher.find()) { - return BigDecimal.valueOf(parseDouble(matcher.group(1))); + String value = requireNonNull(matcher.group(1)); + return BigDecimal.valueOf(parseDouble(value)); } throw new IllegalArgumentException("Не смогу выделить размер купона на одну облигацию из описания: " + description); } diff --git a/src/main/java/ru/investbook/parser/vtb/VtbDerivativeCashFlowTable.java b/src/main/java/ru/investbook/parser/vtb/VtbDerivativeCashFlowTable.java index 634212dc..c45aa0ff 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbDerivativeCashFlowTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbDerivativeCashFlowTable.java @@ -20,17 +20,20 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; import org.spacious_team.table_wrapper.api.TableHeaderColumn; +import ru.investbook.parser.SingleBrokerReport; import java.util.Collection; import java.util.Collections; import java.util.List; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.CashFlowType.DERIVATIVE_PROFIT; import static ru.investbook.parser.vtb.VtbDerivativeCashFlowTable.OpenContractsTableHeader.CONTRACT; @@ -38,22 +41,24 @@ public class VtbDerivativeCashFlowTable extends AbstractVtbCashFlowTable { private static final String TABLE_NAME = "Открытые позиции по Производным финансовым инструментам"; - private final Integer contractId; + private final @Nullable Integer contractId; public VtbDerivativeCashFlowTable(CashFlowEventTable cashFlowEventTable) { super(cashFlowEventTable); - List contracts = getReport().getReportPage() + @SuppressWarnings("method.invocation") + SingleBrokerReport report = getReport(); + List contracts = report.getReportPage() .create(TABLE_NAME, OpenContractsTableHeader.class) .getData(row -> row.getStringCellValue(CONTRACT)); this.contractId = (contracts.size() == 1) ? - getReport().getSecurityRegistrar().declareDerivative(contracts.get(0)) : + report.getSecurityRegistrar().declareDerivative(requireNonNull(contracts.getFirst())) : null; if (contracts.size() > 1) { log.warn(""" Отчет {} содержит информацию о вариационной марже разных контрактов, \ ВТБ не указывает вариационную маржу с привязкой к контракту, \ вариационная маржа не может быть извлечена - """, getReport()); + """, report); } } @@ -66,14 +71,14 @@ protected Collection parseTable() { @Override protected Collection getRow(CashFlowEventTable.CashFlowEvent event) { - CashFlowType eventType = event.getEventType(); + @Nullable CashFlowType eventType = event.getEventType(); if (eventType != DERIVATIVE_PROFIT) { return Collections.emptyList(); } return singletonList(SecurityEventCashFlow.builder() .portfolio(getReport().getPortfolio()) .timestamp(event.getDate()) - .security(contractId) + .security(requireNonNull(contractId)) .eventType(eventType) .value(event.getValue()) .currency(event.getCurrency()) diff --git a/src/main/java/ru/investbook/parser/vtb/VtbDividendTable.java b/src/main/java/ru/investbook/parser/vtb/VtbDividendTable.java index 0b5cc499..1ab9b5a6 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbDividendTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbDividendTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.vtb; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.springframework.util.StringUtils; @@ -31,6 +32,7 @@ import java.util.regex.Pattern; import static java.lang.Double.parseDouble; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.CashFlowType.DIVIDEND; import static ru.investbook.parser.vtb.VtbBrokerReport.minValue; @@ -45,7 +47,7 @@ protected VtbDividendTable(CashFlowEventTable cashFlowEventTable) { @Override protected Collection getRow(CashFlowEventTable.CashFlowEvent event) { - CashFlowType eventType = event.getEventType(); + @Nullable CashFlowType eventType = event.getEventType(); if (eventType != DIVIDEND) { // предположение return Collections.emptyList(); } @@ -76,7 +78,8 @@ static BigDecimal getTax(String description) { Matcher matcher = taxInformationPattern.matcher(description); if (matcher.find()) { try { - return BigDecimal.valueOf(parseDouble(matcher.group(2))); + String number = requireNonNull(matcher.group(2)); + return BigDecimal.valueOf(parseDouble(number)); } catch (Exception e) { log.info("Не смогу выделить сумму налога из описания: {}", description); } diff --git a/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeRateTable.java b/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeRateTable.java index ed780b3f..2d2abd4e 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeRateTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeRateTable.java @@ -65,6 +65,7 @@ protected Collection parseTable() { private Collection buildPortfolioProperty(CurrencyPair currencyPair, String rowHeader) { try { + @SuppressWarnings({"nullness", "DataFlowIssue"}) String value = getReport().getReportPage() .getNextColumnValue(rowHeader) .toString() diff --git a/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeTransactionTable.java b/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeTransactionTable.java index 86cc6652..2cb71645 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeTransactionTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbForeignExchangeTransactionTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.vtb; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.report_parser.api.ForeignExchangeTransaction; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -62,13 +63,13 @@ private Collection parseTable(String tableName) { .getData(getReport(), this::parseRow); } - protected ForeignExchangeTransaction parseRow(TableRow row) { + protected @Nullable ForeignExchangeTransaction parseRow(TableRow row) { boolean isBuy = row.getStringCellValue(DIRECTION).trim().equalsIgnoreCase("Покупка"); BigDecimal value = row.getBigDecimalCellValue(VALUE); if (isBuy) { value = value.negate(); } - BigDecimal marketComission = row.getBigDecimalCellValueOrDefault(MARKET_COMMISSION, null); + @Nullable BigDecimal marketComission = row.getBigDecimalCellValueOrDefault(MARKET_COMMISSION, null); if (marketComission == null) { // сделка не завершена в отчете, в следующем отчете она будет корректно распаршена return null; @@ -91,6 +92,7 @@ protected ForeignExchangeTransaction parseRow(TableRow row) { .build(); } + @Getter enum FxTransactionTableHeader implements TableHeaderColumn { TRADE_ID("№ сделки"), INSTRUMENT("Финансовый инструмент"), @@ -102,7 +104,6 @@ enum FxTransactionTableHeader implements TableHeaderColumn { MARKET_COMMISSION("Комиссия", "за расчет по сделке"), BROKER_COMMISSION("Комиссия", "за заключение сделки"); - @Getter private final TableColumn column; FxTransactionTableHeader(String... words) { diff --git a/src/main/java/ru/investbook/parser/vtb/VtbPortfolioPropertyTable.java b/src/main/java/ru/investbook/parser/vtb/VtbPortfolioPropertyTable.java index 62a9c9cd..4e8d5ef6 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbPortfolioPropertyTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbPortfolioPropertyTable.java @@ -19,6 +19,7 @@ package ru.investbook.parser.vtb; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioProperty; import org.spacious_team.broker.pojo.PortfolioPropertyType; import ru.investbook.parser.SingleBrokerReport; @@ -57,7 +58,7 @@ protected Collection parseTable() { } private BigDecimal getBigDecimalValue() { - Object value = getReport().getReportPage().getNextColumnValue(TOTAL_ASSETS1); + @Nullable Object value = getReport().getReportPage().getNextColumnValue(TOTAL_ASSETS1); if (value == null) { value = getReport().getReportPage().getNextColumnValue(TOTAL_ASSETS2); } diff --git a/src/main/java/ru/investbook/parser/vtb/VtbReportHelper.java b/src/main/java/ru/investbook/parser/vtb/VtbReportHelper.java index eca7413a..d1937a80 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbReportHelper.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbReportHelper.java @@ -18,6 +18,7 @@ package ru.investbook.parser.vtb; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityType; @@ -25,9 +26,9 @@ class VtbReportHelper { static Security getSecurity(String description) { String[] parts = description.split(","); - String name = parts[0].trim(); + @Nullable String name = parts[0].trim(); SecurityType type = SecurityType.STOCK_OR_BOND; - String ticker = null; + @Nullable String ticker = null; if (name.endsWith(" US Equity") || name.endsWith(" US")) { ticker = name.substring(0, name.lastIndexOf(" US")); name = null; diff --git a/src/main/java/ru/investbook/parser/vtb/VtbSecuritiesTable.java b/src/main/java/ru/investbook/parser/vtb/VtbSecuritiesTable.java index 956327fd..43424a52 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbSecuritiesTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbSecuritiesTable.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -31,6 +32,7 @@ import java.util.HashMap; import java.util.Map; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.vtb.VtbSecuritiesTable.VtbSecuritiesTableHeader.NAME_REGNUMBER_ISIN; import static ru.investbook.parser.vtb.VtbSecuritiesTable.VtbSecuritiesTableHeader.SECTION; @@ -46,13 +48,14 @@ protected VtbSecuritiesTable(SingleBrokerReport report) { } @Override - protected Security parseRow(TableRow row) { + protected @Nullable Security parseRow(TableRow row) { if (row.getCellValue(SECTION) == null) { return null; // sub-header row } String description = row.getStringCellValue(NAME_REGNUMBER_ISIN); Security security = VtbReportHelper.getSecurity(description); - int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(security.getIsin(), security::toBuilder); + String isin = requireNonNull(security.getIsin()); + int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(isin, security::toBuilder); security = security.toBuilder().id(securityId).build(); String registrationNumber = description.split(",")[1].toUpperCase().trim(); regNumberToSecurity.put(registrationNumber, security); diff --git a/src/main/java/ru/investbook/parser/vtb/VtbSecurityDepositAndWithdrawalTable.java b/src/main/java/ru/investbook/parser/vtb/VtbSecurityDepositAndWithdrawalTable.java index f1bd6b11..d6d02cdb 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbSecurityDepositAndWithdrawalTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbSecurityDepositAndWithdrawalTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.vtb; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.report_parser.api.SecurityTransaction; import org.spacious_team.table_wrapper.api.TableRow; @@ -31,9 +32,10 @@ import java.util.Optional; import java.util.Set; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.vtb.VtbSecurityFlowTable.VtbSecurityFlowTableHeader.*; -public class VtbSecurityDepositAndWithdrawalTable extends SingleAbstractReportTable { +public class VtbSecurityDepositAndWithdrawalTable extends SingleAbstractReportTable { static final String TABLE_NAME = "Движение ценных бумаг"; @@ -45,7 +47,7 @@ protected VtbSecurityDepositAndWithdrawalTable(SingleBrokerReport report) { } @Override - protected SecurityTransaction parseRow(TableRow row) { + protected @Nullable SecurityTransaction parseRow(TableRow row) { String operation = row.getStringCellValueOrDefault(OPERATION, "").toLowerCase().trim(); switch (operation) { case "перевод цб": // перевод между субсчетами @@ -68,16 +70,17 @@ protected SecurityTransaction parseRow(TableRow row) { String description = row.getStringCellValue(NAME_REGNUMBER_ISIN); Security security = VtbReportHelper.getSecurity(description); Instant timestamp = row.getInstantCellValue(DATE); - String tradeId = generateTradeId(portfolio, timestamp, security.getIsin()); - int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(security.getIsin(), security::toBuilder); + String isin = requireNonNull(security.getIsin()); + String tradeId = generateTradeId(portfolio, timestamp, isin); + int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(isin, security::toBuilder); return SecurityTransaction.builder() - .tradeId(tradeId) - .timestamp(timestamp) - .portfolio(portfolio) - .security(securityId) - .count(row.getIntCellValue(COUNT)) - .build(); + .tradeId(tradeId) + .timestamp(timestamp) + .portfolio(portfolio) + .security(securityId) + .count(row.getIntCellValue(COUNT)) + .build(); } private String generateTradeId(String portfolio, Instant instant, String isin) { @@ -92,8 +95,9 @@ private String generateTradeId(String portfolio, Instant instant, String isin) { throw new RuntimeException("Can't generate trade id"); } - public Optional getBondRedemptionCount(String isin) { + public Optional getBondRedemptionCount(@Nullable String isin) { initializeIfNeed(); - return Optional.ofNullable(bondRedemptions.get(isin)); + return Optional.ofNullable(isin) + .map(bondRedemptions::get); } } diff --git a/src/main/java/ru/investbook/parser/vtb/VtbSecurityFlowTable.java b/src/main/java/ru/investbook/parser/vtb/VtbSecurityFlowTable.java index cc5eff7b..a395fcf5 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbSecurityFlowTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbSecurityFlowTable.java @@ -31,6 +31,7 @@ import java.util.HashMap; import java.util.Map; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.vtb.VtbSecurityFlowTable.VtbSecurityFlowTableHeader.NAME_REGNUMBER_ISIN; public class VtbSecurityFlowTable extends SingleAbstractReportTable { @@ -46,7 +47,8 @@ protected VtbSecurityFlowTable(SingleBrokerReport report) { protected Security parseRow(TableRow row) { String description = row.getStringCellValue(NAME_REGNUMBER_ISIN); Security security = VtbReportHelper.getSecurity(description); - int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(security.getIsin(), security::toBuilder); + String isin = requireNonNull(security.getIsin()); + int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(isin, security::toBuilder); security = security.toBuilder().id(securityId).build(); String registrationNumber = description.split(",")[1].toUpperCase().trim(); regNumberToSecurity.put(registrationNumber, security); diff --git a/src/main/java/ru/investbook/parser/vtb/VtbSecurityQuoteTable.java b/src/main/java/ru/investbook/parser/vtb/VtbSecurityQuoteTable.java index aa05152d..f0232e08 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbSecurityQuoteTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbSecurityQuoteTable.java @@ -18,6 +18,7 @@ package ru.investbook.parser.vtb; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.table_wrapper.api.TableRow; @@ -27,6 +28,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static ru.investbook.parser.vtb.VtbBrokerReport.minValue; import static ru.investbook.parser.vtb.VtbSecuritiesTable.VtbSecuritiesTableHeader.*; @@ -39,31 +41,33 @@ public VtbSecurityQuoteTable(SingleBrokerReport report) { } @Override - protected SecurityQuote parseRow(TableRow row) { - BigDecimal quote = row.getBigDecimalCellValueOrDefault(QUOTE, null); + protected @Nullable SecurityQuote parseRow(TableRow row) { + @Nullable BigDecimal quote = row.getBigDecimalCellValueOrDefault(QUOTE, null); if (quote == null) { return null; } - BigDecimal price = ofNullable(row.getBigDecimalCellValueOrDefault(FACE_VALUE, null)) + @Nullable BigDecimal price = ofNullable(row.getBigDecimalCellValueOrDefault(FACE_VALUE, null)) .filter(faceValue -> faceValue.compareTo(minValue) > 0) .map(faceValue -> faceValue.multiply(quote).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)) .orElse(null); - BigDecimal accruedInterest = null; + @Nullable BigDecimal accruedInterest = null; if (price != null) { // имеет смысл только для облигаций, для акций price = null accruedInterest = ofNullable(row.getBigDecimalCellValueOrDefault(ACCRUED_INTEREST, null)) .filter(interest -> interest.compareTo(minValue) > 0) // otherwise outgoing count may be = 0 .map(interest -> { int count = row.getIntCellValueOrDefault(OUTGOING, -1); + //noinspection ReturnOfNull return (count <= 0) ? null : interest.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP); }).orElse(null); } - String currency = ofNullable(row.getStringCellValueOrDefault(CURRENCY, null)) + @Nullable String currency = ofNullable(row.getStringCellValueOrDefault(CURRENCY, null)) .map(VtbBrokerReport::convertToCurrency) .orElse(null); String description = row.getStringCellValue(NAME_REGNUMBER_ISIN); Security security = VtbReportHelper.getSecurity(description); - int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(security.getIsin(), security::toBuilder); + String isin = requireNonNull(security.getIsin()); + int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(isin, security::toBuilder); return SecurityQuote.builder() .security(securityId) .timestamp(getReport().getReportEndDateTime()) diff --git a/src/main/java/ru/investbook/parser/vtb/VtbSecurityTransactionTable.java b/src/main/java/ru/investbook/parser/vtb/VtbSecurityTransactionTable.java index fea77e9a..dbe9a99a 100644 --- a/src/main/java/ru/investbook/parser/vtb/VtbSecurityTransactionTable.java +++ b/src/main/java/ru/investbook/parser/vtb/VtbSecurityTransactionTable.java @@ -33,6 +33,7 @@ import java.util.HashSet; import java.util.Set; +import static java.util.Objects.requireNonNull; import static ru.investbook.parser.vtb.VtbBrokerReport.minValue; import static ru.investbook.parser.vtb.VtbSecurityTransactionTable.VtbSecurityTransactionTableHeader.*; @@ -68,7 +69,7 @@ protected SecurityTransaction parseRow(TableRow row) { Security security = VtbReportHelper.getSecurity(row.getStringCellValue(NAME_AND_ISIN)); securities.add(security); - int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(security.getIsin(), security::toBuilder); + int securityId = getReport().getSecurityRegistrar().declareStockOrBondByIsin(requireNonNull(security.getIsin()), security::toBuilder); return SecurityTransaction.builder() .timestamp(row.getInstantCellValue(DATE)) diff --git a/src/main/java/ru/investbook/report/DerivativeEvents.java b/src/main/java/ru/investbook/report/DerivativeEvents.java index b42cab1f..0a1b3987 100644 --- a/src/main/java/ru/investbook/report/DerivativeEvents.java +++ b/src/main/java/ru/investbook/report/DerivativeEvents.java @@ -20,11 +20,11 @@ import lombok.Builder; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.pojo.Transaction; import org.spacious_team.broker.pojo.TransactionCashFlow; -import org.springframework.lang.Nullable; import java.math.BigDecimal; import java.util.ArrayList; @@ -39,9 +39,7 @@ public class DerivativeEvents { @Getter @Builder public static class DerivativeDailyEvents { - @Nullable - private final SecurityEventCashFlow dailyProfit; - @Nullable + private final @Nullable SecurityEventCashFlow dailyProfit; private final LinkedHashMap> dailyTransactions; private final BigDecimal totalProfit; private final int position; diff --git a/src/main/java/ru/investbook/report/DerivativeEventsFactory.java b/src/main/java/ru/investbook/report/DerivativeEventsFactory.java index c02b108f..83e5a9ae 100644 --- a/src/main/java/ru/investbook/report/DerivativeEventsFactory.java +++ b/src/main/java/ru/investbook/report/DerivativeEventsFactory.java @@ -19,6 +19,7 @@ package ru.investbook.report; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.Security; @@ -51,6 +52,8 @@ import java.util.stream.Collectors; import static java.util.Collections.singleton; +import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; @Component @RequiredArgsConstructor @@ -68,19 +71,22 @@ public DerivativeEvents getDerivativeEvents(Portfolio portfolio, Security contra Deque transactions = getTransactions(portfolio, contract, filter); Map securityEventCashFlows = getSecurityEventCashFlows(portfolio, contract, filter); - LocalDate firstEventDate = getContractFirstEventDate(transactions, securityEventCashFlows); - LocalDate lastEventDate = Optional.ofNullable(getContractLastEventDate(transactions, securityEventCashFlows)) + @Nullable LocalDate firstEventDate = getContractFirstEventDate(transactions, securityEventCashFlows); + @Nullable LocalDate lastEventDate = Optional.ofNullable(getContractLastEventDate(transactions, securityEventCashFlows)) .orElse(firstEventDate); DerivativeEvents derivativeEvents = new DerivativeEvents(); BigDecimal totalProfit = BigDecimal.ZERO; int currentPosition = 0; - LocalDate currentDay = firstEventDate; - while (currentDay != null && currentDay.compareTo(lastEventDate) <= 0) { + @Nullable LocalDate currentDay = firstEventDate; + //noinspection ConstantValue + while (nonNull(currentDay) && nonNull(lastEventDate) && currentDay.compareTo(lastEventDate) <= 0) { Deque dailyTransactions = getDailyTransactions(transactions, currentDay); - SecurityEventCashFlow cash = securityEventCashFlows.get(currentDay); + @Nullable SecurityEventCashFlow cash = securityEventCashFlows.get(currentDay); + //noinspection ConstantValue if ((dailyTransactions != null && !dailyTransactions.isEmpty()) || (cash != null && !cash.getValue().equals(BigDecimal.ZERO))) { + //noinspection OptionalOfNullableMisuse currentPosition += Optional.ofNullable(dailyTransactions) .orElse(new LinkedList<>()) .stream() @@ -104,7 +110,7 @@ public DerivativeEvents getDerivativeEvents(Portfolio portfolio, Security contra private LinkedList getTransactions(Portfolio portfolio, Security contract, ViewFilter filter) { return transactionRepository .findBySecurityIdAndPortfolioInAndTimestampBetweenOrderByTimestampAscTradeIdAsc( - contract.getId(), + requireNonNull(contract.getId()), singleton(portfolio.getId()), filter.getFromDate(), filter.getToDate()) @@ -117,7 +123,7 @@ private Map getSecurityEventCashFlows(Portfoli return securityEventCashFlowRepository .findByPortfolioIdInAndSecurityIdAndCashFlowTypeIdAndTimestampBetweenOrderByTimestampAsc( singleton(portfolio.getId()), - contract.getId(), + requireNonNull(contract.getId()), CashFlowType.DERIVATIVE_PROFIT.getId(), filter.getFromDate(), filter.getToDate()) @@ -126,25 +132,25 @@ private Map getSecurityEventCashFlows(Portfoli .collect(Collectors.toMap(e -> getTradeDay(e.getTimestamp()), Function.identity())); } - private LocalDate getContractFirstEventDate(Deque transactions, + private @Nullable LocalDate getContractFirstEventDate(Deque transactions, Map securityEventCashFlows) { - LocalDate firstTransactionDate, firstEventDate; + @Nullable LocalDate firstTransactionDate, firstEventDate; firstEventDate = firstTransactionDate = Optional.ofNullable(transactions.peekFirst()) .map(t -> ZonedDateTime.ofInstant(t.getTimestamp(), MOEX_TIMEZONE).toLocalDate()) .orElse(null); ArrayList cashFlows = new ArrayList<>(securityEventCashFlows.keySet()); if (!cashFlows.isEmpty()) { Collections.sort(cashFlows); - LocalDate firstCashFlowDate = cashFlows.get(0); + @Nullable LocalDate firstCashFlowDate = cashFlows.getFirst(); firstEventDate = (firstTransactionDate == null || firstCashFlowDate.compareTo(firstTransactionDate) < 0) ? firstCashFlowDate : firstTransactionDate; } return firstEventDate; } - private LocalDate getContractLastEventDate(Deque transactions, + private @Nullable LocalDate getContractLastEventDate(Deque transactions, Map securityEventCashFlows) { - LocalDate lastTransactionDate, lastEventDate; + @Nullable LocalDate lastTransactionDate, lastEventDate; lastEventDate = lastTransactionDate = Optional.ofNullable(transactions.peekLast()) .map(Transaction::getTimestamp) .map(DerivativeEventsFactory::getTradeDay) @@ -152,7 +158,7 @@ private LocalDate getContractLastEventDate(Deque transactions, ArrayList cashFlows = new ArrayList<>(securityEventCashFlows.keySet()); if (!cashFlows.isEmpty()) { Collections.sort(cashFlows); - LocalDate lastCashFlowDate = cashFlows.get(cashFlows.size() - 1); + @Nullable LocalDate lastCashFlowDate = cashFlows.getLast(); lastEventDate = (lastTransactionDate == null || lastCashFlowDate.compareTo(lastTransactionDate) > 0) ? lastCashFlowDate : lastTransactionDate; } @@ -177,6 +183,7 @@ private static LocalDate getTradeDay(Instant instant) { private LinkedHashMap> getCashFlows(Deque dailyTransactions) { LinkedHashMap> dailyTransactionsCashFlows = new LinkedHashMap<>(); + //noinspection ConstantValue if (dailyTransactions != null) { for (Transaction transaction : dailyTransactions) { if (transaction.getId() != null) { @@ -189,7 +196,7 @@ private LinkedHashMap> getCa private Map getTransactionCashFlows(Transaction transaction) { return transactionCashFlowRepository - .findByTransactionId(transaction.getId()) + .findByTransactionId(requireNonNull(transaction.getId())) .stream() .map(transactionCashFlowConverter::fromEntity) .collect(Collectors.toMap(TransactionCashFlow::getEventType, Function.identity())); diff --git a/src/main/java/ru/investbook/report/FifoPositions.java b/src/main/java/ru/investbook/report/FifoPositions.java index 1a4f3a16..6a856e13 100644 --- a/src/main/java/ru/investbook/report/FifoPositions.java +++ b/src/main/java/ru/investbook/report/FifoPositions.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.pojo.Transaction; @@ -34,6 +35,7 @@ import static java.lang.Integer.min; import static java.lang.Integer.signum; import static java.lang.Math.abs; +import static java.util.Objects.nonNull; @Getter @Slf4j @@ -49,53 +51,60 @@ public class FifoPositions { public FifoPositions(Deque transactions, Deque redemptions) { this.transactions = transactions; this.redemptions = redemptions; - updateSecuritiesPastPositions(transactions); - processTransactions(transactions); - processRedemptions(redemptions); + updateSecuritiesPastPositions(transactions, positionHistories); + processTransactions(transactions, openedPositions, closedPositions); + processRedemptions(redemptions, positionHistories, openedPositions, closedPositions); this.currentOpenedPositionsCount = Optional.ofNullable(positionHistories.peekLast()) .map(PositionHistory::getOpenedPositions) .orElse(0); } - private void processTransactions(Deque transactions) { + private static void processTransactions(Deque transactions, + Deque openedPositions, + Deque closedPositions) { for (Transaction transaction : transactions) { - if (isIncreasePosition(transaction)) { - this.openedPositions.add(new OpenedPosition(transaction)); + if (isIncreasePosition(transaction, openedPositions)) { + openedPositions.add(new OpenedPosition(transaction)); } else { - closePositions(transaction, CashFlowType.PRICE); + closePositions(transaction, CashFlowType.PRICE, openedPositions, closedPositions); } } } - private void processRedemptions(Deque redemptions) { - if (!redemptions.isEmpty() && (redemptions.peek() != null)) { + private static void processRedemptions(Deque redemptions, + Deque positionHistories, + Deque openedPositions, + Deque closedPositions) { + //noinspection ConstantValue + if (!redemptions.isEmpty() && nonNull(redemptions.peek())) { int security = redemptions.peek().getSecurity(); LinkedList redemptionTransactions = redemptions.stream() .map(FifoPositions::convertBondRedemptionToTransaction) .collect(Collectors.toCollection(LinkedList::new)); - updateSecuritiesPastPositions(redemptionTransactions); + updateSecuritiesPastPositions(redemptionTransactions, positionHistories); redemptionTransactions.forEach( - redemption -> closePositions(redemption, CashFlowType.REDEMPTION)); - if (!this.openedPositions.isEmpty() || this.positionHistories.getLast().getOpenedPositions() != 0) { + redemption -> closePositions(redemption, CashFlowType.REDEMPTION, openedPositions, closedPositions)); + if (!openedPositions.isEmpty() || positionHistories.getLast().getOpenedPositions() != 0) { log.error("Предоставлены не все транзакции по бумаге " + security + ", в истории портфеля есть событие погашения номинала облигаций по " + redemptions.stream().mapToInt(SecurityEventCashFlow::getCount).sum() + - " бумагам, однако в портфеле остались " + this.positionHistories.getLast().getOpenedPositions() + + " бумагам, однако в портфеле остались " + positionHistories.getLast().getOpenedPositions() + " открытые позиции"); } } } - private void updateSecuritiesPastPositions(Queue transactions) { - int openedPosition = (!this.positionHistories.isEmpty()) ? this.positionHistories.peekLast().getOpenedPositions() : 0; + private static void updateSecuritiesPastPositions(Queue transactions, Deque positionHistories) { + @SuppressWarnings("dereference.of.nullable") // is never null for not empty deque + int openedPosition = !positionHistories.isEmpty() ? positionHistories.peekLast().getOpenedPositions() : 0; for (Transaction transaction : transactions) { openedPosition += transaction.getCount(); - this.positionHistories.add(new PositionHistory(transaction, openedPosition)); + positionHistories.add(new PositionHistory(transaction, openedPosition)); } } - private boolean isIncreasePosition(Transaction transaction) { - OpenedPosition position = openedPositions.peek(); + private static boolean isIncreasePosition(Transaction transaction, Deque openedPositions) { + @Nullable OpenedPosition position = openedPositions.peek(); return position == null || position.getUnclosedPositions() == 0 || (signum(transaction.getCount()) == signum(position.getUnclosedPositions())); @@ -104,10 +113,14 @@ private boolean isIncreasePosition(Transaction transaction) { /** * @param closing position decreasing transaction */ - private void closePositions(Transaction closing, CashFlowType closingEvent) { + private static void closePositions(Transaction closing, + CashFlowType closingEvent, + Deque openedPositions, + Deque closedPositions) { int closingCount = abs(closing.getCount()); while (!openedPositions.isEmpty() && closingCount > 0) { - OpenedPosition opening = openedPositions.peek(); + @SuppressWarnings("assignment") + OpenedPosition opening = openedPositions.peek(); // is never null for not empty deque int openedCount = abs(opening.getUnclosedPositions()); if (openedCount <= closingCount) { openedPositions.remove(); diff --git a/src/main/java/ru/investbook/report/FifoPositionsFactory.java b/src/main/java/ru/investbook/report/FifoPositionsFactory.java index fd4fc295..62750993 100644 --- a/src/main/java/ru/investbook/report/FifoPositionsFactory.java +++ b/src/main/java/ru/investbook/report/FifoPositionsFactory.java @@ -44,6 +44,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.SecurityType.CURRENCY_PAIR; @Component @@ -64,7 +65,8 @@ public FifoPositions get(Security security, Portfolio portfolio) { } public FifoPositions get(Security security, FifoPositionsFilter filter) { - return get(security.getId(), security.getType(), filter); + int securityId = requireNonNull(security.getId()); + return get(securityId, security.getType(), filter); } /** diff --git a/src/main/java/ru/investbook/report/ForeignExchangeRateService.java b/src/main/java/ru/investbook/report/ForeignExchangeRateService.java index 652e5e38..8f857250 100644 --- a/src/main/java/ru/investbook/report/ForeignExchangeRateService.java +++ b/src/main/java/ru/investbook/report/ForeignExchangeRateService.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityQuote.SecurityQuoteBuilder; import org.spacious_team.broker.pojo.SecurityType; @@ -56,6 +57,7 @@ public class ForeignExchangeRateService { // base-currency -> quote-currency -> local date -> exchange-rate private final Map>> cacheByDate = new ConcurrentHashMap<>(); @Value("${server.port}") + @SuppressWarnings("unused") private int serverPort; /** @@ -69,7 +71,7 @@ public BigDecimal getExchangeRate(String baseCurrency, String quoteCurrency) { if (baseCurrency.equalsIgnoreCase(quoteCurrency)) { return BigDecimal.ONE; } - BigDecimal exchangeRate = getFromCache(baseCurrency, quoteCurrency); + @Nullable BigDecimal exchangeRate = getFromCache(baseCurrency, quoteCurrency); if (exchangeRate != null) { return exchangeRate; } else if (baseCurrency.equalsIgnoreCase("RUB")) { @@ -141,7 +143,7 @@ public BigDecimal getExchangeRate(String baseCurrency, String quoteCurrency, Loc if (baseCurrency.equalsIgnoreCase(quoteCurrency)) { return BigDecimal.ONE; } - BigDecimal exchangeRate = getFromCache(baseCurrency, quoteCurrency, atDate); + @Nullable BigDecimal exchangeRate = getFromCache(baseCurrency, quoteCurrency, atDate); if (exchangeRate != null) { return exchangeRate; } else if (baseCurrency.equalsIgnoreCase("RUB")) { @@ -216,12 +218,12 @@ private void cache(String baseCurrency, String quoteCurrency, LocalDate localDat .putIfAbsent(localDate, exchangeRate); } - private BigDecimal getFromCache(String baseCurrency, String quoteCurrency) { + private @Nullable BigDecimal getFromCache(String baseCurrency, String quoteCurrency) { return this.cache.computeIfAbsent(baseCurrency, k -> new ConcurrentHashMap<>()) .get(quoteCurrency); } - private BigDecimal getFromCache(String baseCurrency, String quoteCurrency, LocalDate localDate) { + private @Nullable BigDecimal getFromCache(String baseCurrency, String quoteCurrency, LocalDate localDate) { return this.cacheByDate.computeIfAbsent(baseCurrency, k -> new ConcurrentHashMap<>()) .computeIfAbsent(quoteCurrency, k -> new ConcurrentHashMap<>()) .get(localDate); @@ -263,7 +265,7 @@ public BigDecimal convertValueToCurrency(BigDecimal value, String fromCurrency, * курсу по-умолчанию, если официальный курс не известен. */ public SecurityQuote convertQuoteToCurrency(SecurityQuote quote, String toCurrency, SecurityType securityType) { - String fromCurrency = quote.getCurrency(); + @Nullable String fromCurrency = quote.getCurrency(); if (fromCurrency == null || fromCurrency.equalsIgnoreCase(toCurrency)) { return quote; } else { @@ -277,13 +279,15 @@ private static SecurityQuoteBuilder convertQuoteToCurrency(SecurityQuote quote, Supplier exchangeRateSupplier, SecurityType securityType) { BigDecimal exchangeRate = exchangeRateSupplier.get(); - BigDecimal accruedInterest = quote.getAccruedInterest(); + BigDecimal quoteValue = quote.getQuote(); + @Nullable BigDecimal price = quote.getPrice(); + @Nullable BigDecimal accruedInterest = quote.getAccruedInterest(); boolean nonCurrencyQuote = (securityType == BOND) || ((securityType == STOCK_OR_BOND) && (accruedInterest != null)) || (securityType == DERIVATIVE); return quote.toBuilder() - .quote(nonCurrencyQuote ? quote.getQuote() : quote.getQuote().multiply(exchangeRate)) - .price((quote.getPrice() == null) ? null : quote.getPrice().multiply(exchangeRate)) + .quote(nonCurrencyQuote ? quoteValue : quoteValue.multiply(exchangeRate)) + .price((price == null) ? null : price.multiply(exchangeRate)) .accruedInterest((accruedInterest == null) ? null : accruedInterest.multiply(exchangeRate)); } } diff --git a/src/main/java/ru/investbook/report/InternalRateOfReturn.java b/src/main/java/ru/investbook/report/InternalRateOfReturn.java index 19d2af62..e4116523 100644 --- a/src/main/java/ru/investbook/report/InternalRateOfReturn.java +++ b/src/main/java/ru/investbook/report/InternalRateOfReturn.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.decampo.xirr.NewtonRaphson; import org.decampo.xirr.Xirr; import org.spacious_team.broker.pojo.Security; @@ -42,6 +43,7 @@ import java.util.Set; import java.util.stream.Collectors; +import static java.util.Objects.requireNonNull; import static java.util.Optional.empty; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toCollection; @@ -70,11 +72,11 @@ public class InternalRateOfReturn { * Возвращает внутреннюю норму доходности вложений. Не рассчитывается для срочных инструментов, т.к. * вложение (гарантийное обеспечение) не хранится на данный момент в БД. * - * @param quote may be null only if current security position is zero - * @return internal rate of return if can be calculated or null otherwise + * @param quote may be null, if current security position equals to 0. Otherwise, null result is returned + * @return internal rate of return, if it can be calculated, or null otherwise */ - public Double calc( - Collection portfolios, Security security, SecurityQuote quote, Instant fromDate, Instant toDate) { + public @Nullable Double calc( + Collection portfolios, Security security, @Nullable SecurityQuote quote, Instant fromDate, Instant toDate) { try { boolean isDerivative = (security.getType() == DERIVATIVE); @@ -115,7 +117,7 @@ public Double calc( private String getTransactionCurrency(FifoPositions positions) { return positions.getTransactions() .stream() - .map(t -> transactionCashFlowRepository.findByTransactionIdAndCashFlowType(t.getId(), PRICE)) + .map(t -> transactionCashFlowRepository.findByTransactionIdAndCashFlowType(requireNonNull(t.getId()), PRICE)) .flatMap(Optional::stream) .map(TransactionCashFlowEntity::getCurrency) .findAny() @@ -136,21 +138,23 @@ private org.decampo.xirr.Transaction castToXirrTransaction(SecurityEventCashFlow toLocalDate(cashFlowEntity.getTimestamp())); } - private Optional castToXirrTransaction(SecurityQuote quote, + @SuppressWarnings("dereference.of.nullable") + private Optional castToXirrTransaction(@Nullable SecurityQuote quote, String toCurrency, int positionCount, SecurityType securityType) { return ofNullable(quote) .map(_quote -> _quote.getDirtyPriceInCurrency(securityType == DERIVATIVE)) - .map(dirtyPrice -> convertToCurrency(dirtyPrice, quote.getCurrency(), toCurrency)) + .map(dirtyPrice -> convertToCurrency(dirtyPrice, requireNonNull(quote.getCurrency()), toCurrency)) .map(dirtyPrice -> new org.decampo.xirr.Transaction( positionCount * dirtyPrice.doubleValue(), toLocalDate(quote.getTimestamp()))); } private Optional getTransactionValue(Transaction t, String toCurrency) { - BigDecimal value = null; - if (t.getId() != null) { // bond redemption, accounted by other way, skipping - value = transactionCashFlowRepository.findByTransactionId(t.getId()) + @Nullable BigDecimal value = null; + @Nullable Integer transactionId = t.getId(); + if (transactionId != null) { // bond redemption, accounted by other way, skipping + value = transactionCashFlowRepository.findByTransactionId(transactionId) .stream() .map(entity -> convertToCurrency(entity.getValue(), entity.getCurrency(), toCurrency)) .reduce(BigDecimal.ZERO, BigDecimal::add); @@ -164,14 +168,14 @@ public List getSecurityEventCashFlowEntities(Collec return portfolios.isEmpty() ? securityEventCashFlowRepository .findBySecurityIdAndCashFlowTypeIdInAndTimestampBetweenOrderByTimestampAsc( - security.getId(), + requireNonNull(security.getId()), cashFlowTypes, ViewFilter.get().getFromDate(), ViewFilter.get().getToDate()) : securityEventCashFlowRepository .findByPortfolioIdInAndSecurityIdAndCashFlowTypeIdInAndTimestampBetweenOrderByTimestampAsc( portfolios, - security.getId(), + requireNonNull(security.getId()), cashFlowTypes, ViewFilter.get().getFromDate(), ViewFilter.get().getToDate()); diff --git a/src/main/java/ru/investbook/report/PaidInterest.java b/src/main/java/ru/investbook/report/PaidInterest.java index b7172430..b8c67857 100644 --- a/src/main/java/ru/investbook/report/PaidInterest.java +++ b/src/main/java/ru/investbook/report/PaidInterest.java @@ -20,6 +20,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.spacious_team.broker.pojo.Transaction; @@ -49,7 +50,7 @@ Map> get(CashFlowType type) { } public List get(CashFlowType payType, Position position) { - List value = this.get(payType).get(position); + @Nullable List value = this.get(payType).get(position); return (value != null) ? value : Collections.emptyList(); } diff --git a/src/main/java/ru/investbook/report/PaidInterestFactory.java b/src/main/java/ru/investbook/report/PaidInterestFactory.java index 00f7adf1..2b9ab5bf 100644 --- a/src/main/java/ru/investbook/report/PaidInterestFactory.java +++ b/src/main/java/ru/investbook/report/PaidInterestFactory.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.Security; @@ -44,6 +45,7 @@ import java.util.stream.Collectors; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.CashFlowType.*; @Component @@ -68,7 +70,7 @@ private PaidInterest create( for (CashFlowType type : PAY_TYPES) { paidInterest.get(type) .putAll(getPositionWithPayments( - portfolio, security.getId(), positions, type, fromDate, toDate)); + portfolio, requireNonNull(security.getId()), positions, type, fromDate, toDate)); } return paidInterest; } @@ -102,12 +104,12 @@ private Map> getPositionWithPayments(Strin getPayments(cash, paidPositions).forEach((position, cashs) -> cashs.forEach(securityCash -> - payments.computeIfAbsent(position, p -> new ArrayList<>()) + payments.computeIfAbsent(position, _ -> new ArrayList<>()) .add(securityCash))); } catch (Exception e) { log.warn("{}, выплата будет отображена в отчете по фиктивной позиции покупки ЦБ от даты {}", e.getMessage(), PaidInterest.fictitiousPositionInstant); - payments.computeIfAbsent(PaidInterest.getFictitiousPositionPayment(cash), key -> new ArrayList<>()) + payments.computeIfAbsent(PaidInterest.getFictitiousPositionPayment(cash), _ -> new ArrayList<>()) .add(cash); } } @@ -125,7 +127,7 @@ private Instant getBookClosureDate(Deque positionHistories, Sec Iterator it = positionHistories.descendingIterator(); // дата перечисления дивидендов/купонов Эмитентом (дата фиксации реестра акционеров) // с точностью до временного интервала между 2-мя соседними транзакции - Instant bookClosureDate = null; + @Nullable Instant bookClosureDate = null; while (it.hasNext()) { PositionHistory positionHistory = it.next(); Instant pastInstant = positionHistory.getInstant(); @@ -164,7 +166,7 @@ private Map> getPayments(SecurityEventCash BigDecimal pay = payPerOne .multiply(BigDecimal.valueOf(count)) .setScale(6, RoundingMode.HALF_UP); - payments.computeIfAbsent(position, key -> new ArrayList<>()) + payments.computeIfAbsent(position, _ -> new ArrayList<>()) .add(cash.toBuilder() .count(count) .value(pay) diff --git a/src/main/java/ru/investbook/report/Table.java b/src/main/java/ru/investbook/report/Table.java index b9acbdbc..82c7aa03 100644 --- a/src/main/java/ru/investbook/report/Table.java +++ b/src/main/java/ru/investbook/report/Table.java @@ -18,6 +18,8 @@ package ru.investbook.report; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.HashMap; import java.util.LinkedList; @@ -37,7 +39,7 @@ public static Record newRecord() { return new Record(); } - public static class Record extends HashMap { + public static class Record extends HashMap { public static Record EMPTY = new Record(); public Record() { diff --git a/src/main/java/ru/investbook/report/ViewFilter.java b/src/main/java/ru/investbook/report/ViewFilter.java index ed812785..6180fc18 100644 --- a/src/main/java/ru/investbook/report/ViewFilter.java +++ b/src/main/java/ru/investbook/report/ViewFilter.java @@ -21,6 +21,7 @@ import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; +import org.checkerframework.checker.nullness.qual.Nullable; import ru.investbook.web.model.ViewFilterModel; import java.time.Instant; @@ -32,12 +33,13 @@ import java.util.function.Supplier; import static java.time.ZoneId.systemDefault; +import static java.util.Objects.requireNonNull; @Getter @Builder(toBuilder = true) @EqualsAndHashCode public class ViewFilter { - private static final ThreadLocal filters = ThreadLocal.withInitial(() -> null); + private static final ThreadLocal<@Nullable ViewFilter> filters = ThreadLocal.withInitial(() -> null); public static final Instant defaultFromDate = Instant.ofEpochSecond(0); private static final Function toInstant = date -> date.atStartOfDay(systemDefault()).toInstant(); @@ -78,7 +80,7 @@ public static void set(ViewFilter viewFilter) { } public static ViewFilter get() { - return filters.get(); + return requireNonNull(filters.get(), "ViewFilter is not set"); } public static void remove() { diff --git a/src/main/java/ru/investbook/report/excel/CashFlowExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/CashFlowExcelTableFactory.java index b8268f15..3bfed98a 100644 --- a/src/main/java/ru/investbook/report/excel/CashFlowExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/CashFlowExcelTableFactory.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.pojo.Portfolio; @@ -100,13 +101,13 @@ private void appendCurrencyInfo(Portfolio portfolio, Table table) { public void appendCashBalance(Portfolio portfolio, Table table) { Map currencyToValues = getCashBalances(portfolio); - Table.Record rubCashBalanceRecord = null; + Table.@Nullable Record rubCashBalanceRecord = null; for (Table.Record record : table) { - String currency = Optional.ofNullable((String) record.get(CURRENCY_NAME)) + @Nullable String currency = Optional.ofNullable((String) record.get(CURRENCY_NAME)) .map(String::toUpperCase) .orElse(null); if (currency != null) { - BigDecimal value = currencyToValues.get(currency); + @Nullable BigDecimal value = currencyToValues.get(currency); record.put(CASH_BALANCE, value); } else { rubCashBalanceRecord = record; @@ -114,7 +115,7 @@ public void appendCashBalance(Portfolio portfolio, Table table) { } } // print RUB after all already printed currencies - BigDecimal rubCash = currencyToValues.get("RUB"); + @Nullable BigDecimal rubCash = currencyToValues.get("RUB"); if (rubCash != null) { if (rubCashBalanceRecord == null) { rubCashBalanceRecord = new Table.Record(); diff --git a/src/main/java/ru/investbook/report/excel/CashFlowExcelTableView.java b/src/main/java/ru/investbook/report/excel/CashFlowExcelTableView.java index 72faeb39..2bbef144 100644 --- a/src/main/java/ru/investbook/report/excel/CashFlowExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/CashFlowExcelTableView.java @@ -25,6 +25,7 @@ import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; +import org.checkerframework.checker.nullness.qual.Nullable; import org.decampo.xirr.NewtonRaphson; import org.decampo.xirr.Transaction; import org.decampo.xirr.Xirr; @@ -76,7 +77,7 @@ public CashFlowExcelTableView(PortfolioRepository portfolioRepository, } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(CASH.ordinal(), 17 * 256); sheet.setColumnWidth(CASH_RUB.ordinal(), 22 * 256); @@ -123,14 +124,14 @@ private double getXirrProfitInPercent(Table table, BigDecimal liquidationValueRu } private Optional castRecordToXirrTransaction(Table.Record record) { - Object cash = record.get(CASH); - Object currency = record.get(CURRENCY); - Double cashInRub = null; + @Nullable Object cash = record.get(CASH); + @Nullable Object currency = record.get(CURRENCY); + @Nullable Double cashInRub = null; if (cash instanceof Number n && currency instanceof String cur) { cashInRub = n.doubleValue() * foreignExchangeRateService.getExchangeRateToRub(cur).doubleValue(); } - Object date = record.get(DATE); - Transaction transaction = (cashInRub != null && date instanceof Instant instant) ? + @Nullable Object date = record.get(DATE); + @Nullable Transaction transaction = (cashInRub != null && date instanceof Instant instant) ? new Transaction(cashInRub, LocalDate.ofInstant(instant, ZoneId.systemDefault())) : null; return Optional.ofNullable(transaction); diff --git a/src/main/java/ru/investbook/report/excel/CellStyles.java b/src/main/java/ru/investbook/report/excel/CellStyles.java index 00f1a0a8..3412b011 100644 --- a/src/main/java/ru/investbook/report/excel/CellStyles.java +++ b/src/main/java/ru/investbook/report/excel/CellStyles.java @@ -38,6 +38,7 @@ public class CellStyles { private final CellStyle intStyle; private final CellStyle percentStyle; + @SuppressWarnings("method.invocation") public CellStyles(Workbook book) { this.defaultStyle = createDefaultStyle(book); this.headerStyle = createHeaderStyle(book); diff --git a/src/main/java/ru/investbook/report/excel/CommissionExcelTableView.java b/src/main/java/ru/investbook/report/excel/CommissionExcelTableView.java index 2f4a9436..53c26058 100644 --- a/src/main/java/ru/investbook/report/excel/CommissionExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/CommissionExcelTableView.java @@ -53,7 +53,7 @@ public CommissionExcelTableView(PortfolioRepository portfolioRepository, } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(COMMISSION.ordinal(), 30 * 256); sheet.setColumnWidth(DESCRIPTION.ordinal(), 75 * 256); diff --git a/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableFactory.java index d2089f8d..7df03398 100644 --- a/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableFactory.java @@ -19,6 +19,7 @@ package ru.investbook.report.excel; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.Security; @@ -94,6 +95,7 @@ private Table getContractProfit(Security contract, DerivativeEvents derivativeEv Table.Record record = new Table.Record(); contractProfit.add(record); LinkedHashMap> dailyTransactions = dailyEvents.getDailyTransactions(); + //noinspection ConstantValue if (dailyTransactions != null) { boolean isFirstRowOfDay = true; for (Map.Entry> e : dailyTransactions.entrySet()) { @@ -126,7 +128,7 @@ record = new Table.Record(); isFirstRowOfDay = false; } } - SecurityEventCashFlow dailyProfit = dailyEvents.getDailyProfit(); + @Nullable SecurityEventCashFlow dailyProfit = dailyEvents.getDailyProfit(); if (dailyProfit != null) { record.put(DATE, dailyProfit.getTimestamp()); record.put(DERIVATIVE_PROFIT_DAY, dailyProfit.getValue()); diff --git a/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableView.java b/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableView.java index e28a78cf..b778255f 100644 --- a/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/DerivativesMarketProfitExcelTableView.java @@ -54,7 +54,7 @@ public DerivativesMarketProfitExcelTableView(PortfolioRepository portfolioReposi } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(CONTRACT.ordinal(), 24 * 256); sheet.setColumnWidth(AMOUNT.ordinal(), 18 * 256); diff --git a/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableFactory.java index a4b64619..2e551ed9 100644 --- a/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableFactory.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.Security; @@ -52,6 +53,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.*; import static org.spacious_team.broker.pojo.CashFlowType.DERIVATIVE_PROFIT; @@ -148,6 +150,7 @@ private Table.Record getSecurityStatus(Collection portfolios, String con row.put(LAST_TRANSACTION_DATE, ofNullable(transactions.peekLast()) .map(Transaction::getTimestamp) .orElse(null)); + //noinspection DataFlowIssue row.put(LAST_EVENT_DATE, getLastEventDate(portfolios, contracts)); row.put(BUY_COUNT, transactions .stream() @@ -182,13 +185,13 @@ private Deque getTransactions(Collection portfolios, Set positionsFactory.getTransactions(contract.getId(), pf)) + .map(contract -> positionsFactory.getTransactions(requireNonNull(contract.getId()), pf)) .flatMap(Collection::stream) .sorted(Comparator.comparing(Transaction::getTimestamp)) .collect(toCollection(LinkedList::new)); } - private Instant getLastEventDate(Collection portfolios, Collection contracts) { + private @Nullable Instant getLastEventDate(Collection portfolios, Collection contracts) { ViewFilter filter = ViewFilter.get(); return contracts.stream() .map(contract -> securityProfitService.getLastEventTimestamp( diff --git a/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableView.java b/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableView.java index fc78563c..49a0ac89 100644 --- a/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/DerivativesMarketTotalProfitExcelTableView.java @@ -51,6 +51,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import static ru.investbook.report.ForeignExchangeRateService.RUB; import static ru.investbook.report.excel.DerivativesMarketTotalProfitExcelTableHeader.*; @@ -133,9 +134,10 @@ private ExcelTable createExcelTables(Portfolio portfolio, String sheetName, Stri } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); - for (TableHeader header : headerType.getEnumConstants()) { + TableHeader[] tableHeader = requireNonNull(headerType.getEnumConstants()); + for (TableHeader header : tableHeader) { sheet.setColumnWidth(header.ordinal(), 16 * 256); } sheet.setColumnWidth(CONTRACT_GROUP.ordinal(), 24 * 256); diff --git a/src/main/java/ru/investbook/report/excel/ExcelChartPlotHelper.java b/src/main/java/ru/investbook/report/excel/ExcelChartPlotHelper.java index 6f8ae8e3..ea5b12fe 100644 --- a/src/main/java/ru/investbook/report/excel/ExcelChartPlotHelper.java +++ b/src/main/java/ru/investbook/report/excel/ExcelChartPlotHelper.java @@ -82,6 +82,7 @@ static XDDFChartData createScatterChartData(XSSFChart chart) { static XDDFChartData createPieChartData(XSSFChart chart) { setPieChartStyle(chart); + @SuppressWarnings("argument") XDDFChartData data = chart.createData(ChartTypes.PIE, null, null); data.setVaryColors(true); return data; diff --git a/src/main/java/ru/investbook/report/excel/ExcelTable.java b/src/main/java/ru/investbook/report/excel/ExcelTable.java index 3d80b320..576be00a 100644 --- a/src/main/java/ru/investbook/report/excel/ExcelTable.java +++ b/src/main/java/ru/investbook/report/excel/ExcelTable.java @@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Workbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Portfolio; import ru.investbook.report.Table; @@ -30,7 +31,7 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Slf4j public class ExcelTable { - private final Portfolio portfolio; + private final @Nullable Portfolio portfolio; private final String sheetName; private final Table table; private final ExcelTableView creator; diff --git a/src/main/java/ru/investbook/report/excel/ExcelTableView.java b/src/main/java/ru/investbook/report/excel/ExcelTableView.java index 4185c63e..c533e7b7 100644 --- a/src/main/java/ru/investbook/report/excel/ExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/ExcelTableView.java @@ -30,6 +30,7 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellRangeAddress; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Portfolio; import org.springframework.beans.factory.annotation.Value; import ru.investbook.converter.PortfolioConverter; @@ -47,12 +48,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static java.util.Objects.requireNonNull; +import static org.springframework.util.CollectionUtils.isEmpty; import static ru.investbook.report.excel.StockMarketProfitExcelTableHeader.ROW_NUM_PLACE_HOLDER; @Slf4j @@ -100,15 +102,16 @@ protected Collection getPortfolios(Collection allowedPo /** * Thread safe method + * * @param portfolio accept null or portfolio */ - public void createSheet(Portfolio portfolio, - Workbook book, - String sheetName, - Table table, - CellStyles styles) { + public & TableHeader> void createSheet(@Nullable Portfolio portfolio, + Workbook book, + String sheetName, + Table table, + CellStyles styles) { if (table == null || table.isEmpty()) return; - Class headerType = getHeaderType(table); + @Nullable Class headerType = getHeaderType(table); if (headerType == null) return; synchronized (book) { long t0 = System.nanoTime(); @@ -120,10 +123,11 @@ public void createSheet(Portfolio portfolio, table.addFirst(totalRow); } int rowNum = 0; - for (Map tableRow : table) { + for (Table.Record tableRow : table) { Row row = sheet.createRow(++rowNum); - for (TableHeader header : headerType.getEnumConstants()) { - Object value = tableRow.get(header); + TableHeader[] tableHeader = requireNonNull(headerType.getEnumConstants()); + for (TableHeader header : tableHeader) { + @Nullable Object value = tableRow.get(header); if (value == null) { continue; } @@ -170,13 +174,17 @@ public void createSheet(Portfolio portfolio, } } - private Class getHeaderType(Table table) { + private & TableHeader> @Nullable Class getHeaderType(Table table) { for (Table.Record record : table) { - if (record.isEmpty()) continue; - return record.keySet() - .iterator() - .next() - .getClass(); + if (!isEmpty(record)) { + //noinspection unchecked + return (Class) record.keySet() + .stream() + .map(TableHeader::getClass) + .filter(Class::isEnum) + .findAny() + .orElseThrow(); + } } return null; } @@ -185,11 +193,12 @@ private String validateExcelSheetName(String name) { return invalidExcelSheetNameChars.matcher(name).replaceAll("-"); } - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { Row row = sheet.createRow(0); - row.setHeight((short)-1); + row.setHeight((short) -1); CreationHelper createHelper = sheet.getWorkbook().getCreationHelper(); - for (TableHeader header : headerType.getEnumConstants()) { + TableHeader[] tableHeader = requireNonNull(headerType.getEnumConstants()); + for (TableHeader header : tableHeader) { Cell cell = row.createCell(header.ordinal()); cell.setCellValue(header.getDescription()); cell.setCellStyle(style); @@ -223,6 +232,7 @@ protected void sheetPreCreate(Sheet sheet, Table table) { } protected void sheetPostCreate(Sheet sheet, Class headerType, CellStyles styles) { - sheet.setAutoFilter(new CellRangeAddress(0, sheet.getLastRowNum(), 0, (headerType.getEnumConstants().length - 1))); + TableHeader[] tableHeader = requireNonNull(headerType.getEnumConstants()); + sheet.setAutoFilter(new CellRangeAddress(0, sheet.getLastRowNum(), 0, (tableHeader.length - 1))); } } diff --git a/src/main/java/ru/investbook/report/excel/ExcelView.java b/src/main/java/ru/investbook/report/excel/ExcelView.java index 93e55081..ad3a0dbf 100644 --- a/src/main/java/ru/investbook/report/excel/ExcelView.java +++ b/src/main/java/ru/investbook/report/excel/ExcelView.java @@ -26,13 +26,10 @@ import ru.investbook.report.ViewFilter; import java.io.OutputStream; -import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.stream.Collectors; import static java.lang.Math.min; @@ -59,27 +56,22 @@ public void create(OutputStream out, ViewFilter filter) { } @Transactional(readOnly = true) - public void writeTo(Workbook book, ViewFilter filter, CellStyles styles) throws InterruptedException, ExecutionException { - ExecutorService tableWriterExecutor = Executors.newSingleThreadExecutor(); - Collection> sheetWriterFutures = new ArrayList<>(); - int cpuCnt = Runtime.getRuntime().availableProcessors(); - List usedExcelTableViews = getExcelTableViews(filter); - for (int idx = 0, delta = 1; idx < usedExcelTableViews.size(); ) { - int fromIndex = idx; - idx += delta; - delta = cpuCnt; - int toIndex = min(idx, usedExcelTableViews.size()); - List tables = usedExcelTableViews.subList(fromIndex, toIndex) - .parallelStream() - .map(excelTableView -> getExcelTables(excelTableView, filter)) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - Future future = tableWriterExecutor.submit(() -> writeExcelTables(tables, book, styles)); - sheetWriterFutures.add(future); - } - - for(Future future : sheetWriterFutures) { - future.get(); + public void writeTo(Workbook book, ViewFilter filter, CellStyles styles) { + try (ExecutorService tableWriterExecutor = Executors.newSingleThreadExecutor()) { + int cpuCnt = Runtime.getRuntime().availableProcessors(); + List usedExcelTableViews = getExcelTableViews(filter); + for (int idx = 0, delta = 1; idx < usedExcelTableViews.size(); ) { + int fromIndex = idx; + idx += delta; + delta = cpuCnt; + int toIndex = min(idx, usedExcelTableViews.size()); + List tables = usedExcelTableViews.subList(fromIndex, toIndex) + .parallelStream() + .map(excelTableView -> getExcelTables(excelTableView, filter)) + .flatMap(Collection::stream) + .toList(); + tableWriterExecutor.submit(() -> writeExcelTables(tables, book, styles)); + } } if (book.getNumberOfSheets() == 0) { diff --git a/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableFactory.java index c371cc3c..ff149444 100644 --- a/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableFactory.java @@ -19,6 +19,7 @@ package ru.investbook.report.excel; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.Transaction; @@ -104,9 +105,12 @@ private Table.Record getOpenedPositionProfit(OpenedPosition position) { Transaction transaction = position.getOpenTransaction(); row.put(OPEN_DATE, transaction.getTimestamp()); row.put(COUNT, Math.abs(position.getCount()) * Integer.signum(transaction.getCount())); + //noinspection DataFlowIssue row.put(OPEN_PRICE, getTransactionCashFlow(transaction, CashFlowType.PRICE, 1d / transaction.getCount())); double multiplier = Math.abs(1d * position.getCount() / transaction.getCount()); + //noinspection DataFlowIssue row.put(OPEN_AMOUNT, getTransactionCashFlow(transaction, CashFlowType.PRICE, multiplier)); + //noinspection DataFlowIssue row.put(OPEN_COMMISSION, getTransactionCashFlow(transaction, CashFlowType.FEE, multiplier)); return row; } @@ -118,7 +122,7 @@ private Table.Record getClosedPositionProfit(ClosedPosition position) { Transaction transaction = position.getCloseTransaction(); double multiplier = Math.abs(1d * position.getCount() / transaction.getCount()); row.put(CLOSE_DATE, transaction.getTimestamp()); - BigDecimal closeAmount; + @Nullable BigDecimal closeAmount; if (position.getClosingEvent() == CashFlowType.PRICE) { closeAmount = getTransactionCashFlow(transaction, CashFlowType.PRICE, multiplier); } else { @@ -126,6 +130,7 @@ private Table.Record getClosedPositionProfit(ClosedPosition position) { " не может быть закрыта событием типа " + position.getClosingEvent()); } row.put(CLOSE_AMOUNT, closeAmount); + //noinspection DataFlowIssue row.put(CLOSE_COMMISSION, getTransactionCashFlow(transaction, CashFlowType.FEE, multiplier)); boolean isLongPosition = isLongPosition(position); row.put(FORECAST_TAX, getForecastTax(isLongPosition)); @@ -138,12 +143,13 @@ private boolean isLongPosition(ClosedPosition position) { return position.getOpenTransaction().getCount() > 0; } - private BigDecimal getTransactionCashFlow(Transaction transaction, CashFlowType type, double multiplier) { - if (transaction.getId() == null) { + private @Nullable BigDecimal getTransactionCashFlow(Transaction transaction, CashFlowType type, double multiplier) { + @Nullable Integer transactionId = transaction.getId(); + if (transactionId == null) { return null; } return transactionCashFlowRepository - .findByTransactionIdAndCashFlowType(transaction.getId(), type) + .findByTransactionIdAndCashFlowType(transactionId, type) .map(cash -> cash.getValue() .multiply(BigDecimal.valueOf(multiplier)) .abs() diff --git a/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableView.java b/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableView.java index 8afc161b..1fa1f631 100644 --- a/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/ForeignMarketProfitExcelTableView.java @@ -34,6 +34,7 @@ import java.util.Optional; import java.util.function.UnaryOperator; +import static java.util.Objects.requireNonNull; import static ru.investbook.report.excel.ExcelFormulaHelper.sumAbsValues; import static ru.investbook.report.excel.ForeignMarketProfitExcelTableHeader.*; @@ -54,9 +55,10 @@ public ForeignMarketProfitExcelTableView(PortfolioRepository portfolioRepository } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); - for (TableHeader header : headerType.getEnumConstants()) { + TableHeader[] tableHeader = requireNonNull(headerType.getEnumConstants()); + for (TableHeader header : tableHeader) { sheet.setColumnWidth(header.ordinal(), 18 * 256); } sheet.setColumnWidth(CURRENCY_PAIR.ordinal(), 20 * 256); diff --git a/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableFactory.java index e50a7849..3ae3cc8c 100644 --- a/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableFactory.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.pojo.Portfolio; @@ -35,7 +36,6 @@ import java.time.LocalDate; import java.time.Month; import java.time.ZoneId; -import java.time.format.TextStyle; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -43,6 +43,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.time.format.TextStyle.FULL_STANDALONE; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.CashFlowType.*; import static ru.investbook.report.excel.ForeignPortfolioPaymentExcelTableHeader.*; import static ru.investbook.report.excel.TaxExcelTableFactory.isDividendOrCouponTax; @@ -86,7 +88,7 @@ private Table getTable(List cashFlows) { table.add(new Table.Record()); Table.Record monthTotalRecord = new Table.Record(); table.add(monthTotalRecord); - Month month = null; + @MonotonicNonNull Month month = null; int sumRowCount = 0; for (EventCashFlow cash : cashFlows) { Instant timestamp = cash.getTimestamp(); @@ -111,7 +113,7 @@ private Table getTable(List cashFlows) { table.add(record); sumRowCount++; } - calcTotalRecord(monthTotalRecord, month, sumRowCount); + calcTotalRecord(monthTotalRecord, requireNonNull(month), sumRowCount); if (!cashFlows.isEmpty()) { foreignExchangeRateTableFactory.appendExchangeRates(table, CURRENCY_NAME, EXCHANGE_RATE); } @@ -121,7 +123,7 @@ private Table getTable(List cashFlows) { private void calcTotalRecord(Table.Record monthTotalRecord, Month month, int sumRowCount) { if (sumRowCount != 0) { - monthTotalRecord.put(DATE, StringUtils.capitalize(month.getDisplayName(TextStyle.FULL_STANDALONE, Locale.getDefault()))); + monthTotalRecord.put(DATE, StringUtils.capitalize(month.getDisplayName(FULL_STANDALONE, Locale.getDefault()))); monthTotalRecord.put(CASH_RUB, "=SUM(OFFSET(" + CASH_RUB.getCellAddr() + ",1,0," + sumRowCount + ",1))"); } } diff --git a/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableView.java b/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableView.java index b5354e61..c0a8a818 100644 --- a/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/ForeignPortfolioPaymentExcelTableView.java @@ -53,7 +53,7 @@ public ForeignPortfolioPaymentExcelTableView(PortfolioRepository portfolioReposi } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(CASH_RUB.ordinal(), 18 * 256); sheet.setColumnWidth(DESCRIPTION.ordinal(), 120 * 256); diff --git a/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableFactory.java index 33da847f..84de13ac 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableFactory.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.PortfolioCash; @@ -62,6 +63,7 @@ import static java.lang.Double.parseDouble; import static java.util.Collections.singleton; import static java.util.Comparator.comparing; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.*; import static org.spacious_team.broker.pojo.CashFlowType.CASH; import static org.spacious_team.broker.pojo.PortfolioPropertyType.TOTAL_ASSETS_RUB; @@ -132,10 +134,12 @@ private Table createTable(List cashFlows, private void addInvestmentColumns(List cashFlows, Table table) { for (EventCashFlow cashFlow : cashFlows) { Table.Record record = recordOf(table, cashFlow.getTimestamp(), cashFlow.getCurrency()); - record.merge(INVESTMENT_AMOUNT, cashFlow.getValue(), (v1, v2) -> ((BigDecimal) v1).add(((BigDecimal) v2))); - record.computeIfAbsent(INVESTMENT_AMOUNT_USD, $ -> foreignExchangeRateTableFactory + record.merge(INVESTMENT_AMOUNT, cashFlow.getValue(), (v1, v2) -> + requireNonNull((BigDecimal) v1) + .add(requireNonNull(((BigDecimal) v2)))); + record.computeIfAbsent(INVESTMENT_AMOUNT_USD, _ -> foreignExchangeRateTableFactory .cashConvertToUsdExcelFormula(cashFlow.getCurrency(), INVESTMENT_AMOUNT, EXCHANGE_RATE)); - record.computeIfAbsent(TOTAL_INVESTMENT_USD, $ -> + record.computeIfAbsent(TOTAL_INVESTMENT_USD, _ -> "=SUM(" + INVESTMENT_AMOUNT_USD.getColumnIndex() + "3:" + INVESTMENT_AMOUNT_USD.getCellAddr() + ")"); } } @@ -174,12 +178,12 @@ private void addAssetsColumns(LinkedHashMap totalAssets, Ta private void addAssetsGrowthColumn(Table table) { try { double usdToRubExchangeRate = foreignExchangeRateService.getExchangeRateToRub("USD").doubleValue(); - Double divider = null; + @Nullable Double divider = null; double investmentUsd = 0; - Double prevAssetsGrownValue = null; + @Nullable Double prevAssetsGrownValue = null; for (Table.Record record : table) { investmentUsd += getInvestmentUsd(record, usdToRubExchangeRate); - Number assetsRub = (Number) record.get(ASSETS_RUB); + @Nullable Number assetsRub = (Number) record.get(ASSETS_RUB); if (assetsRub != null) { double assetsUsd = assetsRub.doubleValue() / usdToRubExchangeRate; divider = updateDivider(divider, assetsUsd, investmentUsd, prevAssetsGrownValue); @@ -196,9 +200,9 @@ private void addAssetsGrowthColumn(Table table) { } private double getInvestmentUsd(Table.Record record, double usdToRubExchangeRate) { - BigDecimal investment = (BigDecimal) record.get(INVESTMENT_AMOUNT); + @Nullable BigDecimal investment = (BigDecimal) record.get(INVESTMENT_AMOUNT); if (investment != null) { - String fromCurrency = record.get(INVESTMENT_CURRENCY).toString(); + String fromCurrency = requireNonNull(record.get(INVESTMENT_CURRENCY)).toString(); if (Objects.equals(fromCurrency, "RUB")) { // используем тот же коэффициент-курс для приведения, что и в вызывающем цикле return investment.doubleValue() / usdToRubExchangeRate; @@ -211,7 +215,7 @@ private double getInvestmentUsd(Table.Record record, double usdToRubExchangeRate return 0; } - private Double updateDivider(Double divider, double assetsUsd, double investmentUsd, Double prevAssetsGrowth) { + private @Nullable Double updateDivider(@Nullable Double divider, double assetsUsd, double investmentUsd, @Nullable Double prevAssetsGrowth) { if (divider == null) { if (prevAssetsGrowth == null) { // начинаем график роста активов с нулевой отметки @@ -232,8 +236,9 @@ private Double updateDivider(Double divider, double assetsUsd, double investment private static void addSp500GrowthColumn(Map sp500, Table table) { boolean isSp500ValueKnown = false; for (Table.Record record : table) { - LocalDate date = (LocalDate) record.get(DATE); - BigDecimal value = sp500.get(date); + @Nullable LocalDate date = (LocalDate) record.get(DATE); + @SuppressWarnings("argument") + @Nullable BigDecimal value = sp500.get(date); if (value != null) { isSp500ValueKnown = true; record.put(SP500, value); diff --git a/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableView.java b/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableView.java index b586e2ff..f0b9c383 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioAnalysisExcelTableView.java @@ -88,7 +88,7 @@ private static boolean showOnlySummary(ViewFilter filter) { } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(INVESTMENT_CURRENCY.ordinal(), 17 * 256); sheet.setColumnWidth(ASSETS_RUB.ordinal(), 17 * 256); @@ -143,6 +143,7 @@ protected void sheetPostCreate(Sheet sheet, Class headerT plotChart("Остаток денежных средств, USD", sheet, PortfolioAnalysisExcelTableView::addCashBalanceGraph); } + @SuppressWarnings("argument") private static void addInvestmentAndAssetsGraph(String name, Sheet sheet) { int rowCount = sheet.getLastRowNum(); XSSFSheet _sheet = (XSSFSheet) sheet; @@ -166,6 +167,7 @@ private static void addInvestmentAndAssetsGraph(String name, Sheet sheet) { chart.plot(chartData); } + @SuppressWarnings("argument") private static void addPortfolioGrowthGraph(String name, Sheet sheet) { int rowCount = sheet.getLastRowNum(); XSSFSheet _sheet = (XSSFSheet) sheet; diff --git a/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableFactory.java index 5358013d..12455c6b 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableFactory.java @@ -20,6 +20,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.SecurityEventCashFlow; import org.springframework.stereotype.Component; @@ -43,6 +45,7 @@ import java.util.Set; import java.util.stream.Collectors; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.CashFlowType.*; import static ru.investbook.report.excel.PortfolioPaymentExcelTableHeader.*; @@ -85,7 +88,7 @@ private Table getTable(List cashFlows) { table.add(new Table.Record()); Table.Record monthTotalRecord = new Table.Record(); table.add(monthTotalRecord); - Month month = null; + @MonotonicNonNull Month month = null; int sumRowCount = 0; for (SecurityEventCashFlow cash : cashFlows) { Instant timestamp = cash.getTimestamp(); @@ -112,7 +115,7 @@ private Table getTable(List cashFlows) { table.add(record); sumRowCount++; } - calcTotalRecord(monthTotalRecord, month, sumRowCount); + calcTotalRecord(monthTotalRecord, requireNonNull(month), sumRowCount); if (!cashFlows.isEmpty()) { foreignExchangeRateTableFactory.appendExchangeRates(table, CURRENCY_NAME, EXCHANGE_RATE); } @@ -127,7 +130,7 @@ private void calcTotalRecord(Table.Record monthTotalRecord, Month month, int sum } } - private static String getPaymentType(SecurityEventCashFlow cash) { + private static @Nullable String getPaymentType(SecurityEventCashFlow cash) { return switch (cash.getEventType()) { case DIVIDEND -> "Дивиденды"; case COUPON -> "Купоны"; diff --git a/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableView.java b/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableView.java index 705e54fa..949430f2 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioPaymentExcelTableView.java @@ -53,7 +53,7 @@ public PortfolioPaymentExcelTableView(PortfolioRepository portfolioRepository, } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(SECURITY.ordinal(), 45 * 256); sheet.setColumnWidth(CASH_RUB.ordinal(), 18 * 256); diff --git a/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactory.java index 99fabaf6..1b546798 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactory.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.PortfolioCash; @@ -57,6 +58,7 @@ import java.util.stream.Collectors; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static org.spacious_team.broker.pojo.SecurityType.*; import static ru.investbook.report.excel.PortfolioStatusExcelTableFactoryProportionHelper.setCurrentProportionFormula; @@ -196,7 +198,7 @@ private Table.Record getSecurityStatus(Collection portfolios, Security s SecurityType securityType = security.getType(); row.put(SECURITY, securityType == CURRENCY_PAIR ? - getCurrencyPair(security.getTicker()) : + getCurrencyPair(requireNonNull(security.getTicker())) : ofNullable(security.getName()) .or(() -> ofNullable(security.getTicker())) .orElse(security.getIsin())); @@ -232,7 +234,7 @@ private Table.Record getSecurityStatus(Collection portfolios, Security s .mapToInt(SecurityEventCashFlow::getCount) .sum()); - SecurityQuote quote = null; + @Nullable SecurityQuote quote = null; int count = positions.getCurrentOpenedPositionsCount(); row.put(COUNT, count); if (count == 0) { diff --git a/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactoryProportionHelper.java b/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactoryProportionHelper.java index 5aa07a89..a40e9bc7 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactoryProportionHelper.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableFactoryProportionHelper.java @@ -18,6 +18,7 @@ package ru.investbook.report.excel; +import org.checkerframework.checker.nullness.qual.Nullable; import ru.investbook.report.Table; import java.math.BigDecimal; @@ -75,7 +76,7 @@ private static boolean isDerivativeCurrencyPairOrCashBalance(Table.Record record } private static boolean isDerivativeOrCurrencyPair(Table.Record record) { - Object type = record.get(TYPE); + @Nullable Object type = record.get(TYPE); return Objects.equals(type, DERIVATIVE.getDescription()) || Objects.equals(type, CURRENCY_PAIR.getDescription()); } @@ -93,7 +94,7 @@ private static BigDecimal getCurrentAmount(Table.Record record) { .multiply(toBigDecimal(record.get(COUNT))); } - private static BigDecimal toBigDecimal(Object value) { + private static BigDecimal toBigDecimal(@Nullable Object value) { if (value instanceof BigDecimal n) { return n; } diff --git a/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableView.java b/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableView.java index 8331dc5c..fda01a62 100644 --- a/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/PortfolioStatusExcelTableView.java @@ -50,6 +50,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; import static ru.investbook.report.excel.ExcelChartPlotHelper.*; import static ru.investbook.report.excel.ExcelConditionalFormatHelper.highlightNegativeByRed; @@ -129,9 +130,10 @@ private ExcelTable createExcelTables(Portfolio portfolio, String sheetName, Stri } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); - for (TableHeader header : headerType.getEnumConstants()) { + TableHeader[] tableHeader = requireNonNull(headerType.getEnumConstants()); + for (TableHeader header : tableHeader) { sheet.setColumnWidth(header.ordinal(), 15 * 256); } sheet.setColumnWidth(SECURITY.ordinal(), 44 * 256); diff --git a/src/main/java/ru/investbook/report/excel/SecuritiesDepositAndWithdrawalExcelTableView.java b/src/main/java/ru/investbook/report/excel/SecuritiesDepositAndWithdrawalExcelTableView.java index 6297c084..6ed557fc 100644 --- a/src/main/java/ru/investbook/report/excel/SecuritiesDepositAndWithdrawalExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/SecuritiesDepositAndWithdrawalExcelTableView.java @@ -54,7 +54,7 @@ public SecuritiesDepositAndWithdrawalExcelTableView(PortfolioRepository portfoli } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(SECURITY.ordinal(), 45 * 256); sheet.setColumnWidth(COUNT.ordinal(), 18 * 256); diff --git a/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableFactory.java index 4c5663ba..f1675774 100644 --- a/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableFactory.java @@ -19,6 +19,7 @@ package ru.investbook.report.excel; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.Security; @@ -55,6 +56,7 @@ import java.util.stream.Collectors; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static ru.investbook.report.ForeignExchangeRateService.RUB; import static ru.investbook.report.excel.StockMarketProfitExcelTableHeader.*; @@ -146,7 +148,7 @@ private Table.Record getOpenedPositionProfit(T positi Transaction transaction = position.getOpenTransaction(); row.put(OPEN_DATE, transaction.getTimestamp()); row.put(COUNT, Math.abs(position.getCount()) * Integer.signum(transaction.getCount())); - String openPrice = getTransactionCashFlow(transaction, CashFlowType.PRICE, 1d / transaction.getCount(), toCurrency); + @Nullable String openPrice = getTransactionCashFlow(transaction, CashFlowType.PRICE, 1d / transaction.getCount(), toCurrency); if (openPrice == null && (position instanceof ClosedPosition)) { // ЦБ введены, а не куплены, принимаем цену покупки = цене продажи, чтобы не было финансового результата Transaction closeTransaction = ((ClosedPosition) position).getCloseTransaction(); @@ -157,7 +159,9 @@ private Table.Record getOpenedPositionProfit(T positi row.put(OPEN_AMOUNT, "=ABS(" + OPEN_PRICE.getCellAddr() + "*" + COUNT.getCellAddr() + ")"); } double multiplier = Math.abs(1d * position.getCount() / transaction.getCount()); + //noinspection DataFlowIssue row.put(OPEN_ACCRUED_INTEREST, getTransactionCashFlow(transaction, CashFlowType.ACCRUED_INTEREST, multiplier, toCurrency)); + //noinspection DataFlowIssue row.put(OPEN_COMMISSION, getTransactionCashFlow(transaction, CashFlowType.FEE, multiplier, toCurrency)); return row; } @@ -169,7 +173,8 @@ private Table.Record getClosedPositionProfit(ClosedPosition position, String toC Transaction transaction = position.getCloseTransaction(); double multiplier = Math.abs(1d * position.getCount() / transaction.getCount()); row.put(CLOSE_DATE, transaction.getTimestamp()); - String closeAmount = switch (position.getClosingEvent()) { + @SuppressWarnings("switch.expression") + @Nullable String closeAmount = switch (position.getClosingEvent()) { case PRICE -> getTransactionCashFlow(transaction, CashFlowType.PRICE, multiplier, toCurrency); case REDEMPTION -> getRedemptionCashFlow(transaction.getPortfolio(), transaction.getSecurity(), multiplier, toCurrency); default -> throw new IllegalArgumentException("ЦБ " + transaction.getSecurity() + @@ -181,7 +186,9 @@ private Table.Record getClosedPositionProfit(ClosedPosition position, String toC closeAmount = getTransactionCashFlow(position.getOpenTransaction(), CashFlowType.PRICE, withdrawalMultiplier, toCurrency); } row.put(CLOSE_AMOUNT, closeAmount); + //noinspection DataFlowIssue row.put(CLOSE_ACCRUED_INTEREST, getTransactionCashFlow(transaction, CashFlowType.ACCRUED_INTEREST, multiplier, toCurrency)); + //noinspection DataFlowIssue row.put(CLOSE_COMMISSION, getTransactionCashFlow(transaction, CashFlowType.FEE, multiplier, toCurrency)); boolean isLongPosition = isLongPosition(position); row.put(FORECAST_TAX, getForecastTax(isLongPosition)); @@ -196,9 +203,13 @@ private boolean isLongPosition(ClosedPosition position) { private Table.Record getPaidInterestProfit(Position position, PaidInterest paidInterest, String toCurrency) { Table.Record info = new Table.Record(); + //noinspection DataFlowIssue info.put(COUPON, convertPaidInterestToExcelFormula(paidInterest.get(CashFlowType.COUPON, position), toCurrency)); + //noinspection DataFlowIssue info.put(AMORTIZATION, convertPaidInterestToExcelFormula(paidInterest.get(CashFlowType.AMORTIZATION, position), toCurrency)); + //noinspection DataFlowIssue info.put(DIVIDEND, convertPaidInterestToExcelFormula(paidInterest.get(CashFlowType.DIVIDEND, position), toCurrency)); + //noinspection DataFlowIssue info.put(TAX, convertPaidInterestToExcelFormula(paidInterest.get(CashFlowType.TAX, position), toCurrency)); if (!toCurrency.equals(RUB) || !paidInterest.getCurrencies().stream().allMatch(RUB::equals)) { // Если речь о сделках в иностранной валюте или хотя бы одна выплата была в иностранной валюте, @@ -208,12 +219,12 @@ private Table.Record getPaidInterestProfit(Position position, PaidInterest paidI return info; } - private String getTransactionCashFlow(Transaction transaction, CashFlowType type, double multiplier, String toCurrency) { + private @Nullable String getTransactionCashFlow(Transaction transaction, CashFlowType type, double multiplier, String toCurrency) { if (PaidInterest.isFictitiousPositionTransaction(transaction)) { return null; } return transactionCashFlowRepository - .findByTransactionIdAndCashFlowType(transaction.getId(), type) + .findByTransactionIdAndCashFlowType(requireNonNull(transaction.getId()), type) .map(cash -> { BigDecimal value = cash.getValue() .multiply(BigDecimal.valueOf(multiplier)) @@ -224,7 +235,7 @@ private String getTransactionCashFlow(Transaction transaction, CashFlowType type .orElse(null); } - private String getRedemptionCashFlow(String portfolio, Integer securityId, double multiplier, String toCurrency) { + private @Nullable String getRedemptionCashFlow(String portfolio, Integer securityId, double multiplier, String toCurrency) { List cashFlows = securityEventCashFlowRepository .findByPortfolioIdInAndSecurityIdAndCashFlowTypeIdAndTimestampBetweenOrderByTimestampAsc( singleton(portfolio), @@ -254,12 +265,13 @@ private String getTransactionCurrency(Transaction transaction, String fallbackCu return fallbackCurrency; } return transactionCashFlowRepository - .findByTransactionIdAndCashFlowType(transaction.getId(), CashFlowType.PRICE) + .findByTransactionIdAndCashFlowType(requireNonNull(transaction.getId()), CashFlowType.PRICE) .map(TransactionCashFlowEntity::getCurrency) .orElse(fallbackCurrency); } - public String convertPaidInterestToExcelFormula(List pays, String toCurrency) { + private @Nullable String convertPaidInterestToExcelFormula(List pays, String toCurrency) { + //noinspection ConstantValue if (pays == null || pays.isEmpty()) { return null; } diff --git a/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableView.java b/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableView.java index 252898c2..45aaec15 100644 --- a/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/StockMarketProfitExcelTableView.java @@ -80,7 +80,7 @@ private List getCurrencies(Portfolio portfolio) { } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(SECURITY.ordinal(), 45 * 256); sheet.setColumnWidth(OPEN_AMOUNT.ordinal(), 16 * 256); diff --git a/src/main/java/ru/investbook/report/excel/TaxExcelTableFactory.java b/src/main/java/ru/investbook/report/excel/TaxExcelTableFactory.java index c2e079be..40796830 100644 --- a/src/main/java/ru/investbook/report/excel/TaxExcelTableFactory.java +++ b/src/main/java/ru/investbook/report/excel/TaxExcelTableFactory.java @@ -19,6 +19,7 @@ package ru.investbook.report.excel; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.EventCashFlow; import org.spacious_team.broker.pojo.Portfolio; @@ -82,7 +83,7 @@ private void addRecordToTable(Table table, EventCashFlow cash) { table.add(record); } - static boolean isDividendOrCouponTax(String description) { + static boolean isDividendOrCouponTax(@Nullable String description) { if (description == null) { return false; } diff --git a/src/main/java/ru/investbook/report/excel/TaxExcelTableView.java b/src/main/java/ru/investbook/report/excel/TaxExcelTableView.java index ea0ea689..e3d604d4 100644 --- a/src/main/java/ru/investbook/report/excel/TaxExcelTableView.java +++ b/src/main/java/ru/investbook/report/excel/TaxExcelTableView.java @@ -53,7 +53,7 @@ public TaxExcelTableView(PortfolioRepository portfolioRepository, } @Override - protected void writeHeader(Sheet sheet, Class headerType, CellStyle style) { + protected & TableHeader> void writeHeader(Sheet sheet, Class headerType, CellStyle style) { super.writeHeader(sheet, headerType, style); sheet.setColumnWidth(TAX.ordinal(), 30 * 256); sheet.setColumnWidth(TAX_RUB.ordinal(), 30 * 256); diff --git a/src/main/java/ru/investbook/report/html/HtmlView.java b/src/main/java/ru/investbook/report/html/HtmlView.java index 04d337e0..9e9d11b6 100644 --- a/src/main/java/ru/investbook/report/html/HtmlView.java +++ b/src/main/java/ru/investbook/report/html/HtmlView.java @@ -41,6 +41,7 @@ import java.io.OutputStream; import java.util.concurrent.ExecutionException; +import static java.util.Objects.requireNonNull; import static ru.investbook.report.html.ExcelFormulaEvaluatorHelper.evaluateFormulaCells; @Component @@ -95,14 +96,17 @@ private void addCssStyle(Document htmlDocument) { @page { size: 1980px 1400px landscape; } tr { border-bottom: 1pt solid #eee; } """); - htmlDocument.getFirstChild() // html - .getFirstChild() // head - .appendChild(style); + @SuppressWarnings({"assignment", "dereference.of.nullable"}) + Node head = htmlDocument.getFirstChild() // html + .getFirstChild(); // head + head.appendChild(style); } private void addReportFileDownloadLink(Document htmlDocument) { - Node body = htmlDocument.getFirstChild() // html - .getLastChild(); // body + @SuppressWarnings("dereference.of.nullable") + Node body = requireNonNull( + htmlDocument.getFirstChild() // html + .getLastChild()); // body Element div = htmlDocument.createElement("div"); div.setAttribute("style", "float: right"); @@ -124,8 +128,10 @@ private void addReportFileDownloadLink(Document htmlDocument) { } private void addHomeLink(Document htmlDocument) { - Node body = htmlDocument.getFirstChild() // html - .getLastChild(); // body + @SuppressWarnings("dereference.of.nullable") + Node body = requireNonNull( + htmlDocument.getFirstChild() // html + .getLastChild()); // body Element doc = htmlDocument.createElement("a"); doc.setTextContent("[Описание таблиц]"); diff --git a/src/main/java/ru/investbook/repository/RepositoryHelper.java b/src/main/java/ru/investbook/repository/RepositoryHelper.java index 7b58d4e5..ce68fbfe 100644 --- a/src/main/java/ru/investbook/repository/RepositoryHelper.java +++ b/src/main/java/ru/investbook/repository/RepositoryHelper.java @@ -18,25 +18,28 @@ package ru.investbook.repository; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.exception.ConstraintViolationException; import java.sql.SQLException; import java.util.Objects; +import static java.util.Objects.nonNull; + public class RepositoryHelper { /** * May return false positive result if NOT NULL column set by NULL (or for other constraint violations) * for not H2 or MariaDB RDBMS */ - public static boolean isUniqIndexViolationException(Throwable t) { + public static boolean isUniqIndexViolationException(@Nullable Throwable t) { do { if (t instanceof ConstraintViolationException) { // todo Не точное условие, нужно выбирать - Throwable cause = t.getCause(); + @Nullable Throwable cause = t.getCause(); if (cause instanceof SQLException sqlException) { int errorCode = sqlException.getErrorCode(); - String sqlState = sqlException.getSQLState(); + @Nullable String sqlState = sqlException.getSQLState(); String packageName = cause.getClass().getPackageName(); // https://www.h2database.com/javadoc/org/h2/api/ErrorCode.html#DUPLICATE_KEY_1 if (errorCode == 23505 && Objects.equals(packageName, "org.h2.jdbc")) { @@ -50,7 +53,7 @@ public static boolean isUniqIndexViolationException(Throwable t) { } return true; // other databases } - } while ((t = t.getCause()) != null); + } while (nonNull(t) && nonNull(t = t.getCause())); return false; } } diff --git a/src/main/java/ru/investbook/repository/SecurityDescriptionRepository.java b/src/main/java/ru/investbook/repository/SecurityDescriptionRepository.java index 033acb6f..8026a370 100644 --- a/src/main/java/ru/investbook/repository/SecurityDescriptionRepository.java +++ b/src/main/java/ru/investbook/repository/SecurityDescriptionRepository.java @@ -18,6 +18,7 @@ package ru.investbook.repository; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; @@ -30,7 +31,7 @@ public interface SecurityDescriptionRepository extends JpaRepository { @Transactional - default void createOrUpdateSector(int securityId, String sector) { + default void createOrUpdateSector(int securityId, @Nullable String sector) { if (existsById(securityId)) { updateSector(securityId, sector); } else { @@ -41,10 +42,10 @@ default void createOrUpdateSector(int securityId, String sector) { @Transactional @Modifying @Query("UPDATE SecurityDescriptionEntity SET sector = :sector WHERE security = :securityId") - void updateSector(int securityId, String sector); + void updateSector(int securityId, @Nullable String sector); @Transactional - default void createSectorIfNotExists(int securityId, String sector) { + default void createSectorIfNotExists(int securityId, @Nullable String sector) { if (!existsById(securityId)) { SecurityDescriptionEntity entity = new SecurityDescriptionEntity(); entity.setSecurity(securityId); diff --git a/src/main/java/ru/investbook/repository/specs/EventCashFlowEntitySearchSpecification.java b/src/main/java/ru/investbook/repository/specs/EventCashFlowEntitySearchSpecification.java index 1f26cf97..ca210911 100644 --- a/src/main/java/ru/investbook/repository/specs/EventCashFlowEntitySearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/EventCashFlowEntitySearchSpecification.java @@ -23,6 +23,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.EventCashFlowEntity; import ru.investbook.entity.EventCashFlowEntity_; @@ -41,7 +42,7 @@ public class EventCashFlowEntitySearchSpecification implements Specification root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { return Stream.of( filterByPortfolio(root, builder, EventCashFlowEntity_.portfolio, portfolio), filterByDateFrom(root, builder, EventCashFlowEntity_.timestamp, dateFrom), diff --git a/src/main/java/ru/investbook/repository/specs/ForeignExchangeRateSearchSpecification.java b/src/main/java/ru/investbook/repository/specs/ForeignExchangeRateSearchSpecification.java index c1afeb24..a946d128 100644 --- a/src/main/java/ru/investbook/repository/specs/ForeignExchangeRateSearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/ForeignExchangeRateSearchSpecification.java @@ -24,8 +24,8 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; -import org.springframework.lang.Nullable; import ru.investbook.entity.ForeignExchangeRateEntity; import ru.investbook.entity.ForeignExchangeRateEntityPk_; import ru.investbook.entity.ForeignExchangeRateEntity_; @@ -40,10 +40,10 @@ @RequiredArgsConstructor(staticName = "of") public class ForeignExchangeRateSearchSpecification implements Specification { private final String currency; - private final LocalDate date; + private final @Nullable LocalDate date; @Override - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { return Stream.of( filterByCurrency(root, builder), filterByDate(root, builder)) @@ -52,8 +52,7 @@ public Predicate toPredicate(Root root, CriteriaQuery .orElseGet(builder::conjunction); } - @Nullable - private Predicate filterByCurrency(Root root, CriteriaBuilder builder) { + private @Nullable Predicate filterByCurrency(Root root, CriteriaBuilder builder) { if (hasText(currency)) { Path path = root.get(ForeignExchangeRateEntity_.pk) .get(ForeignExchangeRateEntityPk_.CURRENCY_PAIR); @@ -62,8 +61,7 @@ private Predicate filterByCurrency(Root root, Criteri return null; } - @Nullable - private Predicate filterByDate(Root root, CriteriaBuilder builder) { + private @Nullable Predicate filterByDate(Root root, CriteriaBuilder builder) { if (date == null) { return null; } diff --git a/src/main/java/ru/investbook/repository/specs/PortfolioCashSearchSpecification.java b/src/main/java/ru/investbook/repository/specs/PortfolioCashSearchSpecification.java index c29dd92d..9995f3d4 100644 --- a/src/main/java/ru/investbook/repository/specs/PortfolioCashSearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/PortfolioCashSearchSpecification.java @@ -23,6 +23,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.PortfolioCashEntity; import ru.investbook.entity.PortfolioCashEntity_; @@ -31,6 +32,7 @@ import java.util.Objects; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; import static ru.investbook.repository.specs.SpecificationHelper.*; @@ -42,7 +44,8 @@ public class PortfolioCashSearchSpecification implements Specification root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { + requireNonNull(query); return Stream.of( filterByPortfolioName(root, builder, PortfolioCashEntity_.portfolio, portfolio, query), filterByDateFrom(root, builder, PortfolioCashEntity_.timestamp, dateFrom), diff --git a/src/main/java/ru/investbook/repository/specs/PortfolioPropertySearchSpecification.java b/src/main/java/ru/investbook/repository/specs/PortfolioPropertySearchSpecification.java index aaf8b403..0e1f6132 100644 --- a/src/main/java/ru/investbook/repository/specs/PortfolioPropertySearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/PortfolioPropertySearchSpecification.java @@ -23,6 +23,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.PortfolioPropertyEntity; @@ -39,11 +40,11 @@ public class PortfolioPropertySearchSpecification implements Specification { private final String portfolio; private final LocalDate date; - private final PortfolioPropertyType property; + private final @Nullable PortfolioPropertyType property; @Override - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { - String propertyName = (property == null) ? null : property.name(); + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { + @Nullable String propertyName = (property == null) ? null : property.name(); return Stream.of( filterByPortfolio(root, builder, PortfolioPropertyEntity_.portfolio, portfolio), filterByInstantBelongsToDate(root, builder, PortfolioPropertyEntity_.timestamp, date), diff --git a/src/main/java/ru/investbook/repository/specs/SecurityDepositSearchSpecification.java b/src/main/java/ru/investbook/repository/specs/SecurityDepositSearchSpecification.java index bb0d8a4e..522980a7 100644 --- a/src/main/java/ru/investbook/repository/specs/SecurityDepositSearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/SecurityDepositSearchSpecification.java @@ -24,6 +24,7 @@ import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Subquery; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.TransactionCashFlowEntity; import ru.investbook.entity.TransactionCashFlowEntity_; @@ -34,6 +35,7 @@ import java.util.Objects; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; import static lombok.AccessLevel.PRIVATE; @@ -52,7 +54,8 @@ public static SecurityDepositSearchSpecification of(String portfolio, } @Override - public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { + requireNonNull(query); return Stream.of( filterByNullPrice(root, builder, query), specification.toPredicate(root, query, builder)) diff --git a/src/main/java/ru/investbook/repository/specs/SecurityDescriptionSearchSpecification.java b/src/main/java/ru/investbook/repository/specs/SecurityDescriptionSearchSpecification.java index db2de0b7..b175553d 100644 --- a/src/main/java/ru/investbook/repository/specs/SecurityDescriptionSearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/SecurityDescriptionSearchSpecification.java @@ -23,6 +23,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.SecurityDescriptionEntity; import ru.investbook.entity.SecurityDescriptionEntity_; @@ -30,6 +31,7 @@ import java.util.Objects; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; import static ru.investbook.repository.specs.SpecificationHelper.filterByLike; import static ru.investbook.repository.specs.SpecificationHelper.filterBySecurityId; @@ -40,7 +42,8 @@ public class SecurityDescriptionSearchSpecification implements Specification root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { + requireNonNull(query); return Stream.of( filterBySecurityId(root, builder, SecurityDescriptionEntity_.security, security, query), filterByLike(root, builder, SecurityDescriptionEntity_.sector, securitySector)) diff --git a/src/main/java/ru/investbook/repository/specs/SecurityEventCashFlowEntitySearchSpecification.java b/src/main/java/ru/investbook/repository/specs/SecurityEventCashFlowEntitySearchSpecification.java index 0521d7d3..a968ee59 100644 --- a/src/main/java/ru/investbook/repository/specs/SecurityEventCashFlowEntitySearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/SecurityEventCashFlowEntitySearchSpecification.java @@ -24,6 +24,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.CashFlowTypeEntity_; @@ -46,7 +47,7 @@ public class SecurityEventCashFlowEntitySearchSpecification implements Specifica @Override public Predicate toPredicate(Root root, - CriteriaQuery query, + @Nullable CriteriaQuery query, CriteriaBuilder builder) { return Stream.of( filterByPortfolio(root, builder, SecurityEventCashFlowEntity_.portfolio, portfolio), diff --git a/src/main/java/ru/investbook/repository/specs/SecurityQuoteSearchSpecification.java b/src/main/java/ru/investbook/repository/specs/SecurityQuoteSearchSpecification.java index 6a029455..78eb3a36 100644 --- a/src/main/java/ru/investbook/repository/specs/SecurityQuoteSearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/SecurityQuoteSearchSpecification.java @@ -23,6 +23,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.SecurityQuoteEntity; import ru.investbook.entity.SecurityQuoteEntity_; @@ -31,6 +32,7 @@ import java.util.Objects; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; import static ru.investbook.repository.specs.SpecificationHelper.*; @@ -41,7 +43,8 @@ public class SecurityQuoteSearchSpecification implements Specification root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { + requireNonNull(query); return Stream.of( filterBySecurity(root, builder, SecurityQuoteEntity_.security, security), filterByEquals(root, builder, SecurityQuoteEntity_.currency, currency), diff --git a/src/main/java/ru/investbook/repository/specs/SpecificationHelper.java b/src/main/java/ru/investbook/repository/specs/SpecificationHelper.java index 7d7785c1..7f295643 100644 --- a/src/main/java/ru/investbook/repository/specs/SpecificationHelper.java +++ b/src/main/java/ru/investbook/repository/specs/SpecificationHelper.java @@ -26,8 +26,7 @@ import jakarta.persistence.criteria.Subquery; import jakarta.persistence.metamodel.SingularAttribute; import lombok.NoArgsConstructor; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; import ru.investbook.entity.PortfolioEntity; import ru.investbook.entity.PortfolioEntity_; import ru.investbook.entity.SecurityEntity; @@ -44,11 +43,10 @@ @NoArgsConstructor(access = PRIVATE) class SpecificationHelper { - @Nullable - static Predicate filterBySecurity(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - String security) { + static @Nullable Predicate filterBySecurity(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + String security) { if (hasText(security)) { Path securityPath = root.get(attribute); return builder.or( @@ -59,12 +57,11 @@ static Predicate filterBySecurity(Root root, return null; } - @Nullable - static Predicate filterBySecurityId(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - String security, - CriteriaQuery query) { + static @Nullable Predicate filterBySecurityId(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + String security, + CriteriaQuery query) { if (hasText(security)) { // Do sub-query because SecurityEntity is not related to SecurityDescriptionEntity in Java model, so // can not do join query in Criteria API @@ -84,11 +81,10 @@ static Predicate filterBySecurityId(Root root, return null; } - @Nullable - static Predicate filterByDateFrom(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - LocalDate dateFrom) { + static @Nullable Predicate filterByDateFrom(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + @Nullable LocalDate dateFrom) { if (dateFrom == null) { return null; } @@ -99,11 +95,10 @@ static Predicate filterByDateFrom(Root root, startOfDay); } - @Nullable - static Predicate filterByDateTo(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - LocalDate dateTo) { + static @Nullable Predicate filterByDateTo(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + @Nullable LocalDate dateTo) { if (dateTo == null) { return null; } @@ -115,11 +110,10 @@ static Predicate filterByDateTo(Root root, endOfDay); } - @Nullable - static Predicate filterByInstantBelongsToDate(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - LocalDate date) { + static @Nullable Predicate filterByInstantBelongsToDate(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + @Nullable LocalDate date) { if (date == null) { return null; } @@ -172,11 +166,10 @@ static Predicate filterByPortfolioName(Root root, .value(enabledPortfolioIds); } - @Nullable - static Predicate filterByEquals(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - @Nullable String value) { + static @Nullable Predicate filterByEquals(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + @Nullable String value) { if (hasText(value)) { Path path = root.get(attribute); return builder.equal(path, value); @@ -184,11 +177,10 @@ static Predicate filterByEquals(Root root, return null; } - @Nullable - static Predicate filterByLike(Root root, - CriteriaBuilder builder, - SingularAttribute attribute, - @Nullable String value) { + static @Nullable Predicate filterByLike(Root root, + CriteriaBuilder builder, + SingularAttribute attribute, + @Nullable String value) { if (hasText(value)) { Path path = root.get(attribute); return filterByLike(builder, path, value); @@ -196,7 +188,7 @@ static Predicate filterByLike(Root root, return null; } - private static Predicate filterByLike(CriteriaBuilder builder, Path path, @NonNull String value) { + private static Predicate filterByLike(CriteriaBuilder builder, Path path, String value) { return builder.like(builder.lower(path), "%" + value.toLowerCase() + "%"); } } diff --git a/src/main/java/ru/investbook/repository/specs/TransactionSearchSpecification.java b/src/main/java/ru/investbook/repository/specs/TransactionSearchSpecification.java index 436fe3a7..2cb602a9 100644 --- a/src/main/java/ru/investbook/repository/specs/TransactionSearchSpecification.java +++ b/src/main/java/ru/investbook/repository/specs/TransactionSearchSpecification.java @@ -23,6 +23,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.jpa.domain.Specification; import ru.investbook.entity.TransactionEntity; import ru.investbook.entity.TransactionEntity_; @@ -31,6 +32,7 @@ import java.util.Objects; import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; import static ru.investbook.repository.specs.SpecificationHelper.*; @@ -42,7 +44,8 @@ public class TransactionSearchSpecification implements Specification root, CriteriaQuery query, CriteriaBuilder builder) { + public Predicate toPredicate(Root root, @Nullable CriteriaQuery query, CriteriaBuilder builder) { + requireNonNull(query); return Stream.of( filterByPortfolioName(root, builder, TransactionEntity_.portfolio, portfolio, query), filterBySecurity(root, builder, TransactionEntity_.security, security), diff --git a/src/main/java/ru/investbook/service/AssetsAndCashServiceImpl.java b/src/main/java/ru/investbook/service/AssetsAndCashServiceImpl.java index ca349c01..ad7e073e 100644 --- a/src/main/java/ru/investbook/service/AssetsAndCashServiceImpl.java +++ b/src/main/java/ru/investbook/service/AssetsAndCashServiceImpl.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.PortfolioCash; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.spacious_team.broker.pojo.SecurityType; @@ -134,7 +135,7 @@ private Optional getTotalAssetsByCurrentOrLastTransactionQuoteEstima try { long t0 = nanoTime(); FifoPositionsFilter filter = FifoPositionsFilter.of(portfolio); - BigDecimal assetsInRub = securityRepository.findByTypeIn(stockBondAndAssetTypes) + @Nullable BigDecimal assetsInRub = securityRepository.findByTypeIn(stockBondAndAssetTypes) .stream() .map(securityConverter::fromEntity) .map(security -> investmentProportionService diff --git a/src/main/java/ru/investbook/service/InvestmentProportionService.java b/src/main/java/ru/investbook/service/InvestmentProportionService.java index f1216cc3..cf2c8b2c 100644 --- a/src/main/java/ru/investbook/service/InvestmentProportionService.java +++ b/src/main/java/ru/investbook/service/InvestmentProportionService.java @@ -38,6 +38,7 @@ import java.util.stream.Stream; import static java.lang.System.nanoTime; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.*; import static org.spacious_team.broker.pojo.SecurityType.*; @@ -118,7 +119,7 @@ public Optional getOpenedPositionsCostByCurrentOrLastTransactionQuot } private String getEconomicSector(SecurityInvestment securityInvestment) { - return securityDescriptionRepository.findById(securityInvestment.security().getId()) + return securityDescriptionRepository.findById(requireNonNull(securityInvestment.security().getId())) .map(SecurityDescriptionEntity::getSector) .orElse(SecuritySectorService.UNKNOWN_SECTOR); } diff --git a/src/main/java/ru/investbook/service/SecurityProfitService.java b/src/main/java/ru/investbook/service/SecurityProfitService.java index c6cce63d..9855f0dd 100644 --- a/src/main/java/ru/investbook/service/SecurityProfitService.java +++ b/src/main/java/ru/investbook/service/SecurityProfitService.java @@ -18,6 +18,7 @@ package ru.investbook.service; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityQuote; @@ -57,7 +58,7 @@ BigDecimal getGrossProfit( BigDecimal sumPaymentsForType( Collection portfolios, Security security, CashFlowType cashFlowType, String toCurrency); - SecurityQuote getSecurityQuote(Security security, String toCurrency, Instant to); + @Nullable SecurityQuote getSecurityQuote(Security security, String toCurrency, Instant to); /** * Возвращает котировку по последней сделке diff --git a/src/main/java/ru/investbook/service/SecurityProfitServiceImpl.java b/src/main/java/ru/investbook/service/SecurityProfitServiceImpl.java index 0588f22a..78469141 100644 --- a/src/main/java/ru/investbook/service/SecurityProfitServiceImpl.java +++ b/src/main/java/ru/investbook/service/SecurityProfitServiceImpl.java @@ -19,6 +19,7 @@ package ru.investbook.service; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Security; import org.spacious_team.broker.pojo.SecurityQuote; @@ -52,6 +53,7 @@ import java.util.Set; import static java.lang.Math.signum; +import static java.util.Objects.requireNonNull; import static org.spacious_team.broker.pojo.SecurityType.CURRENCY_PAIR; import static org.springframework.util.StringUtils.hasLength; @@ -73,13 +75,14 @@ public class SecurityProfitServiceImpl implements SecurityProfitService { public Optional getLastEventTimestamp( Collection portfolios, Security security, Set events, Instant from, Instant to) { + Integer securityId = requireNonNull(security.getId()); Optional optional = portfolios.isEmpty() ? securityEventCashFlowRepository .findFirstBySecurityIdAndCashFlowTypeIdInAndTimestampBetweenOrderByTimestampDesc( - security.getId(), events, from, to) : + securityId, events, from, to) : securityEventCashFlowRepository .findFirstByPortfolioIdInAndSecurityIdAndCashFlowTypeIdInAndTimestampBetweenOrderByTimestampDesc( - portfolios, security.getId(), events, from, to); + portfolios, securityId, events, from, to); return optional.map(SecurityEventCashFlowEntity::getTimestamp); } @@ -115,10 +118,10 @@ private BigDecimal getStockOrBondPurchaseCost(FifoPositions positions, String to .flatMap(Optional::stream) .reduce(BigDecimal.ZERO, BigDecimal::add); // если ценная бумага не вводилась на счет, а была куплена (есть цена покупки) for (ClosedPosition closedPosition : positions.getClosedPositions()) { - BigDecimal openAmount = getTransactionValue(closedPosition.getOpenTransaction(), CashFlowType.PRICE, toCurrency) + @Nullable BigDecimal openAmount = getTransactionValue(closedPosition.getOpenTransaction(), CashFlowType.PRICE, toCurrency) .map(value -> getOpenAmount(value, closedPosition)) .orElse(null); - BigDecimal closeAmount = getTransactionValue(closedPosition.getCloseTransaction(), CashFlowType.PRICE, toCurrency) + @Nullable BigDecimal closeAmount = getTransactionValue(closedPosition.getCloseTransaction(), CashFlowType.PRICE, toCurrency) .map(value -> getClosedAmount(value, closedPosition)) .orElse(null); if (openAmount != null && closeAmount != null) { @@ -142,7 +145,7 @@ private BigDecimal getStockOrBondPurchaseCost(FifoPositions positions, String to } private boolean isStockSplit(Transaction transaction) { - if (!transactionCashFlowRepository.isDepositOrWithdrawal(transaction.getId())) { + if (!transactionCashFlowRepository.isDepositOrWithdrawal(requireNonNull(transaction.getId()))) { return false; } LocalDate transactionDay = LocalDate.ofInstant(transaction.getTimestamp(), zoneId); @@ -181,7 +184,7 @@ private Optional getTransactionValue(Transaction t, CashFlowType typ return Optional.empty(); } return transactionCashFlowRepository - .findByTransactionIdAndCashFlowType(t.getId(), type) + .findByTransactionIdAndCashFlowType(requireNonNull(t.getId()), type) .map(entity -> convertToCurrency(entity.getValue(), entity.getCurrency(), toCurrency)); } @@ -220,40 +223,42 @@ public BigDecimal sumPaymentsForType(Collection portfolios, Security sec private List getSecurityEventCashFlowEntities(Collection portfolios, Security security, CashFlowType cashFlowType) { + Integer securityId = requireNonNull(security.getId()); return portfolios.isEmpty() ? securityEventCashFlowRepository .findBySecurityIdAndCashFlowTypeIdAndTimestampBetweenOrderByTimestampAsc( - security.getId(), + securityId, cashFlowType.getId(), ViewFilter.get().getFromDate(), ViewFilter.get().getToDate()) : securityEventCashFlowRepository .findByPortfolioIdInAndSecurityIdAndCashFlowTypeIdAndTimestampBetweenOrderByTimestampAsc( portfolios, - security.getId(), + securityId, cashFlowType.getId(), ViewFilter.get().getFromDate(), ViewFilter.get().getToDate()); } @Override - public SecurityQuote getSecurityQuote(Security security, String toCurrency, Instant to) { + public @Nullable SecurityQuote getSecurityQuote(Security security, String toCurrency, Instant to) { + Integer securityId = requireNonNull(security.getId()); if (security.getType() == CURRENCY_PAIR) { - String currencyPair = securityRepository.findCurrencyPair(security.getId()).orElseThrow(); + String currencyPair = securityRepository.findCurrencyPair(securityId).orElseThrow(); String baseCurrency = currencyPair.substring(0, 3); LocalDate toDate = LocalDate.ofInstant(to, ZoneId.systemDefault()); BigDecimal lastPrice = toDate.isBefore(LocalDate.now()) ? foreignExchangeRateService.getExchangeRateOrDefault(baseCurrency, toCurrency, toDate) : foreignExchangeRateService.getExchangeRate(baseCurrency, toCurrency); return SecurityQuote.builder() - .security(security.getId()) + .security(securityId) .timestamp(to) .quote(lastPrice) .currency(toCurrency) .build(); } return securityQuoteRepository - .findFirstBySecurityIdAndTimestampLessThanOrderByTimestampDesc(security.getId(), to) + .findFirstBySecurityIdAndTimestampLessThanOrderByTimestampDesc(securityId, to) .map(securityQuoteConverter::fromEntity) .map(_quote -> foreignExchangeRateService.convertQuoteToCurrency(_quote, toCurrency, security.getType())) .map(_quote -> hasLength(_quote.getCurrency()) ? _quote : _quote.toBuilder() @@ -264,7 +269,8 @@ public SecurityQuote getSecurityQuote(Security security, String toCurrency, Inst @Override public Optional getSecurityQuoteFromLastTransaction(Security security, String toCurrency) { - return transactionRepository.findFirstBySecurityIdOrderByTimestampDesc(security.getId()) + //noinspection ReturnOfNull + return transactionRepository.findFirstBySecurityIdOrderByTimestampDesc(requireNonNull(security.getId())) .map(t -> transactionCashFlowRepository .findByTransactionIdAndCashFlowTypeIn(t.getId(), priceAndAccruedInterestTypes) .stream() diff --git a/src/main/java/ru/investbook/service/Sp500Service.java b/src/main/java/ru/investbook/service/Sp500Service.java index a4773c7e..825e91bf 100644 --- a/src/main/java/ru/investbook/service/Sp500Service.java +++ b/src/main/java/ru/investbook/service/Sp500Service.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Workbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; import org.spacious_team.table_wrapper.api.TableHeaderColumn; @@ -40,6 +41,7 @@ import java.time.Duration; import java.util.Objects; +import static java.util.Objects.requireNonNull; import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl; @Service @@ -61,7 +63,7 @@ public class Sp500Service { public void update() { try { long t0 = System.nanoTime(); - Resource resource = restTemplate.getForObject(uri, Resource.class); + @Nullable Resource resource = restTemplate.getForObject(uri, Resource.class); updateBy(resource); log.info("Индекс S&P 500 обновлен за {}", Duration.ofNanos(System.nanoTime() - t0)); } catch (Exception e) { @@ -69,12 +71,13 @@ public void update() { } } - private void updateBy(Resource resource) throws IOException { - Objects.requireNonNull(resource, () -> "Не удалось скачать S&P 500 с адреса " + uri); + private void updateBy(@Nullable Resource resource) throws IOException { + requireNonNull(resource, () -> "Не удалось скачать S&P 500 с адреса " + uri); Workbook book = new HSSFWorkbook(resource.getInputStream()); new ExcelSheet(book.getSheetAt(0)) .createNameless("Effective date", TableHeader.class) .stream() + .filter(Objects::nonNull) .map(Sp500Service::getIndexValue) .forEach(this::save); } diff --git a/src/main/java/ru/investbook/service/Suppliers.java b/src/main/java/ru/investbook/service/Suppliers.java index af922df0..26935599 100644 --- a/src/main/java/ru/investbook/service/Suppliers.java +++ b/src/main/java/ru/investbook/service/Suppliers.java @@ -18,17 +18,22 @@ package ru.investbook.service; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.function.Supplier; public class Suppliers { + /** + * Returns result caching Supplier + */ public static Supplier memorize(Supplier supplier) { return new Supplier<>() { - private T value; + private @Nullable T value = null; @Override public T get() { - return value == null ? + return (value == null) ? value = supplier.get() : value; } diff --git a/src/main/java/ru/investbook/service/cbr/CbrForeignExchangeRateServiceExcelImpl.java b/src/main/java/ru/investbook/service/cbr/CbrForeignExchangeRateServiceExcelImpl.java index 9f95ae97..49835b2d 100644 --- a/src/main/java/ru/investbook/service/cbr/CbrForeignExchangeRateServiceExcelImpl.java +++ b/src/main/java/ru/investbook/service/cbr/CbrForeignExchangeRateServiceExcelImpl.java @@ -23,6 +23,7 @@ import lombok.SneakyThrows; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.ForeignExchangeRate; import org.spacious_team.table_wrapper.api.PatternTableColumn; import org.spacious_team.table_wrapper.api.TableColumn; @@ -40,6 +41,8 @@ import java.util.Map; import java.util.Objects; +import static java.util.Objects.requireNonNull; + public class CbrForeignExchangeRateServiceExcelImpl extends AbstractCbrForeignExchangeRateService { private static final String uri = "https://www.cbr.ru/Queries/UniDbQuery/DownloadExcel/98956?" + "VAL_NM_RQ={currency}&" + @@ -60,7 +63,7 @@ public CbrForeignExchangeRateServiceExcelImpl(ForeignExchangeRateRepository fore @SneakyThrows @Override protected void updateCurrencyRate(String currencyPair, String currencyId, LocalDate fromDate) { - Resource resource = restTemplate.getForObject( + @Nullable Resource resource = restTemplate.getForObject( uri, Resource.class, Map.of("currency", currencyId, @@ -69,13 +72,14 @@ protected void updateCurrencyRate(String currencyPair, String currencyId, LocalD updateBy(resource, currencyPair); } - private void updateBy(Resource resource, String currencyPair) throws IOException { - Objects.requireNonNull(resource, () -> "Не удалось скачать курсы валют"); + private void updateBy(@Nullable Resource resource, String currencyPair) throws IOException { + requireNonNull(resource, "Не удалось скачать курсы валют"); Workbook book = new XSSFWorkbook(resource.getInputStream()); new ExcelSheet(book.getSheetAt(0)) .createNameless("data", TableHeader.class) .stream() - .map(row -> getRate(row, currencyPair)) + .filter(Objects::nonNull) + .map(row -> getRate(requireNonNull(row), currencyPair)) .forEach(this::save); } diff --git a/src/main/java/ru/investbook/service/moex/MoexDerivativeCodeService.java b/src/main/java/ru/investbook/service/moex/MoexDerivativeCodeService.java index 4fc47ea8..ccbf9cc4 100644 --- a/src/main/java/ru/investbook/service/moex/MoexDerivativeCodeService.java +++ b/src/main/java/ru/investbook/service/moex/MoexDerivativeCodeService.java @@ -19,6 +19,9 @@ package ru.investbook.service.moex; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; import org.springframework.stereotype.Component; import java.time.LocalDate; @@ -31,13 +34,14 @@ import static java.lang.Character.isAlphabetic; import static java.lang.Character.isDigit; import static java.lang.Integer.parseInt; +import static java.util.Objects.isNull; import static java.util.Optional.empty; import static java.util.stream.Collectors.toMap; /** * Converts derivative short names (for ex. Si-6.21) to secid (a.k.a ticker codes, for ex SiM1) * - * @See Опционы на акции + * @see Опционы на акции * @see Specifications ticker codes for Futures and Options */ @Slf4j @@ -181,7 +185,7 @@ public class MoexDerivativeCodeService { {"W4", "WHEAT"} // Индекс пшеницы }).collect(toMap(a -> a[0], a -> a[1])); - private final Map shortnameToCodes; + private final Map shortnameToCodes; private final Character[] futuresMonthCodes = new Character[]{'F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z'}; @@ -194,13 +198,14 @@ public class MoexDerivativeCodeService { // 2000-th, 2010-th, 2020-th and so on private final int currentYearDecade = currentYear / 10 * 10; + @SuppressWarnings("argument") private MoexDerivativeCodeService() { this.shortnameToCodes = codeToShortnames.entrySet() .stream() .collect(toMap( Entry::getValue, Entry::getKey, - (e1, e2) -> { + (_, _) -> { throw new RuntimeException(); }, HashMap::new)); @@ -221,7 +226,7 @@ public boolean isFutures(String contract) { /** * @return true for futures in format {@code SiM1} */ - public boolean isFuturesCode(String contract) { + public boolean isFuturesCode(@Nullable String contract) { return contract != null && contract.length() == 4 && hasOnlyLetters(contract, 0, 2) && // smells like a futures code @@ -232,7 +237,7 @@ public boolean isFuturesCode(String contract) { /** * @return true for futures in format {@code Si-3.21} */ - public boolean isFuturesShortname(String contract) { + public boolean isFuturesShortname(@Nullable String contract) { try { if (contract == null) return false; int dotIdx = contract.length() - 3; @@ -254,7 +259,7 @@ public boolean isFuturesShortname(String contract) { return false; } - private boolean hasOnlyLetters(String string, int beginIndex, int endIndex) { + private boolean hasOnlyLetters(String string, @SuppressWarnings("SameParameterValue") int beginIndex, int endIndex) { int i = Math.max(0, beginIndex); i = Math.min(string.length(), i); int cnt = Math.max(0, endIndex); @@ -279,7 +284,7 @@ public boolean isOption(String contract) { /** * @return true for option in format {@code BR10BF0}, {@code BR-10BF0} (знак "-" это часть цены) and {@code GZ300CG2D} */ - public boolean isOptionCode(String contract) { + public boolean isOptionCode(@Nullable String contract) { if (contract == null) return false; int length = contract.length(); if (length > 5) { @@ -300,7 +305,7 @@ public boolean isOptionCode(String contract) { * @return true for option in format {@code BR-7.20M250620СA10}, {@code BR-7.20M250620СA-10}, * {@code BR-7.16M270616CA 50} and {@code GAZPP220722CE 300} */ - public boolean isOptionShortname(String contract) { + public boolean isOptionShortname(@Nullable String contract) { if (contract == null) return false; int EIdx = getOptionExpirationTypeCharPosition(contract); if (EIdx == -1) { @@ -367,7 +372,7 @@ private int getOptionAccountTypeCharPosition(String contract, int expirationChar /** * @return {@code SiM1} for futures contract in {@code Si-6.21} or {@code SiM1} format */ - public Optional getFuturesCode(String contract) { + public Optional getFuturesCode(@Nullable String contract) { try { if (contract == null) return empty(); int dashIdx = contract.indexOf('-'); @@ -390,8 +395,10 @@ public Optional getFuturesCode(String contract) { /** * @return {@code Si-6.21} for futures contract in {@code Si-6.21} or {@code SiM1} format */ - public Optional getFuturesShortname(String contract) { - if (isFuturesShortname(contract)) { + public Optional getFuturesShortname(@Nullable String contract) { + if (contract == null) { + return Optional.empty(); + } else if (isFuturesShortname(contract)) { return Optional.of(contract); } else if (contract.length() != 4) { return empty(); @@ -400,7 +407,7 @@ public Optional getFuturesShortname(String contract) { if (month != -1) { char yearChar = contract.charAt(3); if (isDigit(yearChar)) { - String prefix = codeToShortnames.get(contract.substring(0, 2)); + @Nullable String prefix = codeToShortnames.get(contract.substring(0, 2)); if (prefix != null) { int year = getShortnameYear(yearChar); if (year != -1) { @@ -415,7 +422,7 @@ public Optional getFuturesShortname(String contract) { /** * Convert derivative codes before storing to DB if you need */ - public String convertDerivativeCode(String code) { + public @PolyNull String convertDerivativeCode(@PolyNull String code) { return isFuturesCode(code) ? getFuturesShortname(code).orElse(code) : code; @@ -426,8 +433,10 @@ public String convertDerivativeCode(String code) { * For example for {@code MXI-6.21}, {@code MMM1}, {@code MXI-6.21M170621CA3000} and {@code MM3000BF1} returns "MM". * Returns empty optional if argument is not futures or optional. */ - public Optional getContractGroup(String contract) { - if (isFuturesShortname(contract) || isOptionShortname(contract)) { + public Optional getContractGroup(@Nullable String contract) { + if (isNull(contract)) { + return empty(); + } else if (isFuturesShortname(contract) || isOptionShortname(contract)) { String shortname = contract.substring(0, contract.indexOf('-')); return Optional.ofNullable(shortnameToCodes.get(shortname)); } else if (isFuturesCode(contract) || isOptionCode(contract)) { diff --git a/src/main/java/ru/investbook/service/moex/MoexIssClient.java b/src/main/java/ru/investbook/service/moex/MoexIssClient.java index 4475021c..60bc1bd6 100644 --- a/src/main/java/ru/investbook/service/moex/MoexIssClient.java +++ b/src/main/java/ru/investbook/service/moex/MoexIssClient.java @@ -18,6 +18,7 @@ package ru.investbook.service.moex; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityType; @@ -38,7 +39,7 @@ public interface MoexIssClient { * * @return true if Moex hasn't quotes */ - boolean isDerivativeAndExpired(String shortnameOrSecid, SecurityType securityType); + boolean isDerivativeAndExpired(@Nullable String shortnameOrSecid, SecurityType securityType); /** * @param contract option's code (moex secid) in {@code Si65000BC9}, {@code Si65000BC9D}, {@code RI180000BD1} or diff --git a/src/main/java/ru/investbook/service/moex/MoexIssClientImpl.java b/src/main/java/ru/investbook/service/moex/MoexIssClientImpl.java index f61e2e91..b7e416d5 100644 --- a/src/main/java/ru/investbook/service/moex/MoexIssClientImpl.java +++ b/src/main/java/ru/investbook/service/moex/MoexIssClientImpl.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityQuote.SecurityQuoteBuilder; import org.spacious_team.broker.pojo.SecurityType; @@ -36,6 +37,7 @@ import java.util.concurrent.ConcurrentHashMap; import static java.lang.String.valueOf; +import static java.util.Objects.requireNonNull; import static java.util.Optional.empty; import static org.spacious_team.broker.pojo.SecurityType.*; @@ -87,10 +89,10 @@ public class MoexIssClientImpl implements MoexIssClient { "securities.columns=SECID,PREVDATE,PREVADMITTEDQUOTE,PREVSETTLEPRICE,PREVPRICE,ACCRUEDINT,LOTSIZE,LOTVALUE,MINSTEP,STEPPRICE"; private static final String contractDescription = "http://iss.moex.com/iss/securities/{secId}.json?" + "iss.meta=off&iss.only=description&description.columns=name,value"; + private static volatile int currentYear = getCurrentYear(); + private static volatile long fastCoarseDayCounter = getFastCoarseDayCounter(); private final MoexDerivativeCodeService moexDerivativeCodeService; private final RestTemplate restTemplate; - private int currentYear = getCurrentYear(); - private long fastCoarseDayCounter = getFastCoarseDayCounter(); private final Map> optionCodeToShortNames = new ConcurrentHashMap<>(); private final Map> optionUnderlingFutures = new ConcurrentHashMap<>(); @@ -162,7 +164,7 @@ public Optional getQuote(String moexSecId, MoexMarketDescription .filter(moexDerivativeCodeService::isFutures) .flatMap(underlyingSecid -> getMarket(underlyingSecid) .flatMap(underlyingMarket -> getQuote(underlyingSecid, underlyingMarket))) - .map(futuresContract -> futuresContract.getPrice() + .map(futuresContract -> requireNonNull(futuresContract.getPrice()) .divide(futuresContract.getQuote(), 6, RoundingMode.HALF_UP)) .map(oneUnitPrice -> quote.get().getQuote().multiply(oneUnitPrice)) .map(optionalPrice -> quote.get().toBuilder() @@ -173,7 +175,7 @@ public Optional getQuote(String moexSecId, MoexMarketDescription return quote; } - public boolean isDerivativeAndExpired(String shortnameOrSecid, SecurityType securityType) { + public boolean isDerivativeAndExpired(@Nullable String shortnameOrSecid, SecurityType securityType) { try { if (securityType == DERIVATIVE && shortnameOrSecid != null) { int currentYear = getCurrentYear(); @@ -205,7 +207,7 @@ public boolean isDerivativeAndExpired(String shortnameOrSecid, SecurityType secu return false; } - private int getCurrentYear() { + private static int getCurrentYear() { if (fastCoarseDayCounter != getFastCoarseDayCounter()) { fastCoarseDayCounter = getFastCoarseDayCounter(); currentYear = LocalDate.now().getYear(); @@ -239,6 +241,7 @@ private Optional getOptionUnderlingFuturesFromMoex(String contract) { .flatMap(moexDerivativeCodeService::getFuturesCode); } + @SuppressWarnings("return") private Optional getContractDescriptionFromMoex(String contract, String key) { try { return Optional.ofNullable(restTemplate.getForObject(contractDescription, Map.class, contract)) diff --git a/src/main/java/ru/investbook/service/moex/MoexJsonResponseParser.java b/src/main/java/ru/investbook/service/moex/MoexJsonResponseParser.java index 381ad36d..830e6546 100644 --- a/src/main/java/ru/investbook/service/moex/MoexJsonResponseParser.java +++ b/src/main/java/ru/investbook/service/moex/MoexJsonResponseParser.java @@ -19,6 +19,7 @@ package ru.investbook.service.moex; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; import java.util.Collection; @@ -29,6 +30,7 @@ import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; @Slf4j public class MoexJsonResponseParser { @@ -36,22 +38,23 @@ public class MoexJsonResponseParser { @SuppressWarnings("unchecked") public static List> convertFromIntObjectMap(Map indicesResponse) { try { - indicesResponse = (Map) indicesResponse.values().iterator().next(); - List columnNames = (List) indicesResponse.get("columns"); + @SuppressWarnings("assignment") + Map response = (Map) indicesResponse.values().iterator().next(); + @Nullable List columnNames = (List) response.get("columns"); Collection> dataObjects = - (Collection>) Optional.ofNullable(indicesResponse.get("data")).orElseThrow(); + (Collection>) Optional.ofNullable(response.get("data")).orElseThrow(); List> namedItems = new ArrayList<>(dataObjects.size()); for (List obj : dataObjects) { HashMap namedObject = new HashMap<>(); for (int i = 0, cnt = obj.size(); i < cnt; i++) { - namedObject.put(columnNames.get(i), obj.get(i)); + namedObject.put(requireNonNull(columnNames).get(i), obj.get(i)); } namedItems.add(unmodifiableMap(namedObject)); } return unmodifiableList(namedItems); } catch (Exception e) { log.info("Can't parse Moex ISS response: {}", indicesResponse, e); - return null; + return List.of(); } } } diff --git a/src/main/java/ru/investbook/service/moex/MoexMarketDescription.java b/src/main/java/ru/investbook/service/moex/MoexMarketDescription.java index 7251a207..d87b5209 100644 --- a/src/main/java/ru/investbook/service/moex/MoexMarketDescription.java +++ b/src/main/java/ru/investbook/service/moex/MoexMarketDescription.java @@ -21,6 +21,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Map; @@ -35,13 +36,13 @@ class MoexMarketDescription { private final String market; private final String board; @Getter - private final String currency; // may be null (exactly null for futures, options, currency pairs) + private final @Nullable String currency; // may be null (exactly null for futures, options, currency pairs) static MoexMarketDescription of(Map description) { String engine = valueOf(requireNonNull(description.get("engine"))); String market = valueOf(requireNonNull(description.get("market"))); String board = valueOf(requireNonNull(description.get("boardid"))); - String currency = ofNullable(description.get("currencyid")) + @Nullable String currency = ofNullable(description.get("currencyid")) .map(String::valueOf) .orElse(null); return new MoexMarketDescription(engine, market, board, currency); diff --git a/src/main/java/ru/investbook/service/moex/MoexSecurityQuoteHelper.java b/src/main/java/ru/investbook/service/moex/MoexSecurityQuoteHelper.java index 3133662c..bf4647b1 100644 --- a/src/main/java/ru/investbook/service/moex/MoexSecurityQuoteHelper.java +++ b/src/main/java/ru/investbook/service/moex/MoexSecurityQuoteHelper.java @@ -21,6 +21,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.SecurityQuote; import org.spacious_team.broker.pojo.SecurityQuote.SecurityQuoteBuilder; @@ -66,10 +67,10 @@ static Optional parse(Map description) { .or(() -> ofNullable(description.get("PREVPRICE"))) // currency, share/bond, forts .map(MoexSecurityQuoteHelper::toBigDecimal) .orElseThrow(); - BigDecimal accruedInterest = ofNullable(description.get("ACCRUEDINT")) + @Nullable BigDecimal accruedInterest = ofNullable(description.get("ACCRUEDINT")) .map(MoexSecurityQuoteHelper::toBigDecimal) .orElse(null); - BigDecimal price = null; // share + @Nullable BigDecimal price = null; // share if (accruedInterest != null) { // bond price = toBigDecimal(description.get("LOTVALUE")) .divide(toBigDecimal(description.get("LOTSIZE")), 4, RoundingMode.HALF_UP) @@ -91,7 +92,7 @@ static Optional parse(Map description) { } } - private static BigDecimal toBigDecimal(Object value) { + private static BigDecimal toBigDecimal(@Nullable Object value) { return BigDecimal.valueOf( Double.parseDouble( String.valueOf( diff --git a/src/main/java/ru/investbook/service/smartlab/SmartlabShareSectors.java b/src/main/java/ru/investbook/service/smartlab/SmartlabShareSectors.java index 6ff17c38..564a0428 100644 --- a/src/main/java/ru/investbook/service/smartlab/SmartlabShareSectors.java +++ b/src/main/java/ru/investbook/service/smartlab/SmartlabShareSectors.java @@ -19,6 +19,7 @@ package ru.investbook.service.smartlab; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -110,7 +111,7 @@ private static Stream getSmartlabShareIds(Element sectorElement) { return Stream.empty(); } - private static String convertToSmartlabShareId(Element shareElement) { + private static @Nullable String convertToSmartlabShareId(Element shareElement) { try { String href = shareElement.attr("href"); int pos = href.lastIndexOf('/'); diff --git a/src/main/java/ru/investbook/upgrade/SqlDataImporter.java b/src/main/java/ru/investbook/upgrade/SqlDataImporter.java index c44bdc1a..8a5940e1 100644 --- a/src/main/java/ru/investbook/upgrade/SqlDataImporter.java +++ b/src/main/java/ru/investbook/upgrade/SqlDataImporter.java @@ -57,7 +57,7 @@ private boolean importSqlData(Path path) { Instant t0 = Instant.now(); Path absolutePath = path.toAbsolutePath().normalize(); String command = "RUNSCRIPT FROM '" + absolutePath + "' CHARSET 'UTF-8'"; - boolean isH2v1_xFile = path.getFileName().toString().contains("2022.9"); + boolean isH2v1_xFile = String.valueOf(path.getFileName()).contains("2022.9"); if (isH2v1_xFile) { command += " FROM_1X"; } diff --git a/src/main/java/ru/investbook/web/BrokerEmailReportController.java b/src/main/java/ru/investbook/web/BrokerEmailReportController.java index fb0d1477..60e31f25 100644 --- a/src/main/java/ru/investbook/web/BrokerEmailReportController.java +++ b/src/main/java/ru/investbook/web/BrokerEmailReportController.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.spacious_team.broker.report_parser.api.BrokerReportFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -45,7 +46,7 @@ public class BrokerEmailReportController { private final Collection brokerReportFactories; private final MailboxReportParserService mailboxReportParserService; - private MailboxDescriptor defaultMailboxDescriptor; + private volatile @MonotonicNonNull MailboxDescriptor defaultMailboxDescriptor; @GetMapping public String getPage(Model model) { @@ -64,7 +65,7 @@ public Object uploadBrokerReports(@ModelAttribute("mailboxDescriptor") MailboxDe "из почтового ящика " + mailbox.getLogin() + " на " + mailbox.getServer()); modelAndView.setViewName("success"); defaultMailboxDescriptor = mailbox; - defaultMailboxDescriptor.setPassword(null); + defaultMailboxDescriptor.setPassword(""); // do not store pass in RAM return modelAndView; } catch (Exception e) { return errorPage( diff --git a/src/main/java/ru/investbook/web/BrokerFileReportController.java b/src/main/java/ru/investbook/web/BrokerFileReportController.java index e17bd1a7..1214b279 100644 --- a/src/main/java/ru/investbook/web/BrokerFileReportController.java +++ b/src/main/java/ru/investbook/web/BrokerFileReportController.java @@ -38,6 +38,7 @@ import java.util.Objects; import java.util.concurrent.ConcurrentLinkedQueue; +import static java.util.Objects.requireNonNull; import static org.springframework.util.StringUtils.hasLength; import static ru.investbook.web.ReportControllerHelper.errorPage; import static ru.investbook.web.ReportControllerHelper.getBrokerNames; @@ -78,7 +79,7 @@ public ResponseEntity uploadBrokerReports(@RequestParam("reports") Multi private void uploadReport(MultipartFile report, String broker, Collection exceptions) { try (InputStream inputStream = report.getInputStream()) { // creates new input stream - brokerReportParserService.parseReport(inputStream, report.getOriginalFilename(), broker); + brokerReportParserService.parseReport(inputStream, requireNonNull(report.getOriginalFilename()), broker); } catch (Exception e) { exceptions.add(e); } diff --git a/src/main/java/ru/investbook/web/forms/controller/EventCashFlowController.java b/src/main/java/ru/investbook/web/forms/controller/EventCashFlowController.java index ddc41b4d..75f94540 100644 --- a/src/main/java/ru/investbook/web/forms/controller/EventCashFlowController.java +++ b/src/main/java/ru/investbook/web/forms/controller/EventCashFlowController.java @@ -93,7 +93,7 @@ private EventCashFlowModel getEventCashFlow(Integer id) { } @PostMapping - public String postEventCashFlow(@Valid @ModelAttribute("event") EventCashFlowModel event) { + public String postEventCashFlow(@ModelAttribute("event") @Valid EventCashFlowModel event) { selectedPortfolio = event.getPortfolio(); eventCashFlowFormsService.save(event); return "events/view-single"; diff --git a/src/main/java/ru/investbook/web/forms/controller/ForeignExchangeRateController.java b/src/main/java/ru/investbook/web/forms/controller/ForeignExchangeRateController.java index ae600a33..25cd4e50 100644 --- a/src/main/java/ru/investbook/web/forms/controller/ForeignExchangeRateController.java +++ b/src/main/java/ru/investbook/web/forms/controller/ForeignExchangeRateController.java @@ -90,7 +90,7 @@ private ForeignExchangeRateModel getForeignExchangeRate(LocalDate date, String b } @PostMapping - public String postForeignExchangeRate(@Valid @ModelAttribute("rate") ForeignExchangeRateModel rate) { + public String postForeignExchangeRate(@ModelAttribute("rate") @Valid ForeignExchangeRateModel rate) { foreignExchangeRateFormsService.save(rate); foreignExchangeRateService.invalidateCache(); return "foreign-exchange-rates/view-single"; diff --git a/src/main/java/ru/investbook/web/forms/controller/PortfolioCashController.java b/src/main/java/ru/investbook/web/forms/controller/PortfolioCashController.java index 5e9808c4..8e63fdd0 100644 --- a/src/main/java/ru/investbook/web/forms/controller/PortfolioCashController.java +++ b/src/main/java/ru/investbook/web/forms/controller/PortfolioCashController.java @@ -92,7 +92,7 @@ private PortfolioCashModel getPortfolioCash(Integer id, Supplier { boolean isEnabled = !archive.getPortfolios().contains(portfolio); diff --git a/src/main/java/ru/investbook/web/forms/controller/PortfolioPropertyController.java b/src/main/java/ru/investbook/web/forms/controller/PortfolioPropertyController.java index d5754d06..e933b546 100644 --- a/src/main/java/ru/investbook/web/forms/controller/PortfolioPropertyController.java +++ b/src/main/java/ru/investbook/web/forms/controller/PortfolioPropertyController.java @@ -93,7 +93,7 @@ private PortfolioPropertyModel getPortfolioProperty(Integer id, } @PostMapping("/total-assets") - public String postCash(@Valid @ModelAttribute("property") PortfolioPropertyTotalAssetsModel property) { + public String postCash(@ModelAttribute("property") @Valid PortfolioPropertyTotalAssetsModel property) { selectedPortfolio = property.getPortfolio(); portfolioPropertyFormsService.save(property); return "portfolio-properties/total-assets-view-single"; diff --git a/src/main/java/ru/investbook/web/forms/controller/SecurityDepositController.java b/src/main/java/ru/investbook/web/forms/controller/SecurityDepositController.java index ff1a33df..2b5b5855 100644 --- a/src/main/java/ru/investbook/web/forms/controller/SecurityDepositController.java +++ b/src/main/java/ru/investbook/web/forms/controller/SecurityDepositController.java @@ -86,13 +86,13 @@ public String getCreateSplitForm(Model model) { */ @PostMapping @Override - public String postTransaction(@Valid @ModelAttribute("transaction") TransactionModel transaction) { + public String postTransaction(@ModelAttribute("transaction") @Valid TransactionModel transaction) { super.postTransaction(transaction); return "security-deposit/view-single"; } @PostMapping("split") - public String postSplit(@Valid @ModelAttribute("split") SplitModel splitModel) { + public String postSplit(@ModelAttribute("split") @Valid SplitModel splitModel) { selectedPortfolio = splitModel.getPortfolio(); transactionFormsService.save(splitModel); fifoPositionsFactory.invalidateCache(); diff --git a/src/main/java/ru/investbook/web/forms/controller/SecurityDescriptionController.java b/src/main/java/ru/investbook/web/forms/controller/SecurityDescriptionController.java index 2f136b13..b97946e6 100644 --- a/src/main/java/ru/investbook/web/forms/controller/SecurityDescriptionController.java +++ b/src/main/java/ru/investbook/web/forms/controller/SecurityDescriptionController.java @@ -116,7 +116,7 @@ private SecurityDescriptionModel getSecurityDescription(Integer securityId) { } @PostMapping - public String postTransaction(@Valid @ModelAttribute("securityDescription") SecurityDescriptionModel securityDescription) { + public String postTransaction(@ModelAttribute("securityDescription") @Valid SecurityDescriptionModel securityDescription) { securityDescriptionFormsService.save(securityDescription); return "security-descriptions/view-single"; } diff --git a/src/main/java/ru/investbook/web/forms/controller/SecurityEventCashFlowController.java b/src/main/java/ru/investbook/web/forms/controller/SecurityEventCashFlowController.java index dab5ab52..491d9e7d 100644 --- a/src/main/java/ru/investbook/web/forms/controller/SecurityEventCashFlowController.java +++ b/src/main/java/ru/investbook/web/forms/controller/SecurityEventCashFlowController.java @@ -96,7 +96,7 @@ private SecurityEventCashFlowModel getSecurityEventCashFlow(Integer id) { } @PostMapping - public String postSecurityEventCashFlow(@Valid @ModelAttribute("event") SecurityEventCashFlowModel event) { + public String postSecurityEventCashFlow(@ModelAttribute("event") @Valid SecurityEventCashFlowModel event) { selectedPortfolio = event.getPortfolio(); securityEventCashFlowFormsService.save(event); fifoPositionsFactory.invalidateCache(); diff --git a/src/main/java/ru/investbook/web/forms/controller/SecurityQuoteController.java b/src/main/java/ru/investbook/web/forms/controller/SecurityQuoteController.java index 938cf539..80dceb39 100644 --- a/src/main/java/ru/investbook/web/forms/controller/SecurityQuoteController.java +++ b/src/main/java/ru/investbook/web/forms/controller/SecurityQuoteController.java @@ -96,7 +96,7 @@ private SecurityQuoteModel getSecurityQuote(Integer id) { } @PostMapping - public String postSecurityQuote(@Valid @ModelAttribute("quote") SecurityQuoteModel quote) { + public String postSecurityQuote(@ModelAttribute("quote") @Valid SecurityQuoteModel quote) { securityQuoteFormsService.save(quote); return "security-quotes/view-single"; } diff --git a/src/main/java/ru/investbook/web/forms/controller/TransactionController.java b/src/main/java/ru/investbook/web/forms/controller/TransactionController.java index 44281027..5f6d2981 100644 --- a/src/main/java/ru/investbook/web/forms/controller/TransactionController.java +++ b/src/main/java/ru/investbook/web/forms/controller/TransactionController.java @@ -100,7 +100,7 @@ private TransactionModel getTransaction(Integer id) { * @param transaction transaction attribute for save, same model attribute used for display in view */ @PostMapping - public String postTransaction(@Valid @ModelAttribute("transaction") TransactionModel transaction) { + public String postTransaction(@ModelAttribute("transaction") @Valid TransactionModel transaction) { selectedPortfolio = transaction.getPortfolio(); transactionFormsService.save(transaction); fifoPositionsFactory.invalidateCache(); diff --git a/src/main/java/ru/investbook/web/forms/model/ArchivedPortfolioModel.java b/src/main/java/ru/investbook/web/forms/model/ArchivedPortfolioModel.java index b124d8cc..ab3cadbc 100644 --- a/src/main/java/ru/investbook/web/forms/model/ArchivedPortfolioModel.java +++ b/src/main/java/ru/investbook/web/forms/model/ArchivedPortfolioModel.java @@ -18,7 +18,6 @@ package ru.investbook.web.forms.model; -import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.Set; @@ -26,7 +25,6 @@ @Data public class ArchivedPortfolioModel { - @NotNull private Set portfolios; } diff --git a/src/main/java/ru/investbook/web/forms/model/EventCashFlowModel.java b/src/main/java/ru/investbook/web/forms/model/EventCashFlowModel.java index 65c4bafc..6513d0c9 100644 --- a/src/main/java/ru/investbook/web/forms/model/EventCashFlowModel.java +++ b/src/main/java/ru/investbook/web/forms/model/EventCashFlowModel.java @@ -22,9 +22,9 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.lang.Nullable; import java.math.BigDecimal; import java.time.LocalDate; @@ -37,47 +37,41 @@ @Data public class EventCashFlowModel { - @Nullable - private Integer id; + private @Nullable Integer id; - @NotEmpty - private String portfolio; + private @NotEmpty String portfolio; - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotNull @DateTimeFormat(pattern = "HH:mm:ss") - private LocalTime time = LocalTime.NOON; + private @NotNull LocalTime time = LocalTime.NOON; - @NotNull - private CashFlowType type; + private @NotNull CashFlowType type; /** * Negative value for cash out, otherwise - positive */ - @NotNull - private BigDecimal value; + private @NotNull BigDecimal value; - @NotEmpty - private String valueCurrency = "RUB"; + private @NotEmpty String valueCurrency = "RUB"; - @Nullable - private String description; + private @Nullable String description; /** * Используется для привязки выплаты к бумаге из того же или другого счета */ - @Nullable - private AttachedSecurity attachedSecurity = new AttachedSecurity(); + private @Nullable AttachedSecurity attachedSecurity = new AttachedSecurity(); public void setValueCurrency(String currency) { this.valueCurrency = currency.toUpperCase(); } + /** + * Used by templates/events/edit-form.html + */ + @SuppressWarnings("unused") public String getStringType() { - if (type == null) return null; return switch (type) { case DIVIDEND, COUPON, REDEMPTION, AMORTIZATION, ACCRUED_INTEREST -> type.name(); case CASH -> { @@ -87,13 +81,14 @@ public String getStringType() { } yield type.name() + (isValuePositive() ? "_IN" : "_OUT"); } - default -> type.name() + (isValuePositive() ? "_IN" : "_OUT"); + default -> type.name() + (isValuePositive() ? "_IN" : "_OUT"); }; } /** * Used by templates/events/edit-form.html */ + @SuppressWarnings("unused") public void setStringType(String value) { if (value.equals("TAX_IIS_A")) { type = CashFlowType.CASH; @@ -113,18 +108,14 @@ public boolean isAttachedToSecurity() { @Data public class AttachedSecurity { - @Nullable - private Integer securityEventCashFlowId; + private @Nullable Integer securityEventCashFlowId; /** * In "name (isin)" or "contract-name" format */ - @Nullable - private String security; + private @Nullable String security; - @Nullable - @Positive - private Integer count; + private @Nullable @Positive Integer count; public boolean isValid() { return hasLength(security) && @@ -135,8 +126,9 @@ public boolean isValid() { /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - public String getSecurityIsin() { - return SecurityHelper.getSecurityIsin(requireNonNull(security)); + public @Nullable String getSecurityIsin() { + requireNonNull(security); + return SecurityHelper.getSecurityIsin(security); } /** @@ -145,14 +137,14 @@ public String getSecurityIsin() { public String getSecurityName() { // Для Типа выплаты TAX может выдавать неверный тип бумаги, // но для текущего алгоритма SecurityHelper.getSecurityName() типа достаточно - SecurityType securityType = switch(type) { + SecurityType securityType = switch (type) { case DIVIDEND -> SecurityType.SHARE; case ACCRUED_INTEREST, AMORTIZATION, REDEMPTION, COUPON -> SecurityType.BOND; case DERIVATIVE_PROFIT, DERIVATIVE_PRICE, DERIVATIVE_QUOTE -> SecurityType.DERIVATIVE; case TAX -> SecurityType.SHARE; // для TAX выдает не верный тип бумаги default -> throw new IllegalArgumentException("Не смог получить тип ЦБ по типу выплаты: " + type); }; - return SecurityHelper.getSecurityName(security, securityType); + return SecurityHelper.getSecurityName(requireNonNull(security), securityType); } } } diff --git a/src/main/java/ru/investbook/web/forms/model/ForeignExchangeRateModel.java b/src/main/java/ru/investbook/web/forms/model/ForeignExchangeRateModel.java index 8f0d307f..c5b74939 100644 --- a/src/main/java/ru/investbook/web/forms/model/ForeignExchangeRateModel.java +++ b/src/main/java/ru/investbook/web/forms/model/ForeignExchangeRateModel.java @@ -30,19 +30,15 @@ @Data public class ForeignExchangeRateModel { - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotEmpty - private String baseCurrency; + private @NotEmpty String baseCurrency; @NotEmpty String quoteCurrency; - @NotNull - @Positive - private BigDecimal rate; + private @Positive @NotNull BigDecimal rate; public void setBaseCurrency(String currency) { this.baseCurrency = currency.toUpperCase(); diff --git a/src/main/java/ru/investbook/web/forms/model/PageableWrapperModel.java b/src/main/java/ru/investbook/web/forms/model/PageableWrapperModel.java index 97383f54..259abc4c 100644 --- a/src/main/java/ru/investbook/web/forms/model/PageableWrapperModel.java +++ b/src/main/java/ru/investbook/web/forms/model/PageableWrapperModel.java @@ -19,6 +19,7 @@ package ru.investbook.web.forms.model; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.data.domain.Page; import java.util.List; @@ -35,18 +36,22 @@ public int getTotal() { return page.getTotalPages(); } + @SuppressWarnings("unused") public int getCurrent() { return page.getNumber(); } - public Integer getNext() { + @SuppressWarnings("unused") + public @Nullable Integer getNext() { return page.hasNext() ? page.nextPageable().getPageNumber() : null; } - public Integer getPrevious() { + @SuppressWarnings("unused") + public @Nullable Integer getPrevious() { return page.hasPrevious() ? page.previousPageable().getPageNumber(): null; } + @SuppressWarnings("unused") public boolean isLast() { return page.isLast(); } diff --git a/src/main/java/ru/investbook/web/forms/model/PortfolioCashModel.java b/src/main/java/ru/investbook/web/forms/model/PortfolioCashModel.java index 672053a1..eb897fae 100644 --- a/src/main/java/ru/investbook/web/forms/model/PortfolioCashModel.java +++ b/src/main/java/ru/investbook/web/forms/model/PortfolioCashModel.java @@ -22,8 +22,8 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.lang.Nullable; import java.math.BigDecimal; import java.time.LocalDate; @@ -33,26 +33,19 @@ @EqualsAndHashCode public class PortfolioCashModel { - @Nullable - private Integer id; + private @Nullable Integer id; - @NotEmpty - private String portfolio; + private @NotEmpty String portfolio; - @Nullable - private String market; + private @Nullable String market; - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotNull @DateTimeFormat(pattern = "HH:mm:ss") - private LocalTime time = LocalTime.NOON; + private @NotNull LocalTime time = LocalTime.NOON; - @NotNull - private BigDecimal cash = BigDecimal.ZERO; + private @NotNull BigDecimal cash = BigDecimal.ZERO; - @NotNull - private String currency = "RUB"; + private @NotEmpty String currency = "RUB"; } diff --git a/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyModel.java b/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyModel.java index 5bdea71d..23edd150 100644 --- a/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyModel.java +++ b/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyModel.java @@ -21,8 +21,8 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.lang.Nullable; import java.time.LocalDate; import java.time.LocalTime; @@ -30,17 +30,13 @@ @Data public abstract class PortfolioPropertyModel { - @Nullable - private Integer id; + private @Nullable Integer id; - @NotEmpty - private String portfolio; + private @NotEmpty String portfolio; - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotNull @DateTimeFormat(pattern = "HH:mm:ss") - private LocalTime time = LocalTime.NOON; + private @NotNull LocalTime time = LocalTime.NOON; } diff --git a/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyTotalAssetsModel.java b/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyTotalAssetsModel.java index 32c12171..f583d46c 100644 --- a/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyTotalAssetsModel.java +++ b/src/main/java/ru/investbook/web/forms/model/PortfolioPropertyTotalAssetsModel.java @@ -32,11 +32,9 @@ @EqualsAndHashCode(callSuper = true) public class PortfolioPropertyTotalAssetsModel extends PortfolioPropertyModel { - @NotNull - private BigDecimal totalAssets; + private @NotNull BigDecimal totalAssets; - @NotNull - private Currency totalAssetsCurrency; + private @NotNull Currency totalAssetsCurrency; public enum Currency { RUB, USD; diff --git a/src/main/java/ru/investbook/web/forms/model/SecurityDescriptionModel.java b/src/main/java/ru/investbook/web/forms/model/SecurityDescriptionModel.java index fa7a8be9..3e5552a8 100644 --- a/src/main/java/ru/investbook/web/forms/model/SecurityDescriptionModel.java +++ b/src/main/java/ru/investbook/web/forms/model/SecurityDescriptionModel.java @@ -21,26 +21,22 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; -import org.springframework.lang.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; @Data public class SecurityDescriptionModel { - @Nullable - private Integer securityId; + private @Nullable Integer securityId; /** * In "name (isin)" or "contract-name" format */ - @NotEmpty - private String security = "Наименование (RU0000000000)"; + private @NotEmpty String security = "Наименование (RU0000000000)"; - @NotEmpty - private String sector; + private @Nullable @NotEmpty String sector; - @NotNull - private SecurityType securityType; + private @NotNull SecurityType securityType; - public void setSecurity(Integer securityId, String securityIsin, String securityName, SecurityType securityType) { + public void setSecurity(Integer securityId, @Nullable String securityIsin, @Nullable String securityName, SecurityType securityType) { this.securityId = securityId; this.security = SecurityHelper.getSecurityDescription(securityIsin, securityName, securityType); this.securityType = securityType; @@ -56,7 +52,7 @@ public String getSecurityName() { /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - public String getSecurityIsin() { + public @Nullable String getSecurityIsin() { return SecurityHelper.getSecurityIsin(security); } } diff --git a/src/main/java/ru/investbook/web/forms/model/SecurityEventCashFlowModel.java b/src/main/java/ru/investbook/web/forms/model/SecurityEventCashFlowModel.java index 205c3375..5cfcbc32 100644 --- a/src/main/java/ru/investbook/web/forms/model/SecurityEventCashFlowModel.java +++ b/src/main/java/ru/investbook/web/forms/model/SecurityEventCashFlowModel.java @@ -22,9 +22,9 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.lang.Nullable; import java.math.BigDecimal; import java.time.LocalDate; @@ -33,47 +33,34 @@ @Data public class SecurityEventCashFlowModel { - @Nullable - private Integer id; + private @Nullable Integer id; - @Nullable - private Integer taxId; + private @Nullable Integer taxId; - @NotEmpty - private String portfolio; + private @NotEmpty String portfolio; - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotNull @DateTimeFormat(pattern = "HH:mm:ss") - private LocalTime time = LocalTime.NOON; + private @NotNull LocalTime time = LocalTime.NOON; /** * In "name (isin)" or "contract-name" format */ - @NotEmpty - private String security; + private @NotEmpty String security; - @NotNull private int count; - @NotNull - private CashFlowType type; + private @NotNull CashFlowType type; - @NotNull - private BigDecimal value; + private @NotNull BigDecimal value; - @NotEmpty - private String valueCurrency = "RUB"; + private @NotEmpty String valueCurrency = "RUB"; - @Nullable - @PositiveOrZero - private BigDecimal tax; + private @Nullable @PositiveOrZero BigDecimal tax; - @Nullable - private String taxCurrency = "RUB"; + private @Nullable String taxCurrency = "RUB"; public void setValueCurrency(String currency) { this.valueCurrency = currency.toUpperCase(); @@ -83,7 +70,7 @@ public void setTaxCurrency(String currency) { this.taxCurrency = currency.toUpperCase(); } - public void setSecurity(String isin, String securityName, SecurityType securityType) { + public void setSecurity(@Nullable String isin, @Nullable String securityName, SecurityType securityType) { this.security = SecurityHelper.getSecurityDescription(isin, securityName, securityType); } @@ -97,12 +84,12 @@ public String getSecurityName() { /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - public String getSecurityIsin() { + public @Nullable String getSecurityIsin() { return SecurityHelper.getSecurityIsin(security); } public SecurityType getSecurityType() { - return switch(type) { + return switch (type) { case DIVIDEND -> SecurityType.SHARE; case ACCRUED_INTEREST, AMORTIZATION, REDEMPTION, COUPON -> SecurityType.BOND; case DERIVATIVE_PROFIT, DERIVATIVE_PRICE, DERIVATIVE_QUOTE -> SecurityType.DERIVATIVE; diff --git a/src/main/java/ru/investbook/web/forms/model/SecurityHelper.java b/src/main/java/ru/investbook/web/forms/model/SecurityHelper.java index 738d5337..3ac262c6 100644 --- a/src/main/java/ru/investbook/web/forms/model/SecurityHelper.java +++ b/src/main/java/ru/investbook/web/forms/model/SecurityHelper.java @@ -18,15 +18,18 @@ package ru.investbook.web.forms.model; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.util.Assert; +import static java.util.Objects.requireNonNull; + public class SecurityHelper { public static String NULL_SECURITY_NAME = ""; /** * For stock or bond "Name (ISIN)", for derivatives - code, for asset - securityName */ - public static String getSecurityDescription(String isin, String securityName, SecurityType securityType) { + public static String getSecurityDescription(@Nullable String isin, @Nullable String securityName, SecurityType securityType) { return switch (securityType) { case SHARE, BOND -> { Assert.isTrue(isin != null || securityName != null, "Отсутствует и ISIN, и наименование ЦБ"); @@ -34,8 +37,8 @@ public static String getSecurityDescription(String isin, String securityName, Se (isin == null ? "" : " (" + isin + ")"); } case DERIVATIVE, CURRENCY, ASSET -> { - Assert.isTrue(securityName != null, "Отсутствует тикер контракта или наименование произвольного актива"); - yield securityName; + Assert.notNull(securityName, "Отсутствует тикер контракта или наименование произвольного актива"); + yield requireNonNull(securityName); } }; } @@ -56,7 +59,7 @@ static String getSecurityName(String securityDescription, SecurityType securityT /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - static String getSecurityIsin(String securityDescription) { + static @Nullable String getSecurityIsin(String securityDescription) { securityDescription = securityDescription.trim(); int len = securityDescription.length(); return isSecurityDescriptionHasIsin(securityDescription) ? diff --git a/src/main/java/ru/investbook/web/forms/model/SecurityQuoteModel.java b/src/main/java/ru/investbook/web/forms/model/SecurityQuoteModel.java index 6ed066b9..9b528dcd 100644 --- a/src/main/java/ru/investbook/web/forms/model/SecurityQuoteModel.java +++ b/src/main/java/ru/investbook/web/forms/model/SecurityQuoteModel.java @@ -23,7 +23,7 @@ import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.PositiveOrZero; import lombok.Data; -import org.springframework.lang.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; import java.math.BigDecimal; import java.time.Instant; @@ -35,34 +35,26 @@ @Data public class SecurityQuoteModel { - @Nullable - private Integer id; + private @Nullable Integer id; /** * In "name (isin)" or "contract-name" format */ - @NotEmpty - private String security; + private @NotEmpty String security; - @NotNull - private SecurityType securityType; + private @NotNull SecurityType securityType; - @NotNull - private Instant timestamp = LocalDate.now().atTime(LocalTime.NOON).atZone(systemDefault()).toInstant(); + private @NotNull Instant timestamp = LocalDate.now().atTime(LocalTime.NOON).atZone(systemDefault()).toInstant(); - @NotNull - @Positive - private BigDecimal quote; + private @NotNull @Positive BigDecimal quote; - @Positive - private BigDecimal price; + private @Nullable @Positive BigDecimal price; - @PositiveOrZero - private BigDecimal accruedInterest; + private @Nullable @PositiveOrZero BigDecimal accruedInterest; - private String currency; + private @Nullable String currency; - public void setSecurity(String isin, String securityName, SecurityType securityType) { + public void setSecurity(@Nullable String isin, @Nullable String securityName, SecurityType securityType) { this.security = SecurityHelper.getSecurityDescription(isin, securityName, securityType); this.securityType = securityType; } @@ -77,7 +69,7 @@ public String getSecurityName() { /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - public String getSecurityIsin() { + public @Nullable String getSecurityIsin() { return SecurityHelper.getSecurityIsin(security); } } diff --git a/src/main/java/ru/investbook/web/forms/model/SplitModel.java b/src/main/java/ru/investbook/web/forms/model/SplitModel.java index c6b7fe3e..c207f62f 100644 --- a/src/main/java/ru/investbook/web/forms/model/SplitModel.java +++ b/src/main/java/ru/investbook/web/forms/model/SplitModel.java @@ -22,6 +22,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; @@ -33,30 +34,22 @@ @Data public class SplitModel { - @NotEmpty - private String portfolio; + private @NotEmpty String portfolio; - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotNull @DateTimeFormat(pattern = "HH:mm") - private LocalTime time = LocalTime.NOON; + private @NotNull LocalTime time = LocalTime.NOON; /** * In "name (isin)" format */ - @NotEmpty - private String security; + private @NotEmpty String security; - @NotNull - @Positive - private int withdrawalCount; + private @Positive int withdrawalCount; - @NotNull - @Positive - private int depositCount; + private @Positive int depositCount; /** * Returns Name from template "Name (ISIN)" for stock and bond, code for derivative, securityName for asset @@ -68,7 +61,7 @@ public String getSecurityName() { /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - public String getSecurityIsin() { + public @Nullable String getSecurityIsin() { return SecurityHelper.getSecurityIsin(security); } diff --git a/src/main/java/ru/investbook/web/forms/model/TransactionModel.java b/src/main/java/ru/investbook/web/forms/model/TransactionModel.java index 4741b9ec..8d9f0cd3 100644 --- a/src/main/java/ru/investbook/web/forms/model/TransactionModel.java +++ b/src/main/java/ru/investbook/web/forms/model/TransactionModel.java @@ -24,17 +24,20 @@ import jakarta.validation.constraints.PositiveOrZero; import jakarta.xml.bind.DatatypeConverter; import lombok.Data; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; +import org.springframework.util.Assert; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDate; import java.time.LocalTime; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; +import static org.springframework.util.StringUtils.hasText; + @Data public class TransactionModel { @@ -48,82 +51,59 @@ public class TransactionModel { } } - @Nullable - private Integer id; + private @Nullable Integer id; - @Nullable - private String tradeId; + private @Nullable String tradeId; - @NotEmpty - private String portfolio; + private @NotEmpty String portfolio; - @NotNull - private Action action; + private @NotNull Action action; - @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date = LocalDate.now(); + private @NotNull LocalDate date = LocalDate.now(); - @NotNull @DateTimeFormat(pattern = "HH:mm:ss") - private LocalTime time = LocalTime.NOON; + private @NotNull LocalTime time = LocalTime.NOON; /** * In "name (isin)" or "contract-name" format */ - @NotEmpty - private String security; + private @NotEmpty String security; - @NotNull - private SecurityType securityType; + private @NotNull SecurityType securityType; - @NotNull - @Positive - private int count; + private @Positive int count; /** * Equals to null for security deposit and withdrawal */ - @Nullable - @Positive - private BigDecimal price; + private @Nullable @Positive BigDecimal price; - @Nullable - @PositiveOrZero - private BigDecimal accruedInterest; + private @Nullable @PositiveOrZero BigDecimal accruedInterest; /** * Price and accrued interest currency */ - @NotEmpty - private String priceCurrency = "RUB"; + private @NotEmpty String priceCurrency = "RUB"; - @Nullable - @Positive - private BigDecimal priceTick; + private @Nullable @Positive BigDecimal priceTick; - @Nullable - @Positive - private BigDecimal priceTickValue; + private @Nullable @Positive BigDecimal priceTickValue; - @Nullable - private String priceTickValueCurrency = "RUB"; + private @Nullable String priceTickValueCurrency = "RUB"; /** * May be null for security deposit and withdrawal */ - @Nullable - @PositiveOrZero - private BigDecimal fee; + private @Nullable @PositiveOrZero BigDecimal fee; - @NotEmpty - private String feeCurrency = "RUB"; + private @NotEmpty String feeCurrency = "RUB"; public enum Action { BUY, CELL } - public void setPriceTickValueCurrency(String priceTickValueCurrency) { + public void setPriceTickValueCurrency(@Nullable String priceTickValueCurrency) { if (priceTickValueCurrency != null) { this.priceTickValueCurrency = priceTickValueCurrency.toUpperCase(); } @@ -137,7 +117,7 @@ public void setFeeCurrency(String feeCurrency) { this.feeCurrency = feeCurrency.toUpperCase(); } - public void setSecurity(String isin, String securityName, SecurityType securityType) { + public void setSecurity(@Nullable String isin, @Nullable String securityName, SecurityType securityType) { this.security = SecurityHelper.getSecurityDescription(isin, securityName, securityType); this.securityType = securityType; } @@ -152,21 +132,21 @@ public String getSecurityName() { /** * Returns ISIN if description in "Name (ISIN)" format, null otherwise */ - public String getSecurityIsin() { + public @Nullable String getSecurityIsin() { return SecurityHelper.getSecurityIsin(security); } public String getTradeId() { - if (!StringUtils.hasText(tradeId)) { + if (!hasText(tradeId)) { setTradeId(createTradeId()); } - return tradeId; + return requireNonNull(tradeId, "Не задан trade-id"); } private String createTradeId() { - if (portfolio == null || security == null || date == null || action == null) { - return null; - } + //noinspection ConstantValue + Assert.isTrue(portfolio != null && security != null && date != null && action != null, + "Невалидные данные, ошибка вычисления trade-id"); String string = portfolio.replaceAll(" ", "") + security.replaceAll(" ", "") + date + @@ -174,7 +154,7 @@ private String createTradeId() { count; synchronized (TransactionModel.class) { try { - md.update(string.getBytes(StandardCharsets.UTF_8)); + md.update(string.getBytes(UTF_8)); return DatatypeConverter.printHexBinary(md.digest()).toLowerCase(); } finally { md.reset(); @@ -183,8 +163,8 @@ private String createTradeId() { } public boolean hasDerivativeTickValue() { - return getPriceTick() != null && getPriceTick().floatValue() > 0.000001 && - getPriceTickValue() != null && getPriceTickValue().floatValue() > 0.000001 && - StringUtils.hasText(getPriceTickValueCurrency()); + return priceTick != null && priceTick.floatValue() > 0.000001 && + priceTickValue != null && priceTickValue.floatValue() > 0.000001 && + hasText(priceTickValueCurrency); } } diff --git a/src/main/java/ru/investbook/web/forms/model/filter/EventCashFlowFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/EventCashFlowFormFilterModel.java index afd6126e..f8f9b46b 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/EventCashFlowFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/EventCashFlowFormFilterModel.java @@ -18,17 +18,18 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class EventCashFlowFormFilterModel extends AbstractFormFilterModel { private String portfolio; - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate dateFrom; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) diff --git a/src/main/java/ru/investbook/web/forms/model/filter/ForeignExchangeRateFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/ForeignExchangeRateFormFilterModel.java index 379d7311..b7d80e32 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/ForeignExchangeRateFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/ForeignExchangeRateFormFilterModel.java @@ -18,14 +18,16 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class ForeignExchangeRateFormFilterModel extends AbstractFormFilterModel { private String currency; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) diff --git a/src/main/java/ru/investbook/web/forms/model/filter/PortfolioCashFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/PortfolioCashFormFilterModel.java index cb7f5e50..fa31653b 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/PortfolioCashFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/PortfolioCashFormFilterModel.java @@ -18,14 +18,16 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class PortfolioCashFormFilterModel extends AbstractFormFilterModel { private String portfolio; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) diff --git a/src/main/java/ru/investbook/web/forms/model/filter/PortfolioPropertyFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/PortfolioPropertyFormFilterModel.java index 837d16ed..7c48dbcb 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/PortfolioPropertyFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/PortfolioPropertyFormFilterModel.java @@ -18,15 +18,17 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.spacious_team.broker.pojo.PortfolioPropertyType; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class PortfolioPropertyFormFilterModel extends AbstractFormFilterModel { private String portfolio; @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) diff --git a/src/main/java/ru/investbook/web/forms/model/filter/SecurityDescriptionFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/SecurityDescriptionFormFilterModel.java index 1c43d986..d5cb4622 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/SecurityDescriptionFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/SecurityDescriptionFormFilterModel.java @@ -18,11 +18,13 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class SecurityDescriptionFormFilterModel extends AbstractFormFilterModel { private String security; private String securitySector; diff --git a/src/main/java/ru/investbook/web/forms/model/filter/SecurityEventCashFlowFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/SecurityEventCashFlowFormFilterModel.java index 9a66deea..8cad8af3 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/SecurityEventCashFlowFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/SecurityEventCashFlowFormFilterModel.java @@ -18,14 +18,16 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class SecurityEventCashFlowFormFilterModel extends AbstractFormFilterModel { private String portfolio; private String security; diff --git a/src/main/java/ru/investbook/web/forms/model/filter/SecurityQuoteFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/SecurityQuoteFormFilterModel.java index 52f90878..37407867 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/SecurityQuoteFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/SecurityQuoteFormFilterModel.java @@ -18,14 +18,16 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class SecurityQuoteFormFilterModel extends AbstractFormFilterModel { private String security; private String currency; diff --git a/src/main/java/ru/investbook/web/forms/model/filter/TransactionFormFilterModel.java b/src/main/java/ru/investbook/web/forms/model/filter/TransactionFormFilterModel.java index 3cc194b7..4fe985d6 100644 --- a/src/main/java/ru/investbook/web/forms/model/filter/TransactionFormFilterModel.java +++ b/src/main/java/ru/investbook/web/forms/model/filter/TransactionFormFilterModel.java @@ -18,14 +18,16 @@ package ru.investbook.web.forms.model.filter; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDate; -@Getter -@Setter +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class TransactionFormFilterModel extends AbstractFormFilterModel { private String portfolio; private String security; diff --git a/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java b/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java index 86ad137f..d34a1483 100644 --- a/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java +++ b/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java @@ -90,21 +90,22 @@ public void save(EventCashFlowModel e) { } private void saveSecurityEventCashFlow(EventCashFlowModel e) { - int savedSecurityId = securityRepositoryHelper.saveSecurity(requireNonNull(e.getAttachedSecurity())); + EventCashFlowModel.AttachedSecurity attachedSecurity = requireNonNull(e.getAttachedSecurity()); + int savedSecurityId = securityRepositoryHelper.saveSecurity(attachedSecurity); SecurityEventCashFlowEntity entity = securityEventCashFlowRepository.save( securityEventCashFlowConverter.toEntity(SecurityEventCashFlow.builder() // no id(), it is always the new object .portfolio(e.getPortfolio()) .timestamp(e.getDate().atTime(e.getTime()).atZone(zoneId).toInstant()) .security(savedSecurityId) - .count(e.getAttachedSecurity().getCount()) + .count(requireNonNull(attachedSecurity.getCount())) .eventType(e.getType()) .value(e.getValue()) .currency(e.getValueCurrency()) .build())); Optional.ofNullable(e.getId()).ifPresent(this::delete); e.setId(null); - requireNonNull(e.getAttachedSecurity()).setSecurityEventCashFlowId(entity.getId()); // used in view + attachedSecurity.setSecurityEventCashFlowId(entity.getId()); // used in view securityEventCashFlowRepository.flush(); } diff --git a/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java b/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java index 7e76089d..d06b20b6 100644 --- a/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java +++ b/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java @@ -42,11 +42,9 @@ import java.math.BigDecimal; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Collection; import java.util.Optional; -import java.util.Set; -import static org.spacious_team.broker.pojo.PortfolioPropertyType.*; +import static org.spacious_team.broker.pojo.PortfolioPropertyType.valueOf; import static org.springframework.data.domain.Sort.Order.asc; import static org.springframework.data.domain.Sort.Order.desc; @@ -59,7 +57,6 @@ public class PortfolioPropertyFormsService { private final PortfolioRepository portfolioRepository; private final PortfolioPropertyConverter portfolioPropertyConverter; private final PortfolioConverter portfolioConverter; - private final Collection properties = Set.of(TOTAL_ASSETS_RUB.name(), TOTAL_ASSETS_USD.name()); @Transactional(readOnly = true) public Optional getById(Integer id) { diff --git a/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java b/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java index 10595226..bfeeb53a 100644 --- a/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java +++ b/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java @@ -19,6 +19,7 @@ package ru.investbook.web.forms.service; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.pojo.SecurityEventCashFlow; @@ -39,10 +40,13 @@ import ru.investbook.web.forms.model.SecurityEventCashFlowModel; import ru.investbook.web.forms.model.filter.SecurityEventCashFlowFormFilterModel; +import java.math.BigDecimal; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Optional; +import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static org.spacious_team.broker.pojo.CashFlowType.DERIVATIVE_PROFIT; import static org.springframework.data.domain.Sort.Order.asc; @@ -94,16 +98,17 @@ public void save(SecurityEventCashFlowModel e) { .currency(e.getValueCurrency()) .build())); e.setId(entity.getId()); // used in view - if (e.getTax() != null && e.getTax().floatValue() > 0.001) { + @Nullable BigDecimal tax = e.getTax(); + if (nonNull(tax) && nonNull(e.getTaxCurrency()) && tax.floatValue() > 0.001) { entity = securityEventCashFlowRepository.save(securityEventCashFlowConverter.toEntity( builder .id(e.getTaxId()) .eventType(CashFlowType.TAX) - .value(e.getTax().negate()) - .currency(e.getTaxCurrency()) + .value(tax.negate()) + .currency(requireNonNull(e.getTaxCurrency())) .build())); e.setTaxId(entity.getId()); - } else if (e.getTaxId() != null) { // taxId exists in db, but no tax value in edited version + } else if (nonNull(e.getTaxId())) { // taxId exists in db, but no tax value in edited version securityEventCashFlowRepository.deleteById(e.getTaxId()); } securityEventCashFlowRepository.flush(); diff --git a/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java b/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java index 87c430d8..e2c8d3d2 100644 --- a/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java +++ b/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java @@ -19,10 +19,9 @@ package ru.investbook.web.forms.service; import lombok.RequiredArgsConstructor; -import org.springframework.lang.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.stereotype.Service; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import ru.investbook.entity.SecurityEntity; import ru.investbook.repository.SecurityRepository; import ru.investbook.service.moex.MoexDerivativeCodeService; @@ -38,7 +37,9 @@ import java.util.Objects; import java.util.Optional; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; +import static org.springframework.util.StringUtils.hasLength; import static ru.investbook.entity.SecurityEntity.isinPattern; @@ -50,6 +51,7 @@ public class SecurityRepositoryHelper { /** * Generate securityId if needed, save security to DB, update securityId for model if needed + * * @return saved security id */ public int saveSecurity(SecurityDescriptionModel m) { @@ -60,6 +62,7 @@ public int saveSecurity(SecurityDescriptionModel m) { /** * Generate securityId if needed, save security to DB, update securityId for model if needed + * * @return saved security id */ public int saveSecurity(EventCashFlowModel.AttachedSecurity s) { @@ -70,6 +73,7 @@ public int saveSecurity(EventCashFlowModel.AttachedSecurity s) { /** * Generate securityId if needed, save security to DB, update securityId for model if needed + * * @return saved security id */ public int saveSecurity(SecurityEventCashFlowModel m) { @@ -78,6 +82,7 @@ public int saveSecurity(SecurityEventCashFlowModel m) { /** * Generate securityId if needed, save security to DB, update securityId for model if needed + * * @return saved security id */ public int saveSecurity(SecurityQuoteModel m) { @@ -86,6 +91,7 @@ public int saveSecurity(SecurityQuoteModel m) { /** * Generate securityId if needed, save security to DB, update securityId for model if needed + * * @return saved security id */ public int saveSecurity(TransactionModel m) { @@ -94,6 +100,7 @@ public int saveSecurity(TransactionModel m) { /** * Generate securityId if needed, save security to DB, update securityId for model if needed + * * @return saved security id */ public int saveSecurity(SplitModel m) { @@ -103,7 +110,7 @@ public int saveSecurity(SplitModel m) { /** * @return securityId from DB */ - private int saveSecurity(Integer securityId, String isin, String securityName, SecurityType securityType) { + private int saveSecurity(@Nullable Integer securityId, @Nullable String isin, String securityName, SecurityType securityType) { SecurityEntity security = ofNullable(securityId) .flatMap(securityRepository::findById) .or(() -> findSecurity(isin, securityName, securityType)) @@ -111,13 +118,13 @@ private int saveSecurity(Integer securityId, String isin, String securityName, S return saveSecurity(security, isin, securityName, securityType, true); } - private int saveSecurity(String isin, String securityName, SecurityType securityType) { + private int saveSecurity(@Nullable String isin, String securityName, SecurityType securityType) { SecurityEntity security = findSecurity(isin, securityName, securityType) .orElseGet(SecurityEntity::new); return saveSecurity(security, isin, securityName, securityType, false); } - private int saveSecurity(SecurityEntity security, String isin, String securityName, + private int saveSecurity(SecurityEntity security, @Nullable String isin, @Nullable String securityName, SecurityType securityType, boolean forceRewriteByEmptyIsin) { isin = validateIsin(isin); if (isin == null && !forceRewriteByEmptyIsin) { @@ -144,32 +151,31 @@ private int saveSecurity(SecurityEntity security, String isin, String securityNa return security.getId(); } - private Optional findSecurity(String isin, String securityName, SecurityType securityType) { + private Optional findSecurity(@Nullable String isin, @Nullable String securityName, SecurityType securityType) { isin = validateIsin(isin); securityName = Objects.equals(securityName, SecurityHelper.NULL_SECURITY_NAME) ? null : securityName; return switch (securityType) { case SHARE, BOND -> { Assert.isTrue(isin != null || securityName != null, "Отсутствует и ISIN, и наименование ЦБ"); - String name = securityName; + @Nullable String name = securityName; yield ofNullable(isin).flatMap(securityRepository::findByIsin) .or(() -> ofNullable(name).flatMap(securityRepository::findByTicker)) .or(() -> ofNullable(name).flatMap(securityRepository::findByName)); } case DERIVATIVE, CURRENCY -> { - Assert.notNull(securityName, "Отсутствует тикер контракта"); + requireNonNull(securityName, "Отсутствует тикер контракта"); yield securityRepository.findByTicker(securityName); } case ASSET -> { - Assert.notNull(securityName, "Отсутствует наименование произвольного актива"); + requireNonNull(securityName, "Отсутствует наименование произвольного актива"); yield securityRepository.findByName(securityName); } }; } - @Nullable - private String validateIsin(String isin) { - isin = StringUtils.hasLength(isin) ? isin : null; + private @Nullable String validateIsin(@Nullable String isin) { + isin = hasLength(isin) ? isin : null; if (isin != null && !isinPattern.matcher(isin).matches()) { throw new IllegalArgumentException("Невалидный ISIN: " + isin); } diff --git a/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java b/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java index 986fbd13..ff585bab 100644 --- a/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java +++ b/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java @@ -19,6 +19,7 @@ package ru.investbook.web.forms.service; import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spacious_team.broker.pojo.CashFlowType; import org.spacious_team.broker.pojo.Portfolio; import org.spacious_team.broker.report_parser.api.AbstractTransaction; @@ -65,6 +66,8 @@ import java.util.concurrent.atomic.AtomicReference; import static java.lang.Math.abs; +import static java.util.Objects.nonNull; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static org.springframework.data.domain.Sort.Order.asc; import static org.springframework.data.domain.Sort.Order.desc; @@ -128,21 +131,22 @@ public void save(TransactionModel tr) { AbstractTransactionBuilder builder; - if (tr.getPrice() != null) { + @Nullable BigDecimal price = tr.getPrice(); + if (nonNull(price)) { builder = switch (tr.getSecurityType()) { case SHARE, BOND, ASSET -> SecurityTransaction.builder() - .value(tr.getPrice().multiply(multiplier)) + .value(price.multiply(multiplier)) .valueCurrency(tr.getPriceCurrency()) .accruedInterest(ofNullable(tr.getAccruedInterest()) .map(v -> v.multiply(multiplier)) .orElse(null)); case DERIVATIVE -> { - BigDecimal value = null; - BigDecimal valueInPoints = tr.getPrice().multiply(multiplier); + @Nullable BigDecimal value = null; + BigDecimal valueInPoints = price.multiply(multiplier); if (tr.hasDerivativeTickValue()) { value = valueInPoints - .multiply(tr.getPriceTickValue()) - .divide(tr.getPriceTick(), 6, RoundingMode.HALF_UP); + .multiply(requireNonNull(tr.getPriceTickValue())) + .divide(requireNonNull(tr.getPriceTick()), 6, RoundingMode.HALF_UP); } yield DerivativeTransaction.builder() .valueInPoints(valueInPoints) @@ -150,11 +154,11 @@ public void save(TransactionModel tr) { .valueCurrency(tr.getPriceTickValueCurrency()); } case CURRENCY -> ForeignExchangeTransaction.builder() - .value(tr.getPrice().multiply(multiplier)) + .value(price.multiply(multiplier)) .valueCurrency(tr.getPriceCurrency()); }; - if (tr.getFee() != null) { + if (nonNull(tr.getFee())) { builder .fee(tr.getFee().negate()) .feeCurrency(tr.getFeeCurrency()); @@ -167,8 +171,11 @@ public void save(TransactionModel tr) { }; } + if (nonNull(tr.getId())) { // is null for new object + builder.id(tr.getId()); + } + AbstractTransaction transaction = builder - .id(tr.getId()) .tradeId(tr.getTradeId()) .portfolio(tr.getPortfolio()) .timestamp(tr.getDate().atTime(tr.getTime()).atZone(zoneId).toInstant()) @@ -188,6 +195,7 @@ private int saveTransaction(AbstractTransaction transaction) { TransactionEntity transactionEntity = transactionRepository.save( transactionConverter.toEntity(transaction.getTransaction())); + //noinspection OptionalOfNullableMisuse Optional.ofNullable(transactionEntity.getId()).ifPresent(transactionCashFlowRepository::deleteByTransactionId); transactionCashFlowRepository.flush(); transaction.toBuilder() @@ -284,14 +292,14 @@ private TransactionModel toTransactionModel(TransactionEntity e) { ofNullable(securityEntity.getName()).orElse(securityEntity.getTicker()), securityType.get()); if (m.getSecurityType() == DERIVATIVE && - m.getPrice() != null && m.getPrice().floatValue() > 0.000001) { + nonNull(m.getPrice()) && m.getPrice().floatValue() > 0.000001) { cashFlows.stream() .filter(value -> CashFlowType.valueOf(value.getCashFlowType().getId()) == CashFlowType.DERIVATIVE_PRICE) .forEach(value -> { m.setPriceTick(BigDecimal.ONE); // information not stored in db, normalizing m.setPriceTickValue(value.getValue() .divide(BigDecimal.valueOf(m.getCount()), 6, RoundingMode.HALF_UP) - .divide(m.getPrice(), 6, RoundingMode.HALF_UP) + .divide(requireNonNull(m.getPrice()), 6, RoundingMode.HALF_UP) .abs()); m.setPriceTickValueCurrency(value.getCurrency()); }); diff --git a/src/test/java/ru/investbook/InvestbookApplicationTest.java b/src/test/java/ru/investbook/InvestbookApplicationTest.java index a335e000..f9b19c0b 100644 --- a/src/test/java/ru/investbook/InvestbookApplicationTest.java +++ b/src/test/java/ru/investbook/InvestbookApplicationTest.java @@ -29,11 +29,11 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.web.servlet.MockMvc; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.testng.Assert.assertNotNull; @SpringBootTest(classes = InvestbookApplication.class) @AutoConfigureMockMvc @@ -65,5 +65,4 @@ void shouldHaveActuatorHandler(String actuatorPath, String jsonPath, String expe .andExpect(status().isOk()) .andExpect(jsonPath("$." + jsonPath).value(expected)); } - } diff --git a/src/test/java/ru/investbook/LoadingPageServerTests.java b/src/test/java/ru/investbook/LoadingPageServerTest.java similarity index 95% rename from src/test/java/ru/investbook/LoadingPageServerTests.java rename to src/test/java/ru/investbook/LoadingPageServerTest.java index 918f81cc..248b7e3b 100644 --- a/src/test/java/ru/investbook/LoadingPageServerTests.java +++ b/src/test/java/ru/investbook/LoadingPageServerTest.java @@ -26,10 +26,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import ru.investbook.loadingpage.LoadingPageServer; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) -class LoadingPageServerTests { +class LoadingPageServerTest { private LoadingPageServer loadingPageServer; diff --git a/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java b/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java index 2fa7cff2..ce8937e9 100644 --- a/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java +++ b/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java @@ -25,7 +25,6 @@ import org.springframework.boot.test.context.SpringBootTest; import java.time.Duration; -import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -35,10 +34,9 @@ @SpringBootTest(properties = "spring.datasource.url=jdbc:h2:mem:testdb;mode=mysql;non_keywords=value") class AbstractEntityRepositoryServiceTest { + static final AtomicInteger i = new AtomicInteger(); @Autowired - private SecurityRestController service; - private final Random random = new Random(); - private final AtomicInteger i = new AtomicInteger(); + SecurityRestController service; @Test void insert() { diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/CashFlowTableTest.java b/src/test/java/ru/investbook/parser/psb/CashFlowTableTest.java similarity index 84% rename from src/test/java/ru/portfolio/portfolio/parser/psb/CashFlowTableTest.java rename to src/test/java/ru/investbook/parser/psb/CashFlowTableTest.java index d8fc85bd..a2afee8b 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/CashFlowTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/CashFlowTableTest.java @@ -18,32 +18,32 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.pojo.EventCashFlow; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.math.BigDecimal; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class CashFlowTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "cash") - Object[][] getData() { + static Object[][] cash() { return new Object[][]{{"E:\\1.xlsx", BigDecimal.valueOf(400 - 760.77)}, {"E:\\Налог.xlsx", BigDecimal.valueOf(-542.0)}}; } - @Test(dataProvider = "cash") + @ParameterizedTest + @MethodSource("cash") void testIsin(String reportFile, BigDecimal expectedCashIn) throws IOException { PsbBrokerReport report = new PsbBrokerReport(reportFile, securityRegistrar); List data = new CashFlowTable(report).getData(); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/CouponAndAmortizationTableTest.java b/src/test/java/ru/investbook/parser/psb/CouponAndAmortizationTableTest.java similarity index 81% rename from src/test/java/ru/portfolio/portfolio/parser/psb/CouponAndAmortizationTableTest.java rename to src/test/java/ru/investbook/parser/psb/CouponAndAmortizationTableTest.java index e6a49393..ff97f4d9 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/CouponAndAmortizationTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/CouponAndAmortizationTableTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2020 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,30 +18,30 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.pojo.SecurityEventCashFlow; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class CouponAndAmortizationTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "isin") - Object[][] getData() { + static Object[][] isin() { return new Object[][] {{"E:\\1.xlsx", "RU000A0ZYAQ7", "RU000A0JV3M2" }}; } - @Test(dataProvider = "isin") + @ParameterizedTest + @MethodSource("isin") void testIsin(String report, String firstIsin, String lastIsin) throws IOException { PsbBrokerReport psbBrokerReport = new PsbBrokerReport(report, securityRegistrar); List data = new CouponAmortizationRedemptionTable(psbBrokerReport).getData(); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/DerivativeCashFlowTableTest.java b/src/test/java/ru/investbook/parser/psb/DerivativeCashFlowTableTest.java similarity index 84% rename from src/test/java/ru/portfolio/portfolio/parser/psb/DerivativeCashFlowTableTest.java rename to src/test/java/ru/investbook/parser/psb/DerivativeCashFlowTableTest.java index 133ba54f..a2dd0b71 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/DerivativeCashFlowTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/DerivativeCashFlowTableTest.java @@ -18,31 +18,31 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.pojo.SecurityEventCashFlow; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.math.BigDecimal; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class DerivativeCashFlowTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "cash-flow") - Object[][] getData() { + static Object[][] cashFlow() { return new Object[][] {{"E:\\Исполнение фьючерса.xlsx", BigDecimal.valueOf(-733.0) }}; } - @Test(dataProvider = "cash-flow") + @ParameterizedTest + @MethodSource("cashFlow") void testIsin(String report, BigDecimal expectedSum) throws IOException { PsbBrokerReport psbBrokerReport = new PsbBrokerReport(report, securityRegistrar); List data = new DerivativeCashFlowTable(psbBrokerReport).getData(); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/DerivativeTransactionTableTest.java b/src/test/java/ru/investbook/parser/psb/DerivativeTransactionTableTest.java similarity index 81% rename from src/test/java/ru/portfolio/portfolio/parser/psb/DerivativeTransactionTableTest.java rename to src/test/java/ru/investbook/parser/psb/DerivativeTransactionTableTest.java index 383f677b..e9cb2ba4 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/DerivativeTransactionTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/DerivativeTransactionTableTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2021 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,30 +18,30 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.report_parser.api.DerivativeTransaction; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class DerivativeTransactionTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "isin") - Object[][] getData() { + static Object[][] isin() { return new Object[][] {{"E:\\Исполнение фьючерса.xlsx", "Si-12.19M191219CA65500", "Si-12.19" }}; } - @Test(dataProvider = "isin") + @ParameterizedTest + @MethodSource("isin") void testIsin(String report, String firstIsin, String lastIsin) throws IOException { PsbBrokerReport psbBrokerReport = new PsbBrokerReport(report, securityRegistrar); List data = new DerivativeTransactionTable(psbBrokerReport).getData(); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/DividendTableTest.java b/src/test/java/ru/investbook/parser/psb/DividendTableTest.java similarity index 75% rename from src/test/java/ru/portfolio/portfolio/parser/psb/DividendTableTest.java rename to src/test/java/ru/investbook/parser/psb/DividendTableTest.java index 5959f0de..34a56893 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/DividendTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/DividendTableTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2020 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,34 +18,36 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.pojo.SecurityEventCashFlow; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class DividendTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "isin") - Object[][] getData() { + static Object[][] cashFisinlow() { return new Object[][] {{"E:\\1.xlsx", "RU000A0JRKT8", "RU000A0JRKT8" }}; } - @Test(dataProvider = "isin") + @ParameterizedTest + @MethodSource("cashFisinlow") void testIsin(String report, String firstIsin, String lastIsin) throws IOException { PsbBrokerReport psbBrokerReport = new PsbBrokerReport(report, securityRegistrar); List data = new DividendTable(psbBrokerReport).getData(); + //noinspection AssertBetweenInconvertibleTypes assertEquals(data.get(0).getSecurity(), firstIsin); + //noinspection AssertBetweenInconvertibleTypes assertEquals(data.get(data.size() - 1).getSecurity(), lastIsin); } } \ No newline at end of file diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/PortfolioCashTableTest.java b/src/test/java/ru/investbook/parser/psb/PortfolioCashTableTest.java similarity index 78% rename from src/test/java/ru/portfolio/portfolio/parser/psb/PortfolioCashTableTest.java rename to src/test/java/ru/investbook/parser/psb/PortfolioCashTableTest.java index 4768c0f7..db67881b 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/PortfolioCashTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/PortfolioCashTableTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2020 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,29 +18,29 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.math.BigDecimal; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class PortfolioCashTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "cash_in") - Object[][] getData() { + static Object[][] cashIn() { return new Object[][] {{"E:\\1.xlsx", BigDecimal.valueOf(350.37)}}; } - @Test(dataProvider = "cash_in") + @ParameterizedTest + @MethodSource("cashIn") void testIsin(String report, BigDecimal expectedCash) throws IOException { PsbBrokerReport psbBrokerReport = new PsbBrokerReport(report, securityRegistrar); assertEquals(new CashTable(psbBrokerReport).getData().get(0).getValue(), expectedCash); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/PortfolioSecuritiesTableTest.java b/src/test/java/ru/investbook/parser/psb/PortfolioSecuritiesTableTest.java similarity index 79% rename from src/test/java/ru/portfolio/portfolio/parser/psb/PortfolioSecuritiesTableTest.java rename to src/test/java/ru/investbook/parser/psb/PortfolioSecuritiesTableTest.java index bbca4142..13f550ab 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/PortfolioSecuritiesTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/PortfolioSecuritiesTableTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2020 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,30 +18,30 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.pojo.Security; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class PortfolioSecuritiesTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "isin") - Object[][] getData() { + static Object[][] isin() { return new Object[][] {{"E:\\1.xlsx", "RU000A0ZZDQ8", "RU000A0JV7J9" }}; } - @Test(dataProvider = "isin") + @ParameterizedTest + @MethodSource("isin") void testIsin(String report, String firstIsin, String lastIsin) throws IOException { List data = new SecuritiesTable(new PsbBrokerReport(report, securityRegistrar)).getData(); assertEquals(data.get(0).getId(), firstIsin); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/ReportParserServiceTest.java b/src/test/java/ru/investbook/parser/psb/ReportParserServiceTest.java similarity index 79% rename from src/test/java/ru/portfolio/portfolio/parser/psb/ReportParserServiceTest.java rename to src/test/java/ru/investbook/parser/psb/ReportParserServiceTest.java index 2713b45c..ddd1682a 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/ReportParserServiceTest.java +++ b/src/test/java/ru/investbook/parser/psb/ReportParserServiceTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2020 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,13 +18,12 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.InvestbookApplication; import ru.investbook.parser.ReportParserService; import ru.investbook.parser.SecurityRegistrar; @@ -32,9 +31,9 @@ import java.io.IOException; import java.nio.file.Paths; -@Ignore +@Disabled @SpringBootTest(classes = InvestbookApplication.class) -public class ReportParserServiceTest extends AbstractTestNGSpringContextTests { +public class ReportParserServiceTest { @Mock SecurityRegistrar securityRegistrar; @@ -42,15 +41,15 @@ public class ReportParserServiceTest extends AbstractTestNGSpringContextTests { @Autowired private ReportParserService reportParserService; - @DataProvider(name = "report") - Object[][] getData() { + static Object[][] report() { return new Object[][] {{"E:\\1.xlsx"}, {"E:\\2.xlsx"}, {"E:\\Исполнение фьючерса.xlsx"}, {"E:\\Налог.xlsx"}}; } - @Test(dataProvider = "report") + @ParameterizedTest + @MethodSource("report") void testParse(String report) throws IOException { PsbBrokerReport brokerReport = new PsbBrokerReport(Paths.get(report), securityRegistrar); PsbReportTables reportTableFactory = new PsbReportTables(brokerReport, null); diff --git a/src/test/java/ru/portfolio/portfolio/parser/psb/TransactionTableTest.java b/src/test/java/ru/investbook/parser/psb/TransactionTableTest.java similarity index 81% rename from src/test/java/ru/portfolio/portfolio/parser/psb/TransactionTableTest.java rename to src/test/java/ru/investbook/parser/psb/TransactionTableTest.java index facb336e..be20e3bf 100644 --- a/src/test/java/ru/portfolio/portfolio/parser/psb/TransactionTableTest.java +++ b/src/test/java/ru/investbook/parser/psb/TransactionTableTest.java @@ -1,6 +1,6 @@ /* * InvestBook - * Copyright (C) 2020 Spacious Team + * Copyright (C) 2023 Spacious Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -18,30 +18,30 @@ package ru.investbook.parser.psb; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.spacious_team.broker.report_parser.api.SecurityTransaction; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Ignore; -import org.testng.annotations.Test; import ru.investbook.parser.SecurityRegistrar; import java.io.IOException; import java.util.List; -import static org.testng.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@Ignore +@Disabled public class TransactionTableTest { @Mock SecurityRegistrar securityRegistrar; - @DataProvider(name = "isin") - Object[][] getData() { + static Object[][] isin() { return new Object[][] {{"E:\\1.xlsx", "RU000A0ZZYP6", "RU000A0JV4L2" }}; } - @Test(dataProvider = "isin") + @ParameterizedTest + @MethodSource("isin") void testIsin(String report, String firstIsin, String lastIsin) throws IOException { PsbBrokerReport psbBrokerReport = new PsbBrokerReport(report, securityRegistrar); List data = new SecurityTransactionTable(psbBrokerReport).getData(); diff --git a/src/test/java/ru/investbook/service/moex/MoexDerivativeCodeServiceTest.java b/src/test/java/ru/investbook/service/moex/MoexDerivativeCodeServiceTest.java index ebe17dd5..bbcf8fff 100644 --- a/src/test/java/ru/investbook/service/moex/MoexDerivativeCodeServiceTest.java +++ b/src/test/java/ru/investbook/service/moex/MoexDerivativeCodeServiceTest.java @@ -26,8 +26,8 @@ import java.util.Optional; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(MockitoExtension.class) public class MoexDerivativeCodeServiceTest {