/* | |
* ****************************************************************************** | |
* Copyright (C) 2007, International Business Machines Corporation and others. | |
* All Rights Reserved. | |
* ****************************************************************************** | |
*/ | |
package com.ibm.icu.dev.tool.tzu; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.OutputStream; | |
import java.io.PrintStream; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
import java.net.MalformedURLException; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
import java.util.Enumeration; | |
import java.util.Iterator; | |
import java.util.MissingResourceException; | |
import java.util.jar.Attributes; | |
import java.util.jar.JarEntry; | |
import java.util.jar.JarFile; | |
import java.util.jar.JarOutputStream; | |
import java.util.jar.Manifest; | |
/** | |
* A class that represents an updatable ICU4J jar file. A file is an updatable | |
* ICU4J jar file if it | |
* <ul> | |
* <li>exists</li> | |
* <li>is a file (ie. not a directory)</li> | |
* <li>does not end with .ear or .war (these file types are unsupported)</li> | |
* <li>ends with .jar</li> | |
* <li>is updatable according the <code>isUpdatable</code></li> | |
* <li>is not signed.</li> | |
* </ul> | |
*/ | |
public class ICUFile { | |
/** | |
* ICU version to use if one cannot be found. | |
*/ | |
public static final String ICU_VERSION_UNKNOWN = "Unknown"; | |
/** | |
* A directory entry that is found in every updatable ICU4J jar file. | |
*/ | |
public static final String TZ_ENTRY_DIR = "com/ibm/icu/impl"; | |
/** | |
* Prefix of the timezone resource filename. | |
*/ | |
public static final String TZ_ENTRY_FILENAME_PREFIX = "zoneinfo"; | |
/** | |
* Extension of the timezone resource filename. | |
*/ | |
public static final String TZ_ENTRY_FILENAME_EXTENSION = ".res"; | |
/** | |
* The timezone resource filename. | |
*/ | |
public static final String TZ_ENTRY_FILENAME = TZ_ENTRY_FILENAME_PREFIX | |
+ TZ_ENTRY_FILENAME_EXTENSION; | |
/** | |
* Key to use when getting the version of a timezone resource. | |
*/ | |
public static final String TZ_VERSION_KEY = "TZVersion"; | |
/** | |
* Timezone version to use if one cannot be found. | |
*/ | |
public static final String TZ_VERSION_UNKNOWN = "Unknown"; | |
/** | |
* The buffer size to use for copying data. | |
*/ | |
private static final int BUFFER_SIZE = 1024; | |
/** | |
* Determines the version of a timezone resource as a standard file without | |
* locking the file. | |
* | |
* @param tzFile | |
* The file representing the timezone resource. | |
* @param logger | |
* The current logger. | |
* @return The version of the timezone resource. | |
*/ | |
public static String findFileTZVersion(File tzFile, Logger logger) { | |
ICUFile rawTZFile = new ICUFile(logger); | |
try { | |
File temp = File.createTempFile("zoneinfo", ".res"); | |
temp.deleteOnExit(); | |
rawTZFile.copyFile(tzFile, temp); | |
return findTZVersion(temp, logger); | |
} catch (IOException ex) { | |
logger.errorln(ex.getMessage()); | |
return null; | |
} | |
} | |
/** | |
* Determines the version of a timezone resource as a standard file, but | |
* locks the file for the duration of the program. | |
* | |
* @param tzFile | |
* The file representing the timezone resource. | |
* @param logger | |
* The current logger. | |
* @return The version of the timezone resource. | |
*/ | |
private static String findTZVersion(File tzFile, Logger logger) { | |
try { | |
String filename = tzFile.getName(); | |
String entryname = filename.substring(0, filename.length() | |
- ".res".length()); | |
URL url = new URL(tzFile.getAbsoluteFile().getParentFile().toURL() | |
.toString()); | |
ClassLoader loader = new URLClassLoader(new URL[] { url }); | |
// UResourceBundle bundle = UResourceBundle.getBundleInstance("", | |
// entryname, loader); | |
URL bundleURL = new URL(new File("icu4j.jar").toURL().toString()); | |
URLClassLoader bundleLoader = new URLClassLoader( | |
new URL[] { bundleURL }); | |
Class bundleClass = bundleLoader | |
.loadClass("com.ibm.icu.util.UResourceBundle"); | |
Method bundleGetInstance = bundleClass.getMethod( | |
"getBundleInstance", new Class[] { String.class, | |
String.class, ClassLoader.class }); | |
Object bundle = bundleGetInstance.invoke(null, new Object[] { "", | |
entryname, loader }); | |
if (bundle != null) { | |
Method bundleGetString = bundleClass.getMethod("getString", | |
new Class[] { String.class }); | |
String tzVersion = (String) bundleGetString.invoke(bundle, | |
new Object[] { TZ_VERSION_KEY }); | |
if (tzVersion != null) | |
return tzVersion; | |
} | |
} catch (MalformedURLException ex) { | |
// this should never happen | |
ex.printStackTrace(); | |
} catch (ClassNotFoundException ex) { | |
// this would most likely happen when UResourceBundle cannot be | |
// resolved, which is when icu4j.jar is not where it should be | |
logger.errorln("icu4j.jar not found"); | |
} catch (NoSuchMethodException ex) { | |
// this can only be caused by a very unlikely scenario | |
ex.printStackTrace(); | |
} catch (IllegalAccessException ex) { | |
// this can only be caused by a very unlikely scenario | |
ex.printStackTrace(); | |
} catch (InvocationTargetException ex) { | |
// if this is holding a MissingResourceException, then this is not | |
// an error -- some zoneinfo files do not have a version number | |
if (!(ex.getTargetException() instanceof MissingResourceException)) | |
ex.printStackTrace(); | |
} | |
return TZ_VERSION_UNKNOWN; | |
} | |
/** | |
* Finds the jar entry in the jar file that represents a timezone resource | |
* and returns it, or null if none is found. | |
* | |
* @param jar | |
* The jar file to search. | |
* @return The jar entry representing the timezone resource in the jar file, | |
* or null if none is found. | |
*/ | |
private static JarEntry getTZEntry(JarFile jar) { | |
JarEntry tzEntry = null; | |
Enumeration e = jar.entries(); | |
while (e.hasMoreElements()) { | |
tzEntry = (JarEntry) e.nextElement(); | |
if (tzEntry.getName().endsWith(TZ_ENTRY_FILENAME)) | |
return tzEntry; | |
} | |
return null; | |
} | |
/** | |
* The ICU4J jar file represented by this ICUFile. | |
*/ | |
private File icuFile; | |
/** | |
* The ICU version of the ICU4J jar. | |
*/ | |
private String icuVersion; | |
/** | |
* The current logger. | |
*/ | |
private Logger logger; | |
/** | |
* The entry for the timezone resource inside the ICU4J jar. | |
*/ | |
private JarEntry tzEntry; | |
/** | |
* The version of the timezone resource inside the ICU4J jar. | |
*/ | |
private String tzVersion; | |
/** | |
* Constructs an ICUFile around a file. See <code>initialize</code> for | |
* details. | |
* | |
* @param file | |
* The file to wrap this ICUFile around. | |
* @param logger | |
* The current logger. | |
* @throws IOException | |
*/ | |
public ICUFile(File file, Logger logger) throws IOException { | |
initialize(file, logger); | |
} | |
/** | |
* Constructs an ICUFile around a file. See <code>initialize</code> for | |
* details. | |
* | |
* @param filename | |
* The file to wrap this ICUFile around. | |
* @param logger | |
* The current logger. | |
* @throws IOException | |
*/ | |
public ICUFile(String filename, Logger logger) throws IOException { | |
if (filename == null || filename.trim().length() == 0) | |
throw new IOException("cannot be blank"); | |
initialize(new File(filename), logger); | |
} | |
/** | |
* Constructs a blank ICUFile. Used internally for timezone resource files | |
* that are not contained within a jar. | |
* | |
* @param logger | |
* The current logger. | |
*/ | |
private ICUFile(Logger logger) { | |
this.logger = logger; | |
} | |
/** | |
* Compares two ICUFiles by the file they represent. | |
* | |
* @param other | |
* The other ICUFile to compare to. | |
* @return Whether the files represented by the two ICUFiles are equal. | |
*/ | |
public boolean equals(Object other) { | |
return (!(other instanceof ICUFile)) ? false : icuFile | |
.equals(((ICUFile) other).icuFile); | |
} | |
/** | |
* Determines the version of a timezone resource in a jar file without | |
* locking the jar file. | |
* | |
* @return The version of the timezone resource. | |
*/ | |
public String findEntryTZVersion() { | |
try { | |
File temp = File.createTempFile("zoneinfo", ".res"); | |
temp.deleteOnExit(); | |
copyEntry(icuFile, tzEntry, temp); | |
return findTZVersion(temp, logger); | |
} catch (IOException ex) { | |
logger.errorln(ex.getMessage()); | |
return null; | |
} | |
} | |
/** | |
* Returns the File object represented by this ICUFile object. | |
* | |
* @return The File object represented by this ICUFile object. | |
*/ | |
public File getFile() { | |
return icuFile; | |
} | |
/** | |
* Returns the filename of this ICUFile object, without the path. | |
* | |
* @return The filename of this ICUFile object, without the path. | |
*/ | |
public String getFilename() { | |
return icuFile.getName(); | |
} | |
/** | |
* Returns the ICU version of this ICU4J jar. | |
* | |
* @return The ICU version of this ICU4J jar. | |
*/ | |
public String getICUVersion() { | |
return icuVersion; | |
} | |
/** | |
* Returns the path of this ICUFile object, without the filename. | |
* | |
* @return The path of this ICUFile object, without the filename. | |
*/ | |
public String getPath() { | |
return icuFile.getAbsoluteFile().getParent(); | |
} | |
// public static String findURLTZVersion(File tzFile) { | |
// try { | |
// File temp = File.createTempFile("zoneinfo", ".res"); | |
// temp.deleteOnExit(); | |
// copyFile(tzFile, temp); | |
// return findTZVersion(temp); | |
// } catch (IOException ex) { | |
// ex.printStackTrace(); | |
// return null; | |
// } | |
// } | |
/** | |
* Returns the timezone resource version. | |
* | |
* @return The timezone resource version. | |
*/ | |
public String getTZVersion() { | |
return tzVersion; | |
} | |
/** | |
* Returns the result of getFile().toString(). | |
* | |
* @return The result of getFile().toString(). | |
*/ | |
public String toString() { | |
return getFile().toString(); | |
} | |
/** | |
* Updates the timezone resource in this ICUFile using | |
* <code>insertURL</code> as the source of the new timezone resource and | |
* the backup directory <code>backupDir</code> to store a copy of the | |
* ICUFile. | |
* | |
* @param insertURL | |
* The url location of the timezone resource to use. | |
* @param backupDir | |
* The directory to store a backup for this ICUFile, or null if | |
* no backup. | |
* @throws IOException | |
* @throws InterruptedException | |
*/ | |
public void update(URL insertURL, File backupDir) throws IOException, | |
InterruptedException { | |
String message = "Updating " + icuFile.toString() + " ..."; | |
logger.printlnToBoth(""); | |
logger.printlnToBoth(message); | |
logger.setStatus(message); | |
if (!icuFile.canRead() || !icuFile.canWrite()) | |
throw new IOException("Missing permissions for " + icuFile); | |
File backupFile = null; | |
if ((backupFile = createBackupFile(icuFile, backupDir)) == null) | |
throw new IOException("Failed to create a backup file."); | |
if (!copyFile(icuFile, backupFile)) | |
throw new IOException("Could not replace the original jar."); | |
if (!createUpdatedJar(backupFile, icuFile, tzEntry, insertURL)) | |
throw new IOException("Could not create an updated jar."); | |
// get the new timezone resource version | |
tzVersion = findEntryTZVersion(); | |
message = "Successfully updated " + icuFile.toString(); | |
logger.printlnToBoth(message); | |
logger.setStatus(message); | |
} | |
/** | |
* Copies the jar entry <code>insertEntry</code> in <code>inputFile</code> | |
* to <code>outputFile</code>. | |
* | |
* @param inputFile | |
* The jar file containing <code>insertEntry</code>. | |
* @param inputEntry | |
* The entry to copy. | |
* @param outputFile | |
* The output file. | |
* @return Whether the operation was successful. | |
*/ | |
private boolean copyEntry(File inputFile, JarEntry inputEntry, | |
File outputFile) { | |
logger.loglnToBoth("Copying from " + inputFile + "!/" + inputEntry | |
+ " to " + outputFile + "."); | |
JarFile jar = null; | |
InputStream istream = null; | |
OutputStream ostream = null; | |
byte[] buffer = new byte[BUFFER_SIZE]; | |
int bytesRead; | |
boolean success = false; | |
try { | |
jar = new JarFile(inputFile); | |
istream = jar.getInputStream(inputEntry); | |
ostream = new FileOutputStream(outputFile); | |
while ((bytesRead = istream.read(buffer)) != -1) | |
ostream.write(buffer, 0, bytesRead); | |
success = true; | |
logger.loglnToBoth("Copy successful."); | |
} catch (IOException ex) { | |
outputFile.delete(); | |
logger.loglnToBoth("Copy failed."); | |
} finally { | |
// safely close the streams | |
if (jar != null) | |
try { | |
jar.close(); | |
} catch (IOException ex) { | |
} | |
if (istream != null) | |
try { | |
istream.close(); | |
} catch (IOException ex) { | |
} | |
if (ostream != null) | |
try { | |
ostream.close(); | |
} catch (IOException ex) { | |
} | |
} | |
return success; | |
} | |
/** | |
* Copies <code>inputFile</code> to <code>outputFile</code>. | |
* | |
* @param inputFile | |
* The input file. | |
* @param outputFile | |
* The output file. | |
* @return Whether the operation was successful. | |
*/ | |
private boolean copyFile(File inputFile, File outputFile) { | |
logger.loglnToBoth("Copying from " + inputFile + " to " + outputFile | |
+ "."); | |
InputStream istream = null; | |
OutputStream ostream = null; | |
byte[] buffer = new byte[BUFFER_SIZE]; | |
int bytesRead; | |
boolean success = false; | |
try { | |
istream = new FileInputStream(inputFile); | |
ostream = new FileOutputStream(outputFile); | |
while ((bytesRead = istream.read(buffer)) != -1) | |
ostream.write(buffer, 0, bytesRead); | |
success = true; | |
logger.loglnToBoth("Copy successful."); | |
} catch (IOException ex) { | |
outputFile.delete(); | |
logger.loglnToBoth("Copy failed."); | |
} finally { | |
// safely close the streams | |
if (istream != null) | |
try { | |
istream.close(); | |
} catch (IOException ex) { | |
} | |
if (ostream != null) | |
try { | |
ostream.close(); | |
} catch (IOException ex) { | |
} | |
} | |
return success; | |
} | |
/** | |
* Creates a temporary file for the jar file <code>inputFile</code> under | |
* the directory <code>backupBase</code> and returns it, or returns null | |
* if a temporary file could not be created. Does not put any data in the | |
* newly created file yet. | |
* | |
* @param inputFile | |
* The file to backup. | |
* @param backupBase | |
* The directory where backups are to be stored. | |
* @return The temporary file that was created. | |
*/ | |
private File createBackupFile(File inputFile, File backupBase) { | |
logger.loglnToBoth("Creating backup file for + " + inputFile + " at " | |
+ backupBase + "."); | |
String filename = inputFile.getName(); | |
String suffix = ".jar"; | |
String prefix = filename.substring(0, filename.length() | |
- ".jar".length()); | |
if (backupBase == null) { | |
try { | |
File backupFile = File.createTempFile(prefix, suffix); | |
backupFile.deleteOnExit(); | |
return backupFile; | |
} catch (IOException ex) { | |
return null; | |
} | |
} | |
File backupFile = null; | |
File backupDesc = null; | |
File backupDir = new File(backupBase.getPath() + File.separator | |
+ prefix); | |
PrintStream ostream = null; | |
try { | |
backupBase.mkdir(); | |
backupDir.mkdir(); | |
backupFile = File.createTempFile(prefix, suffix, backupDir); | |
backupDesc = new File(backupDir.getPath() + File.separator + prefix | |
+ ".txt"); | |
backupDesc.createNewFile(); | |
ostream = new PrintStream(new FileOutputStream(backupDesc)); | |
ostream.println(inputFile.toString()); | |
logger.loglnToBoth("Successfully created backup file at " | |
+ backupFile + "."); | |
} catch (IOException ex) { | |
logger.loglnToBoth("Failed to create backup file."); | |
if (backupFile != null) | |
backupFile.delete(); | |
if (backupDesc != null) | |
backupDesc.delete(); | |
backupDir.delete(); | |
backupFile = null; | |
} finally { | |
if (ostream != null) | |
ostream.close(); | |
} | |
return backupFile; | |
} | |
/** | |
* Copies <code>inputFile</code> to <code>outputFile</code>, replacing | |
* <code>insertEntry</code> with <code>inputURL</code>. | |
* | |
* @param inputFile | |
* The input jar file. | |
* @param outputFile | |
* The output jar file. | |
* @param insertEntry | |
* The entry to be replaced. | |
* @param inputURL | |
* The URL to use in replacing the entry. | |
* @return Whether the operation was successful. | |
*/ | |
private boolean createUpdatedJar(File inputFile, File outputFile, | |
JarEntry insertEntry, URL inputURL) { | |
logger.loglnToBoth("Copying " + inputFile + " to " + outputFile | |
+ ", replacing " + insertEntry + " with " + inputURL + "."); | |
JarFile jar = null; | |
JarOutputStream ostream = null; | |
InputStream istream = null; | |
InputStream jstream = null; | |
byte[] buffer = new byte[BUFFER_SIZE]; | |
int bytesRead; | |
boolean success = false; | |
try { | |
jar = new JarFile(inputFile); | |
ostream = new JarOutputStream(new FileOutputStream(outputFile)); | |
istream = inputURL.openStream(); | |
Enumeration e = jar.entries(); | |
while (e.hasMoreElements()) { | |
JarEntry currentEntry = (JarEntry) e.nextElement(); | |
if (!currentEntry.getName().equals(insertEntry.getName())) { | |
// if the current entry isn't the one that needs updating | |
// write a copy of the old entry from the old file | |
ostream.putNextEntry(new JarEntry(currentEntry.getName())); | |
jstream = jar.getInputStream(currentEntry); | |
while ((bytesRead = jstream.read(buffer)) != -1) | |
ostream.write(buffer, 0, bytesRead); | |
jstream.close(); | |
} else { | |
// if the current entry *is* the one that needs updating | |
// write a new entry based on the input stream (from the | |
// URL) | |
// currentEntry.setTime(System.currentTimeMillis()); | |
ostream.putNextEntry(new JarEntry(currentEntry.getName())); | |
while ((bytesRead = istream.read(buffer)) != -1) | |
ostream.write(buffer, 0, bytesRead); | |
} | |
} | |
success = true; | |
logger.loglnToBoth("Copy successful."); | |
} catch (IOException ex) { | |
outputFile.delete(); | |
logger.loglnToBoth("Copy failed."); | |
} finally { | |
// safely close the streams | |
if (istream != null) | |
try { | |
istream.close(); | |
} catch (IOException ex) { | |
} | |
if (ostream != null) | |
try { | |
ostream.close(); | |
} catch (IOException ex) { | |
} | |
if (jstream != null) | |
try { | |
jstream.close(); | |
} catch (IOException ex) { | |
} | |
if (jar != null) | |
try { | |
jar.close(); | |
} catch (IOException ex) { | |
} | |
} | |
return success; | |
} | |
/** | |
* Performs the shared work of the constructors. Throws an IOException if | |
* <code>file</code>... | |
* <ul> | |
* <li>does not exist</li> | |
* <li>is not a file</li> | |
* <li>ends with .ear or .war (these file types are unsupported)</li> | |
* <li>does not end with .jar</li> | |
* <li>is not updatable according the <code>isUpdatable</code></li> | |
* <li>is signed.</li> | |
* </ul> | |
* If an exception is not thrown, the ICUFile is fully initialized. | |
* | |
* @param file | |
* The file to wrap this ICUFile around. | |
* @param logger | |
* The current logger. | |
* @throws IOException | |
*/ | |
private void initialize(File file, Logger log) throws IOException { | |
this.icuFile = file; | |
this.logger = log; | |
String message = null; | |
if (!file.exists()) { | |
message = "Skipped " + file.getPath() + " (does not exist)."; | |
} else if (!file.isFile()) { | |
message = "Skipped " + file.getPath() + " (not a file)."; | |
} else if (file.getName().endsWith(".ear") | |
|| file.getName().endsWith(".war")) { | |
message = "Skipped " + file.getPath() | |
+ " (this tool does not support .ear and .war files)."; | |
logger.loglnToBoth(message); | |
logger.showInformationDialog(message); | |
} else if (!file.getName().endsWith(".jar")) { | |
message = "Skipped " + file.getPath() + " (not a jar file)."; | |
} else if (!isUpdatable()) { | |
message = "Skipped " + file.getPath() | |
+ " (not an updatable ICU4J jar)."; | |
} else if (isSigned()) { | |
message = "Skipped " + file.getPath() | |
+ " (cannot update signed jars)."; | |
logger.loglnToBoth(message); | |
logger.showInformationDialog(message); | |
} else if (isEclipseFragment()) { | |
message = "Skipped " + file.getPath() | |
+ " (eclipse fragments must be updated through ICU)."; | |
logger.loglnToBoth(message); | |
logger.showInformationDialog(message); | |
} | |
if (message != null) | |
throw new IOException(message); | |
tzVersion = findEntryTZVersion(); | |
} | |
/** | |
* Determines whether the current jar is an Eclipse Data Fragment. | |
* | |
* @return Whether the current jar is an Eclipse Fragment. | |
*/ | |
private boolean isEclipseDataFragment() { | |
return (icuFile.getPath().indexOf( | |
"plugins" + File.separator + "com.ibm.icu.data.update") >= 0 && icuFile | |
.getName().equals("icu-data.jar")); | |
} | |
/** | |
* Determines whether the current jar is an Eclipse Fragment. | |
* | |
* @return Whether the current jar is an Eclipse Fragment. | |
*/ | |
private boolean isEclipseFragment() { | |
return (isEclipseDataFragment() || isEclipseMainFragment()); | |
} | |
/** | |
* Determines whether the current jar is an Eclipse Main Fragment. | |
* | |
* @return Whether the current jar is an Eclipse Fragment. | |
*/ | |
private boolean isEclipseMainFragment() { | |
return (icuFile.getPath().indexOf("plugins") >= 0 && icuFile.getName() | |
.startsWith("com.ibm.icu_")); | |
} | |
/** | |
* Determines whether a timezone resource in a jar file is signed. | |
* | |
* @return Whether a timezone resource in a jar file is signed. | |
*/ | |
private boolean isSigned() { | |
return tzEntry.getCertificates() != null; | |
} | |
/** | |
* Gathers information on the jar file represented by this ICUFile object | |
* and returns whether it is an updatable ICU4J jar file. | |
* | |
* @return Whether the jar file represented by this ICUFile object is an | |
* updatable ICU4J jar file. | |
*/ | |
private boolean isUpdatable() { | |
JarFile jar = null; | |
boolean success = false; | |
try { | |
// open icuFile as a jar file | |
jar = new JarFile(icuFile); | |
// get its manifest to determine the ICU version | |
Manifest manifest = jar.getManifest(); | |
icuVersion = ICU_VERSION_UNKNOWN; | |
if (manifest != null) { | |
Iterator iter = manifest.getEntries().values().iterator(); | |
while (iter.hasNext()) { | |
Attributes attr = (Attributes) iter.next(); | |
String ver = attr | |
.getValue(Attributes.Name.IMPLEMENTATION_VERSION); | |
if (ver != null) { | |
icuVersion = ver; | |
break; | |
} | |
} | |
} | |
// if the jar's directory structure contains TZ_ENTRY_DIR and there | |
// is a timezone resource in the jar, then the jar is updatable | |
success = (jar.getJarEntry(TZ_ENTRY_DIR) != null) | |
&& ((this.tzEntry = getTZEntry(jar)) != null); | |
} catch (IOException ex) { | |
// unable to create the JarFile or unable to get the Manifest | |
// log the unexplained i/o error, but we must drudge on | |
logger.loglnToBoth("Error reading " + icuFile.getPath() + "."); | |
} finally { | |
// close the jar gracefully | |
if (jar != null) | |
try { | |
jar.close(); | |
} catch (IOException ex) { | |
} | |
} | |
// return whether the jar is updatable or not | |
return success; | |
} | |
} |