Skip to content

Commit d839472

Browse files
committed
Merge branch 'download_image'
Implement ability to download an image from another server. Fix #199
2 parents 411a846 + 12de38b commit d839472

37 files changed

+1277
-51
lines changed

NEWS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- (functionality) name of category/country in Russian now are optional fields
1616
- (functionality) preview now is generated after uploading an image
1717
- (functionality) add interface for adding buyers and sellers
18+
- (functionality) add capability to specify image URL (as alternative to providing a file)
1819

1920
0.3
2021
- (functionality) implemented possibility to user to add series to his collection

src/main/config/findbugs-filter.xml

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
<Class name="~.*\.dto\..*" />
99
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2" />
1010
</Match>
11+
<Match>
12+
<!--
13+
String[] allowedContentTypes: potentially caller can modify data after passing it to the
14+
constructor. I don't want to fix it because all such places are known and under our
15+
control.
16+
-->
17+
<Class name="ru.mystamps.web.service.HttpURLConnectionDownloaderService" />
18+
<Bug pattern="EI_EXPOSE_REP2" />
19+
</Match>
1120
<Match>
1221
<!--
1322
It's ok, that we're don't override parent's equals() method.

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

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.springframework.context.MessageSource;
2121
import org.springframework.context.annotation.Bean;
2222
import org.springframework.context.annotation.Configuration;
23+
import org.springframework.context.annotation.Profile;
2324

2425
import lombok.RequiredArgsConstructor;
2526

@@ -131,4 +132,10 @@ public SuggestionController getSuggestionController() {
131132
);
132133
}
133134

135+
@Bean
136+
@Profile({ "test", "travis" })
137+
public TestController getTestController() {
138+
return new TestController();
139+
}
140+
134141
}

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

+9
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
import ru.mystamps.web.Url;
4949
import ru.mystamps.web.controller.converter.LinkEntityDtoGenericConverter;
50+
import ru.mystamps.web.controller.interceptor.DownloadImageInterceptor;
5051
import ru.mystamps.web.support.spring.security.CurrentUserArgumentResolver;
5152

5253
@Configuration
@@ -126,6 +127,10 @@ public Validator getValidator() {
126127
@Override
127128
public void addInterceptors(InterceptorRegistry registry) {
128129
registry.addInterceptor(getLocaleChangeInterceptor());
130+
131+
registry
132+
.addInterceptor(getDownloadImageInterceptor())
133+
.addPathPatterns(Url.ADD_SERIES_PAGE, Url.ADD_IMAGE_SERIES_PAGE);
129134
}
130135

131136
@Override
@@ -149,4 +154,8 @@ private static HandlerInterceptor getLocaleChangeInterceptor() {
149154
return interceptor;
150155
}
151156

157+
private HandlerInterceptor getDownloadImageInterceptor() {
158+
return new DownloadImageInterceptor(servicesConfig.getDownloaderService());
159+
}
160+
152161
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ public CronService getCronService() {
8989
);
9090
}
9191

