/*
 * Copyright
 *   2007 axYus - www.axyus.com
 *   2005 C.Marchand - christophe.marchand@axyus.com
 *
 * This file is part of XEMELIOS.
 *
 * XEMELIOS 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.
 *
 * XEMELIOS 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with XEMELIOS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package fr.gouv.finances.cp.xemelios.updater.ui;

import fr.gouv.finances.cp.xemelios.updater.UpdaterUtil;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;

import jonelo.jacksum.algorithm.AbstractChecksum;
import jonelo.jacksum.algorithm.MD;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.log4j.Logger;
import org.jdesktop.swingx.JXTable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import fr.gouv.finances.cp.utils.PropertiesExpansion;
import fr.gouv.finances.cp.utils.StartUrl;
import fr.gouv.finances.cp.utils.ui.DisplayExceptionDlg;
import fr.gouv.finances.cp.utils.ui.IhmFactory;
import fr.gouv.finances.cp.utils.ui.InfiniteGlassPane;
import fr.gouv.finances.cp.utils.ui.SameSizeVerticalFlowLayout;
import fr.gouv.finances.cp.utils.ui.VerticalFlowLayout;
import fr.gouv.finances.cp.utils.xml.marshal.XmlMarshallable;
import fr.gouv.finances.dgfip.xemelios.common.Constants;
import fr.gouv.finances.dgfip.xemelios.common.NetAccess;
import fr.gouv.finances.dgfip.xemelios.data.DataConfigurationException;
import fr.gouv.finances.cp.xemelios.updater.UpdaterException;
import fr.gouv.finances.cp.xemelios.updater.config.ComponentModel;
import fr.gouv.finances.cp.xemelios.updater.config.ComponentRefModel;
import fr.gouv.finances.cp.xemelios.updater.config.ConfigurationModel;
import fr.gouv.finances.cp.xemelios.updater.config.DeleteModel;
import fr.gouv.finances.cp.xemelios.updater.config.FileListModel;
import fr.gouv.finances.cp.xemelios.updater.config.FileModel;
import fr.gouv.finances.cp.xemelios.updater.config.ParamModel;
import fr.gouv.finances.cp.xemelios.updater.config.SiteModel;
import fr.gouv.finances.cp.xemelios.updater.config.UpdateParser;
import fr.gouv.finances.cp.xemelios.updater.config.XemeliosUpdateModel;
import fr.gouv.finances.dgfip.xemelios.ui.ImageResources;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.JLabel;
import javax.swing.KeyStroke;
import javax.swing.border.EmptyBorder;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import org.jdesktop.swingworker.SwingWorker;

public class DlgUpdate extends JDialog {
    /**
     *
     */
    private static final long serialVersionUID = 3532748822800835233L;
    
    private static Logger logger = Logger.getLogger(DlgUpdate.class);
    
    private static final transient String XEMELIOS_TT = "Composant Xemelios";
    
    private static final transient String CONFIG_TT = "Configuration";
    
    private static final transient String FEATURE_TT = "Fonctionnalité";
    
    private PropertiesExpansion sysProps;
    
    /**
     * The already installed components
     */
    private ComponentHolder installedComponents;
    
    private UpdatePanel updateTab;
    
    /**
     * The available-for-install components
     */
    private Vector<ComponentModel> updateComps = new Vector<ComponentModel>();
    private Hashtable<String,ComponentModel> updateCompsById = new Hashtable<String,ComponentModel>();
    private XPathFactory xpf = null;
    private DocumentBuilderFactory domFactory;
    
    
    public DlgUpdate(Frame owner) {
        super(owner, "Mise à jour de Xemelios", false);
        try {
            xpf = XPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI);
            domFactory = DocumentBuilderFactory.newInstance();
        } catch(Throwable t) {
            xpf = XPathFactory.newInstance();
            t.printStackTrace();
        }
        sysProps = new PropertiesExpansion();
        sysProps.putAll(System.getProperties());
        initComponents();
    }
    
    private void initComponents() {
        // IhmFactory iF = IhmFactory.newInstance();
        Exception initException = null;
        try {
            loadComponentList();
        } catch (Exception ex) {
            initException = ex;
        }
        
        JPanel mainPane = new JPanel(new BorderLayout());
        JPanel centerPane = new JPanel(new VerticalFlowLayout());
        
        JTabbedPane tab = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
        tab.addTab("Composants installés", getComponentsTab());
        tab.addTab("Mises à jour disponibles", getUpdateTab());
        centerPane.add(tab);
        HyperLink releaseNotes = new HyperLink("Notes de version...","http://www.xemelios.org/release-notes.html");
        centerPane.add(releaseNotes);

        mainPane.add(centerPane, BorderLayout.CENTER);
        
        // BOTTOM PART
        JPanel buttonPane = new JPanel(new FlowLayout(FlowLayout.CENTER));
        JButton pbClose = new JButton("Fermer");
        pbClose.addActionListener(new ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent e) {
                setVisible(false);
            };
        });
        buttonPane.add(pbClose);
        mainPane.add(buttonPane, BorderLayout.SOUTH);
        
        setContentPane(mainPane);
        
        pack();
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        setLocationRelativeTo(getParent());
        setGlassPane(new InfiniteGlassPane());
        setVisible(true);
        if (initException != null)
            new DisplayExceptionDlg(this, initException);
    }
    
    private JPanel getComponentsTab() {
        JPanel ret = new JPanel();
        JTable tbl = new JTable(new ComponentsTableModel(installedComponents));
        ret.add(new JScrollPane(tbl));
        return ret;
    }
    
    private JPanel getUpdateTab() {
        updateTab = new UpdatePanel(this);
        return updateTab;
    }
    
    private void loadUpdateList() throws Exception {
        String prop = System.getProperty(Constants.SYSPROP_UPDATE_SOURCE);
        updateComps = new Vector<ComponentModel>();
        updateFromUrl(prop);
        ((UpdCompTableModel) (updateTab.tblComponents.getModel())).setData(updateComps);
        updateTab.pbUpdate.setEnabled(updateComps.size()>0);
    }
    
    private void updateFromUrl(String urlLocation) throws Exception {
        //URL url = new URL(urlLocation);
        HttpClient client = NetAccess.getHttpClient();
        GetMethod meth = new GetMethod(urlLocation);
        int httpRet = client.executeMethod(meth);
        if(httpRet>300) throw new Exception(meth.getStatusText());
        InputStream is = meth.getResponseBodyAsStream();
        UpdateParser up = new UpdateParser();
        up.parse(is);
        is.close();
        XemeliosUpdateModel xum = (XemeliosUpdateModel) up.getMarshallable();
        // logger.debug(xum.getRelease().getV());
        
        if(xum.getXemeliosUpdates()!=null) {
            addNewComponents(ComponentModel.XEMELIOS, xum.getXemeliosUpdates().getComponents());
        }
        if(xum.getConfigUpdates()!=null) {
            addNewComponents(ComponentModel.CONFIG, xum.getConfigUpdates().getComponents());
        }
        if(xum.getFeaturesUpdates()!=null) {
            addNewComponents(ComponentModel.FEATURE, xum.getFeaturesUpdates().getComponents());
        }
        
        if (xum.getForeignSites() != null) {
            for (Iterator<SiteModel> it = xum.getForeignSites().getSites().iterator(); it.hasNext();) {
                SiteModel site = it.next();
                try {
                    updateFromUrl(site.getText());
                } catch (IOException ioEx) {
                    JOptionPane.showMessageDialog(this, "Site de mise à jour introuvable :\n" + ioEx.getMessage(), "Erreur", JOptionPane.ERROR_MESSAGE);
                }
            }
        }
    }
    
    private void addNewComponents(int componentType, Iterable<ComponentModel> cps) {
        for (Iterator<ComponentModel> it = cps.iterator(); it.hasNext();) {
            ComponentModel cm = it.next();
            if (UpdaterUtil.compareReleases(getInstalledcomponentVersion(cm.getId()),cm.getRelease().getV()) < 0) {
                cm.type = componentType;
                // verify if this component is already in update
                ComponentModel oldRelease = updateCompsById.get(cm.getId());
                if(oldRelease!=null) {
                    updateComps.remove(oldRelease);
                    updateCompsById.remove(cm.getId());
                }
                updateComps.add(cm);
                updateCompsById.put(cm.getId(),cm);
            }
        }
    }
    
    private int compareReleases(String r1, String r2) {
        String[] s1 = splitRelease(r1);
        String[] s2 = splitRelease(r2);
//logger.debug("comparing "+r1+" to "+r2);
//logger.debug("s1="+printArray(s1));
//logger.debug("s2="+printArray(s2));
        int ret = 0;
        for(int i=0;i<Math.min(s1.length,s2.length);i++) {
            float a = -1f, b = -1f;
            try { 
                a = Integer.parseInt(s1[i]);
            } catch(Throwable t) {
                if(s1[i].endsWith("+")) {
                    // if there is a "+" in the release number, it is a non-official release,
                    // based on the previous build number. So the "real" number is the previous + 0.5
                    try {
                        a = Integer.parseInt(s1[i].substring(0, s1[i].length()-1));
                        a += 0.5;
                    } catch(Throwable t2) {}
                }
            }
            try {
                b = Integer.parseInt(s2[i]);
            } catch(Throwable t) {
                if(s2[i].endsWith("+")) {
                    // if there is a "+" in the release number, it is a non-official release,
                    // based on the previous build number. So the "real" number is the previous + 0.5
                    try {
                        b = Integer.parseInt(s2[i].substring(0, s2[i].length()-1));
                        b += 0.5;
                    } catch(Throwable t2) {}
                }
            }
            if(a!=-1f && b!=-1f) {
                if(a<b) ret = -1;
                else if(a==b) ret = 0;
                else ret = 1;
            } else {
                ret = s1[i].compareTo(s2[i]);
            }
            if(ret!=0) {
//                logger.debug(a+" "+(ret>0?">":"<")+" "+b);
                return ret;
            }
        }
        if(s1.length>s2.length) ret = 1;
        else if(s1.length<s2.length) ret = -1;
        return ret;
    }
    private String[] splitRelease(String r) {
        return r.split("\\.");
    }
    private static String printArray(String[] s) {
        StringBuffer sb = new StringBuffer();
        sb.append("[");
        for(int i=0;i<s.length;i++) {
            sb.append("\"").append(s[i]).append("\"");
            if(i+1<s.length) sb.append(",");
        }
        sb.append("]");
        return sb.toString();
    }
    
    @SuppressWarnings("serial")
    private class UpdatePanel extends JPanel {
        JXTable tblComponents;
        
        private DlgUpdate parent;
        
        private JButton pbDetails;
        private JButton pbLoad, pbUpdate, pbCheckAll;
        
        public UpdatePanel(DlgUpdate parent) {
            super();
            this.parent = parent;
            initComponents();
        }
        
        private void initComponents() {
            // first state : display only a load button
            setLayout(new BorderLayout());
            tblComponents = new JXTable() {
                private static final long serialVersionUID = 1L;
                
                @Override
                protected void configureEnclosingScrollPane() {
                    Container p = getParent();
                    if (p instanceof JViewport) {
                        Container gp = p.getParent();
                        if (gp instanceof JScrollPane) {
                            JScrollPane scrollPane = (JScrollPane) gp;
                            JViewport viewport = scrollPane.getViewport();
                            if (viewport == null || viewport.getView() != this) {
                                return;
                            }
                            scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE);
                            scrollPane.setBorder(UIManager.getBorder("Table.scrollPaneBorder"));
                        }
                    }
                }
                
                @Override
                public String getToolTipText(MouseEvent evt) {
                    int row = rowAtPoint(evt.getPoint());
                    int column = columnAtPoint(evt.getPoint());
                    if (column == 0) {
                        ComponentModel cm = updateComps.elementAt(row);
                        switch (cm.type) {
                            case ComponentModel.XEMELIOS:
                                return XEMELIOS_TT;
                            case ComponentModel.CONFIG:
                                return CONFIG_TT;
                            case ComponentModel.FEATURE:
                                return FEATURE_TT;
                            default:
                                return null;
                        }
                    }
                    return null;
                }
            };
            tblComponents.setShowGrid(false);
            tblComponents.setShowHorizontalLines(true);
            tblComponents.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
            final UpdCompTableModel model = new UpdCompTableModel(new Vector<ComponentModel>());
            tblComponents.setModel(model);
            tblComponents.getColumnModel().getColumn(0).setMaxWidth(20);
//            tblComponents.getColumnModel().getColumn(1).setMaxWidth(20);
            tblComponents.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
                public void valueChanged(ListSelectionEvent e) {
                    int row = tblComponents.getSelectedRow();
                    pbDetails.setEnabled(row >= 0);
                }
            });
            model.addTableModelListener(new TableModelListener() {
                public void tableChanged(TableModelEvent e) {
                    Object source = e.getSource();
                    if(source instanceof JTable) {
                        JTable table = (JTable)source;
                        pbCheckAll.setEnabled(table.getRowCount()>0);
                    } else if(source instanceof TableModel) {
                        TableModel tm = (TableModel)source;
                        pbCheckAll.setEnabled(tm.getRowCount()>0);
                    }
                }
            });
            add(new JScrollPane(tblComponents), BorderLayout.CENTER);
            
            JPanel buttonPane = new JPanel(new SameSizeVerticalFlowLayout(SameSizeVerticalFlowLayout.TOP));
            pbLoad = new JButton(new ActionLoad(parent));
            buttonPane.add(pbLoad);
            pbDetails = new JButton("Détails");
            pbDetails.setEnabled(false);
            pbDetails.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    new DlgComponentDetail(DlgUpdate.this, updateComps.elementAt(tblComponents.getSelectedRow()));
                }
            });
            buttonPane.add(pbDetails);
            pbUpdate = new JButton("Installer");
            pbUpdate.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    try {
                        pbCheckAll.doClick();
                        doUpdate();
                    } catch (Throwable t) {
                        new DisplayExceptionDlg(DlgUpdate.this, t);
                    }
                }
            });
            pbUpdate.setEnabled(false);
            buttonPane.add(pbUpdate);
            buttonPane.add(Box.createVerticalGlue());
            pbCheckAll = new JButton("Tout sélectionner");
            pbCheckAll.setEnabled(false);
            pbCheckAll.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    model.selectAll();
                }
            });
