Skip to content

Commit bc3fe55

Browse files
committed
feat: implement import of a series sales by URL.
Implementation details: - /series/{id}: a new form for importing a series sale has been added. It's only available when ADD_PURCHASES_AND_SALES Togglz feature is active and user has IMPORT_SERIES_SALES authority - IMPORT_SERIES_SALES authority has been added and granted to admin user by default - migration: JacksonAutoConfiguration should be removed from spring.autoconfigure.exclude property in application*.properties file Fix #995
1 parent 1c084f3 commit bc3fe55

36 files changed

+628
-28
lines changed

NEWS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- (functionality) show similar series on a page with series info
2929
- (infrastructure) add ability to send e-mails via Mailgun API
3030
- (infrastructure) port the integration tests to Robot Framework. Also remove TestNG and FEST assertions
31+
- (functionality) implement import of a series sales by URL
3132

3233
0.3
3334
- (functionality) implemented possibility to user to add series to his collection

pom.xml

-12
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,6 @@
143143
<groupId>org.springframework.boot</groupId>
144144
<artifactId>spring-boot-starter-tomcat</artifactId>
145145
</exclusion>
146-
<exclusion>
147-
<groupId>com.fasterxml.jackson.core</groupId>
148-
<artifactId>jackson-core</artifactId>
149-
</exclusion>
150-
<exclusion>
151-
<groupId>com.fasterxml.jackson.core</groupId>
152-
<artifactId>jackson-databind</artifactId>
153-
</exclusion>
154-
<exclusion>
155-
<groupId>com.fasterxml.jackson.core</groupId>
156-
<artifactId>jackson-annotations</artifactId>
157-
</exclusion>
158146
</exclusions>
159147
</dependency>
160148

