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