Java tutorial
/* * Copyright 2002 - 2013 Pentaho Corporation. All rights reserved. * * This software was developed by Pentaho Corporation and is provided under the terms * of the Mozilla Public License, Version 1.1, or any later version. You may not use * this file except in compliance with the license. If you need a copy of the license, * please go to http://www.mozilla.org/MPL/MPL-1.1.txt. TThe Initial Developer is Pentaho Corporation. * * Software distributed under the Mozilla Public License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. Please refer to * the license for the specific language governing your rights and limitations. */ package org.pentaho.versionchecker; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.methods.GetMethod; /** * Checks for updated software information for the specified applications. <br/> * This class gets version information from the supplied <code>IVersionCheckDataProvider</code> and communicates the * results to the list of supplied <code>IVersionCheckResultHandler</code>. If an error occurs during processing, the * error information will be passed along to the list of supplied <code>IVersionCheckErrorHandler</code>. * * @author dkincade */ @SuppressWarnings({ "rawtypes", "unchecked" }) public class VersionChecker { private static final String PENTAHO_DIR = ".pentaho"; //$NON-NLS-1$ private static final String VERCHECK_PROPS_FILENAME = ".vercheck"; //$NON-NLS-1$ public static final String VERCHECK_CANT_SAVE_GUID = "12345-67890-09876-54321"; //$NON-NLS-1$ public static final boolean DEBUGGING = false; // Set to false for final deliverable, and true for verbose output. // property name constants private static final String PROP_ROOT = "versionchk"; //$NON-NLS-1$ private static final String PROP_SYSTEM_GUID = PROP_ROOT + ".guid"; //$NON-NLS-1$ private static final String PROP_UPDATE = "update"; //$NON-NLS-1$ private static final String PROP_LASTCHECK = "lastcheck"; //$NON-NLS-1$ // // So, you're probably asking "why are these now static variables?", huh? Well, the // answer is that, once we've figured out the stuff, we want to keep it hanging out // in the VM. That way, we're not having to re-calculate things over and over. // private static boolean isWritable = true; private static File propsDirectory; private static File propsFile; private static Properties props; private static String guid; static { if (DEBUGGING) { System.out.println("Static Initializer"); //$NON-NLS-1$ } init(); } public static void init() { try { isWritable = true; propsDirectory = null; propsFile = null; guid = null; props = new Properties(); String homeDir = getHomeDir(); if (DEBUGGING) { System.out.println("Home Directory: " + homeDir); //$NON-NLS-1$ } if (homeDir == null) { isWritable = false; if (DEBUGGING) { System.out.println("*** Cannot Write Properties ***"); //$NON-NLS-1$ } } else { propsDirectory = new File(homeDir + (homeDir.endsWith("/") ? "" : File.separator) + PENTAHO_DIR); //$NON-NLS-1$ //$NON-NLS-2$ propsDirectory.mkdirs(); String propsPath = propsDirectory.getCanonicalPath(); propsFile = new File( propsPath + (propsPath.endsWith("/") ? "" : File.separator) + VERCHECK_PROPS_FILENAME); //$NON-NLS-1$ //$NON-NLS-2$ if (DEBUGGING) { System.out.println("Properties Path: " + propsPath); //$NON-NLS-1$ } } } catch (Throwable th) { isWritable = false; } loadProperties(); LoadOrGenerateGuid(); } public static boolean getIsWritable() { return isWritable; } public static String getPropertiesDirectory() throws IOException { return (propsDirectory != null) ? propsDirectory.getCanonicalPath() : null; } private static void LoadOrGenerateGuid() { guid = props.getProperty(PROP_SYSTEM_GUID); if (DEBUGGING) { System.out.println("Loaded GUID: " + guid); //$NON-NLS-1$ } if (guid == null) { // generate guid generateGUID(); // save guid props.setProperty(PROP_SYSTEM_GUID, guid); saveProperties(); } } private static void generateGUID() { if (isWritable) { guid = UUIDUtil.getUUIDAsString(); if (DEBUGGING) { System.out.println("Generated GUID: " + guid); //$NON-NLS-1$ } } else { guid = VERCHECK_CANT_SAVE_GUID; } } private static boolean testWritabilityOfFolder(String testName) { boolean rtn = false; if ((testName != null) && (testName.length() > 0)) { String test1 = testName + (testName.endsWith("/") ? "" : File.separator) + "test_pentaho_write_.txt"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ File testThisDir = new File(test1); try { if (!testThisDir.exists()) { // What if we can create the test_pentaho_write_.txt file, but we can't delete // it? Well, this will check that, and will only try creating the file if // it isn't there. Someone could bypass this directory if: // // a. They create the test_pentaho_write_.txt file in the folder // b. ACL out the ability to delete that file // c. Create a .vercheck file that's unwritable in the same folder. // testThisDir.createNewFile(); } rtn = true; testThisDir.delete(); } catch (IOException ignored) { // Ignored on purpose // If we're allowed to write, we're OK - we don't *have* to be able // to delete the temporary file we've created. if (DEBUGGING) { ignored.printStackTrace(); } } } return rtn; } private static String getHomeDir() { // First, try the users' home directory. String homeDir = System.getProperty("user.home"); //$NON-NLS-1$ if ((homeDir == null) || (homeDir.length() == 0) || !(testWritabilityOfFolder(homeDir))) { // OK, that didn't work. Try the folder this program was // launched from. Can I write there? homeDir = "."; //$NON-NLS-1$ if (!testWritabilityOfFolder(homeDir)) { // Uuugh - last resort, but I should be // able to write to the temp directory. // If not, then we can't write anywhere, so // return null. homeDir = System.getProperty("java.io.tmpdir"); //$NON-NLS-1$ if (!testWritabilityOfFolder(homeDir)) { homeDir = null; } } } return homeDir; } private static Properties loadProperties() { if (isWritable) { FileInputStream fis = null; try { fis = new FileInputStream(propsFile); props.clear(); props.load(fis); } catch (Exception e) { // suppress any loading issues if (DEBUGGING) { e.printStackTrace(); } } finally { try { if (fis != null) { fis.close(); } } catch (Exception e) { if (DEBUGGING) { e.printStackTrace(); } // suppress any closing issues } } } return props; } public static String getGuid() { return guid; } private static void saveProperties() { if (isWritable) { FileOutputStream fos = null; try { fos = new FileOutputStream(propsFile); props.store(fos, "Pentaho Version Checker Properties"); //$NON-NLS-1$ } catch (Exception e) { if (DEBUGGING) { e.printStackTrace(); } // suppress any saving issues } finally { try { if (fos != null) { fos.close(); } } catch (Exception e) { if (DEBUGGING) { e.printStackTrace(); } // suppress any closing issues } } } } /** * The data provider that will be used to retrieve the data about this instance of the running application */ protected IVersionCheckDataProvider dataProvider; /** * The set of results handlers that will be called every time a version check occurs. */ protected final Set resultHandlers = new HashSet(); /** * The set of error handlers that will be called in the event there is an error while during processing */ protected final Set errorHandlers = new HashSet(); /** * Default URL used if none is provided - read from the resource bundle */ private static final String DEFAULT_URL = VersionCheckResourceBundle .getString("VersionChecker.CODE_default_url"); //$NON-NLS-1$ private static final String DEFAULT_TIMEOUT_MILLIS = VersionCheckResourceBundle .getString("VersionChecker.CODE_default_timeout_millis"); //$NON-NLS-1$ /** * Default constructor */ public VersionChecker() { } /** * Sets the data provider that will be used to retrieve the data about this instance of the running application. If * this method is called multiple times, the last data provider specified will be used. */ public void setDataProvider(IVersionCheckDataProvider dataProvider) { this.dataProvider = dataProvider; } /** * Adds a results handler to the list of handlers that will be called with the version check results every time the * check occurs. */ public void addResultHandler(IVersionCheckResultHandler resultHandler) { if (resultHandler != null) { resultHandlers.add(resultHandler); } } /** * Removes the specified result handler from the list of result handlers. */ public void removeResultHandler(IVersionCheckResultHandler resultHandler) { resultHandlers.remove(resultHandler); } /** * Adds an error handler to the list of error handlers that will be notified when an error occurs. */ public void addErrorHandler(IVersionCheckErrorHandler errorHandler) { if (errorHandler != null) { errorHandlers.add(errorHandler); } } /** * Removes an error handler from the list of error handlers what will be notified when an error occurs; */ public void removeErrorHandler(IVersionCheckErrorHandler errorHandler) { errorHandlers.remove(errorHandler); } /** * Performs the version check by sending the request to the Pentaho server and passing the results off to the * specified results checker. If an error is encountered, the error handlers will be notified. <br> * NOTE: If no DataProvider is specified, this method will still execute. */ public void performCheck(boolean ignoreExistingUpdates) { final HttpClient httpClient = getHttpClient(); final HttpMethod httpMethod = getHttpMethod(); try { int timeout = 30000; try { timeout = Integer.parseInt(DEFAULT_TIMEOUT_MILLIS); } catch (Exception e) { // ignore if (DEBUGGING) { e.printStackTrace(); } } httpClient.getHttpConnectionManager().getParams().setSoTimeout(timeout); // Set the URL and parameters setURL(httpMethod, guid); // Execute the request final int resultCode = httpClient.executeMethod(httpMethod); if (resultCode != HttpURLConnection.HTTP_OK) { // TODO - improve this throw new Exception("Invalid Result Code Returned: " + resultCode); //$NON-NLS-1$ } String resultXml = httpMethod.getResponseBodyAsString(); resultXml = checkForUpdates(dataProvider, resultXml, props, ignoreExistingUpdates); // Pass the results along processResults(resultXml); // save properties file with updated timestamp // note that any updates changed above will be saved also if (dataProvider != null) { String lastCheckProp = PROP_ROOT + "." + dataProvider.getApplicationID() + "." + //$NON-NLS-1$ //$NON-NLS-2$ dataProvider.getApplicationVersion() + "." + PROP_LASTCHECK; //$NON-NLS-1$ props.setProperty(lastCheckProp, new Date().toString()); saveProperties(); } // Clean up httpMethod.releaseConnection(); } catch (Exception e) { // IOException covers URIExcecption and HttpException if (DEBUGGING) { e.printStackTrace(); } handleException(e); } } /** * This utility method checks for updates Update the .updates property, and also supports suppression of update if * requested * * @param resultXml * the xml from the server * @param propsToCheck * the global properties object * @param ignoreExistingUpdates * true if we should ignore existing updates * * @return original or suppressed resultXml */ static String checkForUpdates(IVersionCheckDataProvider dataProvider, String resultXml, Properties propsToCheck, boolean ignoreExistingUpdates) { if (dataProvider != null) { int updateLoc = resultXml.indexOf("<update"); //$NON-NLS-1$ if (updateLoc >= 0) { boolean found = true; while (updateLoc >= 0) { // extract version and type the old fashioned way to avoid including libs int versionLocBegin = resultXml.indexOf(" version=\"", updateLoc); //$NON-NLS-1$ int versionLocEnd = resultXml.indexOf("\"", versionLocBegin + 10); //$NON-NLS-1$ String version = resultXml.substring(versionLocBegin + 10, versionLocEnd); int typeLocBegin = resultXml.indexOf(" type=\"", updateLoc); //$NON-NLS-1$ int typeLocEnd = resultXml.indexOf("\"", typeLocBegin + 7); //$NON-NLS-1$ String type = resultXml.substring(typeLocBegin + 7, typeLocEnd); int titleLocBegin = resultXml.indexOf(" title=\"", updateLoc); //$NON-NLS-1$ int titleLocEnd = resultXml.indexOf("\"", titleLocBegin + 8); //$NON-NLS-1$ String title = resultXml.substring(titleLocBegin + 8, titleLocEnd); String versionAndType = title + " " + version + " " + type; //$NON-NLS-1$ //$NON-NLS-2$ // locate the version in the properties String updateProp = PROP_ROOT + "." + dataProvider.getApplicationID() + "." + //$NON-NLS-1$ //$NON-NLS-2$ dataProvider.getApplicationVersion() + "." + PROP_UPDATE; //$NON-NLS-1$ String updateVal = propsToCheck.getProperty(updateProp, ""); //$NON-NLS-1$ // if the version isn't in the list of updates if (updateVal.indexOf(versionAndType) < 0) { if (updateVal.length() > 0) { updateVal += ","; //$NON-NLS-1$ } updateVal += versionAndType; propsToCheck.setProperty(updateProp, updateVal); found = false; } // next update location updateLoc = resultXml.indexOf("<update", updateLoc + 1); //$NON-NLS-1$ } // if suppressExistingUpdates is true and all the updates // listed have been found before, suppress the update if (found && ignoreExistingUpdates) { return "<vercheck protocol=\"1.0\"/>"; //$NON-NLS-1$ } } } return resultXml; } /** * Sets the URL (and parameters) for the request in the HttpMethod. The data provider information is sed to set the * parameters * * @param method * the method which will have the URL set * @throws URIException * Indicates an error creating the URI */ protected void setURL(HttpMethod method, String guid) throws URIException { String urlBase = null; final Map parameters = new HashMap(); // If we have a data provider, get the parameters from there if (dataProvider != null) { // Get the URL urlBase = dataProvider.getBaseURL(); // Get the extra parameters final Map params = dataProvider.getExtraInformation(); if (params != null && params.size() > 0) { parameters.putAll(params); } // Add the specific parameters final String productID = dataProvider.getApplicationID(); final String version = dataProvider.getApplicationVersion(); final int depth = dataProvider.getDepth(); final String vi = computeVI(productID); parameters.put("depth", "" + depth); //$NON-NLS-1$ //$NON-NLS-2$ parameters.put("prodID", productID); //$NON-NLS-1$ parameters.put("version", version); //$NON-NLS-1$ parameters.put("guid", guid); //$NON-NLS-1$ parameters.put("vi", vi); //$NON-NLS-1$ } // Use the default URL if none is specified if (urlBase == null) { urlBase = getDefaultURL(); } // Set the url in the method String urlCreated = createURL(urlBase, parameters); URI uri = new URI(urlCreated, true); method.setURI(uri); } /** * Returns the default URL. This is stored in the properties file * * @return the URL retrieved that should be used as the default URL */ protected String getDefaultURL() { return DEFAULT_URL; } /** * Creats the URL with query string based off the base url and the parameters passed * * @param urlBase * the first part of the url * @param parameters * the parameters to add as part of the query string * @return the complete URL and query string */ @SuppressWarnings("deprecation") protected static String createURL(final String urlBase, Map parameters) { // Create the query string from the url and the parameters final StringBuffer queryString = new StringBuffer(); queryString.append(urlBase); if (parameters != null) { String connector = ""; //$NON-NLS-1$ if (urlBase.indexOf('?') == -1) { connector = "?"; //$NON-NLS-1$ } else if (!urlBase.endsWith("&")) { //$NON-NLS-1$ connector = "&"; //$NON-NLS-1$ } for (final Iterator it = parameters.keySet().iterator(); it.hasNext();) { final Object key = it.next(); if (key != null) { final Object obj = parameters.get(key); final String value = (obj != null ? obj.toString() : ""); //$NON-NLS-1$ queryString.append(connector).append(URLEncoder.encode(key.toString())).append('=') .append(URLEncoder.encode(value)); connector = "&"; //$NON-NLS-1$ } } } // Return the generated query string return queryString.toString(); } /** * Computes the VI field for the data provided. The VI is the MD5 encryption of the concatination of the productID and * the guid. */ protected static final String computeVI(final String productID) { return DigestUtils.md5Hex((productID == null ? "" : productID) + (guid == null ? "" : guid)); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Passes the results along to each of the results processors specified. Each result processing will be handled in a * try/catch block to prevent an exception from chaining upward out of this flow of control. <br> * NOTE: This is not done using threads ... therefore (right or wrong), if one handler takes a long time to process, * the remaining handlers will have to wait * * @param results * the results passed to each handler */ protected void processResults(final String results) { for (final Iterator it = resultHandlers.iterator(); it.hasNext();) { try { final IVersionCheckResultHandler resultHandler = (IVersionCheckResultHandler) it.next(); resultHandler.processResults(results); } catch (final Throwable t) { System.err.println(VersionCheckResourceBundle .getString("VersionChecker.ERROR_0001_ERROR_THROWN_FROM_RESULTS_HANDLER")); // TODO log message //$NON-NLS-1$ } } } /** * Passes the exception information along to the exception handlers. Each exception handler's processing will be * contained in a try/catch block to prevent an error from chaining upwards out of this flow of control. */ protected void handleException(final Exception e) { for (final Iterator it = errorHandlers.iterator(); it.hasNext();) { try { final IVersionCheckErrorHandler errorHandler = (IVersionCheckErrorHandler) it.next(); errorHandler.handleException(e); } catch (final Throwable t) { System.err.println(VersionCheckResourceBundle .getString("VersionChecker.ERROR_0001_ERROR_THROWN_FROM_ERROR_HANDLER")); // TODO log message //$NON-NLS-1$ } } } /** * Returns the HttpClient to be used during processing. If a default HttpClient is not specified, a new HttpClient * will be used. This exists for two reasons: * <ol> * <li>Allows subclasses to specify a HttpClient with different parameters * <li>Unit Testing * </ol> * * @return the HttpClient to be used for processing */ protected HttpClient getHttpClient() { return defaultHttpClient != null ? defaultHttpClient : new HttpClient(); } /** * Returns the GetMethod object to be used during processing. If a default GetMethod is not specified, a new GetMethod * will be used. This exists for two reasons: * <ol> * <li>Allows subclasses to specify a GetMethod with different parameters * <li>Unit Testing * </ol> * NOTE: This method returns a HttpMethod not specifically a GetMethod. This allows a subclass to change over to * PostMethod later. * * @return the HttpMethod to be used during processing. */ protected HttpMethod getHttpMethod() { return defaultHttpMethod != null ? defaultHttpMethod : new GetMethod(); } // ************************************************************************** // * Used for Unit Testing * // ************************************************************************** protected VersionChecker(final HttpClient defaultHttpClient, final HttpMethod defaultHttpMethod) { this(); setDefaultHttpClient(defaultHttpClient); setDefaultHttpMethod(defaultHttpMethod); } protected void setDefaultHttpClient(final HttpClient httpClient) { this.defaultHttpClient = httpClient; } protected void setDefaultHttpMethod(final HttpMethod httpMethod) { this.defaultHttpMethod = httpMethod; } private HttpClient defaultHttpClient; private HttpMethod defaultHttpMethod; }