/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso 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.
*
* Jspresso 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 Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.util.resources.server;
import static org.jspresso.framework.util.image.ImageHelper.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jspresso.framework.util.exception.NestedRuntimeException;
import org.jspresso.framework.util.gui.Dimension;
import org.jspresso.framework.util.gui.Icon;
import org.jspresso.framework.util.http.HttpRequestHolder;
import org.jspresso.framework.util.image.ImageHelper;
import org.jspresso.framework.util.io.IoHelper;
import org.jspresso.framework.util.resources.AbstractResource;
import org.jspresso.framework.util.resources.IActiveResource;
import org.jspresso.framework.util.resources.IResource;
import org.jspresso.framework.util.resources.IResourceBase;
import org.jspresso.framework.util.url.UrlHelper;
/**
* This servlet class returns the web resource which matches the specified id
* request parameter requesting it to the resource manager.
*
* @author Vincent Vandenschrick
*/
public abstract class ResourceProviderServlet extends HttpServlet {
private static final long serialVersionUID = 5253634459280974738L;
/**
* the url pattern to activate a resource download.
*/
private static final String DOWNLOAD_SERVLET_URL_PATTERN = "/download";
/**
* id.
*/
private static final String ID_PARAMETER = "id";
/**
* height.
*/
private static final String IMAGE_HEIGHT_PARAMETER = "height";
/**
* imageUrl.
*/
private static final String IMAGE_URL_PARAMETER = "imageUrl";
/**
* width.
*/
private static final String IMAGE_WIDTH_PARAMETER = "width";
/**
* preferSVG.
*/
private static final String IMAGE_PREFER_SVG_PARAMETER = "preferSVG";
/**
* localUrl.
*/
private static final String LOCAL_URL_PARAMETER = "localUrl";
/**
* omitFileName.
*/
private static final String OMIT_FILE_NAME_PARAMETER = "omitFileName";
/**
* the url pattern to activate a resource upload.
*/
private static final String UPLOAD_SERVLET_URL_PATTERN = "/upload";
/**
* the regex pattern to match in order to allow the download of a local
* resource.
*/
private static final String DEFAULT_LOCAL_URL_REGEX = "(classpath|http):[A-Za-z0-9_\\-/ ]*\\." +
"(png|jpg|jpeg|gif|pdf|swf.?)(&width=\\d*+&height=\\d*+)?";
private static final String ALLOWED_LOCAL_URL_REGEX_KEY = "allowedLocalUrlRegex";
private Pattern allowedLocalUrlPattern = Pattern
.compile(
DEFAULT_LOCAL_URL_REGEX,
Pattern.CASE_INSENSITIVE);
private static final Logger LOG = LoggerFactory
.getLogger(ResourceProviderServlet.class);
/**
* {@inheritDoc}
*/
@Override
public void init() {
String allowedLocalUrlRegex = getInitParameter(ALLOWED_LOCAL_URL_REGEX_KEY);
if (allowedLocalUrlRegex != null && allowedLocalUrlRegex.length() > 0) {
allowedLocalUrlPattern = Pattern.compile(allowedLocalUrlRegex,
Pattern.CASE_INSENSITIVE);
}
}
/**
* Computes the url where the resource is available for download.
*
* @param id
* the resource id.
* @return the resource url.
*/
public static String computeDownloadUrl(String id) {
HttpServletRequest request = HttpRequestHolder.getServletRequest();
return computeDownloadUrl(request, id);
}
/**
* Computes the url where the image is available for download.
*
* @param icon
* the icon to load the image for.
* @param dimension
* the requested dimension for the icon if the icon dimension is not
* set.
* @return the resource url.
*/
public static String computeImageResourceDownloadUrl(Icon icon,
Dimension dimension) {
if (icon == null) {
return null;
}
Dimension actualIconSize = dimension;
if (icon.getDimension() != null) {
actualIconSize = icon.getDimension();
}
return computeImageResourceDownloadUrl(icon.getIconImageURL(),
actualIconSize);
}
/**
* Computes the url where the image is available for download.
*
* @param localImageUrl
* the image local url.
* @param dimension
* the requested dimension for the image.
* @return the resource url.
*/
public static String computeImageResourceDownloadUrl(String localImageUrl,
Dimension dimension) {
if (localImageUrl != null) {
HttpServletRequest request = HttpRequestHolder.getServletRequest();
StringBuilder buf = new StringBuilder("?" + IMAGE_URL_PARAMETER + "="
+ localImageUrl);
if (dimension != null) {
buf.append("&" + IMAGE_WIDTH_PARAMETER + "=").append(dimension.getWidth());
buf.append("&" + IMAGE_HEIGHT_PARAMETER + "=").append(dimension.getHeight());
}
return computeUrl(request, buf.toString());
}
return null;
}
/**
* Computes the url where the resource is available for download.
*
* @param localUrl
* the resource local url.
* @return the resource url.
*/
public static String computeLocalResourceDownloadUrl(String localUrl) {
return computeLocalResourceDownloadUrl(localUrl, false);
}
/**
* Computes the url where the resource is available for download.
*
* @param localUrl
* the resource local url.
* @param omitFileName
* when set to true, the file name will not be added as
* Content-disposition header in the response. This helps to
* workaround security issues in flash SWFLoader.
* @return the resource url.
*/
public static String computeLocalResourceDownloadUrl(String localUrl,
boolean omitFileName) {
if (localUrl != null) {
HttpServletRequest request = HttpRequestHolder.getServletRequest();
return computeUrl(request, "?" + OMIT_FILE_NAME_PARAMETER + "="
+ omitFileName + "&" + LOCAL_URL_PARAMETER + "=" + localUrl);
}
return null;
}
/**
* Computes a static URL based on servlet request.
*
* @param relativePath
* the relative path.
* @return the absolute static URL.
*/
public static String computeStaticUrl(String relativePath) {
return computeStaticUrl(HttpRequestHolder.getServletRequest(), relativePath);
}
/**
* Computes the url where the resource can be uploaded.
*
* @return the resource url.
*/
public static String computeUploadUrl() {
HttpServletRequest request = HttpRequestHolder.getServletRequest();
return computeUploadUrl(request);
}
/**
* Computes the url where the resource is available for download.
*
* @param request
* the incoming HTTP request.
* @param id
* the resource id.
* @return the resource url.
*/
private static String computeDownloadUrl(HttpServletRequest request, String id) {
return computeUrl(request, "?" + ID_PARAMETER + "=" + id);
}
/**
* Computes a static URL based on servlet request.
*
* @param request
* the servlet request.
* @param relativePath
* the relative path.
* @return the absolute static URL.
*/
private static String computeStaticUrl(HttpServletRequest request,
String relativePath) {
return request.getScheme() + "://" + request.getServerName() + ":"
+ request.getServerPort() + request.getContextPath() + "/"
+ relativePath;
}
/**
* Computes the url where the resource can be uploaded.
*
* @param request
* the incoming HTTP request.
* @return the resource url.
*/
private static String computeUploadUrl(HttpServletRequest request) {
String baseUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath()
+ UPLOAD_SERVLET_URL_PATTERN;
return baseUrl;
}
private static String computeUrl(HttpServletRequest request,
String getParameters) {
String baseUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath()
+ DOWNLOAD_SERVLET_URL_PATTERN;
return baseUrl + getParameters;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
HttpRequestHolder.setServletRequest(request);
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(request);
response.setContentType("text/xml");
ServletOutputStream out = response.getOutputStream();
for (FileItem item : items) {
if (!item.isFormField()) {
out.print("<resource");
IResourceBase uploadResource = new UploadResourceAdapter(
"application/octet-stream", item);
String resourceId = ResourceManager.getInstance().register(
uploadResource);
out.print(" id=\"" + resourceId);
// Sometimes prevents the browser to parse back the result
// out.print("\" name=\"" + HtmlHelper.escapeForHTML(item.getName()));
out.println("\" />");
}
}
out.flush();
out.close();
} catch (Exception ex) {
LOG.error("An unexpected error occurred while uploading the content.", ex);
} finally {
HttpRequestHolder.setServletRequest(null);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
try {
HttpRequestHolder.setServletRequest(request);
String localUrlSpec = request.getParameter(LOCAL_URL_PARAMETER);
String imageUrlSpec = request.getParameter(IMAGE_URL_PARAMETER);
String id = request.getParameter(ID_PARAMETER);
boolean ommitFileName = Boolean.parseBoolean(request
.getParameter(OMIT_FILE_NAME_PARAMETER));
if (id == null && localUrlSpec == null && imageUrlSpec == null) {
throw new ServletException("No resource id nor local URL specified.");
}
BufferedInputStream inputStream = null;
if (id != null) {
IResourceBase resource = ResourceManager.getInstance()
.getRegistered(id);
if (resource == null) {
throw new ServletException("Bad resource id : " + id);
}
response.setContentType(resource.getMimeType());
if (!ommitFileName) {
completeFileName(response, resource.getName());
}
long resourceLength = resource.getSize();
if (resourceLength > 0) {
response.setContentLength((int) resourceLength);
}
if (resource instanceof IResource) {
inputStream = new BufferedInputStream(
((IResource) resource).getContent());
} else if (resource instanceof IActiveResource) {
OutputStream outputStream = response.getOutputStream();
try {
writeActiveResource((IActiveResource) resource, outputStream);
} catch (RuntimeException ex) {
try (PrintStream ps = new PrintStream(outputStream)) {
ex.printStackTrace(new PrintWriter(ps, true));
}
throw ex;
}
}
} else if (localUrlSpec != null) {
if (!UrlHelper.isClasspathUrl(localUrlSpec)) {
// we must append parameters that are passed AFTER the localUrl
// parameter as they must be considered as part of the localUrl.
String queryString = request.getQueryString();
localUrlSpec = queryString.substring(
queryString.indexOf(LOCAL_URL_PARAMETER)
+ LOCAL_URL_PARAMETER.length() + 1, queryString.length());
}
if (isLocalUrlAllowed(localUrlSpec)) {
URL localUrl = UrlHelper.createURL(localUrlSpec);
if (localUrl == null) {
throw new ServletException("Bad local URL : " + localUrlSpec);
}
if (!ommitFileName) {
completeFileName(response, localUrl.getFile());
}
inputStream = new BufferedInputStream(localUrl.openStream());
} else {
LOG.warn(
"The resource provider servlet filtered a forbidden local URL request ({}). You can adapt the regex "
+ "security filtering options by modifying the [{}] init parameter on the servlet.",
localUrlSpec, ALLOWED_LOCAL_URL_REGEX_KEY);
LOG.warn("Current value is {} = {}", ALLOWED_LOCAL_URL_REGEX_KEY,
allowedLocalUrlPattern.pattern());
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
} else {
if (isLocalUrlAllowed(imageUrlSpec)) {
URL imageUrl = UrlHelper.createURL(imageUrlSpec);
if (imageUrl == null) {
throw new ServletException("Bad image URL : " + imageUrlSpec);
}
String file = imageUrl.getFile();
if (!ommitFileName) {
completeFileName(response, file);
}
String width = request.getParameter(IMAGE_WIDTH_PARAMETER);
String height = request.getParameter(IMAGE_HEIGHT_PARAMETER);
String preferSVGValue = request.getParameter(IMAGE_PREFER_SVG_PARAMETER);
boolean preferSVG = false;
if ("true".equalsIgnoreCase(preferSVGValue)) {
preferSVG = true;
}
String extension = null;
if (file != null) {
int lastDotIndex = file.lastIndexOf(".");
if (lastDotIndex >= 0 && lastDotIndex < file.length()) {
extension = file.substring(lastDotIndex + 1).toLowerCase();
if (preferSVG && !extension.equalsIgnoreCase(SVG)) {
URL svgImageUrl = UrlHelper.createURL(imageUrlSpec.replaceAll(extension, SVG));
if (svgImageUrl != null) {
try (InputStream svgIS = svgImageUrl.openStream()) {
imageUrl = svgImageUrl;
file = file.replaceAll(extension, SVG);
extension = SVG;
} catch (IOException ioe) {
// SVG alternative does not exist.
}
}
}
}
String contentType = URLConnection.guessContentTypeFromName(file);
if (contentType == null) {
if (extension != null && extension.equals(SVG)) {
contentType = SVG_CONTENT_TYPE;
}
}
if (contentType != null) {
response.setContentType(contentType);
}
}
if (width != null && height != null) {
byte[] scaledImageBytes = ImageHelper.scaleImage(imageUrl, Integer.parseInt(width), Integer.parseInt(height),
extension == null ? PNG : extension);
if (scaledImageBytes != null) {
inputStream = new BufferedInputStream(new ByteArrayInputStream(scaledImageBytes));
}
} else {
inputStream = new BufferedInputStream(imageUrl.openStream());
}
} else {
LOG.warn(
"The resource provider servlet filtered a forbidden image URL request ({}). You can adapt the regex "
+ "security filtering options by modifying the [{}] init parameter on the servlet.",
imageUrlSpec, ALLOWED_LOCAL_URL_REGEX_KEY);
LOG.warn("Current value is {} = {}", ALLOWED_LOCAL_URL_REGEX_KEY,
allowedLocalUrlPattern.pattern());
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
if (inputStream != null) {
BufferedOutputStream outputStream = new BufferedOutputStream(
response.getOutputStream());
IoHelper.copyStream(inputStream, outputStream);
inputStream.close();
outputStream.close();
}
} catch (ServletException | IOException ex) {
LOG.error(
"An exception occurred when dealing with the following request : [{}]",
request.getRequestURL(), ex);
try {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (IOException ioe) {
throw new NestedRuntimeException(ioe,
"An exception occurred while sending back a "
+ HttpServletResponse.SC_NOT_FOUND + "error.");
}
} finally {
HttpRequestHolder.setServletRequest(null);
}
}
/**
* Writes an active resource to the servlet output stream.
*
* @param resource
* the resource to write.
* @param outputStream
* the servlet outputStream.
* @throws IOException
* whenever an IO exception occurs.
*/
protected void writeActiveResource(IActiveResource resource,
OutputStream outputStream) throws IOException {
resource.writeToContent(outputStream);
}
private void completeFileName(HttpServletResponse response, String fileName) {
String actualFileName = fileName;
if (fileName != null && fileName.length() > 0) {
int pathIndex = fileName.lastIndexOf("/");
if (pathIndex > 0) {
actualFileName = fileName.substring(pathIndex + 1);
}
response.setHeader("Content-Disposition", "attachment; filename="
+ actualFileName);
}
}
private boolean isLocalUrlAllowed(String localUrl) {
return localUrl == null
|| allowedLocalUrlPattern.matcher(localUrl).matches();
}
private static class UploadResourceAdapter extends AbstractResource {
private final FileItem item;
/**
* Constructs a new {@code UploadResourceAdapter} instance.
*
* @param mimeType
* the resource mime type.
* @param item
* the resource file item.
*/
public UploadResourceAdapter(String mimeType, FileItem item) {
super(mimeType);
this.item = item;
}
/**
* {@inheritDoc}
*/
@Override
public InputStream getContent() throws IOException {
return item.getInputStream();
}
/**
* {@inheritDoc}
*/
@Override
public String getName() {
return item.getName();
}
/**
* {@inheritDoc}
*/
@Override
public long getSize() {
return item.getSize();
}
}
}