1 /******************************************************************************* 2 * Copyright (c) 2020 1C-Soft LLC and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * George Suaridze (1C-Soft LLC) - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.ui.internal.cheatsheets.views; 15 16 import java.io.ByteArrayOutputStream; 17 import java.io.UnsupportedEncodingException; 18 import java.net.MalformedURLException; 19 import java.net.URL; 20 import java.net.URLDecoder; 21 import java.nio.charset.StandardCharsets; 22 import java.text.MessageFormat; 23 import java.util.Properties; 24 25 import org.eclipse.core.commands.ParameterizedCommand; 26 import org.eclipse.core.commands.common.CommandException; 27 import org.eclipse.jface.dialogs.MessageDialog; 28 import org.eclipse.swt.custom.BusyIndicator; 29 import org.eclipse.swt.widgets.Display; 30 import org.eclipse.ui.IWorkbench; 31 import org.eclipse.ui.IWorkbenchPage; 32 import org.eclipse.ui.IWorkbenchWindow; 33 import org.eclipse.ui.PartInitException; 34 import org.eclipse.ui.PlatformUI; 35 import org.eclipse.ui.browser.IWorkbenchBrowserSupport; 36 import org.eclipse.ui.commands.ICommandService; 37 import org.eclipse.ui.handlers.IHandlerService; 38 import org.eclipse.ui.internal.cheatsheets.CheatSheetPlugin; 39 import org.eclipse.ui.internal.cheatsheets.Messages; 40 41 /** 42 * A factory that knows how to create cheatsheet URL actions. 43 * <p> 44 * A cheatsheet URL is a valid http url, with org.eclipse.ui.cheatsheet as a 45 * host. 46 * </p> 47 * <p> 48 * A cheatsheet url instance is created by parsing the url and retrieving the 49 * embedded "command" and parameters. For example, the following urls are valid 50 * cheatsheet urls: 51 * <p> 52 * 53 * <pre> 54 * http://org.eclipse.ui.cheatsheet/showView?id=org.eclipse.pde.runtime.LogView 55 * http://org.eclipse.ui.cheatsheet/execute?command=org.eclipse.ui.newWizard%28newWizardId%3Dorg.eclipse.ui.wizards.new.project%29" 56 * </pre> 57 * <p> 58 * When parsed, the first url has "showView" as a command, and "id" parameter. 59 * While the second "execute" as a command and "command" as parameter with 60 * "newWizardId" as command parameter. 61 * </p> 62 * <p> 63 * For now it supports two commands: 64 * <li>showView - to activate given view by its id</li> 65 * <li>execute - to execute eclipse command</li> 66 */ 67 public class CheatSheetHyperlinkActionFactory { 68 69 private static final String CHEAT_SHEET_PROTOCOL = "http"; //$NON-NLS-1$ 70 private static final String CHEAT_SHEET_HOST_ID = "org.eclipse.ui.cheatsheet"; //$NON-NLS-1$ 71 72 private static final String EXECUTE = "execute"; //$NON-NLS-1$ 73 private static final String SHOW_VIEW = "showView"; //$NON-NLS-1$ 74 75 private static final String KEY_ID = "id"; //$NON-NLS-1$ 76 private static final String KEY_COMAND = "command"; //$NON-NLS-1$ 77 private static final String KEY_DECODE = "decode"; //$NON-NLS-1$ 78 79 private static final String VALUE_TRUE = "true"; //$NON-NLS-1$ 80 81 /** 82 * Creates {@link CheatSheetHyperlinkAction} for given url string 83 * 84 * @param urlString 85 * - url string representation, cannot be {@code null} 86 * @return appropriate cheatsheet action {@link CheatSheetHyperlinkAction} 87 */ create(String urlString)88 public CheatSheetHyperlinkAction create(String urlString) { 89 if (urlString == null) { 90 return new FallbackAction(urlString); 91 } 92 try { 93 URL url = new URL(urlString); 94 if (isCheatSheetHyperlink(url)) { 95 String action = getPathAsAction(url); 96 Properties parameters = getQueryParameters(url); 97 switch (action) { 98 case EXECUTE: 99 return new CommandAction(getParameter(parameters, KEY_COMAND)); 100 case SHOW_VIEW: 101 return new ShowViewAction(getParameter(parameters, KEY_ID)); 102 default: 103 CheatSheetPlugin.getPlugin().getLog().error("Unsupported action: " + action, null); //$NON-NLS-1$ 104 } 105 return new FallbackAction(urlString); 106 } else if (url.getProtocol() != null) { 107 return new OpenInBrowserAction(url); 108 } else { 109 return new FallbackAction(urlString); 110 } 111 } catch (MalformedURLException e) { 112 CheatSheetPlugin.getPlugin().getLog().error("Malformed URL: " + urlString, e); //$NON-NLS-1$ 113 return new FallbackAction(urlString); 114 } 115 } 116 isCheatSheetHyperlink(URL url)117 private boolean isCheatSheetHyperlink(URL url) { 118 if (!url.getProtocol().equalsIgnoreCase(CHEAT_SHEET_PROTOCOL)) { 119 return false; 120 } 121 if (url.getHost().equalsIgnoreCase(CHEAT_SHEET_HOST_ID)) { 122 return true; 123 } 124 return false; 125 } 126 getQueryParameters(URL url)127 private Properties getQueryParameters(URL url) { 128 Properties properties = new Properties(); 129 String query = url.getQuery(); 130 if (query == null) { 131 return properties; 132 } 133 String[] params = query.split("&"); //$NON-NLS-1$ 134 for (int i = 0; i < params.length; i++) { 135 String[] keyValuePair = params[i].split("="); //$NON-NLS-1$ 136 if (keyValuePair.length != 2) { 137 CheatSheetPlugin.getPlugin().getLog() 138 .warn(MessageFormat.format("Ignoring the following Cheatsheet URL parameter: {0}", params[i])); //$NON-NLS-1$ 139 continue; 140 } 141 142 String key = urlDecode(keyValuePair[0]); 143 if (key == null) { 144 CheatSheetPlugin.getPlugin().getLog() 145 .warn(MessageFormat.format("Failed to URL decode key: {0}", keyValuePair[0])); //$NON-NLS-1$ 146 continue; 147 } 148 149 String value = urlDecode(keyValuePair[1]); 150 if (value == null) { 151 CheatSheetPlugin.getPlugin().getLog() 152 .warn(MessageFormat.format("Failed to URL decode value: {0}", keyValuePair[1])); //$NON-NLS-1$ 153 continue; 154 } 155 156 properties.setProperty(key, value); 157 } 158 return properties; 159 } 160 urlDecode(String encodedURL)161 private String urlDecode(String encodedURL) { 162 int len = encodedURL.length(); 163 ByteArrayOutputStream os = new ByteArrayOutputStream(len); 164 165 for (int i = 0; i < len;) { 166 switch (encodedURL.charAt(i)) { 167 case '%': 168 if (len >= i + 3) { 169 os.write(Integer.parseInt(encodedURL.substring(i + 1, i + 3), 16)); 170 } 171 i += 3; 172 break; 173 case '+': // exception from standard 174 os.write(' '); 175 i++; 176 break; 177 default: 178 os.write(encodedURL.charAt(i++)); 179 break; 180 } 181 } 182 return new String(os.toByteArray(), StandardCharsets.UTF_8); 183 } 184 getPathAsAction(URL url)185 private String getPathAsAction(URL url) { 186 String action = url.getPath(); 187 if (action != null) { 188 action = action.substring(1); 189 } 190 return action; 191 } 192 getParameter(Properties parameters, String parameterId)193 private String getParameter(Properties parameters, String parameterId) { 194 String value = parameters.getProperty(parameterId); 195 String decode = parameters.getProperty(KEY_DECODE); 196 197 if (value != null) { 198 try { 199 if (decode != null && decode.equalsIgnoreCase(VALUE_TRUE)) { 200 return decode(value, "UTF-8"); //$NON-NLS-1$ 201 } 202 return value; 203 } catch (Exception e) { 204 CheatSheetPlugin.getPlugin().getLog().error("Failed to decode URL: " + parameterId, e); //$NON-NLS-1$ 205 } 206 } 207 return value; 208 } 209 decode(String s, String enc)210 private String decode(String s, String enc) throws UnsupportedEncodingException { 211 try { 212 return URLDecoder.decode(s, enc); 213 } catch (Exception ex) { 214 return s; 215 } 216 } 217 218 public static abstract class CheatSheetHyperlinkAction { 219 220 /** 221 * Executes action. 222 */ execute()223 public final void execute() { 224 Display display = Display.getDefault(); 225 BusyIndicator.showWhile(display, () -> { 226 doExecute(display); 227 }); 228 229 } 230 doExecute(Display display)231 protected abstract void doExecute(Display display); 232 } 233 234 private static class FallbackAction extends CheatSheetHyperlinkAction { 235 236 private final String url; 237 FallbackAction(String url)238 public FallbackAction(String url) { 239 this.url = url; 240 } 241 242 @Override doExecute(Display display)243 public void doExecute(Display display) { 244 MessageDialog.openInformation(display.getActiveShell(), 245 null, 246 MessageFormat.format(Messages.CHEAT_SHEET_UNSUPPORTED_LINK_ACTIVATION_MESSAGE, url)); 247 } 248 } 249 250 private static class OpenInBrowserAction extends CheatSheetHyperlinkAction { 251 252 private final URL url; 253 OpenInBrowserAction(URL url)254 public OpenInBrowserAction(URL url) { 255 this.url = url; 256 } 257 258 @Override doExecute(Display display)259 public void doExecute(Display display) { 260 try { 261 IWorkbenchBrowserSupport support = PlatformUI.getWorkbench().getBrowserSupport(); 262 support.getExternalBrowser().openURL(url); 263 } catch (PartInitException e) { 264 CheatSheetPlugin.getPlugin().getLog().error("Cheatsheet failed to get Browser support.", e); //$NON-NLS-1$ 265 } 266 } 267 } 268 269 private static class ShowViewAction extends CheatSheetHyperlinkAction { 270 271 private final String viewId; 272 ShowViewAction(String viewId)273 public ShowViewAction(String viewId) { 274 this.viewId = viewId; 275 } 276 277 @Override doExecute(Display display)278 protected void doExecute(Display display) { 279 IWorkbench workbench = PlatformUI.getWorkbench(); 280 IWorkbenchWindow activeWorkbenchWindow = workbench.getActiveWorkbenchWindow(); 281 if (activeWorkbenchWindow != null) { 282 try { 283 activeWorkbenchWindow.getActivePage().showView(viewId, null, IWorkbenchPage.VIEW_ACTIVATE); 284 } catch (PartInitException e) { 285 CheatSheetPlugin.getPlugin().getLog().error("Error while activating view: " + viewId, e); //$NON-NLS-1$ 286 } 287 } 288 } 289 } 290 291 private static class CommandAction extends CheatSheetHyperlinkAction { 292 293 private final String command; 294 CommandAction(String command)295 public CommandAction(String command) { 296 this.command = command; 297 } 298 299 @Override doExecute(Display display)300 protected void doExecute(Display display) { 301 ICommandService commandService = PlatformUI.getWorkbench().getService(ICommandService.class); 302 IHandlerService handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class); 303 if (commandService == null || handlerService == null) { 304 CheatSheetPlugin.getPlugin().getLog().error( 305 "Could not get ICommandService or IHandlerService while trying to execute: " + command, null); //$NON-NLS-1$ 306 return; 307 } 308 try { 309 ParameterizedCommand pCommand = commandService.deserialize(command); 310 handlerService.executeCommand(pCommand, null); 311 } catch (CommandException e) { 312 CheatSheetPlugin.getPlugin().getLog().error("Could not execute command: " + command, e); //$NON-NLS-1$ 313 } 314 } 315 } 316 } 317