// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.google.appengine.demos.mandelbrot;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.jsr107cache.Cache;
import net.sf.jsr107cache.CacheException;
import net.sf.jsr107cache.CacheManager;
/**
* {@code FractalTileServlet} generates and serves image tiles that
* make up a fractal image of arbitrary size and resolution.
*
* <p>This servlet expects to receive a tile level as well as x and y
* coordinates for the requested tile via {@link
* HttpServletRequest#getPathInfo()}. For example, if this servlet
* were mapped to the URI pattern {@code /tiles/*"}, it would expect
* to receive requests such as {@code /tiles/1/0_0.png}, which would
* correspond to a single tile that encompassed all of the image at
* level 1. At higher levels, more tiles would be possible
* (e.g. {@code /tiles/10/9_7.png}.
*
* <p>Generated tiles will be stored in {@link MemcacheService} in an
* attempt to serve commonly-viewed tiles more quickly. However, note
* that the amount of imagery that this servlet can serve is
* practically limitless, so the percentage of the image that can be
* stored in the cache at any given time is extraordinarily low.
*
* @author schwardo@google.com (Don Schwarz)
* @author nickjohnson@google.com (Nick Johnson)
*/
public class FractalTileServlet extends HttpServlet {
private static final Logger logger = Logger.getLogger(FractalTileServlet.class.getName());
/**
* Expect requests of the form {@code level/x_y.ext}, where {@code
* level}, {@code x}, and {@code y} are integers and {@code ext} is
* a file extension.
*/
private static final Pattern PATH_INFO_PATTERN = Pattern.compile("^/(\\d+)/(\\d+)_(\\d+)\\..*$");
/**
* Maximum amount of time that clients are allowed to cache tiles.
* This is just an arbitrary amount of time, so we'll just use one
* year.
*/
private static final long MAX_AGE = 365 * 24 * 60 * 60;
/**
* Store a single {@link Cache} reference for use across requests.
*/
private Cache cache;
/**
* This {@link TileFactory} will be used to generate {@link
* PixelSource} objects for each request.
*/
private TileFactory tileFactory;
/**
* This {@link ImageWriter} will be used to generate the byte stream
* that we return to the client.
*/
private ImageWriter imageWriter;
/**
* Initialize the above fields.
*/
@Override
public void init() throws ServletException {
// We could extract parameters from ServletContext here if we had
// other fractal types, parameters, or even alternate image types.
cache = createCache();
tileFactory = new TileFactory(new MandelbrotSource(new Palette()));
imageWriter = new PngWriter();
}
/**
* Look up the request in the distributed cache and, if found, serve
* the image directly. If it is not found, call {@ink
* generateImage} and insert the results into the cache.
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String cacheKey = getCacheKey(request);
logger.info("Retrieving " + cacheKey + " from cache...");
long start = System.nanoTime();
byte[] image = (byte[]) cache.get(cacheKey);
if (image == null) {
logger.info("Not found, generating...");
image = generateImage(request);
logger.info("Generated. Adding to cache...");
cache.put(cacheKey, image);
logger.info("Added.");
} else {
logger.info("Found.");
}
response.setContentType(imageWriter.getContentType());
response.setDateHeader("Expires", System.currentTimeMillis() + MAX_AGE * 1000);
response.setHeader("Cache-Control", "max-age=" + MAX_AGE + ", public");
response.getOutputStream().write(image);
}
/**
* Extract the tile level and coodinates from {@code request} and
* generate an image that corresponds to the requested tile.
*/
private byte[] generateImage(HttpServletRequest request)
throws IOException, ServletException {
Matcher match = PATH_INFO_PATTERN.matcher(request.getPathInfo());
if (!match.matches()) {
throw new ServletException("Could not match: " + request.getPathInfo());
}
int level = Integer.parseInt(match.group(1));
int tileX = Integer.parseInt(match.group(2));
int tileY = Integer.parseInt(match.group(3));
return imageWriter.generateImage(tileFactory.createTile(level, tileX, tileY));
}
/**
* Extract a key from {@code request} suitable for use in a cache.
* This includes not only the request URI, but also the version
* identifier of the current application.
*/
private String getCacheKey(HttpServletRequest request) {
return request.getRequestURI();
}
/**
* Create a {@link Cache} from the default {@code net.sf.jsr107cache}
* implementation with no custom properties.
*/
private Cache createCache() throws ServletException {
Map<String, Object> props = Collections.emptyMap();
try {
return CacheManager.getInstance().getCacheFactory().createCache(props);
} catch (CacheException ex) {
throw new ServletException("Could not initialize cache:", ex);
}
}
}