From 384ed4ca10c768536a0a87aa4eda2bd9bfa6f4d7 Mon Sep 17 00:00:00 2001 From: Steve Malers Date: Thu, 30 Mar 2023 00:54:47 -0600 Subject: [PATCH] Add usage tracking using Google Analytics, issue #257 --- build-util/README.md | 3 +- build-util/copy-to-co-dnr-gcp.bash | 3 +- build-util/create-gcp-tstool-usage-index.bash | 299 ++++++++++++++++++ conf/product.properties | 4 +- src/DWR/DMI/tstool/TSToolMain.java | 116 +++++-- 5 files changed, 399 insertions(+), 26 deletions(-) create mode 100644 build-util/create-gcp-tstool-usage-index.bash diff --git a/build-util/README.md b/build-util/README.md index 91c11d60..dc39afd1 100644 --- a/build-util/README.md +++ b/build-util/README.md @@ -5,7 +5,8 @@ This folder contains useful scripts and other files used in the development/buil | **File/Folder** | **Description** | | -- | -- | | `copy-to-co-dnr-gcp.bash` | Copy the TSTool installer to State of Colorado GCP server. The istaller is in the repository `/dist` folder and the TSTool version correspond to current code version. | -| `create-gcp-tstool-index.bash` | Create and upload an index of TSTool installers. | +| `create-gcp-tstool-index.bash` | Create and upload the index of TSTool installers. | +| `create-gcp-tstool-usage-index.bash` | Create and upload the TSTool usage index. | | `git-check-tstool.sh` | Determine status of TSTool repositories compared to remote repositories. | | `git-clone-all-tstool.sh | Clone all TSTool repositories, helpful when first setting up a development environment. | | `git-tag-all-tstool.sh` | Tag all TSTool repositories with the same tag, used to coordinate releases. | diff --git a/build-util/copy-to-co-dnr-gcp.bash b/build-util/copy-to-co-dnr-gcp.bash index 8d55c432..3ca9854c 100644 --- a/build-util/copy-to-co-dnr-gcp.bash +++ b/build-util/copy-to-co-dnr-gcp.bash @@ -185,9 +185,10 @@ syncFiles() { updateIndex() { local answer echo "" - read -p "Do you want to update the GCP index file [Y/n]? " answer + read -p "Do you want to update the GCP index and usage index files [Y/n]? " answer if [ -z "${answer}" -o "${answer}" = "y" -o "${answer}" = "Y" ]; then ${scriptFolder}/create-gcp-tstool-index.bash + ${scriptFolder}/create-gcp-tstool-usage-index.bash fi } diff --git a/build-util/create-gcp-tstool-usage-index.bash b/build-util/create-gcp-tstool-usage-index.bash new file mode 100644 index 00000000..62bf4e55 --- /dev/null +++ b/build-util/create-gcp-tstool-usage-index.bash @@ -0,0 +1,299 @@ +#!/bin/bash +# +# Create the following files on GCP bucket, such as +# +# opencdss.state.co.us/tstool/14.8.0/usage/index.html +# +# These index.html file contain Google Analytics 4 tracking and are accessed by TSTool +# at startup to track usage by version. + +# Supporting functions, alphabetized. + +# Determine the operating system that is running the script: +# - sets the variable operatingSystem to cygwin, linux, or mingw (Git Bash) +checkOperatingSystem() { + if [ ! -z "${operatingSystem}" ]; then + # Have already checked operating system so return. + return + fi + operatingSystem="unknown" + os=`uname | tr [a-z] [A-Z]` + case "${os}" in + CYGWIN*) + operatingSystem="cygwin" + operatingSystemShort="cyg" + ;; + LINUX*) + operatingSystem="linux" + operatingSystemShort="lin" + ;; + MINGW*) + operatingSystem="mingw" + operatingSystemShort="min" + ;; + esac + echoStderr "" + echoStderr "Detected operatingSystem=${operatingSystem} operatingSystemShort=${operatingSystemShort}" + echoStderr "" +} + +# Echo a string to standard error (stderr). +# This is done so that output printed to stdout is not mixed with stderr. +echoStderr() { + echo "$@" >&2 +} + +# Check whether a file exists on GCP storage: +# - function argument should be Google storage URL gs:opencdss... etc. +gcpUtilFileExists() { + local fileToCheck + fileToCheck=$1 + # The following will return 0 if the file exists, 1 if not. + gsutil.cmd -q stat ${fileToCheck} + return $? +} + +# Get the user's login to local temporary files: +# - Git Bash apparently does not set ${USER} environment variable +# - Set USER as script variable only if environment variable is not already set +# - See: https://unix.stackexchange.com/questions/76354/who-sets-user-and-username-environment-variables +getUserLogin() { + if [ -z "${USER}" ]; then + if [ ! -z "${LOGNAME}" ]; then + USER=${LOGNAME} + fi + fi + if [ -z "${USER}" ]; then + USER=$(logname) + fi + # Else - not critical since used for temporary files. +} + +# Parse the command parameters: +# - use the getopt command line program so long options can be handled +parseCommandLine() { + local optstring optstringLong + local exitCode + local GETOPT_OUT + + # Single character options. + optstring="dhv" + # Long options. + optstringLong="debug,help,version" + # Parse the options using getopt command. + GETOPT_OUT=$(getopt --options ${optstring} --longoptions ${optstringLong} -- "$@") + exitCode=$? + if [ ${exitCode} -ne 0 ]; then + # Error parsing the parameters such as unrecognized parameter. + echoStderr "" + printUsage + exit 1 + fi + # The following constructs the command by concatenating arguments. + eval set -- "${GETOPT_OUT}" + # Loop over the options. + while true; do + #echo "Command line option is ${opt}" + case "${1}" in + --debug) # --debug Indicate to output debug messages. + echoStderr "--debug detected - will print debug messages." + debug="true" + shift 1 + ;; + -h|--help) # -h or --help Print the program usage. + printUsage + exit 0 + ;; + -v|--version) # -v or --version Print the program version. + printVersion + exit 0 + ;; + --) # No more arguments. + shift + break + ;; + *) # Unknown option. + echoStderr "" + echoStderr "Invalid option: ${1}" >&2 + printUsage + exit 1 + ;; + esac + done +} + +# Print the usage. +printUsage() { + echoStderr "" + echoStderr "Usage: ${scriptName}" + echoStderr "" + echoStderr "Create the product GCP usage index file: ${gcpIndexHtmlUrl}" + echoStderr "" + echoStderr "--debug Print debug messages, for troubleshooting." + echoStderr "-h, --help Print usage." + echoStderr "-v, --version Print script version." + echoStderr "" +} + +# Print the version. +printVersion() { + echoStderr "${version}" +} + +# Upload the index.html file for the static website download page +# - this is basic at the moment but can be improved in the future such as +# software.openwaterfoundation.org page, but for only one product, with list of variants and versions +uploadIndexHtmlFile() { + local indexHtmlTmpFile gcpIndexHtmlUrl + + # Create an index.html file for upload. + indexHtmlTmpFile="/tmp/${USER}-tstool-usage-index.html" + gcpIndexHtmlUrl="${gcpFolderUrl}/${tstoolVersion}/usage/index.html" + + echo ' + + + + + + +' > ${indexHtmlTmpFile} + +echo " + + + + +" >> ${indexHtmlTmpFile} + +echo ' +OpenCDSS TSTool Usage Tracking + + +

