CheckForUpdates.java :  » Ajax » GWT » com » google » gwt » dev » shell » Java Open Source

Java Open Source » Ajax » GWT 
GWT » com » google » gwt » dev » shell » CheckForUpdates.java
/*
 * Copyright 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.shell;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.About;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.prefs.Preferences;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Orchestrates a best-effort attempt to find out if a new version of GWT is
 * available.
 */
public class CheckForUpdates {

  /**
   * Represents a GWT version.
   */
  public static class GwtVersion implements Comparable<GwtVersion> {

    private final int[] version = new int[3];

    /**
     * Create a version that avoids any nagging -- "0.0.999".
     */
    public GwtVersion() {
      version[2] = 999;
    }

    /**
     * Parse a version number as a string. An empty or null string are
     * explicitly allowed and are equivalent to "0.0.0".
     * 
     * @param versionString
     * @throws NumberFormatException
     */
    public GwtVersion(String versionString) throws NumberFormatException {
      if (versionString == null) {
        return;
      }
      int part = 0;
      int v = 0;
      int len = versionString.length();
      for (int i = 0; i < len; ++i) {
        char ch = versionString.charAt(i);
        if (ch == '.') {
          if (part >= version.length) {
            throw new NumberFormatException();
          }
          version[part++] = v;
          v = 0;
        } else if (Character.isDigit(ch)) {
          int digit = Character.digit(ch, 10);
          if (digit < 0) {
            throw new NumberFormatException();
          }
          v = v * 10 + digit;
        }
      }
      version[part++] = v;
    }

    public int compareTo(GwtVersion o) {
      for (int i = 0; i <= 2; ++i) {
        if (version[i] != o.version[i]) {
          return version[i] - o.version[i];
        }
      }
      return 0;
    }

    public int getPart(int part) {
      // TODO: something besides IORE here?
      return version[part];
    }

    public boolean isNoNagVersion() {
      return version[2] == 999;
    }

    public boolean isUnspecified() {
      return version[0] == 0 && version[1] == 0 && version[2] == 0;
    }

    @Override
    public String toString() {
      StringBuilder buf = new StringBuilder();
      buf.append(version[0]).append('.').append(version[1]).append('.');
      buf.append(version[2]);
      return buf.toString();
    }
  }

  /**
   * Returns the result of an update check.
   */
  public interface UpdateResult {
    /**
     * @return the new version of GWT available.
     */
    GwtVersion getNewVersion();

    /**
     * @return the URL for details about the new version.
     */
    URL getURL();
  }

  public static final long ONE_DAY = 24 * 60 * 60 * 1000;
  public static final long ONE_MINUTE = 60 * 1000;

  // System properties used by CheckForUpdates
  protected static final String PROPERTY_DEBUG_HTTP_GET = "gwt.debugLowLevelHttpGet";
  protected static final String PROPERTY_FORCE_NONNATIVE = "gwt.forceVersionCheckNonNative";
  protected static final String PROPERTY_PREFS_NAME = "gwt.prefsPathName";
  protected static final String PROPERTY_QUERY_URL = "gwt.forceVersionCheckURL";

  // Log levels -- in general we want the logging of the update process
  // to not be visible to normal users.
  private static final TreeLogger.Type CHECK_ERROR = TreeLogger.DEBUG;
  private static final TreeLogger.Type CHECK_INFO = TreeLogger.SPAM;
  private static final TreeLogger.Type CHECK_SPAM = TreeLogger.SPAM;
  private static final TreeLogger.Type CHECK_WARN = TreeLogger.SPAM;

  // Preferences keys
  private static final String FIRST_LAUNCH = "firstLaunch";
  private static final String HIGHEST_RUN_VERSION = "highestRunVersion";
  private static final String LAST_PING = "lastPing";
  private static final String NEXT_PING = "nextPing";

  // Uncomment one of constants below to try different variations of failure to
  // make sure we never interfere with the app running.

  // Check against a fake server to see failure to contact server.
  // protected static final String QUERY_URL =
  // "http://nonexistenthost:1111/gwt/currentversion.xml";

  // Check 404 on a real location that doesn't have the file.
  // protected static final String QUERY_URL =
  // "http://www.google.com/gwt/currentversion.xml";

  // A test URL for seeing it actually work in a sandbox.
  // protected static final String QUERY_URL =
  // "http://localhost/gwt/currentversion.xml";

