/*
This file is part of leafdigital picstory.
picstory 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 3 of the License, or
(at your option) any later version.
picstory 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 picstory. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 Samuel Marshall.
*/
package com.leafdigital.picstory;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.concurrent.Semaphore;
import javax.imageio.*;
import javax.imageio.stream.FileImageOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
/**
* Handles requests for story page.
*/
public class StoryHandler extends RequestHandler
{
private final static int BUFFER_SIZE = 64 * 1024;
private File cacheRoot, storyRoot;
private Semaphore resizeSemaphore;
private static enum Size
{
W800(800),
W600(600),
W400(400),
W300(300),
W200(200),
W100(100);
int width;
Size(int width)
{
this.width = width;
}
public int getMaxWidth()
{
return width;
}
public int getMaxHeight()
{
return (width * 3) / 4;
}
}
/**
* @param mainServlet Main servlet
* @param cacheRoot Root folder for cache
* @param storyRoot Root folder for stories
* @param resizeThreads Max number of simultaneous image resizes
* @throws ServletException Any error constructing standard objects
*/
public StoryHandler(MainServlet mainServlet, File cacheRoot, File storyRoot,
int resizeThreads)
throws ServletException
{
super(mainServlet);
this.cacheRoot = cacheRoot;
this.storyRoot = storyRoot;
resizeSemaphore = new Semaphore(resizeThreads);
}
/**
* @param r Request
* @param storyName Story name
* @throws UserException Error processing story
* @throws IOException Any I/O error
*/
public void get(Request r, String storyName) throws UserException, IOException
{
// Get story
Story story = getMainServlet().getStories().getStory(
storyName, r.isReload());
// Output story
getMainServlet().sendPage(r, "story", story.getTitle(), story.getContent());
}
/**
* Call to return the initial xml that can be used to construct a story file.
* @param r Request
* @param storyName Storry name
* @throws UserException Story folder doesn't exist
* @throws IOException Any I/O error
*/
public void getBasicXml(Request r, String storyName) throws UserException, IOException
{
// Get folder
File folder = new File(storyRoot, storyName);
if(!folder.exists())
{
throw new UserException(HttpServletResponse.SC_NOT_FOUND,
"Story folder '" + Util.esc(storyName) + "' not found");
}
// List images in folder
File[] files = folder.listFiles(new FilenameFilter()
{
@Override
public boolean accept(File dir, String name)
{
return name.endsWith(".jpg");
}
});
if(files == null)
{
files = new File[0];
}
Arrays.sort(files, new Comparator<File>()
{
@Override
public int compare(File o1, File o2)
{
long diff = o1.lastModified() - o2.lastModified();
if (diff < 0)
{
return -1;
}
else if(diff > 0)
{
return 1;
}
else
{
return o1.getName().compareTo(o2.getName());
}
}
});
// Create index file that contains all these
StringBuilder out = new StringBuilder();
out.append("<picstory date=\"2100-01-01\">\n"
+ "\t<title>Title</title>\n"
+ "\t<description>\n"
+ "\t\t<p>Description</p>\n"
+ "\t</description>\n"
+ "\t<story>\n"
+ "\t\t<subhead>Under construction</subhead>\n"
+ "\t\t<p>This story's under construction. Please come back later.</p>\n");
boolean first = true;
for(File file : files)
{
out.append("\t\t<pic src=\"" + file.getName().replaceFirst("\\.jpg$", "")
+ "\"" + (first ? " indexpic=\"y\"" : "") + ">\n"
+ "\t\t\t\n"
+ "\t\t</pic>\n");
first = false;
}
out.append("\t</story>\n</picstory>\n");
// Send as download file
r.getResponse().addHeader("Content-Disposition",
"attachment; filename=index.xml");
r.outputText(HttpServletResponse.SC_OK, "text/xml", out.toString());
}
/**
* @param r Request
* @param storyName Story name
* @param picName Pic name
* @param hash Hash (short)
* @param sizeString Size string
* @throws IOException Any error
* @throws UserException File not found, etc
*/
public void getPic(Request r, String storyName, String picName, String hash,
String sizeString) throws IOException, UserException
{
// Handle if-modified-since (it never is, because of the hash)
if(r.handleIfModifiedSince())
{
return;
}
// Get story and pic
Story story = getMainServlet().getStories().getStory(
storyName, r.isReload());
Pic pic = story.getPic(picName);
if(pic == null)
{
throw new UserException(HttpServletResponse.SC_NOT_FOUND,
"Picture '" + Util.esc(picName) + "' not found");
}
// Redirect if hash changed
if(!pic.getHash().equals(hash))
{
r.redirect(picName + "." + pic.getHash() + "." + sizeString + ".jpg");
return;
}
// Check size is valid
Size size;
try
{
size = Size.valueOf(sizeString.toUpperCase());
}
catch(IllegalArgumentException e)
{
throw new UserException(HttpServletResponse.SC_NOT_FOUND,
"Size '" + sizeString + "' not available");
}
// OK, all valid, so let's send it
r.preventExpiry();
byte[] buffer = new byte[BUFFER_SIZE];
File picFile = getPicFile(storyName, pic, size);
OutputStream out = r.outputBinaryHeaders(
HttpServletResponse.SC_OK, "image/jpeg", (int)picFile.length());
FileInputStream in = new FileInputStream(picFile);
while(true)
{
int read = in.read(buffer);
if(read <= 0)
{
break;
}
out.write(buffer, 0, read);
}
in.close();
out.close();
}
private File getPicFile(String storyName, Pic pic, Size size)
throws InternalException
{
// Look for file in cache folder
File cache = new File(new File(cacheRoot, storyName),
pic.getFilename() + "." + pic.getHash() + "."
+ size.toString().toLowerCase() + ".jpg");
synchronized(pic)
{
if(!cache.exists())
{
try
{
resizeSemaphore.acquire();
// Original file
File original = new File(new File(storyRoot, storyName),
pic.getFilename() + ".jpg");
BufferedImage image = ImageIO.read(original);
// Calculate new size
int restrictWidth1 = image.getWidth(), restrictHeight1 = image.getHeight();
if(image.getWidth() > size.getMaxWidth())
{
restrictHeight1 = (int)Math.round(
(double)size.getMaxWidth() / (double)image.getWidth() * image.getHeight());
restrictWidth1 = size.getMaxWidth();
}
int restrictWidth2 = image.getWidth(), restrictHeight2 = image.getHeight();
if(image.getHeight() > size.getMaxHeight())
{
restrictWidth2 = (int)Math.round(
(double)size.getMaxHeight() / (double)image.getHeight() * image.getWidth());
restrictHeight2 = size.getMaxHeight();
}
int newWidth = Math.min(restrictWidth1, restrictWidth2);
int newHeight = Math.min(restrictHeight1, restrictHeight2);
// Image needs resizing
if(newWidth < image.getWidth() || newHeight < image.getHeight())
{
Image scaled = image.getScaledInstance(newWidth, newHeight,
Image.SCALE_AREA_AVERAGING);
image = null;
image = new BufferedImage(newWidth, newHeight,
BufferedImage.TYPE_INT_RGB);
image.getGraphics().drawImage(scaled, 0, 0, null);
}
// Create directory if required
File parent = cache.getParentFile();
if(!parent.exists())
{
if(!parent.mkdir())
{
throw new InternalException("Error creating folder");
}
}
// Write file
ImageWriter jpegWriter =
ImageIO.getImageWritersByFormatName("jpeg").next();
ImageWriteParam param = jpegWriter.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(0.75f);
FileImageOutputStream output = new FileImageOutputStream(cache);
jpegWriter.setOutput(output);
jpegWriter.write(null, new IIOImage(image, null, null), param);
output.close();
}
catch(Exception e)
{
throw new InternalException(
"Error processing file " + cache.getName(), e);
}
finally
{
resizeSemaphore.release();
}
}
}
return cache;
}
}