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 -> 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