  // The real URL that should be used.
  private static final String QUERY_URL = "http://tools.google.com/webtoolkit/currentversion.xml";

  private static String getTextOfLastElementHavingTag(Document doc,
      String tagName) {
    NodeList nodeList = doc.getElementsByTagName(tagName);
    int n = nodeList.getLength();
    if (n > 0) {
      Element elem = (Element) nodeList.item(n - 1);
      // Assume the first child is the value.
      //
      Node firstChild = elem.getFirstChild();
      if (firstChild != null) {
        String text = firstChild.getNodeValue();
        return text;
      }
    }

    return null;
  }

  private String entryPoint;
  private TreeLogger logger;
  private GwtVersion myVersion;

  /**
   * Create an update checker which will poll a server URL and log a message
   * about an update if available.
   * 
   * @param logger TreeLogger to use
   */
  public CheckForUpdates(TreeLogger logger) {
    this(logger, null);
  }

  /**
   * Create an update checker which will poll a server URL and log a message
   * about an update if available.
   * 
   * @param logger TreeLogger to use
   * @param entryPoint the name of the main entry point used for this execution
   */
  public CheckForUpdates(TreeLogger logger, String entryPoint) {
    this.logger = logger;
    this.entryPoint = entryPoint;
    try {
      myVersion = new GwtVersion(About.GWT_VERSION_NUM);
    } catch (NumberFormatException e) {
      // if our build version number is bogus, use one that avoids nagging
      myVersion = new GwtVersion();
    }
  }

  /**
   * Check for updates and log to the logger if they are available.
   * 
   * @return an UpdateResult or null if there is no new update
   */
  public UpdateResult check() {
    return check(0);
  }

  /**
   * Check for updates and log to the logger if they are available.
   * 
   * @return an UpdateResult or null if there is no new update
   */
  public UpdateResult check(long minCheckMillis) {
    TreeLogger branch = logger.branch(CHECK_INFO, "Checking for updates");
    try {
      String prefsName = System.getProperty(PROPERTY_PREFS_NAME);
      Preferences prefs;
      if (prefsName != null) {
        prefs = Preferences.userRoot().node(prefsName);
      } else {
        prefs = Preferences.userNodeForPackage(CheckForUpdates.class);
      }

      String queryURL = QUERY_URL;
      String forceCheckURL = System.getProperty(PROPERTY_QUERY_URL);

      if (forceCheckURL != null) {
        branch.log(CHECK_INFO, "Explicit version check URL: " + forceCheckURL);
        queryURL = forceCheckURL;
      }

      // Get our unique user id (based on absolute timestamp).
      //
      long currentTimeMillis = System.currentTimeMillis();
      String firstLaunch = prefs.get(FIRST_LAUNCH, null);
      if (firstLaunch == null) {
        firstLaunch = Long.toHexString(currentTimeMillis);
        prefs.put(FIRST_LAUNCH, firstLaunch);
        branch.log(CHECK_SPAM, "Setting first launch to " + firstLaunch);
      } else {
        branch.log(CHECK_SPAM, "First launch was " + firstLaunch);
      }

      // See if enough time has passed.
      //
      String lastPing = prefs.get(LAST_PING, "0");
      if (lastPing != null) {
        try {
          long lastPingTime = Long.parseLong(lastPing);
          if (currentTimeMillis < lastPingTime + minCheckMillis) {
            // it's not time yet
            branch.log(CHECK_INFO, "Last ping was " + new Date(lastPingTime)
                + ", min wait is " + minCheckMillis + "ms");
            return null;
          }
        } catch (NumberFormatException e) {
          branch.log(CHECK_WARN, "Error parsing last ping time", e);
        }
      }

      // See if it's time for our next ping yet.
      //
      String nextPing = prefs.get(NEXT_PING, "0");
      if (nextPing != null) {
        try {
          long nextPingTime = Long.parseLong(nextPing);
          if (currentTimeMillis < nextPingTime) {
            // it's not time yet
            branch.log(CHECK_INFO, "Next ping is not until "
                + new Date(nextPingTime));
            return null;
          }
        } catch (NumberFormatException e) {
          branch.log(CHECK_WARN, "Error parsing next ping time", e);
        }
      }

      // See if new version is available.
      //
      String url = queryURL + "?v=" + About.GWT_VERSION_NUM + "&id="
          + firstLaunch + "&r=" + About.GWT_SVNREV;
      if (entryPoint != null) {
        url += "&e=" + entryPoint;
      }

      branch.log(CHECK_INFO, "Checking for new version at " + url);

      // Do the HTTP GET.
      //
      byte[] response;
      String fullUserAgent = makeUserAgent();
      if (System.getProperty(PROPERTY_FORCE_NONNATIVE) == null) {
        // Use subclass.
        //
        response = doHttpGet(branch, fullUserAgent, url);
      } else {
        // Use the pure Java version, but it probably doesn't work with proxies.
        //
        response = httpGetNonNative(branch, fullUserAgent, url);
      }

      if (response == null || response.length == 0) {
        // Problem. Quietly fail.
        //
        branch.log(CHECK_ERROR,
            "Failed to obtain current version info via HTTP");
        return null;
      }

      // Parse and process the response.
      // Bad responses will be silently ignored.
      //
      return parseResponse(branch, prefs, response);

    } catch (Throwable e) {
      // Always silently ignore any errors.
      //
      branch.log(CHECK_INFO, "Exception while processing version info", e);
    }
    return null;
  }

