r2457 - in trunk/nuiton-utils/src: main/java/org/nuiton/util test/java/org/nuiton/util test/resources test/resources/ApplicationUpdater test/resources/ApplicationUpdater/App1 test/resources/ApplicationUpdater/App2 test/resources/ApplicationUpdater/App3 test/resources/ApplicationUpdater/zip test/resources/properties
Author: bpoussin Date: 2013-01-04 16:05:56 +0100 (Fri, 04 Jan 2013) New Revision: 2457 Url: http://nuiton.org/projects/nuiton-utils/repository/revisions/2457 Log: Evolution #2502: Add class to check and download new application version Added: trunk/nuiton-utils/src/main/java/org/nuiton/util/ApplicationUpdater.java trunk/nuiton-utils/src/test/java/org/nuiton/util/ApplicationUpdaterTest.java trunk/nuiton-utils/src/test/resources/ApplicationUpdater/ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/Readme.txt trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/version.appup trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App2/ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App2/Readme.txt trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/Readme.txt trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/version.appup trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App1-0.3.zip trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App2-7.zip trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App3-7.zip trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterNetworkTest.properties trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterTest.properties Added: trunk/nuiton-utils/src/main/java/org/nuiton/util/ApplicationUpdater.java =================================================================== --- trunk/nuiton-utils/src/main/java/org/nuiton/util/ApplicationUpdater.java (rev 0) +++ trunk/nuiton-utils/src/main/java/org/nuiton/util/ApplicationUpdater.java 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,463 @@ +package org.nuiton.util; + + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.SocketAddress; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Permet de telecharger des mises a jour d'application. + * + * Le principe est qu'un fichier properties pointe par une URL indique les + * information necessaire pour la recuperation de l'application. + * + * Si une nouvelle version de l'application existe, elle est alors telechargee + * et dezipper dans un repertoire specifique (elle ne remplace pas l'application + * courante). + * + * Il est alors a la charge d'un script de mettre en place cette nouvelle application + * a la place de l'ancienne. + * + * Il est possible d'interagir avec ApplicationUpdater via l'implantation d'un + * {@link ApplicationUpdaterCallback} passer en parametre de la methode {@link #update} + * + * <h3>Configuration possible</h3> + * Vous pouvez passer un ApplicationConfig dans le constructeur ou utiliser + * la recherche du fichier de configuration par defaut (ApplicationUpdater.properties) + * + * Cette configuration permet de récupérer les informations suivantes: + * <li>http_proxy: le proxy a utiliser pour l'acces au reseau (ex: squid.chezmoi.fr:8080) + * <li>os.name: le nom du systeme d'exploitation sur lequel l'application fonctionne (ex: Linux) + * <li>os.arch: l'architecture du systeme d'exploitation sur lequel l'application fonctionne (ex: amd64) + * + * <h3>format du fichier de properties</h3> + * [osName.][osArch.]appName.version=version de l'application + * [osName.][osArch.]appName.url=url du zip de la nouvelle version + * + * appName est a remplacer par le nom de l'application. Il est possible + * d'avoir plusieurs application dans le meme fichier ou plusieurs version + * en fonction de l'os et de l'architecture. + * + * osName et osArch sont toujours en minuscule + * + * @author poussin + * @version $Revision$ + * + * Last update: $Date$ + * by : $Author$ + * + * @since 2.7 + */ +public class ApplicationUpdater { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + static private Log log = LogFactory.getLog(ApplicationUpdater.class); + + final static private String SEPARATOR_KEY = "."; + + final static public String HTTP_PROXY = "http_proxy"; + + final static public String URL_KEY = "url"; + final static public String VERSION_KEY = "version"; + final static public String VERSION_FILE = "version.appup"; + + protected ApplicationConfig config; + + /** + * Utilise le fichier de configuration par defaut: ApplicationUpdater.properties + */ + public ApplicationUpdater() { + this(null); + } + + /** + * + * @param config La configuration a utiliser pour rechercher le proxy (http_proxy) + * et os.name, os.arch + */ + public ApplicationUpdater(ApplicationConfig config) { + if (config == null) { + try { + config = new ApplicationConfig( + ApplicationUpdater.class.getSimpleName() + ".properties"); + config.parse(); + config = config.getSubConfig( + ApplicationUpdater.class.getSimpleName() + SEPARATOR_KEY); + } catch (ArgumentsParserException eee) { + throw new RuntimeException(eee); + } + } + this.config = config; + } + + + + /** + * + * @param url url where properties file is downloadable. This properties + * must contains information on application release + * @param currentDir directory where application is currently + * @param destDir default directory to put new application version, can be null if you used callback + * @param async if true, check is done in background mode + * @param callback callback used to interact with updater, can be null + */ + public void update(URL propertiesURL, File currentDir, File destDir, boolean async, ApplicationUpdaterCallback callback) { + Updater up = new Updater(config, propertiesURL, currentDir, destDir, callback); + if (async) { + Thread thread = new Thread(up, ApplicationUpdater.class.getSimpleName()); + thread.start(); + } else { + up.run(); + } + } + + /** + * Permet d'interagir avec ApplicationUpdater + */ + static public interface ApplicationUpdaterCallback { + /** + * Appeler avant la recuperation des nouvelles versions + * + * Permet de modifier le repertoire destination ou l'url du zip de + * l'application pour une application/version + * particuliere ou d'annuler la mise a jour en le supprimant de la map + * qui sera retourne + * + * @param appToUpdate liste des applications a mettre a jour + * @return null or empty map if we don't want update, otherwize list of + * app to update + * + */ + Map<String, ApplicationInfo> updateToDo(Map<String, ApplicationInfo> appToUpdate); + + /** + * Appeler une fois qu'une mise a jour a parfaitement fonctionne + * + * @param name le nom de l'application + * @param oldVersion l'ancienne version + * @param newVersion la nouvelle version + * @param applicationURL l'url d'ou provient le zip de l'application + * @param dest le repertoire ou se trouve la nouvelle version + */ + void updateDone( + Map<String, ApplicationInfo> appToUpdate, + Map<String, Exception> appUpdateError); + + /** + * Called when exception occur during process initialization + * @param propertiesURL url use to download properties release information + * @param eee exception throw during process + */ + void aborted(String propertiesURL, Exception eee); + } + + static public class ApplicationInfo { + public String name; + public String oldVersion; + public String newVersion; + public String url; + public File destDir; + + public ApplicationInfo(String name, String oldVersion, String newVersion, String url, File destDir) { + this.name = name; + this.oldVersion = oldVersion; + this.newVersion = newVersion; + this.url = url; + this.destDir = destDir; + } + + @Override + public String toString() { + String result = String.format( + "App: %s, oldVersion: %s, newVersion: %s, url: %s, destDir:%s", + name, oldVersion, newVersion, url, destDir); + return result; + } + + } + + /** + * La classe ou le travail est reellement fait, peut-etre appeler dans + * un thread si necessaire + */ + static public class Updater implements Runnable { + + protected ApplicationConfig config; + protected URL url; + protected File currentDir; + protected File destDir; + protected ApplicationUpdaterCallback callback; + + public Updater(ApplicationConfig config, URL url, + File currentDir, File destDir, ApplicationUpdaterCallback callback) { + this.config = config; + this.url = url; + this.currentDir = currentDir; + this.destDir = destDir; + this.callback = callback; + } + + /** + * <li>Recupere le fichier properties contenant les informations de mise a jour + * <li>liste les applications et leur version actuelle + * <li>pour chaque application a mettre a jour recupere le zip et le decompresse + * + * Si callback existe envoi les messages necessaire + */ + public void run() { + try { + Proxy proxy = getProxy(config); + ApplicationConfig releaseConfig = getUpdaterConfig(proxy); + + List<String> appNames = getApplicationName(releaseConfig); + Map<String, String> appVersions = getCurrentVersion(appNames, currentDir); + + log.debug("application current version: " + appVersions); + + // recherche des applications a mettre a jour + Map<String, ApplicationInfo> appToUpdate = new HashMap<String, ApplicationInfo>(); + for (String app : appNames) { + String currentVersion = appVersions.get(app); + String newVersion = releaseConfig.getOption(app + SEPARATOR_KEY + VERSION_KEY); + boolean greater = VersionUtil.greaterThan(newVersion, currentVersion); + log.debug(String.format("for %s Current(%s) < newVersion(%s) ? %s", + app, currentVersion, newVersion, greater)); + if (greater) { + String urlString = releaseConfig.getOption( + app + SEPARATOR_KEY + URL_KEY); + + appToUpdate.put(app, new ApplicationInfo( + app, currentVersion, newVersion, urlString, destDir)); + } + } + + // offre la possibilite a l'appelant de modifier les valeurs par defaut + if (callback != null) { + appToUpdate = callback.updateToDo(appToUpdate); + } + + // mise a jour + Map<String, Exception> appUpdateError = new HashMap<String, Exception>(); + for (Map.Entry<String, ApplicationInfo> appInfo : appToUpdate.entrySet()) { + String app = appInfo.getKey(); + ApplicationInfo info = appInfo.getValue(); + try { + doUpdate(proxy, appInfo.getValue()); + } catch (Exception eee) { + appUpdateError.put(app, eee); + try { + // clear data if error occur during uncompress operation + File dest = new File(info.destDir, info.name); + if (dest.exists()) { + log.debug(String.format("Cleaning destination directory due to error '%s'", dest)); + FileUtils.deleteDirectory(dest); + } + } catch(Exception doNothing) { + log.debug("Can't clean directory", doNothing); + } + + + log.warn(String.format( + "Can't update application '%s' with url '%s'", + app, info.url)); + log.debug("Application update aborted because: ", eee); + } + } + + // envoi le resultat a l'appelant s'il le souhaite + if (callback != null) { + callback.updateDone(appToUpdate, appUpdateError); + } + } catch(Exception eee) { + log.warn("Can't update"); + log.info("Application update aborted because: ", eee); + if (callback != null) { + callback.aborted(String.valueOf(url), eee); + } + } + } + + /** + * Decompresse le zip qui est pointer par l'url dans le repertoire + * specifie, et ajoute le fichier contenant la version de l'application. + * Le repertoire root du zip est renomme par le nom de l'application. + * Par exemple si un fichier se nomme "monApp-1.2/Readme.txt" il se + * nommera au final "monApp/Readme.txt" + * + * @param proxy le proxy a utiliser pour la connexion a l'url + * @param info information sur l'application a mettre a jour + * @throws Exception + */ + protected void doUpdate(Proxy proxy, ApplicationInfo info) throws Exception { + if (info.destDir != null) { + // suppression d'une ancienne version si elle existait + File dest = new File(info.destDir, info.name); + if (dest.exists()) { + log.warn(String.format("Remove destination directory for new data '%s'", dest)); + FileUtils.deleteDirectory(dest); + } + + URL applicationURL = toURL(info.url); + InputStream in = new BufferedInputStream( + applicationURL.openConnection(proxy).getInputStream()); + ZipUtil.uncompressAndRename(in, info.destDir, "^[^/]+", info.name); + File versionFile = new File(info.destDir, info.name + File.separator + VERSION_FILE); + FileUtils.writeStringToFile(versionFile, info.newVersion); + log.info(String.format( + "Application '%s' is uptodate with version '%s' in '%s'", + info.name, info.newVersion, info.destDir)); + } else { + log.info(String.format("Update for '%s' aborted because destination dir is set to null", info.name)); + } + } + + /** + * Converti le path en URL. Path doit etre une URL, mais pour les fichiers + * au lieu d'etre absolue ils peuvent etre relatif, un traitement special + * est donc fait pour ce cas. Cela est necessaire pour facilement faire + * des tests unitaires independant de la machine ou il sont fait + * + * @param path + * @return + */ + protected URL toURL(String path) throws MalformedURLException { + URL result; + if (StringUtils.startsWith(path, "file:")) { + File f = new File(StringUtils.substringAfter(path, "file:")); + result = f.toURI().toURL(); + } else { + result = new URL(path); + } + return result; + } + + /** + * Return config prepared for os and arch + * + * @return + * @throws Exception + */ + protected ApplicationConfig getUpdaterConfig(Proxy proxy) throws Exception { + String osName = StringUtils.lowerCase(config.getOsName()); + String osArch = StringUtils.lowerCase(config.getOsArch()); + + if (log.isDebugEnabled()) { + log.debug(String.format( + "Try to load properties from '%s' with proxy '%s'", + url, proxy)); + } + + InputStream in = new BufferedInputStream( + url.openConnection(proxy).getInputStream()); + Properties prop = new Properties(); + prop.load(in); + + if (log.isDebugEnabled()) { + log.debug(String.format( + "Properties loaded from '%s'\n%s", + url, prop)); + } + + // load config with new properties as default + ApplicationConfig result = new ApplicationConfig(prop); + // don't parse. We want only prop in applicationConfig + result = result.getSubConfig( + ApplicationUpdater.class.getSimpleName() + SEPARATOR_KEY); + + result = result.getSubConfig(osName + SEPARATOR_KEY); + result = result.getSubConfig(osArch + SEPARATOR_KEY); + return result; + } + + /** + * Recupere le proxy http a utiliser pour les connexions reseaux + * + * @param config + * @return + */ + protected Proxy getProxy(ApplicationConfig config) { + Proxy result = Proxy.NO_PROXY; + String proxyHost = config.getOption(HTTP_PROXY); + try { + proxyHost = StringUtils.substringAfter(proxyHost, "://"); + if (StringUtils.isNotBlank(proxyHost)) { + String hostname = StringUtils.substringBefore(proxyHost, ":"); + String port = StringUtils.substringAfter(proxyHost, ":"); + if (StringUtils.isNumeric(port)) { + int portNumber = Integer.parseInt(port); + SocketAddress socket = new InetSocketAddress(hostname, portNumber); + result = new Proxy(Proxy.Type.HTTP, socket); + } else { + log.warn(String.format("Invalide proxy port number '%s', not used proxy", port)); + } + } + } catch (Exception eee) { + log.warn(String.format("Can't use proxy '%s'", proxyHost), eee); + } + return result; + } + + /** + * Recherche pour chaque application la version courante + * @param apps la liste des applications a rechercher + * @return + */ + protected Map<String, String> getCurrentVersion(List<String> apps, File dir) { + Map<String, String> result = new HashMap<String, String>(); + for (String app : apps) { + File f = new File(dir, app + File.separator + VERSION_FILE); + String version = "0"; + try { + version = FileUtils.readFileToString(f); + } catch (IOException ex) { + log.warn(String.format( + "Can't find file version '%s' for application '%s', this file should be '%s'", + VERSION_FILE, app, f)); + } + version = StringUtils.trim(version); + result.put(app, version); + } + return result; + } + + /** + * Retourne la liste des noms d'application se trouvant dans la + * configuration + * + * @param config + * @return + */ + protected List<String> getApplicationName(ApplicationConfig config) { + Pattern p = Pattern.compile("([^.]+)\\.version"); + List<String> result = new LinkedList<String>(); + for (String v : config.getFlatOptions().stringPropertyNames()) { + Matcher match = p.matcher(v); + if (match.matches()) { + result.add(match.group(1)); + } else if (StringUtils.endsWith(v, ".version")) { + log.debug(String.format("value is not valid application version '%s'",v)); + } + } + return result; + } + + } + +} Added: trunk/nuiton-utils/src/test/java/org/nuiton/util/ApplicationUpdaterTest.java =================================================================== --- trunk/nuiton-utils/src/test/java/org/nuiton/util/ApplicationUpdaterTest.java (rev 0) +++ trunk/nuiton-utils/src/test/java/org/nuiton/util/ApplicationUpdaterTest.java 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,62 @@ +package org.nuiton.util; + + +import java.io.File; +import java.net.URL; +import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.nuiton.util.ApplicationUpdater.ApplicationInfo; + +/** + * + * @author poussin + * @version $Revision$ + * + * Last update: $Date$ + * by : $Author$ + */ +public class ApplicationUpdaterTest { + + /** to use log facility, just put in your code: log.info(\"...\"); */ + static private Log log = LogFactory.getLog(ApplicationUpdaterTest.class); + + static private class Callback implements ApplicationUpdater.ApplicationUpdaterCallback { + + public Map<String, ApplicationInfo> updateToDo(Map<String, ApplicationInfo> appToUpdate) { + log.info("Application to update\n" + appToUpdate); + return appToUpdate; + } + + public void updateDone(Map<String, ApplicationInfo> appToUpdate, Map<String, Exception> appUpdateError) { + for (Map.Entry<String, Exception> e : appUpdateError.entrySet()) { + log.info(String.format("Error during update for application '%s'", e.getKey()), e.getValue()); + } + } + + public void aborted(String propertiesURL, Exception eee) { + log.info(String.format("Update aborted for url '%s'", propertiesURL), eee); + } + + } + + @Test + public void testUpdate() throws Exception { + ApplicationUpdater up = new ApplicationUpdater(); + URL url = new File("src/test/resources/properties/ApplicationUpdaterTest.properties").toURI().toURL(); + File current = new File("src/test/resources/ApplicationUpdater"); + File dest = new File("target/test/ApplicationUpdater/NEW"); + up.update(url, current, dest, false, new Callback()); + } + + @Test + public void testUpdateNetwork() throws Exception { + ApplicationUpdater up = new ApplicationUpdater(); + URL url = new URL("http://svn.nuiton.org/svn/nuiton-utils/trunk/nuiton-utils/src/test/resources..."); + File current = new File("src/test/resources/ApplicationUpdater"); + File dest = new File("target/test/ApplicationUpdater/NEWNETWORK"); + up.update(url, current, dest, false, new Callback()); + } + +} Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/Readme.txt =================================================================== --- trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/Readme.txt (rev 0) +++ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/Readme.txt 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,2 @@ +Application 1 +v0.1 Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/version.appup =================================================================== --- trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/version.appup (rev 0) +++ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App1/version.appup 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1 @@ +0.1 \ No newline at end of file Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App2/Readme.txt =================================================================== --- trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App2/Readme.txt (rev 0) +++ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App2/Readme.txt 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,2 @@ +Application 2 +v4 Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/Readme.txt =================================================================== --- trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/Readme.txt (rev 0) +++ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/Readme.txt 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,2 @@ +Application 3 +v7.1 Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/version.appup =================================================================== --- trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/version.appup (rev 0) +++ trunk/nuiton-utils/src/test/resources/ApplicationUpdater/App3/version.appup 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1 @@ +7.1 \ No newline at end of file Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App1-0.3.zip =================================================================== (Binary files differ) Property changes on: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App1-0.3.zip ___________________________________________________________________ Added: svn:mime-type + application/zip Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App2-7.zip =================================================================== (Binary files differ) Property changes on: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App2-7.zip ___________________________________________________________________ Added: svn:mime-type + application/zip Added: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App3-7.zip =================================================================== (Binary files differ) Property changes on: trunk/nuiton-utils/src/test/resources/ApplicationUpdater/zip/App3-7.zip ___________________________________________________________________ Added: svn:mime-type + application/zip Added: trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterNetworkTest.properties =================================================================== --- trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterNetworkTest.properties (rev 0) +++ trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterNetworkTest.properties 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,6 @@ +App1.version=0.3 +App1.url=http://svn.nuiton.org/svn/nuiton-utils/trunk/nuiton-utils/src/test/resources... +linux.amd64.App2.version=7 +linux.amd64.App2.url=http://svn.nuiton.org/svn/nuiton-utils/trunk/nuiton-utils/src/test/resources... +linux.x86.App3.version=7 +linux.x86.App3.url=http://svn.nuiton.org/svn/nuiton-utils/trunk/nuiton-utils/src/test/resources... Added: trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterTest.properties =================================================================== --- trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterTest.properties (rev 0) +++ trunk/nuiton-utils/src/test/resources/properties/ApplicationUpdaterTest.properties 2013-01-04 15:05:56 UTC (rev 2457) @@ -0,0 +1,6 @@ +App1.version=0.3 +App1.url=file:src/test/resources/ApplicationUpdater/zip/App1-0.3.zip +linux.amd64.App2.version=7 +linux.amd64.App2.url=file:src/test/resources/ApplicationUpdater/zip/App2-7.zip +linux.x86.App3.version=7 +linux.x86.App3.url=file:src/test/resources/ApplicationUpdater/zip/App3-7.zip
participants (1)
-
bpoussin@users.nuiton.org