/*
* Copyright (C) 2005-2012 BetaCONCEPT Limited
*
* This file is part of Astroboa.
*
* Astroboa is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Astroboa is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Astroboa. If not, see <http://www.gnu.org/licenses/>.
*/
package org.betaconceptframework.astroboa.resourceapi.filter;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.activation.MimetypesFileTypeMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.betaconceptframework.astroboa.api.model.CmsRepository;
import org.betaconceptframework.astroboa.api.service.RepositoryService;
import org.betaconceptframework.astroboa.resourceapi.utility.BinaryChannelFileAccessInfoProcessor;
import org.betaconceptframework.astroboa.resourceapi.utility.ContentApiUtils;
import org.betaconceptframework.utility.ImageUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Filter responsible to serve binary channel contents from jcr repository.
*
* It serves request url of name pattern
*
* <context-root>/f/binaryChannel/enc/<encryptedFileAccessInfo> or
* <context-root>/f/binaryChannel/<repository-id>/<binary-channel-relative-system-path>/<mime-type>/<source-file-name>
*
* In order to provide stream for binary channel it replaces <context-root>/binaryChannel/ with
* absolute path of repository home directory. All available repository home directories are provided by {@link RepositoryService repositoryService}
* during filter initialization.
*
* @author Gregory Chomatas (gchomatas@betaconcept.com)
* @author Savvas Triantafyllou (striantafyllou@betaconcept.com)
*
*/
public class BinaryChannelLoaderFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(BinaryChannelLoaderFilter.class);
private static final String BINARY_CHANNEL_FILTER_PREFIX = "/f/binaryChannel/";
private static final String ENCRYTPION_PREFIX = "enc";
private Map<String, String> repositoryHomeDirectoriesPerRepositoryId = new HashMap<String, String>();
private RepositoryService repositoryService;
public void setRepositoryService(RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
try{
String fileAccessInfo = extractFileAccessInfo(httpServletRequest);
if (StringUtils.isBlank(fileAccessInfo)){
logger.warn("Invalid http request {} sent to binary channel filter", httpServletRequest.getRequestURI());
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else{
logger.debug("Processing request {} in binary channel filter", fileAccessInfo);
//Determine if file access info is encrypted or not
if (fileAccessInfo.startsWith(ENCRYTPION_PREFIX)){
fileAccessInfo = decryptRequest(StringUtils.substringAfter(fileAccessInfo, ENCRYTPION_PREFIX+"/"));
}
loadAndReturnContentOfBinaryChannel(fileAccessInfo, httpServletResponse);
}
}
catch (Exception e){
logger.error("", e);
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
private void loadAndReturnContentOfBinaryChannel(String fileAccessInfo, HttpServletResponse httpServletResponse) throws IOException {
BinaryChannelFileAccessInfoProcessor fileAccessInfoProcessor = new BinaryChannelFileAccessInfoProcessor(fileAccessInfo);
if (!fileAccessInfoProcessor.processFileAccessInfo()) {
logger.warn("Invalid file access info "+ fileAccessInfo);
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else{
String repositoryId = fileAccessInfoProcessor.getRepositoryId();
String fileName = fileAccessInfoProcessor.getFileName();
String mimeType = fileAccessInfoProcessor.getMimeType();
String width = fileAccessInfoProcessor.getWidth();
String height = fileAccessInfoProcessor.getHeight();
String contentDispositionType = fileAccessInfoProcessor.getContentDispositionType();
String relativePathToStream = fileAccessInfoProcessor.getRelativePathToStream();
//Check that repository home directory exists
if (MapUtils.isEmpty(repositoryHomeDirectoriesPerRepositoryId) || StringUtils.isBlank(repositoryId) ||
! repositoryHomeDirectoriesPerRepositoryId.containsKey(repositoryId)){
logger.error("No available home directory exists for repository "+ repositoryId);
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else{
logger.debug("Ready to serve binary channel : path {}, mime-type: {}, filename :{}",
new Object[]{relativePathToStream, mimeType, fileName});
File resource = null;
try {
//Load file
resource = new File(repositoryHomeDirectoriesPerRepositoryId.get(repositoryId)+File.separator+relativePathToStream);
if (!resource.exists()){
logger.warn("Could not locate resource "+ repositoryHomeDirectoriesPerRepositoryId.get(repositoryId)+File.separator+relativePathToStream);
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else{
//It may well be the case where filename and mime type are not provided
//in file access info (especially if binary channel is unmanaged).
//In this cases obtain filename and mime type from resource
if (StringUtils.isBlank(fileName)){
fileName = resource.getName();
}
if (StringUtils.isBlank(mimeType)){
mimeType = new MimetypesFileTypeMap().getContentType(resource);
}
ServletOutputStream servletOutputStream = httpServletResponse.getOutputStream();
httpServletResponse.setDateHeader("Last-Modified", resource.lastModified());
httpServletResponse.setContentType(mimeType);
String processedFilename = org.betaconceptframework.utility.FilenameUtils.convertFilenameGreekCharactersToEnglishAndReplaceInvalidCharacters(fileName);
//
byte[] resourceByteArray = FileUtils.readFileToByteArray(resource);
if (StringUtils.isNotBlank(mimeType) && mimeType.startsWith("image/")){
resourceByteArray = resizeImageResource(resourceByteArray, mimeType, width, height);
httpServletResponse.setHeader("Content-Disposition", contentDispositionType+";filename="
+(width != null ? "W"+width : "")
+(height != null ?"H"+height: "")
+(width != null || height != null ? "-":"")
+processedFilename);
}
else{
//Resource is not an image. Set charset encoding
httpServletResponse.setCharacterEncoding("UTF-8");
}
if (! httpServletResponse.containsHeader("Content-Disposition")){
httpServletResponse.setHeader("Content-Disposition", contentDispositionType+";filename="+processedFilename);
}
httpServletResponse.setHeader("ETag", ContentApiUtils.createETag(resource.lastModified(), resourceByteArray.length));
httpServletResponse.setContentLength(resourceByteArray.length);
try{
IOUtils.write(resourceByteArray, servletOutputStream);
servletOutputStream.flush();
}
catch(Exception e)
{
//Something went wrong while writing data to stream.
//Just log to debug and not to warn
logger.debug("", e);
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
catch (Exception e) {
logger.error("", e);
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
}
private byte[] resizeImageResource(byte[] resourceByteArray, String mimeType, String width, String height)
throws IOException, InterruptedException, Exception {
int imageWidth = 0;
int imageHeight = 0;
if (StringUtils.isNotBlank(width)){
imageWidth = Integer.valueOf(width);
}
if (StringUtils.isNotBlank(height)){
imageHeight = Integer.valueOf(height);
}
if (imageWidth != 0 && imageHeight != 0) {
return ImageUtils.bufferedImageToByteArray(ImageUtils.resize(resourceByteArray, imageWidth, imageHeight), mimeType);
}
if (imageWidth != 0 && imageHeight == 0) {
return ImageUtils.bufferedImageToByteArray(ImageUtils.scaleToWidth(resourceByteArray, imageWidth), mimeType);
}
if (imageWidth == 0 && imageHeight != 0) {
return ImageUtils.bufferedImageToByteArray(ImageUtils.scaleToHeight(resourceByteArray, imageHeight), mimeType);
}
return resourceByteArray;
}
private String decryptRequest(String encryptedFileAccessInfo) {
return encryptedFileAccessInfo;
}
private String extractFileAccessInfo(HttpServletRequest httpServletRequest) {
//remove context-path and binaryChannel filter path from request
return StringUtils.replace(httpServletRequest.getRequestURI(), httpServletRequest.getContextPath()+BINARY_CHANNEL_FILTER_PREFIX, "");
}
/*private void calculateAbsoluteFileSystemPathAndFilenameForBinaryData(String contextPath,
String requestURI,
String absoluteFileSystemPathToBinaryData,
String binaryDataFilenameAndSuffix) {
String requiredURIRegularExpression = "^" +
contextPath +
BINARY_CHANNEL_PREFIX +
"([0-9abcdef]{2})" + // group 1
"/" +
"([0-9abcdef]{2})" + // group 2
"/" +
"([0-9abcdef]{2})" + // group 3
"/" +
"([0-9abcdef]{40})" + // group 4
"/" +
"([A-Za-z0-9_\\-]+)" + // group 5
"\\." +
"([A-Za-z0-9_\\-]{3})"; // group 6
Pattern requiredURIPattern = Pattern.compile(requiredURIRegularExpression);
Matcher uriMatcher = requiredURIPattern.matcher(requestURI);
if (uriMatcher.matches()) {
absoluteFileSystemPathToBinaryData = repositoryHomeDir + File.separator +
uriMatcher.group(1) +
uriMatcher.group(2) +
uriMatcher.group(3) +
uriMatcher.group(4);
binaryDataFilenameAndSuffix = uriMatcher.group(5) + "." + uriMatcher.group(6);
}
}*/
public void init(FilterConfig filterConfig) throws ServletException {
List<CmsRepository> availableRepositories = repositoryService.getAvailableCmsRepositories();
if (CollectionUtils.isNotEmpty(availableRepositories)){
for (CmsRepository cmsRepository: availableRepositories){
repositoryHomeDirectoriesPerRepositoryId.put(cmsRepository.getId(), cmsRepository.getRepositoryHomeDirectory());
}
}
else{
if (MapUtils.isEmpty(repositoryHomeDirectoriesPerRepositoryId))
logger.warn("Found no repository to load its home directory.");
}
}
}