Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.apache.myfaces.trinidadinternal.image.cache.FileSystemImageCache Maven / Gradle / Ivy
Go to download
Private implementation of the Apache MyFaces Trinidad project
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.myfaces.trinidadinternal.image.cache;
import java.awt.Color;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.color.CMMException;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.share.io.InputStreamProvider;
import org.apache.myfaces.trinidad.util.ArrayMap;
import org.apache.myfaces.trinidadinternal.agent.TrinidadAgent;
import org.apache.myfaces.trinidadinternal.image.ImageConstants;
import org.apache.myfaces.trinidadinternal.image.ImageContext;
import org.apache.myfaces.trinidadinternal.image.ImageProvider;
import org.apache.myfaces.trinidadinternal.image.ImageProviderRequest;
import org.apache.myfaces.trinidadinternal.image.ImageProviderResponse;
import org.apache.myfaces.trinidadinternal.image.ImageRenderer;
import org.apache.myfaces.trinidadinternal.image.ImageType;
import org.apache.myfaces.trinidadinternal.image.ImageTypeManager;
import org.apache.myfaces.trinidadinternal.image.encode.ImageEncoder;
import org.apache.myfaces.trinidadinternal.image.encode.ImageEncoderManager;
import org.apache.myfaces.trinidadinternal.image.util.FileUtils;
import org.apache.myfaces.trinidadinternal.image.util.MapArea;
import org.apache.myfaces.trinidadinternal.image.xml.ImageProviderRequestUtils;
import org.apache.myfaces.trinidadinternal.share.config.Configuration;
import org.apache.myfaces.trinidadinternal.share.xml.XMLProvider;
import org.apache.myfaces.trinidadinternal.share.xml.XMLUtils;
import org.apache.myfaces.trinidadinternal.style.util.GraphicsUtils;
import org.apache.myfaces.trinidadinternal.util.nls.LocaleUtils;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* FileSystemImageCache is an ImageProvider implementation which caches
* images on the file system. Since the FileSystemImageCache
* are fairly expensive objects to create, FileSystemImageCache instances
* are shared across applications in the same VM. Clients can access
* the shared FileSystemImageCache instance for a particular file system
* cache location via the getSharedCache method.
*
* @see org.apache.myfaces.trinidadinternal.image.ImageProvider
*
* @version $Name: $ ($Revision: adfrt/faces/adf-faces-impl/src/main/java/oracle/adfinternal/view/faces/image/cache/FileSystemImageCache.java#0 $) $Date: 10-nov-2005.19:06:06 $
*/
public class FileSystemImageCache implements ImageProvider, ImageConstants
{
static public final String BLOCK_IMAGE_GENERATION =
"org.apache.myfaces.trinidad.image.BlockImageGeneration";
/**
* Returns a shared cache instance.
* @param realPath The real path of the root directory of the cache. If the
* specified path does not exist and can not be created, and
* IllegalArgumentException is thrown.
* @return Returns an ImageProvider instance which can be used to
* obtain cached images.
*/
public static ImageProvider getSharedCache(
String realPath
)
{
// First, normalize the path to make sure that lookups don't
// fail due to insignificant path incosistencies.
realPath = _getCanonicalPath(realPath);
ImageProvider cache = _sSharedCaches.get(realPath);
// If we didn't find a shared cache, create a new cache
// and cache it in the shared cache cache. :-)
if (cache == null)
{
// Create the new cache
cache = new FileSystemImageCache(realPath);
// Before we save the new cache, make sure another thread hasn't
// already cached a different instance. Synchronize to lock up
// _sSharedCaches.
synchronized (_sSharedCaches)
{
ImageProvider tmp = _sSharedCaches.get(realPath);
if (tmp != null)
{
// Stick with tmp
cache = tmp;
}
else
{
_sSharedCaches.put(realPath, cache);
}
}
}
return cache;
}
/**
* Creates a FileSystemImageCache. Clients should use getSharedCache()
* to obtain FileSystemImageCache instances.
*
* @param realPath The real path of the root directory of the cache. If the
* specified path does not exist and can not be created, and
* IllegalArgumentException is thrown.
*/
protected FileSystemImageCache(String realPath)
{
_realPath = realPath;
// Make sure cache directory exists
File f = new File(_realPath);
if (!f.exists() && !f.mkdirs())
{
throw new IllegalArgumentException(_CACHE_DIRECTORY_ERROR + realPath);
}
// Initialize our table of per-Locale caches. Note on table size:
// We don't expect a single FileSystemImageCache to be serving up images
// for a huge number of different locales - using a small table size.
_caches = new ConcurrentHashMap(19);
}
/**
* Implementation of ImageCache.getImage().
*
* @see org.apache.myfaces.trinidadinternal.image.ImageProvider#getImage
*/
public ImageProviderResponse getImage(
ImageContext context,
ImageProviderRequest request
)
{
// FileSystemImageCache's implementation of ImageProvider.getImage()
// does three things:
//
// 1. Look for the image in the in-memory cache.
// 2. Look for the image in the file-system cache. (It may
// have been generated by another FileSystemImageCache instance
// running in a separate process.)
// 3. Try to generate a new image.
//
// In general, if the image isn't produced by step #3, the request
// is cached as a "miss", so that future lookups for the same request
// will return quickly.
try
{
// Get the ImageType for this request
ImageType type = _getImageType(context, request);
assert (type != null);
// Get the in-memory Cache for this request
Cache cache = _getCache(context, type);
assert (cache != null);
// Convert the ImageProviderRequest into a Cache key.
Object key = null;
// If the ImageProviderRequest object is a CacheKey instance,
// we just use that. Otherwise, we have to use a CacheKeyFactory
// to convert the requested properties into a key
if (request instanceof CacheKey)
key = request;
else
key = _getCacheKey(context, type, request);
assert (key != null);
// Check the in-memory Cache for a matching image
CacheEntry entry = _getImageFromCache(context, cache, key,
request);
// If we've got a match, return it
if (entry != null)
{
if (entry == _MISS_ENTRY)
return null;
if (entry == _RETRY_ENTRY)
{
// _RETRY_ENTRYs are used to indicate that a problem
// occurred when image generation was last attempted,
// possibly due to a network error with the Tecate
// Servlet. Check to see if we should retry again
// now. If so, just keep going - otherwise, the request
// is considered a miss.
if (!_shouldRetry(context))
return null;
}
else
{
return entry;
}
}
// Next, check for a match on the file system. A matching image
// may have been generated by another FileSystemImageCache instance
// running in a different process. We need the request properties
// to do this, in order to produce the target file name.
Map properties = _getFilteredProperties(context, type, request);
// If we don't have any properties, we can't render.
if (properties == null)
{
// Cache the miss.
_putMissEntry(context, type, request, cache, key);
return null;
}
// Now, check the file system
entry = _getImageFromFileSystem(context, type, cache, key, properties);
// If we've got a match, return it
if (entry != null)
{
if (entry == _MISS_ENTRY)
return null;
// If we've got a retry entry at this point, let's retry it!
if (entry != _RETRY_ENTRY)
return entry;
}
// Finally, try generating a new image to satistfy the request
return _generateImage(context,
type,
request,
cache,
key,
properties);
}
catch (CacheException e)
{
_LOG.warning(e);
}
return null;
}
// Loads metadata from file system into a Cache object
private void _loadCache(
ImageContext context,
Cache cache,
boolean localized
) throws CacheException
{
// Check to make sure locale-specific cache directory exists
String localeString = _getLocaleString(context);
// Real path of the root of the locale-specific cache
String realPath = _realPath + File.separatorChar;
if (localized)
realPath += (localeString + File.separatorChar);
File directory = new File(realPath);
if (!directory.exists() && (!directory.mkdir()))
_error(_CACHE_DIRECTORY_ERROR + realPath);
// Create the cache and load metadata from imx files
if (_LOG.isFine())
_LOG.fine("Initializing image cache: " + realPath + " ...");
String files[] = directory.list(IMXFilter.getInstance());
if (files == null)
return;
// We generate the URI for each image in this buffer
long lastLog = 0;
// Read in each metadata file and create a cache entry
for (int i=0; i 5000)
{
if (_LOG.isFine())
_LOG.fine("Loading image " + i + " of " + files.length +
" from image cache: " + realPath);
lastLog = current;
}
String imxName = files[i];
String imxPath = realPath + imxName;
// Create a parser to use for parsing IMX files
XMLProvider parser = _getXMLProvider(context);
assert (parser != null);
try
{
_loadImage(context, cache, new File(imxPath), parser);
}
catch (CacheException e)
{
_LOG.warning(e);
}
}
if (_LOG.isFine())
_LOG.fine("Finished initializing image cache: " + realPath);
}
// Returns a fully loaded cache for the specified Locale
private Cache _getCache(ImageContext context, ImageType type)
throws CacheException
{
boolean localized = _isTypeLocalized(type);
String language = _getLocaleString(context);
Cache cache = null;
if (localized)
cache = _caches.get(language);
else
cache = _globalCache;
if (cache != null)
return cache;
// Create a new cache.
// Note - don't bother synchronizing while cache is being created,
// this may take a while.
cache = new Cache();
if (localized)
{
// Store off our new cache in the caches table
// Make sure some other thread hasn't already loaded
// the same cache
if (!_caches.containsKey(language))
{
// Install the empty Cache object into the _caches table before
// loading so that other threads don't have to wait for the cache
// to finish loading files from the file system. If a second
// thread comes through before the Cache finishes loading, the
// second thread will find any images it needs on the file system
// and load them concurrently.
_caches.put(language, cache);
_loadCache(context, cache, localized);
}
else
{
cache = _caches.get(language);
}
}
else
{
if (_globalCache == null)
{
// Install the empty Cache object before loading so that other
// threads don't have to wait for the cache to finish loading files
// from the file system.
_globalCache = cache;
_loadCache(context, cache, localized);
}
else
{
cache = _globalCache;
}
}
if (cache == null)
{
String message = _CREATE_CACHE_ERROR + _realPath;
if (localized)
message += "/" + language;
_error(message);
}
return cache;
}
// Looks up an image in the in-memory cache
private CacheEntry _getImageFromCache(
ImageContext context,
Cache cache,
Object key,
ImageProviderRequest request
) throws CacheException
{
// Check the Cache
CacheEntry entry = cache.get(context, key);
// If the image hasn't been generated yet, return null
if ((entry == null) || (entry == _MISS_ENTRY) || (entry == _RETRY_ENTRY))
return entry;
// If the image has been generated, make sure it still exists
// For performance reasons, we avoid checking every time.
if (_checkModified(context))
{
long time = System.currentTimeMillis();
if (time > entry.getLastChecked() + _LAST_CHECK_INTERVAL)
{
if (!_imageExists(entry.getImageURI()))
{
cache.remove(context, key, entry);
return null;
}
// Also checked to see if the image is still valid
if (!entry.isValid(context, request))
{
cache.remove(context, key, entry);
// We need to remove the image file too
_removeImageFromFileSystem(entry);
return null;
}
entry.setLastChecked(time);
}
}
return entry;
}
// Loads potential matching images from the file system. If a match
// is found, the corresponding CacheEntry is returned.
private CacheEntry _getImageFromFileSystem(
ImageContext context,
ImageType type,
Cache cache,
Object key,
Map properties
) throws CacheException
{
// We identify potential matches on the file system by checking
// files with names that match the name that we would use when
// saving the image generated by this request. The assumption here
// is that other FileSystemImageCache instances which write to this
// cache directory use the same naming conventions as this instance.
// So, if a matching image has been generated by another
// FileSystemImageCache instance in another process, that image should
// have the same base name as the same image generated by this instance.
// Get the name that we would use for this image
String name = _getFileName(context, type, properties);
assert (name != null);
// We use an XMLProvider to parse the IMX file. However, we don't
// load the XMLProvider until we've actually got something to parse.
XMLProvider parser = null;
// Load all images which match the base name
while (true)
{
// Get the next unique name for this base name
String uniqueName = cache.getUniqueName(name);
// Convert the name into the file system path of the IMX file
String path = _getRealPath(context, type, uniqueName, __IMX_EXTENSION);
File file = new File(path);
// If the IMX file doesn't exist, stop checking for matches
if (!file.exists())
{
// We free up the unique name that we obtained from the Cache
// so that it can be reused.
cache.releaseUniqueName(uniqueName);
return null;
}
// Try loading the image into the in-memory cache
if (parser == null)
{
parser = _getXMLProvider(context);
assert (parser != null);
}
Object newKey = null;
try
{
newKey = _loadImage(context, cache, file, parser);
}
catch (CacheException e)
{
// If we couldn't load the image, just continue on...
_LOG.warning(e);
}
// If we've got a match, return the corresponding entry.
// Otherwise, move on to the next unique name...
if (key.equals(newKey))
return cache.get(context, key);
}
}
// Generates an image and stores it in the cache. Returns the CacheEntry
// for the generated image, or null if the image wasn't generated.
private CacheEntry _generateImage(
ImageContext context,
ImageType type,
ImageProviderRequest request,
Cache cache,
Object key,
Map properties
)
throws CacheException
{
// Regardless of whether renering is servlet-based or local,
// rendering produces a status code and a set of response properties.
// The image is returned in the response properties Map as an
// array of bytes, using the _IMAGE_DATA_KEY property.
// The response properties Map must be large enough to hold:
// 1. WIDTH_RESPONSE_KEY
// 2. HEIGHT_RESPONSE_KEY
// 3. IMAGE_MAP_AREAS_RESPONSE_KEY
ArrayMap responseProperties =
new ArrayMap(3);
byte[] imageData = null;
try
{
// Render the image locally
imageData = _renderImageLocal(context,
type,
properties,
responseProperties);
}
catch (CMMException e)
{
// We have seen seemingly random CMMExceptions generated
// at various points during image rendering. Rather than
// allow these unchecked exceptions to propagate up and
// out of Tecate, let's catch and log these here. That way,
// instead of terminating the page render with a CMMException,
// the page will be allowed to continue rendering, possibly
// with alternate content for images which failed to generate.
_LOG.warning(e);
}
if (imageData == null)
{
// Cache the request as a miss, so we don't check again.
_putMissEntry(context, type, request, cache, key);
return null;
}
// Before we start writing to the file system, we need to make sure
// that the cache directory still exists. We could just do this up
// front, but File.exists() does have some overhead, so we wait until
// we know that we are actually going to have to write.
cache = _checkCacheExists(context, type);
assert (cache != null);
// One more time before we write, make sure the image hasn't shown
// up some how in the meantime...
CacheEntry entry = _getImageFromCache(context, cache, key, request);
if (entry == null)
entry = _getImageFromFileSystem(context, type, cache, key, properties);
if (entry != null)
{
if ((entry != _MISS_ENTRY) && (entry != _RETRY_ENTRY))
return entry;
}
// Get the base name for the generated file
String name = _getFileName(context, type, properties);
assert (name != null);
// Make sure the name is unique
String uniqueName = cache.getUniqueName(name);
assert (uniqueName != null);
// Write the image and the IMX file
File imxFile = _writeImageMetadataFile(context,
type,
uniqueName,
properties,
responseProperties);
// If we didn't generate the IMX file successfully, don't bother
// trying to save the image.
if (imxFile == null)
return null;
File imageFile = _writeImageFile(context,
type,
uniqueName,
imageData,
properties);
// If the write didn't succeed, blow away the IMX file and
// return null. We'll try again next time...
if (imageFile == null)
{
imxFile.delete();
return null;
}
// Put the succesful cache entry in the in-memory cache and return it.
return _putCachedImage(context,
type,
request,
cache,
key,
uniqueName,
properties,
responseProperties);
}
// Loads an image from the file system into the cache. Returns the
// key used to cache the image, or null if the image could not be
// loaded. Returns the key used to cache the image, or null if image
// couldn't be loaded.
private Object _loadImage(
ImageContext context,
Cache cache,
File imxFile,
XMLProvider parser
)
throws CacheException
{
String imxPath = imxFile.getPath();
String imxName = imxFile.getName();
// Get an InputStream for the IMX file
InputStream in = null;
try
{
in = new FileInputStream(imxPath);
}
catch (FileNotFoundException fnfe)
{
_error (_METADATA_FILE_ERROR, fnfe);
}
// If we made it this far, we should have an InputStream
assert (in != null);
// Wrap up the stream in a UTF8 Reader
Reader reader = _getUTF8Reader(in);
// Wrap up the Reader in an InputSource
InputSource source = new InputSource(reader);
source.setSystemId(imxPath);
ImageProviderRequest request = null;
try
{
request =
ImageProviderRequestUtils.createImageProviderRequest(context,
parser,
source);
}
catch (SAXException e)
{
_error(_XML_DECODING_ERROR + imxPath, e);
}
catch (IOException e)
{
_error(_XML_DECODING_ERROR + imxPath, e);
}
finally
{
try { in.close(); } catch (IOException e) { _error(e); }
}
if (request == null)
return null;
// Get the ImageType from the request
ImageType type = _getImageType(context, request);
assert (type != null);
// Get the properties from the request
Map properties = request.getRenderProperties(context);
assert (properties != null);
// Get the base name for the URI (minus the extension)
int dotIndex = imxName.lastIndexOf('.');
String baseName = (dotIndex == -1) ?
imxName : imxName.substring(0, dotIndex);
// Generate the new URI
StringBuffer uriBuffer = new StringBuffer(imxName.length() + 3);
if (_isTypeLocalized(type))
{
uriBuffer.append(_getLocaleString(context));
uriBuffer.append(_URI_DELIMITER);
}
uriBuffer.append(baseName);
// Replace ".imx" with our image extension
String extension = _getImageEncodingExtension(context, properties);
assert (extension != null);
uriBuffer.append(extension);
String uri = uriBuffer.toString();
// Make sure the required files exist - if not don't add it to cache
if (!_imageExists(uri))
return null;
// Create the cache key
CacheKeyFactory keyFactory = _getCacheKeyFactory(type);
Object key = keyFactory.getCacheKey(context, properties);
// Create the cache entry
CacheEntry entry = _createCacheEntry(context, type, uri, properties);
// Cache it
cache.put(context, key, entry);
return key;
}
// Stores the specified image in the cache
private CacheEntry _putCachedImage(
ImageContext context,
ImageType type,
ImageProviderRequest request,
Cache cache,
Object key,
String name,
Map properties,
Map responseProperties
) throws CacheException
{
// Make sure that we've got a new CacheKey. We wouldn't want to
// use a CacheKey provided by the client - as the client might be
// reusing CacheKey instances.
if (request == key)
{
key = _getCacheKey(context, type, request);
assert (key != null);
}
// Generate the URI for this image
boolean localized = _isTypeLocalized(type);
String language = _getLocaleString(context);
assert (language != null);
String extension = _getImageEncodingExtension(context, properties);
assert (extension != null);
int length = name.length() + extension.length();
if (localized)
length += language.length();
StringBuffer buffer = new StringBuffer(length);
if (localized)
{
buffer.append(language);
buffer.append("/");
}
buffer.append(name);
buffer.append(extension);
String uri = buffer.toString();
// Create the cache entry
CacheEntry entry = _createCacheEntry(context, type, uri,
responseProperties);
cache.put(context, key, entry);
return entry;
}
// Caches a miss
private void _putMissEntry(
ImageContext context,
ImageType type,
ImageProviderRequest request,
Cache cache,
Object key
) throws CacheException
{
// Make sure that we've got a new CacheKey. We wouldn't want to
// use a CacheKey provided by the client - as the client might be
// reusing CacheKey instances.
if (request == key)
{
key = _getCacheKey(context, type, request);
assert (key != null);
}
cache.put(context, key, _MISS_ENTRY);
}
// Removes the image represented by the CacheEntry from
// the file system cache
private void _removeImageFromFileSystem(
CacheEntry entry
)
{
String uri = entry.getImageURI();
String imagePath = _getRealPath(uri);
File imageFile = new File(imagePath);
imageFile.delete();
// Also remove the IMX file
String encoding = entry.getEncoding();
ImageEncoderManager manager =
ImageEncoderManager.getDefaultImageEncoderManager();
String extension = manager.getImageExtension(encoding);
// Build up the path for the IMX file
String imxPath = imagePath.substring(0,
imagePath.length() - extension.length());
File imxFile = new File(imxPath + __IMX_EXTENSION);
imxFile.delete();
}
// Render the requested image locally.
// Returns the image data as a byte array.
private byte[] _renderImageLocal(
ImageContext context,
ImageType type,
Map properties,
Map responseProperties
) throws CacheException
{
Configuration config = context.getConfiguration();
// See if we don't want any image generation (which is basically
// just for the renderkit test). Always generate a width and a height,
// and generate an area map if needed
if (Boolean.TRUE.equals(config.getProperty(BLOCK_IMAGE_GENERATION)))
{
responseProperties.put(WIDTH_RESPONSE_KEY, _TEST_WIDTH);
responseProperties.put(HEIGHT_RESPONSE_KEY, _TEST_HEIGHT);
Object o = properties.get(TABS_KEY);
if (o != null)
{
int length = ((Object[]) o).length;
MapArea[] areas = new MapArea[length];
for (int i = 0; i < length; i++)
{
areas[i] = new MapArea(new Rectangle(i, 0, 1, 1));
}
responseProperties.put(IMAGE_MAP_AREAS_RESPONSE_KEY, areas);
}
return new byte[0];
}
// Before we do any rendering check for Configuration.HEADLESS
if (Boolean.TRUE.equals(config.getProperty(Configuration.HEADLESS)) ||
!GraphicsUtils.isGraphicalEnvironment())
{
// We're special casing colorized icons so that we can generate
// them even if we don't have an X server
if (TECATE_NAMESPACE.equals(type.getNamespaceURI()) &&
COLORIZED_ICON_NAME.equals(type.getLocalName()))
{
return _readColorizedIconData(context,
properties,
responseProperties);
}
return null;
}
// Get the ImageRenderer to use for the requested type
ImageRenderer renderer = _getImageRenderer(type);
assert (renderer != null);
// Render the image
Image image = renderer.renderImage(context,
properties,
responseProperties);
// If we didn't get an Image, we're done.
if (image == null)
return null;
// Convert the generated image to a byte array
ByteArrayOutputStream out = new ByteArrayOutputStream();
try
{
ImageEncoder encoder = _getImageEncoder(context, properties);
assert (encoder != null);
encoder.encodeImage(image, out);
}
catch (IOException e)
{
_error(e);
}
finally
{
image.flush();
}
return out.toByteArray();
}
// Read in image data from an InputStream
private byte[] _readImageData(
InputStream in
) throws IOException, CacheException
{
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = (in.read(buffer))) >= 0)
out.write(buffer, 0, length);
return out.toByteArray();
}
// Reads in source icon data from the specified InputStreamProvider
private byte[] _readSourceIconData(
InputStreamProvider provider
) throws IOException, CacheException
{
InputStream in = null;
try
{
in = provider.openInputStream();
}
catch (IOException e)
{
// If we are unable to open the InputStream at all,
// consider it a miss.
return null;
}
byte[] iconData = null;
try
{
iconData = _readImageData(in);
}
finally
{
in.close();
}
return iconData;
}
// Tests whether it is time to retry generating the image
// for a particular request
private boolean _shouldRetry(ImageContext context)
{
// Back when we supported an external URL for generation,
// this might return false
return true;
}
// Writes an image data to the file system
private File _writeImageFile(
ImageContext context,
ImageType type,
String name,
byte[] data,
Map properties
) throws CacheException
{
// Derive the full path of the image file
String extension = _getImageEncodingExtension(context, properties);
String path = _getRealPath(context, type, name, extension);
File file = new File(path);
try
{
OutputStream out = new FileOutputStream(file);
out.write(data);
out.close();
}
catch (IOException e)
{
_error(e);
}
return file;
}
// Writes an image map to the file system
private File _writeImageMetadataFile(
ImageContext context,
ImageType type,
String name,
Map properties,
Map responseProperties
)
throws CacheException
{
// Derive the full path of the IMX file
String path = _getRealPath(context, type, name, __IMX_EXTENSION);
File file = new File(path);
PrintWriter writer = null;
try
{
// One last time... Check to see if the File already exists.
// It may have been created by another process. If the file doesn't
// exist, we attempt to create it by calling File.createNewFile(),
// which should guarantee us that we are creating a unique file.
if (file.exists() || !file.createNewFile())
{
// At this point, we just give up until next time.
return null;
}
writer = new PrintWriter(FileUtils.getUTF8Writer(path));
}
catch (IOException e)
{
_error(e);
}
try
{
_writeImageProviderRequest(context,
type,
properties,
responseProperties,
writer);
}
finally
{
writer.flush();
writer.close();
// Check for errors
if (writer.checkError())
{
if (file.exists())
file.delete();
_error(_XML_ENCODING_ERROR + path);
}
}
return file;
}
// Writes out the ImageProviderRequest request
private void _writeImageProviderRequest(
ImageContext context,
ImageType type,
Map properties,
Map responseProperties,
PrintWriter writer
) throws CacheException
{
// Write out the XML declaration - we always use UTF-8
writer.println("");
try
{
ImageProviderRequestUtils.encodeImageProviderRequest(context,
type.getNamespaceURI(),
type.getLocalName(),
type,
properties,
responseProperties,
writer);
}
catch (IllegalArgumentException e)
{
throw new CacheException(e);
}
}
//
// Everything from here on out is utility methods used to
// simplify the above code
//
// Checks whether the cache still exists. Called before writing to
// avoid writing to a cache directory which has been deleted. If the
// cache directory has been deleted, it is re-created and a new, empty
// Cache obect is returned.
private Cache _checkCacheExists(
ImageContext context,
ImageType type
) throws CacheException
{
boolean localized = _isTypeLocalized(type);
String language = _getLocaleString(context);
String directoryPath = _getRealPath(localized ? language : "");
File directory = new File(directoryPath);
if (!directory.exists())
{
// If the directory no longer exists, dump the corresponding
// Cache instance, if it exists and create a new directory.
if (localized)
{
synchronized (_caches)
{
if (_caches.containsKey(language))
{
_caches.remove(language);
}
// While we are at it, check to see if the global cache
// directory exists - and if not, blow away the global
// Cache. We do this, since the call to mkdirs() will
// recreate both the locale-specific cache directory, as
// well as the global cache directory. If we didn't blow
// away the global cache, we'll get an error logged for every
// missing file in the global cache.
File parent = directory.getParentFile();
if ((parent != null) && (!parent.exists()))
{
synchronized (this)
{
_globalCache = null;
}
}
}
}
else
{
synchronized (this)
{
_globalCache = null;
}
}
// Try creating the directory now. Subsequent calls to _getCache()
// will create the new Cache object.
directory.mkdirs();
}
// Generate a name for the image
return _getCache(context, type);
}
// Tests whether or not to check for modifications
private boolean _checkModified(ImageContext context)
{
// check configuration once and cache whether or not
// to check if the files exist
if (_configNotChecked)
{
_checkModified = true;
_configNotChecked = false;
}
return _checkModified;
}
// Creates a CacheEntry with the specified uri and properties
private CacheEntry _createCacheEntry(
ImageContext context,
ImageType type,
String uri,
Map properties)
{
// Get the cache entry
int width = _getIntSize(properties, WIDTH_RESPONSE_KEY);
int height = _getIntSize(properties, HEIGHT_RESPONSE_KEY);
MapArea[] areas = (MapArea[])properties.get(IMAGE_MAP_AREAS_RESPONSE_KEY);
String encoding = _getImageEncoding(context, properties);
if (areas == null)
{
Object checkSource = type.getProperty(ImageType.CHECK_SOURCE_PROPERTY);
if (Boolean.TRUE.equals(checkSource))
return new SourceCheckingCacheEntry(uri, width, height, encoding);
return new CacheEntry(uri, width, height, encoding);
}
return new MapCacheEntry(uri, width, height, areas, encoding);
}
// Throws a CacheException with a message
private void _error(String message)
throws CacheException
{
throw new CacheException(message);
}
// Throws a CacheException with a Throwable
private void _error(Throwable t)
throws CacheException
{
throw new CacheException(t);
}
// Throws a CacheException with a message and Throwable
private void _error(String message, Throwable t)
throws CacheException
{
throw new CacheException(message, t);
}
// Returns the key for the request
private Object _getCacheKey(
ImageContext context,
ImageType type,
ImageProviderRequest request
)
throws CacheException
{
// Get a key based on the requested properties
CacheKeyFactory keyFactory = _getCacheKeyFactory(type);
assert (keyFactory != null);
Object key = keyFactory.getCacheKey(context,
request.getRenderProperties(context));
if (key == null)
_error(_CACHE_KEY_ERROR + type);
return key;
}
// Returns the CacheKeyFactory for the specified type
private CacheKeyFactory _getCacheKeyFactory(ImageType type)
throws CacheException
{
CacheKeyFactory keyFactory = (CacheKeyFactory)
type.getProperty(CacheKeyFactory.CACHE_KEY_FACTORY_PROPERTY);
if (keyFactory == null)
_error(_CACHE_KEY_FACTORY_ERROR + type);
return keyFactory;
}
// Utility method for getting canonical paths. File.getCanonicalPath()
// can be slow, so we cache canonical paths to avoid calling
// getCanonicalPath() each time we need to get a shared FileSystemImageCache
// instance.
private static String _getCanonicalPath(String path)
{
String canonicalPath = _sCanonicalPaths.get(path);
if (canonicalPath != null)
return canonicalPath;
File file = new File(path);
try
{
canonicalPath = file.getCanonicalPath();
}
catch (IOException e)
{
throw new IllegalArgumentException(_CACHE_PATH_ERROR + path);
}
if (canonicalPath != null)
_sCanonicalPaths.put(path, canonicalPath);
return canonicalPath;
}
// Gets the base file name for the requested image
private String _getFileName(
ImageContext context,
ImageType type,
Map properties
)
throws CacheException
{
// First, get the base name for the requested image
NameProvider nameProvider = (NameProvider)
type.getProperty(NameProvider.NAME_PROVIDER_PROPERTY);
if (nameProvider == null)
_error(_NAME_PROVIDER_ERROR + type);
String name = nameProvider.getName(context, properties);
if (name == null)
_error(_NAME_PROVIDING_ERROR + type);
return name;
}
// Returns the complete set of properties needed to render/encode
// the requested image
private Map _getFilteredProperties(
ImageContext context,
ImageType type,
ImageProviderRequest request
)
{
Map properties = request.getRenderProperties(context);
if (properties == null)
return null;
// Make sure the encoding is specified
if (properties.get(ENCODING_TYPE_KEY) == null)
{
String encoding = _getImageEncoding(context, properties);
properties.put(ENCODING_TYPE_KEY, encoding);
}
PropertiesFilter filter = (PropertiesFilter)
type.getProperty(PropertiesFilter.PROPERTIES_FILTER_PROPERTY);
if (filter != null)
return filter.filterProperties(context, properties);
return properties;
}
// Returns the ImageEncoder to use for the requested image
private ImageEncoder _getImageEncoder(
ImageContext context,
Map properties
) throws CacheException
{
String encoding = _getImageEncoding(context, properties);
// _getImageEncoding() always returns non-null encoding
assert (encoding != null);
ImageEncoderManager manager =
ImageEncoderManager.getDefaultImageEncoderManager();
ImageEncoder encoder = manager.getImageEncoder(encoding);
if (encoder == null)
_error(_IMAGE_ENCODER_ERROR + encoding);
return encoder;
}
// Returns the encoding to use for images generated
// using the specified context
private String _getImageEncoding(
ImageContext context,
Map properties)
{
String encoding = (String)properties.get(ENCODING_TYPE_KEY);
if (encoding != null)
return encoding;
// Pick an encoding based on the Configuration/Agent
Configuration config = context.getConfiguration();
TrinidadAgent agent = context.getAgent();
//encodings was part of old Agent capabilities, not in new Agent Caps. So replaced
//Object encodingsObj = agent.getCapability(AdfFacesAgent.CAP_IMAGE_ENCODINGS);
//int encodings = ((encodingsObj == null)
// ? (AdfFacesAgent.IMAGE_ENCODINGS_CAP_GIF |
// AdfFacesAgent.IMAGE_ENCODINGS_CAP_PNG)
// : ((Number) encodingsObj).intValue());
// If GIF is enabled and supported, use it
//if (((encodings & AdfFacesAgent.IMAGE_ENCODINGS_CAP_GIF) != 0) &&
// !Boolean.FALSE.equals(config.getProperty(_GIF_ENABLED)))
//{
// return ImageEncoderManager.GIF_TYPE;
//}
if ((agent.getCapability(TrinidadAgent.CAP_GIF_TYPE_IMAGE) == Boolean.TRUE) &&
!Boolean.FALSE.equals(config.getProperty(_GIF_ENABLED)))
{
return ImageEncoderManager.GIF_TYPE;
}
//encodings was part of old Agent capabilities, not in new Agent Caps. So replaced
//if ((encodings & AdfFacesAgent.IMAGE_ENCODINGS_CAP_PNG) != 0)
//{
// return ImageEncoderManager.PNG_TYPE;
//}
if (agent.getCapability(TrinidadAgent.CAP_PNG_TYPE_IMAGE) == Boolean.TRUE)
{
return ImageEncoderManager.PNG_TYPE;
}
// There must be some supported encoding!
assert false;
// What else can we do?
return ImageEncoderManager.PNG_TYPE;
}
// Returns the extension for the current image encoding
private String _getImageEncodingExtension(
ImageContext context,
Map properties
) throws CacheException
{
String encoding = _getImageEncoding(context, properties);
// _getImageEncoding() always returns non-null encoding
assert (encoding != null);
ImageEncoderManager manager =
ImageEncoderManager.getDefaultImageEncoderManager();
String extension = manager.getImageExtension(encoding);
if (extension == null)
_error(_IMAGE_ENCODING_EXTENSION_ERROR + encoding);
return extension;
}
// Returns the ImageRenderer for the specified type
private ImageRenderer _getImageRenderer(
ImageType type
)
throws CacheException
{
ImageRenderer renderer = (ImageRenderer)
type.getProperty(ImageType.IMAGE_RENDERER_PROPERTY);
if (renderer == null)
_error(_IMAGE_RENDERER_ERROR + type);
return renderer;
}
// Gets an ImageType
private ImageType _getImageType(
ImageContext context,
ImageProviderRequest request
)
throws CacheException
{
String namespace = request.getNamespaceURI();
String name = request.getLocalName();
ImageTypeManager manager = CacheUtils.getImageTypeManager(context);
assert (manager != null);
ImageType type = manager.getImageType(namespace, name);
if (type == null)
_error(_IMAGE_TYPE_ERROR + namespace + ", " + name);
return type;
}
// Returns an int from the specified Map
private int _getIntSize(
Map properties,
Object key)
{
Integer value = (Integer)properties.get(key);
if (value == null)
return ImageProviderResponse.UNKNOWN_SIZE;
return value.intValue();
}
// Returns string for the locale specified by the ImageContext
private String _getLocaleString(ImageContext context)
{
Locale locale = context.getLocaleContext().getTranslationLocale();
String language = locale.getLanguage();
// Chinese is a special case. We keep Simplified Chinese and
// Traditional Chinese images in separate directories in order
// to avoid collisions when the "Dialog" font is used. In general,
// this shouldn't be a problem - as locale-specific Albany fonts should
// be used in the future - which should be sufficient to distinguish
// between Simplified/Traditional Chinese images in the same directory.
if (!_CHINESE_LANGUAGE.equals(language))
return language;
return _isSimplifiedChinese(locale.getCountry()) ?
_SIMPLIFIED_CHINESE_DIRECTORY :
_TRADITIONAL_CHINESE_DIRECTORY;
}
// Returns the real path for the specified uri
private String _getRealPath(String uri)
{
int length = _realPath.length() + uri.length() + 1;
StringBuffer buffer = new StringBuffer(length);
buffer.append(_realPath);
buffer.append(File.separatorChar);
buffer.append(uri);
return buffer.toString();
}
// Returns the real path for the specified name
private String _getRealPath(
ImageContext context,
ImageType type,
String name,
String extension)
{
boolean localized = _isTypeLocalized(type);
String language = _getLocaleString(context);
assert (language != null);
int length = _realPath.length() + name.length() + 1;
if (localized)
length += (language.length() + 1);
if (extension != null)
length += extension.length();
StringBuffer buffer = new StringBuffer(length + 1);
buffer.append(_realPath);
buffer.append(File.separatorChar);
// Append the language if it is specified
if (localized)
{
buffer.append(language);
buffer.append(File.separatorChar);
}
buffer.append(name);
if (extension != null)
buffer.append(extension);
return buffer.toString();
}
// Converts an InputStream into a UTF8 Reader
private Reader _getUTF8Reader(InputStream in)
throws CacheException
{
Reader reader = null;
try
{
reader = new BufferedReader(new InputStreamReader(in, _UTF8));
}
catch (UnsupportedEncodingException e)
{
_error(e);
}
// Reader should never be null at this point
assert (reader != null);
return reader;
}
// Returns an XMLProvider to use for parsing XML documents
private XMLProvider _getXMLProvider(ImageContext context)
throws CacheException
{
// Create a parser to use for parsing IMX files
Configuration config = context.getConfiguration();
XMLProvider parser = XMLUtils.getXMLProvider(config);
if (parser == null)
_error(_XML_PROVIDER_ERROR);
return parser;
}
// Utility method to test whether the image file corresponding to
// the specified uri actually exists
private boolean _imageExists(String uri)
{
// First, check to see if the image exists
String path = _getRealPath(uri);
File file = new File(path);
return file.exists();
}
// Tests whether the specified Chinese region uses Simplified glyphs
private static boolean _isSimplifiedChinese(String region)
{
// Right now, the only Chinese locale that uses Simplified
// characters is zh_CN.
return ("CN".equals(region));
}
// Tests whether the image type is localized
private boolean _isTypeLocalized(ImageType type)
{
Object localized = type.getProperty(ImageType.LOCALIZED_PROPERTY);
return !Boolean.FALSE.equals(localized);
}
// Reads colorized icon image data from the request properties
private byte[] _readColorizedIconData(
ImageContext context,
Map properties,
Map responseProperties
) throws CacheException
{
// Only bother doing this if we're generating GIFs
Object encoding = _getImageEncoding(context, properties);
if (!ImageEncoderManager.GIF_TYPE.equals(encoding))
return null;
// Only bother doing this if we're in LTR
if (CacheUtils.getReadingDirection(context, properties) !=
LocaleUtils.DIRECTION_LEFTTORIGHT)
{
// We can't use the source icon because it needs
// to be flipped to RTL
return null;
}
// Only bother doing this if we're using the default colors
Color darkColor = (Color)properties.get(DARK_COLOR_KEY);
if ((darkColor != null) &&
((darkColor.getRGB() & 0x00ffffff) != 0x00336699))
{
return null;
}
Color darkAccentColor = (Color)properties.get(DARK_ACCENT_COLOR_KEY);
if ((darkAccentColor != null) &&
((darkAccentColor.getRGB() & 0x00ffffff) != 0x00d2d8b0))
{
return null;
}
InputStreamProvider provider = (InputStreamProvider)properties.get(
SOURCE_INPUT_STREAM_PROVIDER_KEY);
if (provider != null)
{
byte[] data = null;;
try
{
data = _readSourceIconData(provider);
}
catch (IOException e)
{
_error(e);
}
if (data != null)
{
// Make sure we've got a GIF
if (data[0] == 'G' && (data[1] == 'I') && (data[2] == 'F'))
{
// We've got a GIF - get the width/height
int width = (data[6] | (data[7] << 8));
int height = (data[8] | (data[9] << 8));
responseProperties.put(WIDTH_RESPONSE_KEY, width);
responseProperties.put(HEIGHT_RESPONSE_KEY, height);
return data;
}
}
}
return null;
}
// FilenameFilter for .imx files
private static class IMXFilter implements FilenameFilter
{
private IMXFilter() {}
public static FilenameFilter getInstance()
{
if (_sInstance == null)
_sInstance = new IMXFilter();
return _sInstance;
}
public boolean accept(File dir, String name)
{
return name.endsWith(__IMX_EXTENSION);
}
private static IMXFilter _sInstance;
}
// Cache root directory
private String _realPath;
// Hashtable containing caches (ie. Hashtables) hashed by Locale.
// Each cache stores mappings from PropertiesKeys to the "base name"
// of the image/metadata/map.
private ConcurrentHashMap _caches;
// The locale-independent cache
private Cache _globalCache;
// Table of shared FileSystemImageCaches, hashed by path.
// Note on table size: We don't expect to have very many FileSystemCaches
// running in a single VM - table can be small.
// -= Simon Lessard =-
// FIXME: The code already take care of synchronization when needed it seems,
// using a HashMap should be more performant.
private static final Hashtable _sSharedCaches =
new Hashtable(19);
// Extension for IMX files
static final String __IMX_EXTENSION = ".imx";
private static final String _URI_DELIMITER = "/";
// The default time interval for which we wait before re-checking whether an
// cache entry's image exists.
private static final long _LAST_CHECK_INTERVAL = 10000;
// Whether the config was checked already
private boolean _configNotChecked = true;
// Whether to check if files exist
private boolean _checkModified = true;
// The CacheEntry used to represent cached misses.
private static final CacheEntry _MISS_ENTRY = new CacheEntry(null, -1, -1);
// The CacheEntry used for misses which should be retried
private static final CacheEntry _RETRY_ENTRY = new CacheEntry(null, -1, -1);
// Error messages
private static final String _CACHE_PATH_ERROR =
"Could not get canonical path for image cache directory ";
private static final String _CACHE_DIRECTORY_ERROR =
"Could not create image cache directory ";
private static final String _XML_ENCODING_ERROR =
"Error while generating image metadata file ";
private static final String _CREATE_CACHE_ERROR =
"Could not create image cache for ";
private static final String _XML_DECODING_ERROR =
"Could not decode image metadata from ";
private static final String _CACHE_KEY_ERROR =
"Could not create cache key for image type ";
private static final String _CACHE_KEY_FACTORY_ERROR =
"No CacheKeyFactory registered for image type ";
private static final String _METADATA_FILE_ERROR =
"Could not locate metadata file ";
private static final String _IMAGE_TYPE_ERROR =
"No image type registered for ";
private static final String _IMAGE_RENDERER_ERROR =
"No ImageRenderer registered for image type ";
private static final String _NAME_PROVIDER_ERROR =
"No NameProvider registered for image type ";
private static final String _NAME_PROVIDING_ERROR =
"Could not get image file name name for image type ";
private static final String _IMAGE_ENCODER_ERROR =
"No ImageEncoder registered for image encoding type ";
private static final String _IMAGE_ENCODING_EXTENSION_ERROR =
"No file extension registered for image encoding type ";
private static final String _XML_PROVIDER_ERROR =
"Could not load XMLProvider";
// UTF8 encoding name
private static String _UTF8 = "UTF8";
// Some constants related to special handling for Chinese language.
// Chinese language images are placed in separate directories depending
// on whether the region uses Simplified or Traditional Chinese
private static final String _CHINESE_LANGUAGE = "zh";
private static final String _SIMPLIFIED_CHINESE_DIRECTORY = "zhs";
private static final String _TRADITIONAL_CHINESE_DIRECTORY = "zht";
private static final Integer _TEST_WIDTH = Integer.valueOf(23);
private static final Integer _TEST_HEIGHT = Integer.valueOf(32);
// Configuration property to test whether GIF support is enabled.
private static final String _GIF_ENABLED = "gifEnabled";
// -= Simon Lessard =-
// FIXME: Hashtable is synchronized and more than often inefficient
// use a HashMap instead
private static final Hashtable _sCanonicalPaths =
new Hashtable(19);
private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(FileSystemImageCache.class);
}