All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.util.ApplicationUpdater Maven / Gradle / Ivy

There is a newer version: 3.1
Show newest version
package org.nuiton.util;

/*
 * #%L
 * Nuiton Utils :: Nuiton Utils
 * $Id: ApplicationUpdater.java 2513 2013-02-26 07:22:43Z tchemit $
 * $HeadURL: http://svn.nuiton.org/svn/nuiton-utils/tags/nuiton-utils-2.6.10/nuiton-utils/src/main/java/org/nuiton/util/ApplicationUpdater.java $
 * %%
 * Copyright (C) 2004 - 2013 CodeLutin
 * %%
 * This program 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 program 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 General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * .
 * #L%
 */


import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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;
import org.apache.commons.vfs2.AllFileSelector;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.FileSystemOptions;
import org.apache.commons.vfs2.VFS;
import org.apache.commons.vfs2.provider.http.HttpFileSystemConfigBuilder;

/**
 * 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 decompressee 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}
 *
 * 

Configuration possible

* 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: *
  • http_proxy: le proxy a utiliser pour l'acces au reseau (ex: squid.chezmoi.fr:8080) *
  • os.name: le nom du systeme d'exploitation sur lequel l'application fonctionne (ex: Linux) *
  • os.arch: l'architecture du systeme d'exploitation sur lequel l'application fonctionne (ex: amd64) * *

    format du fichier de properties

    * *
  • [osName.][osArch.]appName.version=version de l'application
  • *
  • [osName.][osArch.]appName.auth=true ou false selon que l'acces a l'url * demande une authentification a fournir par le callback * (voir {@link ApplicationUpdaterCallback#updateToDo})
  • *
  • [osName.][osArch.]appName.url=url du fichier compresse de la nouvelle version * (format commons-vfs2)
  • * * 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 * *

    format des fichiers compresses

    * * Le fichier compresse doit avoir un repertoire racine qui contient l'ensemble de l'application * c-a-d que les fichiers ne doivent pas etre directement a la racine lorsqu'on * decompresse le fichier. * * exemple de contenu de fichier compresse convenable *
     * MonApp-0.3/Readme.txt
     * MonApp-0.3/License.txt
     * 
    * * Ceci est du au fait qu'on renomme le repertoire racine avec le nom de l'application, * donc si le repertoire racine n'existe pas ou qu'il y a plusieurs repertoires * a la racine le resultat de l'operation n'est pas celui souhaite * *

    os.name and os.arch

    * * * * * * * *
    os.nameos.arch
    linuxamd64
    linuxi386
    macppc
    windowsx86
    solarissparc
    * * os.name est tronque apres le 1er mot donc "windows 2000" et "windows 2003" * deviennet tous les deux "windows". Si vous souhaitez gérer plus finement vos * url de telechargement vous pouvez modifier les donnees via * {@link ApplicationUpdaterCallback#updateToDo(java.util.Map) } en modifiant * l'url avant de retourner la map * * @author poussin * @version $Revision: 2513 $ * * Last update: $Date: 2013-02-26 08:22:43 +0100 (Tue, 26 Feb 2013) $ * by : $Author: tchemit $ * * @since 2.6.6 * @deprecated since 2.6.10 (replaced by org.nuiton.util.updater.ApplicationUpdater * in nuiton-updater module), will be removed in version 2.7.1. */ @Deprecated 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 AUTHENTICATION_KEY = "auth"; 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(String vfsPropertiesURL, File currentDir, File destDir, boolean async, ApplicationUpdaterCallback callback) { Updater up = new Updater(config, vfsPropertiesURL, 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. * * Si {@link ApplicationInfo#needAuthentication} est vrai, il faut que * les valeurs {@link ApplicationInfo#login} et {@link ApplicationInfo#password} * soient renseignees. Si elle ne le sont pas la recuperation de la * ressource echouera. Pour des raisons de securite vous pouvez souhaiter * mettre le mot de passe sous une forme encrypte. Dans ce cas il doit * etre encadrer par '{' et '}'. Pour encrypter le mot de passe * vous devez utiliser: *
             * java -cp commons-vfs-2.0.jar org.apache.commons.vfs2.util.EncryptUtil encrypt mypassword
             * 
    * @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 updateToDo(Map appToUpdate); /** * Appeler au démarrage d'une mise à jour. * * @param info application à mettre à jour * @since 2.7 */ void startUpdate(ApplicationInfo info); /** * 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 appToUpdate, Map 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 boolean needAuthentication; public String login; public char[] password; public File destDir; public ApplicationInfo(String name, String oldVersion, String newVersion, String url, File destDir, boolean needAuthentication) { this.name = name; this.oldVersion = oldVersion; this.newVersion = newVersion; this.url = url; this.needAuthentication = needAuthentication; this.destDir = destDir; } public void setAuthentication(String login, char[] password) { this.login = login; this.password = password; } @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 String vfsPropertiesUrl; protected File currentDir; protected File destDir; protected ApplicationUpdaterCallback callback; public Updater(ApplicationConfig config, String vfsPropertiesUrl, File currentDir, File destDir, ApplicationUpdaterCallback callback) { this.config = config; this.vfsPropertiesUrl = vfsPropertiesUrl; this.currentDir = currentDir; this.destDir = destDir; this.callback = callback; } /** *
  • Recupere le fichier properties contenant les informations de mise a jour *
  • liste les applications et leur version actuelle *
  • pour chaque application a mettre a jour recupere le zip et le decompresse * * Si callback existe envoi les messages necessaire */ public void run() { try { FileSystemOptions vfsConfig = getVFSConfig(config); ApplicationConfig releaseConfig = getUpdaterConfig(vfsConfig, vfsPropertiesUrl); List appNames = getApplicationName(releaseConfig); Map appVersions = getCurrentVersion(appNames, currentDir); log.debug("application current version: " + appVersions); // recherche des applications a mettre a jour Map appToUpdate = new HashMap(); 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); boolean needAuthentication = releaseConfig.getOptionAsBoolean( app + SEPARATOR_KEY + AUTHENTICATION_KEY); appToUpdate.put(app, new ApplicationInfo( app, currentVersion, newVersion, urlString, destDir, needAuthentication)); } } // offre la possibilite a l'appelant de modifier les valeurs par defaut if (callback != null) { appToUpdate = callback.updateToDo(appToUpdate); } // mise a jour Map appUpdateError = new HashMap(); for (Map.Entry appInfo : appToUpdate.entrySet()) { String app = appInfo.getKey(); ApplicationInfo info = appInfo.getValue(); try { doUpdate(vfsConfig, 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(vfsPropertiesUrl, 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(FileSystemOptions vfsConfig, ApplicationInfo info) throws Exception { if (info.destDir != null) { File dest = new File(info.destDir, info.name); String url = toVfsURL(info.url); if (info.needAuthentication) { url = StringUtils.replaceOnce(url, "://", String.format("://%s:%s@", info.login, new String(info.password))); } if (callback!=null) { callback.startUpdate(info); } deepCopy(vfsConfig, url, dest.getAbsolutePath()); // ajout du fichier de version File versionFile = new File(dest, 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)); } } /** * Recupere le contenu du repertoire de l'archive pour le mettre dans targetPath * si targetPath existait deja, il est supprime au prealable. * * Si l'archive a plus d'un repertoire root, une exception est levee * * @param srcPath source path de la forme vfs2 ex:"zip:http://www.nuiton.org/attachments/download/830/nuiton-utils-2.6.5-deps.zip" * @param targetPath le path destination * @throws FileSystemException */ protected void deepCopy(FileSystemOptions vfsConfig, String srcPath, String targetPath) throws FileSystemException { FileSystemManager fsManager = VFS.getManager(); FileObject archive = fsManager.resolveFile(srcPath, vfsConfig); FileObject[] children = archive.getChildren(); if (children.length == 1) { FileObject child = children[0]; FileObject target = fsManager.resolveFile(toVfsURL(targetPath), vfsConfig); target.delete(new AllFileSelector()); target.copyFrom(child, new AllFileSelector()); } else { throw new RuntimeException("must have only one root directory"); } } /** * Converti le path en URL vfs2. 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 String toVfsURL(String path) { String result = path; Pattern p = Pattern.compile("(.*?file:)([^/][^!]*)(.*)"); Matcher m = p.matcher(path); if (m.matches()) { String filepath = m.group(2); File f = new File(filepath); result = path.replaceAll( "(.*?file:)([^/][^!]*)(.*)", "$1"+f.getAbsolutePath()+"$3"); } return result; } /** * Return config prepared for os and arch * * @return * @throws Exception */ protected ApplicationConfig getUpdaterConfig(FileSystemOptions vfsConfig, String vfsPropertiesUrl) throws Exception { String osName = StringUtils.lowerCase(config.getOsName()); String osArch = StringUtils.lowerCase(config.getOsArch()); // take only first part for osName (windows 2000 or windows 2003 -> windows) osName = StringUtils.substringBefore(osName, " "); if (log.isDebugEnabled()) { log.debug(String.format("Try to load properties from '%s'", vfsPropertiesUrl)); } Properties prop = new Properties(); FileSystemManager fsManager = VFS.getManager(); FileObject properties = fsManager.resolveFile(toVfsURL(vfsPropertiesUrl), vfsConfig); try { InputStream in = new BufferedInputStream(properties.getContent().getInputStream()); prop.load(in); } finally { try { properties.close(); } catch (Exception doNothing) { log.debug("Can't close vfs file", doNothing); } } if (log.isDebugEnabled()) { log.debug(String.format( "Properties loaded from '%s'\n%s", vfsPropertiesUrl, 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 FileSystemOptions getVFSConfig(ApplicationConfig config) { FileSystemOptions result = new FileSystemOptions(); 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); HttpFileSystemConfigBuilder.getInstance().setProxyHost(result, hostname); HttpFileSystemConfigBuilder.getInstance().setProxyPort(result, portNumber); } 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 getCurrentVersion(List apps, File dir) { Map result = new HashMap(); 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 getApplicationName(ApplicationConfig config) { Pattern p = Pattern.compile("([^.]+)\\.version"); List result = new LinkedList(); 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; } } }




  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy