Skip to content

Commit 5e4a9e6

Browse files
committed
/series/add: allow to specify image URL.
1 parent 7e5f02e commit 5e4a9e6

32 files changed

+1035
-20
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);
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

+67-2
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,7 @@
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.interceptor.DownloadImageInterceptor;
6567
import ru.mystamps.web.dao.dto.EntityWithIdDto;
6668
import ru.mystamps.web.dao.dto.LinkEntityDto;
6769
import ru.mystamps.web.dao.dto.PurchaseAndSaleDto;
@@ -72,6 +74,7 @@
7274
import ru.mystamps.web.service.SeriesSalesService;
7375
import ru.mystamps.web.service.SeriesService;
7476
import ru.mystamps.web.service.TransactionParticipantService;
77+
import ru.mystamps.web.service.dto.DownloadResult;
7578
import ru.mystamps.web.service.dto.FirstLevelCategoryDto;
7679
import ru.mystamps.web.service.dto.SeriesDto;
7780
import ru.mystamps.web.support.spring.security.Authority;
@@ -176,15 +179,34 @@ public View showFormWithCountry(
176179
return view;
177180
}
178181

179-
@PostMapping(Url.ADD_SERIES_PAGE)
182+
@PostMapping(path = Url.ADD_SERIES_PAGE, params = "imageUrl")
183+
public String processInputWithImageUrl(
184+
@Validated({ Default.class,
185+
AddSeriesForm.ImageUrlChecks.class,
186+
AddSeriesForm.ReleaseDateChecks.class,
187+
AddSeriesForm.ImageChecks.class }) AddSeriesForm form,
188+
BindingResult result,
189+
@CurrentUser Integer currentUserId,
190+
Locale userLocale,
191+
Model model,
192+
HttpServletRequest request) {
193+
194+
return processInput(form, result, currentUserId, userLocale, model, request);
195+
}
196+
197+
@PostMapping(path = Url.ADD_SERIES_PAGE, params = "!imageUrl")
180198
public String processInput(
181199
@Validated({ Default.class,
200+
AddSeriesForm.RequireImageCheck.class,
182201
AddSeriesForm.ReleaseDateChecks.class,
183202
AddSeriesForm.ImageChecks.class }) AddSeriesForm form,
184203
BindingResult result,
185204
@CurrentUser Integer currentUserId,
186205
Locale userLocale,
187-
Model model) {
206+
Model model,
207+
HttpServletRequest request) {
208+
209+
loadErrorsFromDownloadInterceptor(form, result, request);
188210

189211
if (result.hasErrors()) {
190212
String lang = LocaleUtils.getLanguageOrNull(userLocale);
@@ -199,6 +221,7 @@ public String processInput(
199221

200222
// don't try to re-display file upload field
201223
form.setImage(null);
224+
form.setDownloadedImage(null);
202225
return null;
203226
}
204227

@@ -475,6 +498,48 @@ private void addSeriesSalesFormToModel(Model model) {
475498
model.addAttribute("buyers", buyers);
476499
}
477500

501+
private static void loadErrorsFromDownloadInterceptor(
502+
AddSeriesForm form,
503+
BindingResult result,
504+
HttpServletRequest request) {
505+
506+
Object downloadResultErrorCode =
507+
request.getAttribute(DownloadImageInterceptor.ERROR_CODE_ATTR_NAME);
508+
509+
if (downloadResultErrorCode == null) {
510+
return;
511+
}
512+
513+
if (downloadResultErrorCode instanceof DownloadResult.Code) {
514+
DownloadResult.Code code = (DownloadResult.Code)downloadResultErrorCode;
515+
switch (code) {
516+
case INVALID_URL:
517+
// Url is being validated by @URL, to avoid showing an error message
518+
// twice we're skipping error from an interceptor.
519+
break;
520+
case INSUFFICIENT_PERMISSIONS:
521+
// A user without permissions has tried to download a file. It means that he
522+
// didn't specify a file but somehow provide a URL to an image. In this case,
523+
// let's show an error message that file is required.
524+
result.rejectValue(
525+
"image",
526+
"ru.mystamps.web.support.beanvalidation.NotEmptyFilename.message"
527+
);
528+
form.setImageUrl(null);
529+
break;
530+
default:
531+
result.rejectValue(
532+
DownloadImageInterceptor.DOWNLOADED_IMAGE_FIELD_NAME,
533+
DownloadResult.class.getName() + "." + code.toString(),
534+
"Could not download image"
535+
);
536+
break;
537+
}
538+
}
539+
540+
request.removeAttribute(DownloadImageInterceptor.ERROR_CODE_ATTR_NAME);
541+
}
542+
478543
private static void addImageFormToModel(Model model) {
479544
AddImageForm form = new AddImageForm();
480545
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+
}

src/main/java/ru/mystamps/web/controller/dto/AddSeriesForm.java

+64-11
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
import javax.validation.constraints.NotNull;
2626
import javax.validation.constraints.Size;
2727

28+
import org.apache.commons.lang3.StringUtils;
29+
2830
import org.hibernate.validator.constraints.Range;
31+
import org.hibernate.validator.constraints.URL;
2932

3033
import org.springframework.web.multipart.MultipartFile;
3134

@@ -37,6 +40,7 @@
3740
import ru.mystamps.web.dao.dto.LinkEntityDto;
3841
import ru.mystamps.web.service.dto.AddSeriesDto;
3942
import ru.mystamps.web.support.beanvalidation.CatalogNumbers;
43+
import ru.mystamps.web.support.beanvalidation.HasImageOrImageUrl;
4044
import ru.mystamps.web.support.beanvalidation.ImageFile;
4145
import ru.mystamps.web.support.beanvalidation.MaxFileSize;
4246
import ru.mystamps.web.support.beanvalidation.MaxFileSize.Unit;
@@ -45,6 +49,7 @@
4549
import ru.mystamps.web.support.beanvalidation.NotNullIfFirstField;
4650
import ru.mystamps.web.support.beanvalidation.Price;
4751
import ru.mystamps.web.support.beanvalidation.ReleaseDateIsNotInFuture;
52+
import ru.mystamps.web.support.beanvalidation.RequireImageOrImageUrl;
4853

4954
import static ru.mystamps.web.validation.ValidationRules.MAX_DAYS_IN_MONTH;
5055
import static ru.mystamps.web.validation.ValidationRules.MAX_IMAGE_SIZE;
@@ -58,6 +63,7 @@
5863
@Setter
5964
// TODO: combine price with currency to separate class
6065
@SuppressWarnings({ "PMD.TooManyFields", "PMD.AvoidDuplicateLiterals" })
66+
@RequireImageOrImageUrl(groups = AddSeriesForm.ImageUrl1Checks.class)
6167
@NotNullIfFirstField.List({
6268
@NotNullIfFirstField(
6369
first = "month", second = "year", message = "{month.requires.year}",
@@ -69,7 +75,7 @@
6975
)
7076
})
7177
@ReleaseDateIsNotInFuture(groups = AddSeriesForm.ReleaseDate3Checks.class)
72-
public class AddSeriesForm implements AddSeriesDto {
78+
public class AddSeriesForm implements AddSeriesDto, HasImageOrImageUrl {
7379

7480
// FIXME: change type to plain Integer
7581
@NotNull
@@ -128,13 +134,48 @@ public class AddSeriesForm implements AddSeriesDto {
128134
@Size(max = MAX_SERIES_COMMENT_LENGTH, message = "{value.too-long}")
129135
private String comment;
130136

131-
@NotNull
132-
@NotEmptyFilename(groups = Image1Checks.class)
133-
@NotEmptyFile(groups = Image2Checks.class)
134-
@MaxFileSize(value = MAX_IMAGE_SIZE, unit = Unit.Kbytes, groups = Image3Checks.class)
135-
@ImageFile(groups = Image3Checks.class)
137+
// Name of this field should match with the value of
138+
// DownloadImageInterceptor.UPLOADED_IMAGE_FIELD_NAME.
139+
@NotEmptyFilename(groups = RequireImageCheck.class)
140+
@NotEmptyFile(groups = Image1Checks.class)
141+
@MaxFileSize(value = MAX_IMAGE_SIZE, unit = Unit.Kbytes, groups = Image2Checks.class)
142+
@ImageFile(groups = Image2Checks.class)
136143
private MultipartFile image;
137144

145+
// Name of this field must match with the value of DownloadImageInterceptor.URL_PARAMETER_NAME.
146+
@URL(groups = AddSeriesForm.ImageUrl2Checks.class)
147+
private String imageUrl;
148+
149+
// This field holds a file that was downloaded from imageUrl.
150+
// Name of this field must match with the value of
151+
// DownloadImageInterceptor.DOWNLOADED_IMAGE_FIELD_NAME.
152+
@NotEmptyFile(groups = Image1Checks.class)
153+
@MaxFileSize(value = MAX_IMAGE_SIZE, unit = Unit.Kbytes, groups = Image2Checks.class)
154+
@ImageFile(groups = Image2Checks.class)
155+
private MultipartFile downloadedImage;
156+
157+
@Override
158+
public MultipartFile getImage() {
159+
if (hasImage()) {
160+
return image;
161+
}
162+
163+
return downloadedImage;
164+
}
165+
166+
// This method has to be implemented to satisfy HasImageOrImageUrl requirements.
167+
// The latter is being used by RequireImageOrImageUrl validator.
168+
@Override
169+
public boolean hasImage() {
170+
return image != null && StringUtils.isNotEmpty(image.getOriginalFilename());
171+
}
172+
173+
// This method has to be implemented to satisfy HasImageOrImageUrl requirements.
174+
// The latter is being used by RequireImageOrImageUrl validator.
175+
@Override
176+
public boolean hasImageUrl() {
177+
return StringUtils.isNotEmpty(imageUrl);
178+
}
138179

139180
@GroupSequence({
140181
ReleaseDate1Checks.class,
@@ -153,10 +194,25 @@ public interface ReleaseDate2Checks {
153194
public interface ReleaseDate3Checks {
154195
}
155196

197+
public interface RequireImageCheck {
198+
}
199+
200+
@GroupSequence({
201+
ImageUrl1Checks.class,
202+
ImageUrl2Checks.class,
203+
})
204+
public interface ImageUrlChecks {
205+
}
206+
207+
public interface ImageUrl1Checks {
208+
}
209+
210+
public interface ImageUrl2Checks {
211+
}
212+
156213
@GroupSequence({
157214
Image1Checks.class,
158-
Image2Checks.class,
159-
Image3Checks.class
215+
Image2Checks.class
160216
})
161217
public interface ImageChecks {
162218
}
@@ -167,7 +223,4 @@ public interface Image1Checks {
167223
public interface Image2Checks {
168224
}
169225

170-
public interface Image3Checks {
171-
}
172-
173226
}

0 commit comments

Comments
 (0)