92+
@Bean
93+
public DownloaderService getDownloaderService() {
94+
return new HttpURLConnectionDownloaderService(new String[]{"image/jpeg", "image/png"});
95+
}
96+
9297
@Bean
9398
public ImageService getImageService() {
9499
return new ImageServiceImpl(

src/main/java/ru/mystamps/web/controller/SeriesController.java

+105-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Map;
3030
import java.util.Objects;
3131

32+
import javax.servlet.http.HttpServletRequest;
3233
import javax.servlet.http.HttpServletResponse;
3334
import javax.validation.Valid;
3435
import javax.validation.groups.Default;
@@ -62,6 +63,8 @@
6263
import ru.mystamps.web.controller.dto.AddImageForm;
6364
import ru.mystamps.web.controller.dto.AddSeriesForm;
6465
import ru.mystamps.web.controller.dto.AddSeriesSalesForm;
66+
import ru.mystamps.web.controller.dto.NullableImageUrl;
67+
import ru.mystamps.web.controller.interceptor.DownloadImageInterceptor;
6568
import ru.mystamps.web.dao.dto.EntityWithIdDto;
6669
import ru.mystamps.web.dao.dto.LinkEntityDto;
6770
import ru.mystamps.web.dao.dto.PurchaseAndSaleDto;
@@ -72,6 +75,7 @@
7275
import ru.mystamps.web.service.SeriesSalesService;
7376
import ru.mystamps.web.service.SeriesService;
7477
import ru.mystamps.web.service.TransactionParticipantService;
78+
import ru.mystamps.web.service.dto.DownloadResult;
7579
import ru.mystamps.web.service.dto.FirstLevelCategoryDto;
7680
import ru.mystamps.web.service.dto.SeriesDto;
7781
import ru.mystamps.web.support.spring.security.Authority;
@@ -176,15 +180,34 @@ public View showFormWithCountry(
176180
return view;
177181
}
178182

179-
@PostMapping(Url.ADD_SERIES_PAGE)
183+
@PostMapping(path = Url.ADD_SERIES_PAGE, params = "imageUrl")
184+
public String processInputWithImageUrl(
185+
@Validated({ Default.class,
186+
AddSeriesForm.ImageUrlChecks.class,
187+
AddSeriesForm.ReleaseDateChecks.class,
188+
AddSeriesForm.ImageChecks.class }) AddSeriesForm form,
189+
BindingResult result,
190+
@CurrentUser Integer currentUserId,
191+
Locale userLocale,
192+
Model model,
193+
HttpServletRequest request) {
194+
195+
return processInput(form, result, currentUserId, userLocale, model, request);
196+
}
197+
198+
@PostMapping(path = Url.ADD_SERIES_PAGE, params = "!imageUrl")
180199
public String processInput(
181200
@Validated({ Default.class,
201+
AddSeriesForm.RequireImageCheck.class,
182202
AddSeriesForm.ReleaseDateChecks.class,
183203
AddSeriesForm.ImageChecks.class }) AddSeriesForm form,
184204
BindingResult result,
185205
@CurrentUser Integer currentUserId,
186206
Locale userLocale,
187-
Model model) {
207+
Model model,
208+
HttpServletRequest request) {
209+
210+
loadErrorsFromDownloadInterceptor(form, result, request);
188211

189212
if (result.hasErrors()) {
190213
String lang = LocaleUtils.getLanguageOrNull(userLocale);
@@ -199,6 +222,7 @@ public String processInput(
199222

200223
// don't try to re-display file upload field
201224
form.setImage(null);
225+
form.setDownloadedImage(null);
202226
return null;
203227
}
204228

@@ -242,14 +266,47 @@ public String showInfo(
242266
return "series/info";
243267
}
244268

245-
@PostMapping(Url.ADD_IMAGE_SERIES_PAGE)
269+
@SuppressWarnings("checkstyle:parameternumber")
270+
@PostMapping(path = Url.ADD_IMAGE_SERIES_PAGE, params = "imageUrl")
271+
public String processImageWithImageUrl(
272+
@Validated({
273+
AddImageForm.ImageUrlChecks.class,
274+
AddImageForm.ImageChecks.class
275+
}) AddImageForm form,
276+
BindingResult result,
277+
@PathVariable("id") Integer seriesId,
278+
Model model,
279+
@CurrentUser Integer currentUserId,
280+
Locale userLocale,
281+
HttpServletRequest request,
282+
HttpServletResponse response)
283+
throws IOException {
284+
285+
return processImage(
286+
form,
287+
result,
288+
seriesId,
289+
model,
290+
currentUserId,
291+
userLocale,
292+
request,
293+
response
294+
);
295+
}
296+
297+
@SuppressWarnings("checkstyle:parameternumber")
298+
@PostMapping(path = Url.ADD_IMAGE_SERIES_PAGE, params = "!imageUrl")
246299
public String processImage(
247-
@Valid AddImageForm form,
300+
@Validated({
301+
AddImageForm.RequireImageCheck.class,
302+
AddImageForm.ImageChecks.class })
303+
AddImageForm form,
248304
BindingResult result,
249305
@PathVariable("id") Integer seriesId,
250306
Model model,
251307
@CurrentUser Integer currentUserId,
252308
Locale userLocale,
309+
HttpServletRequest request,
253310
HttpServletResponse response)
254311
throws IOException {
255312

@@ -265,6 +322,8 @@ public String processImage(
265322
return null;
266323
}
267324

325+
loadErrorsFromDownloadInterceptor(form, result, request);
326+
268327
boolean maxQuantityOfImagesExceeded = !isAdmin() && !isAllowedToAddingImages(series);
269328
model.addAttribute("maxQuantityOfImagesExceeded", maxQuantityOfImagesExceeded);
270329

@@ -475,6 +534,48 @@ private void addSeriesSalesFormToModel(Model model) {
475534
model.addAttribute("buyers", buyers);
476535
}
477536

537+
private static void loadErrorsFromDownloadInterceptor(
538+
NullableImageUrl form,
539+
BindingResult result,
540+
HttpServletRequest request) {
541+
542+
Object downloadResultErrorCode =
543+
request.getAttribute(DownloadImageInterceptor.ERROR_CODE_ATTR_NAME);
544+
545+
if (downloadResultErrorCode == null) {
546+
return;
547+
}
548+
549+
if (downloadResultErrorCode instanceof DownloadResult.Code) {
550+
DownloadResult.Code code = (DownloadResult.Code)downloadResultErrorCode;
551+
switch (code) {
552+
case INVALID_URL:
553+
// Url is being validated by @URL, to avoid showing an error message
554+
// twice we're skipping error from an interceptor.
555+
break;
556+
case INSUFFICIENT_PERMISSIONS:
557+
// A user without permissions has tried to download a file. It means that he
558+
// didn't specify a file but somehow provide a URL to an image. In this case,
559+
// let's show an error message that file is required.
560+
result.rejectValue(
561+
"image",
562+
"ru.mystamps.web.support.beanvalidation.NotEmptyFilename.message"
563+
);
564+
form.nullifyImageUrl();
565+
break;
566+
default:
567+
result.rejectValue(
568+
DownloadImageInterceptor.DOWNLOADED_IMAGE_FIELD_NAME,
569+
DownloadResult.class.getName() + "." + code.toString(),
570+
"Could not download image"
571+
);
572+
break;
573+
}
574+
}
575+
576+
request.removeAttribute(DownloadImageInterceptor.ERROR_CODE_ATTR_NAME);
577+
}
578+
478579
private static void addImageFormToModel(Model model) {
479580
AddImageForm form = new AddImageForm();
480581
model.addAttribute("addImageForm", form);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2009-2017 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.controller;
19+
20+
import java.io.IOException;
21+
22+
import javax.servlet.http.HttpServletResponse;
23+
24+
import org.springframework.stereotype.Controller;
25+
import org.springframework.web.bind.annotation.GetMapping;
26+
import org.springframework.web.bind.annotation.ResponseBody;
27+
28+
import ru.mystamps.web.Url;
29+
30+
@Controller
31+
public class TestController {
32+
33+
@GetMapping("/test/invalid/response-301")
34+
public void redirect(HttpServletResponse response) {
35+
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
36+
response.setHeader("Location", Url.SITE);
37+
}
38+
39+
@GetMapping("/test/invalid/response-400")
40+
public void badRequest(HttpServletResponse response) throws IOException {
41+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
42+
}
43+
44+
@GetMapping("/test/invalid/response-404")
45+
public void notFound(HttpServletResponse response) throws IOException {
46+
response.sendError(HttpServletResponse.SC_NOT_FOUND);
47+
}
48+
49+
@GetMapping("/test/invalid/empty-jpeg-file")
50+
public void emptyJpegFile(HttpServletResponse response) {
51+
response.setContentType("image/jpeg");
52+
response.setContentLength(0);
53+
}
54+
55+
@GetMapping(path = "/test/invalid/not-image-file", produces = "application/json")
56+
@ResponseBody
57+
public String simpleJson() {
58+
return "test";
59+
}
60+
61+
}

0 commit comments

Comments
 (0)