001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.update;
020
021import java.io.File;
022import java.util.ArrayList;
023
024import org.w3c.dom.Document;
025import org.w3c.dom.Element;
026import org.w3c.dom.Node;
027
028import icy.file.FileUtil;
029import icy.update.ElementDescriptor.ElementFile;
030import icy.util.StringUtil;
031import icy.util.XMLUtil;
032import icy.util.ZipUtil;
033
034public class Updater
035{
036    public static final String ICYKERNEL_NAME = "ICY Kernel";
037    public static final String ICYUPDATER_NAME = "ICY Updater";
038
039    public static final String UPDATE_DIRECTORY = FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + "update";
040    public static final String BACKUP_DIRECTORY = FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + "backup";
041    public static final String UPDATE_BASE_NAME = "update";
042    public static final String UPDATE_EXT_NAME = ".xml";
043    public static final String UPDATE_NAME = UPDATE_BASE_NAME + UPDATE_EXT_NAME;
044    public static final String VERSION_NAME = "version.xml";
045    public static final String UPDATER_NAME = "updater.jar";
046
047    public static final String ARG_NOSTART = "-nostart";
048    public static final String ARG_UPDATE = "-update";
049
050    private static final String ID_ELEMENTS = "elements";
051    private static final String ID_ELEMENT = "element";
052    private static final String ID_OBSOLETES = "obsoletes";
053    private static final String ID_LOCALPATH = "localpath";
054
055    // /**
056    // * Get update elements.<br>
057    // * Compare local elements with online element and return a list of element<br>
058    // * which need to be updated.
059    // */
060    // public static ArrayList<ElementDescriptor> getUpdateElements()
061    // {
062    // return getUpdateElements(getLocalElements());
063    // }
064
065    // /**
066    // * Update the local version.xml file so it contains only present elements with correct
067    // * modification date.
068    // */
069    // public static boolean validateLocalElementsXML()
070    // {
071    // // get local elements
072    // final ArrayList<ElementDescriptor> localElements = getLocalElements();
073    // // validate them
074    // validateLocalElements(localElements);
075    // // and save to local XML file
076    // return saveElementsToXML(localElements, VERSION_NAME, false);
077    // }
078
079    // public static boolean updateXML()
080    // {
081    // final ArrayList<ElementDescriptor> localElements = getLocalElements();
082    // final ArrayList<ElementDescriptor> onlineElements = getOnlineElements();
083    //
084    // // update local list
085    // for (ElementDescriptor onlineElement : onlineElements)
086    // {
087    // final ElementDescriptor localElement = findElement(onlineElement.getName(), localElements);
088    // // local element absent or outdated ?
089    // if (localElement == null)
090    // localElements.add(onlineElement);
091    // else if (onlineElement.getVersion().isGreater(localElement.getVersion()))
092    // // set new version
093    // localElement.setVersion(onlineElement.getVersion());
094    // }
095    //
096    // // save new version XML file return
097    // return saveElementsToXML(localElements, VERSION_NAME, false);
098    // }
099
100    /**
101     * Validate the specified list of elements against local files.<br>
102     * This actually remove missing files and update the file modification date.
103     */
104    public static void validateElements(ArrayList<ElementDescriptor> elements)
105    {
106        // validate elements against local files
107        for (int i = elements.size() - 1; i >= 0; i--)
108        {
109            final ElementDescriptor element = elements.get(i);
110
111            // validate element
112            element.validate();
113
114            // no more valid file ? --> remove element
115            if (element.getFilesNumber() == 0)
116                elements.remove(i);
117        }
118    }
119
120    /**
121     * Get the list of local elements.<br>
122     * Elements are fetched from local version.xml file then validated with local files.
123     */
124    public static ArrayList<ElementDescriptor> getLocalElements()
125    {
126        // get local elements from XML file
127        final ArrayList<ElementDescriptor> result = loadElementsFromXML(
128                FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + VERSION_NAME);
129
130        // validate elements
131        validateElements(result);
132
133        return result;
134    }
135
136    /**
137     * Get the list of online elements (online update.xml file)
138     */
139    public static ArrayList<ElementDescriptor> getOnlineElements()
140    {
141        return loadElementsFromXML(UPDATE_DIRECTORY + FileUtil.separator + UPDATE_NAME);
142    }
143
144    /**
145     * Get update elements.<br>
146     * Compare specified local elements with online element and return a list of element<br>
147     * which need to be updated.
148     */
149    public static ArrayList<ElementDescriptor> getUpdateElements(ArrayList<ElementDescriptor> localElements)
150    {
151        final ArrayList<ElementDescriptor> result = new ArrayList<ElementDescriptor>();
152        final ArrayList<ElementDescriptor> onlineElements = getOnlineElements();
153
154        // build update list
155        for (ElementDescriptor onlineElement : onlineElements)
156        {
157            final ElementDescriptor localElement = findElement(onlineElement.getName(), localElements);
158            // get update element (differences between online and local element)
159            final ElementDescriptor updateElement = ElementDescriptor.getUpdateElement(localElement, onlineElement);
160
161            if (updateElement != null)
162                // add the element to update list
163                result.add(updateElement);
164        }
165
166        return result;
167    }
168
169    /**
170     * Get the list of obsoletes files
171     */
172    public static ArrayList<String> getObsoletes()
173    {
174        final ArrayList<String> result = new ArrayList<String>();
175
176        final Document document = XMLUtil.loadDocument(UPDATE_DIRECTORY + FileUtil.separator + UPDATE_NAME, false);
177
178        if (document != null)
179        {
180            // TODO: check if we can really remove that
181            // document.normalizeDocument();
182
183            // get obsoletes node
184            final Node obsoletes = XMLUtil.getElement(document.getDocumentElement(), ID_OBSOLETES);
185
186            // get all local path
187            final ArrayList<Node> nodesLocalpath = XMLUtil.getChildren(obsoletes, ID_LOCALPATH);
188            if (nodesLocalpath != null)
189            {
190                for (Node n : nodesLocalpath)
191                {
192                    final String value = XMLUtil.getValue((Element) n, "");
193                    if (!StringUtil.isEmpty(value, true))
194                        result.add(value);
195                }
196            }
197        }
198
199        return result;
200    }
201
202    public static ArrayList<ElementDescriptor> loadElementsFromXML(String path)
203    {
204        final ArrayList<ElementDescriptor> result = new ArrayList<ElementDescriptor>();
205
206        final Document document = XMLUtil.loadDocument(path, true);
207
208        if (document != null)
209        {
210            // TODO: check if we can really remove that
211            // document.normalizeDocument();
212
213            // get elements node
214            final Node elements = XMLUtil.getElement(document.getDocumentElement(), ID_ELEMENTS);
215
216            // get elements
217            final ArrayList<Node> nodesElement = XMLUtil.getChildren(elements, ID_ELEMENT);
218            if (nodesElement != null)
219            {
220                for (Node n : nodesElement)
221                    result.add(new ElementDescriptor(n));
222            }
223        }
224
225        return result;
226    }
227
228    /**
229     * Save the specified elements to the specified filename
230     */
231    public static boolean saveElementsToXML(ArrayList<ElementDescriptor> elements, String path, boolean onlineSave)
232    {
233        final Document document = XMLUtil.createDocument(true);
234
235        final Element elementsNode = XMLUtil.addElement(document.getDocumentElement(), ID_ELEMENTS);
236
237        // set elements
238        for (ElementDescriptor element : elements)
239            element.saveToNode(XMLUtil.addElement(elementsNode, ID_ELEMENT), onlineSave);
240
241        return XMLUtil.saveDocument(document, path);
242    }
243
244    /**
245     * Find an element in the specified list
246     */
247    public static ElementDescriptor findElement(String name, ArrayList<ElementDescriptor> list)
248    {
249        for (ElementDescriptor element : list)
250            if (name.equals(element.getName()))
251                return element;
252
253        return null;
254    }
255
256    /**
257     * Find an element from his local path in the specified list
258     */
259    // private static ElementDescriptor findElementFromLocalPath(String path,
260    // ArrayList<ElementDescriptor> list)
261    // {
262    // for (ElementDescriptor element : list)
263    // if (element.hasLocalPath(path))
264    // return element;
265    //
266    // return null;
267    // }
268
269    /**
270     * Return true if some update files are present in the update directory
271     */
272    public static boolean hasUpdateFiles()
273    {
274        final String[] paths = FileUtil.getFiles(UPDATE_DIRECTORY, null, true, false, false);
275
276        for (String path : paths)
277        {
278            final String filename = FileUtil.getFileName(path);
279
280            // check if we have others files other than updater and XML definitions
281            if ((!filename.equals(UPDATER_NAME)) && (!filename.equals(UPDATE_NAME)))
282                return true;
283        }
284
285        return false;
286    }
287
288    /**
289     * Update the specified "update" element (move files from update to application directory)<br>
290     * then modify local elements list according to changes made.
291     * 
292     * @return true if update succeed, false otherwise
293     */
294    public static boolean udpateElement(ElementDescriptor updateElement, ArrayList<ElementDescriptor> localElements)
295    {
296        // update all element files
297        if (Updater.updateFiles(updateElement.getFiles()))
298        {
299            // then modify local elements list
300            updateElementInfos(updateElement, localElements);
301            return true;
302        }
303
304        return false;
305    }
306
307    /**
308     * Update local elements according to changes presents in updateElement
309     */
310    public static void clearElementInfos(ElementDescriptor updateElement, ArrayList<ElementDescriptor> localElements)
311    {
312        // find corresponding current local element
313        final ElementDescriptor localElement = Updater.findElement(updateElement.getName(), localElements);
314
315        // remove it
316        localElements.remove(localElement);
317    }
318
319    /**
320     * Update local elements according to changes presents in updateElement
321     */
322    public static void updateElementInfos(ElementDescriptor updateElement, ArrayList<ElementDescriptor> localElements)
323    {
324        // find corresponding current local element
325        final ElementDescriptor localElement = Updater.findElement(updateElement.getName(), localElements);
326
327        // local element doesn't exist
328        if (localElement == null)
329            // add it
330            localElements.add(updateElement);
331        else
332            // just update local element with update element info
333            localElement.update(updateElement);
334    }
335
336    /**
337     * Update the specified files
338     */
339    public static boolean updateFiles(ArrayList<ElementFile> files)
340    {
341        for (ElementFile file : files)
342            // if update fails --> exit
343            if (!updateFile(file))
344                return false;
345
346        return true;
347    }
348
349    /**
350     * Update the specified local file
351     */
352    public static boolean updateFile(ElementFile file)
353    {
354        final String localPath = file.getLocalPath();
355
356        // directory type file --> extract it
357        if (file.isDirectory())
358        {
359            final String dirName = UPDATE_DIRECTORY + FileUtil.separator + localPath;
360            final String zipName = dirName + ".zip";
361
362            // rename directory type file (no extension) to zip file
363            if (!FileUtil.rename(dirName, zipName, true))
364                return false;
365            // extract zip file
366            if (!ZipUtil.extract(zipName))
367                return false;
368        }
369
370        if (updateFile(localPath, file.getDateModif()))
371        {
372            final File dest = new File(FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath);
373
374            // there is no reason the file doesn't exists but anyway...
375            if (dest.exists())
376            {
377                if (file.isExecutable())
378                    dest.setExecutable(true, false);
379                if (file.isWritable())
380                    if (!dest.setWritable(true, false))
381                        dest.setWritable(true, true);
382
383                return true;
384            }
385        }
386
387        return false;
388    }
389
390    /**
391     * Backup the specified local file
392     */
393    public static boolean backup(String localPath)
394    {
395        final String src = FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath;
396        // file exist ? backup it
397        if (FileUtil.exists(src))
398        {
399            final String dest = BACKUP_DIRECTORY + FileUtil.separator + localPath;
400
401            if (!FileUtil.copy(src, dest, true, true))
402                return false;
403
404            // verify that backup file exist
405            return FileUtil.exists(dest);
406        }
407
408        return true;
409    }
410
411    /**
412     * Update the specified local file
413     */
414    public static boolean updateFile(String localPath, long dateModif)
415    {
416        // no update needed
417        if (!needUpdate(localPath, dateModif))
418            return true;
419
420        // backup file
421        if (!backup(localPath))
422        {
423            // backup failed
424            System.err.println("Updater.udpateFile(" + localPath + ") failed :");
425            // System.err.println("Cannot backup file to '" + BACKUP_DIRECTORY + FileUtil.separator
426            // + localPath);
427            return false;
428        }
429
430        // move file
431        if (!FileUtil.rename(UPDATE_DIRECTORY + FileUtil.separator + localPath,
432                FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath, true))
433        {
434            // move failed
435            System.err.println("Updater.udpateFile('" + localPath + "') failed !");
436            // System.err.println("Cannot rename file from '" + UPDATE_DIRECTORY +
437            // FileUtil.separator + localPath
438            // + "' to '" + localPath + "'");
439            return false;
440        }
441
442        return true;
443    }
444
445    /**
446     * Return true if specified file is different from the update file (in Update directory)
447     */
448    public static boolean needUpdate(String localPath, long dateModif)
449    {
450        final File localFile = new File(FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + localPath);
451
452        return (!localFile.exists()) || (dateModif == 0L) || (localFile.lastModified() != dateModif);
453    }
454
455    /**
456     * Process to restoration (in case the update failed)
457     */
458    public static boolean restore()
459    {
460        final int len = BACKUP_DIRECTORY.length();
461        // get files only (no directory)
462        final String[] paths = FileUtil.getFiles(BACKUP_DIRECTORY, null, true, false, false);
463        boolean result = true;
464
465        for (String backupPath : paths)
466        {
467            final String finalPath = backupPath.substring(len + 1);
468
469            // don't restore updater
470            if (finalPath.equals(UPDATER_NAME))
471                continue;
472
473            if (!FileUtil.rename(backupPath, finalPath, true))
474            {
475                // rename failed (FileUtil.rename is already displaying error messages if needed)
476                System.err.println("Updater.restore() cannot restore '" + finalPath + "', you should do it manually.");
477                result = false;
478            }
479        }
480
481        return result;
482    }
483
484    /**
485     * Delete obsoletes files
486     */
487    public static void deleteObsoletes()
488    {
489        // delete obsolete files
490        for (String obsolete : getObsoletes())
491            FileUtil.delete(obsolete, true);
492    }
493}