org.xwiki.webjars.script.WebJarsScriptService.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.webjars.script.WebJarsScriptService.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.webjars.script;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.extension.Extension;
import org.xwiki.extension.repository.CoreExtensionRepository;
import org.xwiki.extension.repository.InstalledExtensionRepository;
import org.xwiki.resource.ResourceReference;
import org.xwiki.resource.ResourceReferenceSerializer;
import org.xwiki.resource.SerializeResourceReferenceException;
import org.xwiki.resource.UnsupportedResourceReferenceException;
import org.xwiki.script.service.ScriptService;
import org.xwiki.url.ExtendedURL;
import org.xwiki.webjars.internal.WebJarsResourceReference;
import org.xwiki.wiki.descriptor.WikiDescriptorManager;

/**
 * Make it easy to use WebJars in scripts. For example it can compute an XWiki WebJars URL.
 * 
 * @version $Id: 3bc56e019280ffef28aeca37cc9ed50e0cf99237 $
 * @since 6.0M1
 */
@Component
@Named("webjars")
@Singleton
public class WebJarsScriptService implements ScriptService {
    private static final String RESOURCE_SEPARATOR = "/";

    /**
     * The name of the parameter that specifies the WebJar version.
     */
    private static final String VERSION = "version";

    /**
     * The name of the parameter that specifies the wiki in which the resource is located. If not specified then
     * the wiki used will be the current wiki.
     */
    private static final String WIKI = "wiki";

    /**
     * The default {@code groupId} for Maven projects that produce WebJars.
     */
    private static final String DEFAULT_WEBJAR_GROUP_ID = "org.webjars";

    @Inject
    private Logger logger;

    /**
     * Used to check if the WebJar is a core extension and to get its version.
     */
    @Inject
    private CoreExtensionRepository coreExtensionRepository;

    /**
     * Used to check if the WebJar is an installed extension and to get its version.
     */
    @Inject
    private InstalledExtensionRepository installedExtensionRepository;

    /**
     * Used to get the id of the current wiki in order to determine the current extension namespace.
     */
    @Inject
    private WikiDescriptorManager wikiDescriptorManager;

    @Inject
    private ResourceReferenceSerializer<ResourceReference, ExtendedURL> defaultResourceReferenceSerializer;

    /**
     * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the current wiki.
     *
     * @param resourceName the resource asked using the format {@code <webjarId>/<version>/<path/to/resource>}
     *                     (e.g. {@code angular/2.1.11/angular.js"})
     * @return the computed URL
     */
    public String url(String resourceName) {
        if (StringUtils.isEmpty(resourceName)) {
            return null;
        }

        String[] parts = resourceName.split(RESOURCE_SEPARATOR, 3);
        if (parts.length < 3) {
            logger.warn("Invalid webjar resource name [{}]. Expected format is 'webjarId/version/path'",
                    resourceName);
            return null;
        }

        // Prefix the webjarId with a fake groupId just to make sure that the colon character (:) is not interpreted as
        // separator in the webjarId. This is required in order to ensure that the behavior of this method doesn't
        // change. Note that the groupdId is ignored if the WebJar version is specified so the fake groupId won't have
        // any effect.
        return url("fakeGroupId:" + parts[0], null, parts[2], Collections.singletonMap(VERSION, parts[1]));
    }

    /**
     * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the current wiki.
     *
     * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is
     *            {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the
     *            {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular}
     *            translates to {@code org.webjars:angular})
     * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just
     *            {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js})
     * @return the URL to load the WebJar resource (relative to the context path of the web application)
     */
    public String url(String webjarId, String path) {
        return url(webjarId, null, path, null);
    }

