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

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