Skip to content

Commit 5937d16

Browse files
committed
Add support for generating image previews.
Fix #387
1 parent 54cd812 commit 5937d16

32 files changed

+448
-23
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ env:
1111
before_script:
1212
- if [ "$SPRING_PROFILES_ACTIVE" = 'travis' ]; then
1313
mysql -u travis -e 'CREATE DATABASE mystamps CHARACTER SET utf8;';
14-
mkdir -p /tmp/uploads;
14+
mkdir -p /tmp/uploads /tmp/preview;
1515
if [ "$TRAVIS_BRANCH" = 'prod' ]; then
1616
pip install --user ansible==2.1.1.0;
1717
fi;

NEWS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- (functionality) add possibility for adding series sales
1414
- (functionality) add interface for viewing series sales (contributed by Sergey Chechenev)
1515
- (functionality) name of category/country in Russian now are optional fields
16+
- (functionality) preview now is generated after uploading an image
1617

1718
0.3
1819
- (functionality) implemented possibility to user to add series to his collection

docker/Dockerfile

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ ENV SPRING_PROFILES_ACTIVE=test
1616
# See also: https://docs.docker.com/engine/reference/builder/#run
1717
RUN mkdir /data \
1818
&& useradd mystamps --home-dir /data/mystamps --create-home --comment 'MyStamps' \
19-
&& mkdir /data/uploads /data/heap-dumps \
20-
&& chown mystamps:mystamps /data/uploads /data/heap-dumps
19+
&& mkdir /data/uploads /data/preview /data/heap-dumps \
20+
&& chown mystamps:mystamps /data/uploads /data/preview /data/heap-dumps
2121

2222
# Creates mount points and marks them as holding externally mounted volumes from native host.
2323
# See also: https://docs.docker.com/engine/reference/builder/#volume
24-
VOLUME /data/uploads /data/heap-dumps
24+
VOLUME /data/uploads /data/preview /data/heap-dumps
2525

2626
# Sets the user name to use when running the image and for any subsequent RUN, CMD and ENTRYPOINT instructions.
2727
# See also: https://docs.docker.com/engine/reference/builder/#user

pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
<version>${javax.validation.version}</version>
2323
</dependency>
2424

25+
<dependency>
26+
<groupId>net.coobird</groupId>
27+
<artifactId>thumbnailator</artifactId>
28+
<version>${thumbnailator.version}</version>
29+
</dependency>
30+
2531
<dependency>
2632
<groupId>org.apache.commons</groupId>
2733
<artifactId>commons-lang3</artifactId>
@@ -521,6 +527,7 @@
521527
<subethasmtp.version>3.1.7</subethasmtp.version>
522528
<surefire.plugin.version>2.19.1</surefire.plugin.version>
523529
<testng.version>6.8.8</testng.version>
530+
<thumbnailator.version>0.4.8</thumbnailator.version>
524531

