001package jmri.util;
002
003import java.awt.Desktop;
004import java.awt.event.ActionEvent;
005import java.io.*;
006import java.net.HttpURLConnection;
007import java.net.URI;
008import java.net.URISyntaxException;
009import java.util.List;
010import java.util.ServiceLoader;
011
012import javax.annotation.Nonnull;
013import javax.swing.*;
014
015import jmri.InstanceManager;
016import jmri.JmriException;
017import jmri.util.gui.GuiLafPreferencesManager;
018import jmri.util.swing.JmriJOptionPane;
019import jmri.web.server.WebServerPreferences;
020
021/**
022 * Common utility methods for displaying JMRI help pages.
023 * <p>
024 * This class was created to contain common Java Help information but is now
025 * changed to use a web browser instead.
026 *
027 * @author Bob Jacobsen Copyright 2007
028 * @author Daniel Bergqvist Copyright 2021
029 */
030public class HelpUtil {
031
032    private HelpUtil() {
033        // this is a class of static methods
034    }
035
036    /**
037     * Append a help menu to the menu bar.
038     *
039     * @param menuBar the menu bar to add the help menu to
040     * @param ref     context-sensitive help reference
041     * @param direct  true if this call should complete the help menu by adding the
042     *                general help
043     * @return new Help menu, in case user wants to add more items or null if unable
044     *         to create the help menu
045     */
046    public static JMenu helpMenu(JMenuBar menuBar, String ref, boolean direct) {
047        JMenu helpMenu = makeHelpMenu(ref, direct);
048        if (menuBar != null) {
049            menuBar.add(helpMenu);
050        }
051        return helpMenu;
052    }
053
054    public static JMenu makeHelpMenu(String ref, boolean direct) {
055        JMenu helpMenu = new JMenu(Bundle.getMessage("ButtonHelp"));
056        var helpMenuItem = makeHelpMenuItem(ref);
057        if (helpMenuItem != null) {
058            helpMenu.add(helpMenuItem);
059        }
060
061        if (direct) {
062            ServiceLoader<MenuProvider> providers = ServiceLoader.load(MenuProvider.class);
063            providers.forEach(provider -> provider.getHelpMenuItems().forEach(i -> {
064                if (i != null) {
065                    helpMenu.add(i);
066                } else {
067                    helpMenu.addSeparator();
068                }
069            }));
070        }
071        return helpMenu;
072    }
073
074    public static JMenuItem makeHelpMenuItem(String ref) {
075        if (ref == null) return null;
076        
077        JMenuItem menuItem = new JMenuItem(Bundle.getMessage("MenuItemWindowHelp"));
078
079        menuItem.addActionListener((ignore) -> displayHelpRef(ref));
080
081        return menuItem;
082    }
083
084    public static void addHelpToComponent(java.awt.Component component, String ref) {
085        enableHelpOnButton(component, ref);
086    }
087
088    // https://coderanch.com/how-to/javadoc/javahelp-2.0_05/javax/help/HelpBroker.html#enableHelpOnButton(java.awt.Component,%20java.lang.String,%20javax.help.HelpSet)
089    public static void enableHelpOnButton(java.awt.Component comp, String id) {
090        if (comp instanceof javax.swing.AbstractButton) {
091            ((javax.swing.AbstractButton) comp).addActionListener((ignore) -> displayHelpRef(id));
092        } else if (comp instanceof java.awt.Button) {
093            ((java.awt.Button) comp).addActionListener((ignore) -> displayHelpRef(id));
094        } else {
095            throw new IllegalArgumentException("comp is not a javax.swing.AbstractButton or a java.awt.Button");
096        }
097    }
098
099    public static void displayHelpRef(String ref) {
100        log.debug("displayHelpRef: {}", ref);
101
102        // Plugin help is included in the plugin JAR file
103        boolean isPluginHelp = ref.startsWith("plugin:");
104
105        // We only have English (en) and French (fr) help files
106        // and we assume that plugins doesn't have French help files.
107        boolean isFrench = "fr"
108                .equals(InstanceManager.getDefault(GuiLafPreferencesManager.class).getLocale().getLanguage());
109        String localeStr = isFrench && !isPluginHelp ? "fr" : "en";
110
111        HelpUtilPreferences preferences = InstanceManager.getDefault(HelpUtilPreferences.class);
112
113        String tempFile;
114        if (isPluginHelp) {
115            tempFile = "plugin";
116            ref = ref.substring("plugin:".length());
117        } else {
118            tempFile = "help/" + localeStr;
119        }
120        tempFile += "/" + ref.replace(".", "/");
121        String[] fileParts = tempFile.split("_", 2);
122        String file = fileParts[0] + ".shtml";
123        if (fileParts.length > 1) {
124            file = file + "#" + fileParts[1];
125        }
126
127        String url;
128        boolean webError = false;
129
130        // Use jmri.org if selected.
131        if (preferences.getOpenHelpOnline() && !isPluginHelp) {
132            url = "https://www.jmri.org/" + file;
133            if (jmri.util.HelpUtil.showWebPage(ref, url)) return;
134            webError = true;
135        }
136
137        // Use the local JMRI web server if selected or if plugin help
138        if (preferences.getOpenHelpOnJMRIWebServer() || isPluginHelp) {
139            WebServerPreferences webServerPreferences = InstanceManager.getDefault(WebServerPreferences.class);
140            String port = Integer.toString(webServerPreferences.getPort());
141            url = "http://localhost:" + port + "/" + file;
142            log.debug("displayHelpRef: url: {}", url);
143            if (jmri.util.HelpUtil.showWebPage(ref, url)) return;
144            webError = true;
145        }
146
147        if (webError) {
148            JmriJOptionPane.showMessageDialog(null,
149                    Bundle.getMessage("HelpWeb_ServerError"),
150                    Bundle.getMessage("HelpWeb_Title"),
151                    JmriJOptionPane.ERROR_MESSAGE);
152
153            // Don't show any more help if plugin
154            if (isPluginHelp) return;
155        }
156
157        // Open a local help file by default or a failure of jmri.org or the local JMRI web server.
158        String fileName;
159        try {
160            fileName = HelpUtil.createStubFile(ref, localeStr);
161        } catch (IOException iox) {
162            log.error("Unable to create the stub file for \"{}\" ", ref);
163            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("HelpError_StubFile", ref),
164                    Bundle.getMessage("HelpStub_Title"), JmriJOptionPane.ERROR_MESSAGE);
165            return;
166        }
167
168        File f = new File(fileName);
169        if (!f.exists()) {
170            log.error("The help reference \"{}\" is not found. File is not found: {}", ref, fileName);
171            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("HelpError_ReferenceNotFound", ref),
172                    Bundle.getMessage("HelpError_Title"), JmriJOptionPane.ERROR_MESSAGE);
173            return;
174        }
175
176        if (SystemType.isWindows()) {
177            try {
178                openWindowsFile(f);
179            } catch (JmriException e) {
180                log.error("unable to show help page {} in Windows due to:", ref, e);
181            }
182            return;
183        }
184
185        url = "file://" + fileName;
186        jmri.util.HelpUtil.showWebPage(ref, url);
187    }
188
189    public static String createStubFile(String helpKey, String locale) throws IOException {
190        String stubLocation = FileUtil.getHomePath() + "jmrihelp/";
191        FileUtil.createDirectory(stubLocation);
192        log.debug("---- stub location: {}", stubLocation);
193
194        String htmlLocation = FileUtil.getProgramPath() + "help/" + locale + "/local/";
195        log.debug("---- html location: {}", htmlLocation);
196
197        String template = FileUtil.readFile(new File(htmlLocation + "stub_template.html"));
198        String expandedHelpKey = helpKey.replace(".", "/");
199        int pos = expandedHelpKey.indexOf('_');
200        if (pos == -1) {
201            expandedHelpKey = expandedHelpKey + ".shtml";
202        } else {
203            expandedHelpKey = expandedHelpKey.substring(0, pos) + ".shtml"
204                    + "#" + expandedHelpKey.substring(pos+1);
205        }
206        String contents = template.replace("<!--HELP_KEY-->", htmlLocation + "index.html#" + helpKey);
207        contents = contents.replace("<!--URL_HELP_KEY-->", expandedHelpKey);
208
209        PrintWriter printWriter = new PrintWriter(stubLocation + "stub.html");
210        printWriter.print(contents);
211        printWriter.close();
212        return stubLocation + "stub.html";
213    }
214
215    public static void openWindowsFile(File file) throws JmriException {
216        try {
217            if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
218                Desktop.getDesktop().open(file);
219            } else {
220                throw new JmriException("Failed to connect to browser. java.awt.Desktop in Windows doesn't support Action.OPEN");
221            }
222        } catch (IOException ex) {
223            throw new JmriException(
224                    String.format("Failed to connect to browser. Error loading help file %s", file.getName()), ex);
225        }
226    }
227
228    public static boolean showWebPage(String ref, String url) {
229        boolean result = false;
230        try {
231            jmri.util.HelpUtil.openWebPage(url);
232            result = true;
233        } catch (JmriException e) {
234            log.warn("unable to show help page {} due to:", ref, e);
235        }
236        return result;
237    }
238
239    public static void openWebPage(String url) throws JmriException {
240        try {
241            URI uri = new URI(url);
242            if (!url.toLowerCase().startsWith("file://")) {
243                HttpURLConnection request = (HttpURLConnection) uri.toURL().openConnection();
244                request.setRequestMethod("GET");
245                request.connect();
246                if (request.getResponseCode() != 200) {
247                    throw new JmriException(String.format("Failed to connect to web page: %d, %s",
248                            request.getResponseCode(), request.getResponseMessage()));
249                }
250            }
251            if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
252                // Open browser to URL with draft report
253                Desktop.getDesktop().browse(uri);
254            } else {
255                throw new JmriException("Failed to connect to web page. java.awt.Desktop doesn't suppport Action.BROWSE");
256            }
257        } catch (IOException | URISyntaxException e) {
258            throw new JmriException(
259                    String.format("Failed to connect to web page. Exception thrown: %s", e.getMessage()), e);
260        }
261    }
262
263    public static Action getHelpAction(final String name, final Icon icon, final String id) {
264        return new AbstractAction(name, icon) {
265            @Override
266            public void actionPerformed(ActionEvent event) {
267                displayHelpRef(id);
268            }
269        };
270    }
271
272    // initialize logging
273    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HelpUtil.class);
274
275    public interface MenuProvider {
276
277        /**
278         * Get the menu items to include in the menu. Any menu item that is null will be
279         * replaced with a separator.
280         *
281         * @return the list of menu items
282         */
283        @Nonnull
284        List<JMenuItem> getHelpMenuItems();
285
286    }
287}