| /* |
| * 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.File; |
| 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.URL; |
| import java.net.URLConnection; |
| 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.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 = 605232520271754719L; |
| |
| /** 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 = false; |
| |
| /** 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. */ |
| private Map/*<String, CachedFontFile>*/ fontfileMap = null; |
| |
| /** mapping of font url -> file modified date (for all fonts that have failed to load) */ |
| private Map failedFontMap/*<String, Long>*/ = null; |
| |
| /** |
| * Default constructor |
| */ |
| public FontCache() { |
| //nop |
| } |
| |
| 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) |
| */ |
| 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 java.io.FileInputStream(cacheFile); |
| in = new java.io.BufferedInputStream(in); |
| 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 |
| */ |
| 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 { |
| if (log.isTraceEnabled()) { |
| 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) { |
| String embedFile = fontInfo.getEmbedFile(); |
| String metricsFile = fontInfo.getMetricsFile(); |
| return (embedFile != null) ? embedFile : metricsFile; |
| } |
| 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 (int i = 0; i < urls.length; i++) { |
| String urlStr = urls[i]; |
| 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 java.util.HashMap/*<String, CachedFontFile>*/(); |
| } |
| return fontfileMap; |
| } |
| |
| /** |
| * Adds a font info to cache |
| * @param fontInfo font info |
| */ |
| public void addFont(EmbedFontInfo fontInfo) { |
| String cacheKey = getCacheKey(fontInfo); |
| synchronized (changeLock) { |
| CachedFontFile cachedFontFile; |
| if (containsFont(cacheKey)) { |
| cachedFontFile = (CachedFontFile)getFontFileMap().get(cacheKey); |
| if (!cachedFontFile.containsFont(fontInfo)) { |
| cachedFontFile.put(fontInfo); |
| } |
| } else { |
| // try and determine modified date |
| File fontFile = getFileFromUrls(new String[] |
| {fontInfo.getEmbedFile(), fontInfo.getMetricsFile()}); |
| long lastModified = (fontFile != null ? fontFile.lastModified() : -1); |
| 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) ? (CachedFontFile) 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 = ((Long)getFailedFontMap().get(embedUrl)).longValue(); |
| 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, new Long(lastModified)); |
| changed = true; |
| } |
| } |
| } |
| |
| private Map/*<String, Long>*/ getFailedFontMap() { |
| if (failedFontMap == null) { |
| failedFontMap = new java.util.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 URL. |
| * @param url the URL |
| * @return the last modified date/time |
| */ |
| public static long getLastModified(URL url) { |
| try { |
| 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 = null; |
| |
| public CachedFontFile(long lastModified) { |
| setLastModified(lastModified); |
| } |
| |
| private Map/*<String, EmbedFontInfo>*/ getFileFontsMap() { |
| if (filefontsMap == null) { |
| filefontsMap = new java.util.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 (EmbedFontInfo[])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; |
| } |
| |
| } |
| } |