525532
<!-- Redefine default value from spring-boot-dependencies (https://github.com/spring-projects/spring-boot/blob/v1.4.6.RELEASE/spring-boot-dependencies/pom.xml) -->
526533
<thymeleaf-extras-springsecurity4.version>3.0.2.RELEASE</thymeleaf-extras-springsecurity4.version>

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public final class Url {
6464

6565
public static final String INFO_COLLECTION_PAGE = "/collection/{slug}";
6666

67-
public static final String GET_IMAGE_PAGE = "/image/{id}";
67+
public static final String GET_IMAGE_PAGE = "/image/{id}";
68+
public static final String GET_IMAGE_PREVIEW_PAGE = "/image/preview/{id}";
6869

6970
public static final String FORBIDDEN_PAGE = "/error/403";
7071
public static final String NOT_FOUND_PAGE = "/error/404";
@@ -145,6 +146,7 @@ public static Map<String, String> asMap(boolean serveContentFromSingleHost) {
145146
map.put("SELECTIZE_CSS", SELECTIZE_CSS);
146147
map.put("SELECTIZE_JS", SELECTIZE_JS);
147148
map.put("GET_IMAGE_PAGE", GET_IMAGE_PAGE);
149+
map.put("GET_IMAGE_PREVIEW_PAGE", GET_IMAGE_PREVIEW_PAGE);
148150
map.put("FAVICON_ICO", FAVICON_ICO);
149151
map.put("MAIN_CSS", MAIN_CSS);
150152
map.put("CATALOG_UTILS_JS", CATALOG_UTILS_JS);
@@ -153,6 +155,7 @@ public static Map<String, String> asMap(boolean serveContentFromSingleHost) {
153155
} else {
154156
// Use separate domain for our own resources
155157
map.put("GET_IMAGE_PAGE", STATIC_RESOURCES_URL + GET_IMAGE_PAGE);
158+
map.put("GET_IMAGE_PREVIEW_PAGE", STATIC_RESOURCES_URL + GET_IMAGE_PREVIEW_PAGE);
156159
map.put("FAVICON_ICO", STATIC_RESOURCES_URL + FAVICON_ICO);
157160
map.put("MAIN_CSS", STATIC_RESOURCES_URL + MAIN_CSS);
158161
map.put("CATALOG_UTILS_JS", STATIC_RESOURCES_URL + CATALOG_UTILS_JS);

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

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public CronService getCronService() {
8080
public ImageService getImageService() {
8181
return new ImageServiceImpl(
8282
strategiesConfig.getImagePersistenceStrategy(),
83+
new TimedImagePreviewStrategy(new ThumbnailatorImagePreviewStrategy()),
8384
daoConfig.getImageDao()
8485
);
8586
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class FsStrategiesConfig implements StrategiesConfig {
5757
@Override
5858
public ImagePersistenceStrategy getImagePersistenceStrategy() {
5959
return new FilesystemImagePersistenceStrategy(
60-
env.getRequiredProperty("app.upload.dir")
60+
env.getRequiredProperty("app.upload.dir"),
61+
env.getRequiredProperty("app.preview.dir")
6162
);
6263
}
6364

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

+22
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,27 @@ public void getImage(@PathVariable("id") Integer imageId, HttpServletResponse re
6060
response.getOutputStream().write(image.getData());
6161
}
6262

63+
@GetMapping(Url.GET_IMAGE_PREVIEW_PAGE)
64+
public void getImagePreview(@PathVariable("id") Integer imageId, HttpServletResponse response)
65+
throws IOException {
66+
67+
if (imageId == null) {
68+
response.sendError(HttpServletResponse.SC_NOT_FOUND);
69+
return;
70+
}
71+
72+
ImageDto image = imageService.getOrCreatePreview(imageId);
73+
if (image == null) {
74+
// return original image when error has occurred
75+
getImage(imageId, response);
76+
return;
77+
}
78+
79+
response.setContentType("image/" + image.getType().toLowerCase(Locale.ENGLISH));
80+
response.setContentLength(image.getData().length);
81+
82+
response.getOutputStream().write(image.getData());
83+
}
84+
6385
}
6486

src/main/java/ru/mystamps/web/dao/ImageDataDao.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
import ru.mystamps.web.dao.dto.DbImageDto;
2222

2323
public interface ImageDataDao {
24-
DbImageDto findByImageId(Integer imageId);
24+
DbImageDto findByImageId(Integer imageId, boolean preview);
2525
Integer add(AddImageDataDbDto imageData);
2626
}

src/main/java/ru/mystamps/web/dao/dto/AddImageDataDbDto.java

+1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@
2525
public class AddImageDataDbDto {
2626
private Integer imageId;
2727
private byte[] content;
28+
private boolean preview;
2829
}

src/main/java/ru/mystamps/web/dao/impl/JdbcImageDataDao.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package ru.mystamps.web.dao.impl;
1919

20-
import java.util.Collections;
2120
import java.util.HashMap;
2221
import java.util.Map;
2322

@@ -48,11 +47,15 @@ public class JdbcImageDataDao implements ImageDataDao {
4847
private String addImageDataSql;
4948

5049
@Override
51-
public DbImageDto findByImageId(Integer imageId) {
50+
public DbImageDto findByImageId(Integer imageId, boolean preview) {
51+
Map<String, Object> params = new HashMap<>();
52+
params.put("image_id", imageId);
53+
params.put("preview", preview);
54+
5255
try {
5356
return jdbcTemplate.queryForObject(
5457
findByImageIdSql,
55-
Collections.singletonMap("image_id", imageId),
58+
params,
5659
RowMappers::forDbImageDto
5760
);
5861
} catch (EmptyResultDataAccessException ignored) {
@@ -65,6 +68,7 @@ public Integer add(AddImageDataDbDto imageData) {
6568
Map<String, Object> params = new HashMap<>();
6669
params.put("image_id", imageData.getImageId());
6770
params.put("content", imageData.getContent());
71+
params.put("preview", imageData.isPreview());
6872

6973
KeyHolder holder = new GeneratedKeyHolder();
7074

src/main/java/ru/mystamps/web/service/DatabaseImagePersistenceStrategy.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public void save(MultipartFile file, ImageInfoDto image) {
5353
AddImageDataDbDto imageData = new AddImageDataDbDto();
5454
imageData.setImageId(image.getId());
5555
imageData.setContent(file.getBytes());
56+
imageData.setPreview(false);
5657

5758
Integer id = imageDataDao.add(imageData);
5859
LOG.info("Image #{}: meta data has been saved to #{}", image.getId(), id);
@@ -63,9 +64,21 @@ public void save(MultipartFile file, ImageInfoDto image) {
6364
}
6465
}
6566

67+
@Override
68+
public void savePreview(byte[] data, ImageInfoDto image) {
69+
AddImageDataDbDto imageData = new AddImageDataDbDto();
70+
imageData.setImageId(image.getId());
71+
imageData.setContent(data);
72+
imageData.setPreview(true);
73+
74+
imageDataDao.add(imageData);
75+
76+
LOG.info("Image #{}: preview has been saved", image.getId());
77+
}
78+
6679
@Override
6780
public ImageDto get(ImageInfoDto image) {
68-
DbImageDto imageDto = imageDataDao.findByImageId(image.getId());
81+
DbImageDto imageDto = imageDataDao.findByImageId(image.getId(), false);
6982
if (imageDto == null) {
7083
LOG.warn("Image #{}: content not found", image.getId());
7184
return null;
@@ -74,4 +87,15 @@ public ImageDto get(ImageInfoDto image) {
7487
return imageDto;
7588
}
7689

90+
@Override
91+
public ImageDto getPreview(ImageInfoDto image) {
92+
DbImageDto imageDto = imageDataDao.findByImageId(image.getId(), true);
93+
if (imageDto == null) {
94+
LOG.info("Image #{}: preview not found", image.getId());
95+
return null;
96+
}
97+
98+
return imageDto;
99+
}
100+
77101
}

src/main/java/ru/mystamps/web/service/FilesystemImagePersistenceStrategy.java

+53-4
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ public class FilesystemImagePersistenceStrategy implements ImagePersistenceStrat
4040
LoggerFactory.getLogger(FilesystemImagePersistenceStrategy.class);
4141

4242
private final File storageDir;
43+
private final File previewDir;
4344

44-
public FilesystemImagePersistenceStrategy(String storageDir) {
45+
public FilesystemImagePersistenceStrategy(String storageDir, String previewDir) {
4546
this.storageDir = new File(storageDir);
47+
this.previewDir = new File(previewDir);
4648
}
4749

4850
@PostConstruct
@@ -54,11 +56,29 @@ public void init() {
5456

5557
} else if (!storageDir.canWrite()) {
5658
LOG.warn(
59+
// TODO(java9): log also user: ProcessHandle.current().info().user()
5760
"Directory '{}' exists but isn't writable for the current user! "
5861
+ "Image uploading won't work.",
5962
storageDir
6063
);
6164
}
65+
66+
LOG.info("Image previews will be saved into {} directory", previewDir);
67+
68+
if (!previewDir.exists()) { // NOPMD: ConfusingTernary (it's ok for me)
69+
LOG.warn(
70+
"Directory '{}' doesn't exist! Image preview generation won't work",
71+
previewDir
72+
);
73+
74+
} else if (!previewDir.canWrite()) {
75+
// TODO(java9): log also user: ProcessHandle.current().info().user()
76+
LOG.warn(
77+
"Directory '{}' exists but isn't writable for the current user! "
78+
+ "Image preview generation won't work",
79+
previewDir
80+
);
81+
}
6282
}
6383

6484
@Override
@@ -74,9 +94,27 @@ public void save(MultipartFile file, ImageInfoDto image) {
7494
}
7595
}
7696

97+
@Override
98+
public void savePreview(byte[] data, ImageInfoDto image) {
99+
try {
100+
Path dest = generateFilePath(previewDir, image);
101+
writeToFile(data, dest);
102+
103+
LOG.info("Image preview data has been written into file {}", dest);
104+
105+
} catch (IOException ex) {
106+
throw new ImagePersistenceException(ex);
107+
}
108+
}
109+
77110
@Override
78111
public ImageDto get(ImageInfoDto image) {
79-
return get(storageDir, image);
112+
return get(storageDir, image, true);
113+
}
114+
115+
@Override
116+
public ImageDto getPreview(ImageInfoDto image) {
117+
return get(previewDir, image, false);
80118
}
81119

82120
// protected to allow spying
@@ -93,6 +131,11 @@ protected void writeToFile(MultipartFile file, Path dest) throws IOException {
93131
Files.copy(file.getInputStream(), dest);
94132
}
95133

134+
// protected to allow spying
135+
protected void writeToFile(byte[] data, Path dest) throws IOException {
136+
Files.write(dest, data);
137+
}
138+
96139
// protected to allow spying
97140
protected boolean exists(Path path) {
98141
return Files.exists(path);
@@ -103,10 +146,16 @@ protected byte[] toByteArray(Path dest) throws IOException {
103146
return Files.readAllBytes(dest);
104147
}
105148

106-
private ImageDto get(File dir, ImageInfoDto image) {
149+
private ImageDto get(File dir, ImageInfoDto image, boolean logWarning) {
107150
Path dest = generateFilePath(dir, image);
108151
if (!exists(dest)) {
109-
LOG.warn("Found image without content: #{} ({} doesn't exist)", image.getId(), dest);
152+
if (logWarning) {
153+
LOG.warn(
154+
"Image #{}: content not found ({} doesn't exist)",
155+
image.getId(),
156+
dest
157+
);
158+
}
110159
return null;
111160
}
112161

src/main/java/ru/mystamps/web/service/ImagePersistenceStrategy.java

+2
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@
2424

2525
public interface ImagePersistenceStrategy {
2626
void save(MultipartFile file, ImageInfoDto image);
27+
void savePreview(byte[] data, ImageInfoDto image);
2728
ImageDto get(ImageInfoDto image);
29+
ImageDto getPreview(ImageInfoDto image);
2830
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.service;
19+
20+
public interface ImagePreviewStrategy {
21+
byte[] createPreview(byte[] image);
22+
}

src/main/java/ru/mystamps/web/service/ImageService.java

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
public interface ImageService {
2727
Integer save(MultipartFile file);
2828
ImageDto get(Integer imageId);
29+
ImageDto getOrCreatePreview(Integer imageId);
2930
void addToSeries(Integer seriesId, Integer imageId);
3031
List<Integer> findBySeriesId(Integer seriesId);
3132
}

0 commit comments

Comments
 (0)