  /**
   * Default implementation just uses the platform-independent method. A
   * subclass should override this method for platform-dependent proxy handling,
   * for example.
   * 
   * @param branch TreeLogger to use
   * @param userAgent user agent string to send in request
   * @param url URL to fetch
   * @return byte array of response, or null if an error
   */
  protected byte[] doHttpGet(TreeLogger branch, String userAgent, String url) {
    return httpGetNonNative(branch, userAgent, url);
  }

  /**
   * This default implementation uses regular Java HTTP, which doesn't deal with
   * proxies automagically. See the IE6 subclasses for an implementation that
   * does deal with proxies.
   * 
   * @param branch TreeLogger to use
   * @param userAgent user agent string to send in request
   * @param url URL to fetch
   * @return byte array of response, or null if an error
   */
  protected byte[] httpGetNonNative(TreeLogger branch, String userAgent,
      String url) {
    Throwable caught;
    InputStream is = null;
    try {
      URL urlToGet = new URL(url);
      URLConnection conn = urlToGet.openConnection();
      conn.setRequestProperty("User-Agent", userAgent);
      is = conn.getInputStream();
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[4096];
      int bytesRead;
      while ((bytesRead = is.read(buffer)) != -1) {
        baos.write(buffer, 0, bytesRead);
      }
      byte[] response = baos.toByteArray();
      return response;
    } catch (MalformedURLException e) {
      caught = e;
    } catch (IOException e) {
      caught = e;
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
        }
      }
    }

    if (System.getProperty(PROPERTY_DEBUG_HTTP_GET) != null) {
      branch.log(CHECK_ERROR, "Exception in HTTP request", caught);
    }

    return null;
  }

  private void appendUserAgentProperty(StringBuffer sb, String propName) {
    String propValue = System.getProperty(propName);
    if (propValue != null) {
      if (sb.length() > 0) {
        sb.append("; ");
      }
      sb.append(propName);
      sb.append("=");
      sb.append(propValue);
    }
  }

  /**
   * Creates a user-agent string by combining standard Java properties.
   */
  private String makeUserAgent() {
    String ua = "GWT Freshness Checker";

    StringBuffer extra = new StringBuffer();
    appendUserAgentProperty(extra, "java.vendor");
    appendUserAgentProperty(extra, "java.version");
    appendUserAgentProperty(extra, "os.arch");
    appendUserAgentProperty(extra, "os.name");
    appendUserAgentProperty(extra, "os.version");

    if (extra.length() > 0) {
      ua += " (" + extra.toString() + ")";
    }

    return ua;
  }

  private UpdateResult parseResponse(TreeLogger branch, Preferences prefs,
      byte[] response) throws IOException, ParserConfigurationException,
      SAXException {

    branch.log(CHECK_SPAM, "Parsing response (length " + response.length + ")");

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = factory.newDocumentBuilder();
    ByteArrayInputStream bais = new ByteArrayInputStream(response);

    // Parse the XML.
    //
    builder.setErrorHandler(new ErrorHandler() {

      public void error(SAXParseException exception) throws SAXException {
        // fail quietly
      }

      public void fatalError(SAXParseException exception) throws SAXException {
        // fail quietly
      }

      public void warning(SAXParseException exception) throws SAXException {
        // fail quietly
      }
    });
    Document doc = builder.parse(bais);

    // The latest version number.
    //
    String versionString = getTextOfLastElementHavingTag(doc, "latest-version");
    if (versionString == null) {
      // Not valid; quietly fail.
      //
      branch.log(CHECK_ERROR, "Failed to find <latest-version>");
      return null;
    }
    GwtVersion currentReleasedVersion;
    try {
      currentReleasedVersion = new GwtVersion(versionString.trim());
    } catch (NumberFormatException e) {
      branch.log(CHECK_ERROR, "Bad version: " + versionString, e);
      return null;
    }

    // Ping delay for server-controlled throttling.
    //
    String pingDelaySecsStr = getTextOfLastElementHavingTag(doc,
        "min-wait-seconds");
    int pingDelaySecs = 0;
    if (pingDelaySecsStr == null) {
      // Not valid; quietly fail.
      //
      branch.log(CHECK_ERROR, "Missing <min-wait-seconds>");
      return null;
    }
    try {
      pingDelaySecs = Integer.parseInt(pingDelaySecsStr.trim());
    } catch (NumberFormatException e) {
      // Not a valid number; quietly fail.
      //
      branch.log(CHECK_ERROR, "Bad min-wait-seconds number: "
          + pingDelaySecsStr);
      return null;
    }

    String url = getTextOfLastElementHavingTag(doc, "notification-url");

    if (url == null) {
      // no URL, so write the HTML locally and provide a URL from that

      // Read the HTML.
      //
      String html = getTextOfLastElementHavingTag(doc, "notification");

      if (html == null) {
        // Not valid; quietly fail.
        //
        branch.log(CHECK_ERROR, "Missing <notification>");
        return null;
      }
      PrintWriter writer = null;
      try {
        String tempDir = System.getProperty("java.io.tmpdir");
        File updateHtml = new File(tempDir, "gwt-update-"
            + currentReleasedVersion + ".html");
        writer = new PrintWriter(new FileOutputStream(updateHtml));
        writer.print(html);
        url = "file://" + updateHtml.getAbsolutePath();
      } finally {
        if (writer != null) {
          writer.close();
        }
      }
    }

    // Okay -- this is a valid response.
    //
    return processResponse(branch, prefs, currentReleasedVersion,
        pingDelaySecs, url);
  }

  private UpdateResult processResponse(TreeLogger branch, Preferences prefs,
      final GwtVersion serverVersion, int pingDelaySecs, final String notifyUrl) {

    // Record a ping; don't ping again until the delay is up.
    //
    long currentTimeMillis = System.currentTimeMillis();
    long nextPingTime = currentTimeMillis + pingDelaySecs * 1000;
    prefs.put(NEXT_PING, String.valueOf(nextPingTime));
    prefs.put(LAST_PING, String.valueOf(currentTimeMillis));

    branch.log(CHECK_INFO, "Ping delay is " + pingDelaySecs + "; next ping at "
        + new Date(nextPingTime));

    if (myVersion.isNoNagVersion()) {
      // If the version number indicates no nagging about updates, exit here
      // once we have recorded the next ping time. No-nag versions (ie,
      // trunk builds) should also not update the highest version that has been
      // run.
      return null;
    }

    // Update the highest version of GWT that has been run if we are later.
    GwtVersion highestRunVersion = new GwtVersion(prefs.get(
        HIGHEST_RUN_VERSION, null));
    if (myVersion.compareTo(highestRunVersion) > 0) {
      highestRunVersion = myVersion;
      prefs.put(HIGHEST_RUN_VERSION, highestRunVersion.toString());
    }

    // Are we up to date already?
    //
    if (highestRunVersion.compareTo(serverVersion) >= 0) {
      // Yes, we are.
      //
      branch.log(CHECK_INFO, "Server version (" + serverVersion
          + ") is not newer than " + highestRunVersion);
      return null;
    }

    // Commence nagging.
    //
    URL url = null;
    try {
      url = new URL(notifyUrl);
    } catch (MalformedURLException e) {
      logger.log(CHECK_ERROR, "Malformed notify URL: " + notifyUrl, e);
    }
    final URL finalUrl = url;
    return new UpdateResult() {
      public GwtVersion getNewVersion() {
        return serverVersion;
      }

      public URL getURL() {
        return finalUrl;
      }
    };
  }
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.