blob: a38207f8eb40d3278db66484385b7ab05b1eb42c [file] [log] [blame]
/*
* ******************************************************************************
* 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;
}
}