package net.sf.openrocket.communication;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.BuildProperties;
import net.sf.openrocket.util.ComparablePair;
import net.sf.openrocket.util.LimitedInputStream;
public class UpdateInfoRetriever {
private static final Logger log = LoggerFactory.getLogger(UpdateInfoRetriever.class);
private UpdateInfoFetcher fetcher = null;
/**
* Start an asynchronous task that will fetch information about the latest
* OpenRocket version. This will overwrite any previous fetching operation.
* This call will return immediately.
*/
public void start() {
fetcher = new UpdateInfoFetcher();
fetcher.setName("UpdateInfoFetcher");
fetcher.setDaemon(true);
fetcher.start();
}
/**
* Check whether the update info fetching is still in progress.
*
* @return <code>true</code> if the communication is still in progress.
*/
public boolean isRunning() {
if (fetcher == null) {
throw new IllegalStateException("startFetchUpdateInfo() has not been called");
}
return fetcher.isAlive();
}
/**
* Retrieve the result of the background update info fetcher. This method returns
* the result of the previous call to {@link #start()}. It must be
* called before calling this method.
* <p>
* This method will return <code>null</code> if the info fetcher is still running or
* if it encountered a problem in communicating with the server. The difference can
* be checked using {@link #isRunning()}.
*
* @return the update result, or <code>null</code> if the fetching is still in progress
* or an error occurred while communicating with the server.
* @throws IllegalStateException if {@link #start()} has not been called.
*/
public UpdateInfo getUpdateInfo() {
if (fetcher == null) {
throw new IllegalStateException("start() has not been called");
}
return fetcher.info;
}
/**
* Parse the data received from the server.
*
* @param r the Reader from which to read.
* @return an UpdateInfo construct, or <code>null</code> if the data was invalid.
* @throws IOException if an I/O exception occurs.
*/
/* package-private */
static UpdateInfo parseUpdateInput(Reader r) throws IOException {
BufferedReader reader;
if (r instanceof BufferedReader) {
reader = (BufferedReader) r;
} else {
reader = new BufferedReader(r);
}
String version = null;
ArrayList<ComparablePair<Integer, String>> updates =
new ArrayList<ComparablePair<Integer, String>>();
String str = reader.readLine();
while (str != null) {
if (str.matches("^Version: *[0-9]+\\.[0-9]+\\.[0-9]+[a-zA-Z0-9.-]* *$")) {
version = str.substring(8).trim();
} else if (str.matches("^[0-9]+:\\p{Print}+$")) {
int index = str.indexOf(':');
int value = Integer.parseInt(str.substring(0, index));
String desc = str.substring(index + 1).trim();
if (!desc.equals("")) {
updates.add(new ComparablePair<Integer, String>(value, desc));
}
}
// Ignore anything else
str = reader.readLine();
}
if (version != null) {
return new UpdateInfo(version, updates);
} else {
return null;
}
}
/**
* An asynchronous task that fetches and parses the update info.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
private class UpdateInfoFetcher extends Thread {
private volatile UpdateInfo info = null;
@Override
public void run() {
try {
doConnection();
} catch (IOException e) {
log.info("Fetching update failed: " + e);
return;
}
}
private void doConnection() throws IOException {
String url = Communicator.UPDATE_INFO_URL + "?" + Communicator.VERSION_PARAM + "="
+ Communicator.encode(BuildProperties.getVersion());
HttpURLConnection connection = Communicator.connectionSource.getConnection(url);
connection.setConnectTimeout(Communicator.CONNECTION_TIMEOUT);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod("GET");
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setRequestProperty("X-OpenRocket-Version",
Communicator.encode(BuildProperties.getVersion() + " " + BuildProperties.getBuildSource()));
connection.setRequestProperty("X-OpenRocket-ID",
Communicator.encode(Application.getPreferences().getUniqueID()));
connection.setRequestProperty("X-OpenRocket-OS",
Communicator.encode(System.getProperty("os.name") + " " +
System.getProperty("os.arch")));
connection.setRequestProperty("X-OpenRocket-Java",
Communicator.encode(System.getProperty("java.vendor") + " " +
System.getProperty("java.version")));
connection.setRequestProperty("X-OpenRocket-Country",
Communicator.encode(System.getProperty("user.country") + " " +
System.getProperty("user.timezone")));
connection.setRequestProperty("X-OpenRocket-Locale",
Communicator.encode(Locale.getDefault().toString()));
connection.setRequestProperty("X-OpenRocket-CPUs", "" + Runtime.getRuntime().availableProcessors());
InputStream is = null;
try {
connection.connect();
log.debug("Update response code: " + connection.getResponseCode());
if (connection.getResponseCode() == Communicator.UPDATE_INFO_NO_UPDATE_CODE) {
// No updates are available
log.info("No updates available");
info = new UpdateInfo();
return;
}
if (connection.getResponseCode() != Communicator.UPDATE_INFO_UPDATE_AVAILABLE) {
// Error communicating with server
log.warn("Unknown server response code: " + connection.getResponseCode());
return;
}
String contentType = connection.getContentType();
if (contentType == null ||
contentType.toLowerCase(Locale.ENGLISH).indexOf(Communicator.UPDATE_INFO_CONTENT_TYPE) < 0) {
// Unknown response type
log.warn("Unknown Content-type received:" + contentType);
return;
}
// Update is available, parse input
is = connection.getInputStream();
is = new LimitedInputStream(is, Communicator.MAX_INPUT_BYTES);
String encoding = connection.getContentEncoding();
if (encoding == null || encoding.equals(""))
encoding = "UTF-8";
BufferedReader reader = new BufferedReader(new InputStreamReader(is, encoding));
String version = null;
ArrayList<ComparablePair<Integer, String>> updates =
new ArrayList<ComparablePair<Integer, String>>();
String line = reader.readLine();
while (line != null) {
if (line.matches("^Version:[a-zA-Z0-9._ -]{1,30}$")) {
version = line.substring(8).trim();
} else if (line.matches("^[0-9]{1,9}:\\P{Cntrl}{1,300}$")) {
String[] split = line.split(":", 2);
int n = Integer.parseInt(split[0]);
updates.add(new ComparablePair<Integer, String>(n, split[1].trim()));
}
// Ignore line otherwise
line = reader.readLine();
}
// Check version input
if (version == null || version.length() == 0 ||
version.equalsIgnoreCase(BuildProperties.getVersion())) {
// Invalid response
log.warn("Invalid version received, ignoring.");
return;
}
info = new UpdateInfo(version, updates);
log.info("Found update: " + info);
} finally {
try {
if (is != null)
is.close();
connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}