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