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