package com.limegroup.gnutella.updates;
import java.io.File;
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.SAXException;
import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.Connection;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HttpClientManager;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.FileUtils;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.ThreadFactory;
/**
* Used for parsing the signed_update_file.xml and updating any values locally.
* Has a singleton pattern.
*/
public class UpdateManager {
private static final Log LOG = LogFactory.getLog(UpdateManager.class);
/**
* Used when handshaking with other LimeWires.
*/
private String latestVersion;
/**
* The language specific string that contains the new features of the
* version discovered in the network
*/
private String message = "";
/**
* true if message is as per the user's language preferences.
*/
private boolean usesLocale;
private static UpdateManager INSTANCE=null;
public static final String SPECIAL_VERSION = "@version@";
/**
* Whether or not we think we have a valid file on disk.
*/
private boolean isValid;
/**
* Constructor, reads the latest update.xml file from the last run on the
* network, and srores the values in latestVersion, message and usesLocale.
* latestVersion is the only variable whose value is used after start up.
* The other two message and usesLocale are used only once when showing the
* user a message at start up. So although this class is a singleton, it's
* safe for the constructor to set these two values for the whole session.
*/
private UpdateManager() {
latestVersion = "0.0.0";
byte[] content = FileUtils.readFileFully(new File(CommonUtils.getUserSettingsDir(),"update.xml"));
if(content != null) {
//we dont really need to verify, but we may as well...so here goes.
UpdateMessageVerifier verifier = new UpdateMessageVerifier(content, true);//from disk
boolean verified = verifier.verifySource();
if(verified) {
try {
String xml = new String(verifier.getMessageBytes(),"UTF-8");
UpdateFileParser parser = new UpdateFileParser(xml);
latestVersion = parser.getVersion();
message = parser.getMessage();
usesLocale = parser.usesLocale();
isValid = true;
} catch(SAXException sax) {
LOG.error("invalid update xml", sax);
} catch(IOException iox) {
LOG.error("iox updating", iox);
}
}
}
}
public static synchronized UpdateManager instance() {
if(INSTANCE==null)
INSTANCE = new UpdateManager();
return INSTANCE;
}
public synchronized String getVersion() {
Assert.that(latestVersion!=null,"version not initilaized");
return latestVersion;
}
/**
* Returns whether or not we have a valid file on disk.
*/
public boolean isValid() {
return isValid;
}
public void checkAndUpdate(Connection connection) {
String nv = connection.getVersion();
if(LOG.isTraceEnabled())
LOG.trace("Update check: myVersion: "+
latestVersion+", theirs: "+nv);
String myVersion = null;
if(latestVersion.equals(SPECIAL_VERSION))
myVersion = "0.0.0"; // consider special to be empty for this purpose.
else //use the original value of latestVersion
myVersion = latestVersion;
if(!isGreaterVersion(nv,myVersion))
return;
if(nv.equals(SPECIAL_VERSION))// should never see this on the network!!
return;//so this should never happen
final Connection c = connection;
final String myversion = myVersion;
ThreadFactory.startThread(new Runnable() {
public void run() {
LOG.trace("Getting update file");
final String UPDATE = "/update.xml";
//if we get host or port incorrectly, we will not be able to
//establish a connection and just return, its fail safe.
String ip = c.getAddress();
int port = c.getPort();
String connectTo = "http://" + ip + ":" + port + UPDATE;
HttpClient client = HttpClientManager.getNewClient();
HttpMethod get = new GetMethod(connectTo);
get.addRequestHeader("Cache-Control", "no-cache");
get.addRequestHeader("User-Agent", CommonUtils.getHttpServer());
get.addRequestHeader(HTTPHeaderName.CONNECTION.httpStringValue(),
"close");
try {
client.executeMethod(get);
byte[] data = get.getResponseBody();
if( data == null )
return;
UpdateMessageVerifier verifier =
new UpdateMessageVerifier(data, false);// from network
boolean verified = false;
try {
verified = verifier.verifySource();
} catch (ClassCastException ccx) {
verified = false;
}
if(!verified)
return;
LOG.trace("Verified file contents");
String xml = new String(verifier.getMessageBytes(),"UTF-8");
UpdateFileParser parser = new UpdateFileParser(xml);
if(LOG.isTraceEnabled())
LOG.trace("New version: "+parser.getVersion());
//we checked for new version while handshaking, but we
//should check again with the authenticated xml data.
String newVersion = parser.getVersion();
if(newVersion==null)
return;
if(isGreaterVersion(newVersion,myversion)) {
LOG.trace("committing new update file");
synchronized(UpdateManager.this) {
commitVersionFile(data);//could throw an exception
//committed file, update the value of latestVersion
latestVersion = newVersion;
if(LOG.isTraceEnabled())
LOG.trace("commited file. Latest is:" +
latestVersion);
}
}
} catch(IOException iox) {
LOG.warn("iox on network, on disk, who knows??", iox);
//IOException - reading from socket, writing to disk etc.
return;
} catch(SAXException sx) {
LOG.error("invalid xml", sx);
//SAXException - parsing the xml
return; //We can't continue...forget it.
} catch(Throwable t) {
ErrorService.error(t);
} finally {
if( get != null )
get.releaseConnection();
}
}//end of run
}, "UpdateFileRequestor");
}
/**
* compares newVer with oldVer. and returns true iff newVer is a newer
* version, false if neVer <= older.
* <p>
* <pre>
* treats @version@ as the highest version possible. The danger is that
* we may try to get updates from all files that have @version@ in the
* field. This is undesirable. So if we think the latest version is
* @version@ we do not put an X-Version header in the handshaking.
* </pre>
*/
public static boolean isGreaterVersion(String newVer, String oldVer) {
if(newVer==null && oldVer==null)
return false;
if(newVer==null)//old is newer
return false;
if(oldVer==null) //new is newer
return true;
if(newVer.equals(oldVer))//same
return false;
if(newVer.equals(SPECIAL_VERSION)) //new is newer
return true;
if(oldVer.equals(SPECIAL_VERSION)) //old is newer
return false;
//OK. Now lets look at numbers
int o1, o2 = -1;
int n1, n2 = -1;
try {
StringTokenizer tokenizer = new StringTokenizer(oldVer,".");
if(tokenizer.countTokens() < 2)
return false;
o1 = (new Integer(tokenizer.nextToken())).intValue();
o2 = (new Integer(tokenizer.nextToken())).intValue();
tokenizer = new StringTokenizer(newVer,".");
if(tokenizer.countTokens() < 2)
return false;
n1 = (new Integer(tokenizer.nextToken())).intValue();
n2 = (new Integer(tokenizer.nextToken())).intValue();
} catch(NumberFormatException nfe) {
return false;
}
if(n1>o1)
return true;
else if(n1==o1 && n2>o2)
return true;
return false;
}
/**
* writes data to signed_updateFile
*/
private void commitVersionFile(byte[] data) throws IOException {
boolean ret = FileUtils.verySafeSave(CommonUtils.getUserSettingsDir(), "update.xml", data);
if(!ret)
throw new IOException("couldn't safely save!");
}
}