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