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