All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.henkexbg.gallery.service.impl.GalleryServiceImpl Maven / Gradle / Ivy

Go to download

There is a newer version: 1.1.1
Show newest version
/**
 * Copyright (c) 2016 Henrik Bjerne
 * 

* 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.github.henkexbg.gallery.service.impl; import static org.apache.commons.io.FilenameUtils.*; import static org.apache.commons.io.FileUtils.*; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; import com.github.henkexbg.gallery.bean.GalleryDirectory; import org.apache.commons.io.comparator.NameFileComparator; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.henkexbg.gallery.service.GalleryAuthorizationService; import com.github.henkexbg.gallery.service.GalleryService; import com.github.henkexbg.gallery.service.ImageResizeService; import com.github.henkexbg.gallery.service.VideoConversionService; import com.github.henkexbg.gallery.bean.GalleryFile; import com.github.henkexbg.gallery.bean.GalleryFile.GalleryFileType; import com.github.henkexbg.gallery.service.exception.NotAllowedException; import javax.annotation.PostConstruct; /** * Implementation of the {@link GalleryService} interface. A number of services * are tied together via this class, such as services for authorization, * resizing and conversion. In addition, this class defines a number of file * formats it will accept. Any other formats will be disregarded even if the * files are allowed in terms of location. * * @author Henrik Bjerne */ public class GalleryServiceImpl implements GalleryService { public static final String VIDEO_MODE_ORIGINAL = "ORIGINAL"; public static final String DEFAULT_IMAGE_FILE_ENDING = "jpg"; public static final String DIR_IMAGE_DIR_NAME = "_directoryImages_"; private final Logger LOG = LoggerFactory.getLogger(getClass()); private GalleryAuthorizationService galleryAuthorizationService; private ImageResizeService imageResizeService; private VideoConversionService videoConversionService; private File resizeDir; private Set allowedFileExtensions; private final IOFileFilter fileFilter = new CaseInsensitiveFileEndingFilter(); private int maxImageWidth = 5000; private int maxImageHeight = 5000; private int directoryImageMaxAgeMinutes = 1440; private File dirImageDir; @PostConstruct public void setUp() { dirImageDir = new File(resizeDir, DIR_IMAGE_DIR_NAME); if (!dirImageDir.exists()) { dirImageDir.mkdir(); } } public void setGalleryAuthorizationService(GalleryAuthorizationService galleryAuthorizationService) { this.galleryAuthorizationService = galleryAuthorizationService; } public void setImageResizeService(ImageResizeService imageResizeService) { this.imageResizeService = imageResizeService; } public void setVideoConversionService(VideoConversionService videoConversionService) { this.videoConversionService = videoConversionService; } public void setResizeDir(File resizeDir) { this.resizeDir = resizeDir; } public void setAllowedFileExtensions(Set allowedFileExtensions) { this.allowedFileExtensions = allowedFileExtensions; } public void setMaxImageWidth(int maxImageWidth) { this.maxImageWidth = maxImageWidth; } public void setMaxImageHeight(int maxImageHeight) { this.maxImageHeight = maxImageHeight; } public void setDirectoryImageMaxAgeMinutes(int directoryImageMaxAgeMinutes) { this.directoryImageMaxAgeMinutes = directoryImageMaxAgeMinutes; } @Override public List getRootDirectories() throws IOException, NotAllowedException { List rootDirCodes = new ArrayList( galleryAuthorizationService.getRootPathsForCurrentUser().keySet()); List galleryDirectories = new ArrayList<>(rootDirCodes.size()); Collections.sort(rootDirCodes, String.CASE_INSENSITIVE_ORDER); for (String oneRootDirCode : rootDirCodes) { File oneRootDir = getRealFileOrDir(oneRootDirCode); galleryDirectories.add(createGalleryDirectory(oneRootDirCode, oneRootDir)); } return galleryDirectories; } @Override public GalleryFile getImage(String publicPath, int width, int height) throws IOException, NotAllowedException { LOG.debug("Entering getImage(publicPath={}, width={}, height={}", publicPath, width, height); if (width <= 0 || width > maxImageWidth || height <= 0 || height > maxImageHeight) { String errorMessage = String.format("Non valid image size requested. Width: %s, height: %s", width, height); LOG.error(errorMessage); throw new IOException(errorMessage); } File realFile = getRealFileOrDir(publicPath); if (realFile.isDirectory()) { realFile = getDirectoryImage(realFile); if (realFile == null) { // Should never happen, but directory images can exist as empty files if there // are no images in that // directory. This is just an extra check that we don't even bother to resize // that case throw new FileNotFoundException(); } } File resizedImage = null; boolean isVideo = isVideo(realFile); if (isVideo) { resizedImage = determineResizedVideoImage(realFile, width, height); } else { resizedImage = determineResizedImage(realFile, width, height); } LOG.debug("Resized filename: {}", resizedImage.getCanonicalPath()); if (!resizedImage.exists()) { LOG.debug("Resized file did not exist."); if (!realFile.exists()) { String errorMessage = String.format("Main realFile %s did not exist. Could not resize.", realFile.getCanonicalPath()); LOG.error(errorMessage); throw new FileNotFoundException(errorMessage); } if (isVideo) { videoConversionService.generateImageForVideo(realFile, resizedImage, width, height); } else { imageResizeService.resizeImage(realFile, resizedImage, width, height); } } return createGalleryFile(publicPath, resizedImage); } @Override public List getDirectoryListingFiles(String publicPath) throws IOException, NotAllowedException { File dir = getRealFileOrDir(publicPath); if (!dir.isDirectory()) { LOG.debug("File {} is not a directory. Returning null.", dir); return null; } Collection fileCollection = (Collection) listFiles(dir, fileFilter, null); List fileList = new ArrayList(fileCollection); Collections.sort(fileList, NameFileComparator.NAME_INSENSITIVE_COMPARATOR); LOG.debug("Found {} files for path {}", fileList.size(), publicPath); ArrayList galleryFiles = new ArrayList(fileList.size()); for (File oneFile : fileList) { galleryFiles.add(createGalleryFile(publicPath + '/' + oneFile.getName(), oneFile)); } return galleryFiles; } @Override public List getDirectories(String publicPath) throws IOException, NotAllowedException { File dir = getRealFileOrDir(publicPath); List galleryDirectories = new ArrayList<>(); if (dir.isDirectory()) { List directories = Arrays.asList(dir.listFiles(File::isDirectory)); LOG.debug("Found {} directories for path {}", directories.size(), publicPath); Collections.sort(directories, NameFileComparator.NAME_INSENSITIVE_COMPARATOR); for (File oneDir : directories) { String oneDirPublicPath = buildPublicPathForFileInPublicDir(publicPath, oneDir); galleryDirectories.add(createGalleryDirectory(oneDirPublicPath, oneDir)); } } return galleryDirectories; } @Override public List getAllVideos() throws IOException, NotAllowedException { Map rootPathsForCurrentUser = galleryAuthorizationService.getRootPathsForCurrentUser(); Map> rootPathVideos = new HashMap<>(); for (Entry oneEntry : rootPathsForCurrentUser.entrySet()) { List videosForRootPath = listFiles(oneEntry.getValue(), fileFilter, FileFilterUtils.directoryFileFilter()).stream().filter(f -> isVideoNoException(f)) .collect(Collectors.toList()); rootPathVideos.put(oneEntry.getKey(), videosForRootPath); } List galleryFiles = new ArrayList<>(); for (Entry> oneEntry : rootPathVideos.entrySet()) { for (File oneFile : oneEntry.getValue()) { galleryFiles.add(getGalleryFile(getPublicPathFromRealFile(oneEntry.getKey(), oneFile))); } } LOG.debug("Returning {} video files", galleryFiles.size()); return galleryFiles; } @Override public GalleryFile getGalleryFile(String publicPath) throws IOException, NotAllowedException { LOG.debug("Entering getGalleryFile(publicPath={})", publicPath); File file = getRealFileOrDir(publicPath); if (file.isDirectory()) { throw new FileNotFoundException("Directories not allowed in this method!"); } return createGalleryFile(publicPath, file); } @Override public List getAvailableVideoModes() { List availableVideoModes = new ArrayList<>(videoConversionService.getAvailableVideoModes()); availableVideoModes.add(VIDEO_MODE_ORIGINAL); return availableVideoModes; } @Override public GalleryFile getVideo(String publicPath, String videoMode) throws IOException, NotAllowedException { LOG.debug("Entering getVideo(publicPath={}, videoMode={}", publicPath, videoMode); if (StringUtils.isEmpty(videoMode) || !getAvailableVideoModes().contains(videoMode)) { throw new IOException("videoMode not defined!"); } File video = getRealFileOrDir(publicPath); File convertedVideo = null; if (VIDEO_MODE_ORIGINAL.equals(videoMode)) { LOG.debug("Video mode was {}. Will return original video.", VIDEO_MODE_ORIGINAL); convertedVideo = video; } else { convertedVideo = determineConvertedVideo(video, videoMode); LOG.debug("Converted video filename: {}", convertedVideo); if (!convertedVideo.exists()) { LOG.debug("Resized file did not exist."); if (!video.exists()) { String errorMessage = String.format("Main video %s did not exist. Could not resize.", video.getCanonicalPath()); LOG.error(errorMessage); throw new FileNotFoundException(errorMessage); } videoConversionService.convertVideo(video, convertedVideo, videoMode); } } return createGalleryFile(publicPath, convertedVideo); } /** * Generates a public path for a file, given the public path of the directory * the file resides in and the actual file. * * @param directoryPublicPath Public path of directory. * @param fileInPublicDir Actual file. * @return The public path for the given file */ private String buildPublicPathForFileInPublicDir(String directoryPublicPath, File fileInPublicDir) { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(directoryPublicPath); pathBuilder.append(File.separator); pathBuilder.append(fileInPublicDir.getName()); return separatorsToUnix(pathBuilder.toString()); } /** * Looks up the actual file based on the public path. This method also checks * that the current user has right to access the file in question. * * @param publicPath Public path * @return The corresponding file, if existing and user is allowed to access * @throws IOException If any file operation fails * @throws FileNotFoundException If file cannot be found * @throws NotAllowedException If explicitly not allowed to access file */ private File getRealFileOrDir(String publicPath) throws IOException, FileNotFoundException, NotAllowedException { LOG.debug("Entering getRealFileOrDir(publicPath={})", publicPath); if (StringUtils.isBlank(publicPath)) { throw new FileNotFoundException("Could not extract code from empty path!"); } int index = publicPath.indexOf("/"); if (index < 0) { index = publicPath.length(); } String baseDirCode = publicPath.substring(0, index); LOG.debug("baseDirCode: {}", baseDirCode); File baseDir = galleryAuthorizationService.getRootPathsForCurrentUser().get(baseDirCode); if (baseDir == null) { String errorMessage = String.format("Could not find basedir for base dir code {}", baseDirCode); LOG.error(errorMessage); throw new FileNotFoundException(errorMessage); } File file = null; String relativePath = publicPath.substring(index, publicPath.length()); LOG.debug("Relative path: {}", relativePath); if (StringUtils.isNotBlank(relativePath)) { file = new File(baseDir, relativePath); if (!galleryAuthorizationService.isAllowed(file)) { throw new NotAllowedException("File " + file + " not allowed!"); } } else { // Don't need to check allowed on baseDir as this was just returned // from the authorization service. file = baseDir; } if (!file.exists()) { throw new FileNotFoundException("File not found!"); } if (!file.isDirectory() && !isAllowedExtension(file)) { throw new NotAllowedException("File " + publicPath + " did not have an allowed file extension"); } return file; } /** * A kind of inverse lookup - finding the public path given the actual file. * NOTE! This method does NOT verify that the current user actually has * the right to access the given publicRoot! It is the responsibility of calling * methods to make sure only allowed root paths are used. * * @param publicRoot Public root dir * @param file Actual file * @return The public path of the given file for the given publicRoot. * @throws IOException * @throws NotAllowedException */ private String getPublicPathFromRealFile(String publicRoot, File file) throws IOException, NotAllowedException { String actualFilePath = file.getCanonicalPath(); File rootFile = galleryAuthorizationService.getRootPathsForCurrentUser().get(publicRoot); String relativePath = actualFilePath.substring(rootFile.getCanonicalPath().length(), actualFilePath.length()); StringBuilder builder = new StringBuilder(); builder.append(publicRoot); builder.append(relativePath); String publicPath = separatorsToUnix(builder.toString()); LOG.debug("Actual file: {}, generated public path: {}", file, publicPath); return publicPath; } /** * Creates a {@link GalleryDirectory} given the public path and the actual * directory. Public path is required as multiple public paths can point to the * same actual directory. * * @param publicPath Public path. * @param actualDir Directory. * @return A {@link GalleryDirectory} for the given parameters * @throws IOException */ private GalleryDirectory createGalleryDirectory(String publicPath, File actualDir) throws IOException { GalleryDirectory galleryDirectory = new GalleryDirectory(); galleryDirectory.setPublicPath(publicPath); galleryDirectory.setName(actualDir.getName()); File directoryImage = getDirectoryImage(actualDir); if (directoryImage != null) { // Use the public path of the directory, and combine it with the image galleryDirectory.setImage(createGalleryFile(publicPath, directoryImage)); } return galleryDirectory; } /** * Retrieves the image for a directory. If necessary the image will be generated * first. * * @param directory Directory * @return The generated image, or null if no image could be generated, for * example because there are no images in the directory. * @throws IOException */ private File getDirectoryImage(File directory) throws IOException { File directoryImage = determineDirectoryImage(directory); if (!directoryImage.exists() || directoryImage.lastModified() < System.currentTimeMillis() - (directoryImageMaxAgeMinutes * 60000)) { LOG.debug("Evaluating directory image for {}", directory); List imagesForCompositeDirectoryImage = findImagesForCompositeDirectoryImage(directory); if (!imagesForCompositeDirectoryImage.isEmpty()) { long newestSourceImageTimestamp = imagesForCompositeDirectoryImage.stream() .sorted((a, b) -> Long.compare(b.lastModified(), a.lastModified())).findFirst().get() .lastModified(); if (directoryImage.exists() && newestSourceImageTimestamp < directoryImage.lastModified()) { // Extra optimization. If the directory image has expired, but the composite // images are not newer // than the current directory image, just update the last modified on the // directory image LOG.debug("Keeping expired directory image, renewing timestamp"); directoryImage.setLastModified(System.currentTimeMillis()); } else { LOG.debug("Will generate new composite image for directory {}", directoryImage); try { imageResizeService.generateCompositeImage(imagesForCompositeDirectoryImage, directoryImage, maxImageWidth, maxImageHeight); } catch (IOException ioe) { String errorMessage = String.format( "Error when generating composite image for %s. Returning null.", directory.getCanonicalPath()); LOG.error(errorMessage, ioe); directoryImage = null; } } } else { // Create empty file so that we can check the timestamp towards it and not // always try to generate a new // file for directories without images. directoryImage.createNewFile(); } } if (directoryImage == null || !directoryImage.exists() || directoryImage.length() == 0) { return null; } return directoryImage; } /** * Searches through the given directory for images that can be used for a * composite directory image. * * @param directory Directory * @return A list with files pointing to images of approved file content types. * May return empty list if none found */ private List findImagesForCompositeDirectoryImage(File directory) { final int nrImages = 4; List foundFiles = listFiles(directory, fileFilter, null).stream().filter(f -> !isVideoNoException(f)) .collect(Collectors.toList()); if (foundFiles.size() >= nrImages) { return foundFiles; } File[] directories = directory.listFiles(File::isDirectory); if (directories != null) { int i = 0; while (i < directories.length && foundFiles.size() < nrImages) { foundFiles.addAll(listFiles(directories[i], fileFilter, null).stream() .filter(f -> !isVideoNoException(f)).collect(Collectors.toList())); i++; } } return foundFiles.subList(0, Math.min(nrImages, foundFiles.size())); } /** * Creates a {@link GalleryFile} for the given file. Public path is required as * multiple public paths can point to the same actual file. This method should * always be given a file with an approved image or video, and never a * directory. * * @param publicPath Public path. * @param actualFile File to convert to {@link GalleryFile}. * @return A {@link GalleryFile} based on the given parameters * @throws IOException */ private GalleryFile createGalleryFile(String publicPath, File actualFile) throws IOException { String contentType = getContentType(actualFile); GalleryFile galleryFile = new GalleryFile(); galleryFile.setPublicPath(publicPath); galleryFile.setActualFile(actualFile); galleryFile.setContentType(contentType); if (isVideo(actualFile)) { galleryFile.setType(GalleryFileType.VIDEO); } else { galleryFile.setType(GalleryFileType.IMAGE); } return galleryFile; } /** * Determines the content type for a given file. Will delegate to JVM/operating * system. * * @param file File. * @return Content type for given file. * @throws IOException */ private String getContentType(File file) throws IOException { return Files.probeContentType(file.toPath()); } /** * Simpler helper method that ignores the possible exception when checking * whether a given file is a video. Will return false if an exception is caught. * * @param file File. * @return True if video, false if not or exception was thrown. */ private boolean isVideoNoException(File file) { try { return isVideo(file); } catch (IOException ioe) { return false; } } /** * Simpler helper method that determines whether a file is a video. * * @param file File. * @return True if video. * @throws IOException */ private boolean isVideo(File file) throws IOException { return StringUtils.startsWith(getContentType(file), "video"); } /** * Generates the filename for a resized file and creates a file object (does not * perform any file operation) given a file and its rescaling parameters. resize * parameters. * * @param originalFile * @param width * @param height * @return * @throws IOException */ private File determineResizedImage(File originalFile, int width, int height) throws IOException { String resizePart = Integer.valueOf(width).toString() + "x" + Integer.valueOf(height).toString(); File resizedImage = new File(resizeDir, File.separator + resizePart + File.separator + escapeFilePath(originalFile) + (originalFile.isDirectory() ? '.' + DEFAULT_IMAGE_FILE_ENDING : "")); return resizedImage; } /** * Generates the filename for a resized image for a video and creates a file * object (does not perform any file operation) given a video file and its * rescaling parameters. resize parameters. * * @param originalFile Video. * @param width Max width to scale image to. * @param height Max height to scale image to. * @return A {@link File} object for the scaled image. * @throws IOException */ private File determineResizedVideoImage(File originalFile, int width, int height) throws IOException { File resizedImage = determineResizedImage(originalFile, width, height); return new File(resizedImage.getCanonicalPath() + '.' + DEFAULT_IMAGE_FILE_ENDING); } /** * Generates the filename for a converted video and creates a file object (does * not perform any file operation) given an original video file and its * rescaling parameters. * * @param originalFile Video. * @param videoMode Video mode as per {@link #getAvailableVideoModes()}. * @return A {@link File} object for the converted video * @throws IOException */ private File determineConvertedVideo(File originalFile, String videoMode) throws IOException { File convertedVideo = new File(resizeDir, File.separator + videoMode + File.separator + escapeFilePath(originalFile)); return convertedVideo; } /** * Determines the file (or essentially filename) of an image dedicated for a * directory. * * @param directory Directory * @return A file pointing to the directory image. * @throws IOException */ private File determineDirectoryImage(File directory) throws IOException { String filename = directory.getName() + '-' + directory.getCanonicalPath().hashCode() + '.' + DEFAULT_IMAGE_FILE_ENDING; File dirImage = new File(dirImageDir, filename); return dirImage; } /** * Small util method helping with escaping any characters that would not be * allowed in a path. The obvious use case here is Windows and it's drive letter * followed by a ':'. Since the whole path will be appended to another root path * that character is not allowed. * * @param file * @return * @throws IOException */ private String escapeFilePath(File file) throws IOException { return file.getCanonicalPath().replace(":", "_"); } /** * Checks whether file has an allowed file extension. Checked towards * {@link #allowedFileExtensions} in a case insensitive way. * * @param file File * @return True if allowed */ private boolean isAllowedExtension(File file) { return allowedFileExtensions.contains(getExtension(file.getName()).toLowerCase()); } /** * Filters allowed files based on file ending. Special case as well for * directory images that are generated by this class, that should not be part of * normal file listing. */ private class CaseInsensitiveFileEndingFilter implements IOFileFilter { @Override public boolean accept(File file) { return isAllowedExtension(file); } @Override public boolean accept(File dir, String name) { return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy