/* * 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 -> 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; } } }