package org.openslx.dozmod.util; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.jar.Attributes; import java.util.jar.Manifest; import org.apache.commons.io.FileUtils; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.log4j.Logger; import org.openslx.dozmod.App; import org.openslx.dozmod.Branding; import org.openslx.sat.thrift.version.Version; import org.openslx.util.Json; public class ClientVersion { private static final Logger LOGGER = Logger.getLogger(ClientVersion.class); private static long localRevisionTime = 0; private static long remoteRevisionTime = 0; private static String localRevision = "???"; private static String remoteRevision = "???"; private static String changelog = "???"; private static Thread remoteThread = null; static { loadLocalVersion(); loadRemoteVersion("https://" + Branding.getMasterServerAddress() + "/dozmod/" + Version.VERSION + "/version.json"); } /** * Gets the local revision id if loading it worked, "???" otherwise. * * @return id as String */ public static String getLocalRevision() { return localRevision; } /** * @return */ public static long getLocalRevTimestamp() { return localRevisionTime; } /** * Gets the revision id of the latest remote version * * @return id as String if loading worked, "???" otherwise */ public static String getRemoteRevision() { waitRemote(); return remoteRevision; } /** * Gets the timestamp of the latest remote version * * @return timestamp as Long if loading it worked, 0L otherwise */ public static long getRemoteRevTimestamp() { waitRemote(); return remoteRevisionTime; } /** * Gets the changelog * * @return log as String */ public static String getChangelog() { waitRemote(); return changelog; } /** * Checks if we are running latest application version * * @return true if there is no newer version, false otherwise */ public static boolean isNewest() { // if either local or remote version is unknown, just pretend there's no update // as there most likely isn't and we'd just annoy the user // TODO: Report "fail" state so at least on manual update check we can tell that it failed waitRemote(); if (localRevisionTime == 0 || remoteRevisionTime == 0) return true; return localRevisionTime >= remoteRevisionTime; } /** * Loads the local version information from the jar's MANIFEST.MF * into the fields 'localRevision' and 'localRevisionTime' */ private static void loadLocalVersion() { Class clazz = ClientVersion.class; String className = clazz.getSimpleName() + ".class"; String classPath = clazz.getResource(className).toString(); if (!classPath.startsWith("jar")) { // Class not from JAR return; } String manifestPath = classPath.replaceAll("![^!]*$", "") + "!/META-INF/MANIFEST.MF"; Manifest manifest = null; try (InputStream stream = new URL(manifestPath).openStream()) { manifest = new Manifest(stream); } catch (Exception e) { if (manifest == null) { LOGGER.error("Could not open MANIFEST", e); return; } } Attributes attributes = manifest.getMainAttributes(); // if attr are null, then we couldn't open the jar's MANIFEST // since we are probably not in a jar context, just do nothing if (attributes == null) return; String manifestRev = null; String manifestRevTime = null; try { manifestRev = attributes.getValue("Build-Revision"); } catch (Exception e) { LOGGER.warn("Error while reading revision: ", e); } try { manifestRevTime = attributes.getValue("Build-Revision-Timestamp"); } catch (Exception e) { LOGGER.warn("Error while reading timestamp: ", e); } if (manifestRev != null) { localRevision = manifestRev; } if (manifestRevTime != null) { try { // hax since we get milliseconds not seconds localRevisionTime = Long.valueOf(manifestRevTime) / 1000L; } catch (NumberFormatException e) { LOGGER.warn("Build timestamp is not a number!", e); } } } /** * Loads the given UrlString as JSON and saves the remote information * into fields 'remoteRevision' and 'remoteRevisionTime' * * The remote JSON should have 'timestamp' and 'revision', like: * { "timestamp": 1, "revision": 2 } */ private static void loadRemoteVersion(final String urlString) { remoteThread = new Thread(new Runnable() { @Override public void run() { App.waitForInit(); String json = null; try (InputStream reader = new URL(urlString).openStream()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); buffer.write(reader); buffer.close(); json = new String(buffer.toByteArray(), StandardCharsets.UTF_8); } catch (Exception e) { if (json == null) { LOGGER.error("Could not fetch remote version", e); return; } } VersionQuery query = Json.deserialize(json, VersionQuery.class); remoteRevision = query.revision; // seconds timestamp here... remoteRevisionTime = query.timestamp; changelog = query.changelog; } }); remoteThread.start(); } private static synchronized void waitRemote() { if (remoteThread == null) return; try { remoteThread.join(); } catch (InterruptedException e) { } remoteThread = null; } /** * Class for GSON json parsing */ static class VersionQuery { long timestamp; String revision; String changelog; } public static void createJson(String name) throws IOException { loadLocalVersion(); if (localRevisionTime == 0) throw new RuntimeException("Missing manifest/data in jar: No revision time found"); if (localRevision == null || localRevision.isEmpty()) throw new RuntimeException("Missing manifest/data in jar: No commit hash found"); System.out.println("Please enter change log. To finish, put a '.' on a single line"); StringBuilder sb = new StringBuilder(); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); for (String line; (line = br.readLine()) != null;) { if (line.equals(".")) break; sb.append(line); sb.append('\n'); } VersionQuery vq = new VersionQuery(); vq.timestamp = localRevisionTime; vq.revision = localRevision; vq.changelog = sb.toString(); String data = Json.serialize(vq); FileUtils.writeStringToFile(new File(name), data, StandardCharsets.UTF_8); System.out.println(); System.out.println("Created json file at " + name); System.out.println("This build is using Thrift RPC interface version >> " + Version.VERSION + " <<"); System.out.println(); } }