1 /*
   2  * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any questions.
  24  */
  25 
  26 package sun.font;
  27 
  28 import java.awt.Font;
  29 import java.io.File;
  30 import java.io.FileInputStream;
  31 import java.io.FileOutputStream;
  32 import java.io.IOException;
  33 import java.net.InetAddress;
  34 import java.net.UnknownHostException;
  35 import java.nio.charset.Charset;
  36 import java.util.HashMap;
  37 import java.util.HashSet;
  38 import java.util.logging.Logger;
  39 import java.util.Properties;
  40 import java.util.Scanner;
  41 import sun.awt.FontConfiguration;
  42 import sun.awt.FontDescriptor;
  43 import sun.awt.SunToolkit;
  44 import sun.font.CompositeFontDescriptor;
  45 import sun.font.FontManager;
  46 import sun.font.FontManager.FontConfigInfo;
  47 import sun.font.FontManager.FcCompFont;
  48 import sun.font.FontManager.FontConfigFont;
  49 import sun.java2d.SunGraphicsEnvironment;
  50 
  51 public class FcFontConfiguration extends FontConfiguration {
  52 
  53     /** Version of the cache file format understood by this code.
  54      * Its part of the file name so that we can rev this at
  55      * any time, even in a minor JDK update.
  56      * It is stored as the value of the "version" property.
  57      * This is distinct from the version of "libfontconfig" that generated
  58      * the cached results, and which is the "fcversion" property in the file.
  59      * {@code FontConfiguration.getVersion()} also returns a version string,
  60      * and has meant the version of the fontconfiguration.properties file
  61      * that was read. Since this class doesn't use such files, then what
  62      * that really means is whether the methods on this class return
  63      * values that are compatible with the classes that do directly read
  64      * from such files. It is a compatible subset of version "1".
  65      */
  66     private static final String fileVersion = "1";
  67     private String fcInfoFileName = null;
  68 
  69     private FcCompFont[] fcCompFonts = null;
  70 
  71     public FcFontConfiguration(SunGraphicsEnvironment environment) {
  72         super(environment);
  73         init();
  74     }
  75 
  76     /* This isn't called but is needed to satisfy super-class contract. */
  77     public FcFontConfiguration(SunGraphicsEnvironment environment,
  78                                boolean preferLocaleFonts,
  79                                boolean preferPropFonts) {
  80         super(environment, preferLocaleFonts, preferPropFonts);
  81         init();
  82     }
  83 
  84     @Override
  85     public synchronized boolean init() {
  86         if (fcCompFonts != null) {
  87             return true;
  88         }
  89 
  90         setFontConfiguration();
  91         readFcInfo();
  92         if (fcCompFonts == null) {
  93             fcCompFonts = FontManager.loadFontConfig();
  94             if (fcCompFonts != null) {
  95                 try {
  96                     writeFcInfo();
  97                 } catch (Exception e) {
  98                     if (SunGraphicsEnvironment.debugFonts) {
  99                         Logger logger =
 100                             Logger.getLogger("sun.awt.FontConfiguration");
 101                         logger.warning("Exception writing fcInfo " + e);
 102                     }
 103                 }
 104             } else if (SunGraphicsEnvironment.debugFonts) {
 105                 Logger logger = Logger.getLogger("sun.awt.FontConfiguration");
 106                 logger.warning("Failed to get info from libfontconfig");
 107             }
 108         } else {
 109             FontManager.populateFontConfig(fcCompFonts);
 110         }
 111 
 112         if (fcCompFonts == null) {
 113             return false; // couldn't load fontconfig.
 114         }
 115 
 116         // NB already in a privileged block from SGE
 117         String javaHome = System.getProperty("java.home");
 118         if (javaHome == null) {
 119             throw new Error("java.home property not set");
 120         }
 121         String javaLib = javaHome + File.separator + "lib";
 122         getInstalledFallbackFonts(javaLib);
 123 
 124         return true;
 125     }
 126 
 127     @Override
 128     public String getFallbackFamilyName(String fontName,
 129                                         String defaultFallback) {
 130         // maintain compatibility with old font.properties files, which either
 131         // had aliases for TimesRoman & Co. or defined mappings for them.
 132         String compatibilityName = getCompatibilityFamilyName(fontName);
 133         if (compatibilityName != null) {
 134             return compatibilityName;
 135         }
 136         return defaultFallback;
 137     }
 138 
 139     @Override
 140     protected String
 141         getFaceNameFromComponentFontName(String componentFontName) {
 142         return null;
 143     }
 144 
 145     @Override
 146     protected String
 147         getFileNameFromComponentFontName(String componentFontName) {
 148         return null;
 149     }
 150 
 151     @Override
 152     public String getFileNameFromPlatformName(String platformName) {
 153         /* Platform name is the file name, but rather than returning
 154          * the arg, return null*/
 155         return null;
 156     }
 157 
 158     @Override
 159     protected Charset getDefaultFontCharset(String fontName) {
 160         return Charset.forName("ISO8859_1");
 161     }
 162 
 163     @Override
 164     protected String getEncoding(String awtFontName,
 165                                  String characterSubsetName) {
 166         return "default";
 167     }
 168 
 169     @Override
 170     protected void initReorderMap() {
 171         reorderMap = new HashMap();
 172     }
 173 
 174     @Override
 175     public FontDescriptor[] getFontDescriptors(String fontName, int style) {
 176         return new FontDescriptor[0];
 177     }
 178 
 179     @Override
 180     public int getNumberCoreFonts() {
 181         return 1;
 182     }
 183 
 184     @Override
 185     public String[] getPlatformFontNames() {
 186         HashSet<String> nameSet = new HashSet<String>();
 187         FcCompFont[] fcCompFonts = FontManager.loadFontConfig();
 188         for (int i=0; i<fcCompFonts.length; i++) {
 189             for (int j=0; j<fcCompFonts[i].allFonts.length; j++) {
 190                 nameSet.add(fcCompFonts[i].allFonts[j].fontFile);
 191             }
 192         }
 193         return nameSet.toArray(new String[0]);
 194     }
 195 
 196     @Override
 197     public String getExtraFontPath() {
 198         return null;
 199     }
 200 
 201     @Override
 202     public boolean needToSearchForFile(String fileName) {
 203         return false;
 204     }
 205 
 206     private FontConfigFont[] getFcFontList(FcCompFont[] fcFonts,
 207                                            String fontname, int style) {
 208 
 209         if (fontname.equals("dialog")) {
 210             fontname = "sansserif";
 211         } else if (fontname.equals("dialoginput")) {
 212             fontname = "monospaced";
 213         }
 214         for (int i=0; i<fcFonts.length; i++) {
 215             if (fontname.equals(fcFonts[i].jdkName) &&
 216                 style == fcFonts[i].style) {
 217                 return fcFonts[i].allFonts;
 218             }
 219         }
 220         return fcFonts[0].allFonts;
 221     }
 222 
 223     @Override
 224     public CompositeFontDescriptor[] get2DCompositeFontInfo() {
 225 
 226         FcCompFont[] fcCompFonts = FontManager.loadFontConfig();
 227 
 228         CompositeFontDescriptor[] result =
 229                 new CompositeFontDescriptor[NUM_FONTS * NUM_STYLES];
 230 
 231         for (int fontIndex = 0; fontIndex < NUM_FONTS; fontIndex++) {
 232             String fontName = publicFontNames[fontIndex];
 233 
 234             for (int styleIndex = 0; styleIndex < NUM_STYLES; styleIndex++) {
 235 
 236                 String faceName = fontName + "." + styleNames[styleIndex];
 237                 FontConfigFont[] fcFonts =
 238                     getFcFontList(fcCompFonts,
 239                                   fontNames[fontIndex], styleIndex);
 240 
 241                 int numFonts = fcFonts.length;
 242                 // fall back fonts listed in the lib/fonts/fallback directory
 243                 if (installedFallbackFontFiles != null) {
 244                     numFonts += installedFallbackFontFiles.length;
 245                 }
 246 
 247                 String[] fileNames = new String[numFonts];
 248 
 249                 int index;
 250                 for (index = 0; index < fcFonts.length; index++) {
 251                     fileNames[index] = fcFonts[index].fontFile;
 252                 }
 253 
 254                 if (installedFallbackFontFiles != null) {
 255                     System.arraycopy(fileNames, index,
 256                                      installedFallbackFontFiles,
 257                                      0, installedFallbackFontFiles.length);
 258                 }
 259 
 260                 result[fontIndex * NUM_STYLES + styleIndex]
 261                         = new CompositeFontDescriptor(
 262                             faceName,
 263                             1,
 264                             null,
 265                             fileNames,
 266                             null, null);
 267             }
 268         }
 269         return result;
 270     }
 271 
 272     /**
 273      * Gets the OS version string from a Linux release-specific file.
 274      */
 275     private String getVersionString(File f){
 276         try {
 277             Scanner sc  = new Scanner(f);
 278             return sc.findInLine("(\\d)+((\\.)(\\d)+)*");
 279         }
 280         catch (Exception e){
 281         }
 282         return null;
 283     }
 284 
 285     /**
 286      * Sets the OS name and version from environment information.
 287      */
 288     @Override
 289     protected void setOsNameAndVersion() {
 290 
 291         super.setOsNameAndVersion();
 292 
 293         if (!osName.equals("Linux")) {
 294             return;
 295         }
 296         try {
 297             File f;
 298             if ((f = new File("/etc/lsb-release")).canRead()) {
 299                     /* Ubuntu and (perhaps others) use only lsb-release.
 300                      * Syntax and encoding is compatible with java properties.
 301                      * For Ubuntu the ID is "Ubuntu".
 302                      */
 303                     Properties props = new Properties();
 304                     props.load(new FileInputStream(f));
 305                     osName = props.getProperty("DISTRIB_ID");
 306                     osVersion =  props.getProperty("DISTRIB_RELEASE");
 307             } else if ((f = new File("/etc/redhat-release")).canRead()) {
 308                 osName = "RedHat";
 309                 osVersion = getVersionString(f);
 310             } else if ((f = new File("/etc/SuSE-release")).canRead()) {
 311                 osName = "SuSE";
 312                 osVersion = getVersionString(f);
 313             } else if ((f = new File("/etc/turbolinux-release")).canRead()) {
 314                 osName = "Turbo";
 315                 osVersion = getVersionString(f);
 316             } else if ((f = new File("/etc/fedora-release")).canRead()) {
 317                 osName = "Fedora";
 318                 osVersion = getVersionString(f);
 319             } else if ((f = new File("/etc/sun-release")).canRead()) {
 320                 osName = "Sun";
 321                 osVersion = getVersionString(f);
 322             }
 323         } catch (Exception e) {
 324             if (SunGraphicsEnvironment.debugFonts) {
 325                 Logger logger = Logger.getLogger("sun.awt.FontConfiguration");
 326                 logger.warning("Exception identifying Linux distro.");
 327             }
 328         }
 329     }
 330 
 331     private File getFcInfoFile() {
 332         if (fcInfoFileName == null) {
 333             // NB need security permissions to get true IP address, and
 334             // we should have those as the whole initialisation is in a
 335             // doPrivileged block. But in this case no exception is thrown,
 336             // and it returns the loop back address, and so we end up with
 337             // "localhost"
 338             String hostname;
 339             try {
 340                 hostname = InetAddress.getLocalHost().getHostName();
 341             } catch (UnknownHostException e) {
 342                 hostname = "localhost";
 343             }
 344             String userDir = System.getProperty("user.home");
 345             String version = System.getProperty("java.version");
 346             String fs = File.separator;
 347             String dir = userDir+fs+".java"+fs+"fonts"+fs+version;
 348             String lang = SunToolkit.getStartupLocale().getLanguage();
 349             String name = "fcinfo-"+fileVersion+"-"+hostname+"-"+
 350                 osName+"-"+osVersion+"-"+lang+".properties";
 351             fcInfoFileName = dir+fs+name;
 352         }
 353         return new File(fcInfoFileName);
 354     }
 355 
 356     private void writeFcInfo() {
 357         Properties props = new Properties();
 358         props.setProperty("version", fileVersion);
 359         FontConfigInfo fcInfo = FontManager.getFontConfigInfo();
 360         props.setProperty("fcversion", Integer.toString(fcInfo.fcVersion));
 361         if (fcInfo.cacheDirs != null) {
 362             for (int i=0;i<fcInfo.cacheDirs.length;i++) {
 363                 if (fcInfo.cacheDirs[i] != null) {
 364                    props.setProperty("cachedir."+i,  fcInfo.cacheDirs[i]);
 365                 }
 366             }
 367         }
 368         for (int i=0; i<fcCompFonts.length; i++) {
 369             FcCompFont fci = fcCompFonts[i];
 370             String styleKey = fci.jdkName+"."+fci.style;
 371             props.setProperty(styleKey+".length",
 372                               Integer.toString(fci.allFonts.length));
 373             for (int j=0; j<fci.allFonts.length; j++) {
 374                 props.setProperty(styleKey+"."+j+".family",
 375                                   fci.allFonts[j].familyName);
 376                 props.setProperty(styleKey+"."+j+".file",
 377                                   fci.allFonts[j].fontFile);
 378             }
 379         }
 380         try {
 381             /* This writes into a temp file then renames when done.
 382              * Since the rename is an atomic action within the same
 383              * directory no client will ever see a partially written file.
 384              */
 385             File fcInfoFile = getFcInfoFile();
 386             File dir = fcInfoFile.getParentFile();
 387             dir.mkdirs();
 388             File tempFile = File.createTempFile("fcinfo", null, dir);
 389             FileOutputStream fos = new FileOutputStream(tempFile);
 390             props.store(fos,
 391                       "JDK Font Configuration Generated File: *Do Not Edit*");
 392             fos.close();
 393             boolean renamed = tempFile.renameTo(fcInfoFile);
 394             if (!renamed && SunGraphicsEnvironment.debugFonts) {
 395                 System.out.println("rename failed");
 396                 Logger logger = Logger.getLogger("sun.awt.FontConfiguration");
 397                 logger.warning("Failed renaming file to "+ getFcInfoFile());
 398             }
 399         } catch (Exception e) {
 400             if (SunGraphicsEnvironment.debugFonts) {
 401                 Logger logger = Logger.getLogger("sun.awt.FontConfiguration");
 402                 logger.warning("IOException writing to "+ getFcInfoFile());
 403             }
 404         }
 405     }
 406 
 407     /* We want to be able to use this cache instead of invoking
 408      * fontconfig except when we can detect the system cache has changed.
 409      * But there doesn't seem to be a way to find the location of
 410      * the system cache.
 411      */
 412     private void readFcInfo() {
 413         File fcFile = getFcInfoFile();
 414         if (!fcFile.exists()) {
 415             return;
 416         }
 417         Properties props = new Properties();
 418         try {
 419             FileInputStream fis = new FileInputStream(fcFile);
 420             props.load(fis);
 421             fis.close();
 422         } catch (IOException e) {
 423             if (SunGraphicsEnvironment.debugFonts) {
 424                 Logger logger = Logger.getLogger("sun.awt.FontConfiguration");
 425                 logger.warning("IOException reading from "+fcFile.toString());
 426             }
 427             return;
 428         }
 429         String version = (String)props.get("version");
 430         if (version == null || !version.equals(fileVersion)) {
 431             return;
 432         }
 433 
 434         // If there's a new, different fontconfig installed on the
 435         // system, we invalidate our fontconfig file.
 436         String fcVersionStr = (String)props.get("fcversion");
 437         if (fcVersionStr != null) {
 438             int fcVersion;
 439             try {
 440                 fcVersion = Integer.parseInt(fcVersionStr);
 441                 if (fcVersion != 0 &&
 442                     fcVersion != FontManager.getFontConfigVersion()) {
 443                     return;
 444                 }
 445             } catch (Exception e) {
 446                 if (SunGraphicsEnvironment.debugFonts) {
 447                     Logger logger =
 448                         Logger.getLogger("sun.awt.FontConfiguration");
 449                     logger.warning("Exception parsing version " +
 450                                    fcVersionStr);
 451                 }
 452                 return;
 453             }
 454         }
 455 
 456         // If we can locate the fontconfig cache dirs, then compare the
 457         // time stamp of those with our properties file. If we are out
 458         // of date then re-generate.
 459         long lastModified = fcFile.lastModified();
 460         int cacheDirIndex = 0;
 461         while (cacheDirIndex<4) { // should never be more than 2 anyway.
 462             String dir = (String)props.get("cachedir."+cacheDirIndex);
 463             if (dir == null) {
 464                 break;
 465             }
 466             File dirFile = new File(dir);
 467             if (dirFile.exists() && dirFile.lastModified() > lastModified) {
 468                 return;
 469             }
 470             cacheDirIndex++;
 471         }
 472 
 473         String[] names = { "sansserif", "serif", "monospaced" };
 474         String[] fcnames = { "sans", "serif", "monospace" };
 475         int namesLen = names.length;
 476         int numStyles = 4;
 477         FcCompFont[] fci = new FcCompFont[namesLen*numStyles];
 478 
 479         try {
 480             for (int i=0; i<namesLen; i++) {
 481                 for (int s=0; s<numStyles; s++) {
 482                     int index = i*numStyles+s;
 483                     fci[index] = new FcCompFont();
 484                     String key = names[i]+"."+s;
 485                     fci[index].jdkName = names[i];
 486                     fci[index].fcFamily = fcnames[i];
 487                     fci[index].style = s;
 488                     String lenStr = (String)props.get(key+".length");
 489                     int nfonts = Integer.parseInt(lenStr);
 490                     if (nfonts <= 0) {
 491                         return; // bad file
 492                     }
 493                     fci[index].allFonts = new FontConfigFont[nfonts];
 494                     for (int f=0; f<nfonts; f++) {
 495                         fci[index].allFonts[f] = new FontConfigFont();
 496                         String fkey = key+"."+f+".family";
 497                         String family = (String)props.get(fkey);
 498                         fci[index].allFonts[f].familyName = family;
 499                         fkey = key+"."+f+".file";
 500                         String file = (String)props.get(fkey);
 501                         if (file == null) {
 502                             return; // bad file
 503                         }
 504                         fci[index].allFonts[f].fontFile = file;
 505                     }
 506                     fci[index].firstFont =  fci[index].allFonts[0];
 507 
 508                 }
 509             }
 510             fcCompFonts = fci;
 511         } catch (Throwable t) {
 512             if (SunGraphicsEnvironment.debugFonts) {
 513                 Logger logger = Logger.getLogger("sun.awt.FontConfiguration");
 514                 logger.warning(t.toString());
 515             }
 516         }
 517     }
 518 }