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