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}