001/*
002 * $Id: PersistenceManager.java,v 1.44 2011/03/24 16:06:31 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv;
032
033import java.awt.Insets;
034import java.awt.Rectangle;
035import java.awt.event.ActionEvent;
036import java.awt.event.ActionListener;
037import java.io.File;
038import java.io.FileOutputStream;
039import java.io.IOException;
040
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.Enumeration;
045import java.util.HashSet;
046import java.util.Hashtable;
047import java.util.LinkedHashMap;
048import java.util.List;
049import java.util.Map;
050import java.util.Set;
051import java.util.zip.ZipEntry;
052import java.util.zip.ZipInputStream;
053
054import javax.swing.ButtonGroup;
055import javax.swing.JCheckBox;
056import javax.swing.JComboBox;
057import javax.swing.JComponent;
058import javax.swing.JLabel;
059import javax.swing.JPanel;
060import javax.swing.JRadioButton;
061import javax.swing.JTextField;
062
063import org.apache.batik.util.DoublyIndexedTable.Entry;
064import org.slf4j.Logger;
065import org.slf4j.LoggerFactory;
066
067import org.w3c.dom.Document;
068import org.w3c.dom.Element;
069import org.w3c.dom.Node;
070
071import ucar.unidata.data.DataChoice;
072import ucar.unidata.data.DataSource;
073import ucar.unidata.data.DataSourceDescriptor;
074import ucar.unidata.data.DataSourceImpl;
075import ucar.unidata.idv.DisplayControl;
076import ucar.unidata.idv.IdvManager;
077import ucar.unidata.idv.IdvObjectStore;
078import ucar.unidata.idv.IdvPersistenceManager;
079import ucar.unidata.idv.IdvResourceManager;
080import ucar.unidata.idv.IntegratedDataViewer;
081import ucar.unidata.idv.MapViewManager;
082import ucar.unidata.idv.SavedBundle;
083import ucar.unidata.idv.ViewDescriptor;
084import ucar.unidata.idv.ViewManager;
085import ucar.unidata.idv.control.DisplayControlImpl;
086import ucar.unidata.idv.ui.IdvComponentGroup;
087import ucar.unidata.idv.ui.IdvComponentHolder;
088import ucar.unidata.idv.ui.LoadBundleDialog;
089import ucar.unidata.idv.ui.WindowInfo;
090import ucar.unidata.util.ColorTable;
091import ucar.unidata.util.FileManager;
092import ucar.unidata.util.GuiUtils;
093import ucar.unidata.util.IOUtil;
094import ucar.unidata.util.LogUtil;
095import ucar.unidata.util.Misc;
096import ucar.unidata.util.PollingInfo;
097import ucar.unidata.util.StringUtil;
098import ucar.unidata.util.Trace;
099import ucar.unidata.util.TwoFacedObject;
100import ucar.unidata.xml.XmlResourceCollection;
101
102import edu.wisc.ssec.mcidasv.control.ImagePlanViewControl;
103import edu.wisc.ssec.mcidasv.probes.ReadoutProbe;
104import edu.wisc.ssec.mcidasv.ui.McvComponentGroup;
105import edu.wisc.ssec.mcidasv.ui.McvComponentHolder;
106import edu.wisc.ssec.mcidasv.ui.UIManager;
107import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
108import edu.wisc.ssec.mcidasv.util.XPathUtils;
109import edu.wisc.ssec.mcidasv.util.XmlUtil;
110
111/**
112 * <p>McIDAS-V has 99 problems, and bundles are several of 'em. Since the UI of
113 * alpha 10 relies upon component groups and McIDAS-V needs to support IDV and
114 * bundles prior to alpha 10, we must add facilities for coping with bundles
115 * that may not contain component groups. Here's a list of the issues and how
116 * they are resolved:</p> 
117 * 
118 * <p><ul>
119 * <li>Bundles prior to alpha 9 use the <code>TabbedUIManager</code>. Each tab
120 * is, internally, an IDV window. This is reflected in the contents of bundles,
121 * so the IDV wants to create a new window for each tab upon loading. Alpha 10
122 * allows the user to force bundles to only create one window. This work is
123 * done in {@link #injectComponentGroups(List)}.</li>
124 * 
125 * <li>The IDV allows users to save bundles that contain <i>both</i> 
126 * {@link ucar.unidata.idv.ViewManager}s with component groups and without! 
127 * This is actually only a problem when limiting the windows; 
128 * <code>injectComponentGroups</code> has to wrap ViewManagers without 
129 * component groups in dynamic skins. These ViewManagers must be removed 
130 * from the bundle's internal list of ViewManagers, as they don't exist until
131 * the dynamic skin is built. <i>Do not simply clear the list!</i> The 
132 * ViewManagers within component groups must appear in it, otherwise the IDV
133 * does not add them to the {@link ucar.unidata.idv.VMManager}. If limiting 
134 * windows is off, everything will be caught properly by the unpersisting 
135 * facilities in {@link edu.wisc.ssec.mcidasv.ui.UIManager}.</li>
136 * 
137 * @see IdvPersistenceManager
138 * @see UIManager
139 */
140public class PersistenceManager extends IdvPersistenceManager {
141
142    /** Key used to access a bundle's McIDAS-V in-depth versioning info section. */
143    public static final String ID_MCV_VERSION = "mcvversion";
144
145    private static final Logger logger = LoggerFactory.getLogger(PersistenceManager.class);
146
147    /**
148     * Macro used as a place holder for wherever the IDV decides to place 
149     * extracted contents of a bundle. 
150     */
151    private static final String MACRO_ZIDVPATH = '%'+PROP_ZIDVPATH+'%';
152
153    static ucar.unidata.util.LogUtil.LogCategory log_ =
154        ucar.unidata.util.LogUtil.getLogInstance(IdvManager.class.getName());
155
156    /** Is the bundle being saved a layout bundle? */
157    private boolean savingDefaultLayout = false;
158
159    /** Stores the last active ViewManager from <i>before</i> a bundle load. */
160    private ViewManager lastBeforeBundle = null;
161
162    /** 
163     * Whether or not the user wants to attempt merging bundled layers into
164     * current displays.
165     */
166    private boolean mergeBundledLayers = false;
167
168    /** Whether or not a bundle is actively loading. */
169    private boolean bundleLoading = false;
170
171    /** Cache the parameter sets XML */
172    private XmlResourceCollection parameterSets;
173    private static Document parameterSetsDocument;
174    private static Element parameterSetsRoot;
175    private static final String TAG_FOLDER = "folder";
176    private static final String TAG_DEFAULT = "default";
177    private static final String ATTR_NAME = "name";
178
179    /** Use radio buttons to control state saving */
180    private JRadioButton layoutOnlyRadio;
181    private JRadioButton layoutSourcesRadio;
182    private JRadioButton layoutSourcesDataRadio;
183    
184    /**
185     * Java requires this constructor. 
186     */
187    public PersistenceManager() {
188        this(null);
189    }
190
191    /**
192     * @see ucar.unidata.idv.IdvPersistenceManager#PersistenceManager(IntegratedDataViewer)
193     */
194    public PersistenceManager(IntegratedDataViewer idv) {
195        super(idv);
196           
197        //TODO: Saved for future development
198/**
199        layoutOnlyRadio = new JRadioButton("Layout only");
200        layoutOnlyRadio.addActionListener(new ActionListener() {
201            public void actionPerformed(final ActionEvent e) {
202                saveJythonBox.setSelectedIndex(0);
203                saveJython = false;
204                makeDataRelativeCbx.setSelected(false);
205                makeDataRelative = false;
206                saveDataSourcesCbx.setSelected(false);
207                saveDataSources = false;
208                saveDataCbx.setSelected(false);
209                saveData = false;
210            }
211        });
212
213        layoutSourcesRadio = new JRadioButton("Layout & Data Sources");
214        layoutSourcesRadio.addActionListener(new ActionListener() {
215            public void actionPerformed(final ActionEvent e) {
216                saveJythonBox.setSelectedIndex(1);
217                saveJython = true;
218                makeDataRelativeCbx.setSelected(false);
219                makeDataRelative = false;
220                saveDataSourcesCbx.setSelected(true);
221                saveDataSources = true;
222                saveDataCbx.setSelected(false);
223                saveData = false;
224            }
225        });
226        
227        layoutSourcesDataRadio = new JRadioButton("Layout, Data Sources & Data");
228        layoutSourcesRadio.addActionListener(new ActionListener() {
229            public void actionPerformed(final ActionEvent e) {
230                saveJythonBox.setSelectedIndex(1);
231                saveJython = true;
232                makeDataRelativeCbx.setSelected(false);
233                makeDataRelative = false;
234                saveDataSourcesCbx.setSelected(true);
235                saveDataSources = true;
236                saveDataCbx.setSelected(true);
237                saveData = true;
238            }
239        });
240        //Group the radio buttons.
241        layoutSourcesRadio.setSelected(true);
242        ButtonGroup group = new ButtonGroup();
243        group.add(layoutOnlyRadio);
244        group.add(layoutSourcesRadio);
245        group.add(layoutSourcesDataRadio);
246*/
247        
248    }
249
250    /**
251     * Returns the last active {@link ViewManager} from <i>before</i> loading
252     * the most recent bundle.
253     * 
254     * @return Either the ViewManager or {@code null} if there was no previous
255     * ViewManager (such as loading a default bundle/layout).
256     */
257    public ViewManager getLastViewManager() {
258        return lastBeforeBundle;
259    }
260
261    /**
262     * Returns whether or not a bundle is currently being loaded.
263     * 
264     * @return Either {@code true} if {@code instantiateFromBundle} is doing 
265     * what it needs to do, or {@code false}.
266     * 
267     * @see #instantiateFromBundle(Hashtable, boolean, LoadBundleDialog, boolean, Hashtable, boolean, boolean, boolean)
268     */
269    public boolean isBundleLoading() {
270        return bundleLoading;
271    }
272
273    public boolean getMergeBundledLayers() {
274        logger.trace("mergeBundledLayers={}", mergeBundledLayers);
275        return mergeBundledLayers;
276    }
277
278    private void setMergeBundledLayers(final boolean newValue) {
279        logger.trace("old={} new={}", mergeBundledLayers, newValue);
280        mergeBundledLayers = newValue;
281    }
282
283    @Override public boolean getSaveDataSources() {
284        boolean result = false;
285        if (!savingDefaultLayout) {
286            result = super.getSaveDataSources();
287        }
288        logger.trace("getSaveDataSources={} savingDefaultLayout={}", result, savingDefaultLayout);
289        return result;
290    }
291
292    @Override public boolean getSaveDisplays() {
293        boolean result = false;
294        if (!savingDefaultLayout) {
295            result = super.getSaveDisplays();
296        }
297        logger.trace("getSaveDisplays={} savingDefaultLayout={}", result, savingDefaultLayout);
298        return result;
299    }
300
301    @Override public boolean getSaveViewState() {
302        boolean result = true;
303        if (!savingDefaultLayout) {
304            result = super.getSaveViewState();
305        }
306        logger.trace("getSaveViewState={} savingDefaultLayout={}", result, savingDefaultLayout);
307        return result;
308    }
309
310    @Override public boolean getSaveJython() {
311        boolean result = false;
312        if (!savingDefaultLayout) {
313            result = super.getSaveJython();
314        }
315        logger.trace("getSaveJython={} savingDefaultLayout={}", result, savingDefaultLayout);
316        return result;
317    }
318
319    public void doSaveAsDefaultLayout() {
320        String layoutFile = getResourceManager().getResources(IdvResourceManager.RSC_BUNDLES).getWritable();
321        // do prop check here?
322        File f = new File(layoutFile);
323        if (f.exists()) {
324            boolean result = GuiUtils.showYesNoDialog(null, "Saving a new default layout will overwrite your existing default layout. Do you wish to continue?", "Overwrite Confirmation");
325            if (!result) {
326                return;
327            }
328        }
329
330        savingDefaultLayout = true;
331        try {
332            String xml = getBundleXml(true, true);
333            if (xml != null) {
334                IOUtil.writeFile(layoutFile, xml);
335            }
336        } catch (Exception e) {
337            e.printStackTrace();
338        } finally {
339            savingDefaultLayout = false;
340        }
341    }
342
343    @Override public JPanel getFileAccessory() {
344        // Always save displays and data sources
345        saveDisplaysCbx.setSelected(true);
346        saveDisplays = true;
347        saveViewStateCbx.setSelected(true);
348        saveViewState = true;
349        saveDataSourcesCbx.setSelected(true);
350        saveDataSources = true;
351
352        return GuiUtils.top(
353            GuiUtils.vbox(
354                Misc.newList(
355                                GuiUtils.inset(new JLabel("Bundle save options:"),
356                                new Insets(0, 5, 5, 0)),
357                                saveJythonBox,
358                                makeDataRelativeCbx)));
359    }
360    
361    /**
362     * Have the user select an xidv filename and
363     * write the current application state to it.
364     * This also sets the current file name and
365     * adds the file to the history list.
366     */
367    public void doSaveAs() {
368        String filename =
369            FileManager.getWriteFile(getArgsManager().getBundleFileFilters(),
370                                     "mcvz", getFileAccessory());
371        if (filename == null) {
372            return;
373        }
374        setCurrentFileName(filename);
375
376        boolean prevMakeDataEditable = makeDataEditable;
377        makeDataEditable = makeDataEditableCbx.isSelected();
378
379        boolean prevMakeDataRelative = makeDataRelative;
380        makeDataRelative = makeDataRelativeCbx.isSelected();
381        if (doSave(filename)) {
382            getPublishManager().publishContent(filename, null, publishCbx);
383            getIdv().addToHistoryList(filename);
384        }
385        makeDataEditable = prevMakeDataEditable;
386        makeDataRelative = prevMakeDataRelative;
387
388    }
389
390    /**
391     * Overridden so that McIDAS-V can: 
392     * <ul>
393     * <li>add better versioning information to bundles</li>
394     * <li>remove {@link edu.wisc.ssec.mcidasv.probes.ReadoutProbe ReadoutProbes} from the {@code displayControls} that are getting persisted.</li>
395     * <li>disallow saving multi-banded ADDE data sources until we have fix!</li>
396     * </ul>
397     */
398    @Override protected boolean addToBundle(Hashtable data, List dataSources,
399            List displayControls, List viewManagers,
400            String jython) 
401    {
402        logger.trace("hacking bundle output!");
403        // add in some extra versioning information
404        StateManager stateManager = (StateManager)getIdv().getStateManager();
405        if (data != null) {
406            data.put(ID_MCV_VERSION, stateManager.getVersionInfo());
407        }
408        logger.trace("hacking displayControls={}", displayControls);
409        logger.trace("hacking dataSources={}", dataSources);
410        // remove ReadoutProbes from the list and possibly save off multibanded
411        // ADDE data sources
412        if (displayControls != null) {
413//            Set<DataSourceImpl> observed = new HashSet<DataSourceImpl>();
414            Map<DataSourceImpl, List<DataChoice>> observed = new LinkedHashMap<DataSourceImpl, List<DataChoice>>();
415            List<DisplayControl> newControls = new ArrayList<DisplayControl>();
416            for (DisplayControl dc : (List<DisplayControl>)displayControls) {
417                if (dc instanceof ReadoutProbe) {
418                    logger.trace("skipping readoutprobe!");
419                    continue;
420                } else if (dc instanceof ImagePlanViewControl) {
421                    ImagePlanViewControl imageControl = (ImagePlanViewControl)dc;
422                    List<DataSourceImpl> tmp = (List<DataSourceImpl>)imageControl.getDataSources();
423                    for (DataSourceImpl src : tmp) {
424                        if (observed.containsKey(src)) {
425                            observed.get(src).addAll(src.getDataChoices());
426                            logger.trace("already seen src={} new selection={}", src);
427                        } else {
428                            logger.trace("haven't seen src={}", src);
429                            List<DataChoice> selected = new ArrayList<DataChoice>(imageControl.getDataChoices());
430                            observed.put(src, selected);
431                        }
432                    }
433                    logger.trace("found an image control: {} datasrcs={} datachoices={}", new Object[] { imageControl, imageControl.getDataSources(), imageControl.getDataChoices() });
434                    newControls.add(dc);
435                } else {
436                    logger.trace("found some kinda thing: {}", dc.getClass().getName());
437                    newControls.add(dc);
438                }
439            }
440            for (Map.Entry<DataSourceImpl, List<DataChoice>> entry : observed.entrySet()) {
441                logger.trace("multibanded src={} choices={}", entry.getKey(), entry.getValue());
442            }
443            displayControls = newControls;
444        }
445
446        return super.addToBundle(data, dataSources, displayControls, viewManagers, jython);
447    }
448
449    @Override public List getLocalBundles() {
450        List<SavedBundle> allBundles = new ArrayList<SavedBundle>();
451        List<String> dirs = new ArrayList<String>();
452        String sitePath = getResourceManager().getSitePath();
453
454        Collections.addAll(dirs, getStore().getLocalBundlesDir());
455
456        if (sitePath != null) {
457            dirs.add(IOUtil.joinDir(sitePath, IdvObjectStore.DIR_BUNDLES));
458        }
459
460        for (String top : dirs) {
461            List<File> subdirs = 
462                IOUtil.getDirectories(Collections.singletonList(top), true);
463            for (File subdir : subdirs) {
464                loadBundlesInDirectory(allBundles, 
465                    fileToCategories(top, subdir.getPath()), subdir);
466            }
467        }
468        return allBundles;
469    }
470
471    protected void loadBundlesInDirectory(List<SavedBundle> allBundles,
472            List categories, File file) {
473        String[] localBundles = file.list();
474
475        for (int i = 0; i < localBundles.length; i++) {
476            String filename = IOUtil.joinDir(file.toString(), localBundles[i]);
477            if (ArgumentManager.isBundle(filename)) {
478                allBundles.add(new SavedBundle(filename,
479                    IOUtil.stripExtension(localBundles[i]), categories, true));
480            }
481        }
482    }
483
484    /**
485     * <p>
486     * Overridden so that McIDAS-V can redirect to the version of this method
487     * that supports limiting the number of new windows.
488     * </p>
489     * 
490     * @see #decodeXml(String, boolean, String, String, boolean, boolean,
491     *      Hashtable, boolean, boolean, boolean)
492     */
493    @Override public void decodeXml(String xml, final boolean fromCollab,
494        String xmlFile, final String label, final boolean showDialog,
495        final boolean shouldMerge, final Hashtable bundleProperties,
496        final boolean removeAll, final boolean letUserChangeData) 
497    {
498        decodeXml(xml, fromCollab, xmlFile, label, showDialog, shouldMerge,
499            bundleProperties, removeAll, letUserChangeData, false);
500    }
501
502    /**
503     * <p>
504     * Hijacks control of the IDV's bundle loading facilities. Due to the way
505     * versions of McIDAS-V prior to alpha 10 handled tabs, the user will end
506     * up with a new window for each tab in the bundle. McIDAS-V alpha 10 has
507     * the ability to only create one new window and have everything else go
508     * into that window's tabs.
509     * </p>
510     * 
511     * @see IdvPersistenceManager#decodeXmlFile(String, String, boolean, boolean, Hashtable)
512     * @see #decodeXml(String, boolean, String, String, boolean, boolean, Hashtable,
513     *      boolean, boolean, boolean)
514     */
515    @Override public boolean decodeXmlFile(String xmlFile, String label,
516                                 boolean checkToRemove,
517                                 boolean letUserChangeData,
518                                 Hashtable bundleProperties) {
519
520        String name = ((label != null) ? label : IOUtil.getFileTail(xmlFile));
521
522        boolean shouldMerge = getStore().get(PREF_OPEN_MERGE, true);
523
524        boolean removeAll   = false;
525
526        boolean limitNewWindows = false;
527
528        boolean mergeLayers = false;
529        setMergeBundledLayers(false);
530
531        if (checkToRemove) {
532            // ok[0] = did the user press cancel 
533            boolean[] ok = getPreferenceManager().getDoRemoveBeforeOpening(name);
534
535            if (!ok[0]) {
536                return false;
537            }
538
539            if (!ok[1] && !ok[2]) { // create new [opt=0]
540                removeAll = false;
541                shouldMerge = false;
542                mergeLayers = false;
543            }
544            if (!ok[1] && ok[2]) { // add new tabs [opt=2]
545                removeAll = false;
546                shouldMerge = true;
547                mergeLayers = false;
548            }
549            if (ok[1] && !ok[2]) { // merge with active [opt=1]
550                removeAll = false;
551                shouldMerge = false;
552                mergeLayers = true;
553            }
554            if (ok[1] && ok[2]) { // replace session [opt=3]
555                removeAll = true;
556                shouldMerge = true;
557                mergeLayers = false;
558            }
559
560            logger.trace("removeAll={} shouldMerge={} mergeLayers={}", new Object[] { removeAll, shouldMerge, mergeLayers });
561
562            setMergeBundledLayers(mergeLayers);
563
564            if (removeAll) {
565                // Remove the displays first because, if we remove the data 
566                // some state can get cleared that might be accessed from a 
567                // timeChanged on the unremoved displays
568                getIdv().removeAllDisplays();
569                // Then remove the data
570                getIdv().removeAllDataSources();
571            }
572
573            if (ok.length == 4) {
574                limitNewWindows = ok[3];
575            }
576        }
577
578        // the UI manager may need to know which ViewManager was active *before*
579        // we loaded the bundle.
580        lastBeforeBundle = getVMManager().getLastActiveViewManager();
581
582        ArgumentManager argsManager = (ArgumentManager)getArgsManager();
583
584        boolean isZidv = ArgumentManager.isZippedBundle(xmlFile);
585
586        if (!isZidv && !ArgumentManager.isXmlBundle(xmlFile)) {
587            //If we cannot tell what it is then try to open it as a zidv file
588            try {
589                ZipInputStream zin = 
590                    new ZipInputStream(IOUtil.getInputStream(xmlFile));
591                isZidv = (zin.getNextEntry() != null);
592            } catch (Exception e) {}
593        }
594
595        String bundleContents = null;
596        try {
597            //Is this a zip file
598            logger.trace("bundle file={} isZipped={}", xmlFile, ArgumentManager.isZippedBundle(xmlFile));
599            if (ArgumentManager.isZippedBundle(xmlFile)) {
600                boolean ask   = getStore().get(PREF_ZIDV_ASK, true);
601                boolean toTmp = getStore().get(PREF_ZIDV_SAVETOTMP, true);
602                String  dir   = getStore().get(PREF_ZIDV_DIRECTORY, "");
603                if (ask || ((dir.length() == 0) && !toTmp)) {
604
605                    JCheckBox askCbx = 
606                        new JCheckBox("Don't show this again", !ask);
607
608                    JRadioButton tmpBtn =
609                        new JRadioButton("Write to temporary directory", toTmp);
610
611                    JRadioButton dirBtn = 
612                        new JRadioButton("Write to:", !toTmp);
613
614                    GuiUtils.buttonGroup(tmpBtn, dirBtn);
615                    JTextField dirFld = new JTextField(dir, 30);
616                    JComponent dirComp = GuiUtils.centerRight(
617                                            dirFld,
618                                            GuiUtils.makeFileBrowseButton(
619                                                dirFld, true, null));
620
621                    JComponent contents =
622                        GuiUtils
623                            .vbox(GuiUtils
624                                .inset(new JLabel("Where should the data files be written to?"),
625                                        5), tmpBtn,
626                                        GuiUtils.hbox(dirBtn, dirComp),
627                                            GuiUtils
628                                                .inset(askCbx,
629                                                    new Insets(5, 0, 0, 0)));
630
631                    contents = GuiUtils.inset(contents, 5);
632                    if (!GuiUtils.showOkCancelDialog(null, "Zip file data",
633                            contents, null)) {
634                        return false;
635                    }
636
637                    ask = !askCbx.isSelected();
638
639                    toTmp = tmpBtn.isSelected();
640
641                    dir = dirFld.getText().toString().trim();
642
643                    getStore().put(PREF_ZIDV_ASK, ask);
644                    getStore().put(PREF_ZIDV_SAVETOTMP, toTmp);
645                    getStore().put(PREF_ZIDV_DIRECTORY, dir);
646                    getStore().save();
647                }
648
649                String tmpDir = dir;
650                if (toTmp) {
651                    tmpDir = getIdv().getObjectStore().getUserTmpDirectory();
652                    tmpDir = IOUtil.joinDir(tmpDir, Misc.getUniqueId());
653                }
654                IOUtil.makeDir(tmpDir);
655
656                getStateManager().putProperty(PROP_ZIDVPATH, tmpDir);
657                ZipInputStream zin =
658                    new ZipInputStream(IOUtil.getInputStream(xmlFile));
659                ZipEntry ze = null;
660
661                while ((ze = zin.getNextEntry()) != null) {
662                    String entryName = ze.getName();
663
664                    if (ArgumentManager.isXmlBundle(entryName.toLowerCase())) {
665                        bundleContents = new String(IOUtil.readBytes(zin,
666                                null, false));
667                    } else {
668//                        String xmlPath = IOUtil.joinDir(tmpDir, entryName);
669                        if (IOUtil.writeTo(zin, new FileOutputStream(IOUtil.joinDir(tmpDir, entryName))) < 0) {
670                            return false;
671                        }
672                    }
673                }
674            } else {
675                Trace.call1("Decode.readContents");
676                bundleContents = IOUtil.readContents(xmlFile);
677                Trace.call2("Decode.readContents");
678            }
679
680            // TODO: this can probably go one day. I altered the prefix of the
681            // comp group classes. Old: "McIDASV...", new: "Mcv..."
682            // just gotta be sure to fix the references in the bundles.
683            // only people using the nightly build will be affected.
684            if (bundleContents != null) {
685                bundleContents = StringUtil.substitute(bundleContents, 
686                    OLD_COMP_STUFF, NEW_COMP_STUFF);
687                bundleContents = StringUtil.substitute(bundleContents, 
688                    OLD_SOURCE_MACRO, NEW_SOURCE_MACRO);
689            }
690            
691            
692            Trace.call1("Decode.decodeXml");
693            decodeXml(bundleContents, false, xmlFile, name, true,
694                      shouldMerge, bundleProperties, removeAll,
695                      letUserChangeData, limitNewWindows);
696            Trace.call2("Decode.decodeXml");
697            return true;
698        } catch (Throwable exc) {
699            if (contents == null) {
700                logException("Unable to load bundle:" + xmlFile, exc);
701            } else {
702                logException("Unable to evaluate bundle:" + xmlFile, exc);
703            }
704            return false;
705        }
706    }
707
708    // replace "old" references in a bundle's XML to the "new" classes.
709    private static final String OLD_COMP_STUFF = "McIDASVComp";
710    private static final String NEW_COMP_STUFF = "McvComp";
711
712    private static final String OLD_SOURCE_MACRO = "%fulldatasourcename%";
713    private static final String NEW_SOURCE_MACRO = "%datasourcename%";
714
715    /**
716     * <p>Overridden so that McIDAS-V can redirect to the version of this 
717     * method that supports limiting the number of new windows.</p>
718     * 
719     * @see #decodeXmlInner(String, boolean, String, String, boolean, boolean, Hashtable, boolean, boolean, boolean)
720     */
721    @Override protected synchronized void decodeXmlInner(String xml,
722                                                         boolean fromCollab, 
723                                                         String xmlFile, 
724                                                         String label, 
725                                                         boolean showDialog, 
726                                                         boolean shouldMerge, 
727                                                         Hashtable bundleProperties,
728                                                         boolean didRemoveAll, 
729                                                         boolean changeData) {
730
731        decodeXmlInner(xml, fromCollab, xmlFile, label, showDialog, 
732                      shouldMerge, bundleProperties, didRemoveAll, changeData, 
733                      false);
734
735    }
736
737    /**
738     * <p>
739     * Overridden so that McIDAS-V can redirect to the version of this method
740     * that supports limiting the number of new windows.
741     * </p>
742     * 
743     * @see #instantiateFromBundle(Hashtable, boolean, LoadBundleDialog,
744     *      boolean, Hashtable, boolean, boolean, boolean)
745     */
746    @Override protected void instantiateFromBundle(Hashtable ht,
747        boolean fromCollab, LoadBundleDialog loadDialog, boolean shouldMerge,
748        Hashtable bundleProperties, boolean didRemoveAll,
749        boolean letUserChangeData) throws Exception 
750    {
751        instantiateFromBundle(ht, fromCollab, loadDialog, shouldMerge,
752            bundleProperties, didRemoveAll, letUserChangeData, false);
753    }
754
755    /**
756     * <p>
757     * Hijacks the second part of the IDV bundle loading pipeline so that
758     * McIDAS-V can limit the number of new windows.
759     * </p>
760     * 
761     * @see IdvPersistenceManager#decodeXml(String, boolean,
762     *      String, String, boolean, boolean, Hashtable, boolean, boolean)
763     * @see #decodeXmlInner(String, boolean, String, String, boolean, boolean,
764     *      Hashtable, boolean, boolean, boolean)
765     */
766    public void decodeXml(final String xml, final boolean fromCollab,
767        final String xmlFile, final String label, final boolean showDialog,
768        final boolean shouldMerge, final Hashtable bundleProperties,
769        final boolean removeAll, final boolean letUserChangeData,
770        final boolean limitWindows) 
771    {
772
773        if (!getStateManager().getShouldLoadBundlesSynchronously()) {
774            Runnable runnable = new Runnable() {
775
776                public void run() {
777                    decodeXmlInner(xml, fromCollab, xmlFile, label,
778                        showDialog, shouldMerge, bundleProperties, removeAll,
779                        letUserChangeData, limitWindows);
780                }
781            };
782            Misc.run(runnable);
783        } else {
784            decodeXmlInner(xml, fromCollab, xmlFile, label, showDialog,
785                shouldMerge, bundleProperties, removeAll, letUserChangeData,
786                limitWindows);
787        }
788    }
789
790    /**
791     * <p>Hijacks the third part of the bundle loading pipeline.</p>
792     * 
793     * @see IdvPersistenceManager#decodeXmlInner(String, boolean, String, String, boolean, boolean, Hashtable, boolean, boolean)
794     * @see #instantiateFromBundle(Hashtable, boolean, LoadBundleDialog, boolean, Hashtable, boolean, boolean, boolean)
795     */
796    protected synchronized void decodeXmlInner(String xml, boolean fromCollab, 
797                                               String xmlFile, String label,
798                                               boolean showDialog, 
799                                               boolean shouldMerge, 
800                                               Hashtable bundleProperties, 
801                                               boolean didRemoveAll, 
802                                               boolean letUserChangeData, 
803                                               boolean limitNewWindows) {
804
805        LoadBundleDialog loadDialog = new LoadBundleDialog(this, label);
806
807        boolean inError = false;
808
809        if ( !fromCollab) {
810            showWaitCursor();
811            if (showDialog) {
812                loadDialog.showDialog();
813            }
814        }
815
816        if (xmlFile != null) {
817            getStateManager().putProperty(PROP_BUNDLEPATH,
818                                          IOUtil.getFileRoot(xmlFile));
819        }
820
821        getStateManager().putProperty(PROP_LOADINGXML, true);
822        try {
823            xml = applyPropertiesToBundle(xml);
824            if (xml == null) {
825                return;
826            }
827
828//            checkForBadMaps(xmlFile);
829
830            Trace.call1("Decode.toObject");
831            Object data = getIdv().getEncoderForRead().toObject(xml);
832            Trace.call2("Decode.toObject");
833
834            if (data != null) {
835                Hashtable properties = new Hashtable();
836                if (data instanceof Hashtable) {
837                    Hashtable ht = (Hashtable) data;
838
839                    instantiateFromBundle(ht, fromCollab, loadDialog,
840                                          shouldMerge, bundleProperties,
841                                          didRemoveAll, letUserChangeData, 
842                                          limitNewWindows);
843
844                } else if (data instanceof DisplayControl) {
845                    ((DisplayControl) data).initAfterUnPersistence(getIdv(),
846                                                                   properties);
847                    loadDialog.addDisplayControl((DisplayControl) data);
848                } else if (data instanceof DataSource) {
849                    getIdv().getDataManager().addDataSource((DataSource)data);
850                } else if (data instanceof ColorTable) {
851                    getColorTableManager().doImport(data, true);
852                } else {
853                    LogUtil.userErrorMessage(log_,
854                                             "Decoding xml. Unknown object type:"
855                                             + data.getClass().getName());
856                }
857
858                if ( !fromCollab && getIdv().haveCollabManager()) {
859                    getCollabManager().write(getCollabManager().MSG_BUNDLE,
860                                             xml);
861                }
862            }
863        } catch (Throwable exc) {
864            if (xmlFile != null) {
865                logException("Error loading bundle: " + xmlFile, exc);
866            } else {
867                logException("Error loading bundle", exc);
868            }
869
870            inError = true;
871        }
872
873        if (!fromCollab) {
874            showNormalCursor();
875        }
876
877        getStateManager().putProperty(PROP_BUNDLEPATH, "");
878        getStateManager().putProperty(PROP_ZIDVPATH, "");
879        getStateManager().putProperty(PROP_LOADINGXML, false);
880
881        if (!inError && getIdv().getInteractiveMode() && xmlFile != null) {
882            getIdv().addToHistoryList(xmlFile);
883        }
884
885        loadDialog.dispose();
886        if (loadDialog.getShouldRemoveItems()) {
887            List displayControls = loadDialog.getDisplayControls();
888            for (int i = 0; i < displayControls.size(); i++) {
889                try {
890                    ((DisplayControl) displayControls.get(i)).doRemove();
891                } catch (Exception exc) {
892                    logger.warn("unexpected exception={}", exc);
893                }
894            }
895            List dataSources = loadDialog.getDataSources();
896            for (int i = 0; i < dataSources.size(); i++) {
897                getIdv().removeDataSource((DataSource) dataSources.get(i));
898            }
899        }
900
901        loadDialog.clear();
902    }
903
904    // initial pass at trying to fix bundles with resources mcv hasn't heard of
905    private void checkForBadMaps(final String bundlePath) {
906        String xpath = "//property[@name=\"InitialMap\"]/string|//property[@name=\"MapStates\"]//property[@name=\"Source\"]/string";
907        for (Node node : XPathUtils.nodes(bundlePath, xpath)) {
908            String mapPath = node.getTextContent();
909            if (mapPath.contains("_dir/")) { // hahaha this needs some work
910                List<String> toks = StringUtil.split(mapPath, "_dir/");
911                if (toks.size() == 2) {
912                    String plugin = toks.get(0).replace("/", "");
913                    logger.trace("plugin: {} map: {}", plugin, mapPath);
914                }
915            } else {
916                logger.trace("normal map: {}", mapPath);
917            }
918        }
919    }
920
921    /**
922     * <p>
923     * Builds a list of an incoming bundle's
924     * {@link ucar.unidata.idv.ViewManager}s that are part of a component
925     * group.
926     * </p>
927     * 
928     * <p>
929     * The reason for only being interested in component groups is because any
930     * windows <i>not</i> using component groups will be made into a dynamic
931     * skin. The associated ViewManagers do not technically exist until the
932     * skin has been &quot;built&quot;, so there's nothing to do. These
933     * ViewManagers must also be removed from the bundle's list of
934     * ViewManagers.
935     * </p>
936     * 
937     * <p>
938     * However, any ViewManagers associated with component groups still need to
939     * appear in the bundle's ViewManager list, and that's where this method
940     * comes into play!
941     * </p>
942     * 
943     * @param windows WindowInfos to be searched.
944     * 
945     * @return List of ViewManagers inside any component groups.
946     */
947    protected static List<ViewManager> extractCompGroupVMs(
948        final List<WindowInfo> windows) 
949    {
950
951        List<ViewManager> newList = new ArrayList<ViewManager>();
952
953        for (WindowInfo window : windows) {
954            Collection<Object> comps =
955                window.getPersistentComponents().values();
956
957            for (Object comp : comps) {
958                if (!(comp instanceof IdvComponentGroup)) {
959                    continue;
960                }
961
962                IdvComponentGroup group = (IdvComponentGroup)comp;
963                List<IdvComponentHolder> holders =
964                    group.getDisplayComponents();
965
966                for (IdvComponentHolder holder : holders) {
967                    if (holder.getViewManagers() != null) {
968                        logger.trace("extracted: {}", holder.getViewManagers().size());
969                        newList.addAll(holder.getViewManagers());
970                    }
971                }
972            }
973        }
974        return newList;
975    }
976
977    /**
978     * <p>Does the work in fixing the collisions described in the
979     * <code>instantiateFromBundle</code> javadoc. Basically just queries the
980     * {@link ucar.unidata.idv.VMManager} for each 
981     * {@link ucar.unidata.idv.ViewManager}. If a match is found, a new ID is
982     * generated and associated with the ViewManager, its 
983     * {@link ucar.unidata.idv.ViewDescriptor}, and any associated 
984     * {@link ucar.unidata.idv.DisplayControl}s.</p>
985     * 
986     * @param vms ViewManagers in the incoming bundle.
987     * 
988     * @see #instantiateFromBundle(Hashtable, boolean, LoadBundleDialog, boolean, Hashtable, boolean, boolean, boolean)
989     */
990    protected void reverseCollisions(final List<ViewManager> vms) {
991        for (ViewManager vm : vms) {
992            ViewDescriptor vd = vm.getViewDescriptor();
993            ViewManager current = getVMManager().findViewManager(vd);
994            if (current != null) {
995                ViewDescriptor oldVd = current.getViewDescriptor();
996                String oldId = oldVd.getName();
997                String newId = "view_" + Misc.getUniqueId();
998
999                oldVd.setName(newId);
1000                current.setUniqueId(newId);
1001
1002                List<DisplayControlImpl> controls = current.getControls();
1003                for (DisplayControlImpl control : controls) {
1004                    control.resetViewManager(oldId, newId);
1005                }
1006            }
1007        }
1008    }
1009
1010    /**
1011     * <p>Builds a single window with a single component group. The group 
1012     * contains component holders that correspond to each window or component
1013     * holder stored in the incoming bundle.</p>
1014     * 
1015     * @param windows The bundle's list of 
1016     *                {@link ucar.unidata.idv.ui.WindowInfo}s.
1017     * 
1018     * @return List of WindowInfos that contains only one element/window.
1019     * 
1020     * @throws Exception Bubble up any exceptions from 
1021     *                   <code>makeImpromptuSkin</code>.
1022     */
1023    protected List<WindowInfo> injectComponentGroups(
1024        final List<WindowInfo> windows) throws Exception {
1025
1026        McvComponentGroup group = 
1027            new McvComponentGroup(getIdv(), "Group");
1028
1029        group.setLayout(McvComponentGroup.LAYOUT_TABS);
1030
1031        Hashtable<String, McvComponentGroup> persist = 
1032            new Hashtable<String, McvComponentGroup>();
1033
1034        for (WindowInfo window : windows) {
1035            List<IdvComponentHolder> holders = buildHolders(window);
1036            for (IdvComponentHolder holder : holders)
1037                group.addComponent(holder);
1038        }
1039
1040        persist.put("comp1", group);
1041
1042        // build a new window that contains our component group.
1043        WindowInfo limitedWindow = new WindowInfo();
1044        limitedWindow.setPersistentComponents(persist);
1045        limitedWindow.setSkinPath(Constants.BLANK_COMP_GROUP);
1046        limitedWindow.setIsAMainWindow(true);
1047        limitedWindow.setTitle("Super Test");
1048        limitedWindow.setViewManagers(new ArrayList<ViewManager>());
1049        limitedWindow.setBounds(windows.get(0).getBounds());
1050
1051        // make a new list so that we can populate the list of windows with 
1052        // our single window.
1053        List<WindowInfo> newWindow = new ArrayList<WindowInfo>();
1054        newWindow.add(limitedWindow);
1055        return newWindow;
1056    }
1057
1058    /**
1059     * <p>
1060     * Builds an altered copy of <code>windows</code> that preserves the
1061     * number of windows while ensuring all displays are inside component
1062     * holders.
1063     * </p>
1064     * 
1065     * @throws Exception Bubble up dynamic skin exceptions.
1066     * 
1067     * @see #injectComponentGroups(List)
1068     */
1069    // TODO: better name!!
1070    protected List<WindowInfo> betterInject(final List<WindowInfo> windows)
1071        throws Exception 
1072    {
1073
1074        List<WindowInfo> newList = new ArrayList<WindowInfo>();
1075
1076        for (WindowInfo window : windows) {
1077            McvComponentGroup group = new McvComponentGroup(getIdv(), "Group");
1078
1079            group.setLayout(McvComponentGroup.LAYOUT_TABS);
1080
1081            Hashtable<String, McvComponentGroup> persist =
1082                new Hashtable<String, McvComponentGroup>();
1083
1084            List<IdvComponentHolder> holders = buildHolders(window);
1085            for (IdvComponentHolder holder : holders) {
1086                group.addComponent(holder);
1087            }
1088
1089            persist.put("comp1", group);
1090            WindowInfo newWindow = new WindowInfo();
1091            newWindow.setPersistentComponents(persist);
1092            newWindow.setSkinPath(Constants.BLANK_COMP_GROUP);
1093            newWindow.setIsAMainWindow(window.getIsAMainWindow());
1094            newWindow.setViewManagers(new ArrayList<ViewManager>());
1095            newWindow.setBounds(window.getBounds());
1096
1097            newList.add(newWindow);
1098        }
1099        return newList;
1100    }
1101
1102    /**
1103     * <p>Builds a list of component holders with all of <code>window</code>'s
1104     * displays.</p>
1105     * 
1106     * @throws Exception Bubble up any problems creating a dynamic skin.
1107     */
1108    // TODO: refactor
1109    protected List<IdvComponentHolder> buildHolders(final WindowInfo window) 
1110        throws Exception {
1111
1112        List<IdvComponentHolder> holders = 
1113            new ArrayList<IdvComponentHolder>();
1114
1115        if (!window.getPersistentComponents().isEmpty()) {
1116            Collection<Object> comps = 
1117                window.getPersistentComponents().values();
1118
1119            for (Object comp : comps) {
1120                if (!(comp instanceof IdvComponentGroup)) {
1121                    continue;
1122                }
1123
1124                IdvComponentGroup group = (IdvComponentGroup)comp;
1125                holders.addAll(McVGuiUtils.getComponentHolders(group));
1126            }
1127        } else {
1128            holders.add(makeDynSkin(window));
1129        }
1130
1131        return holders;
1132    }
1133
1134    /**
1135     * <p>Builds a list of any dynamic skins in the bundle and adds them to the
1136     * UIMananger's &quot;cache&quot; of encountered ViewManagers.</p>
1137     * 
1138     * @param windows The bundle's windows.
1139     * 
1140     * @return Any dynamic skins in <code>windows</code>.
1141     */
1142    public List<ViewManager> mapDynamicSkins(final List<WindowInfo> windows) {
1143        List<ViewManager> vms = new ArrayList<ViewManager>();
1144        for (WindowInfo window : windows) {
1145            Collection<Object> comps = 
1146                window.getPersistentComponents().values();
1147
1148            for (Object comp : comps) {
1149                if (!(comp instanceof IdvComponentGroup)) {
1150                    continue;
1151                }
1152
1153                List<IdvComponentHolder> holders = 
1154                    new ArrayList<IdvComponentHolder>(
1155                            ((IdvComponentGroup)comp).getDisplayComponents());
1156
1157                for (IdvComponentHolder holder : holders) {
1158                    if (!McVGuiUtils.isDynamicSkin(holder)) {
1159                        continue;
1160                    }
1161                    List<ViewManager> tmpvms = holder.getViewManagers();
1162                    for (ViewManager vm : tmpvms) {
1163                        vms.add(vm);
1164                        UIManager.savedViewManagers.put(
1165                            vm.getViewDescriptor().getName(), vm);
1166                    }
1167                    holder.setViewManagers(new ArrayList<ViewManager>());
1168                }
1169            }
1170        }
1171        return vms;
1172    }
1173
1174    /**
1175     * Attempts to reconcile McIDAS-V's ability to easily load all files in a
1176     * directory with the way the IDV expects file data sources to behave upon
1177     * unpersistence.
1178     * 
1179     * <p>The problem is twofold: the paths referenced in the data source's 
1180     * {@code Sources} may not exist, and the <i>persistence</i> code combines
1181     * each individual file into a blob.
1182     * 
1183     * <p>The current solution is to note that the data source's 
1184     * {@link PollingInfo} is used by {@link ucar.unidata.data.FilesDataSource#initWithPollingInfo}
1185     * to replace the contents of the data source's file paths. Simply 
1186     * overwrite {@code PollingInfo#filePaths} with the path to the blob.
1187     * 
1188     * @param ds {@code List} of {@link DataSourceImpl}s to inspect and/or fix.
1189     * Cannot be {@code null}.
1190     * 
1191     * @see #isBulkDataSource(DataSourceImpl)
1192     */
1193    private void fixBulkDataSources(final List<DataSourceImpl> ds) {
1194        String zidvPath = getStateManager().getProperty(PROP_ZIDVPATH, "");
1195
1196        // bail out if the macro replacement cannot work
1197        if (zidvPath.length() == 0) {
1198            return;
1199        }
1200
1201        for (DataSourceImpl d : ds) {
1202            boolean isBulk = isBulkDataSource(d);
1203            if (!isBulk) {
1204                continue;
1205            }
1206
1207            // err... now do the macro sub and replace the contents of 
1208            // data paths with the singular element in temp paths?
1209            List<String> tempPaths = new ArrayList<String>(d.getTmpPaths());
1210            String tempPath = tempPaths.get(0);
1211            tempPath = tempPath.replace(MACRO_ZIDVPATH, zidvPath);
1212            tempPaths.set(0, tempPath);
1213            PollingInfo p = d.getPollingInfo();
1214            p.setFilePaths(tempPaths);
1215        }
1216    }
1217
1218    /**
1219     * Attempts to determine whether or not a given {@link DataSourceImpl} is
1220     * the result of a McIDAS-V {@literal "bulk load"}.
1221     * 
1222     * @param d {@code DataSourceImpl} to check. Cannot be {@code null}.
1223     * 
1224     * @return {@code true} if the {@code DataSourceImpl} matched the criteria.
1225     */
1226    private boolean isBulkDataSource(final DataSourceImpl d) {
1227        Hashtable properties = d.getProperties();
1228        if (properties.containsKey("bulk.load")) {
1229            // woohoo! no need to do the guesswork.
1230            Object value = properties.get("bulk.load");
1231            if (value instanceof String) {
1232                return Boolean.valueOf((String)value);
1233            } else if (value instanceof Boolean) {
1234                return (Boolean)value;
1235            }
1236        }
1237
1238        DataSourceDescriptor desc = d.getDescriptor();
1239        boolean localFiles = desc.getFileSelection();
1240
1241        List filePaths = d.getDataPaths();
1242        List tempPaths = d.getTmpPaths();
1243        if (filePaths == null || filePaths.isEmpty()) {
1244            return false;
1245        }
1246
1247        if (tempPaths == null || tempPaths.isEmpty()) {
1248            return false;
1249        }
1250
1251        // the least-involved heuristic i've found is:
1252        // localFiles == true
1253        // tempPaths.size() == 1 && filePaths.size() >= 2
1254        // and then we have a bulk load...
1255        // if those checks don't suffice, you can also look for the "prop.pollinfo" key
1256        // if the PollingInfo object has a filePaths list, with one element whose last directory matches 
1257        // the data source "name" (then you are probably good).
1258        if ((localFiles == true) && ((tempPaths.size() == 1) && (filePaths.size() >= 2))) {
1259            return true;
1260        }
1261
1262        // end of line
1263        return false;
1264    }
1265
1266    /**
1267     * <p>Overridden so that McIDAS-V can preempt the IDV's bundle loading. 
1268     * There will be problems if any of the incoming 
1269     * {@link ucar.unidata.idv.ViewManager}s share an ID with an existing 
1270     * ViewManager. While this case may seem unlikely, it can be triggered 
1271     * when loading a bundle and then reloading. The problem is that the 
1272     * ViewManagers are the same, and if the previous ViewManagers were not 
1273     * removed, the IDV doesn't know what to do.</p>
1274     * 
1275     * <p>Assigning the incoming ViewManagers a new ID, <i>and associating its
1276     * {@link ucar.unidata.idv.ViewDescriptor}s and 
1277     * {@link ucar.unidata.idv.DisplayControl}s</i> with the new ID fixes this
1278     * problem.</p>
1279     * 
1280     * <p>McIDAS-V also allows the user to limit the number of new windows the
1281     * bundle may create. If enabled, one new window will be created, and any
1282     * additional windows will become tabs (component holders) inside the new
1283     * window.</p>
1284     * 
1285     * <p>McIDAS-V also prefers the bundles being loaded to be in a 
1286     * semi-regular regular state. For example, say you have bundle containing
1287     * only data. The bundle will probably not contain lists of WindowInfos or
1288     * ViewManagers. Perhaps the bundle contains nested component groups as 
1289     * well! McIDAS-V will alter the unpersisted bundle state (<i>not the 
1290     * actual bundle file</i>) to make it fit into the expected idiom. Mostly
1291     * this just entails wrapping things in component groups and holders while
1292     * &quot;flattening&quot; any nested component groups.</p>
1293     * 
1294     * @param ht Holds unpersisted objects.
1295     * 
1296     * @param fromCollab Did the bundle come from the collab stuff?
1297     * 
1298     * @param loadDialog Show the bundle loading dialog?
1299     * 
1300     * @param shouldMerge Merge bundle contents into an existing window?
1301     * 
1302     * @param bundleProperties If non-null, use the set of time indices for 
1303     *                         data sources?
1304     * 
1305     * @param didRemoveAll Remove all data and displays?
1306     * 
1307     * @param letUserChangeData Allow changes to the data path?
1308     * 
1309     * @param limitNewWindows Only create one new window?
1310     * 
1311     * @see IdvPersistenceManager#instantiateFromBundle(Hashtable, boolean, LoadBundleDialog, boolean, Hashtable, boolean, boolean)
1312     */
1313    // TODO: check the accuracy of the bundleProperties javadoc above
1314    protected void instantiateFromBundle(Hashtable ht, 
1315                                         boolean fromCollab,
1316                                         LoadBundleDialog loadDialog,
1317                                         boolean shouldMerge,
1318                                         Hashtable bundleProperties,
1319                                         boolean didRemoveAll,
1320                                         boolean letUserChangeData,
1321                                         boolean limitNewWindows) 
1322            throws Exception {
1323
1324        // hacky way of allowing other classes to determine whether or not
1325        // a bundle is loading
1326        bundleLoading = true;
1327
1328        // every bundle should have lists corresponding to these ids
1329        final String[] important = { 
1330            ID_VIEWMANAGERS, ID_DISPLAYCONTROLS, ID_WINDOWS,
1331        };
1332        populateEssentialLists(important, ht);
1333
1334        List<ViewManager> vms = (List)ht.get(ID_VIEWMANAGERS);
1335        List<DisplayControlImpl> controls = (List)ht.get(ID_DISPLAYCONTROLS);
1336        List<WindowInfo> windows = (List)ht.get(ID_WINDOWS);
1337
1338        List<DataSourceImpl> dataSources = (List)ht.get("datasources");
1339        if (dataSources != null) {
1340            fixBulkDataSources(dataSources);
1341        }
1342
1343        // older hydra bundles may contain ReadoutProbes in the list of
1344        // display controls. these are not needed, so they get removed.
1345//        controls = removeReadoutProbes(controls);
1346        ht.put(ID_DISPLAYCONTROLS, controls);
1347
1348        if (vms.isEmpty() && windows.isEmpty() && !controls.isEmpty()) {
1349            List<ViewManager> fudged = generateViewManagers(controls);
1350            List<WindowInfo> buh = wrapViewManagers(fudged);
1351
1352            windows.addAll(buh);
1353            vms.addAll(fudged);
1354        }
1355
1356        // make sure that the list of windows contains no nested comp groups
1357        flattenWindows(windows);
1358
1359        // remove any component holders that don't contain displays
1360        windows = removeUIHolders(windows);
1361
1362        // generate new IDs for any collisions--typically happens if the same
1363        // bundle is loaded without removing the previously loaded VMs.
1364        reverseCollisions(vms);
1365
1366        // if the incoming bundle has dynamic skins, we've gotta be sure to
1367        // remove their ViewManagers from the bundle's list of ViewManagers!
1368        // remember, because they are dynamic skins, the ViewManagers should
1369        // not exist until the skin is built.
1370        if (McVGuiUtils.hasDynamicSkins(windows)) {
1371            mapDynamicSkins(windows);
1372        }
1373
1374        List<WindowInfo> newWindows;
1375        if (limitNewWindows && windows.size() > 1) {
1376            newWindows = injectComponentGroups(windows);
1377        } else {
1378            newWindows = betterInject(windows);
1379        }
1380
1381//          if (limitNewWindows && windows.size() > 1) {
1382//              // make a single new window with a single component group. 
1383//              // the group's holders will correspond to each window in the 
1384//              // bundle.
1385//              List<WindowInfo> newWindows = injectComponentGroups(windows);
1386//              ht.put(ID_WINDOWS, newWindows);
1387//
1388//              // if there are any component groups in the bundle, we must 
1389//              // take care that their VMs appear in this list. VMs wrapped 
1390//              // in dynamic skins don't "exist" at this point, so they do 
1391//              // not need to be in this list.
1392//              ht.put(ID_VIEWMANAGERS, extractCompGroupVMs(newWindows));
1393//          }
1394
1395        ht.put(ID_WINDOWS, newWindows);
1396
1397        ht.put(ID_VIEWMANAGERS, extractCompGroupVMs(newWindows));
1398
1399        // hand our modified bundle information off to the IDV
1400        super.instantiateFromBundle(ht, fromCollab, loadDialog, shouldMerge, 
1401                                    bundleProperties, didRemoveAll, 
1402                                    letUserChangeData);
1403
1404        // no longer needed; the bundle is done loading.
1405        UIManager.savedViewManagers.clear();
1406        bundleLoading = false;
1407    }
1408
1409//    private List<DisplayControlImpl> removeReadoutProbes(final List<DisplayControlImpl> controls) {
1410//        List<DisplayControlImpl> filtered = new ArrayList<DisplayControlImpl>();
1411//        for (DisplayControlImpl dc : controls) {
1412//            if (dc instanceof ReadoutProbe) {
1413//                try {
1414//                    dc.doRemove();
1415//                } catch (Exception e) {
1416//                    LogUtil.logException("Problem removing redundant readout probe", e);
1417//                }
1418//            } else if (dc != null) {
1419//                filtered.add(dc);
1420//            }
1421//        }
1422//        return filtered;
1423//    }
1424
1425    private List<WindowInfo> wrapViewManagers(final List<ViewManager> vms) {
1426        List<WindowInfo> windows = new ArrayList<WindowInfo>();
1427        for (ViewManager vm : vms) {
1428            WindowInfo window = new WindowInfo();
1429            window.setIsAMainWindow(true);
1430            window.setSkinPath("/ucar/unidata/idv/resources/skins/skin.xml");
1431            window.setTitle("asdf");
1432            List<ViewManager> vmList = new ArrayList<ViewManager>();
1433            vmList.add(vm);
1434            window.setViewManagers(vmList);
1435            window.setBounds(new Rectangle(200, 200, 200, 200));
1436            windows.add(window);
1437        }
1438        return windows;
1439    }
1440
1441    private List<ViewManager> generateViewManagers(final List<DisplayControlImpl> controls) {
1442        List<ViewManager> vms = new ArrayList<ViewManager>();
1443        for (DisplayControlImpl control : controls) {
1444            ViewManager vm = getVMManager().findOrCreateViewManager(control.getDefaultViewDescriptor(), "");
1445            vms.add(vm);
1446        }
1447        return vms;
1448    }
1449
1450    /**
1451     * <p>Alters <code>windows</code> so that no windows in the bundle contain
1452     * nested component groups.</p>
1453     */
1454    protected void flattenWindows(final List<WindowInfo> windows) {
1455        for (WindowInfo window : windows) {
1456            Map<String, Object> persist = window.getPersistentComponents();
1457            Set<Map.Entry<String, Object>> blah = persist.entrySet();
1458            for (Map.Entry<String, Object> entry : blah) {
1459                if (!(entry.getValue() instanceof IdvComponentGroup)) {
1460                    continue;
1461                }
1462
1463                IdvComponentGroup group = (IdvComponentGroup)entry.getValue();
1464                if (McVGuiUtils.hasNestedGroups(group)) {
1465                    entry.setValue(flattenGroup(group));
1466                }
1467            }
1468        }
1469    }
1470
1471    /**
1472     * @return An altered version of <code>nested</code> that contains no 
1473     *         nested component groups.
1474     */
1475    protected IdvComponentGroup flattenGroup(final IdvComponentGroup nested) {
1476        IdvComponentGroup flat = 
1477            new IdvComponentGroup(getIdv(), nested.getName());
1478
1479        flat.setLayout(nested.getLayout());
1480        flat.setShowHeader(nested.getShowHeader());
1481        flat.setUniqueId(nested.getUniqueId());
1482
1483        List<IdvComponentHolder> holders = 
1484            McVGuiUtils.getComponentHolders(nested);
1485
1486        for (IdvComponentHolder holder : holders) {
1487            flat.addComponent(holder);
1488            holder.setParent(flat);
1489        }
1490
1491        return flat;
1492    }
1493
1494    /**
1495     * @return An altered <code>group</code> containing only component holders
1496     *         with displays.
1497     */
1498    protected static List<IdvComponentHolder> removeUIHolders(final IdvComponentGroup group) {
1499        List<IdvComponentHolder> newHolders = 
1500            new ArrayList<IdvComponentHolder>(group.getDisplayComponents());
1501
1502        for (IdvComponentHolder holder : newHolders) {
1503            if (McVGuiUtils.isUIHolder(holder)) {
1504                newHolders.remove(holder);
1505            }
1506        }
1507
1508        return newHolders;
1509    }
1510
1511    /**
1512     * <p>Ensures that the lists corresponding to the ids in <code>ids</code>
1513     * actually exist in <code>table</code>, even if they are empty.</p>
1514     */
1515    // TODO: not a fan of this method.
1516    protected static void populateEssentialLists(final String[] ids, final Hashtable<String, Object> table) {
1517        for (String id : ids) {
1518            if (table.get(id) == null) {
1519                table.put(id, new ArrayList<Object>());
1520            }
1521        }
1522    }
1523
1524    /**
1525     * <p>Returns an altered copy of <code>windows</code> containing only 
1526     * component holders that have displays.</p>
1527     * 
1528     * <p>The IDV allows users to embed HTML controls or things like the 
1529     * dashboard into component holders. This ability, while powerful, could
1530     * make for a confusing UI.</p>
1531     */
1532    protected static List<WindowInfo> removeUIHolders(
1533        final List<WindowInfo> windows) {
1534
1535        List<WindowInfo> newList = new ArrayList<WindowInfo>();
1536        for (WindowInfo window : windows) {
1537            // TODO: ought to write a WindowInfo cloning method
1538            WindowInfo newWin = new WindowInfo();
1539            newWin.setViewManagers(window.getViewManagers());
1540            newWin.setSkinPath(window.getSkinPath());
1541            newWin.setIsAMainWindow(window.getIsAMainWindow());
1542            newWin.setBounds(window.getBounds());
1543            newWin.setTitle(window.getTitle());
1544
1545            Hashtable<String, IdvComponentGroup> persist = 
1546                new Hashtable<String, IdvComponentGroup>(
1547                    window.getPersistentComponents()); 
1548
1549            for (Map.Entry<String, IdvComponentGroup> e : persist.entrySet()) {
1550
1551                IdvComponentGroup g = e.getValue();
1552
1553                List<IdvComponentHolder> holders = g.getDisplayComponents();
1554                if (holders == null || holders.isEmpty()) {
1555                    continue;
1556                }
1557
1558                List<IdvComponentHolder> newHolders = 
1559                    new ArrayList<IdvComponentHolder>();
1560
1561                // filter out any holders that don't contain view managers
1562                for (IdvComponentHolder holder : holders) {
1563                    if (!McVGuiUtils.isUIHolder(holder)) {
1564                        newHolders.add(holder);
1565                    }
1566                }
1567
1568                g.setDisplayComponents(newHolders);
1569            }
1570
1571            newWin.setPersistentComponents(persist);
1572            newList.add(newWin);
1573        }
1574        return newList;
1575    }
1576
1577    /**
1578     * <p>Uses the {@link ucar.unidata.idv.ViewManager}s in <code>info</code> 
1579     * to build a dynamic skin.</p>
1580     * 
1581     * @param info Window that needs to become a dynamic skin.
1582     * 
1583     * @return A {@link edu.wisc.ssec.mcidasv.ui.McvComponentHolder} containing 
1584     *         the ViewManagers inside <code>info</code>.
1585     * 
1586     * @throws Exception Bubble up any XML problems.
1587     */
1588    public McvComponentHolder makeDynSkin(final WindowInfo info) throws Exception {
1589        Document doc = XmlUtil.getDocument(SIMPLE_SKIN_TEMPLATE);
1590        Element root = doc.getDocumentElement();
1591
1592        Element panel = XmlUtil.findElement(root, DYNSKIN_TAG_PANEL,
1593                                            DYNSKIN_ATTR_ID, DYNSKIN_ID_VALUE);
1594
1595        List<ViewManager> vms = info.getViewManagers();
1596
1597        panel.setAttribute(DYNSKIN_ATTR_COLS, Integer.toString(vms.size()));
1598
1599        for (ViewManager vm : vms) {
1600
1601            Element view = doc.createElement(DYNSKIN_TAG_VIEW);
1602
1603            view.setAttribute(DYNSKIN_ATTR_CLASS, vm.getClass().getName());
1604            view.setAttribute(DYNSKIN_ATTR_VIEWID, vm.getUniqueId());
1605
1606            StringBuffer props = new StringBuffer(DYNSKIN_PROPS_GENERAL);
1607
1608            if (vm instanceof MapViewManager) {
1609                if (((MapViewManager)vm).getUseGlobeDisplay()) {
1610                    props.append(DYNSKIN_PROPS_GLOBE);
1611                }
1612            }
1613
1614            view.setAttribute(DYNSKIN_ATTR_PROPS, props.toString());
1615
1616            panel.appendChild(view);
1617
1618            UIManager.savedViewManagers.put(vm.getViewDescriptor().getName(), vm);
1619        }
1620
1621        McvComponentHolder holder = 
1622            new McvComponentHolder(getIdv(), XmlUtil.toString(root));
1623
1624        holder.setType(McvComponentHolder.TYPE_DYNAMIC_SKIN);
1625        holder.setName(DYNSKIN_TMPNAME);
1626        holder.doMakeContents();
1627        return holder;
1628    }
1629    
1630    private static final String DYNSKIN_TMPNAME = "Dynamic Skin Test";
1631    private static final String DYNSKIN_TAG_PANEL = "panel";
1632    private static final String DYNSKIN_TAG_VIEW = "idv.view";
1633    private static final String DYNSKIN_ATTR_ID = "id";
1634    private static final String DYNSKIN_ATTR_COLS = "cols";
1635    private static final String DYNSKIN_ATTR_PROPS = "properties";
1636    private static final String DYNSKIN_ATTR_CLASS = "class";
1637    private static final String DYNSKIN_ATTR_VIEWID = "viewid";
1638    private static final String DYNSKIN_PROPS_GLOBE = "useGlobeDisplay=true;initialMapResources=/edu/wisc/ssec/mcidasv/resources/maps.xml;";
1639    private static final String DYNSKIN_PROPS_GENERAL = "clickToFocus=true;showToolBars=true;shareViews=true;showControlLegend=true;initialSplitPaneLocation=0.2;legendOnLeft=false;size=300:400;shareGroup=view%versionuid%;";
1640    private static final String DYNSKIN_ID_VALUE = "mcv.content";
1641
1642    /** XML template for generating dynamic skins. */
1643    private static final String SIMPLE_SKIN_TEMPLATE = 
1644        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1645        "<skin embedded=\"true\">\n" +
1646        "  <ui>\n" +
1647        "    <panel layout=\"border\" bgcolor=\"red\">\n" +
1648        "      <idv.menubar place=\"North\"/>\n" +
1649        "      <panel layout=\"border\" place=\"Center\">\n" +
1650        "        <panel layout=\"flow\" place=\"North\">\n" +
1651        "          <idv.toolbar id=\"idv.toolbar\" place=\"West\"/>\n" +
1652        "          <panel id=\"idv.favoritesbar\" place=\"North\"/>\n" +
1653        "        </panel>\n" +
1654        "        <panel embeddednode=\"true\" id=\"mcv.content\" layout=\"grid\" place=\"Center\">\n" +
1655        "        </panel>" +
1656        "      </panel>\n" +
1657        "      <component idref=\"bottom_bar\"/>\n" +
1658        "    </panel>\n" +
1659        "  </ui>\n" +
1660        "  <styles>\n" +
1661        "    <style class=\"iconbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip);ui.setBorder(this,etched);\" mouse_exit=\"ui.setText(idv.messagelabel,);ui.setBorder(this,button);\"/>\n" +
1662        "    <style class=\"textbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip)\" mouse_exit=\"ui.setText(idv.messagelabel,)\"/>\n" +
1663        "  </styles>\n" +
1664        "  <components>\n" +
1665        "    <idv.statusbar place=\"South\" id=\"bottom_bar\"/>\n" +
1666        "  </components>\n" +
1667        "  <properties>\n" +
1668        "    <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" +
1669        "  </properties>\n" +
1670        "</skin>\n";
1671    
1672    
1673    
1674    /**
1675     * Write the parameter sets
1676     */
1677    public void writeParameterSets() {
1678        if (parameterSets != null) {
1679                
1680                //DAVEP: why is our write failing?
1681                if (!parameterSets.hasWritableResource()) {
1682                        System.err.println("Oops--lost writable resource");
1683                }
1684                
1685                try {
1686                        parameterSets.writeWritable();
1687                } catch (IOException exc) {
1688                LogUtil.logException("Error writing " + parameterSets.getDescription(), exc);
1689                }
1690                
1691                parameterSets.setWritableDocument(parameterSetsDocument, parameterSetsRoot);
1692        }
1693    }
1694    
1695    /**
1696     * Get the node representing the parameterType
1697     * 
1698     * @param parameterType What type of parameter set
1699     *
1700     * @return Element representing parameterType node
1701     */
1702    private Element getParameterTypeNode(String parameterType) {
1703        if (parameterSets == null) {
1704                parameterSets = getIdv().getResourceManager().getXmlResources(ResourceManager.RSC_PARAMETERSETS);
1705            if (parameterSets.hasWritableResource()) {
1706                parameterSetsDocument = parameterSets.getWritableDocument("<parametersets></parametersets>");
1707                parameterSetsRoot = parameterSets.getWritableRoot("<parametersets></parametersets>");
1708            }
1709            else {
1710                System.err.println("No writable resource found");
1711                return null;
1712            }
1713        }
1714
1715        Element parameterTypeNode = null;
1716        try {
1717                List<Element> rootTypes = XmlUtil.findChildren(parameterSetsRoot, parameterType);
1718                if (rootTypes.size() == 0) {
1719                        parameterTypeNode = parameterSetsDocument.createElement(parameterType);
1720                        parameterSetsRoot.appendChild(parameterTypeNode);
1721                        System.out.println("Created new " + parameterType + " node");
1722                        writeParameterSets();
1723                }
1724                else if (rootTypes.size() == 1) {
1725                        parameterTypeNode = rootTypes.get(0);
1726                        System.out.println("Found existing " + parameterType + " node");
1727                }
1728        } catch (Exception exc) {
1729                LogUtil.logException("Error loading " + parameterSets.getDescription(), exc);
1730        }
1731        return parameterTypeNode;
1732    }
1733
1734    /**
1735     * Get a list of all of the categories for the given parameterType
1736     *
1737     * @param parameterType What type of parameter set
1738     *
1739     * @return List of (String) categories
1740     */
1741    public List<String> getAllParameterSetCategories(String parameterType) {
1742        List<String> allCategories = new ArrayList<String>();
1743        try {
1744                Element rootType = getParameterTypeNode(parameterType);
1745                if (rootType!=null) {
1746                        allCategories =
1747                                XmlUtil.findDescendantNamesWithSeparator(rootType, TAG_FOLDER, CATEGORY_SEPARATOR);
1748                }
1749        } catch (Exception exc) {
1750                LogUtil.logException("Error loading " + parameterSets.getDescription(), exc);
1751        }
1752
1753        return allCategories;
1754    }
1755    
1756
1757    /**
1758     * Get the list of {@link ParameterSet}s that are writable
1759     *
1760     * @param parameterType The type of parameter set
1761     *
1762     * @return List of writable parameter sets
1763     */
1764    public List<ParameterSet> getAllParameterSets(String parameterType) {
1765        List<ParameterSet> allParameterSets = new ArrayList<ParameterSet>();
1766        try {
1767                Element rootType = getParameterTypeNode(parameterType);
1768                if (rootType!=null) {
1769                List<String> defaults =
1770                        XmlUtil.findDescendantNamesWithSeparator(rootType, TAG_DEFAULT, CATEGORY_SEPARATOR);
1771                        
1772                        for (final String aDefault : defaults) {
1773                                Element anElement = XmlUtil.getElementAtNamedPath(rootType, stringToCategories(aDefault));
1774                                List<String> defaultParts = stringToCategories(aDefault);
1775                                int lastIndex = defaultParts.size() - 1;
1776                                String defaultName = defaultParts.get(lastIndex);
1777                                defaultParts.remove(lastIndex);
1778                                String folderName = StringUtil.join(CATEGORY_SEPARATOR, defaultParts);
1779                                ParameterSet newSet = new ParameterSet(defaultName, folderName, parameterType, anElement);
1780                                allParameterSets.add(newSet);
1781                        }
1782
1783                }
1784        } catch (Exception exc) {
1785                LogUtil.logException("Error loading " + ResourceManager.RSC_PARAMETERSETS.getDescription(), exc);
1786        }
1787        
1788        return allParameterSets;
1789    }
1790
1791
1792    /**
1793     * Add the directory
1794     *
1795     * @param parameterType The type of parameter set
1796     * @param category The category (really a ">" delimited string)
1797     * @return true if the create was successful. False if there already is a category with that name
1798     */
1799    public boolean addParameterSetCategory(String parameterType, String category) {
1800        System.out.println("addParameterSetCategory: " + category);
1801                Element rootType = getParameterTypeNode(parameterType);
1802        XmlUtil.makeElementAtNamedPath(rootType, stringToCategories(category), TAG_FOLDER);
1803        writeParameterSets();
1804        return true;
1805    }
1806    
1807    /**
1808     * Delete the given parameter set
1809     *
1810     * @param parameterType The type of parameter set
1811     * @param name The name of the parameter set
1812     */
1813    public void deleteParameterSet(String parameterType, ParameterSet set) {
1814        Element parameterElement = set.getElement();
1815        Node parentNode = parameterElement.getParentNode();
1816        parentNode.removeChild((Node)parameterElement);
1817        writeParameterSets();
1818    }
1819
1820
1821    /**
1822     * Delete the directory and all of its contents
1823     * that the given category represents.
1824     *
1825     * @param parameterType The type of parameter set
1826     * @param category The category (really a ">" delimited string)
1827     */
1828    public void deleteParameterSetCategory(String parameterType, String category) {
1829                Element rootType = getParameterTypeNode(parameterType);
1830        Element parameterSetElement = XmlUtil.getElementAtNamedPath(rootType, stringToCategories(category));
1831        Node parentNode = parameterSetElement.getParentNode();
1832        parentNode.removeChild((Node)parameterSetElement);
1833        writeParameterSets();
1834    }
1835
1836
1837    /**
1838     * Rename the parameter set
1839     *
1840     * @param parameterType The type of parameter set
1841     * @param set The parameter set
1842     */
1843    public void renameParameterSet(String parameterType, ParameterSet set) {
1844        String name = set.getName();
1845        Element parameterElement = set.getElement();
1846//        while (true) {
1847            name = GuiUtils.getInput("Enter a new name", "Name: ", name);
1848            if (name == null) {
1849                return;
1850            }
1851                        name = StringUtil.replaceList(name.trim(),
1852                                        new String[] { "<", ">", "/", "\\", "\"" },
1853                                        new String[] { "_", "_", "_", "_",  "_"  }
1854                        );
1855            if (name.length() == 0) {
1856                return;
1857            }
1858//        }
1859        parameterElement.setAttribute("name", name);
1860        writeParameterSets();
1861    }
1862    
1863    /**
1864     * Move the bundle to the given category area
1865     *
1866     * @param parameterType The type of parameter set
1867     * @param set The parameter set
1868     * @param categories Where to move to
1869     */
1870    public void moveParameterSet(String parameterType, ParameterSet set, List categories) {
1871                Element rootType = getParameterTypeNode(parameterType);
1872        Element parameterElement = set.getElement();
1873        Node parentNode = parameterElement.getParentNode();
1874        parentNode.removeChild((Node)parameterElement);
1875        Node newParentNode = XmlUtil.getElementAtNamedPath(rootType, categories);
1876        newParentNode.appendChild(parameterElement);
1877        writeParameterSets();
1878    }
1879
1880    /**
1881     * Move the bundle category
1882     *
1883     * @param parameterType The type of parameter set
1884     * @param fromCategories The category to move
1885     * @param toCategories Where to move to
1886     */
1887    public void moveParameterSetCategory(String parameterType, List fromCategories, List toCategories) {
1888                Element rootType = getParameterTypeNode(parameterType);
1889        Element parameterSetElementFrom = XmlUtil.getElementAtNamedPath(rootType, fromCategories);
1890        Node parentNode = parameterSetElementFrom.getParentNode();
1891        parentNode.removeChild((Node)parameterSetElementFrom);
1892        Node parentNodeTo = (Node)XmlUtil.getElementAtNamedPath(rootType, toCategories);
1893        parentNodeTo.appendChild(parameterSetElementFrom);
1894        writeParameterSets();
1895    }
1896
1897    /**
1898     * Show the Save Parameter Set dialog
1899     */
1900    public boolean saveParameterSet(String parameterType, Hashtable parameterValues) {
1901
1902        try {
1903                String title = "Save Parameter Set";
1904
1905                // Create the category dropdown
1906                List<String> categories = getAllParameterSetCategories(parameterType);
1907                final JComboBox catBox = new JComboBox();
1908                catBox.setToolTipText(
1909                                "<html>Categories can be entered manually. <br>Use '>' as the category delimiter. e.g.:<br>General > Subcategory</html>");
1910                catBox.setEditable(true);
1911                McVGuiUtils.setComponentWidth(catBox, McVGuiUtils.ELEMENT_DOUBLE_WIDTH);
1912                GuiUtils.setListData(catBox, categories);
1913
1914                // Create the default name dropdown
1915                final JComboBox nameBox = new JComboBox();
1916                nameBox.setEditable(true);
1917                List tails = new ArrayList();
1918
1919                List<ParameterSet> pSets = getAllParameterSets(parameterType);
1920                for (int i = 0; i < pSets.size(); i++) {
1921                        ParameterSet pSet = pSets.get(i);
1922                        tails.add(new TwoFacedObject(pSet.getName(), pSet));
1923                }
1924                java.util.Collections.sort(tails);
1925
1926                tails.add(0, new TwoFacedObject("", null));
1927                GuiUtils.setListData(nameBox, tails);
1928                nameBox.addActionListener(new ActionListener() {
1929                        public void actionPerformed(ActionEvent ae) {
1930                                Object selected = nameBox.getSelectedItem();
1931                                if ( !(selected instanceof TwoFacedObject)) {
1932                                        return;
1933                                }
1934                                TwoFacedObject tfo = (TwoFacedObject) selected;
1935                                List cats = ((ParameterSet) tfo.getId()).getCategories();
1936                                //                      if ((cats.size() > 0) && !catSelected) {
1937                                if ((cats.size() > 0)) {
1938                                        catBox.setSelectedItem(
1939                                                        StringUtil.join(CATEGORY_SEPARATOR, cats));
1940                                }
1941                        }
1942                });
1943
1944                JPanel panel = McVGuiUtils.sideBySide(
1945                                McVGuiUtils.makeLabeledComponent("Category:", catBox),
1946                                McVGuiUtils.makeLabeledComponent("Name:", nameBox)
1947                );
1948
1949                String name = "";
1950                String category = "";
1951                while (true) {
1952                        if ( !GuiUtils.askOkCancel(title, panel)) {
1953                                return false;
1954                        }
1955                        name = StringUtil.replaceList(nameBox.getSelectedItem().toString().trim(),
1956                                        new String[] { "<", ">", "/", "\\", "\"" },
1957                                        new String[] { "_", "_", "_", "_",  "_"  }
1958                        );
1959                        if (name.length() == 0) {
1960                                LogUtil.userMessage("Please enter a name");
1961                                continue;
1962                        }
1963                        category = StringUtil.replaceList(catBox.getSelectedItem().toString().trim(),
1964                                        new String[] { "/", "\\", "\"" },
1965                                        new String[] { "_", "_",  "_"  }
1966                        );
1967                        if (category.length() == 0) {
1968                                LogUtil.userMessage("Please enter a category");
1969                                continue;
1970                        }
1971                        break;
1972                }
1973
1974                // Create a new element from the hashtable
1975                Element rootType = getParameterTypeNode(parameterType);
1976                Element parameterElement = parameterSetsDocument.createElement(TAG_DEFAULT);
1977            for (Enumeration e = parameterValues.keys(); e.hasMoreElements(); ) {
1978                Object nextKey = e.nextElement();
1979                String attribute = (String)nextKey;
1980                String value = (String)parameterValues.get(nextKey);
1981                parameterElement.setAttribute(attribute, value);
1982            }
1983
1984                // Set the name to the one we entered
1985                parameterElement.setAttribute(ATTR_NAME, name);
1986
1987                Element categoryNode = XmlUtil.makeElementAtNamedPath(rootType, stringToCategories(category), TAG_FOLDER);
1988//              Element categoryNode = XmlUtil.getElementAtNamedPath(rootType, stringToCategories(category));
1989
1990                categoryNode.appendChild(parameterElement);     
1991                writeParameterSets();
1992        }
1993        catch (Exception e) {
1994                e.printStackTrace();
1995                return false;
1996        }
1997        
1998        return true;
1999    }
2000
2001}