001: /*
002: * Copyright 2006 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.shell;
017:
018: import com.google.gwt.dev.About;
019:
020: import org.w3c.dom.Document;
021: import org.w3c.dom.Element;
022: import org.w3c.dom.Node;
023: import org.w3c.dom.NodeList;
024: import org.xml.sax.ErrorHandler;
025: import org.xml.sax.SAXException;
026: import org.xml.sax.SAXParseException;
027:
028: import java.io.ByteArrayInputStream;
029: import java.io.ByteArrayOutputStream;
030: import java.io.IOException;
031: import java.io.InputStream;
032: import java.net.MalformedURLException;
033: import java.net.URL;
034: import java.net.URLConnection;
035: import java.util.Date;
036: import java.util.prefs.Preferences;
037:
038: import javax.xml.parsers.DocumentBuilder;
039: import javax.xml.parsers.DocumentBuilderFactory;
040: import javax.xml.parsers.ParserConfigurationException;
041:
042: /**
043: * Orchestrates a best-effort attempt to find out if a new version of GWT is
044: * available.
045: */
046: public abstract class CheckForUpdates {
047:
048: /**
049: * Abstract the action to take when an update is available.
050: */
051: public static interface UpdateAvailableCallback {
052: void onUpdateAvailable(String html);
053: }
054:
055: protected static final String LAST_SERVER_VERSION = "lastServerVersion";
056: private static final boolean DEBUG_VERSION_CHECK;
057: private static final String FIRST_LAUNCH = "firstLaunch";
058: private static final String NEXT_PING = "nextPing";
059:
060: // Uncomment one of constants below to try different variations of failure to
061: // make sure we never interfere with the app running.
062:
063: // Check against a fake server to see failure to contact server.
064: // protected static final String QUERY_URL =
065: // "http://nonexistenthost:1111/gwt/currentversion.xml";
066:
067: // Check 404 on a real location that doesn't have the file.
068: // protected static final String QUERY_URL =
069: // "http://www.google.com/gwt/currentversion.xml";
070:
071: // A test URL for seeing it actually work in a sandbox.
072: // protected static final String QUERY_URL =
073: // "http://localhost/gwt/currentversion.xml";
074:
075: // The real URL that should be used.
076: private static final String QUERY_URL = "http://tools.google.com/webtoolkit/currentversion.xml";
077:
078: private static final int VERSION_PARTS = 3;
079: private static final String VERSION_REGEXP = "\\d+\\.\\d+\\.\\d+";
080:
081: static {
082: // Do this in a static initializer so we can ignore all exceptions.
083: //
084: boolean debugVersionCheck = false;
085: try {
086: if (System.getProperty("gwt.debugVersionCheck") != null) {
087: debugVersionCheck = true;
088: }
089: } catch (Throwable e) {
090: // Always silently ignore any errors.
091: //
092: } finally {
093: DEBUG_VERSION_CHECK = debugVersionCheck;
094: }
095: }
096:
097: /**
098: * Determines whether the server version is definitively newer than the client
099: * version. If any errors occur in the comparison, this method returns false
100: * to avoid unwanted erroneous notifications.
101: *
102: * @param clientVersion The current client version
103: * @param serverVersion The current server version
104: * @return true if the server is definitely newer, otherwise false
105: */
106: protected static boolean isServerVersionNewer(String clientVersion,
107: String serverVersion) {
108: if (clientVersion == null || serverVersion == null) {
109: return false;
110: }
111:
112: // must match expected format
113: if (!clientVersion.matches(VERSION_REGEXP)
114: || !serverVersion.matches(VERSION_REGEXP)) {
115: return false;
116: }
117:
118: // extract the relevant parts
119: String[] clientParts = clientVersion.split("\\.");
120: String[] serverParts = serverVersion.split("\\.");
121: if (clientParts.length != VERSION_PARTS
122: || serverParts.length != VERSION_PARTS) {
123: return false;
124: }
125:
126: // examine piece by piece from most significant to least significant
127: for (int i = 0; i < VERSION_PARTS; ++i) {
128: try {
129: int clientPart = Integer.parseInt(clientParts[i]);
130: int serverPart = Integer.parseInt(serverParts[i]);
131: if (serverPart < clientPart) {
132: return false;
133: }
134:
135: if (serverPart > clientPart) {
136: return true;
137: }
138: } catch (NumberFormatException e) {
139: return false;
140: }
141: }
142:
143: return false;
144: }
145:
146: private static String getTextOfLastElementHavingTag(Document doc,
147: String tagName) {
148: NodeList nodeList = doc.getElementsByTagName(tagName);
149: int n = nodeList.getLength();
150: if (n > 0) {
151: Element elem = (Element) nodeList.item(n - 1);
152: // Assume the first child is the value.
153: //
154: Node firstChild = elem.getFirstChild();
155: if (firstChild != null) {
156: String text = firstChild.getNodeValue();
157: return text;
158: }
159: }
160:
161: return null;
162: }
163:
164: private static void parseResponse(Preferences prefs,
165: byte[] response, UpdateAvailableCallback callback)
166: throws IOException, ParserConfigurationException,
167: SAXException {
168:
169: if (DEBUG_VERSION_CHECK) {
170: System.out.println("Parsing response (length "
171: + response.length + ")");
172: }
173:
174: DocumentBuilderFactory factory = DocumentBuilderFactory
175: .newInstance();
176: DocumentBuilder builder = factory.newDocumentBuilder();
177: ByteArrayInputStream bais = new ByteArrayInputStream(response);
178:
179: // Parse the XML.
180: //
181: builder.setErrorHandler(new ErrorHandler() {
182:
183: public void error(SAXParseException exception)
184: throws SAXException {
185: // fail quietly
186: }
187:
188: public void fatalError(SAXParseException exception)
189: throws SAXException {
190: // fail quietly
191: }
192:
193: public void warning(SAXParseException exception)
194: throws SAXException {
195: // fail quietly
196: }
197: });
198: Document doc = builder.parse(bais);
199:
200: // The latest version number.
201: //
202: String version = getTextOfLastElementHavingTag(doc,
203: "latest-version");
204: if (version == null) {
205: // Not valid; quietly fail.
206: //
207: if (DEBUG_VERSION_CHECK) {
208: System.out.println("Failed to find <latest-version>");
209: }
210: return;
211: } else {
212: version = version.trim();
213: }
214:
215: String[] versionParts = version.split("\\.");
216: if (versionParts.length != 3) {
217: // Not valid; quietly fail.
218: //
219: if (DEBUG_VERSION_CHECK) {
220: System.out.println("Bad version format: " + version);
221: }
222: return;
223: }
224: try {
225: Integer.parseInt(versionParts[0]);
226: Integer.parseInt(versionParts[1]);
227: Integer.parseInt(versionParts[2]);
228: } catch (NumberFormatException e) {
229: // Not valid; quietly fail.
230: //
231: if (DEBUG_VERSION_CHECK) {
232: System.out.println("Bad version number: " + version);
233: }
234: return;
235: }
236:
237: // Ping delay for server-controlled throttling.
238: //
239: String pingDelaySecsStr = getTextOfLastElementHavingTag(doc,
240: "min-wait-seconds");
241: int pingDelaySecs = 0;
242: if (pingDelaySecsStr == null) {
243: // Not valid; quietly fail.
244: //
245: if (DEBUG_VERSION_CHECK) {
246: System.out.println("Missing <min-wait-seconds>");
247: }
248: return;
249: } else {
250: try {
251: pingDelaySecs = Integer.parseInt(pingDelaySecsStr
252: .trim());
253: } catch (NumberFormatException e) {
254: // Not a valid number; quietly fail.
255: //
256: if (DEBUG_VERSION_CHECK) {
257: System.out.println("Bad min-wait-seconds number: "
258: + pingDelaySecsStr);
259: }
260: return;
261: }
262: }
263:
264: // Read the HTML.
265: //
266: String html = getTextOfLastElementHavingTag(doc, "notification");
267:
268: if (html == null) {
269: // Not valid; quietly fail.
270: //
271: if (DEBUG_VERSION_CHECK) {
272: System.out.println("Missing <notification>");
273: }
274: return;
275: }
276:
277: // Okay -- this is a valid response.
278: //
279: processResponse(prefs, version, pingDelaySecs, html, callback);
280: }
281:
282: private static void processResponse(Preferences prefs,
283: String version, int pingDelaySecs, String html,
284: UpdateAvailableCallback callback) {
285:
286: // Record a ping; don't ping again until the delay is up.
287: //
288: long nextPingTime = System.currentTimeMillis() + pingDelaySecs
289: * 1000;
290: prefs.put(NEXT_PING, String.valueOf(nextPingTime));
291:
292: if (DEBUG_VERSION_CHECK) {
293: System.out.println("Ping delay is " + pingDelaySecs
294: + "; next ping at " + new Date(nextPingTime));
295: }
296:
297: /*
298: * Stash the version we got last time for comparison below, and record for
299: * next time the version we just got.
300: */
301: String lastServerVersion = prefs.get(LAST_SERVER_VERSION, null);
302: prefs.put(LAST_SERVER_VERSION, version);
303:
304: // Are we up to date already?
305: //
306: if (!isServerVersionNewer(About.GWT_VERSION_NUM, version)) {
307:
308: // Yes, we are.
309: //
310: if (DEBUG_VERSION_CHECK) {
311: System.out.println("Server version is not newer");
312: }
313: return;
314: }
315:
316: // Have we already prompted for this particular server version?
317: //
318: if (version.equals(lastServerVersion)) {
319:
320: // We've already nagged the user once. Don't do it again.
321: //
322: if (DEBUG_VERSION_CHECK) {
323: System.out
324: .println("A notification has already been shown for "
325: + version);
326: }
327: return;
328: }
329:
330: if (DEBUG_VERSION_CHECK) {
331: System.out.println("Server version has changed to "
332: + version + "; notification will be shown");
333: }
334:
335: // Commence nagging.
336: //
337: callback.onUpdateAvailable(html);
338: }
339:
340: public void check(final UpdateAvailableCallback callback) {
341:
342: try {
343: String forceCheckURL = System
344: .getProperty("gwt.forceVersionCheckURL");
345:
346: if (forceCheckURL != null && DEBUG_VERSION_CHECK) {
347: System.out.println("Explicit version check URL: "
348: + forceCheckURL);
349: }
350:
351: // Get our unique user id (based on absolute timestamp).
352: //
353: long currentTimeMillis = System.currentTimeMillis();
354: Preferences prefs = Preferences
355: .userNodeForPackage(CheckForUpdates.class);
356:
357: // Get our unique user id (based on absolute timestamp).
358: //
359: String firstLaunch = prefs.get(FIRST_LAUNCH, null);
360: if (firstLaunch == null) {
361: firstLaunch = Long.toHexString(currentTimeMillis);
362: prefs.put(FIRST_LAUNCH, firstLaunch);
363:
364: if (DEBUG_VERSION_CHECK) {
365: System.out.println("Setting first launch to "
366: + firstLaunch);
367: }
368: } else {
369: if (DEBUG_VERSION_CHECK) {
370: System.out.println("First launch was "
371: + firstLaunch);
372: }
373: }
374:
375: // See if it's time for our next ping yet.
376: //
377: String nextPing = prefs.get(NEXT_PING, "0");
378: if (nextPing != null) {
379: try {
380: long nextPingTime = Long.parseLong(nextPing);
381: if (currentTimeMillis < nextPingTime) {
382: // it's not time yet
383: if (DEBUG_VERSION_CHECK) {
384: System.out
385: .println("Next ping is not until "
386: + new Date(nextPingTime));
387: }
388: return;
389: }
390: } catch (NumberFormatException e) {
391: // ignore
392: }
393: }
394:
395: // See if new version is available.
396: //
397: String queryURL = forceCheckURL != null ? forceCheckURL
398: : QUERY_URL;
399: String url = queryURL + "?v=" + About.GWT_VERSION_NUM
400: + "&id=" + firstLaunch;
401:
402: if (DEBUG_VERSION_CHECK) {
403: System.out
404: .println("Checking for new version at " + url);
405: }
406:
407: // Do the HTTP GET.
408: //
409: byte[] response;
410: String fullUserAgent = makeUserAgent();
411: if (System.getProperty("gwt.forceVersionCheckNonNative") == null) {
412: // Use subclass.
413: //
414: response = doHttpGet(fullUserAgent, url);
415: } else {
416: // Use the pure Java version, but it probably doesn't work with proxies.
417: //
418: response = httpGetNonNative(fullUserAgent, url);
419: }
420:
421: if (response == null || response.length == 0) {
422: // Problem. Quietly fail.
423: //
424: if (DEBUG_VERSION_CHECK) {
425: System.out
426: .println("Failed to obtain current version info via HTTP");
427: }
428: return;
429: }
430:
431: // Parse and process the response.
432: // Bad responses will be silently ignored.
433: //
434: parseResponse(prefs, response, callback);
435:
436: } catch (Throwable e) {
437: // Always silently ignore any errors.
438: //
439: if (DEBUG_VERSION_CHECK) {
440: System.out
441: .println("Exception while processing version info");
442: e.printStackTrace();
443: }
444: }
445: }
446:
447: protected abstract byte[] doHttpGet(String userAgent, String url);
448:
449: /**
450: * This default implementation uses regular Java HTTP, which doesn't deal with
451: * proxies automagically. See the IE6 subclasses for an implementation that
452: * does deal with proxies.
453: */
454: protected byte[] httpGetNonNative(String userAgent, String url) {
455: Throwable caught;
456: InputStream is = null;
457: try {
458: URL urlToGet = new URL(url);
459: URLConnection conn = urlToGet.openConnection();
460: conn.setRequestProperty("User-Agent", userAgent);
461: is = conn.getInputStream();
462: ByteArrayOutputStream baos = new ByteArrayOutputStream();
463: byte[] buffer = new byte[4096];
464: int bytesRead;
465: while ((bytesRead = is.read(buffer)) != -1) {
466: baos.write(buffer, 0, bytesRead);
467: }
468: byte[] response = baos.toByteArray();
469: return response;
470: } catch (MalformedURLException e) {
471: caught = e;
472: } catch (IOException e) {
473: caught = e;
474: } finally {
475: if (is != null) {
476: try {
477: is.close();
478: } catch (IOException e) {
479: }
480: }
481: }
482:
483: if (System.getProperty("gwt.debugLowLevelHttpGet") != null) {
484: caught.printStackTrace();
485: }
486:
487: return null;
488: }
489:
490: private void appendUserAgentProperty(StringBuffer sb,
491: String propName) {
492: String propValue = System.getProperty(propName);
493: if (propValue != null) {
494: if (sb.length() > 0) {
495: sb.append("; ");
496: }
497: sb.append(propName);
498: sb.append("=");
499: sb.append(propValue);
500: }
501: }
502:
503: /**
504: * Creates a user-agent string by combining standard Java properties.
505: */
506: private String makeUserAgent() {
507: String ua = "GWT Freshness Checker";
508:
509: StringBuffer extra = new StringBuffer();
510: appendUserAgentProperty(extra, "java.vendor");
511: appendUserAgentProperty(extra, "java.version");
512: appendUserAgentProperty(extra, "os.arch");
513: appendUserAgentProperty(extra, "os.name");
514: appendUserAgentProperty(extra, "os.version");
515:
516: if (extra.length() > 0) {
517: ua += " (" + extra.toString() + ")";
518: }
519:
520: return ua;
521: }
522: }
|