Class | Line # | Actions | |||||
---|---|---|---|---|---|---|---|
WebJarsScriptService | 58 | 65 | 0% | 26 | 13 |
1 | /* | |
2 | * See the NOTICE file distributed with this work for additional | |
3 | * information regarding copyright ownership. | |
4 | * | |
5 | * This is free software; you can redistribute it and/or modify it | |
6 | * under the terms of the GNU Lesser General Public License as | |
7 | * published by the Free Software Foundation; either version 2.1 of | |
8 | * the License, or (at your option) any later version. | |
9 | * | |
10 | * This software is distributed in the hope that it will be useful, | |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 | * Lesser General Public License for more details. | |
14 | * | |
15 | * You should have received a copy of the GNU Lesser General Public | |
16 | * License along with this software; if not, write to the Free | |
17 | * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA | |
18 | * 02110-1301 USA, or see the FSF site: http://www.fsf.org. | |
19 | */ | |
20 | package org.xwiki.webjars.script; | |
21 | ||
22 | import java.util.ArrayList; | |
23 | import java.util.Arrays; | |
24 | import java.util.Collections; | |
25 | import java.util.LinkedHashMap; | |
26 | import java.util.List; | |
27 | import java.util.Map; | |
28 | ||
29 | import javax.inject.Inject; | |
30 | import javax.inject.Named; | |
31 | import javax.inject.Singleton; | |
32 | ||
33 | import org.apache.commons.lang3.StringUtils; | |
34 | import org.apache.commons.lang3.exception.ExceptionUtils; | |
35 | import org.slf4j.Logger; | |
36 | import org.xwiki.component.annotation.Component; | |
37 | import org.xwiki.extension.Extension; | |
38 | import org.xwiki.extension.repository.CoreExtensionRepository; | |
39 | import org.xwiki.extension.repository.InstalledExtensionRepository; | |
40 | import org.xwiki.resource.ResourceReference; | |
41 | import org.xwiki.resource.ResourceReferenceSerializer; | |
42 | import org.xwiki.resource.SerializeResourceReferenceException; | |
43 | import org.xwiki.resource.UnsupportedResourceReferenceException; | |
44 | import org.xwiki.script.service.ScriptService; | |
45 | import org.xwiki.url.ExtendedURL; | |
46 | import org.xwiki.webjars.internal.WebJarsResourceReference; | |
47 | import org.xwiki.wiki.descriptor.WikiDescriptorManager; | |
48 | ||
49 | /** | |
50 | * Make it easy to use WebJars in scripts. For example it can compute an XWiki WebJars URL. | |
51 | * | |
52 | * @version $Id: 3bc56e019280ffef28aeca37cc9ed50e0cf99237 $ | |
53 | * @since 6.0M1 | |
54 | */ | |
55 | @Component | |
56 | @Named("webjars") | |
57 | @Singleton | |
58 | public class WebJarsScriptService implements ScriptService | |
59 | { | |
60 | private static final String RESOURCE_SEPARATOR = "/"; | |
61 | ||
62 | /** | |
63 | * The name of the parameter that specifies the WebJar version. | |
64 | */ | |
65 | private static final String VERSION = "version"; | |
66 | ||
67 | /** | |
68 | * The name of the parameter that specifies the wiki in which the resource is located. If not specified then | |
69 | * the wiki used will be the current wiki. | |
70 | */ | |
71 | private static final String WIKI = "wiki"; | |
72 | ||
73 | /** | |
74 | * The default {@code groupId} for Maven projects that produce WebJars. | |
75 | */ | |
76 | private static final String DEFAULT_WEBJAR_GROUP_ID = "org.webjars"; | |
77 | ||
78 | @Inject | |
79 | private Logger logger; | |
80 | ||
81 | /** | |
82 | * Used to check if the WebJar is a core extension and to get its version. | |
83 | */ | |
84 | @Inject | |
85 | private CoreExtensionRepository coreExtensionRepository; | |
86 | ||
87 | /** | |
88 | * Used to check if the WebJar is an installed extension and to get its version. | |
89 | */ | |
90 | @Inject | |
91 | private InstalledExtensionRepository installedExtensionRepository; | |
92 | ||
93 | /** | |
94 | * Used to get the id of the current wiki in order to determine the current extension namespace. | |
95 | */ | |
96 | @Inject | |
97 | private WikiDescriptorManager wikiDescriptorManager; | |
98 | ||
99 | @Inject | |
100 | private ResourceReferenceSerializer<ResourceReference, ExtendedURL> defaultResourceReferenceSerializer; | |
101 | ||
102 | /** | |
103 | * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the current wiki. | |
104 | * | |
105 | * @param resourceName the resource asked using the format {@code <webjarId>/<version>/<path/to/resource>} | |
106 | * (e.g. {@code angular/2.1.11/angular.js"}) | |
107 | * @return the computed URL | |
108 | */ | |
109 | 1066 | public String url(String resourceName) |
110 | { | |
111 | 1066 | if (StringUtils.isEmpty(resourceName)) { |
112 | 0 | return null; |
113 | } | |
114 | ||
115 | 1066 | String[] parts = resourceName.split(RESOURCE_SEPARATOR, 3); |
116 | 1066 | if (parts.length < 3) { |
117 | 0 | logger.warn("Invalid webjar resource name [{}]. Expected format is 'webjarId/version/path'", resourceName); |
118 | 0 | return null; |
119 | } | |
120 | ||
121 | // Prefix the webjarId with a fake groupId just to make sure that the colon character (:) is not interpreted as | |
122 | // separator in the webjarId. This is required in order to ensure that the behavior of this method doesn't | |
123 | // change. Note that the groupdId is ignored if the WebJar version is specified so the fake groupId won't have | |
124 | // any effect. | |
125 | 1066 | return url("fakeGroupId:" + parts[0], null, parts[2], Collections.singletonMap(VERSION, parts[1])); |
126 | } | |
127 | ||
128 | /** | |
129 | * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the current wiki. | |
130 | * | |
131 | * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is | |
132 | * {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the | |
133 | * {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular} | |
134 | * translates to {@code org.webjars:angular}) | |
135 | * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just | |
136 | * {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js}) | |
137 | * @return the URL to load the WebJar resource (relative to the context path of the web application) | |
138 | */ | |
139 | 23052 | public String url(String webjarId, String path) |
140 | { | |
141 | 23052 | return url(webjarId, null, path, null); |
142 | } | |
143 | ||
144 | /** | |
145 | * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the passed namespace. | |
146 | * | |
147 | * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is | |
148 | * {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the | |
149 | * {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular} | |
150 | * translates to {@code org.webjars:angular}) | |
151 | * @param namespace the namespace in which the webjars resources will be loaded from (e.g. for a wiki namespace you | |
152 | * should use the format {@code wiki:<wikiId>}). If null then defaults to the current wiki | |
153 | * namespace. And if the passed namespace doesn't exist, falls back to the main wiki namespace | |
154 | * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just | |
155 | * {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js}) | |
156 | * @return the URL to load the WebJar resource (relative to the context path of the web application) | |
157 | * @since 8.1M2 | |
158 | */ | |
159 | 0 | public String url(String webjarId, String namespace, String path) |
160 | { | |
161 | 0 | return url(webjarId, namespace, path, null); |
162 | } | |
163 | ||
164 | /** | |
165 | * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar. | |
166 | * | |
167 | * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is | |
168 | * {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the | |
169 | * {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular} | |
170 | * translates to {@code org.webjars:angular}) | |
171 | * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just | |
172 | * {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js}) | |
173 | * @param params additional query string parameters to add to the returned URL; there are two known (reserved) | |
174 | * parameters: {@code version} (the WebJar version) and {@code evaluate} (a boolean parameter that | |
175 | * specifies if the requested resource has Velocity code that needs to be evaluated); besides these you | |
176 | * can pass whatever parameters you like (they will be taken into account or not depending on the | |
177 | * resource) | |
178 | * @return the URL to load the WebJar resource (relative to the context path of the web application) | |
179 | */ | |
180 | 1603 | public String url(String webjarId, String path, Map<String, ?> params) |
181 | { | |
182 | // For backward-compatibility reasons, we still support passing the target wiki in parameters | |
183 | 1603 | String namespace = null; |
184 | 1603 | if (params != null) { |
185 | // For backward-compatibility reasons we still support passing the target wiki in parameters | |
186 | 1603 | String wikiId = (String) params.get(WIKI); |
187 | 1603 | if (!StringUtils.isEmpty(wikiId)) { |
188 | 2 | namespace = constructNamespace(wikiId); |
189 | } | |
190 | } | |
191 | ||
192 | 1603 | return url(webjarId, namespace, path, params); |
193 | } | |
194 | ||
195 | /** | |
196 | * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the passed namespace. | |
197 | * | |
198 | * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is | |
199 | * {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the | |
200 | * {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular} | |
201 | * translates to {@code org.webjars:angular}) | |
202 | * @param namespace the namespace in which the webjars resources will be loaded from (e.g. for a wiki namespace you | |
203 | * should use the format {@code wiki:<wikiId>}). If null then defaults to the current wiki | |
204 | * namespace. And if the passed namespace doesn't exist, falls back to the main wiki namespace | |
205 | * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just | |
206 | * {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js}) | |
207 | * @param params additional query string parameters to add to the returned URL; there are two known (reserved) | |
208 | * parameters: {@code version} (the WebJar version) and {@code evaluate} (a boolean parameter that | |
209 | * specifies if the requested resource has Velocity code that needs to be evaluated); besides these you | |
210 | * can pass whatever parameters you like (they will be taken into account or not depending on the | |
211 | * resource) | |
212 | * @return the URL to load the WebJar resource (relative to the context path of the web application) | |
213 | * @since 8.1M2 | |
214 | */ | |
215 | 25721 | public String url(String webjarId, String namespace, String path, Map<String, ?> params) |
216 | { | |
217 | 25722 | if (StringUtils.isEmpty(webjarId)) { |
218 | 0 | return null; |
219 | } | |
220 | ||
221 | 25722 | String groupId = DEFAULT_WEBJAR_GROUP_ID; |
222 | 25722 | String artifactId = webjarId; |
223 | 25722 | int groupSeparatorPosition = webjarId.indexOf(':'); |
224 | 25722 | if (groupSeparatorPosition >= 0) { |
225 | // A different group id. | |
226 | 14891 | groupId = webjarId.substring(0, groupSeparatorPosition); |
227 | 14891 | artifactId = webjarId.substring(groupSeparatorPosition + 1); |
228 | } | |
229 | ||
230 | 25722 | Map<String, Object> urlParams = new LinkedHashMap<>(); |
231 | 25721 | if (params != null) { |
232 | 2670 | urlParams.putAll(params); |
233 | } | |
234 | ||
235 | // For backward-compatibility reasons we still support passing the target wiki in parameters. However we need | |
236 | // to remove it from the params if that's the case since we don't want to output a URL with the wiki id in the | |
237 | // query string (since the namespace is now part of the URL). | |
238 | 25721 | urlParams.remove(WIKI); |
239 | ||
240 | 25721 | Object version = urlParams.remove(VERSION); |
241 | 25722 | if (version == null) { |
242 | // Try to determine the version based on the extensions that are currently installed or provided by default. | |
243 | 24654 | version = getVersion(String.format("%s:%s", groupId, artifactId), namespace); |
244 | } | |
245 | ||
246 | // Construct a WebJarsResourceReference so that we can serialize it! | |
247 | 25722 | WebJarsResourceReference resourceReference = |
248 | getResourceReference(artifactId, version, namespace, path, urlParams); | |
249 | ||
250 | 25722 | ExtendedURL extendedURL; |
251 | 25722 | try { |
252 | 25722 | extendedURL = this.defaultResourceReferenceSerializer.serialize(resourceReference); |
253 | } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException e) { | |
254 | 0 | this.logger.warn("Error while serializing WebJar URL for id [{}], path = [{}]. Root cause = [{}]", |
255 | webjarId, path, ExceptionUtils.getRootCauseMessage(e)); | |
256 | 0 | return null; |
257 | } | |
258 | ||
259 | 25721 | return extendedURL.serialize(); |
260 | } | |
261 | ||
262 | 25722 | private WebJarsResourceReference getResourceReference(String artifactId, Object version, String namespace, |
263 | String path, Map<String, Object> urlParams) | |
264 | { | |
265 | 25721 | List<String> segments = new ArrayList<>(); |
266 | 25722 | segments.add(artifactId); |
267 | // Don't include the version if it's not specified and there's no installed/core extension that matches the | |
268 | // given WebJar id. | |
269 | 25722 | if (version != null) { |
270 | 25430 | segments.add((String) version); |
271 | } | |
272 | 25721 | segments.addAll(Arrays.asList(path.split(RESOURCE_SEPARATOR))); |
273 | ||
274 | // When a JavaScript resource is loaded using RequireJS the resource URL must not include the ".js" suffix (by | |
275 | // default) if the URL is relative and doesn't have a query string. Before XWIKI-10881 (Introduce a proper | |
276 | // "webjars" type instead of reusing the "bin" type) all WebJar URLs had a query string (the resource path) so | |
277 | // we were forced to specify the ".js" suffix when using RequireJS. The resource path is currently no longer | |
278 | // part of the query string and thus the ".js" suffix must now be omitted (otherwise RequireJS will ask for | |
279 | // ".js.js"), unless the resource has parameters (e.g. the resource is evaluated). In order to preserve | |
280 | // backwards compatibility with existing extensions and also in order to fix this mess (the developer doesn't | |
281 | // know when to put the ".js" suffix and when not) we have decided to add a fake query string if the ".js" | |
282 | // suffix is specified and there is no query string (thus preventing RequireJS from requesting ".js.js"). | |
283 | 25722 | if (path.endsWith(".js") && urlParams.isEmpty()) { |
284 | 7445 | urlParams.put("r", "1"); |
285 | } | |
286 | ||
287 | 25721 | WebJarsResourceReference resourceReference = |
288 | new WebJarsResourceReference(resolveNamespace(namespace), segments); | |
289 | 25720 | for (Map.Entry<String, Object> parameterEntry : urlParams.entrySet()) { |
290 | 9046 | resourceReference.addParameter(parameterEntry.getKey(), parameterEntry.getValue()); |
291 | } | |
292 | ||
293 | 25722 | return resourceReference; |
294 | } | |
295 | ||
296 | 24655 | private String getVersion(String extensionId, String namespace) |
297 | { | |
298 | // Look for WebJars that are core extensions. | |
299 | 24654 | Extension extension = this.coreExtensionRepository.getCoreExtension(extensionId); |
300 | 24655 | if (extension == null) { |
301 | // Look for WebJars that are installed on the passed namespace (if defined), the current wiki or the main | |
302 | // wiki. | |
303 | 296 | String selectedNamespace = resolveNamespace(namespace); |
304 | 296 | extension = this.installedExtensionRepository.getInstalledExtension(extensionId, selectedNamespace); |
305 | 296 | if (extension == null) { |
306 | // Fallback by looking in the main wiki | |
307 | 292 | selectedNamespace = constructNamespace(this.wikiDescriptorManager.getMainWikiId()); |
308 | 292 | extension = this.installedExtensionRepository.getInstalledExtension(extensionId, selectedNamespace); |
309 | 292 | if (extension == null) { |
310 | 292 | return null; |
311 | } | |
312 | } | |
313 | } | |
314 | 24362 | return extension.getId().getVersion().getValue(); |
315 | } | |
316 | ||
317 | 26017 | private String resolveNamespace(String namespace) |
318 | { | |
319 | 26016 | String resolvedNamespace; |
320 | 26018 | if (StringUtils.isNotEmpty(namespace)) { |
321 | 5 | resolvedNamespace = namespace; |
322 | } else { | |
323 | 26013 | resolvedNamespace = constructNamespace(getCurrentWikiId()); |
324 | } | |
325 | 26018 | return resolvedNamespace; |
326 | } | |
327 | ||
328 | 26011 | private String getCurrentWikiId() |
329 | { | |
330 | 26012 | return this.wikiDescriptorManager.getCurrentWikiId(); |
331 | } | |
332 | ||
333 | 26307 | private String constructNamespace(String wikiId) |
334 | { | |
335 | 26307 | return String.format("wiki:%s", wikiId); |
336 | } | |
337 | } |