Java tutorial
/* * MIT License * * Copyright (c) 2017 Oleg Popov <github@opopov.com> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.opopov.cloud.image.service; import com.opopov.cloud.image.utils.Utils; import com.opopov.cloud.image.api.ImageStitchingConfiguration; import com.opopov.cloud.image.exception.ImageLoadTimeoutException; import com.opopov.cloud.image.exception.ImageWriteException; import com.opopov.cloud.image.exception.SourceImageLoadException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.context.request.async.DeferredResult; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; /** * Created by elx01 on 3/20/17. */ @org.springframework.stereotype.Component public class ImageStitchingServiceImpl implements ImageStitchingService { private static final String content = "Test"; @Autowired Utils utils; @Autowired AsyncRestTemplate remoteResource; @Autowired Validator validator; public ImageStitchingServiceImpl() { super(); } private Optional<DecodedImage> decompressImage(int imageIndex, IndexMap indexMap, ResponseEntity<byte[]> resp) { Optional<BufferedImage> image = decompressImage(resp); Optional<DecodedImage> result = image.isPresent() ? Optional.of(new DecodedImage(image.get(), imageIndex)) : Optional.empty(); indexMap.put(imageIndex, result); return result; } private Optional<BufferedImage> decompressImage(ResponseEntity<byte[]> resp) { if (resp.getStatusCode() != HttpStatus.OK) { return Optional.empty(); } byte[] compressedBytes = resp.getBody(); try { BufferedImage image = ImageIO.read(new ByteArrayInputStream(compressedBytes)); return Optional.of(image); } catch (IOException e) { return Optional.empty(); } } @Override public DeferredResult<ResponseEntity<?>> getStitchedImage(@RequestBody ImageStitchingConfiguration config) { validator.validateConfig(config); List<ListenableFuture<ResponseEntity<byte[]>>> futures = config.getUrlList().stream() .map(url -> remoteResource.getForEntity(url, byte[].class)).collect(Collectors.toList()); //wrap the listenable futures into the completable futures //writing loop in pre-8 style, since it would be more concise compared to stream api in this case CompletableFuture[] imageFutures = new CompletableFuture[futures.size()]; int taskIndex = 0; IndexMap indexMap = new IndexMap(config.getRowCount() * config.getColumnCount()); for (ListenableFuture<ResponseEntity<byte[]>> f : futures) { imageFutures[taskIndex] = imageDataFromResponse(taskIndex, indexMap, utils.fromListenableFuture(f)); taskIndex++; } CompletableFuture<Void> allDownloadedAndDecompressed = CompletableFuture.allOf(imageFutures); //Synchronous part - start - writing decompressed bytes to the large image final int DOWNLOAD_AND_DECOMPRESS_TIMEOUT = 30; //30 seconds for each of the individual tasks DeferredResult<ResponseEntity<?>> response = new DeferredResult<>(); boolean allSuccessful = false; byte[] imageBytes = null; try { Void finishResult = allDownloadedAndDecompressed.get(DOWNLOAD_AND_DECOMPRESS_TIMEOUT, TimeUnit.SECONDS); imageBytes = combineImagesIntoStitchedImage(config, indexMap); HttpHeaders headers = new HttpHeaders(); headers.setCacheControl(CacheControl.noCache().getHeaderValue()); headers.setContentType(MediaType.IMAGE_JPEG); allSuccessful = true; } catch (InterruptedException | ExecutionException e) { // basically either download or decompression of the source image failed // just skip it then, we have no image to show response.setErrorResult( new SourceImageLoadException("Unable to load and decode one or more source images", e)); } catch (TimeoutException e) { //send timeout response, via ImageLoadTimeoutException response.setErrorResult(new ImageLoadTimeoutException( String.format("Some of the images were not loaded and decoded before timeout of %d seconds", DOWNLOAD_AND_DECOMPRESS_TIMEOUT), e )); } catch (IOException e) { response.setErrorResult(new ImageWriteException("Error writing image into output buffer", e)); } //Synchronous part - end if (!allSuccessful) { //shoud not get here, some unknown error response.setErrorResult( new ImageLoadTimeoutException("Unknown error", new RuntimeException("Something went wrong") )); return response; } ResponseEntity<?> successResult = ResponseEntity.ok(imageBytes); response.setResult(successResult); return response; } private byte[] combineImagesIntoStitchedImage(@RequestBody ImageStitchingConfiguration config, IndexMap indexMap) throws IOException { int tileWidth = config.getSourceWidth(); int tileHeight = config.getSourceHeight(); //we are creating this big image in memory for the very short time frame, just when //all source data has been downloaded and decoded BufferedImage image = new BufferedImage(config.getRowCount() * tileWidth, config.getColumnCount() * tileHeight, BufferedImage.TYPE_4BYTE_ABGR); Graphics g = image.getGraphics(); int indexOfTileInList = 0; for (int i = 0; i < config.getRowCount(); i++) { for (int j = 0; j < config.getColumnCount(); j++) { Optional<DecodedImage> decoded = indexMap.get(indexOfTileInList++); if (decoded != null) { if (decoded.isPresent()) { g.drawImage(decoded.get().getImage(), i * config.getSourceWidth(), j * config.getSourceHeight(), null); } } } } ByteArrayOutputStream buffer = new ByteArrayOutputStream(); image.flush(); ImageIO.write(image, config.getOutputFormat(), buffer); g.dispose(); return buffer.toByteArray(); } private CompletableFuture<Optional<DecodedImage>> imageDataFromResponse(int imageIndex, IndexMap indexMap, CompletableFuture<ResponseEntity<byte[]>> response) { return response.thenApply(bytes -> decompressImage(imageIndex, indexMap, bytes)); } }