Startup: Add migration tool to migrate all Jetty xml files
Some checks failed
Java CI / build (push) Has been cancelled
Java CI / javadoc-latest (push) Has been cancelled
Java CI / build-java7 (push) Has been cancelled
Java with IzPack Snapshot Setup / setup (push) Has been cancelled

referenced in clients.config command lines.
This changes Ref id to Ref refid, and updates the DTD.
Previous changes for this were for new installs only,
this handles existing files in the eepsite dirs.

This builds on the existing MigrateJetty class,
while removing the ancient Jetty 5-8 to 9 migration code.

The only way that would have been used these days is
if somebody tried to update directly from 0.9.29 or earlier
(8 years ago) to the current release.
With this change, we tell the user to uninstall and reinstall.
This commit is contained in:
zzz
2025-04-26 14:40:29 -04:00
parent 508fe2ce52
commit 8b2a01193b
3 changed files with 123 additions and 335 deletions

View File

@ -154,8 +154,10 @@ public class ClientAppConfig {
try {
List<ClientAppConfig> cacs = getClientApps(cf);
if (!cacs.isEmpty()) {
// Jetty 5/6/7/8 to 9 migration
if (!SystemVersion.isAndroid())
MigrateJetty.migrate(ctx, cacs);
// clients.config to clients.config.d migration
boolean ok = migrate(ctx, cacs, cf, dir);
if (!ok)
rv.addAll(cacs);
@ -188,6 +190,9 @@ public class ClientAppConfig {
System.out.println("Error loading the client app properties from " + f + ' ' + ioe);
}
}
// Jetty id to refid migration
if (!rv.isEmpty() && !SystemVersion.isAndroid())
MigrateJetty.migrate(ctx, rv);
}
}
return rv;

View File

