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