TSTool Software Usage Tracking

+

+See the OpenCDSS TSTool page, +which provides additional information about TSTool. +

+

+This page is accessed by TSTool at startup to track software usage. +The information helps software developers and support understand which TSTool versions are being used. +

' >> ${indexHtmlTmpFile} + + echo '' >> ${indexHtmlTmpFile} + echo '' >> ${indexHtmlTmpFile} + + echoStderr "" + echoStderr "Uploading the usage/index file:" + echoStderr " from: ${indexHtmlTmpFile}" + echoStderr " to: ${gcpIndexHtmlUrl}" + echoStderr "" + read -p "Continue with upload (Y/n)? " answer + if [ -z "${answer}" -o "${answer}" = "Y" -o "${answer}" = "y" ]; then + # Will continue. + : + else + # Exit the script. + exit 0 + fi + + # If here, continue with the copy. + # set -x + gsutil.cmd cp ${indexHtmlTmpFile} ${gcpIndexHtmlUrl} + # { set +x; } 2> /dev/null + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + echoStderr "" + echoStderr "[Error] Error uploading index.html file." + exit 1 + fi +} + +# Entry point for the script. + +# Get the location where this script is located since it may have been run from any folder. +scriptFolder=$(cd $(dirname "$0") && pwd) +scriptName=$(basename $0) +repoFolder=$(dirname "${scriptFolder}") +srcFolder="${repoFolder}/src" + +# Version, mainly used to help understand changes over time when comparing files. +version="1.1.0 (2021-09-01)" + +# Get the TSTool version for the GCP product version folder. +srcMainFolder="${srcFolder}/DWR/DMI/tstool" +tstoolFile="${srcMainFolder}/TSToolMain.java" +if [ -f "${tstoolFile}" ]; then + tstoolVersion=$(cat ${tstoolFile} | grep -m 1 'PROGRAM_VERSION' | cut -d '=' -f 2 | cut -d '(' -f 1 | tr -d " " | tr -d '"') + tstoolModifierVersion=$(getVersionModifier "${tstoolVersion}") +else + echoStderr "[ERROR] Cannot determine TSTool version because file not found:" + echoStderr "[ERROR] ${tstoolFile}" + exit 1 +fi +if [ -z "${tstoolVersion}" ]; then + echoStderr "[ERROR] Cannot determine TSTool version by scanning:" + echoStderr " ${tstoolFile}" + exit 1 +fi + +echoStderr "scriptFolder=${scriptFolder}" +echoStderr "repoFolder=${repoFolder}" +echoStderr "srcFolder=${srcFolder}" +echoStderr "tstoolVersion=${tstoolVersion}" + +# Whether or not debug messages are printed. +debug="false" + +# Root location where files are to be uploaded. +gcpFolderUrl="gs://opencdss.state.co.us/tstool" + +# Parse the command line parameters. +parseCommandLine $@ + +# Determine the user login, used for temporary file location. +getUserLogin + +# Upload the created index file to GCP bucket. +uploadIndexHtmlFile + +# Exit with status from the upload function. +exit $? diff --git a/conf/product.properties b/conf/product.properties index 02089b11..e1ce79bb 100755 --- a/conf/product.properties +++ b/conf/product.properties @@ -13,12 +13,12 @@ product.disabled.jars=junit-3.8.1 # 13.00.03dev (older - don't use this format) # 13.00.03.dev (newer - use this format) # 14.0.0 or 14.0.0.dev1 (newest) -nsis.version=14.7.0 +nsis.version=14.8.0.dev1 # Executable name (without .exe) exe.name=TSTool # Format below is 0.M.N.n # Don't include "beta" or other suffix in the following. -exe.version=0.14.7.0 +exe.version=0.14.8.0 java.src.version=1.8 # The version of Java included with the build java.target.version=1.8 diff --git a/src/DWR/DMI/tstool/TSToolMain.java b/src/DWR/DMI/tstool/TSToolMain.java index 1b2ec2fb..bb9e1664 100644 --- a/src/DWR/DMI/tstool/TSToolMain.java +++ b/src/DWR/DMI/tstool/TSToolMain.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.URL; @@ -95,7 +96,7 @@ public class TSToolMain * - otherwise, there can be problems with the string being interpreted as hex code by installer tools * - as of version 14, do not pad version parts with zeros */ -public static final String PROGRAM_VERSION = "14.7.0 (2023-03-26)"; +public static final String PROGRAM_VERSION = "14.8.0.dev1 (2023-03-29)"; /** Main GUI instance, used when running interactively. @@ -205,7 +206,7 @@ Indicates whether TSTool is running in HTTP server mode (requires command files * @param pluginJarList List of full path to plugin jar file. */ private static void findPluginDataStoreJarFilesNew ( TSToolSession session, List pluginJarList ) { - String routine = "TSToolMain.findDataStorePluginJarFilesNew"; + String routine = TSToolMain.class.getSimpleName() + ".findDataStorePluginJarFilesNew"; // Get a list of jar files in the user plugins folder (will take precedent over installation files). File userPluginsFolder = new File(session.getUserPluginsFolder()); Message.printStatus(2, routine, "Finding plugin datastore jar files in plugins folder \"" + userPluginsFolder + "\""); @@ -232,7 +233,7 @@ private static void findPluginDataStoreJarFilesNew ( TSToolSession session, List * @param pluginJarList List of full path to plugin jar file. */ private static void findPluginDataStoreJarFilesOld ( TSToolSession session, List pluginJarList ) { - String routine = "TSToolMain.findDatastorePluginJarFilesOld"; + String routine = TSToolMain.class.getSimpleName() + ".findDatastorePluginJarFilesOld"; final String [] pluginHomeFolders = { // InstallHome/plugin-datastore __tstoolInstallHome + File.separator + "plugin-datastore", @@ -349,7 +350,7 @@ private static int getMajorVersion () { */ private static void getMatchingFilenamesInTree ( List fileList, File path, String pattern ) throws IOException { - //String routine = "getMatchingFilenamesInTree"; + //String routine = TSTool.class.getSimpleName() + ".getMatchingFilenamesInTree"; if (path.isDirectory()) { String[] children = path.list(); for (int i = 0; i < children.length; i++) { @@ -448,7 +449,7 @@ private static void initializeLoggingLevelsBeforeLogOpened () { Initialize important data relative to the installation home. */ private static void initializeAfterHomeIsKnown () { - String routine = "TSToolMain.initializeAfterHomeIsKnown"; + String routine = TSToolMain.class.getSimpleName() + ".initializeAfterHomeIsKnown"; // Initialize the system data. @@ -508,7 +509,7 @@ private static void loadPluginDataStores(TSToolSession session, @SuppressWarnings("rawtypes") List pluginDataStoreList, @SuppressWarnings("rawtypes") List pluginDataStoreFactoryList, @SuppressWarnings("rawtypes") List pluginCommandList ) { - final String routine = "TSToolMain.loadPluginDataStores"; + final String routine = TSToolMain.class.getSimpleName() + ".loadPluginDataStores"; // Find jar files that contain datastores. // First use the old approach (TSTool 12.06.00 and earlier). final List pluginJarListOld = new ArrayList<>(); @@ -598,7 +599,7 @@ private static void loadPluginDataStoresOld(String messagePrefix, TSToolSession messagePrefix = "Old"; // Use for messages so it is clear whether processing old or new datastore configurations. } - String routine = "TSToolMain.loadPluginDataStores" + messagePrefix; + String routine = TSToolMain.class.getSimpleName() + ".loadPluginDataStores" + messagePrefix; // Create a separate class loader for each plugin to maintain separation. // From this point forward the jar file path does not care if in the user folder or TSTool installation folder. Message.printStatus(2, routine, "Trying to load plugin datastores from " + pluginJarList.size() + " candidate jar files."); @@ -739,7 +740,7 @@ private static void loadPluginDataStoresOld(String messagePrefix, TSToolSession @param args Command line arguments. */ public static void main ( String args[] ) { - String routine = "TSToolMain.main"; + String routine = TSToolMain.class.getSimpleName() + ".main"; try { // Main try... @@ -833,6 +834,7 @@ public static void main ( String args[] ) { // Run TSTool in the run mode indicated by command line parameters. if ( IOUtil.isBatch() ) { + trackUsage ( "batch" ); // Running like "tstool -commands file" (possibly with -nomaingui). TSCommandFileRunner runner = new TSCommandFileRunner(processorProps, pluginCommandClasses); // If the global timeout is set, start a thread that will time out when the batch run is complete. @@ -911,6 +913,7 @@ public static void main ( String args[] ) { } else if ( isBatchServer() ) { String batchServerHotFolder0 = getBatchServerHotFolder(); + trackUsage ( "batchserver" ); Message.printStatus ( 1, routine, "Starting in batch server mode with hot folder \"" + batchServerHotFolder0 + "\"" ); // TODO SAM 2016-02-09 For now keep code here but may make more modular. // Create a runner that will be re-used. @@ -998,6 +1001,7 @@ else if ( isBatchServer() ) { else if ( isHttpServer() ) { // See: http://stackoverflow.com/questions/3732109/simple-http-server-in-java-using-only-java-se-api // Do something simple for now to test. + trackUsage ( "httpserver" ); int port = 8000; HttpServer server = HttpServer.create(new InetSocketAddress(port),0); String root = "/tstool"; @@ -1007,12 +1011,14 @@ else if ( isHttpServer() ) { } else if ( isRestServer() ) { // Run in server mode using REST API. + trackUsage ( "restservelet" ); runRestletServer(); } else { // Run the UI: // - the processor for the UI is created in the called code Message.printStatus ( 2, routine, "Starting TSTool UI..." ); + trackUsage ( "ui" ); try { __tstool_JFrame = new TSTool_JFrame ( session, @@ -1056,7 +1062,7 @@ protected static DataStore openDataStore ( TSToolSession session, PropList dataS TSCommandProcessor processor, @SuppressWarnings("rawtypes") List pluginDataStoreClassList, @SuppressWarnings("rawtypes") List pluginDataStoreFactoryClassList, boolean isBatch ) throws ClassNotFoundException, IllegalAccessException, InstantiationException, Exception { - String routine = "TSToolMain.openDataStore"; + String routine = TSToolMain.class.getSimpleName() + ".openDataStore"; // Open the datastore depending on the type. String dataStoreType = dataStoreProps.getValue("Type"); String dataStoreConfigFile = dataStoreProps.getValue("DataStoreConfigFile"); @@ -1320,7 +1326,7 @@ Open the datastores (e.g., database and web service connection(s)) using datasto protected static void openDataStoresAtStartup ( TSToolSession session, TSCommandProcessor processor, @SuppressWarnings("rawtypes") List pluginDataStoreClassList, @SuppressWarnings("rawtypes") List pluginDataStoreFactoryClassList, boolean isBatch ) { - String routine = "TSToolMain.openDataStoresAtStartup"; + String routine = TSToolMain.class.getSimpleName() + ".openDataStoresAtStartup"; // Allow multiple database connections via the new convention using datastore configuration files. // The following code processes all datastores. @@ -1467,7 +1473,7 @@ public boolean accept(File dir, String name) { @return opened HydroBaseDMI if the connection was made, or null if a problem. */ public static HydroBaseDMI openHydroBase ( TSCommandProcessor processor ) { - String routine = "TSToolMain.openHydroBase"; + String routine = TSToolMain.class.getSimpleName() + ".openHydroBase"; boolean HydroBase_enabled = false; // Whether HydroBaseEnabled = true in TSTool configuration file. String propval = __tstool_props.getValue ( "TSTool.HydroBaseEnabled"); if ( (propval != null) && propval.equalsIgnoreCase("true") ) { @@ -1549,7 +1555,7 @@ public static HydroBaseDMI openHydroBase ( TSCommandProcessor processor ) { directory is known so that remaining information can be captured in the log file. */ private static void openLogFile ( TSToolSession session ) { - String routine = "TSToolMain.openLogFile"; + String routine = TSToolMain.class.getSimpleName() + ".openLogFile"; String user = IOUtil.getProgramUser(); String logFile = null; @@ -1626,7 +1632,7 @@ private static void openLogFile ( TSToolSession session ) { */ public static void parseArgs ( TSToolSession session, String[] args ) throws Exception -{ String routine = "TSToolMain.parseArgs", message; +{ String routine = TSToolMain.class.getSimpleName() + ".parseArgs", message; int pos = 0; // Position in a string. // Allow setting of -home via system property "tstool.home". @@ -1970,9 +1976,9 @@ private static String parseArgsCheckSpaceReplacement(String arg, String spaceRep /** Print the program usage to the log file. */ -public static void printUsage ( ) -{ String nl = System.getProperty ( "line.separator" ); - String routine = "TSToolMain.printUsage"; +public static void printUsage ( ) { + String nl = System.getProperty ( "line.separator" ); + String routine = TSToolMain.class.getSimpleName() + ".printUsage"; int len = PROGRAM_NAME.length(); String format = "%" + len + "." + len + "s"; String blanks = String.format(format,""); @@ -2020,7 +2026,7 @@ public static void printVersion ( ) @param status Program exit status. */ public static void quitProgram ( int status ) { - String routine = "TSToolMain.quitProgram"; + String routine = TSToolMain.class.getSimpleName() + ".quitProgram"; Message.printStatus ( 1, routine, "Exiting with status " + status + "." ); @@ -2035,7 +2041,7 @@ public static void quitProgram ( int status ) { @param configFile Name of the configuration file. */ private static void readConfigFile ( String configFile ) { - String routine = "TSToolMain.readConfigFile"; + String routine = TSToolMain.class.getSimpleName() + ".readConfigFile"; Message.printStatus ( 2, routine, "Reading TSTool configuration information from \"" + configFile + "\"." ); if ( IOUtil.fileReadable(configFile) ) { __tstool_props = new PropList ( configFile ); @@ -2101,7 +2107,7 @@ else if ( prop.getKey().equalsIgnoreCase("TSTool.DiffProgram.Windows") ) { Run TSTool in restlet server mode. */ private static void runRestletServer () { - String routine = "TSToolMain.runRestletServer()"; + String routine = TSToolMain.class.getSimpleName() + ".runRestletServer()"; try { int port = -1; // Default. TSToolServer server = new TSToolServer(); @@ -2162,7 +2168,7 @@ public static void setIcon ( String iconType ) { indicating that a batch run is requested. */ private static void setupUsingCommandFile ( String command_file_arg, boolean is_batch ) { - String routine = "TSToolMain.setupUsingCommandFile"; + String routine = TSToolMain.class.getSimpleName() + ".setupUsingCommandFile"; // Make sure that the command file is an absolute path because it indicates the working // directory for all other processing. @@ -2228,7 +2234,7 @@ private static void setupUsingCommandFile ( String command_file_arg, boolean is_ Set the working directory as the system "user.dir" property. */ private static void setWorkingDirInitial() { - String routine = "TSToolMain.setWorkingDirInitial"; + String routine = TSToolMain.class.getSimpleName() + ".setWorkingDirInitial"; String working_dir = System.getProperty("user.dir"); IOUtil.setProgramWorkingDir ( working_dir ); // Set the dialog because if the running in batch mode and interaction with the graph occurs, @@ -2268,7 +2274,7 @@ private static void setWorkingDirUsingCommandFile ( String commandFileFull ) { @param timeoutSeconds number of seconds before timing out (ignore if <= 0) */ private static void startTimeoutThread ( int timeoutSeconds ) { - String routine = "startTimeoutThread"; + String routine = TSToolMain.class.getSimpleName() + ".startTimeoutThread"; if ( timeoutSeconds <= 0 ) { // No need to start timeout thread. return; @@ -2312,6 +2318,72 @@ public void run() { */ } +/** + * Do a GET request on the OpenCDSS website to cause Google Analytics to track a TSTool usage. + * @param runMode the TSTool run mode, to understand how TSTool is being called + */ +private static void trackUsage ( String runMode ) { + String routine = TSToolMain.class.getSimpleName() + ".trackUsage"; + // Operating system. + String os = "unknown"; + if ( IOUtil.isUNIXMachine() ) { + // This is not totally right but lump them together. + os = "linux"; + } + else { + os = "windows"; + } + + // Get the version (e.g., 1.2.3). + String [] versionParts = TSToolMain.PROGRAM_VERSION.split(" "); + String version = versionParts[0].trim(); + + String usageUrl = "https://opencdss.state.co.us/tstool/" + version + "/usage/index.html?version=" + version + "&os=" + os; + + // Do a get to use Google Analytics: + // - use a short timeout so that this does not adversely impact TSTool startup time + // - set to follow redirects but only temporarily + boolean oldfollowRedirects = false; // Will be set in the try to the current value. + StopWatch sw = new StopWatch(); + sw.start(); + try { + URL url = new URL(usageUrl); + // Save so can set back to the default when done. + oldfollowRedirects = HttpURLConnection.getFollowRedirects(); + HttpURLConnection con = (HttpURLConnection)url.openConnection(); + // Follow redirects in case the documentation hosting location moves. + HttpURLConnection.setFollowRedirects(true); + int timeoutMs = 100; + con.setConnectTimeout(timeoutMs); + con.setReadTimeout(timeoutMs); + // Open the connection. + con.connect(); + int responseCode = con.getResponseCode(); + if ( responseCode != 200 ) { + Message.printWarning(3,routine,"Error (" + responseCode + + ") getting tracking page. Unable to update TSTool tracking. Assume that network is locked."); + } + else { + // Assume that the GET worked. + } + } + catch ( MalformedURLException e ) { + Message.printWarning(2, "", "Unable to display documentation at \"" + usageUrl + "\" - malformed URL." ); + } + catch ( IOException e ) { + Message.printWarning(2, "", "Unable to display documentation at \"" + usageUrl + "\" - IOException (" + e + ")." ); + } + catch ( Exception e ) { + Message.printWarning(2, "", "Unable to display documentation at \"" + usageUrl + "\" - Exception (" + e + ")." ); + } + finally { + // Reset back to the normal default + HttpURLConnection.setFollowRedirects(oldfollowRedirects); + } + sw.stop(); + Message.printStatus(2, routine, "Took " + sw.getMilliseconds() + "ms to update tracking."); +} + } /**