/* This file is part of Cyclos (www.cyclos.org). A project of the Social Trade Organisation (www.socialtrade.org). Cyclos is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Cyclos 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 General Public License for more details. You should have received a copy of the GNU General Public License along with Cyclos; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package nl.strohalm.cyclos.servlets; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import nl.strohalm.cyclos.CyclosConfiguration; import nl.strohalm.cyclos.annotations.Inject; import nl.strohalm.cyclos.entities.customization.images.Image; import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException; import nl.strohalm.cyclos.services.customization.ImageService; import nl.strohalm.cyclos.utils.CustomizationHelper; import nl.strohalm.cyclos.utils.ImageHelper.ImageType; import nl.strohalm.cyclos.utils.SpringHelper; import nl.strohalm.cyclos.utils.TransactionHelper; import nl.strohalm.cyclos.utils.customizedfile.CustomizedFileHandler; import nl.strohalm.cyclos.utils.customizedfile.ImageChangeListener; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; /** * This servlet will show images that are on database * @author luis */ public class ImageByIdServlet extends HttpServlet { /** * An image descriptor for images directly from DB * @author luis */ private class DBImageDescriptor extends ImageDescriptor { private final long id; private DBImageDescriptor(final HttpServletRequest request, final HttpServletResponse response, final long id) { super(request, response); this.id = id; } @Override public void write() throws IOException { transactionHelper.runInCurrentThread(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(final TransactionStatus status) { status.setRollbackOnly(); try { final Image image = imageService.load(id); if (!setLastModified(image.getLastModified().getTimeInMillis())) { // File has not changed since last request. Return return; } setContentType(image.getContentType()); // setContentLength(isThumbnail ? image.getThumbnailSize() : image.getImageSize()); final InputStream in = (isThumbnail ? image.getThumbnail() : image.getImage()).getBinaryStream(); writeContents(in); } catch (final EntityNotFoundException e) { sendError(HttpServletResponse.SC_NOT_FOUND); } catch (final Exception e) { sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } }); } public boolean writeIntoFile(final File file) { return transactionHelper.runInCurrentThread(new TransactionCallback<Boolean>() { @Override public Boolean doInTransaction(final TransactionStatus status) { status.setRollbackOnly(); try { final Image image = imageService.load(id); final InputStream in = (isThumbnail ? image.getThumbnail() : image.getImage()).getBinaryStream(); file.getParentFile().mkdirs(); IOUtils.copy(in, new FileOutputStream(file)); file.setLastModified(image.getLastModified().getTimeInMillis()); return true; } catch (final Exception e) { // Ignore } return false; } }); } } /** * An image descriptor for images from a cache file * @author luis */ private class FileImageDescriptor extends ImageDescriptor { private final File file; private FileImageDescriptor(final HttpServletRequest request, final HttpServletResponse response, final File file) { super(request, response); this.file = file; } @Override public void write() throws IOException { if (!setLastModified(file.lastModified())) { // File has not changed since last request. Return return; } try { setContentType(ImageType.getByContent(file).getContentType()); } catch (final Exception e) { // Ignore } setContentLength(file.length()); try { writeContents(new FileInputStream(file)); } catch (final FileNotFoundException e) { sendError(HttpServletResponse.SC_NOT_FOUND); } } } /** * Base class for image descriptors * @author luis */ private abstract class ImageDescriptor { private HttpServletRequest request; private HttpServletResponse response; private ImageDescriptor(final HttpServletRequest request, final HttpServletResponse response) { this.request = request; this.response = response; } protected void sendError(final int code) { try { response.sendError(code); } catch (final IOException e) { // ignore } } protected void setContentLength(final long length) { response.setContentLength((int) length); } protected void setContentType(final String contentType) { response.setContentType(contentType); } protected boolean setLastModified(final long lastModified) { final long ifModifiedSince = request.getDateHeader("If-Modified-Since"); if (ifModifiedSince > 0 && lastModified <= ifModifiedSince) { // The contents have not changed response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return false; } else { response.setDateHeader("Last-Modified", lastModified); return true; } } protected abstract void write() throws IOException; protected void writeContents(final InputStream in) { try { response.setStatus(HttpServletResponse.SC_OK); IOUtils.copy(in, response.getOutputStream()); response.flushBuffer(); } catch (final Exception e) { sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } finally { IOUtils.closeQuietly(in); } } } public static final String ROOT_CACHE_PATH = "/pages/images/cache"; public static final String IMAGES_CACHE_PATH = ROOT_CACHE_PATH + "/images"; public static final String THUMBNAILS_CACHE_PATH = ROOT_CACHE_PATH + "/thumbnails"; private static final long serialVersionUID = 7494480285703279642L; private static final Log LOG = LogFactory.getLog(ImageByIdServlet.class); private ImageService imageService; private TransactionHelper transactionHelper; private boolean isThumbnail; private boolean useCache; private CustomizationHelper customizationHelper; private File cacheDir; @Override public void init(final ServletConfig config) throws ServletException { super.init(config); final ServletContext context = getServletContext(); // Resolve dependencies SpringHelper.injectBeans(context, this); // Check if we will display thumbnails isThumbnail = "true".equals(config.getInitParameter("thumbnail")); // Check if we will use the disk cache try { useCache = "true".equals(CyclosConfiguration.getCyclosProperties().getProperty("cyclos.imageDiskCache.enable", "true")); } catch (final Exception e) { useCache = false; } // When using the disk cache, create the directories if (useCache) { cacheDir = new File(getServletContext().getRealPath(isThumbnail ? THUMBNAILS_CACHE_PATH : IMAGES_CACHE_PATH)); LOG.debug("Using disk cache for " + (isThumbnail ? "thumbnails" : "images") + " at " + cacheDir.getAbsolutePath()); } // Add a listener for image changes SpringHelper.bean(context, CustomizedFileHandler.class).addImageChangeListener(new ImageChangeListener() { @Override public void onImageChanged(final Image image) { final File file = new File(cacheDir, image.getId().toString()); customizationHelper.deleteFile(file); } }); } @Inject public void setCustomizationHelper(final CustomizationHelper customizationHelper) { this.customizationHelper = customizationHelper; } @Inject public void setImageService(final ImageService imageService) { this.imageService = imageService; } @Inject public void setTransactionHelper(final TransactionHelper transactionHelper) { this.transactionHelper = transactionHelper; } @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { // Get the image id long id; try { id = Long.parseLong(request.getParameter("id")); } catch (final Exception e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } ImageDescriptor image = null; if (useCache) { image = readFromFile(request, response, id); } else { image = readFromDB(request, response, id); } image.write(); } private DBImageDescriptor readFromDB(final HttpServletRequest request, final HttpServletResponse response, final long id) { return new DBImageDescriptor(request, response, id); } private FileImageDescriptor readFromFile(final HttpServletRequest request, final HttpServletResponse response, final long id) { final File file = new File(cacheDir, String.valueOf(id)); if (!file.exists()) { new DBImageDescriptor(request, response, id).writeIntoFile(file); } return new FileImageDescriptor(request, response, file); } }