/* * This file is part of VIUtils. * * Copyright © 2012-2015 Visual Illusions Entertainment * * VIUtils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License along with this library. * If not, see http://www.gnu.org/licenses/lgpl.html. */ package net.visualillusionsent.utils; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.text.MessageFormat; import java.util.StringTokenizer; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Program Checker * <p/> * Used to check if software is the latest version<br> * There is an included programchecker.php in the /viutils/php/ folder inside the jar.<br> * * @author Jason (darkdiplomat) * @version 1.2 * @since 1.3.0 */ public final class ProgramChecker { /* 1.2 @ VIUtils 1.4.1 */ private static final float classVersion = 1.2F; private static final String progNamePreForm = "program=%s", versionForm = "%d.%d.%d", userAgentPreForm = MessageFormat.format("Java/{2} ({3} {4}; {5}; %s/%s;) ProgramChecker/{1,number,0.0} VIUtils/{0}", VIUtils.VERSION, classVersion, SystemUtils.JAVA_VERSION, SystemUtils.OPERATING_SYSTEM, SystemUtils.OS_VERSION, SystemUtils.OS_ARCHITECTURE ); /* {"VERSION":{"MAJOR":"#","MINOR":"#","MICRO":"#"},"STATUS":"$"} or {"VERSION":{"MAJOR":#,"MINOR":#,"MICRO":#},"STATUS":"$"} */ private static final Pattern inputPattern = Pattern.compile("\\{\"VERSION\":\\{\"MAJOR\":(\"\\d+\"|\\d+),\"MINOR\":(\"\\d+\"|\\d+),\"REVISION\":(\"\\d+\"|\\d+)\\},\"STATUS\":\"(\\w+)\"\\}"); private final String progName, userAgent, postOut; private final URL extURL; private final long[] version; private final ProgramStatus status; private long lastCheck; private Long[] latestReported = new Long[]{ 0L, 0L, 0L }; private ProgramStatus checkStatus = ProgramStatus.UNKNOWN; private Status checkResponse = Status.ERROR; private String errorMsg = "ERROR: No query made yet"; private int connTimeOut = 500; private long queryInterval = TimeUnit.MINUTES.toMillis(5); private boolean disabled = false; /** * Creates a new ProgramChecker * * @param progName * the name of the Program being version checked * @param version * the version of the Program being version checked * @param extURL * the {@link URL} path of the programchecker.php script * @param status * the {@link net.visualillusionsent.utils.ProgramStatus} of the Program */ public ProgramChecker(String progName, long[] version, URL extURL, ProgramStatus status) { this.progName = progName; this.version = version; this.extURL = extURL; this.status = status; this.userAgent = String.format(userAgentPreForm, progName, String.format(versionForm, version[0], version[1], version[2])); this.postOut = String.format(progNamePreForm, progName); } /** * Creates a new ProgramChecker * * @param progName * the name of the Program being version checked * @param verMajor * the Major version digit of the Program * @param verMinor * the Minor version digit of the Program * @param verRev * the Revision version digit of the Program * @param extURL * the {@link URL} path of the programchecker.php script * @param status * the {@link net.visualillusionsent.utils.ProgramStatus} of the Program */ public ProgramChecker(String progName, long verMajor, long verMinor, long verRev, URL extURL, ProgramStatus status) { this(progName, new long[]{ verMajor, verMinor, verRev }, extURL, status); } /** * @param progName * the name of the Program being version checked * @param version * the version of the Program * @param extURL * the URL path of the programchecker.php script * @param status * the ProgramStatus of the Program * * @throws Exception * if the version is not a number, or the URL is invalid, or on some other failure */ public ProgramChecker(String progName, String version, String extURL, String status) throws Exception { this(progName, parseVersionString(version), new URL(extURL), ProgramStatus.fromString(status)); } private static long[] parseVersionString(String version) { long[] temp = new long[]{ 0, 0, 0 }; StringTokenizer tokenizer = new StringTokenizer(version, "."); int index = 0; while (tokenizer.hasMoreTokens()) { temp[index++] = Long.parseLong(tokenizer.nextToken()); } return temp; } /** * The reported Status */ public enum Status { LATEST, UPDATE, ERROR, DISABLED } /** * Sets the Connection Timeout in milliseconds * * @param timeOut * the time in milliseconds for a connection timeout */ public final void setConnectionTimeOut(int timeOut) { this.connTimeOut = timeOut; if (connTimeOut < 0) { connTimeOut = 0; } } /** * Sets the time in minutes between actual queries of the external script.<br/> * If set below 1, will default back to 1 * * @param interval * the time in minutes between queries */ public final void setQueryInterval(long interval) { this.queryInterval = interval; if (this.queryInterval < TimeUnit.MINUTES.toMillis(1)) { queryInterval = TimeUnit.MINUTES.toMillis(1); } } /** * Disables the ProgramChecker */ public final void disable() { this.disabled = true; } /** * (Re)Enables the ProgramChecker */ public final void enable() { this.disabled = false; } /** * Parse the input from the PHP Script * * @return inputLine * PHP Script output line of versions/builds */ private String getInput() { if (disabled) { return null; } String received = ""; HttpURLConnection httpConn = null; try { httpConn = getConnection(); httpConn.connect(); httpConn.getOutputStream().write(postOut.getBytes()); httpConn.getOutputStream().flush(); BufferedReader in = new BufferedReader(new InputStreamReader(httpConn.getInputStream())); String temp; while ((temp = in.readLine()) != null) { received += temp; } } catch (Exception ex) { received = "ERROR: " + ex.getMessage(); } finally { if (httpConn != null) { httpConn.disconnect(); } } if (received.isEmpty()) { received = null; } return received; } private HttpURLConnection getConnection() throws IOException { if (disabled) { return null; } HttpURLConnection httpConn = (HttpURLConnection) extURL.openConnection(); httpConn.setConnectTimeout(connTimeOut); httpConn.setReadTimeout(connTimeOut); httpConn.setRequestMethod("POST"); httpConn.setRequestProperty("User-Agent", userAgent); httpConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); httpConn.setRequestProperty("Content-Language", "en-US"); httpConn.setDoOutput(true); return httpConn; } private void parseInput(String input) { Matcher matcher = inputPattern.matcher(input); if (matcher.matches()) { long versionMajor, versionMinor, versionRev; try { versionMajor = Long.parseLong(matcher.group(1).replace("\"", "")); // DIGIT for MAJOR, remove any quotes that may be present versionMinor = Long.parseLong(matcher.group(2).replace("\"", "")); // DIGIT for MINOR, remove any quotes that may be present versionRev = Long.parseLong(matcher.group(3).replace("\"", "")); // DIGIT for REVISION, remove any quotes that may be present checkStatus = ProgramStatus.fromString(matcher.group(4)); // STATUS name } catch (NumberFormatException nfex) { //This probably won't happen given the regex works as intended checkResponse = Status.ERROR; errorMsg = "ERROR: Invalid Response received - Bad Version Numbers"; return; } this.latestReported = new Long[]{ versionMajor, versionMinor, versionRev }; this.checkResponse = Status.UPDATE; // Assume an UPDATE is required if (this.version[0] > versionMajor) { this.checkResponse = Status.LATEST; } else if (this.version[0] == versionMajor) { if (this.version[1] > versionMinor) { this.checkResponse = Status.LATEST; } else if (this.version[1] == versionMinor) { if (this.version[2] > versionRev) { this.checkResponse = Status.LATEST; } else if (this.version[2] == versionRev) { if (this.status.ordinal() >= checkStatus.ordinal()) { this.checkResponse = Status.LATEST; } } } } } else { checkResponse = Status.ERROR; errorMsg = "ERROR: Invalid Response received - Bad Syntax"; } } /** * Checks the status * * @return {@link net.visualillusionsent.utils.ProgramChecker.Status} */ public final Status checkStatus() { if (disabled) { return Status.DISABLED; } long currentTime = System.currentTimeMillis(); if ((lastCheck + queryInterval) <= currentTime) { lastCheck = currentTime; String response = getInput(); if (response.startsWith("ERROR:") || response.startsWith("Fatal")) { checkResponse = Status.ERROR; errorMsg = response; } else { parseInput(response); } } return checkResponse; } /** * Gets the Version reported as Latest in String form * * @return {@link String} representation of the latest version */ public final String getVersionReported() { return String.format(versionForm, latestReported); } /** * Gets the Version reported as Latest as a {@code Long array}<br/> * Index 0 = Major, 1 = Minor, 2 = Micro * * @return {@code Long array} representation of the latest version */ public final Long[] getVersionReport() { return latestReported.clone(); } /** * Gets the {@link net.visualillusionsent.utils.ProgramStatus} reported * * @return {@link net.visualillusionsent.utils.ProgramStatus} reported */ public final ProgramStatus getStatusReport() { return checkStatus; } /** * Gets a pre-generated status message * * @return Status == UPDATE: An update is available for: 'ProgramName' - v'Version' 'Status'<br/> * Status == LATEST: Current Version of: 'ProgramName' is installed<br/> * Status == ERROR: {error message} */ public final String getStatusMessage() { Status status = checkStatus(); switch (status) { case UPDATE: return "An update is available for: '".concat(progName).concat("' - v").concat(getVersionReported()).concat(" ".concat(checkStatus.toString())); case LATEST: return "Current Version of: '".concat(progName).concat("' is installed"); case DISABLED: return "(ProgramChecker Disabled)"; default: return errorMsg; } } /** * Gets this class's version number * * @return the class version */ public static float getClassVersion() { return classVersion; } }