001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
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 {@link ucar.unidata.data.FilesDataSource#initWithPollingInfo}
1289     * to replace the contents of the data source's file paths. Simply 
1290     * overwrite {@code PollingInfo#filePaths} with the path to the blob.
1291     * 
1292     * @param ds {@code List} of {@link DataSourceImpl}s to inspect and/or fix.
1293     * Cannot be {@code null}.
1294     * 
1295     * @see #isBulkDataSource(DataSourceImpl)
1296     */
1297    private void fixBulkDataSources(final List<DataSourceImpl> ds) {
1298        String zidvPath = getStateManager().getProperty(PROP_ZIDVPATH, "");
1299
1300        // bail out if the macro replacement cannot work
1301        if (zidvPath.isEmpty()) {
1302            return;
1303        }
1304
1305        for (DataSourceImpl d : ds) {
1306            boolean isBulk = isBulkDataSource(d);
1307            if (!isBulk) {
1308                continue;
1309            }
1310
1311            // err... now do the macro sub and replace the contents of 
1312            // data paths with the singular element in temp paths?
1313            List<String> tempPaths = new ArrayList<>(d.getTmpPaths());
1314            String tempPath = tempPaths.get(0);
1315            tempPath = tempPath.replace(MACRO_ZIDVPATH, zidvPath);
1316            tempPaths.set(0, tempPath);
1317            PollingInfo p = d.getPollingInfo();
1318            p.setFilePaths(tempPaths);
1319        }
1320    }
1321
1322    /**
1323     * Attempts to determine whether or not a given {@link DataSourceImpl} is
1324     * the result of a McIDAS-V {@literal "bulk load"}.
1325     * 
1326     * @param d {@code DataSourceImpl} to check. Cannot be {@code null}.
1327     * 
1328     * @return {@code true} if the {@code DataSourceImpl} matched the criteria.
1329     */
1330    private boolean isBulkDataSource(final DataSourceImpl d) {
1331        Hashtable properties = d.getProperties();
1332        if (properties.containsKey("bulk.load")) {
1333            // woohoo! no need to do the guesswork.
1334            Object value = properties.get("bulk.load");
1335            if (value instanceof String) {
1336                return Boolean.valueOf((String)value);
1337            } else if (value instanceof Boolean) {
1338                return (Boolean)value;
1339            }
1340        }
1341
1342        DataSourceDescriptor desc = d.getDescriptor();
1343        boolean localFiles = desc.getFileSelection();
1344
1345        List filePaths = d.getDataPaths();
1346        List tempPaths = d.getTmpPaths();
1347        if ((filePaths == null) || filePaths.isEmpty()) {
1348            return false;
1349        }
1350
1351        if ((tempPaths == null) || tempPaths.isEmpty()) {
1352            return false;
1353        }
1354
1355        // the least-involved heuristic i've found is:
1356        // localFiles == true
1357        // tempPaths.size() == 1 && filePaths.size() >= 2
1358        // and then we have a bulk load...
1359        // if those checks don't suffice, you can also look for the "prop.pollinfo" key
1360        // if the PollingInfo object has a filePaths list, with one element whose last directory matches 
1361        // the data source "name" (then you are probably good).
1362        if (localFiles && (tempPaths.size() == 1) && (filePaths.size() >= 2)) {
1363            return true;
1364        }
1365
1366        // end of line
1367        return false;
1368    }
1369
1370    /**
1371     * Overridden so that McIDAS-V can preempt the IDV's bundle loading.
1372     * There will be problems if any of the incoming
1373     * {@link ViewManager ViewManagers} share an ID with an existing
1374     * ViewManager. While this case may seem unlikely, it can be triggered 
1375     * when loading a bundle and then reloading. The problem is that the 
1376     * ViewManagers are the same, and if the previous ViewManagers were not 
1377     * removed, the IDV doesn't know what to do.
1378     * 
1379     * <p>Assigning the incoming ViewManagers a new ID, <i>and associating its
1380     * {@link ViewDescriptor ViewDescriptors} and
1381     * {@link DisplayControl DisplayControls}</i> with the new ID fixes this
1382     * problem.</p>
1383     * 
1384     * <p>McIDAS-V also allows the user to limit the number of new windows the
1385     * bundle may create. If enabled, one new window will be created, and any
1386     * additional windows will become tabs (component holders) inside the new
1387     * window.</p>
1388     * 
1389     * <p>McIDAS-V also prefers the bundles being loaded to be in a 
1390     * semi-regular regular state. For example, say you have bundle containing
1391     * only data. The bundle will probably not contain lists of WindowInfos or
1392     * ViewManagers. Perhaps the bundle contains nested component groups as 
1393     * well! McIDAS-V will alter the unpersisted bundle state (<i>not the 
1394     * actual bundle file</i>) to make it fit into the expected idiom. Mostly
1395     * this just entails wrapping things in component groups and holders while
1396     * &quot;flattening&quot; any nested component groups.</p>
1397     * 
1398     * @param ht Holds unpersisted objects.
1399     * 
1400     * @param fromCollab Did the bundle come from the collab stuff?
1401     * 
1402     * @param loadDialog Show the bundle loading dialog?
1403     * 
1404     * @param shouldMerge Merge bundle contents into an existing window?
1405     * 
1406     * @param bundleProperties If non-null, use the set of time indices for 
1407     *                         data sources?
1408     * 
1409     * @param didRemoveAll Remove all data and displays?
1410     * 
1411     * @param letUserChangeData Allow changes to the data path?
1412     * 
1413     * @param limitNewWindows Only create one new window?
1414     *
1415     * @throws Exception if there was a problem re-instantiating the bundle.
1416     *
1417     * @see IdvPersistenceManager#instantiateFromBundle(Hashtable, boolean, LoadBundleDialog, boolean, Hashtable, boolean, boolean)
1418     */
1419    // TODO: check the accuracy of the bundleProperties javadoc above
1420    protected void instantiateFromBundle(Hashtable ht, 
1421                                         boolean fromCollab,
1422                                         LoadBundleDialog loadDialog,
1423                                         boolean shouldMerge,
1424                                         Hashtable bundleProperties,
1425                                         boolean didRemoveAll,
1426                                         boolean letUserChangeData,
1427                                         boolean limitNewWindows) 
1428            throws Exception {
1429
1430        // hacky way of allowing other classes to determine whether or not
1431        // a bundle is loading
1432        bundleLoading = true;
1433
1434        // every bundle should have lists corresponding to these ids
1435        final String[] important = { 
1436            ID_VIEWMANAGERS, ID_DISPLAYCONTROLS, ID_WINDOWS,
1437        };
1438        populateEssentialLists(important, ht);
1439
1440        List<ViewManager> vms = (List)ht.get(ID_VIEWMANAGERS);
1441        List<DisplayControlImpl> controls = (List)ht.get(ID_DISPLAYCONTROLS);
1442        List<WindowInfo> windows = (List)ht.get(ID_WINDOWS);
1443
1444        List<DataSourceImpl> dataSources = (List)ht.get("datasources");
1445        if (dataSources != null) {
1446            fixBulkDataSources(dataSources);
1447        }
1448
1449        // older hydra bundles may contain ReadoutProbes in the list of
1450        // display controls. these are not needed, so they get removed.
1451//        controls = removeReadoutProbes(controls);
1452        ht.put(ID_DISPLAYCONTROLS, controls);
1453
1454        if (vms.isEmpty() && windows.isEmpty() && !controls.isEmpty()) {
1455            List<ViewManager> fudged = generateViewManagers(controls);
1456            List<WindowInfo> buh = wrapViewManagers(fudged);
1457
1458            windows.addAll(buh);
1459            vms.addAll(fudged);
1460        }
1461
1462        // make sure that the list of windows contains no nested comp groups
1463        flattenWindows(windows);
1464
1465        // remove any component holders that don't contain displays
1466        windows = removeUIHolders(windows);
1467
1468        // generate new IDs for any collisions--typically happens if the same
1469        // bundle is loaded without removing the previously loaded VMs.
1470        reverseCollisions(vms);
1471
1472        // if the incoming bundle has dynamic skins, we've gotta be sure to
1473        // remove their ViewManagers from the bundle's list of ViewManagers!
1474        // remember, because they are dynamic skins, the ViewManagers should
1475        // not exist until the skin is built.
1476        if (McVGuiUtils.hasDynamicSkins(windows)) {
1477            mapDynamicSkins(windows);
1478        }
1479
1480        List<WindowInfo> newWindows;
1481        if (limitNewWindows && (windows.size() > 1)) {
1482            newWindows = injectComponentGroups(windows);
1483        } else {
1484            newWindows = betterInject(windows);
1485        }
1486
1487//          if (limitNewWindows && windows.size() > 1) {
1488//              // make a single new window with a single component group. 
1489//              // the group's holders will correspond to each window in the 
1490//              // bundle.
1491//              List<WindowInfo> newWindows = injectComponentGroups(windows);
1492//              ht.put(ID_WINDOWS, newWindows);
1493//
1494//              // if there are any component groups in the bundle, we must 
1495//              // take care that their VMs appear in this list. VMs wrapped 
1496//              // in dynamic skins don't "exist" at this point, so they do 
1497//              // not need to be in this list.
1498//              ht.put(ID_VIEWMANAGERS, extractCompGroupVMs(newWindows));
1499//          }
1500
1501        ht.put(ID_WINDOWS, newWindows);
1502
1503        ht.put(ID_VIEWMANAGERS, extractCompGroupVMs(newWindows));
1504
1505        // hand our modified bundle information off to the IDV
1506        super.instantiateFromBundle(ht, fromCollab, loadDialog, shouldMerge, 
1507                                    bundleProperties, didRemoveAll, 
1508                                    letUserChangeData);
1509
1510        // no longer needed; the bundle is done loading.
1511        UIManager.savedViewManagers.clear();
1512        bundleLoading = false;
1513    }
1514
1515//    private List<DisplayControlImpl> removeReadoutProbes(final List<DisplayControlImpl> controls) {
1516//        List<DisplayControlImpl> filtered = new ArrayList<DisplayControlImpl>();
1517//        for (DisplayControlImpl dc : controls) {
1518//            if (dc instanceof ReadoutProbe) {
1519//                try {
1520//                    dc.doRemove();
1521//                } catch (Exception e) {
1522//                    LogUtil.logException("Problem removing redundant readout probe", e);
1523//                }
1524//            } else if (dc != null) {
1525//                filtered.add(dc);
1526//            }
1527//        }
1528//        return filtered;
1529//    }
1530
1531    private List<WindowInfo> wrapViewManagers(final List<ViewManager> vms) {
1532        List<WindowInfo> windows = new ArrayList<>(vms.size());
1533        for (ViewManager vm : vms) {
1534            WindowInfo window = new WindowInfo();
1535            window.setIsAMainWindow(true);
1536            window.setSkinPath("/ucar/unidata/idv/resources/skins/skin.xml");
1537            window.setTitle("asdf");
1538            List<ViewManager> vmList = new ArrayList<ViewManager>();
1539            vmList.add(vm);
1540            window.setViewManagers(vmList);
1541            window.setBounds(new Rectangle(200, 200, 200, 200));
1542            windows.add(window);
1543        }
1544        return windows;
1545    }
1546
1547    private List<ViewManager> generateViewManagers(final List<DisplayControlImpl> controls) {
1548        List<ViewManager> vms = new ArrayList<>(controls.size());
1549        for (DisplayControlImpl control : controls) {
1550            ViewManager vm = getVMManager().findOrCreateViewManager(control.getDefaultViewDescriptor(), "");
1551            vms.add(vm);
1552        }
1553        return vms;
1554    }
1555
1556    /**
1557     * Alters {@code windows} so that no windows in the bundle contain
1558     * nested component groups.
1559     *
1560     * @param windows {@code List} of windows to {@literal "flatten"}.
1561     */
1562    protected void flattenWindows(final List<WindowInfo> windows) {
1563        for (WindowInfo window : windows) {
1564            Map<String, Object> persist = window.getPersistentComponents();
1565            Set<Map.Entry<String, Object>> blah = persist.entrySet();
1566            for (Map.Entry<String, Object> entry : blah) {
1567                if (!(entry.getValue() instanceof IdvComponentGroup)) {
1568                    continue;
1569                }
1570
1571                IdvComponentGroup group = (IdvComponentGroup)entry.getValue();
1572                if (McVGuiUtils.hasNestedGroups(group)) {
1573                    entry.setValue(flattenGroup(group));
1574                }
1575            }
1576        }
1577    }
1578
1579    /**
1580     * Alters {@code nested} so that there are no nested component groups.
1581     *
1582     * @param nested Component group to {@literal "flatten"}.
1583     *
1584     * @return An altered version of {@code nested} that contains no
1585     *         nested component groups.
1586     */
1587    protected IdvComponentGroup flattenGroup(final IdvComponentGroup nested) {
1588        IdvComponentGroup flat = 
1589            new IdvComponentGroup(getIdv(), nested.getName());
1590
1591        flat.setLayout(nested.getLayout());
1592        flat.setShowHeader(nested.getShowHeader());
1593        flat.setUniqueId(nested.getUniqueId());
1594
1595        List<IdvComponentHolder> holders = 
1596            McVGuiUtils.getComponentHolders(nested);
1597
1598        for (IdvComponentHolder holder : holders) {
1599            flat.addComponent(holder);
1600            holder.setParent(flat);
1601        }
1602
1603        return flat;
1604    }
1605
1606    /**
1607     * Remove component holders that are {@literal "UI-only"}.
1608     *
1609     * <p>{@literal "UI-only"} refers to things like having the dashboard
1610     * embedded in a component holder.</p>
1611     *
1612     * @param group Component group from which {@literal "UI-only"} holders will
1613     *              be removed.
1614     *
1615     * @return An altered {@code group} containing only component holders
1616     *         with displays.
1617     */
1618    protected static List<IdvComponentHolder> removeUIHolders(final IdvComponentGroup group) {
1619        List<IdvComponentHolder> newHolders = 
1620            new ArrayList<>(group.getDisplayComponents());
1621
1622        for (IdvComponentHolder holder : newHolders) {
1623            if (McVGuiUtils.isUIHolder(holder)) {
1624                newHolders.remove(holder);
1625            }
1626        }
1627
1628        return newHolders;
1629    }
1630
1631    /**
1632     * Ensures that the lists corresponding to the ids in {@code ids}
1633     * actually exist in {@code table}, even if they are empty.
1634     *
1635     * @param ids IDs that should have a corresponding {@code List}.
1636     * @param table Table that should be a mapping of {@code ids} to
1637     *              {@code Lists}.
1638     */
1639    // TODO: not a fan of this method.
1640    protected static void populateEssentialLists(final String[] ids, final Hashtable<String, Object> table) {
1641        for (String id : ids) {
1642            if (table.get(id) == null) {
1643                table.put(id, new ArrayList<>());
1644            }
1645        }
1646    }
1647
1648    /**
1649     * Returns an altered copy of {@code windows} containing only
1650     * component holders that have displays.
1651     * 
1652     * <p>The IDV allows users to embed HTML controls or things like the 
1653     * dashboard into component holders. This ability, while powerful, could
1654     * make for a confusing UI.</p>
1655     *
1656     * @param windows Windows from which {@literal "UI-only"} holders should be
1657     *                removed.
1658     *
1659     * @return {@code List} of windows that contain displays.
1660     */
1661    protected static List<WindowInfo> removeUIHolders(
1662        final List<WindowInfo> windows) {
1663
1664        List<WindowInfo> newList = new ArrayList<>();
1665        for (WindowInfo window : windows) {
1666            // TODO: ought to write a WindowInfo cloning method
1667            WindowInfo newWin = new WindowInfo();
1668            newWin.setViewManagers(window.getViewManagers());
1669            newWin.setSkinPath(window.getSkinPath());
1670            newWin.setIsAMainWindow(window.getIsAMainWindow());
1671            newWin.setBounds(window.getBounds());
1672            newWin.setTitle(window.getTitle());
1673
1674            Hashtable<String, IdvComponentGroup> persist = 
1675                new Hashtable<>(window.getPersistentComponents());
1676
1677            for (Map.Entry<String, IdvComponentGroup> e : persist.entrySet()) {
1678
1679                IdvComponentGroup g = e.getValue();
1680
1681                List<IdvComponentHolder> holders = g.getDisplayComponents();
1682                if (holders == null || holders.isEmpty()) {
1683                    continue;
1684                }
1685
1686                List<IdvComponentHolder> newHolders = new ArrayList<>();
1687
1688                // filter out any holders that don't contain view managers
1689                for (IdvComponentHolder holder : holders) {
1690                    if (!McVGuiUtils.isUIHolder(holder)) {
1691                        newHolders.add(holder);
1692                    }
1693                }
1694
1695                g.setDisplayComponents(newHolders);
1696            }
1697
1698            newWin.setPersistentComponents(persist);
1699            newList.add(newWin);
1700        }
1701        return newList;
1702    }
1703
1704    /**
1705     * Uses the {@link ViewManager ViewManagers} in {@code info}
1706     * to build a dynamic skin.
1707     * 
1708     * @param info Window that needs to become a dynamic skin.
1709     * 
1710     * @return {@link McvComponentHolder} containing the ViewManagers inside
1711     * {@code info}.
1712     * 
1713     * @throws Exception Bubble up any XML problems.
1714     */
1715    public McvComponentHolder makeDynSkin(final WindowInfo info) throws Exception {
1716        Document doc = XmlUtil.getDocument(SIMPLE_SKIN_TEMPLATE);
1717        Element root = doc.getDocumentElement();
1718
1719        Element panel = XmlUtil.findElement(root, DYNSKIN_TAG_PANEL,
1720                                            DYNSKIN_ATTR_ID, DYNSKIN_ID_VALUE);
1721
1722        List<ViewManager> vms = info.getViewManagers();
1723
1724        panel.setAttribute(DYNSKIN_ATTR_COLS, Integer.toString(vms.size()));
1725
1726        for (ViewManager vm : vms) {
1727
1728            Element view = doc.createElement(DYNSKIN_TAG_VIEW);
1729
1730            view.setAttribute(DYNSKIN_ATTR_CLASS, vm.getClass().getName());
1731            view.setAttribute(DYNSKIN_ATTR_VIEWID, vm.getUniqueId());
1732
1733            StringBuffer props = new StringBuffer(DYNSKIN_PROPS_GENERAL);
1734
1735            if (vm instanceof MapViewManager) {
1736                if (((MapViewManager)vm).getUseGlobeDisplay()) {
1737                    props.append(DYNSKIN_PROPS_GLOBE);
1738                }
1739            }
1740
1741            view.setAttribute(DYNSKIN_ATTR_PROPS, props.toString());
1742
1743            panel.appendChild(view);
1744
1745            UIManager.savedViewManagers.put(vm.getViewDescriptor().getName(), vm);
1746        }
1747
1748        McvComponentHolder holder = 
1749            new McvComponentHolder(getIdv(), XmlUtil.toString(root));
1750
1751        holder.setType(McvComponentHolder.TYPE_DYNAMIC_SKIN);
1752        holder.setName(DYNSKIN_TMPNAME);
1753        holder.doMakeContents();
1754        return holder;
1755    }
1756
1757    public static IdvWindow buildDynamicSkin(int width, int height, int rows, int cols, boolean showWidgets, List<PyObject> panelTypes) throws Exception {
1758        String skinTemplate;
1759        if (showWidgets) {
1760            skinTemplate = SIMPLE_SKIN_TEMPLATE;
1761        } else {
1762            skinTemplate = BUILDWINDOW_SKIN_TEMPLATE;
1763        }
1764        Document doc = XmlUtil.getDocument(skinTemplate);
1765        Element root = doc.getDocumentElement();
1766        Element panel = XmlUtil.findElement(root, DYNSKIN_TAG_PANEL, DYNSKIN_ATTR_ID, DYNSKIN_ID_VALUE);
1767        panel.setAttribute(DYNSKIN_ATTR_ROWS, Integer.toString(rows));
1768        panel.setAttribute(DYNSKIN_ATTR_COLS, Integer.toString(cols));
1769        Element view = doc.createElement(DYNSKIN_TAG_VIEW);
1770        for (PyObject panelType : panelTypes) {
1771            String panelTypeRepr = panelType.__repr__().toString();
1772            Element node = doc.createElement(IdvUIManager.COMP_VIEW);
1773            StringBuilder props;
1774            if (showWidgets) {
1775                props = new StringBuilder(DYNSKIN_PROPS_GENERAL);
1776            } else {
1777                props = new StringBuilder(BUILDWINDOW_PROPS_GENERAL);
1778            }
1779            props.append("size=").append(width).append(':').append(height).append(';');
1780            if ("MAP".equals(panelTypeRepr)) {
1781                node.setAttribute(IdvXmlUi.ATTR_CLASS, "ucar.unidata.idv.MapViewManager");
1782            } else if ("GLOBE".equals(panelTypeRepr)) {
1783                node.setAttribute(IdvXmlUi.ATTR_CLASS, "ucar.unidata.idv.MapViewManager");
1784                props.append(DYNSKIN_PROPS_GLOBE);
1785            } else if ("TRANSECT".equals(panelTypeRepr)) {
1786                node.setAttribute(IdvXmlUi.ATTR_CLASS, "ucar.unidata.idv.TransectViewManager");
1787            } else if ("MAP2D".equals(panelTypeRepr)) {
1788                node.setAttribute(IdvXmlUi.ATTR_CLASS, "ucar.unidata.idv.MapViewManager");
1789                props.append("use3D=false;");
1790            }
1791            view.setAttribute(DYNSKIN_ATTR_PROPS, props.toString());
1792            view.appendChild(node);
1793        }
1794        panel.appendChild(view);
1795        UIManager uiManager = (UIManager)McIDASV.getStaticMcv().getIdvUIManager();
1796        String skinPath;
1797        if (showWidgets) {
1798            skinPath = Constants.BLANK_COMP_GROUP;
1799        } else {
1800            skinPath = BUILDWINDOW_COMP_GROUP_HIDE_WIDGETS;
1801        }
1802        Element skinRoot = XmlUtil.getRoot(skinPath, PersistenceManager.class);
1803        IdvWindow window = uiManager.createNewWindow(null, false, "McIDAS-V", skinPath, skinRoot, false, null);
1804        ComponentGroup group = window.getComponentGroups().get(0);
1805        McvComponentHolder holder = new McvComponentHolder(McIDASV.getStaticMcv(), XmlUtil.toString(root));
1806        holder.setType(McvComponentHolder.TYPE_DYNAMIC_SKIN);
1807        group.addComponent(holder);
1808        return window;
1809    }
1810
1811    private static final String DYNSKIN_TMPNAME = "McIDAS-V buildWindow";
1812    private static final String DYNSKIN_TAG_PANEL = "panel";
1813    private static final String DYNSKIN_TAG_VIEW = "idv.view";
1814    private static final String DYNSKIN_ATTR_ID = "id";
1815    private static final String DYNSKIN_ATTR_COLS = "cols";
1816    private static final String DYNSKIN_ATTR_ROWS = "rows";
1817    private static final String DYNSKIN_ATTR_PROPS = "properties";
1818    private static final String DYNSKIN_ATTR_CLASS = "class";
1819    private static final String DYNSKIN_ATTR_VIEWID = "viewid";
1820    private static final String DYNSKIN_PROPS_GLOBE = "useGlobeDisplay=true;initialMapResources=/edu/wisc/ssec/mcidasv/resources/maps.xml;";
1821    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%;";
1822    private static final String DYNSKIN_ID_VALUE = "mcv.content";
1823
1824    /** XML template for generating dynamic skins. */
1825    private static final String SIMPLE_SKIN_TEMPLATE = 
1826        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1827        "<skin embedded=\"true\">\n" +
1828        "  <ui>\n" +
1829        "    <panel layout=\"border\" bgcolor=\"red\">\n" +
1830        "      <idv.menubar place=\"North\"/>\n" +
1831        "      <panel layout=\"border\" place=\"Center\">\n" +
1832        "        <panel layout=\"flow\" place=\"North\">\n" +
1833        "          <idv.toolbar id=\"idv.toolbar\" place=\"West\"/>\n" +
1834        "          <panel id=\"idv.favoritesbar\" place=\"North\"/>\n" +
1835        "        </panel>\n" +
1836        "        <panel embeddednode=\"true\" id=\"mcv.content\" layout=\"grid\" place=\"Center\">\n" +
1837        "        </panel>" +
1838        "      </panel>\n" +
1839        "      <component idref=\"bottom_bar\"/>\n" +
1840        "    </panel>\n" +
1841        "  </ui>\n" +
1842        "  <styles>\n" +
1843        "    <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" +
1844        "    <style class=\"textbtn\" space=\"2\" mouse_enter=\"ui.setText(idv.messagelabel,prop:tooltip)\" mouse_exit=\"ui.setText(idv.messagelabel,)\"/>\n" +
1845        "  </styles>\n" +
1846        "  <components>\n" +
1847        "    <idv.statusbar place=\"South\" id=\"bottom_bar\"/>\n" +
1848        "  </components>\n" +
1849        "  <properties>\n" +
1850        "    <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" +
1851        "  </properties>\n" +
1852        "</skin>\n";
1853
1854    private static final String BUILDWINDOW_COMP_GROUP_HIDE_WIDGETS =
1855        "/edu/wisc/ssec/mcidasv/resources/skins/window/buildwindow-hidewidgets.xml";
1856
1857    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%;";
1858
1859    /** Dynamic skin template for buildWindow. */
1860    private static final String BUILDWINDOW_SKIN_TEMPLATE =
1861        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1862        "<skin embedded=\"true\">\n" +
1863        "  <ui>\n" +
1864        "    <panel layout=\"border\" bgcolor=\"red\">\n" +
1865        "      <panel layout=\"border\" place=\"Center\">\n" +
1866        "        <panel embeddednode=\"true\" id=\"mcv.content\" layout=\"grid\" place=\"Center\">\n" +
1867        "        </panel>" +
1868        "      </panel>\n" +
1869        "    </panel>\n" +
1870        "  </ui>\n" +
1871        "  <properties>\n" +
1872        "    <property name=\"icon.wait.wait\" value=\"/ucar/unidata/idv/images/wait.gif\"/>\n" +
1873        "  </properties>\n" +
1874        "</skin>\n";
1875
1876    /**
1877     * Write the parameter sets
1878     */
1879    public void writeParameterSets() {
1880        if (parameterSets != null) {
1881
1882            //DAVEP: why is our write failing?
1883            if (!parameterSets.hasWritableResource()) {
1884                logger.trace("lost writable resource");
1885            }
1886
1887            try {
1888                parameterSets.writeWritable();
1889            } catch (IOException exc) {
1890                LogUtil.logException("Error writing " + parameterSets.getDescription(), exc);
1891            }
1892
1893            parameterSets.setWritableDocument(parameterSetsDocument, parameterSetsRoot);
1894        }
1895    }
1896    
1897    /**
1898     * Get the node representing the parameterType
1899     * 
1900     * @param parameterType What type of parameter set
1901     *
1902     * @return Element representing parameterType node
1903     */
1904    private Element getParameterTypeNode(String parameterType) {
1905        if (parameterSets == null) {
1906            parameterSets = getIdv().getResourceManager().getXmlResources(ResourceManager.RSC_PARAMETERSETS);
1907            if (parameterSets.hasWritableResource()) {
1908                parameterSetsDocument = parameterSets.getWritableDocument("<parametersets></parametersets>");
1909                parameterSetsRoot = parameterSets.getWritableRoot("<parametersets></parametersets>");
1910            } else {
1911                logger.trace("no writable resource found");
1912                return null;
1913            }
1914        }
1915
1916        Element parameterTypeNode = null;
1917        try {
1918            List<Element> rootTypes = XmlUtil.findChildren(parameterSetsRoot, parameterType);
1919            if (rootTypes.isEmpty()) {
1920                parameterTypeNode = parameterSetsDocument.createElement(parameterType);
1921                parameterSetsRoot.appendChild(parameterTypeNode);
1922                logger.trace("created new '{}' node", parameterType);
1923                writeParameterSets();
1924            }
1925            else if (rootTypes.size() == 1) {
1926                parameterTypeNode = rootTypes.get(0);
1927                logger.trace("found existing '{}' node", parameterType);
1928            }
1929        } catch (Exception exc) {
1930            LogUtil.logException("Error loading " + parameterSets.getDescription(), exc);
1931        }
1932        return parameterTypeNode;
1933    }
1934
1935    /**
1936     * Get a list of all of the categories for the given parameterType
1937     *
1938     * @param parameterType What type of parameter set
1939     *
1940     * @return List of (String) categories
1941     */
1942    public List<String> getAllParameterSetCategories(String parameterType) {
1943        List<String> allCategories = new ArrayList<>();
1944        try {
1945            Element rootType = getParameterTypeNode(parameterType);
1946            if (rootType != null) {
1947                allCategories =
1948                    XmlUtil.findDescendantNamesWithSeparator(rootType, TAG_FOLDER, CATEGORY_SEPARATOR);
1949            }
1950        } catch (Exception exc) {
1951            LogUtil.logException("Error loading " + parameterSets.getDescription(), exc);
1952        }
1953        return allCategories;
1954    }
1955    
1956
1957    /**
1958     * Get the list of {@link ParameterSet}s that are writable
1959     *
1960     * @param parameterType The type of parameter set
1961     *
1962     * @return List of writable parameter sets
1963     */
1964    public List<ParameterSet> getAllParameterSets(String parameterType) {
1965        List<ParameterSet> allParameterSets = new ArrayList<>();
1966        try {
1967            Element rootType = getParameterTypeNode(parameterType);
1968            if (rootType != null) {
1969                List<String> defaults =
1970                    XmlUtil.findDescendantNamesWithSeparator(rootType, TAG_DEFAULT, CATEGORY_SEPARATOR);
1971
1972                for (final String aDefault : defaults) {
1973                    Element anElement = XmlUtil.getElementAtNamedPath(rootType, stringToCategories(aDefault));
1974                    List<String> defaultParts = stringToCategories(aDefault);
1975                    int lastIndex = defaultParts.size() - 1;
1976                    String defaultName = defaultParts.get(lastIndex);
1977                    defaultParts.remove(lastIndex);
1978                    String folderName = StringUtil.join(CATEGORY_SEPARATOR, defaultParts);
1979                    ParameterSet newSet = new ParameterSet(defaultName, folderName, parameterType, anElement);
1980                    allParameterSets.add(newSet);
1981                }
1982            }
1983        } catch (Exception exc) {
1984            LogUtil.logException("Error loading " + ResourceManager.RSC_PARAMETERSETS.getDescription(), exc);
1985        }
1986        return allParameterSets;
1987    }
1988
1989    /**
1990     * Add the directory.
1991     *
1992     * @param parameterType Type of parameter set.
1993     * @param category Category (really a {@literal ">"} delimited string).
1994     *
1995     * @return {@code true} if the create was successful. {@code false} if
1996     * there already is a category with that name
1997     */
1998    public boolean addParameterSetCategory(String parameterType, String category) {
1999        logger.trace("parameter type: '{}' category: '{}'", parameterType, category);
2000        Element rootType = getParameterTypeNode(parameterType);
2001        XmlUtil.makeElementAtNamedPath(rootType, stringToCategories(category), TAG_FOLDER);
2002        writeParameterSets();
2003        return true;
2004    }
2005
2006    /**
2007     * Delete the given parameter set
2008     *
2009     * @param parameterType The type of parameter set
2010     * @param set Parameter set to delete.
2011     */
2012    public void deleteParameterSet(String parameterType, ParameterSet set) {
2013        Element parameterElement = set.getElement();
2014        Node parentNode = parameterElement.getParentNode();
2015        parentNode.removeChild((Node)parameterElement);
2016        writeParameterSets();
2017    }
2018
2019    /**
2020     * Delete the directory and all of its contents that the given category
2021     * represents.
2022     *
2023     * @param parameterType Type of parameter set.
2024     * @param category Category (really a {@literal ">"} delimited string).
2025     */
2026    public void deleteParameterSetCategory(String parameterType, String category) {
2027        Element rootType = getParameterTypeNode(parameterType);
2028        Element parameterSetElement = XmlUtil.getElementAtNamedPath(rootType, stringToCategories(category));
2029        Node parentNode = parameterSetElement.getParentNode();
2030        parentNode.removeChild((Node)parameterSetElement);
2031        writeParameterSets();
2032    }
2033
2034    /**
2035     * Rename the parameter set.
2036     *
2037     * @param parameterType Type of parameter set.
2038     * @param set Parameter set.
2039     */
2040    public void renameParameterSet(String parameterType, ParameterSet set) {
2041        String name = set.getName();
2042        Element parameterElement = set.getElement();
2043//        while (true) {
2044        name = GuiUtils.getInput("Enter a new name", "Name: ", name);
2045        if (name == null) {
2046            return;
2047        }
2048        name = StringUtil.replaceList(name.trim(),
2049            new String[] { "<", ">", "/", "\\", "\"" },
2050            new String[] { "_", "_", "_", "_",  "_"  }
2051        );
2052        if (name.length() == 0) {
2053            return;
2054        }
2055//        }
2056        parameterElement.setAttribute("name", name);
2057        writeParameterSets();
2058    }
2059    
2060    /**
2061     * Move the bundle to the given category area.
2062     *
2063     * @param parameterType Type of parameter set.
2064     * @param set Parameter set.
2065     * @param categories Where to move to.
2066     */
2067    public void moveParameterSet(String parameterType, ParameterSet set, List categories) {
2068        Element rootType = getParameterTypeNode(parameterType);
2069        Element parameterElement = set.getElement();
2070        Node parentNode = parameterElement.getParentNode();
2071        parentNode.removeChild((Node)parameterElement);
2072        Node newParentNode = XmlUtil.getElementAtNamedPath(rootType, categories);
2073        newParentNode.appendChild(parameterElement);
2074        writeParameterSets();
2075    }
2076
2077    /**
2078     * Move the bundle category.
2079     *
2080     * @param parameterType Type of parameter set.
2081     * @param fromCategories Category to move.
2082     * @param toCategories Where to move to.
2083     */
2084    public void moveParameterSetCategory(String parameterType, List fromCategories, List toCategories) {
2085        Element rootType = getParameterTypeNode(parameterType);
2086        Element parameterSetElementFrom = XmlUtil.getElementAtNamedPath(rootType, fromCategories);
2087        Node parentNode = parameterSetElementFrom.getParentNode();
2088        parentNode.removeChild((Node)parameterSetElementFrom);
2089        Node parentNodeTo = (Node)XmlUtil.getElementAtNamedPath(rootType, toCategories);
2090        parentNodeTo.appendChild(parameterSetElementFrom);
2091        writeParameterSets();
2092    }
2093
2094    /**
2095     * Show the Save Parameter Set dialog.
2096     *
2097     * @param parameterType Type of parameter set.
2098     * @param parameterValues Values to save.
2099     *
2100     * @return Whether or not the parameter set was saved.
2101     */
2102    public boolean saveParameterSet(String parameterType, Hashtable parameterValues) {
2103        try {
2104            String title = "Save Parameter Set";
2105
2106            // Create the category dropdown
2107            List<String> categories = getAllParameterSetCategories(parameterType);
2108            final JComboBox catBox = new JComboBox();
2109            catBox.setToolTipText(
2110                "<html>Categories can be entered manually. <br>Use '>' as the category delimiter. e.g.:<br>General > Subcategory</html>");
2111            catBox.setEditable(true);
2112            McVGuiUtils.setComponentWidth(catBox, McVGuiUtils.ELEMENT_DOUBLE_WIDTH);
2113            GuiUtils.setListData(catBox, categories);
2114
2115            // Create the default name dropdown
2116            final JComboBox nameBox = new JComboBox();
2117            nameBox.setEditable(true);
2118
2119            List<ParameterSet> pSets = getAllParameterSets(parameterType);
2120            List tails = new ArrayList(pSets.size() * 2);
2121            for (int i = 0; i < pSets.size(); i++) {
2122                ParameterSet pSet = pSets.get(i);
2123                tails.add(new TwoFacedObject(pSet.getName(), pSet));
2124            }
2125            java.util.Collections.sort(tails);
2126
2127            tails.add(0, new TwoFacedObject("", null));
2128            GuiUtils.setListData(nameBox, tails);
2129            nameBox.addActionListener(new ActionListener() {
2130                public void actionPerformed(ActionEvent ae) {
2131                    Object selected = nameBox.getSelectedItem();
2132                    if ( !(selected instanceof TwoFacedObject)) {
2133                        return;
2134                    }
2135                    TwoFacedObject tfo = (TwoFacedObject) selected;
2136                    List cats = ((ParameterSet) tfo.getId()).getCategories();
2137                    //                          if ((cats.size() > 0) && !catSelected) {
2138                    if (!cats.isEmpty()) {
2139                        catBox.setSelectedItem(
2140                            StringUtil.join(CATEGORY_SEPARATOR, cats));
2141                    }
2142                }
2143            });
2144
2145            JPanel panel = McVGuiUtils.sideBySide(
2146                McVGuiUtils.makeLabeledComponent("Category:", catBox),
2147                McVGuiUtils.makeLabeledComponent("Name:", nameBox)
2148            );
2149
2150            String name = "";
2151            String category = "";
2152            while (true) {
2153                if ( !GuiUtils.askOkCancel(title, panel)) {
2154                    return false;
2155                }
2156                name = StringUtil.replaceList(nameBox.getSelectedItem().toString().trim(),
2157                    new String[] { "<", ">", "/", "\\", "\"" },
2158                    new String[] { "_", "_", "_", "_",  "_"  }
2159                );
2160                if (name.isEmpty()) {
2161                    LogUtil.userMessage("Please enter a name");
2162                    continue;
2163                }
2164                category = StringUtil.replaceList(catBox.getSelectedItem().toString().trim(),
2165                    new String[] { "/", "\\", "\"" },
2166                    new String[] { "_", "_",  "_"  }
2167                );
2168                if (category.isEmpty()) {
2169                    LogUtil.userMessage("Please enter a category");
2170                    continue;
2171                }
2172                break;
2173            }
2174
2175            // Create a new element from the hashtable
2176            Element rootType = getParameterTypeNode(parameterType);
2177            Element parameterElement = parameterSetsDocument.createElement(TAG_DEFAULT);
2178            for (Enumeration e = parameterValues.keys(); e.hasMoreElements(); ) {
2179                Object nextKey = e.nextElement();
2180                String attribute = (String)nextKey;
2181                String value = (String)parameterValues.get(nextKey);
2182                parameterElement.setAttribute(attribute, value);
2183            }
2184
2185            // Set the name to the one we entered
2186            parameterElement.setAttribute(ATTR_NAME, name);
2187
2188            Element categoryNode = XmlUtil.makeElementAtNamedPath(rootType, stringToCategories(category), TAG_FOLDER);
2189//              Element categoryNode = XmlUtil.getElementAtNamedPath(rootType, stringToCategories(category));
2190
2191            categoryNode.appendChild(parameterElement);
2192            writeParameterSets();
2193        }
2194        catch (Exception e) {
2195            logger.error("error while saving parameter set", e);
2196            return false;
2197        }
2198
2199        return true;
2200    }
2201
2202}