1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id: FontCache.java 1805173 2017-08-16 10:50:04Z ssteiner $ */
19 
20 package org.apache.fop.fonts;
21 
22 import java.io.BufferedInputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.ObjectInputStream;
28 import java.io.ObjectOutputStream;
29 import java.io.OutputStream;
30 import java.io.Serializable;
31 import java.net.MalformedURLException;
32 import java.net.URI;
33 import java.net.URL;
34 import java.net.URLConnection;
35 import java.util.HashMap;
36 import java.util.Map;
37 
38 import org.apache.commons.io.FileUtils;
39 import org.apache.commons.io.IOUtils;
40 import org.apache.commons.logging.Log;
41 import org.apache.commons.logging.LogFactory;
42 
43 import org.apache.fop.apps.FOPException;
44 import org.apache.fop.apps.io.InternalResourceResolver;
45 import org.apache.fop.util.LogUtil;
46 
47 /**
48  * Fop cache (currently only used for font info caching)
49  */
50 public final class FontCache implements Serializable {
51 
52     /**
53      * Serialization Version UID. Change this value if you want to make sure the
54      * user's cache file is purged after an update.
55      */
56     private static final long serialVersionUID = 9129238336422194339L;
57 
58     /** logging instance */
59     private static Log log = LogFactory.getLog(FontCache.class);
60 
61     /** FOP's user directory name */
62     private static final String FOP_USER_DIR = ".fop";
63 
64     /** font cache file path */
65     private static final String DEFAULT_CACHE_FILENAME = "fop-fonts.cache";
66 
67     /** has this cache been changed since it was last read? */
68     private transient boolean changed;
69 
70     /** change lock */
71     private final boolean[] changeLock = new boolean[1];
72 
73     /**
74      * master mapping of font url -> font info. This needs to be a list, since a
75      * TTC file may contain more than 1 font.
76      * @serial
77      */
78     private Map<String, CachedFontFile> fontfileMap;
79 
80     /**
81      * mapping of font url -&gt; file modified date (for all fonts that have failed
82      * to load)
83      * @serial
84      */
85     private Map<String, Long> failedFontMap;
86 
readObject(ObjectInputStream ois)87     private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
88         ois.defaultReadObject();
89     }
90 
getUserHome()91     private static File getUserHome() {
92         return toDirectory(System.getProperty("user.home"));
93     }
94 
getTempDirectory()95     private static File getTempDirectory() {
96         return toDirectory(System.getProperty("java.io.tmpdir"));
97     }
98 
toDirectory(String path)99     private static File toDirectory(String path) {
100         if (path != null) {
101             File dir = new File(path);
102             if (dir.exists()) {
103                 return dir;
104             }
105         }
106         return null;
107     }
108 
109     /**
110      * Returns the default font cache file.
111      *
112      * @param forWriting
113      *            true if the user directory should be created
114      * @return the default font cache file
115      */
getDefaultCacheFile(boolean forWriting)116     public static File getDefaultCacheFile(boolean forWriting) {
117         File userHome = getUserHome();
118         if (userHome != null) {
119             File fopUserDir = new File(userHome, FOP_USER_DIR);
120             if (forWriting) {
121                 boolean writable = fopUserDir.canWrite();
122                 if (!fopUserDir.exists()) {
123                     writable = fopUserDir.mkdir();
124                 }
125                 if (!writable) {
126                     userHome = getTempDirectory();
127                     fopUserDir = new File(userHome, FOP_USER_DIR);
128                     fopUserDir.mkdir();
129                 }
130             }
131             return new File(fopUserDir, DEFAULT_CACHE_FILENAME);
132         }
133         return new File(FOP_USER_DIR);
134     }
135 
136     /**
137      * Reads the default font cache file and returns its contents.
138      *
139      * @return the font cache deserialized from the file (or null if no cache
140      *         file exists or if it could not be read)
141      * @deprecated use {@link #loadFrom(File)} instead
142      */
load()143     public static FontCache load() {
144         return loadFrom(getDefaultCacheFile(false));
145     }
146 
147     /**
148      * Reads a font cache file and returns its contents.
149      *
150      * @param cacheFile
151      *            the cache file
152      * @return the font cache deserialized from the file (or null if no cache
153      *         file exists or if it could not be read)
154      */
loadFrom(File cacheFile)155     public static FontCache loadFrom(File cacheFile) {
156         if (cacheFile.exists()) {
157             try {
158                 if (log.isTraceEnabled()) {
159                     log.trace("Loading font cache from "
160                             + cacheFile.getCanonicalPath());
161                 }
162                 InputStream in = new BufferedInputStream(new FileInputStream(cacheFile));
163                 ObjectInputStream oin = new ObjectInputStream(in);
164                 try {
165                     return (FontCache) oin.readObject();
166                 } finally {
167                     IOUtils.closeQuietly(oin);
168                 }
169             } catch (ClassNotFoundException e) {
170                 // We don't really care about the exception since it's just a
171                 // cache file
172                 log.warn("Could not read font cache. Discarding font cache file. Reason: "
173                         + e.getMessage());
174             } catch (IOException ioe) {
175                 // We don't really care about the exception since it's just a
176                 // cache file
177                 log.warn("I/O exception while reading font cache ("
178                         + ioe.getMessage() + "). Discarding font cache file.");
179                 try {
180                     cacheFile.delete();
181                 } catch (SecurityException ex) {
182                     log.warn("Failed to delete font cache file: "
183                             + cacheFile.getAbsolutePath());
184                 }
185             }
186         }
187         return null;
188     }
189 
190     /**
191      * Writes the font cache to disk.
192      *
193      * @throws FOPException fop exception
194      * @deprecated use {@link #saveTo(File)} instead
195      */
save()196     public void save() throws FOPException {
197         saveTo(getDefaultCacheFile(true));
198     }
199 
200     /**
201      * Writes the font cache to disk.
202      *
203      * @param cacheFile
204      *            the file to write to
205      * @throws FOPException
206      *             fop exception
207      */
saveTo(File cacheFile)208     public void saveTo(File cacheFile) throws FOPException {
209         synchronized (changeLock) {
210             if (changed) {
211                 try {
212                     log.trace("Writing font cache to " + cacheFile.getCanonicalPath());
213                     OutputStream out = new java.io.FileOutputStream(cacheFile);
214                     out = new java.io.BufferedOutputStream(out);
215                     ObjectOutputStream oout = new ObjectOutputStream(out);
216                     try {
217                         oout.writeObject(this);
218                     } finally {
219                         IOUtils.closeQuietly(oout);
220                     }
221                 } catch (IOException ioe) {
222                     LogUtil.handleException(log, ioe, true);
223                 }
224                 changed = false;
225                 log.trace("Cache file written.");
226             }
227         }
228     }
229 
230     /**
231      * creates a key given a font info for the font mapping
232      *
233      * @param fontInfo
234      *            font info
235      * @return font cache key
236      */
getCacheKey(EmbedFontInfo fontInfo)237     protected static String getCacheKey(EmbedFontInfo fontInfo) {
238         if (fontInfo != null) {
239             URI embedFile = fontInfo.getEmbedURI();
240             URI metricsFile = fontInfo.getMetricsURI();
241             return (embedFile != null) ? embedFile.toASCIIString() : metricsFile.toASCIIString();
242         }
243         return null;
244     }
245 
246     /**
247      * cache has been updated since it was read
248      *
249      * @return if this cache has changed
250      */
hasChanged()251     public boolean hasChanged() {
252         return this.changed;
253     }
254 
255     /**
256      * is this font in the cache?
257      *
258      * @param embedUrl
259      *            font info
260      * @return boolean
261      */
containsFont(String embedUrl)262     public boolean containsFont(String embedUrl) {
263         return (embedUrl != null && getFontFileMap().containsKey(embedUrl));
264     }
265 
266     /**
267      * is this font info in the cache?
268      *
269      * @param fontInfo
270      *            font info
271      * @return font
272      */
containsFont(EmbedFontInfo fontInfo)273     public boolean containsFont(EmbedFontInfo fontInfo) {
274         return (fontInfo != null && getFontFileMap().containsKey(
275                 getCacheKey(fontInfo)));
276     }
277 
278     /**
279      * Tries to identify a File instance from an array of URLs. If there's no
280      * file URL in the array, the method returns null.
281      *
282      * @param urls
283      *            array of possible font urls
284      * @return file font file
285      */
getFileFromUrls(String[] urls)286     public static File getFileFromUrls(String[] urls) {
287         for (String urlStr : urls) {
288             if (urlStr != null) {
289                 File fontFile = null;
290                 if (urlStr.startsWith("file:")) {
291                     try {
292                         URL url = new URL(urlStr);
293                         fontFile = FileUtils.toFile(url);
294                     } catch (MalformedURLException mfue) {
295                         // do nothing
296                     }
297                 }
298                 if (fontFile == null) {
299                     fontFile = new File(urlStr);
300                 }
301                 if (fontFile.exists() && fontFile.canRead()) {
302                     return fontFile;
303                 }
304             }
305         }
306         return null;
307     }
308 
getFontFileMap()309     private Map<String, CachedFontFile> getFontFileMap() {
310         if (fontfileMap == null) {
311             fontfileMap = new HashMap<String, CachedFontFile>();
312         }
313         return fontfileMap;
314     }
315 
316     /**
317      * Adds a font info to cache
318      *
319      * @param fontInfo
320      *            font info
321      */
addFont(EmbedFontInfo fontInfo, InternalResourceResolver resourceResolver)322     public void addFont(EmbedFontInfo fontInfo, InternalResourceResolver resourceResolver) {
323         String cacheKey = getCacheKey(fontInfo);
324         synchronized (changeLock) {
325             CachedFontFile cachedFontFile;
326             if (containsFont(cacheKey)) {
327                 cachedFontFile = getFontFileMap().get(cacheKey);
328                 if (!cachedFontFile.containsFont(fontInfo)) {
329                     cachedFontFile.put(fontInfo);
330                 }
331             } else {
332                 // try and determine modified date
333                 URI fontUri = resourceResolver.resolveFromBase(fontInfo.getEmbedURI());
334                 long lastModified = getLastModified(fontUri);
335                 cachedFontFile = new CachedFontFile(lastModified);
336                 if (log.isTraceEnabled()) {
337                     log.trace("Font added to cache: " + cacheKey);
338                 }
339                 cachedFontFile.put(fontInfo);
340                 getFontFileMap().put(cacheKey, cachedFontFile);
341                 changed = true;
342             }
343         }
344     }
345 
346     /**
347      * Returns a font from the cache.
348      *
349      * @param embedUrl
350      *            font info
351      * @return CachedFontFile object
352      */
getFontFile(String embedUrl)353     public CachedFontFile getFontFile(String embedUrl) {
354         return containsFont(embedUrl) ? getFontFileMap().get(embedUrl) : null;
355     }
356 
357     /**
358      * Returns the EmbedFontInfo instances belonging to a font file. If the font
359      * file was modified since it was cached the entry is removed and null is
360      * returned.
361      *
362      * @param embedUrl
363      *            the font URL
364      * @param lastModified
365      *            the last modified date/time of the font file
366      * @return the EmbedFontInfo instances or null if there's no cached entry or
367      *         if it is outdated
368      */
getFontInfos(String embedUrl, long lastModified)369     public EmbedFontInfo[] getFontInfos(String embedUrl, long lastModified) {
370         CachedFontFile cff = getFontFile(embedUrl);
371         if (cff.lastModified() == lastModified) {
372             return cff.getEmbedFontInfos();
373         } else {
374             removeFont(embedUrl);
375             return null;
376         }
377     }
378 
379     /**
380      * removes font from cache
381      *
382      * @param embedUrl
383      *            embed url
384      */
removeFont(String embedUrl)385     public void removeFont(String embedUrl) {
386         synchronized (changeLock) {
387             if (containsFont(embedUrl)) {
388                 if (log.isTraceEnabled()) {
389                     log.trace("Font removed from cache: " + embedUrl);
390                 }
391                 getFontFileMap().remove(embedUrl);
392                 changed = true;
393             }
394         }
395     }
396 
397     /**
398      * has this font previously failed to load?
399      *
400      * @param embedUrl
401      *            embed url
402      * @param lastModified
403      *            last modified
404      * @return whether this is a failed font
405      */
isFailedFont(String embedUrl, long lastModified)406     public boolean isFailedFont(String embedUrl, long lastModified) {
407         synchronized (changeLock) {
408             if (getFailedFontMap().containsKey(embedUrl)) {
409                 long failedLastModified = getFailedFontMap().get(
410                         embedUrl);
411                 if (lastModified != failedLastModified) {
412                     // this font has been changed so lets remove it
413                     // from failed font map for now
414                     getFailedFontMap().remove(embedUrl);
415                     changed = true;
416                 }
417                 return true;
418             } else {
419                 return false;
420             }
421         }
422     }
423 
424     /**
425      * Registers a failed font with the cache
426      *
427      * @param embedUrl
428      *            embed url
429      * @param lastModified
430      *            time last modified
431      */
registerFailedFont(String embedUrl, long lastModified)432     public void registerFailedFont(String embedUrl, long lastModified) {
433         synchronized (changeLock) {
434             if (!getFailedFontMap().containsKey(embedUrl)) {
435                 getFailedFontMap().put(embedUrl, lastModified);
436                 changed = true;
437             }
438         }
439     }
440 
getFailedFontMap()441     private Map<String, Long> getFailedFontMap() {
442         if (failedFontMap == null) {
443             failedFontMap = new HashMap<String, Long>();
444         }
445         return failedFontMap;
446     }
447 
448     /**
449      * Clears font cache
450      */
clear()451     public void clear() {
452         synchronized (changeLock) {
453             if (log.isTraceEnabled()) {
454                 log.trace("Font cache cleared.");
455             }
456             fontfileMap = null;
457             failedFontMap = null;
458             changed = true;
459         }
460     }
461 
462     /**
463      * Retrieve the last modified date/time of a URI.
464      *
465      * @param uri the URI
466      * @return the last modified date/time
467      */
getLastModified(URI uri)468     public static long getLastModified(URI uri) {
469         try {
470             URL url = uri.toURL();
471             URLConnection conn = url.openConnection();
472             try {
473                 return conn.getLastModified();
474             } finally {
475                 // An InputStream is created even if it's not accessed, but we
476                 // need to close it.
477                 IOUtils.closeQuietly(conn.getInputStream());
478             }
479         } catch (IOException e) {
480             // Should never happen, because URL must be local
481             log.debug("IOError: " + e.getMessage());
482             return 0;
483         }
484     }
485 
486     private static class CachedFontFile implements Serializable {
487         private static final long serialVersionUID = 4524237324330578883L;
488 
489         /** file modify date (if available) */
490         private long lastModified = -1;
491 
492         private Map<String, EmbedFontInfo> filefontsMap;
493 
CachedFontFile(long lastModified)494         public CachedFontFile(long lastModified) {
495             setLastModified(lastModified);
496         }
497 
getFileFontsMap()498         private Map<String, EmbedFontInfo> getFileFontsMap() {
499             if (filefontsMap == null) {
500                 filefontsMap = new HashMap<String, EmbedFontInfo>();
501             }
502             return filefontsMap;
503         }
504 
put(EmbedFontInfo efi)505         void put(EmbedFontInfo efi) {
506             getFileFontsMap().put(efi.getPostScriptName(), efi);
507         }
508 
containsFont(EmbedFontInfo efi)509         public boolean containsFont(EmbedFontInfo efi) {
510             return efi.getPostScriptName() != null
511                     && getFileFontsMap().containsKey(efi.getPostScriptName());
512         }
513 
getEmbedFontInfos()514         public EmbedFontInfo[] getEmbedFontInfos() {
515             return getFileFontsMap().values().toArray(
516                     new EmbedFontInfo[getFileFontsMap().size()]);
517         }
518 
519         /**
520          * Gets the modified timestamp for font file (not always available)
521          *
522          * @return modified timestamp
523          */
lastModified()524         public long lastModified() {
525             return this.lastModified;
526         }
527 
528         /**
529          * Gets the modified timestamp for font file (used for the purposes of
530          * font info caching)
531          *
532          * @param lastModified
533          *            modified font file timestamp
534          */
setLastModified(long lastModified)535         public void setLastModified(long lastModified) {
536             this.lastModified = lastModified;
537         }
538 
539         /**
540          * @return string representation of this object {@inheritDoc}
541          */
toString()542         public String toString() {
543             return super.toString() + ", lastModified=" + lastModified;
544         }
545 
546     }
547 }
548