@ -22,6 +22,16 @@ import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.VersionComparator;
/**
* Second migration, as of 2.9.0:
*<p>
* Migrate all the jetty*.xml files to change
* Ref id= to Ref refid= because dup ids is a fatal error.
* Also migrate the old configure.dtd to configure_9_3.dtd.
* Reference: https://github.com/jetty/jetty.project/issues/12881
*</p>
*
* First migration, as of 0.9.30:
*<p>
* Migrate the clients.config and jetty.xml files
* from Jetty 5/6 to Jetty 7/8.
* Also migrate jetty.xml from Jetty 7/8 to Jetty 9.
@ -43,6 +53,7 @@ import net.i2p.util.VersionComparator;
*
* Does NOT preserve port number, thread counts, etc. in the migration to 7/8.
* DOES preserve everything in the migration to 9.
*</p>
*
* @since Jetty 6
*/
@ -56,41 +67,57 @@ abstract class MigrateJetty {
private static final String TEST_CLASS = "org.eclipse.jetty.server.Server";
private static final String BACKUP_SUFFIX = ".jetty6";
private static final String BACKUP_SUFFIX_8 = ".jetty8";
private static final String BACKUP_SUFFIX_9 = ".jetty9-id";
private static final String JETTY_TEMPLATE_DIR = "eepsite-jetty9";
private static final String JETTY_TEMPLATE_PKGDIR = "eepsite";
private static final String BASE_CONTEXT = "contexts/base-context.xml";
private static final String CGI_CONTEXT = "contexts/cgi-context.xml";
private static final String PROP_JETTY9_MIGRATED = "router.startup.jetty9.migrated";
private static final String PROP_JETTY9_MIGRATED_2 = "router.startup.jetty-ids.migrated";
/**
* For each entry in apps, if the main class is an old Jetty class,
* migrate it to the new Jetty class, and update the Jetty config files.
*/
public static void migrate(RouterContext ctx, List<ClientAppConfig> apps) {
if (ctx.getBooleanProperty(PROP_JETTY9_MIGRATED))
if (ctx.getBooleanProperty(PROP_JETTY9_MIGRATED_2))
return;
String installed = ctx.getProperty("router.firstVersion");
if (installed != null && VersionComparator.comp(installed, "0.9.30") >= 0) {
ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
if (installed != null && VersionComparator.comp(installed, "2.9.0") >= 0) {
ctx.router().saveConfig(PROP_JETTY9_MIGRATED_2, "true");
return;
}
boolean shouldSave = false;
boolean jetty9success = false;
boolean migrated1 = ctx.getBooleanProperty(PROP_JETTY9_MIGRATED);
if (!migrated1 && installed != null && VersionComparator.comp(installed, "0.9.30") >= 0) {
ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
migrated1 = true;
}
boolean migration2success = false;
for (int i = 0; i < apps.size(); i++) {
ClientAppConfig app = apps.get(i);
String client;
String backupSuffix;
if (app.className.equals(NEW_CLASS)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 7/8 to Jetty 9";
backupSuffix = BACKUP_SUFFIX_8;
} else if (app.className.equals(OLD_CLASS) || app.className.equals(OLD_CLASS_6)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 5/6 " + app.className +
" to Jetty 9 " + NEW_CLASS;
backupSuffix = BACKUP_SUFFIX;
if (migrated1) {
if (app.className.equals(NEW_CLASS)) {
client = "client application " + i + " [" + app.clientName +
"] to fix DTDs and duplicate ids";
backupSuffix = BACKUP_SUFFIX_9;
} else {
continue;
}
} else {
continue;
if (app.className.equals(NEW_CLASS)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 7/8 to Jetty 9";
backupSuffix = BACKUP_SUFFIX_8;
} else if (app.className.equals(OLD_CLASS) || app.className.equals(OLD_CLASS_6)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 5/6 " + app.className +
" to Jetty 9 " + NEW_CLASS;
backupSuffix = BACKUP_SUFFIX;
} else {
continue;
}
}
if (!hasLatestJetty()) {
System.err.println("WARNING: Jetty 7 unavailable, cannot migrate " + client);
@ -102,333 +129,62 @@ abstract class MigrateJetty {
String args[] = LoadClientAppsJob.parseArgs(app.args);
if (args.length == 0)
continue;
String xml = args[0];
File xmlFile = new File(xml);
if (!xmlFile.isAbsolute())
xmlFile = new File(ctx.getAppDir(), xml);
if (!xmlFile.exists()) {
System.err.println("WARNING: XML file " + xmlFile +
" not found, cannot migrate " + client);
if (!migrated1) {
// migration from 0.9.29 or earlier (2017-02-27) straight to 2.9.0 or later
System.err.println("WARNING: Unable to migrate " + client +
", delete client or uninstall and reinstall I2P");
app.disabled = true;
continue;
}
File eepsite = xmlFile.getParentFile();
boolean ok = backupFile(xmlFile, backupSuffix);
if (!ok) {
System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
if (app.className.equals(NEW_CLASS)) {
// Do the migration of 8 to 9, handle additional command-line xml files too
for (int j = 0; j < args.length; j++) {
if (j > 0) {
// probably jetty-ssl.xml
xmlFile = new File(args[j]);
ok = backupFile(xmlFile, backupSuffix);
if (!ok) {
System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
}
boolean ok9 = migrateToJetty9(xmlFile);
if (ok9) {
System.err.println("WARNING: Migrated " + client + ".\n" +
"Check the " + xmlFile.getName() + " file in " + eepsite + ".\n" +
"Your old " + xmlFile.getName() + " file was backed up to " + xmlFile.getAbsolutePath() + BACKUP_SUFFIX_8);
jetty9success = true;
}
System.err.println("Migrating " + client);
// migration 2 below here
// Note that JettyStart automatically copies and adds jetty-gzip.xml
// to the command line, not in the arg list here,
// but it does not contain anything we need to fix.
for (String xml : args) {
if (!xml.endsWith(".xml"))
continue;
File xmlFile = new File(xml);
if (!xmlFile.isAbsolute())
xmlFile = new File(ctx.getAppDir(), xml);
if (!xmlFile.exists()) {
System.err.println("WARNING: XML file " + xmlFile +
" not found, cannot migrate " + client);
continue;
}
continue;
}
// Below here is migration of 5/6 to 9
File baseEep = new File(ctx.getBaseDir(), JETTY_TEMPLATE_DIR);
// in packages, or perhaps on an uninstall/reinstall, the files are in eepsite/
if (!baseEep.exists())
baseEep = new File(ctx.getBaseDir(), JETTY_TEMPLATE_PKGDIR);
if (baseEep.equals(eepsite)) {
// non-split directory yet not an upgrade? shouldn't happen
System.err.println("Eepsite in non-split directory " + eepsite +
", cannot migrate " + client);
continue;
}
// jetty.xml existed before in jetty 5 version, so check this new file
// and if it doesn't exist we can't continue
File baseContext = new File(baseEep, BASE_CONTEXT);
if (!baseContext.exists()) {
System.err.println("WARNING: Cannot find new XML file template " + baseContext +
", cannot migrate " + client);
continue;
}
String newPath = eepsite.getAbsolutePath() + File.separatorChar;
ok = WorkingDir.migrateJettyXml(baseEep, eepsite, "jetty.xml", "./eepsite/", newPath);
if (!ok) {
System.err.println("WARNING: Failed to modify XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
// now we're committed, so don't check any more failure codes
backupAndMigrateFile(baseEep, eepsite, "jetty-ssl.xml", "./eepsite/", newPath);
(new File(eepsite, "contexts")).mkdir();
// ContextProvider scanner only looks for files ending in .xml so we can
// back up to the same directory
backupAndMigrateFile(baseEep, eepsite, BASE_CONTEXT, "./eepsite/", newPath);
backupAndMigrateFile(baseEep, eepsite, CGI_CONTEXT, "./eepsite/", newPath);
backupAndCopyFile(baseEep, eepsite, "jetty-rewrite.xml");
(new File(eepsite, "etc")).mkdir();
// realm.properties: No change from 6 to 7
File to = new File(eepsite, "etc/realm.properties");
if (!to.exists())
WorkingDir.copyFile(new File(baseEep, "etc/realm.properties"), to);
backupAndCopyFile(baseEep, eepsite, "etc/webdefault.xml");
app.className = NEW_CLASS;
shouldSave = true;
System.err.println("WARNING: Migrated " + client + '\n' +
"Check the following files in " + eepsite +
": jetty.xml, " + BASE_CONTEXT + ", and " + CGI_CONTEXT + "\n" +
"Your old jetty.xml was backed up." + '\n' +
"If you modified your jetty.xml to change ports, thread limits, etc, you MUST\n" +
"edit it to change them again. Your port was reset to 7658.");
}
if (shouldSave) {
File cfgFile = ClientAppConfig.configFile(ctx);
boolean ok = backupFile(cfgFile);
if (ok) {
boolean ok = backupFile(xmlFile, backupSuffix);
if (!ok) {
System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
File tmpFile = new File(xmlFile + ".tmp");
try {
ClientAppConfig.writeClientAppConfig(ctx, apps);
System.err.println("WARNING: Migrated clients config file " + cfgFile +
" from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 +
" to Jetty 9 " + NEW_CLASS);
WorkingDir.migrateFileXML(xmlFile, tmpFile,
"<Ref id=", "<Ref refid=",
"/jetty/configure.dtd", "/jetty/configure_9_3.dtd");
ok = FileUtil.rename(tmpFile, xmlFile);
if (!ok)
throw new IOException();
} catch (IOException ioe) {
ok = false;
System.err.println("WARNING: Failed to migrate XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
migration2success = true;
}
if (!ok) {
System.err.println("WARNING: Failed to migrate clients config file " + cfgFile +
" from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 +
" to Jetty 9 " + NEW_CLASS);
}
System.err.println("Migrated " + client);
}
if (jetty9success)
ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
}
/**
* Migrate a jetty.xml file to Jetty 9.
* Unlike above, where we just migrate the new install file over for Jetty 9,
* here we modify the xml file in-place to preserve settings where possible.
*
* @return success
* @since Jetty 9
*/
private static boolean migrateToJetty9(File xmlFile) {
if (xmlFile.getName().equals("jetty-jmx.xml")) {
// This is lazy but nobody's using jmx, not worth the trouble
System.err.println("ERROR: Migration of " + xmlFile +
" file is not supported. Copy new file from $I2P/eepsite-jetty9/jetty-jmx.xml");
return false;
}
// we don't re-migrate from the template, we just add the
// necessary args for the QueuedThreadPool constructor in-place
// and fixup the renamed set call
boolean modified = false;
File eepsite = xmlFile.getParentFile();
File newFile = new File(eepsite, xmlFile.getName() + System.currentTimeMillis() + ".tmp");
FileInputStream in = null;
PrintWriter out = null;
try {
in = new FileInputStream(xmlFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
String s;
boolean foundQTP = false;
boolean foundSTP = false;
boolean foundETP = false;
boolean foundSCC = false;
boolean foundHC = false;
boolean foundSSCC = false;
while ((s = DataHelper.readLine(in)) != null) {
// readLine() doesn't strip \r
if (s.endsWith("\r"))
s = s.substring(0, s.length() - 1);
if (s.contains("Modified by I2P migration script for Jetty 9.") ||
s.contains("This configuration supports Jetty 9.") ||
s.contains("http://www.eclipse.org/jetty/configure_9_0.dtd")) {
if (!modified)
break;
// else we've modified it twice?
} else if (s.contains("org.eclipse.jetty.util.thread.QueuedThreadPool")) {
foundQTP = true;
} else if (foundQTP) {
if (!(s.contains("Modified by") || s.contains("<Arg type=\"int\">"))) {
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(" <Arg type=\"int\">20</Arg> <!-- maxThreads, overridden below -->");
out.println(" <Arg type=\"int\">3</Arg> <!-- minThreads, overridden below -->");
out.println(" <Arg type=\"int\">60000</Arg> <!-- maxIdleTimeMs, overridden below -->");
modified = true;
}
foundQTP = false;
}
if (s.contains("<Set name=\"maxIdleTimeMs\">")) {
// <Set name="maxIdleTimeMs">60000</Set>
s = s.replace("<Set name=\"maxIdleTimeMs\">", "<Set name=\"idleTimeout\">");
modified = true;
} else if (s.contains("<Set name=\"ThreadPool\">")) {
// <Set name="ThreadPool">, must be changed to constructor arg
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
s = s.replace("<Set name=\"ThreadPool\">", "<Arg>");
foundSTP = true;
modified = true;
} else if (foundSTP && !foundETP && s.contains("</Set>") && !s.contains("<Set")) {
// </Set> (close of <Set name="ThreadPool">)
// All the lines above have <Set>...</Set> on the same line, if they don't, this will break.
s = s.replace("</Set>", "</Arg>");
foundETP = true;
} else if (s.contains("org.eclipse.jetty.server.nio.SelectChannelConnector")) {
s = s.replace("org.eclipse.jetty.server.nio.SelectChannelConnector", "org.eclipse.jetty.server.ServerConnector");
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(s);
out.println(" <Arg><Ref id=\"Server\" /></Arg>");
out.println(" <Arg type=\"int\">1</Arg> <!-- number of acceptors -->");
out.println(" <Arg type=\"int\">0</Arg> <!-- default number of selectors -->");
out.println(" <Arg>");
out.println(" <Array type=\"org.eclipse.jetty.server.ConnectionFactory\"> <!-- varargs so we need an array -->");
out.println(" <Item>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConnectionFactory\">");
out.println(" <Arg>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConfiguration\">");
out.println(" <Set name=\"sendServerVersion\">false</Set>");
out.println(" <Set name=\"sendDateHeader\">true</Set>");
out.println(" </New>");
out.println(" </Arg>");
out.println(" </New>");
out.println(" </Item>");
out.println(" </Array>");
out.println(" </Arg>");
modified = true;
continue;
// SSL starts here
} else if (s.contains("org.eclipse.jetty.http.ssl.SslContextFactory")) {
s = s.replace("org.eclipse.jetty.http.ssl.SslContextFactory", "org.eclipse.jetty.util.ssl.SslContextFactory");
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(s);
// don't try to migrate from below, just generate a new list
out.println(" <Set name=\"ExcludeCipherSuites\">");
out.println(" <Array type=\"java.lang.String\">");
for (String ss : I2PSSLSocketFactory.EXCLUDE_CIPHERS) {
out.println(" <Item>" + ss + "</Item>");
}
out.println(" </Array>");
out.println(" </Set>");
out.println(" <Set name=\"ExcludeProtocols\">");
out.println(" <Array type=\"java.lang.String\">");
for (String ss : I2PSSLSocketFactory.EXCLUDE_PROTOCOLS) {
out.println(" <Item>" + ss + "</Item>");
}
out.println(" </Array>");
out.println(" </Set>");
modified = true;
continue;
} else if (s.contains("org.eclipse.jetty.server.ssl.SslSelectChannelConnector")) {
s = s.replace("org.eclipse.jetty.server.ssl.SslSelectChannelConnector", "org.eclipse.jetty.server.ServerConnector");
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(s);
out.println(" <Arg><Ref id=\"Server\" /></Arg>");
out.println(" <Arg type=\"int\">1</Arg> <!-- number of acceptors -->");
out.println(" <Arg type=\"int\">0</Arg> <!-- default number of selectors -->");
out.println(" <Arg>");
out.println(" <Array type=\"org.eclipse.jetty.server.ConnectionFactory\"> <!-- varargs so we need an array -->");
out.println(" <Item>");
out.println(" <New class=\"org.eclipse.jetty.server.SslConnectionFactory\">");
out.println(" <Arg><Ref id=\"sslContextFactory\" /></Arg>");
out.println(" <Arg>http/1.1</Arg>");
out.println(" </New>");
out.println(" </Item>");
out.println(" <Item>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConnectionFactory\">");
out.println(" <Arg>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConfiguration\">");
out.println(" <Set name=\"sendServerVersion\">false</Set>");
out.println(" <Set name=\"sendDateHeader\">true</Set>");
out.println(" </New>");
out.println(" </Arg>");
out.println(" </New>");
out.println(" </Item>");
out.println(" </Array>");
out.println(" </Arg>");
foundSSCC = true;
modified = true;
continue;
} else if (foundSSCC && s.contains("<Set name=\"ExcludeCipherSuites\">")) {
// delete the old ExcludeCipherSuites in this section
do {
s = DataHelper.readLine(in);
} while(s != null && !s.contains("</Set>"));
modified = true;
continue;
} else if (foundSSCC &&
s.contains("<Ref id=\"sslContextFactory\"")) {
// delete old one in this section, replaced above
modified = true;
continue;
} else if (s.contains("<Set name=\"KeyStore\">")) {
s = s.replace("<Set name=\"KeyStore\">", "<Set name=\"KeyStorePath\">");
modified = true;
} else if (s.contains("<Set name=\"TrustStore\">")) {
s = s.replace("<Set name=\"TrustStore\">", "<Set name=\"TrustStorePath\">");
modified = true;
// SSL ends here
} else if (s.contains("class=\"org.eclipse.jetty.deploy.providers.ContextProvider\">")) {
// WebAppProvider now also does what ContextProvider used to do
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
s = s.replace("class=\"org.eclipse.jetty.deploy.providers.ContextProvider\">", "class=\"org.eclipse.jetty.deploy.providers.WebAppProvider\">");
modified = true;
} else if (s.contains("<Set name=\"maxIdleTime\">")) {
s = s.replace("<Set name=\"maxIdleTime\">", "<Set name=\"idleTimeout\">");
modified = true;
} else if (s.contains("<Set name=\"gracefulShutdown\">")) {
s = s.replace("<Set name=\"gracefulShutdown\">", "<Set name=\"stopTimeout\">");
modified = true;
} else if (s.contains("org.eclipse.jetty.server.HttpConfiguration")) {
foundHC = true;
} else if (!foundHC &&
(s.contains("<Set name=\"sendServerVersion\">") ||
s.contains("<Set name=\"sendDateHeader\">"))) {
// old ones for Server, not in HTTPConfiguration section, delete
modified = true;
continue;
} else if (s.contains("<Set name=\"Acceptors\">") ||
s.contains("<Set name=\"acceptors\">") ||
s.contains("<Set name=\"statsOn\">") ||
s.contains("<Set name=\"confidentialPort\">") ||
s.contains("<Set name=\"lowResourcesConnections\">") ||
s.contains("<Set name=\"lowResourcesMaxIdleTime\">") ||
s.contains("<Set name=\"useDirectBuffers\">")) {
// delete
modified = true;
continue;
}
out.println(s);
}
} catch (IOException ioe) {
if (in != null) {
System.err.println("FAILED migration of " + xmlFile + ": " + ioe);
}
return false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) out.close();
}
if (modified) {
return FileUtil.rename(newFile, xmlFile);
} else {
newFile.delete();
return true;
}
if (migration2success)
ctx.router().saveConfig(PROP_JETTY9_MIGRATED_2, "true");
}
/** do we have Jetty 7/8/9? */
private static boolean hasLatestJetty() {
if (!_wasChecked) {

View File

@ -409,6 +409,8 @@ public class WorkingDir {
* Copy over the jetty.xml file with modifications
* It was already copied over once in migrate(), throw that out and
* do it again with modifications.
*
* @return success
*/
static boolean migrateJettyXml(File olddir, File todir, String filename, String oldString, String newString) {
File oldFile = new File(olddir, filename);
@ -417,6 +419,32 @@ public class WorkingDir {
File newFile = new File(todir, filename);
FileInputStream in = null;
PrintWriter out = null;
try {
migrateFileXML(oldFile, newFile, oldString, newString, null, null);
System.err.println("Copied " + oldFile + " with modifications");
return true;
} catch (IOException ioe) {
System.err.println("FAILED copy " + oldFile + ": " + ioe);
return false;
}
}
/**
* Copy over a XML file with modifications.
* Will overwrite any existing newFile.
*
* @param oldString to replace
* @param newString replacement
* @param oldString2 to replace, or null
* @param newString2 replacement, or null
* @throws IOException on all errors
* @since 0.9.66
*/
static void migrateFileXML(File oldFile, File newFile, String oldString, String newString,
String oldString2, String newString2) throws IOException {
FileInputStream in = null;
PrintWriter out = null;
try {
in = new FileInputStream(oldFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
@ -428,14 +456,13 @@ public class WorkingDir {
if (s.indexOf(oldString) >= 0) {
s = s.replace(oldString, newString);
}
if (oldString2 != null && s.indexOf(oldString2) >= 0) {
s = s.replace(oldString2, newString2);
}
out.println(s);
}
out.println("<!-- Modified by I2P User dir migration script -->");
System.err.println("Copied " + oldFile + " with modifications");
return true;
} catch (IOException ioe) {
System.err.println("FAILED copy " + oldFile + ": " + ioe);
return false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) out.close();