src/main/java/ru/mystamps/web/Url.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ public final class Url {
5151
public static final String ADD_SERIES_ASK_PAGE = "/series/{id}/ask";
5252
public static final String INFO_SERIES_PAGE = "/series/{id}";
5353
public static final String ADD_IMAGE_SERIES_PAGE = "/series/{id}/image";
54+
public static final String SERIES_INFO_PAGE_REGEXP = "/series/(\\d+|\\d+/(ask|image))";
5455
public static final String SEARCH_SERIES_BY_CATALOG = "/series/search/by_catalog";
5556

5657
public static final String REQUEST_IMPORT_SERIES_PAGE = "/series/import/request";
5758
public static final String REQUEST_IMPORT_PAGE = "/series/import/request/{id}";
5859
public static final String LIST_IMPORT_REQUESTS_PAGE = "/series/import/requests";
5960

61+
public static final String IMPORT_SERIES_SALES = "/series/sales/import";
62+
6063
public static final String SUGGEST_SERIES_CATEGORY = "/suggest/series_category";
6164
public static final String SUGGEST_SERIES_COUNTRY = "/suggest/series_country";
6265

@@ -91,7 +94,7 @@ public final class Url {
9194
public static final String ADD_SERIES_WITH_COUNTRY_PAGE = "/series/add/country/{slug}";
9295

9396
// MUST be updated when any of our resources were modified
94-
public static final String RESOURCES_VERSION = "v0.3.12";
97+
public static final String RESOURCES_VERSION = "v0.3.13";
9598

9699
// CheckStyle: ignore LineLength for next 7 lines
97100
public static final String MAIN_CSS = "/static/" + RESOURCES_VERSION + "/styles/main.min.css";
@@ -155,6 +158,7 @@ public static Map<String, String> asMap(boolean production) {
155158
map.put("INFO_COUNTRY_PAGE", INFO_COUNTRY_PAGE);
156159
map.put("INFO_SERIES_PAGE", INFO_SERIES_PAGE);
157160
map.put("LIST_IMPORT_REQUESTS_PAGE", LIST_IMPORT_REQUESTS_PAGE);
161+
map.put("IMPORT_SERIES_SALES", IMPORT_SERIES_SALES);
158162
map.put("LOGIN_PAGE", LOGIN_PAGE);
159163
map.put("LOGOUT_PAGE", LOGOUT_PAGE);
160164
map.put("PUBLIC_URL", production ? PUBLIC_URL : SITE);

src/main/java/ru/mystamps/web/config/ControllersConfig.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import ru.mystamps.web.feature.series.SeriesConfig;
3939
import ru.mystamps.web.feature.series.SeriesService;
4040
import ru.mystamps.web.feature.series.importing.SeriesImportConfig;
41+
import ru.mystamps.web.feature.series.importing.sale.SeriesSalesImportConfig;
4142

4243
@Configuration
4344
@RequiredArgsConstructor
@@ -50,7 +51,8 @@
5051
ParticipantConfig.Controllers.class,
5152
ReportConfig.Controllers.class,
5253
SeriesConfig.Controllers.class,
53-
SeriesImportConfig.Controllers.class
54+
SeriesImportConfig.Controllers.class,
55+
SeriesSalesImportConfig.Controllers.class
5456
})
5557
public class ControllersConfig {
5658

src/main/java/ru/mystamps/web/config/MvcConfig.java

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import ru.mystamps.web.feature.country.CountryService;
4646
import ru.mystamps.web.feature.series.DownloadImageInterceptor;
4747
import ru.mystamps.web.feature.series.importing.event.EventsConfig;
48+
import ru.mystamps.web.support.spring.mvc.RestExceptionHandler;
4849
import ru.mystamps.web.support.spring.security.CurrentUserArgumentResolver;
4950

5051
import java.util.List;
@@ -153,6 +154,11 @@ public LocaleResolver getLocaleResolver() {
153154
return resolver;
154155
}
155156

157+
@Bean
158+
public RestExceptionHandler restExceptionHandler() {
159+
return new RestExceptionHandler();
160+
}
161+
156162
private static HandlerInterceptor getLocaleChangeInterceptor() {
157163
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
158164
interceptor.setParamName("lang");

src/main/java/ru/mystamps/web/config/ServicesConfig.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public DownloaderService getImageDownloaderService() {
127127
);
128128
}
129129

130-
@Bean
130+
@Bean(name = "seriesDownloaderService")
131131
public DownloaderService getSeriesDownloaderService() {
132132
return new TimedDownloaderService(
133133
LoggerFactory.getLogger(TimedDownloaderService.class),

src/main/java/ru/mystamps/web/feature/series/importing/RequestImportForm.java

+6
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@
3838
})
3939
public class RequestImportForm implements RequestImportDto {
4040

41+
// @todo #995 Series sale import: use its own interface and form
42+
// @todo #995 /series/sales/import: validate that we have a parser for this url
4143
@NotEmpty(groups = Group.Level1.class)
4244
@Size(
45+
// For series sales a max length is SERIES_SALES_URL_MAX_LENGTH but since they are equal,
46+
// we use IMPORT_REQUEST_URL_MAX_LENGTH here.
47+
// Also, as the import saves nothing, this check actually isn't required. Perhaps,
48+
// we shouldn't validate on this stage and let it fail later, during a sale creation.
4349
max = IMPORT_REQUEST_URL_MAX_LENGTH,
4450
message = "{value.too-long}",
4551
groups = Group.Level2.class

src/main/java/ru/mystamps/web/feature/series/importing/SeriesImportConfig.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.context.ApplicationEventPublisher;
2323
import org.springframework.context.annotation.Bean;
2424
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.context.annotation.Lazy;
2526
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
2627
import ru.mystamps.web.feature.participant.ParticipantService;
2728
import ru.mystamps.web.feature.series.SeriesController;
@@ -67,11 +68,13 @@ public static class Services {
6768
private final ParticipantService participantService;
6869
private final SeriesService seriesService;
6970
private final SeriesSalesService seriesSalesService;
70-
private final SeriesSalesImportService seriesSalesImportService;
7171
private final ApplicationEventPublisher eventPublisher;
7272

7373
@Bean
74-
public SeriesImportService seriesImportService(SeriesImportDao seriesImportDao) {
74+
public SeriesImportService seriesImportService(
75+
SeriesImportDao seriesImportDao,
76+
@Lazy SeriesSalesImportService seriesSalesImportService) {
77+
7578
return new SeriesImportServiceImpl(
7679
LoggerFactory.getLogger(SeriesImportServiceImpl.class),
7780
seriesImportDao,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (C) 2009-2019 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.feature.series.importing.sale;
19+
20+
import lombok.Getter;
21+
import lombok.RequiredArgsConstructor;
22+
23+
import java.math.BigDecimal;
24+
25+
@Getter
26+
@RequiredArgsConstructor
27+
public class SeriesSaleExtractedInfo {
28+
private final Integer sellerId;
29+
private final BigDecimal price;
30+
private final String currency;
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (C) 2009-2019 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.feature.series.importing.sale;
19+
20+
import lombok.RequiredArgsConstructor;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.springframework.http.HttpStatus;
24+
import org.springframework.http.ResponseEntity;
25+
import org.springframework.web.bind.annotation.PostMapping;
26+
import org.springframework.web.bind.annotation.RequestBody;
27+
import org.springframework.web.bind.annotation.RestController;
28+
import ru.mystamps.web.Url;
29+
import ru.mystamps.web.feature.series.importing.RequestImportForm;
30+
31+
import javax.validation.Valid;
32+
33+
@RestController
34+
@RequiredArgsConstructor
35+
public class SeriesSaleImportController {
36+
37+
private static final Logger LOG = LoggerFactory.getLogger(SeriesSaleImportController.class);
38+
39+
private final SeriesSalesImportService seriesSalesImportService;
40+
41+
@PostMapping(Url.IMPORT_SERIES_SALES)
42+
public ResponseEntity<SeriesSaleExtractedInfo> downloadAndParse(
43+
@RequestBody @Valid RequestImportForm form) {
44+
45+
String url = form.getUrl();
46+
47+
try {
48+
SeriesSaleExtractedInfo result = seriesSalesImportService.downloadAndParse(url);
49+
return ResponseEntity.ok(result);
50+
51+
} catch (RuntimeException ex) { // NOPMD: AvoidCatchingGenericException; try to catch-all
52+
LOG.error("Failed to process '{}': {}", url, ex.getMessage());
53+
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
54+
}
55+
}
56+
57+
}

src/main/java/ru/mystamps/web/feature/series/importing/sale/SeriesSalesImportConfig.java

+29-2
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,53 @@
1818
package ru.mystamps.web.feature.series.importing.sale;
1919

2020
import lombok.RequiredArgsConstructor;
21+
import org.springframework.beans.factory.annotation.Qualifier;
2122
import org.springframework.context.annotation.Bean;
2223
import org.springframework.context.annotation.Configuration;
2324
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
25+
import ru.mystamps.web.feature.series.DownloaderService;
26+
import ru.mystamps.web.feature.series.importing.SeriesInfoExtractorService;
27+
import ru.mystamps.web.feature.series.importing.extractor.SiteParserService;
2428

2529
/**
2630
* Spring configuration that is required for importing series sale in an application.
31+
*
32+
* The beans are grouped into two classes to make possible to register a controller
33+
* and the services in the separated application contexts.
2734
*/
2835
@Configuration
2936
public class SeriesSalesImportConfig {
3037

38+
@RequiredArgsConstructor
39+
public static class Controllers {
40+
41+
private final SeriesSalesImportService seriesSalesImportService;
42+
43+
@Bean
44+
public SeriesSaleImportController seriesSaleImportController() {
45+
return new SeriesSaleImportController(seriesSalesImportService);
46+
}
47+
48+
}
49+
3150
@RequiredArgsConstructor
3251
public static class Services {
3352

3453
private final NamedParameterJdbcTemplate jdbcTemplate;
54+
private final SiteParserService siteParserService;
55+
private final SeriesInfoExtractorService extractorService;
3556

3657
@Bean
3758
public SeriesSalesImportService seriesSalesImportService(
38-
SeriesSalesImportDao seriesSalesImportDao) {
59+
SeriesSalesImportDao seriesSalesImportDao,
60+
@Qualifier("seriesDownloaderService") DownloaderService seriesDownloaderService) {
3961

40-
return new SeriesSalesImportServiceImpl(seriesSalesImportDao);
62+
return new SeriesSalesImportServiceImpl(
63+
seriesSalesImportDao,
64+
seriesDownloaderService,
65+
siteParserService,
66+
extractorService
67+
);
4168
}
4269

4370
@Bean

src/main/java/ru/mystamps/web/feature/series/importing/sale/SeriesSalesImportService.java

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package ru.mystamps.web.feature.series.importing.sale;
1919

2020
public interface SeriesSalesImportService {
21+
SeriesSaleExtractedInfo downloadAndParse(String url);
2122
void saveParsedData(Integer requestId, SeriesSalesParsedDataDbDto seriesSalesParsedData);
2223
SeriesSaleParsedDataDto getParsedData(Integer requestId);
2324
}

src/main/java/ru/mystamps/web/feature/series/importing/sale/SeriesSalesImportServiceImpl.java

+63
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,75 @@
1919

2020
import lombok.RequiredArgsConstructor;
2121
import org.apache.commons.lang3.Validate;
22+
import org.springframework.security.access.prepost.PreAuthorize;
2223
import org.springframework.transaction.annotation.Transactional;
24+
import ru.mystamps.web.feature.series.DownloadResult;
25+
import ru.mystamps.web.feature.series.DownloaderService;
26+
import ru.mystamps.web.feature.series.importing.RawParsedDataDto;
27+
import ru.mystamps.web.feature.series.importing.SeriesExtractedInfo;
28+
import ru.mystamps.web.feature.series.importing.SeriesInfoExtractorService;
29+
import ru.mystamps.web.feature.series.importing.extractor.SeriesInfo;
30+
import ru.mystamps.web.feature.series.importing.extractor.SiteParser;
31+
import ru.mystamps.web.feature.series.importing.extractor.SiteParserService;
32+
import ru.mystamps.web.support.spring.security.HasAuthority;
2333

2434
@RequiredArgsConstructor
2535
public class SeriesSalesImportServiceImpl implements SeriesSalesImportService {
2636

2737
private final SeriesSalesImportDao seriesSalesImportDao;
38+
private final DownloaderService downloaderService;
39+
private final SiteParserService siteParserService;
40+
private final SeriesInfoExtractorService extractorService;
41+
42+
// @todo #995 SeriesSalesImportServiceImpl.downloadAndParse(): add unit tests
43+
@Override
44+
@Transactional(readOnly = true)
45+
@PreAuthorize(HasAuthority.IMPORT_SERIES)
46+
@SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes")
47+
public SeriesSaleExtractedInfo downloadAndParse(String url) {
48+
SiteParser parser = siteParserService.findForUrl(url);
49+
if (parser == null) {
50+
throw new RuntimeException("could not find an appropriate parser");
51+
}
52+
53+
DownloadResult result = downloaderService.download(url);
54+
if (result.hasFailed()) {
55+
String message = "could not download: " + result.getCode();
56+
throw new RuntimeException(message);
57+
}
58+
59+
String content = result.getDataAsString();
60+
61+
// @todo #995 SiteParser: introduce a method for parsing only sales-related info
62+
SeriesInfo info = parser.parse(content);
63+
if (info.isEmpty()) {
64+
throw new RuntimeException("could not parse the page");
65+
}
66+
67+
RawParsedDataDto data = new RawParsedDataDto(
68+
null,
69+
null,
70+
null,
71+
null,
72+
null,
73+
null,
74+
null,
75+
info.getSellerName(),
76+
info.getSellerUrl(),
77+
info.getPrice(),
78+
info.getCurrency()
79+
);
80+
81+
// CheckStyle: ignore LineLength for next 1 line
82+
// @todo #995 SeriesInfoExtractorService: introduce a method for parsing only sales-related info
83+
SeriesExtractedInfo seriesInfo = extractorService.extract(url, data);
84+
85+
return new SeriesSaleExtractedInfo(
86+
seriesInfo.getSellerId(),
87+
seriesInfo.getPrice(),
88+
seriesInfo.getCurrency()
89+
);
90+
}
2891

2992
// @todo #834 SeriesSalesImportServiceImpl.saveParsedData(): introduce dto without dates
3093
@Override

0 commit comments

Comments
 (0)