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}