//            buttonPane.add(pbCheckAll);
            
            add(buttonPane, BorderLayout.EAST);
            setMinimumSize(new Dimension(240, 140));
        }
    }
    
    protected void doUpdate() throws NoSuchAlgorithmException {
        boolean bRet = false;
        getGlassPane().setVisible(true);
        // 1 - constitute component list with dependences
        ComponentHolder ch = new ComponentHolder(updateComps);
        try {
            for (ComponentModel cm : updateComps) {
                if (!cm.selected) continue;
                ch.addComponent(cm);
            }
        } catch(DataConfigurationException dcEx) {
            getGlassPane().setVisible(false);
            JOptionPane.showMessageDialog(this,dcEx.getMessage(),"Erreur",JOptionPane.ERROR_MESSAGE);
            return;
        }
        if(ch.comps.size()==0) {
            getGlassPane().setVisible(false);
            JOptionPane.showMessageDialog(this,"Vous devez cocher au moins un composant à installer.","Erreur",JOptionPane.ERROR_MESSAGE);
            return;
        }
        for (ComponentModel cm : ch.comps) {
            cm.selected = true;
//                logger.debug("selected:" + cm);
        }
        updateTab.tblComponents.tableChanged(new TableModelEvent(updateTab.tblComponents.getModel()));

        Downloader downloader = new Downloader(ch);
        try {
//            SwingUtilities.invokeLater(downloader);
            downloader.execute();
            bRet = downloader.get().booleanValue();
        } catch(Exception ex) {
        }
        String msg = "Le téléchargement des nouveaux composants est terminé.\nPour les installer, vous devez redémarrer Xemelios.";
        if (bRet) {
            updateTab.pbDetails.setEnabled(false);
            updateTab.pbLoad.setEnabled(false);
            updateTab.pbUpdate.setEnabled(false);
            JOptionPane.showMessageDialog(this, msg);
        } else {
            if(downloader.getException()!=null) DisplayExceptionDlg.displayException(this, downloader.getException());
            else if(downloader.getErrorMessage()!=null) JOptionPane.showMessageDialog(this,downloader.getErrorMessage(), "ERREUR",JOptionPane.ERROR_MESSAGE);
        }
    }
    
    private File getLocalFileName(String filePath, String fileName) {
        String ret = filePath;
        int pos = ret.indexOf("${");
        while(pos>=0) {
            String before = ret.substring(0,pos);
            int end = ret.indexOf("}");
            String after = ret.substring(end+1);
            String key = ret.substring(pos+2,end);
            ret = before + System.getProperty(key) + after;
            pos = ret.indexOf("${");
        }
        File f = new File(new File(ret),fileName);
        if(f.exists()) return f;
        return null;
    }
    
    
    @SuppressWarnings("serial")
    private class ActionLoad extends AbstractAction {
        private DlgUpdate dialog;
        
        public ActionLoad(DlgUpdate dialog) {
            super("Rechercher");
            this.dialog = dialog;
        }
        
        public void actionPerformed(ActionEvent e) {
            try {
                dialog.loadUpdateList();
            } catch (Exception ex) {
                new DisplayExceptionDlg(dialog, ex);
            }
        }
    }
    
    private void loadComponentList() throws ParserConfigurationException, SAXException, IOException, XPathExpressionException {
        installedComponents = new ComponentHolder(new Vector<ComponentModel>());
        String fileName = System.getProperty(Constants.SYS_PROP_COMPONENTS_FILENAME);
        InputSource is = new InputSource(new FileReader(new File(fileName)));
        // on parse avant pour etre sur de l'implementation utilise (on veut du JAXP)
        DocumentBuilder domBuilder = domFactory.newDocumentBuilder();
        Document dom = domBuilder.parse(is);
        try {
            XPath xp = xpf.newXPath();
//            logger.error(xpf.getClass().getName());
            Object o = xp.evaluate("//component", dom, XPathConstants.NODESET);
            if (o instanceof NodeList) {
                NodeList nl = (NodeList) o;
                for (int i = 0; i < nl.getLength(); i++) {
                    Element el = (Element) nl.item(i);
                    try {
                        installedComponents.addComponent(new XemeliosComponent(el));
                    } catch(DataConfigurationException dcEx) {}
                }
            } else if (o instanceof List) {
                List l = (List) o;
                for (Object obj : l) {
                    if (obj instanceof Element) {
                        try {
                            installedComponents.addComponent(new XemeliosComponent((Element) obj));
                        } catch(DataConfigurationException dcEx) {}
                    } else {
//                        logger.debug(obj.getClass().getName());
                        Class clazz = obj.getClass();
                        Class[] interfaces = clazz.getInterfaces();
//                        for (Class c : interfaces)
//                            logger.debug("\ti:" + c.getName());
                        Class superClass = clazz.getSuperclass();
                        while (superClass != null) {
//                            logger.debug("\tsuper:" + superClass.getName());
                            superClass = superClass.getSuperclass();
                        }
                    }
                }
            }
        } catch (XPathFactoryConfigurationException e) {
            logger.error("in loadComponentList()",e);
        }
    }
    
    private String getInstalledcomponentVersion(String cid) {
        String ret = installedComponents.getComponentVersion(cid);
        return ret == null ? "0" : ret;
    }
    
    private String unescapePath(String path) {
        return sysProps.replace(path);
    }
    
    private class XemeliosComponent extends ComponentModel {
        public XemeliosComponent(Element el) throws XPathExpressionException, XPathFactoryConfigurationException {
            super(null);
            XPath xp = xpf.newXPath();
            setId((String) xp.evaluate("./@id", el, XPathConstants.STRING));
            setRelease((String) xp.evaluate("./@version", el, XPathConstants.STRING));
            setDescription((String) xp.evaluate("./@description", el, XPathConstants.STRING));
        }
        
        public XemeliosComponent(String cid, String version) {
            super(null);
            setId(cid);
            setRelease(version);
        }
    }
    
    @SuppressWarnings("serial")
    private class ComponentsTableModel extends AbstractTableModel {
        private Vector<ComponentModel> data = null;
        
        public ComponentsTableModel(ComponentHolder comps) {
            super();
            data = new Vector<ComponentModel>();
            this.data.addAll(comps.comps);
        }
        
        public int getRowCount() {
            return data.size();
        }
        
        public int getColumnCount() {
            return 2;
        }
        
        public Object getValueAt(int rowIndex, int columnIndex) {
            ComponentModel c = data.get(rowIndex);
            switch (columnIndex) {
                case 0:
                    return c.getDescription().getText();
                case 1:
                    return c.getRelease().getV();
            }
            return null;
        }
        
        @Override
        public String getColumnName(int column) {
            switch (column) {
                case 0:
                    return "Composant";
                case 1:
                    return "Version";
            }
            return "";
        }
    }
    
    @SuppressWarnings("serial")
    private class UpdCompTableModel extends AbstractTableModel {
        private Vector<ComponentModel> components;
        
        private ImageIcon xemeliosIcon, configIcon, featureIcon;
        
        public UpdCompTableModel(Vector<ComponentModel> components) {
            super();
            this.components = components;
//            IhmFactory iF = IhmFactory.newInstance();
            xemeliosIcon = IhmFactory.getIconFromResource(ImageResources.XEMELIOS_COMPONENT_ICON);
            configIcon = IhmFactory.getIconFromResource(ImageResources.CONFIG_COMPONENT_ICON);
            featureIcon = IhmFactory.getIconFromResource(ImageResources.FEATURE_COMPONENT_ICON);
        }
        
        @Override
        public Class<?> getColumnClass(int columnIndex) {
            switch (columnIndex) {
                case 0:
                    return Icon.class;
//                case 1:
//                    return Boolean.class;
                case 1:
                    return String.class;
                default:
                    return super.getColumnClass(columnIndex);
            }
        }
        
        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
//            ComponentModel cm = components.elementAt(rowIndex);
//            if (cm.isVisible())
//                return columnIndex == 1;
            return false;
        }
        
        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            ComponentModel cm = components.elementAt(rowIndex);
            cm.selected = ((Boolean) aValue).booleanValue();
        }
        
        public int getColumnCount() {
            return 2;
        }
        
        public int getRowCount() {
            return components.size();
        }
        
        public Object getValueAt(int rowIndex, int columnIndex) {
            ComponentModel cm = components.elementAt(rowIndex);
            if (columnIndex == 0) {
                switch (cm.type) {
                    case ComponentModel.XEMELIOS:
                        return xemeliosIcon;
                    case ComponentModel.CONFIG:
                        return configIcon;
                    case ComponentModel.FEATURE:
                        return featureIcon;
                    default:
                        return null;
                }
//            } else if (columnIndex == 1) {
//                return new Boolean(cm.selected);
            } else
                return cm.toString();
        }
        
        public void setData(Vector<ComponentModel> data) {
            components = data;
            fireTableDataChanged();
        }
        
        public void selectAll() {
            for(ComponentModel cm:components) cm.selected=true;
            fireTableDataChanged();
        }
    }
    
    @SuppressWarnings("unused")
    private class ComponentHolder {
        private HashMap<String, String> compList;
        private Vector<ComponentModel> updateComps;
        private ArrayList<ComponentModel> comps;
        
        public ComponentHolder(Vector<ComponentModel> updateComps) {
            super();
            compList = new HashMap<String, String>();
            comps = new ArrayList<ComponentModel>();
            this.updateComps=updateComps;
        }
        
        public boolean isComponentPresent(String cid) {
            return compList.get(cid) != null;
        }
        
        public String getComponentVersion(String cid) {
            return compList.get(cid);
        }
        
        public boolean isComponentAlreadySelected(String cid, String version) {
            return isComponentPresent(cid) && getComponentVersion(cid).compareTo(version) >= 0;
        }
        
        public void addComponent(ComponentModel cm) throws DataConfigurationException {
            if (isComponentAlreadySelected(cm.getId(), cm.getRelease().getV()))
                return;
            if (isComponentPresent(cm.getId())) {
                int ret = UpdaterUtil.compareReleases(getComponentVersion(cm.getId()),cm.getRelease().getV());
                if(ret<0) {
                    XemeliosComponent xc = new XemeliosComponent(cm.getId(), getComponentVersion(cm.getId()));
                    comps.remove(xc);
                    compList.remove(cm.getId());
                }
            }
            comps.add(cm);
            compList.put(cm.getId(), cm.getRelease().getV());
            if(cm.getRequires()!=null) {
                for(ComponentRefModel cRef:cm.getRequires().getComponents()) {
                    String installedVersion = installedComponents.getComponentVersion(cRef.getId());
                    if(installedVersion!=null && UpdaterUtil.compareReleases(installedVersion,cRef.getVersion())>=0) continue;
                    ComponentModel dep = updateCompsById.get(cRef.getId());
                    if(dep !=null && UpdaterUtil.compareReleases(dep.getRelease().getV(),cRef.getVersion())>=0)
                        addComponent(dep);
                    else
                        throw new DataConfigurationException("Composant requis introuvable: "+cRef.getId()+" ("+cRef.getVersion()+")");
                }
            } 
        }
        public void removeComponent(String cid) {
            String version = getComponentVersion(cid);
            XemeliosComponent xc = new XemeliosComponent(cid, version);
            comps.remove(xc);
            compList.remove(cid);
        }
    }
    
    public class HyperLink extends JLabel {
        private String uri;
        private Action act;
        public HyperLink(String text,String uri) {
            super(text);
            this.uri=uri;
            act = new AbstractAction() {

                public void actionPerformed(ActionEvent e) {
                    try {
                        StartUrl.startUrl(HyperLink.this.uri);
                    } catch(Exception ex) {}
                }
                
            };
            setForeground(new Color(0x00,0x33,0xFF));
            setFocusable(true);
            setBorder(new EmptyBorder(2,2,2,2));
            getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,0), "CLICK");
            getActionMap().put("CLICK", act);
            addFocusListener(new FocusAdapter() {

                @Override
                public void focusGained(FocusEvent e) {
                    ((HyperLink)e.getComponent()).paintFocus();
                }

                @Override
                public void focusLost(FocusEvent e) {
                    ((HyperLink)e.getComponent()).paintFocus();
                }
                
            });
            addMouseListener(new MouseAdapter() {

                @Override
                public void mouseClicked(MouseEvent e) {
                    HyperLink.this.act.actionPerformed(null);
                }

                @Override
                public void mouseEntered(MouseEvent e) {
                    super.mouseEntered(e);
                    HyperLink.this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    super.mouseExited(e);
                    HyperLink.this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                }
                
            });
        }
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(getForeground());
            int height = getFontMetrics(getFont()).getAscent()+2;
            g.drawLine(0,height,getWidth(),height);
            paintFocus(g);
        }
        protected void paintFocus(Graphics g) {
            if(g==null) return;
            if(isFocusOwner()) {
                g.setColor(UIManager.getColor("Button.focus"));
            } else {
                g.setColor(UIManager.getColor("Label.background"));
            }
            g.drawRect(0, 0, getWidth()-1, getHeight()-1);
        }
        protected void paintFocus() { paintFocus(getGraphics()); }
    }
    public class Downloader extends SwingWorker<Boolean,Integer> {
        private ComponentHolder ch;
        protected String errorMsg = null;
        protected Exception exception = null;
        
        public Exception getException() { return exception; }
        public String getErrorMessage() { return errorMsg; }
        
        private Downloader(ComponentHolder ch) {
            super();
            this.ch=ch;
        }
        @Override
        protected Boolean doInBackground() throws Exception {
            logger.debug("in doInBackground");
            boolean bRet = true;
            try {
                File tmpDir = new File(unescapePath("${xemelios.dir}/tmp-upd"));
                logger.debug("tmpDir=" + tmpDir.getPath());
                tmpDir.mkdirs();

                StringBuilder sb = new StringBuilder();

                HttpClient client = NetAccess.getHttpClient();
                for (ComponentModel cm : ch.comps) {
                    for (FileListModel fl : cm.getFileLists()) {
                        String root = fl.getRoot();
                        for (XmlMarshallable x : fl.getFiles()) {
                            if (x instanceof FileModel) {
                                FileModel fm = (FileModel) x;
                                File localFile = getLocalFileName(fm.getDest(),fm.getName());
                                if(localFile!=null) {
                                    AbstractChecksum localChecksum = new MD("MD5");
                                    FileInputStream fis = new FileInputStream(localFile);
                                    byte[] buffer = new byte[1024];
                                    int read = fis.read(buffer);
                                    while (read > 0) {
                                        localChecksum.update(buffer, 0, read);
                                        read = fis.read(buffer);
                                    }
                                    fis.close();
                                    if(localChecksum.getFormattedValue().equals(fm.getMd5())) {
                                        logger.info("local "+fm.getName()+" is the same than remote one.");
                                        continue;
                                    }
                                }
                                String sUrl = root + "/" + fm.getSource();
                                URL url = new URL(sUrl);
                                //URLConnection con = url.openConnection();
                                AbstractChecksum checksum = new MD("MD5");
                                FileOutputStream fos;
                                File tmpFile = File.createTempFile("upd-", null, tmpDir);
                                sb.append("copy|").append(tmpFile.getName()).append("|").append(fm.getDest() + "/" + fm.getName()).append("\n");
                                fos = new FileOutputStream(tmpFile);
                                logger.info("downloading " + sUrl + " to " + tmpFile.getPath());
                                HttpMethod meth = new GetMethod(sUrl);
                                int returnCode = client.executeMethod(meth);
                                if(returnCode<=300) {
                                    InputStream is = meth.getResponseBodyAsStream();
                                    byte[] buffer = new byte[1024];
                                    int read = is.read(buffer);
                                    while (read > 0) {
                                        fos.write(buffer, 0, read);
                                        checksum.update(buffer, 0, read);
                                        read = is.read(buffer);
                                    }
                                    is.close();
                                    fos.flush();
                                    fos.close();
                                    if (!fm.getMd5().equals(checksum.getFormattedValue())) {
                                        logger.error("File " + fm.getName() + ":\n\texpected  : " + fm.getMd5() + "\n\tcalculated: " + checksum.getFormattedValue());
                                        throw new UpdaterException("Signature de fichier téléchargé invalide - " + fm.getName());
                                    }
                                } else {
//                                    JOptionPane.showMessageDialog(this," "+returnCode+" : "+meth.getStatusText()+"\n"+sUrl,"ERREUR",JOptionPane.ERROR_MESSAGE);
                                    errorMsg = " "+returnCode+" : "+meth.getStatusText()+" : "+sUrl;
                                    bRet = false;
                                    throw new IOException();
                                }
                            } else if (x instanceof DeleteModel) {
                                DeleteModel dm = (DeleteModel) x;
                                sb.append("delete|").append(dm.getName()).append("\n");
                            }
                        }
                    }
                    if (cm.getConfiguration() != null) {
                        ConfigurationModel conf = cm.getConfiguration();
                        sb.append("configuration|").append(conf.getClassName()).append("");
                        if (conf.getParams().size() == 0)
                            sb.append("\n");
                        else {
                            for (ParamModel pm : conf.getParams()) {
                                sb.append("|").append(pm.getName()).append("|").append(pm.getParamValue());
                            }
                            sb.append("\n");
                        }
                    }
                    // declares component in installed components
                    sb.append("install-component|").append(cm.getId()).append("|").append(cm.getRelease().getV()).append("|").append(cm.getDescription().getText()).append("\n");
                }
                bRet = true;
                File batch = new File(tmpDir, "update-script.xem");
                FileOutputStream fos = new FileOutputStream(batch);
                StringBuilder header = new StringBuilder("# update of ").append(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date())).append("\n");
                fos.write(header.toString().getBytes("ISO-8859-1"));
                fos.write(sb.toString().getBytes("ISO-8859-1"));
                // props.storeToXML(fos,"update of
                // "+DateFormat.getDateTimeInstance(DateFormat.LONG,DateFormat.LONG).format(new
                // Date()));
                fos.flush();
                fos.close();
            } catch (UpdaterException uex) {
                //new DisplayExceptionDlg(this, uex);
                exception = uex;
            } catch (MalformedURLException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch(DataConfigurationException dcEx) {
                //JOptionPane.showMessageDialog(this,dcEx.getMessage(),"ERREUR",JOptionPane.ERROR_MESSAGE);
                errorMsg = dcEx.getMessage();
            }
            return new Boolean(bRet);
        }

        @Override
        protected void done() {
            getGlassPane().setVisible(false);
        }
        
    }
}
