You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FontCache.java 17KB

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