blob: 76ef22300593da997a16f238261cc83f1b3dc6e7 [file] [log] [blame]
/*
* 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.
*/
/* $Id$ */
package org.apache.fop.fonts;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.util.LogUtil;
/**
* Fop cache (currently only used for font info caching)
*/
public final class FontCache implements Serializable {
/**
* Serialization Version UID. Change this value if you want to make sure the
* user's cache file is purged after an update.
*/
private static final long serialVersionUID = 9129238336422194339L;
/** logging instance */
private static Log log = LogFactory.getLog(FontCache.class);
/** FOP's user directory name */
private static final String FOP_USER_DIR = ".fop";
/** font cache file path */
private static final String DEFAULT_CACHE_FILENAME = "fop-fonts.cache";
/** has this cache been changed since it was last read? */
private transient boolean changed;
/** change lock */
private final boolean[] changeLock = new boolean[1];
/**
* master mapping of font url -> font info. This needs to be a list, since a
* TTC file may contain more than 1 font.
* @serial
*/
private Map<String, CachedFontFile> fontfileMap;
/**
* mapping of font url -&gt; file modified date (for all fonts that have failed
* to load)
* @serial
*/
private Map<String, Long> failedFontMap;
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
}
private static File getUserHome() {
return toDirectory(System.getProperty("user.home"));
}
private static File getTempDirectory() {
return toDirectory(System.getProperty("java.io.tmpdir"));
}
private static File toDirectory(String path) {
if (path != null) {
File dir = new File(path);
if (dir.exists()) {
return dir;
}
}
return null;
}
/**
* Returns the default font cache file.
*
* @param forWriting
* true if the user directory should be created
* @return the default font cache file
*/
public static File getDefaultCacheFile(boolean forWriting) {
File userHome = getUserHome();
if (userHome != null) {
File fopUserDir = new File(userHome, FOP_USER_DIR);
if (forWriting) {
boolean writable = fopUserDir.canWrite();
if (!fopUserDir.exists()) {
writable = fopUserDir.mkdir();
}
if (!writable) {
userHome = getTempDirectory();
fopUserDir = new File(userHome, FOP_USER_DIR);
fopUserDir.mkdir();
}
}
return new File(fopUserDir, DEFAULT_CACHE_FILENAME);
}
return new File(FOP_USER_DIR);
}
/**
* Reads the default font cache file and returns its contents.
*
* @return the font cache deserialized from the file (or null if no cache
* file exists or if it could not be read)
* @deprecated use {@link #loadFrom(File)} instead
*/
public static FontCache load() {
return loadFrom(getDefaultCacheFile(false));
}
/**
* Reads a font cache file and returns its contents.
*
* @param cacheFile
* the cache file
* @return the font cache deserialized from the file (or null if no cache
* file exists or if it could not be read)
*/
public static FontCache loadFrom(File cacheFile) {
if (cacheFile.exists()) {
try {
if (log.isTraceEnabled()) {
log.trace("Loading font cache from "
+ cacheFile.getCanonicalPath());
}
InputStream in = new BufferedInputStream(new FileInputStream(cacheFile));
ObjectInputStream oin = new ObjectInputStream(in);
try {
return (FontCache) oin.readObject();
} finally {
IOUtils.closeQuietly(oin);
}
} catch (ClassNotFoundException e) {
// We don't really care about the exception since it's just a
// cache file
log.warn("Could not read font cache. Discarding font cache file. Reason: "
+ e.getMessage());
} catch (IOException ioe) {
// We don't really care about the exception since it's just a
// cache file
log.warn("I/O exception while reading font cache ("
+ ioe.getMessage() + "). Discarding font cache file.");
try {
cacheFile.delete();
} catch (SecurityException ex) {
log.warn("Failed to delete font cache file: "
+ cacheFile.getAbsolutePath());
}
}
}
return null;
}
/**
* Writes the font cache to disk.
*
* @throws FOPException fop exception
* @deprecated use {@link #saveTo(File)} instead
*/
public void save() throws FOPException {
saveTo(getDefaultCacheFile(true));
}
/**
* Writes the font cache to disk.
*
* @param cacheFile
* the file to write to
* @throws FOPException
* fop exception
*/
public void saveTo(File cacheFile) throws FOPException {
synchronized (changeLock) {
if (changed) {
try {
log.trace("Writing font cache to " + cacheFile.getCanonicalPath());
OutputStream out = new java.io.FileOutputStream(cacheFile);
out = new java.io.BufferedOutputStream(out);
ObjectOutputStream oout = new ObjectOutputStream(out);
try {
oout.writeObject(this);
} finally {
IOUtils.closeQuietly(oout);
}
} catch (IOException ioe) {
LogUtil.handleException(log, ioe, true);
}
changed = false;
log.trace("Cache file written.");
}
}
}
/**
* creates a key given a font info for the font mapping
*
* @param fontInfo
* font info
* @return font cache key
*/
protected static String getCacheKey(EmbedFontInfo fontInfo) {
if (fontInfo != null) {
URI embedFile = fontInfo.getEmbedURI();
URI metricsFile = fontInfo.getMetricsURI();
return (embedFile != null) ? embedFile.toASCIIString() : metricsFile.toASCIIString();
}
return null;
}
/**
* cache has been updated since it was read
*
* @return if this cache has changed
*/
public boolean hasChanged() {
return this.changed;
}
/**
* is this font in the cache?
*
* @param embedUrl
* font info
* @return boolean
*/
public boolean containsFont(String embedUrl) {
return (embedUrl != null && getFontFileMap().containsKey(embedUrl));
}
/**
* is this font info in the cache?
*
* @param fontInfo
* font info
* @return font
*/
public boolean containsFont(EmbedFontInfo fontInfo) {
return (fontInfo != null && getFontFileMap().containsKey(
getCacheKey(fontInfo)));
}
/**
* Tries to identify a File instance from an array of URLs. If there's no
* file URL in the array, the method returns null.
*
* @param urls
* array of possible font urls
* @return file font file
*/
public static File getFileFromUrls(String[] urls) {
for (String urlStr : urls) {
if (urlStr != null) {
File fontFile = null;
if (urlStr.startsWith("file:")) {
try {
URL url = new URL(urlStr);
fontFile = FileUtils.toFile(url);
} catch (MalformedURLException mfue) {
// do nothing
}
}
if (fontFile == null) {
fontFile = new File(urlStr);
}
if (fontFile.exists() && fontFile.canRead()) {
return fontFile;
}
}
}
return null;
}
private Map<String, CachedFontFile> getFontFileMap() {
if (fontfileMap == null) {
fontfileMap = new HashMap<String, CachedFontFile>();
}
return fontfileMap;
}
/**
* Adds a font info to cache
*
* @param fontInfo
* font info
*/
public void addFont(EmbedFontInfo fontInfo, InternalResourceResolver resourceResolver) {
String cacheKey = getCacheKey(fontInfo);
synchronized (changeLock) {
CachedFontFile cachedFontFile;
if (containsFont(cacheKey)) {
cachedFontFile = getFontFileMap().get(cacheKey);
if (!cachedFontFile.containsFont(fontInfo)) {
cachedFontFile.put(fontInfo);
}
} else {
// try and determine modified date
URI fontUri = resourceResolver.resolveFromBase(fontInfo.getEmbedURI());
long lastModified = getLastModified(fontUri);
cachedFontFile = new CachedFontFile(lastModified);
if (log.isTraceEnabled()) {
log.trace("Font added to cache: " + cacheKey);
}
cachedFontFile.put(fontInfo);
getFontFileMap().put(cacheKey, cachedFontFile);
changed = true;
}
}
}
/**
* Returns a font from the cache.
*
* @param embedUrl
* font info
* @return CachedFontFile object
*/
public CachedFontFile getFontFile(String embedUrl) {
return containsFont(embedUrl) ? getFontFileMap().get(embedUrl) : null;
}
/**
* Returns the EmbedFontInfo instances belonging to a font file. If the font
* file was modified since it was cached the entry is removed and null is
* returned.
*
* @param embedUrl
* the font URL
* @param lastModified
* the last modified date/time of the font file
* @return the EmbedFontInfo instances or null if there's no cached entry or
* if it is outdated
*/
public EmbedFontInfo[] getFontInfos(String embedUrl, long lastModified) {
CachedFontFile cff = getFontFile(embedUrl);
if (cff.lastModified() == lastModified) {
return cff.getEmbedFontInfos();
} else {
removeFont(embedUrl);
return null;
}
}
/**
* removes font from cache
*
* @param embedUrl
* embed url
*/
public void removeFont(String embedUrl) {
synchronized (changeLock) {
if (containsFont(embedUrl)) {
if (log.isTraceEnabled()) {
log.trace("Font removed from cache: " + embedUrl);
}
getFontFileMap().remove(embedUrl);
changed = true;
}
}
}
/**
* has this font previously failed to load?
*
* @param embedUrl
* embed url
* @param lastModified
* last modified
* @return whether this is a failed font
*/
public boolean isFailedFont(String embedUrl, long lastModified) {
synchronized (changeLock) {
if (getFailedFontMap().containsKey(embedUrl)) {
long failedLastModified = getFailedFontMap().get(
embedUrl);
if (lastModified != failedLastModified) {
// this font has been changed so lets remove it
// from failed font map for now
getFailedFontMap().remove(embedUrl);
changed = true;
}
return true;
} else {
return false;
}
}
}
/**
* Registers a failed font with the cache
*
* @param embedUrl
* embed url
* @param lastModified
* time last modified
*/
public void registerFailedFont(String embedUrl, long lastModified) {
synchronized (changeLock) {
if (!getFailedFontMap().containsKey(embedUrl)) {
getFailedFontMap().put(embedUrl, lastModified);
changed = true;
}
}
}
private Map<String, Long> getFailedFontMap() {
if (failedFontMap == null) {
failedFontMap = new HashMap<String, Long>();
}
return failedFontMap;
}
/**
* Clears font cache
*/
public void clear() {
synchronized (changeLock) {
if (log.isTraceEnabled()) {
log.trace("Font cache cleared.");
}
fontfileMap = null;
failedFontMap = null;
changed = true;
}
}
/**
* Retrieve the last modified date/time of a URI.
*
* @param uri the URI
* @return the last modified date/time
*/
public static long getLastModified(URI uri) {
try {
URL url = uri.toURL();
URLConnection conn = url.openConnection();
try {
return conn.getLastModified();
} finally {
// An InputStream is created even if it's not accessed, but we
// need to close it.
IOUtils.closeQuietly(conn.getInputStream());
}
} catch (IOException e) {
// Should never happen, because URL must be local
log.debug("IOError: " + e.getMessage());
return 0;
}
}
private static class CachedFontFile implements Serializable {
private static final long serialVersionUID = 4524237324330578883L;
/** file modify date (if available) */
private long lastModified = -1;
private Map<String, EmbedFontInfo> filefontsMap;
public CachedFontFile(long lastModified) {
setLastModified(lastModified);
}
private Map<String, EmbedFontInfo> getFileFontsMap() {
if (filefontsMap == null) {
filefontsMap = new HashMap<String, EmbedFontInfo>();
}
return filefontsMap;
}
void put(EmbedFontInfo efi) {
getFileFontsMap().put(efi.getPostScriptName(), efi);
}
public boolean containsFont(EmbedFontInfo efi) {
return efi.getPostScriptName() != null
&& getFileFontsMap().containsKey(efi.getPostScriptName());
}
public EmbedFontInfo[] getEmbedFontInfos() {
return getFileFontsMap().values().toArray(
new EmbedFontInfo[getFileFontsMap().size()]);
}
/**
* Gets the modified timestamp for font file (not always available)
*
* @return modified timestamp
*/
public long lastModified() {
return this.lastModified;
}
/**
* Gets the modified timestamp for font file (used for the purposes of
* font info caching)
*
* @param lastModified
* modified font file timestamp
*/
public void setLastModified(long lastModified) {
this.lastModified = lastModified;
}
/**
* @return string representation of this object {@inheritDoc}
*/
public String toString() {
return super.toString() + ", lastModified=" + lastModified;
}
}
}