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

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