    /**
     * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the passed namespace.
     * 
     * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is
     *            {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the
     *            {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular}
     *            translates to {@code org.webjars:angular})
     * @param namespace the namespace in which the webjars resources will be loaded from (e.g. for a wiki namespace you
     *                  should use the format {@code wiki:<wikiId>}). If null then defaults to the current wiki
     *                  namespace. And if the passed namespace doesn't exist, falls back to the main wiki namespace
     * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just
     *            {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js})
     * @return the URL to load the WebJar resource (relative to the context path of the web application)
     * @since 8.1M2
     */
    public String url(String webjarId, String namespace, String path) {
        return url(webjarId, namespace, path, null);
    }

    /**
     * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar.
     * 
     * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is
     *            {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the
     *            {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular}
     *            translates to {@code org.webjars:angular})
     * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just
     *            {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js})
     * @param params additional query string parameters to add to the returned URL; there are two known (reserved)
     *            parameters: {@code version} (the WebJar version) and {@code evaluate} (a boolean parameter that
     *            specifies if the requested resource has Velocity code that needs to be evaluated); besides these you
     *            can pass whatever parameters you like (they will be taken into account or not depending on the
     *            resource)
     * @return the URL to load the WebJar resource (relative to the context path of the web application)
     */
    public String url(String webjarId, String path, Map<String, ?> params) {
        // For backward-compatibility reasons, we still support passing the target wiki in parameters
        String namespace = null;
        if (params != null) {
            // For backward-compatibility reasons we still support passing the target wiki in parameters
            String wikiId = (String) params.get(WIKI);
            if (!StringUtils.isEmpty(wikiId)) {
                namespace = constructNamespace(wikiId);
            }
        }

        return url(webjarId, namespace, path, params);
    }

    /**
     * Creates an URL that can be used to load a resource (JavaScript, CSS, etc.) from a WebJar in the passed namespace.
     *
     * @param webjarId the id of the WebJar that contains the resource; the format of the WebJar id is
     *            {@code groupId:artifactId} (e.g. {@code org.xwiki.platform:xwiki-platform-job-webjar}), where the
     *            {@code groupId} can be omitted if it is {@link #DEFAULT_WEBJAR_GROUP_ID} (i.e. {@code angular}
     *            translates to {@code org.webjars:angular})
     * @param namespace the namespace in which the webjars resources will be loaded from (e.g. for a wiki namespace you
     *                  should use the format {@code wiki:<wikiId>}). If null then defaults to the current wiki
     *                  namespace. And if the passed namespace doesn't exist, falls back to the main wiki namespace
     * @param path the path within the WebJar, starting from the version folder (e.g. you should pass just
     *            {@code angular.js} if the actual path is {@code META-INF/resources/webjars/angular/2.1.11/angular.js})
     * @param params additional query string parameters to add to the returned URL; there are two known (reserved)
     *            parameters: {@code version} (the WebJar version) and {@code evaluate} (a boolean parameter that
     *            specifies if the requested resource has Velocity code that needs to be evaluated); besides these you
     *            can pass whatever parameters you like (they will be taken into account or not depending on the
     *            resource)
     * @return the URL to load the WebJar resource (relative to the context path of the web application)
     * @since 8.1M2
     */
    public String url(String webjarId, String namespace, String path, Map<String, ?> params) {
        if (StringUtils.isEmpty(webjarId)) {
            return null;
        }

        String groupId = DEFAULT_WEBJAR_GROUP_ID;
        String artifactId = webjarId;
        int groupSeparatorPosition = webjarId.indexOf(':');
        if (groupSeparatorPosition >= 0) {
            // A different group id.
            groupId = webjarId.substring(0, groupSeparatorPosition);
            artifactId = webjarId.substring(groupSeparatorPosition + 1);
        }

        Map<String, Object> urlParams = new LinkedHashMap<>();
        if (params != null) {
            urlParams.putAll(params);
        }

        // For backward-compatibility reasons we still support passing the target wiki in parameters. However we need
        // 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
        // query string (since the namespace is now part of the URL).
        urlParams.remove(WIKI);

        Object version = urlParams.remove(VERSION);
        if (version == null) {
            // Try to determine the version based on the extensions that are currently installed or provided by default.
            version = getVersion(String.format("%s:%s", groupId, artifactId), namespace);
        }

        // Construct a WebJarsResourceReference so that we can serialize it!
        WebJarsResourceReference resourceReference = getResourceReference(artifactId, version, namespace, path,
                urlParams);

        ExtendedURL extendedURL;
        try {
            extendedURL = this.defaultResourceReferenceSerializer.serialize(resourceReference);
        } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException e) {
            this.logger.warn("Error while serializing WebJar URL for id [{}], path = [{}]. Root cause = [{}]",
                    webjarId, path, ExceptionUtils.getRootCauseMessage(e));
            return null;
        }

