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.chooser;
030
031import static javax.swing.GroupLayout.DEFAULT_SIZE;
032import static javax.swing.GroupLayout.Alignment.LEADING;
033import static javax.swing.GroupLayout.Alignment.TRAILING;
034
035import java.awt.Component;
036import java.awt.Dimension;
037import java.awt.event.ActionListener;
038import java.io.File;
039import java.util.ArrayList;
040import java.util.Hashtable;
041import java.util.List;
042
043import javax.swing.GroupLayout;
044import javax.swing.JCheckBox;
045import javax.swing.JComponent;
046import javax.swing.JFileChooser;
047import javax.swing.JLabel;
048import javax.swing.JPanel;
049import javax.swing.JRadioButton;
050import javax.swing.JTextField;
051import javax.swing.SwingUtilities;
052
053import edu.wisc.ssec.mcidasv.Constants;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056import org.w3c.dom.Element;
057
058import ucar.unidata.data.DataSource;
059import ucar.unidata.idv.chooser.IdvChooserManager;
060import ucar.unidata.util.FileManager;
061import ucar.unidata.util.GuiUtils;
062import ucar.unidata.util.Misc;
063import ucar.unidata.util.PollingInfo;
064import ucar.unidata.xml.XmlUtil;
065
066import edu.wisc.ssec.mcidasv.ArgumentManager;
067import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
068
069/**
070 * A class for choosing files that can be polled.
071 *
072 * @author IDV development team
073 */
074public class PollingFileChooser extends FileChooser {
075
076    /** Logging object. */
077    private static final Logger logger =
078        LoggerFactory.getLogger(PollingFileChooser.class);
079
080    /** Any initial file system path to start with */
081    public static final String ATTR_DIRECTORY = "directory";
082
083    /** Polling interval */
084    public static final String ATTR_INTERVAL = "interval";
085
086    /** The title attribute */
087    public static final String ATTR_TITLE = "title";
088
089    /** polling info */
090    private PollingInfo pollingInfo;
091    
092    /** file path widget accessible to everyone */
093    private JTextField filePathWidget;
094    
095    /** file pattern widget accessible to everyone */
096    private JTextField filePatternWidget;
097
098    /** Reference to the {@literal "refresh"} checkbox. */
099    private JCheckBox pollingCbx;
100
101    /** Keep track of what was selected and update status accordingly */
102    boolean isDirectory = false;
103    int directoryCount = 0;
104    int fileCount = 0;
105    
106    /**
107     * Create the PollingFileChooser, passing in the manager and the xml element
108     * from choosers.xml
109     *
110     * @param mgr The manager
111     * @param root The xml root
112     *
113     */
114    public PollingFileChooser(IdvChooserManager mgr, Element root) {
115        super(mgr, root);
116        
117        Element chooserNode = getXmlNode();
118        
119        pollingInfo = (PollingInfo) idv.getPreference(PREF_POLLINGINFO + "." + getId());
120        if (pollingInfo == null) {
121            pollingInfo = new PollingInfo();
122            pollingInfo.setMode(PollingInfo.MODE_COUNT);
123            pollingInfo.setName("Directory");
124            pollingInfo.setFilePattern(getAttribute(ATTR_FILEPATTERN, ""));
125            pollingInfo.setFilePaths(Misc.newList(getAttribute(ATTR_DIRECTORY, "")));
126            pollingInfo.setIsActive(XmlUtil.getAttribute(chooserNode, ATTR_POLLON, true));
127
128            pollingInfo.setInterval((long) (XmlUtil.getAttribute(chooserNode, ATTR_INTERVAL, 5.0) * 60 * 1000));
129            int fileCount = 1;
130            String s = XmlUtil.getAttribute(chooserNode, ATTR_FILECOUNT, "1");
131            s = s.trim();
132            if (s.equals("all")) {
133                fileCount = Integer.MAX_VALUE;
134            } else {
135                fileCount = new Integer(s).intValue();
136            }
137            pollingInfo.setFileCount(fileCount);
138        }
139        filePathWidget = pollingInfo.getFilePathWidget();
140        filePatternWidget = pollingInfo.getPatternWidget();
141
142    }
143    
144    /**
145     * An extension of JFileChooser
146     *
147     * @author IDV Development Team
148     * @version $Revision$
149     */
150    public class MyDirectoryChooser extends MyFileChooser {
151
152        /**
153         * Create the file chooser
154         *
155         * @param path   the initial path
156         */
157        public MyDirectoryChooser(String path) {
158            super(path);
159            setMultiSelectionEnabled(getAllowMultiple());
160            setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
161        }
162        
163        /**
164         * Set the selected directory
165         *
166         * @param selectedDirectory  the selected directory
167         */
168        public void setCurrentDirectory(File selectedDirectory) {
169            super.setCurrentDirectory(selectedDirectory);
170            setSelectedFiles(null);
171        }
172        
173        /**
174         * Set the selected files
175         *
176         * @param selectedFiles  the selected files
177         */
178        public void setSelectedFiles(File[] selectedFiles) {
179            fileCount=0;
180            directoryCount=0;
181            if (selectedFiles == null || selectedFiles.length == 0) {
182                isDirectory = true;
183                if (filePathWidget!=null) {
184                    filePathWidget.setText(this.getCurrentDirectory().getAbsolutePath());
185                }
186            }
187            else {
188                isDirectory = false;
189                for (File selectedFile : selectedFiles) {
190                    if (selectedFile.isFile()) fileCount++;
191                    if (selectedFile.isDirectory()) {
192                        directoryCount++;
193                        if (directoryCount==1 && filePathWidget!=null) {
194                            filePathWidget.setText(selectedFile.getAbsolutePath());
195                        }
196                    }
197                }
198            }
199            super.setSelectedFiles(selectedFiles);
200            
201            // Disable load button if we arrived here by typing a directory or file name
202            if (directoryCount > 0 ||
203                    directoryCount == 0 && fileCount == 0 && !isDirectory) {
204                setHaveData(false);
205            }
206            else {
207                setHaveData(true);
208            }
209
210            updateStatus();
211        }
212    
213    }
214
215    /**
216     * Make the file chooser
217     *
218     * @param path   the initial path
219     *
220     * @return  the file chooser
221     */
222    protected JFileChooser doMakeDirectoryChooser(String path) {
223        return new MyDirectoryChooser(path);
224    }
225
226    /**
227     * Override the base class method to catch the do load
228     * This directly handles loading directories and passes off files to selectFiles() and selectFilesInner()
229     */
230    public void doLoadInThread() {
231        Element chooserNode = getXmlNode();
232
233        idv.getStateManager().writePreference(PREF_POLLINGINFO + "." + getId(), pollingInfo);
234        idv.getStateManager().writePreference(PREF_DEFAULTDIR + getId(), pollingInfo.getFile());
235        
236//        userMessage("doLoadInThread: fileCount: " + fileCount + ", directoryCount: " + directoryCount + ", isDirectory: " + isDirectory + ", getHaveData: " + getHaveData() + ", buttonPressed: " + buttonPressed);
237
238        // If a user types in a directory (on Windows) do not try to load that directory.
239        // If the load button was pressed, go for it!
240        if (fileCount == 0 && !buttonPressed) return;
241        
242        // If this is file(s) only, use FileChooser.doLoadInThread()
243        if (fileCount > 0) {
244            super.doLoadInThread();
245            return;
246        }
247        
248        Hashtable properties = new Hashtable();
249        if ( !pollingInfo.applyProperties()) {
250            return;
251        }
252        
253        String title = basename(pollingInfo.getFile());
254        title += "/" + ((JTextField)pollingInfo.getPatternWidget()).getText();
255        pollingInfo.setName(title);
256        properties.put(DataSource.PROP_TITLE, title);
257        properties.put(DataSource.PROP_POLLINFO, pollingInfo.cloneMe());
258
259        String dataSourceId;
260        if (XmlUtil.hasAttribute(chooserNode, ATTR_DATASOURCEID)) {
261            dataSourceId = XmlUtil.getAttribute(chooserNode, ATTR_DATASOURCEID);
262        } else {
263            dataSourceId = getDataSourceId();
264        }
265        
266        makeDataSource(pollingInfo.getFiles(), dataSourceId, properties);
267    }
268
269    /**
270     * Handle the selection of the set of files
271     * Copy from IDV FileChooser, add ability to name and poll
272     */
273    protected boolean selectFilesInner(File[] files, File directory)
274            throws Exception {
275        Element chooserNode = getXmlNode();
276        
277        if ((files == null) || (files.length == 0)) {
278            userMessage("Please select a file");
279            return false;
280        }
281        FileManager.addToHistory(files[0]);
282        List    selectedFiles      = new ArrayList();
283        String  fileNotExistsError = "";
284        boolean didXidv            = false;
285
286        for (int i = 0; i < files.length; i++) {
287            if ( !files[i].exists()) {
288                fileNotExistsError += "File does not exist: " + files[i] + "\n";
289            } else {
290                String filename = files[i].toString();
291                //Check for the bundle or jnlp file
292                if (((ArgumentManager)idv.getArgsManager()).isBundle(filename)
293                        || idv.getArgsManager().isJnlpFile(filename)) {
294                    didXidv = idv.handleAction(filename, null);
295                } else {
296                    selectedFiles.add(filename);
297                }
298            }
299        }
300
301        if (didXidv) {
302            closeChooser();
303            return true;
304        }
305
306        if (selectedFiles.size() == 0) {
307            return false;
308        }
309
310        if (fileNotExistsError.length() > 0) {
311            userMessage(fileNotExistsError);
312            return false;
313        }
314
315        Object definingObject = selectedFiles;
316        String title = selectedFiles.size() + " files";
317        if (selectedFiles.size() == 1) {
318            definingObject = selectedFiles.get(0);
319            title = basename(definingObject.toString());
320        }
321        
322        String dataSourceId;
323        if (XmlUtil.hasAttribute(chooserNode, ATTR_DATASOURCEID)) {
324            dataSourceId = XmlUtil.getAttribute(chooserNode, ATTR_DATASOURCEID);
325        } else {
326            dataSourceId = getDataSourceId();
327        }
328        
329        Hashtable   properties  = new Hashtable();
330        
331        // TODO: I disabled file polling on purpose:
332        // The control for this is in the Directory options and is grayed out
333        //  when selecting single files.  Is this something people want?
334        PollingInfo newPollingInfo = new PollingInfo(false);
335        String pattern = getFilePattern();
336        if ((pattern != null) && (pattern.length() > 0)) {
337            newPollingInfo.setFilePattern(pattern);
338        }
339        newPollingInfo.setName(title);
340        properties.put(DataSource.PROP_TITLE, title);
341        properties.put(DataSource.PROP_POLLINFO, newPollingInfo);
342
343        // explicitly denote whether or not this was a "bulk load". these data
344        // sources require a little extra attention when being unpersisted.
345        properties.put("bulk.load", (selectedFiles.size() > 1));
346        return makeDataSource(definingObject, dataSourceId, properties);
347    }
348    
349    /**
350     * Emulate basename()
351     */
352    private String basename(String path) {
353        if (path.lastIndexOf('/') > 0)
354            path = path.substring(path.lastIndexOf('/'));
355        else if (path.lastIndexOf('\\') > 0)
356            path = path.substring(path.lastIndexOf('\\'));
357        if (path.length() > 1)
358            path = path.substring(1);
359        return path;
360    }
361    
362    /**
363     * Get the tooltip for the load button
364     *
365     * @return The tooltip for the load button
366     */
367    protected String getLoadToolTip() {
368        return "Load the file(s) or directory";
369    }
370
371    /**
372     * Override the base class method to catch the do update.
373     */
374    @Override public void doUpdate() {
375        fileChooser.rescanCurrentDirectory();
376    }
377
378    /**
379     * Process PollingInfo GUI components based on their label and properties
380     * Turn it into a nicely-formatted labeled panel
381     */
382    private JPanel processPollingOption(JLabel label, JPanel panel) {
383        String string = label.getText().trim();
384        
385        // File Pattern
386        if (string.equals("File Pattern:")) {
387            Component panel1 = panel.getComponent(0);
388            if (panel1 instanceof JPanel) {
389                Component[] comps = ((JPanel)panel1).getComponents();
390                if (comps.length == 2) {
391                    List newComps1 = new ArrayList();
392                    List newComps2 = new ArrayList();
393                    if (comps[0] instanceof JPanel) {
394                        Component[] comps2 = ((JPanel)comps[0]).getComponents();
395                        if (comps2.length==1 &&
396                                comps2[0] instanceof JPanel)
397                            comps2=((JPanel)comps2[0]).getComponents();
398                        if (comps2.length == 2) {
399                            if (comps2[0] instanceof JTextField) {
400                                McVGuiUtils.setComponentWidth((JTextField) comps2[0], McVGuiUtils.Width.SINGLE);
401                            }
402                            newComps1.add(comps2[0]);
403                            newComps2.add(comps2[1]);
404                        }
405                    }
406                    newComps1.add(comps[1]);
407                    panel = GuiUtils.vbox(
408                            GuiUtils.left(GuiUtils.hbox(newComps1)),
409                            GuiUtils.left(GuiUtils.hbox(newComps2))
410                    );
411                }
412            }
413        }
414        
415        // Files
416        if (string.equals("Files:")) {
417            Component panel1 = panel.getComponent(0);
418            if (panel1 instanceof JPanel) {
419                Component[] comps = ((JPanel)panel1).getComponents();
420                if (comps.length == 6) {
421                    List newComps1 = new ArrayList();
422                    List newComps2 = new ArrayList();
423                    if (comps[3] instanceof JRadioButton) {
424                        String text = ((JRadioButton) comps[3]).getText().trim();
425                        if (text.equals("All files in last:")) text="All files in last";
426                        ((JRadioButton) comps[3]).setText(text);
427                    }
428                    if (comps[4] instanceof JTextField) {
429                        McVGuiUtils.setComponentWidth((JTextField) comps[4], McVGuiUtils.Width.HALF);
430                    }
431                    if (comps[5] instanceof JLabel) {
432                        String text = ((JLabel) comps[5]).getText().trim();
433                        ((JLabel) comps[5]).setText(text);
434                    }
435                    newComps1.add(comps[0]);
436                    newComps1.add(comps[1]);
437                    newComps2.add(comps[3]);
438                    newComps2.add(comps[4]);
439                    newComps2.add(comps[5]);
440                    panel = GuiUtils.vbox(
441                            GuiUtils.left(GuiUtils.hbox(newComps1)),
442                            GuiUtils.left(GuiUtils.hbox(newComps2))
443                    );
444                }
445            }
446        }
447        
448        // Polling
449        if (string.equals("Polling:")) {
450            Component panel1 = panel.getComponent(0);
451            if (panel1 instanceof JPanel) {
452                Component[] comps = ((JPanel)panel1).getComponents();
453                if (comps.length == 4) {
454                    List newComps = new ArrayList();
455                    if (comps[0] instanceof JCheckBox) {
456                        ((JCheckBox) comps[0]).setText("");
457                        pollingCbx = (JCheckBox)comps[0];
458                        ActionListener listener = buildPollingActionListener();
459                        pollingCbx.addActionListener(listener);
460                    }
461                    if (comps[1] instanceof JLabel) {
462                        String text = ((JLabel) comps[1]).getText().trim();
463                        if (text.equals("Check every:")) text="Refresh every";
464                        ((JLabel) comps[1]).setText(text);
465                    }
466                    if (comps[2] instanceof JTextField) {
467                        McVGuiUtils.setComponentWidth((JTextField) comps[2], McVGuiUtils.Width.HALF);
468                    }
469                    if (comps[3] instanceof JLabel) {
470                        String text = ((JLabel) comps[3]).getText().trim();
471                        ((JLabel) comps[3]).setText(text);
472                    }
473                    newComps.add(comps[0]);
474                    newComps.add(comps[1]);
475                    newComps.add(comps[2]);
476                    newComps.add(comps[3]);
477                    string="";
478                    panel = GuiUtils.left(GuiUtils.hbox(newComps));
479                }
480            }
481        }
482        
483        return McVGuiUtils.makeLabeledComponent(string, panel);
484    }
485    
486    /**
487     * Turn PollingInfo options into a nicely-formatted panel
488     */
489    private JPanel processPollingOptions(List comps) {
490        List newComps = new ArrayList();
491        newComps = new ArrayList();
492        if (comps.size() == 4) {
493//          newComps.add(comps.get(0));
494            
495            // Put Recent and Pattern panels next to each other and make them bordered
496            Component[] labelPanel1 = ((JPanel)comps.get(2)).getComponents();
497            Component[] labelPanel2 = ((JPanel)comps.get(1)).getComponents();
498            if (labelPanel1[1] instanceof JPanel && labelPanel2[1] instanceof JPanel) {
499                JPanel recentPanel = (JPanel)labelPanel1[1];
500                JPanel patternPanel = (JPanel)labelPanel2[1];
501                recentPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Recent Files"));
502                patternPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("File Pattern"));
503                
504                // Make the container panel
505                JPanel filePanel = new JPanel();
506                
507                GroupLayout layout = new GroupLayout(filePanel);
508                filePanel.setLayout(layout);
509                layout.setHorizontalGroup(
510                    layout.createParallelGroup(LEADING)
511                    .addGroup(layout.createSequentialGroup()
512                        .addComponent(recentPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
513                        .addGap(GAP_RELATED)
514                        .addComponent(patternPanel, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
515                        )
516                );
517                layout.setVerticalGroup(
518                    layout.createParallelGroup(LEADING)
519                    .addGroup(layout.createSequentialGroup()
520                        .addGroup(layout.createParallelGroup(TRAILING)
521                            .addComponent(recentPanel, LEADING, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
522                            .addComponent(patternPanel, LEADING, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
523                            )
524                );
525
526                newComps.add(McVGuiUtils.makeLabeledComponent("Directory:", filePanel));
527            }
528            else {
529                newComps.add(comps.get(1));
530                newComps.add(comps.get(2));
531            }
532            newComps.add(comps.get(3));
533        }
534        else {
535            newComps = comps;
536        }
537        return GuiUtils.top(GuiUtils.vbox(newComps));
538    }
539    
540    /**
541     * Set the status message appropriately
542     */
543    protected void updateStatus() {
544        super.updateStatus();
545        String selectedReference = "the selected data";
546        
547        if(!getHaveData()) {
548            setStatus("Select zero, one, or multiple files");
549            GuiUtils.enableTree(bottomPanel, false);
550            return;
551        }
552        
553        if (isDirectory) {
554            selectedReference = "all files in this directory";
555        }
556        else {
557            if (fileCount > 0) {
558                if (fileCount > 1) selectedReference = "the selected files";
559                else selectedReference = "the selected file";
560            }
561            if (directoryCount > 0) {
562                selectedReference = "the selected directory";
563            }
564        }
565        GuiUtils.enableTree(bottomPanel, isDirectory || directoryCount > 0);
566        setStatus("Press \"" + CMD_LOAD + "\" to load " + selectedReference, "buttons");
567    }
568        
569    /**
570     * Get the top panel for the chooser
571     * @return the top panel
572     */
573//    protected JPanel getTopPanel() {
574//      return McVGuiUtils.makeLabeledComponent("Source Name:", pollingInfo.getNameWidget());
575//    }
576    
577    /**
578     * Get the center panel for the chooser
579     * @return the center panel
580     */
581    protected JPanel getCenterPanel() {
582        fileChooser = doMakeDirectoryChooser(getPath());
583        fileChooser.setPreferredSize(new Dimension(300, 300));
584        fileChooser.setMultiSelectionEnabled(getAllowMultiple());
585
586        fileChooser.addPropertyChangeListener(
587            JFileChooser.DIRECTORY_CHANGED_PROPERTY,
588            createPropertyListener()
589        );
590
591        JPanel centerPanel;
592        JComponent accessory = getAccessory();
593        if (accessory == null) {
594            centerPanel = GuiUtils.center(fileChooser);
595        } else {
596            centerPanel = GuiUtils.centerRight(fileChooser, GuiUtils.top(accessory));
597        }
598        centerPanel.setBorder(javax.swing.BorderFactory.createEtchedBorder());
599        return McVGuiUtils.makeLabeledComponent("Files:", centerPanel);
600    }
601    
602    /**
603     * Get the bottom panel for the chooser
604     * @return the bottom panel
605     */
606    protected JPanel getBottomPanel() {
607
608        // Pull apart the PollingInfo components and rearrange them
609        // Don't want to override PollingInfo because it isn't something the user sees
610        // Arranged like: Label, Panel; Label, Panel; Label, Panel; etc...
611        List comps = new ArrayList();
612        List newComps = new ArrayList();
613        pollingInfo.getPropertyComponents(comps, false, pollingInfo.getFileCount()>0);
614        for (int i=0; i<comps.size()-1; i++) {
615            JComponent compLabel = (JComponent)comps.get(i);
616            if (compLabel instanceof JLabel) {
617                i++;
618                JComponent compPanel = (JComponent)comps.get(i);
619                if (compPanel instanceof JPanel) {
620                    newComps.add(processPollingOption((JLabel)compLabel, (JPanel)compPanel));
621                }
622            }
623        }
624        
625        JPanel pollingPanel = processPollingOptions(newComps);
626        return pollingPanel;
627    }
628
629    /**
630     * Returns an {@link ActionListener} that should be listening to
631     * {@link #pollingCbx}.
632     *
633     * <p>This listener allows users to enable/disable directory watches.</p>
634     *
635     * @return {@code ActionListener} that enables/disables directory watching.
636     */
637    private ActionListener buildPollingActionListener() {
638        return e -> {
639            logger.trace("fired: pollingCbx={}", pollingCbx.isSelected());
640            if (!pollingCbx.isSelected()) {
641                handleStopWatchService(
642                    Constants.EVENT_FILECHOOSER_STOP,
643                    "disabled refreshes"
644                );
645            } else {
646                handleStartWatchService(
647                    Constants.EVENT_FILECHOOSER_START,
648                    "enabled refreshes"
649                );
650                SwingUtilities.invokeLater(this::doUpdate);
651            }
652        };
653    }
654
655}
656