/*
 * 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: FontCache.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

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)
/** * 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.
/** * 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
/** logging instance */
private static Log log = LogFactory.getLog(FontCache.class);
FOP's user directory name
/** FOP's user directory name */
private static final String FOP_USER_DIR = ".fop";
font cache file path
/** font cache file path */
private static final String DEFAULT_CACHE_FILENAME = "fop-fonts.cache";
has this cache been changed since it was last read?
/** has this cache been changed since it was last read? */
private transient boolean changed;
change lock
/** 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
/** * 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
/** * 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.
Params:
  • forWriting – true if the user directory should be created
Returns:the default font cache file
/** * 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.
Returns:the font cache deserialized from the file (or null if no cache file exists or if it could not be read)
Deprecated:use loadFrom(File) instead
/** * 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.
Params:
  • cacheFile – the cache file
Returns:the font cache deserialized from the file (or null if no cache file exists or if it could not be read)
/** * 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 saveTo(File) instead
/** * 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.
Params:
  • cacheFile – the file to write to
Throws:
/** * 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
Params:
  • fontInfo – font info
Returns:font cache key
/** * 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
Returns:if this cache has changed
/** * 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?
Params:
  • embedUrl – font info
Returns:boolean
/** * 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?
Params:
  • fontInfo – font info
Returns:font
/** * 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.
Params:
  • urls – array of possible font urls
Returns:file font file
/** * 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
Params:
  • fontInfo – font info
/** * 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.
Params:
  • embedUrl – font info
Returns:CachedFontFile object
/** * 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.
Params:
  • embedUrl – the font URL
  • lastModified – the last modified date/time of the font file
Returns:the EmbedFontInfo instances or null if there's no cached entry or if it is outdated
/** * 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
Params:
  • embedUrl – embed url
/** * 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?
Params:
  • embedUrl – embed url
  • lastModified – last modified
Returns:whether this is a failed font
/** * 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
Params:
  • embedUrl – embed url
  • lastModified – time last modified
/** * 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
/** * 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.
Params:
  • uri – the URI
Returns:the last modified date/time
/** * 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)
/** 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)
Returns:modified timestamp
/** * 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)
Params:
  • lastModified – modified font file timestamp
/** * 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; }
Returns:string representation of this object {@inheritDoc}
/** * @return string representation of this object {@inheritDoc} */
public String toString() { return super.toString() + ", lastModified=" + lastModified; } } }