        return extendedURL.serialize();
    }

    private WebJarsResourceReference getResourceReference(String artifactId, Object version, String namespace,
            String path, Map<String, Object> urlParams) {
        List<String> segments = new ArrayList<>();
        segments.add(artifactId);
        // Don't include the version if it's not specified and there's no installed/core extension that matches the
        // given WebJar id.
        if (version != null) {
            segments.add((String) version);
        }
        segments.addAll(Arrays.asList(path.split(RESOURCE_SEPARATOR)));

        // When a JavaScript resource is loaded using RequireJS the resource URL must not include the ".js" suffix (by
        // default) if the URL is relative and doesn't have a query string. Before XWIKI-10881 (Introduce a proper
        // "webjars" type instead of reusing the "bin" type) all WebJar URLs had a query string (the resource path) so
        // we were forced to specify the ".js" suffix when using RequireJS. The resource path is currently no longer
        // part of the query string and thus the ".js" suffix must now be omitted (otherwise RequireJS will ask for
        // ".js.js"), unless the resource has parameters (e.g. the resource is evaluated). In order to preserve
        // backwards compatibility with existing extensions and also in order to fix this mess (the developer doesn't
        // know when to put the ".js" suffix and when not) we have decided to add a fake query string if the ".js"
        // suffix is specified and there is no query string (thus preventing RequireJS from requesting ".js.js").
        if (path.endsWith(".js") && urlParams.isEmpty()) {
            urlParams.put("r", "1");
        }

        WebJarsResourceReference resourceReference = new WebJarsResourceReference(resolveNamespace(namespace),
                segments);
        for (Map.Entry<String, Object> parameterEntry : urlParams.entrySet()) {
            resourceReference.addParameter(parameterEntry.getKey(), parameterEntry.getValue());
        }

        return resourceReference;
    }

    private String getVersion(String extensionId, String namespace) {
        // Look for WebJars that are core extensions.
        Extension extension = this.coreExtensionRepository.getCoreExtension(extensionId);
        if (extension == null) {
            // Look for WebJars that are installed on the passed namespace (if defined), the current wiki or the main
            // wiki.
            String selectedNamespace = resolveNamespace(namespace);
            extension = this.installedExtensionRepository.getInstalledExtension(extensionId, selectedNamespace);
            if (extension == null) {
                // Fallback by looking in the main wiki
                selectedNamespace = constructNamespace(this.wikiDescriptorManager.getMainWikiId());
                extension = this.installedExtensionRepository.getInstalledExtension(extensionId, selectedNamespace);
                if (extension == null) {
                    return null;
                }
            }
        }
        return extension.getId().getVersion().getValue();
    }

    private String resolveNamespace(String namespace) {
        String resolvedNamespace;
        if (StringUtils.isNotEmpty(namespace)) {
            resolvedNamespace = namespace;
        } else {
            resolvedNamespace = constructNamespace(getCurrentWikiId());
        }
        return resolvedNamespace;
    }

    private String getCurrentWikiId() {
        return this.wikiDescriptorManager.getCurrentWikiId();
    }

    private String constructNamespace(String wikiId) {
        return String.format("wiki:%s", wikiId);
    }
}