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 */
028package edu.wisc.ssec.mcidasv.util;
029
030import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.list;
031
032import java.awt.Desktop;
033import java.io.IOException;
034import java.net.URI;
035import java.net.URISyntaxException;
036import java.util.List;
037
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import ucar.unidata.util.LogUtil;
042
043import edu.wisc.ssec.mcidasv.McIDASV;
044
045/**
046 * A simple utility class for opening a web browser to a given link.
047 */
048public final class WebBrowser {
049    
050    /** Logging object. */
051    private static final Logger logger =
052        LoggerFactory.getLogger(WebBrowser.class);
053    
054    /** Probe Unix-like systems for these browsers, in this order. */
055    private static final List<String> unixBrowsers = 
056        list("firefox", "chromium-browser", "google-chrome", "konqueror",
057             "opera", "mozilla", "netscape");
058    
059    /**
060     * {@code IOException} formatting string used when all browsing methods
061     * have failed.
062     */
063    private static final String ALL_METHODS_ERRMSG = "Could not open '%s'";
064    
065    /**
066     * {@code IOException} formatting string used by
067     * {@link #openOldStyle(String)} when no browsers could be identified on
068     * the system.
069     */
070    private static final String NO_BROWSER_ERRMSG =
071        "Could not find a web browser to launch (tried %s)";
072    
073    /** Message displayed to the user when all browsing methods have failed. */
074    private static final String THINGS_DUN_BROKE_ERRMSG =
075        "All three approaches for opening a browser " +
076            "failed!\nPlease consider sending a support request via\n" +
077            "the button below.\n";
078
079    /** Do not create instances of {@code WebBrowser}. */
080    private WebBrowser() { }
081
082    /**
083     * Attempts to use the system default browser to visit {@code url}. Tries
084     * looking for and executing any browser specified by the IDV property 
085     * {@literal "idv.browser.path"}. 
086     * 
087     * <p>If the property wasn't given or there 
088     * was an error, try the new (as of Java 1.6) way of opening a browser. 
089     * 
090     * <p>If the previous attempts failed (or we're in 1.5), we finally try
091     * some more primitive measures.
092     * 
093     * <p>Note: if you are trying to use this method with a 
094     * {@link javax.swing.JTextPane} you may need to turn off editing via
095     * {@link javax.swing.JTextPane#setEditable(boolean)}.
096     * 
097     * @param url URL to visit.
098     * 
099     * @see #tryUserSpecifiedBrowser(String)
100     * @see #openNewStyle(String)
101     * @see #openOldStyle(String)
102     */
103    public static void browse(final String url) {
104        // if the user has taken the trouble to explicitly provide the path to
105        // a web browser, we should probably prefer it.
106        if (tryUserSpecifiedBrowser(url)) {
107            return;
108        }
109        
110        // try using the JDK-supported approach
111        if (openNewStyle(url)) {
112            return;
113        }
114        
115        // if not, use the hacky stuff.
116        try {
117            openOldStyle(url);
118        } catch (Exception e) {
119            logger.warn(String.format(ALL_METHODS_ERRMSG, url), e);
120            IOException uhoh =
121                new IOException(String.format(ALL_METHODS_ERRMSG, url));
122            LogUtil.logException(THINGS_DUN_BROKE_ERRMSG, uhoh);
123        }
124    }
125    
126    /**
127     * Test whether or not a given URL should be opened in a web browser.
128     *
129     * @param url URL to test. Cannot be {@code null}.
130     *
131     * @return {@code true} if {@code url} begins with either
132     * {@literal "http:"} or {@literal "https:"}.
133     */
134    public static boolean useBrowserForUrl(final String url) {
135        String lowercase = url.toLowerCase();
136        return lowercase.startsWith("http:") || lowercase.startsWith("https:");
137    }
138
139    /**
140     * Use the functionality within {@link java.awt.Desktop} to try opening
141     * the user's preferred web browser.
142     *
143     * @param url URL to visit.
144     * 
145     * @return Either {@code true} if things look ok, {@code false} if there 
146     * were problems.
147     */
148    private static boolean openNewStyle(final String url) {
149        boolean retVal = false;
150        if (Desktop.isDesktopSupported()) {
151            Desktop desktop = Desktop.getDesktop();
152            if (desktop.isSupported(Desktop.Action.BROWSE)) {
153                try {
154                    desktop.browse(new URI(url));
155                    // well... the assumption is that there was not a problem
156                    retVal = true;
157                } catch (URISyntaxException e) {
158                    logger.warn("Bad syntax in URI: "+url, e);
159                } catch (IOException e) {
160                    logger.warn("Problem accessing URI: "+url, e);
161                }
162            }
163        }
164        return retVal;
165    }
166
167    /**
168     * Uses {@link Runtime#exec(String)} to launch the user's preferred web
169     * browser. This method isn't really recommended unless you're stuck with
170     * Java 1.5.
171     * 
172     * <p>Note that the browsers need to be somewhere in the PATH, as this 
173     * method uses the {@code which} command (also needs to be in the PATH!).
174     * 
175     * @param url URL to visit.
176     */
177    private static void openOldStyle(final String url) {
178        try {
179            if (isWindows()) {
180                Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
181            } else if (isMac()) {
182                Runtime.getRuntime().exec("/usr/bin/open "+url);
183            } else {
184                for (String browser : unixBrowsers) {
185                    if (Runtime.getRuntime().exec("which "+browser).waitFor() == 0) {
186                        Runtime.getRuntime().exec(browser+' '+url);
187                        return;
188                    }
189                }
190                String msg = String.format(NO_BROWSER_ERRMSG, unixBrowsers);
191                throw new IOException(msg);
192            }
193        } catch (Exception e) {
194            logger.warn("Could not open URL '"+url+'\'', e);
195        }
196    }
197
198    /**
199     * Attempts to launch the browser pointed at by 
200     * the {@literal "idv.browser.path"} IDV property, if it has been set.
201     * 
202     * @param url URL to open.
203     * 
204     * @return Either {@code true} if the command-line was executed,
205     * {@code false} if either the command-line wasn't launched or
206     * {@literal "idv.browser.path"} was not set.
207     */
208    private static boolean tryUserSpecifiedBrowser(final String url) {
209        McIDASV mcv = McIDASV.getStaticMcv();
210        boolean retVal = false;
211        if (mcv != null) {
212            String browserPath = mcv.getProperty("idv.browser.path", null);
213            if ((browserPath != null) && !browserPath.trim().isEmpty()) {
214                try {
215                    Runtime.getRuntime().exec(browserPath+' '+url);
216                    retVal = true;
217                } catch (Exception e) {
218                    logger.warn("Could not execute '"+browserPath+'\'', e);
219                }
220            }
221        }
222        return retVal;
223    }
224
225    /**
226     * Test for whether or not the current platform is Mac OS X.
227     *
228     * @return Are we shiny, happy OS X users?
229     */
230    private static boolean isMac() {
231        return System.getProperty("os.name", "").startsWith("Mac OS");
232    }
233
234    /**
235     * Test for whether or not the current platform is some form of
236     * {@literal "unix"} (but not OS X!).
237     *
238     * @return Do we perhaps think that beards and suspenders are the height 
239     * of fashion?
240     */
241    private static boolean isUnix() {
242        return !isMac() && !isWindows();
243    }
244
245    /**
246     * Test for whether or not the current platform is Windows.
247     *
248     * @return Are we running Windows??
249     */
250    private static boolean isWindows() {
251        return System.getProperty("os.name", "").startsWith("Windows");
252    }
253
254    public static void main(String[] args) {
255        browse("http://www.rust-lang.org/"); // sassy!
256    }
257}