/******************************************************************************************************************* * Authors: SanAndreasP * Copyright: SanAndreasP * License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International * http://creativecommons.org/licenses/by-nc-sa/4.0/ *******************************************************************************************************************/ package de.sanandrew.core.manpack.managers; import com.google.common.collect.Maps; import com.google.gson.*; import de.sanandrew.core.manpack.mod.ConfigurationManager; import de.sanandrew.core.manpack.mod.ModCntManPack; import de.sanandrew.core.manpack.util.MutableString; import de.sanandrew.core.manpack.util.javatuples.Triplet; import net.minecraft.util.EnumChatFormatting; import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.logging.log4j.Level; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * */ public class SAPUpdateManager { public static final List<Triplet<SAPUpdateManager, MutableBoolean, MutableString>> UPD_MANAGERS = new ArrayList<>(); public static final Map<Integer, Boolean> IS_IN_RENDER_QUEUE = Maps.newHashMap(); private boolean checkedForUpdate = false; private Version version; private String modName; private URL updURL; private String modInfoURL; private File modPackedJar; private UpdateFile updInfo = new UpdateFile(); private final int mgrId; public UpdateDownloader downloader; public static synchronized void setChecked(int mgrId) { UPD_MANAGERS.get(mgrId).getValue1().setTrue(); } public static synchronized void setHasUpdate(int mgrId, String version) { setChecked(mgrId); UPD_MANAGERS.get(mgrId).getValue2().set(version); } public static void setInRenderQueue(int mgrId) { IS_IN_RENDER_QUEUE.put(mgrId, true); } private SAPUpdateManager(String modName, Version version, String updateURL, String modURL, File modJar) { this.modName = modName; this.version = version.clone(); this.modInfoURL = modURL; this.modPackedJar = modJar; try { this.updURL = new URL(updateURL); this.updURL.toURI(); // check validity } catch( MalformedURLException | NullPointerException | URISyntaxException e ) { this.updURL = null; ModCntManPack.UPD_LOG.log(Level.WARN, "The URL to the mod version file is invalid!"); e.printStackTrace(); } this.mgrId = UPD_MANAGERS.size(); } public static SAPUpdateManager createUpdateManager(String modName, Version version, String updateURL, String modURL, File modJar) { SAPUpdateManager updMgr = new SAPUpdateManager(modName, version, updateURL, modURL, modJar); if( updMgr.updURL != null ) { UPD_MANAGERS.add(Triplet.with(updMgr, new MutableBoolean(false), new MutableString(""))); IS_IN_RENDER_QUEUE.put(updMgr.mgrId, false); } return updMgr; } private void check() { Runnable threadProcessor = new Runnable() { @Override public void run() { try { ModCntManPack.UPD_LOG.printf(Level.INFO, "Checking for %s update", SAPUpdateManager.this.getModName()); if( SAPUpdateManager.this.getUpdateURL() == null ) { throw new MalformedURLException("[NULL]"); } Gson gson = new GsonBuilder().registerTypeAdapter(UpdateFile.class, new AnnotatedDeserializer<UpdateFile>()).create(); try( BufferedReader in = new BufferedReader(new InputStreamReader(SAPUpdateManager.this.getUpdateURL().openStream())) ) { SAPUpdateManager.this.updInfo = gson.fromJson(in, UpdateFile.class); } catch( IOException | JsonSyntaxException ex ) { ModCntManPack.UPD_LOG.printf(Level.WARN, "Check for Update failed!", ex); } if( SAPUpdateManager.this.updInfo.version == null || SAPUpdateManager.this.updInfo.version.length() < 1 ) { SAPUpdateManager.setChecked(SAPUpdateManager.this.getId()); return; } Version webVersion = SAPUpdateManager.this.updInfo.getVersionInst(); SAPUpdateManager.this.updInfo.version = webVersion.toString(); // reformat the version number to the format major.minor.revision Version currVersion = SAPUpdateManager.this.getVersion(); String currModName = SAPUpdateManager.this.getModName(); if( webVersion.versionType != EnumVersionType.RELEASE ) { if( ConfigurationManager.subscribeToUnstable || currVersion.versionType != EnumVersionType.RELEASE ) { if( webVersion.versionType.ordinal() > currVersion.versionType.ordinal() ) { switch( webVersion.versionType ) { case BETA: ModCntManPack.UPD_LOG.printf(Level.INFO, "A beta for %s is available: %s", currModName, webVersion); break; case RELEASECANDIDATE: ModCntManPack.UPD_LOG.printf(Level.INFO, "A release candidate for %s is available: %s", currModName, webVersion); break; default: ModCntManPack.UPD_LOG.printf(Level.INFO, "No new update for %s is available.", currModName); SAPUpdateManager.setChecked(SAPUpdateManager.this.getId()); return; } SAPUpdateManager.setHasUpdate(SAPUpdateManager.this.mgrId, webVersion.toString()); return; } else if( webVersion.preVersionNr > currVersion.preVersionNr ) { switch( webVersion.versionType ) { case ALPHA: ModCntManPack.UPD_LOG.printf(Level.INFO, "A new alpha for %s is out: %s", currModName, webVersion); break; case BETA: ModCntManPack.UPD_LOG.printf(Level.INFO, "A new beta for %s is out: %s", currModName, webVersion); break; case RELEASECANDIDATE: ModCntManPack.UPD_LOG.printf(Level.INFO, "A new release candidate for %s is out: %s", currModName, webVersion); break; default: ModCntManPack.UPD_LOG.printf(Level.INFO, "No new update for %s is available.", currModName); SAPUpdateManager.setChecked(SAPUpdateManager.this.getId()); return; } SAPUpdateManager.setHasUpdate(SAPUpdateManager.this.mgrId, webVersion.toString()); return; } } } else { if( webVersion.major > currVersion.major ) { ModCntManPack.UPD_LOG.printf(Level.INFO, "New major update for %s is out: %s", currModName, webVersion); SAPUpdateManager.setHasUpdate(SAPUpdateManager.this.mgrId, webVersion.toString()); return; } else if( webVersion.major == currVersion.major ) { if( webVersion.minor > currVersion.minor ) { ModCntManPack.UPD_LOG.printf(Level.INFO, "New minor update for %s is out: %s", currModName, webVersion); SAPUpdateManager.setHasUpdate(SAPUpdateManager.this.mgrId, webVersion.toString()); return; } else if( webVersion.minor == currVersion.minor ) { if( webVersion.revision > currVersion.revision ) { ModCntManPack.UPD_LOG.printf(Level.INFO, "New bugfix update for %s is out: %s", currModName, webVersion); SAPUpdateManager.setHasUpdate(SAPUpdateManager.this.mgrId, webVersion.toString()); return; } else if( webVersion.revision == currVersion.revision && currVersion.versionType != EnumVersionType.RELEASE ) { ModCntManPack.UPD_LOG.printf(Level.INFO, "A stable release for %s is available: %s", currModName, webVersion); SAPUpdateManager.setHasUpdate(SAPUpdateManager.this.mgrId, webVersion.toString()); return; } } } } ModCntManPack.UPD_LOG.printf(Level.INFO, "No new update for %s is available.", currModName); } catch( IOException ioex ) { ModCntManPack.UPD_LOG.printf(Level.WARN, String.format("Update Check for %s failed!", SAPUpdateManager.this.modName), ioex); } SAPUpdateManager.setChecked(SAPUpdateManager.this.getId()); } }; new Thread(threadProcessor, "SAPUpdateThread").start(); } public void checkForUpdate() { if( !this.checkedForUpdate ) { this.check(); this.checkedForUpdate = true; } } public Version getVersion() { return this.version; } public String getModName() { return this.modName; } public URL getUpdateURL() { return this.updURL; } public String getModInfoURL() { return this.modInfoURL; } public File getModJar() { return this.modPackedJar; } public boolean isModJarValid() { return this.modPackedJar != null && this.modPackedJar.isFile() && this.modPackedJar.getName().endsWith(".jar"); } public int getId() { return this.mgrId; } public UpdateFile getUpdateInfo() { return this.updInfo; } public EnumUpdateSeverity getVersionDiffSeverity() { if( this.updInfo.severityOverride != null && this.updInfo.severityOverride.length() > 0 ) { return EnumUpdateSeverity.valueOf(this.updInfo.severityOverride); } Version updVersion = new Version(this.updInfo.version); if( updVersion.major > this.version.major ) { return EnumUpdateSeverity.SEVERE; } else if( updVersion.major == this.version.major ) { if( updVersion.minor >= this.version.minor + 4 ) { return EnumUpdateSeverity.SEVERE; } else if( updVersion.minor > this.version.minor ) { return EnumUpdateSeverity.MAJOR; } else if( updVersion.minor == this.version.minor ) { if( updVersion.revision >= this.version.revision + 8 ) { return EnumUpdateSeverity.SEVERE; } else if( updVersion.revision >= this.version.revision + 4 ) { return EnumUpdateSeverity.MAJOR; } else if( updVersion.revision > this.version.revision ) { return EnumUpdateSeverity.MINOR; } } } return EnumUpdateSeverity.UNKNOWN; } public void runUpdate() { URL dl = this.updInfo.getDownload(); if( dl != null ) { this.downloader = new UpdateDownloader(dl, this.modPackedJar); } } public static class UpdateFile { @JsonRequired public String version; public String downloadUrl; public String description; public String severityOverride; public String[] changelog; public UpdateFile() { } public URL getDownload() { try { URL dl = new URL(this.downloadUrl); dl.toURI(); return dl; } catch( MalformedURLException | URISyntaxException e ) { return null; } } public Version getVersionInst() { return new Version(version); } } public enum EnumUpdateSeverity { MINOR(EnumChatFormatting.GREEN), MAJOR(EnumChatFormatting.YELLOW), SEVERE(EnumChatFormatting.RED), UNKNOWN(EnumChatFormatting.WHITE); public final EnumChatFormatting format; EnumUpdateSeverity(EnumChatFormatting formatting) { this.format = formatting; } } public static final class Version implements Cloneable { public final int revision; public final int minor; public final int major; public final EnumVersionType versionType; public final int preVersionNr; private static final Pattern[] VERSION_PATTERNS = new Pattern[] { // {1.7.10-}1.0.0{-beta{.2}} Pattern.compile("(\\d+.\\d+[\\.|_]\\d+-)?(?<major>\\d+)\\.(?<minor>\\d+)[\\.|_](?<revision>\\d+)(-(?<prType>alpha|beta|rc)(\\.(?<prNr>\\d+))?)?") }; public Version(int majorNr, int minorNr, int revisionNr) { this.major = majorNr; this.minor = minorNr; this.revision = revisionNr; this.versionType = EnumVersionType.RELEASE; this.preVersionNr = 0; } public Version(int majorNr, int minorNr, int revisionNr, EnumVersionType type, int preVersionNr) { this.major = majorNr; this.minor = minorNr; this.revision = revisionNr; this.versionType = type; this.preVersionNr = preVersionNr; } public Version(String version) { if( version != null ) { Matcher matcher; for( Pattern verPattern : VERSION_PATTERNS ) { matcher = verPattern.matcher(version); if( matcher.find() ) { this.major = Integer.valueOf(matcher.group("major")); this.minor = Integer.valueOf(matcher.group("minor")); this.revision = Integer.valueOf(matcher.group("revision")); if( matcher.group("prType") != null ) { String prType = matcher.group("prType"); switch( prType ) { case "alpha": this.versionType = EnumVersionType.ALPHA; break; case "beta": this.versionType = EnumVersionType.BETA; break; case "rc": this.versionType = EnumVersionType.RELEASECANDIDATE; break; default: this.versionType = EnumVersionType.UNKNOWN; } this.preVersionNr = matcher.group("prNr") != null ? Integer.valueOf(matcher.group("prNr")) : 1; } else { this.versionType = EnumVersionType.RELEASE; this.preVersionNr = 0; } return; } } } this.major = -1; this.minor = -1; this.revision = -1; this.versionType = EnumVersionType.UNKNOWN; this.preVersionNr = -1; // FMLLog.log(ModCntManPack.UPD_LOG, Level.WARN, // "Version Number for the mod %s could not be compiled! The version number %s does not have the required formatting!", // this.modName, version); } @Override public String toString() { if( this.versionType == EnumVersionType.RELEASE ) { return String.format("%d.%d.%d", this.major, this.minor, this.revision); } else { return String.format("%d.%d.%d-%s.%d", this.major, this.minor, this.revision, this.versionType, this.preVersionNr); } } @Override public Version clone() { return new Version(this.major, this.minor, this.revision, this.versionType, this.preVersionNr); } } public enum EnumVersionType { ALPHA("alpha"), BETA("beta"), RELEASECANDIDATE("rc"), RELEASE, UNKNOWN; private final String versionStr; EnumVersionType() { this.versionStr = this.name(); } EnumVersionType(String verStr) { this.versionStr = verStr; } @Override public String toString() { return this.versionStr; } } /** * code from http://stackoverflow.com/a/14245807 */ @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.FIELD ) @interface JsonRequired { } static class AnnotatedDeserializer<T> implements JsonDeserializer<T> { public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { T pojo = new Gson().fromJson(je, type); Field[] fields = pojo.getClass().getDeclaredFields(); for( Field f : fields ) { if( f.getAnnotation(JsonRequired.class) != null ) { try { f.setAccessible(true); if( f.get(pojo) == null ) { throw new JsonParseException("Missing field in JSON: " + f.getName()); } } catch( IllegalArgumentException | IllegalAccessException ex ) { ModCntManPack.UPD_LOG.log(Level.WARN, ex, null); } } } return pojo; } } }