All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.webjars.RequireJS Maven / Gradle / Ivy

There is a newer version: 0.52
Show newest version
package org.webjars;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.*;

import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES;
import static com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.w3c.dom.Node;

import org.apache.commons.lang3.StringUtils;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;

public final class RequireJS {

    public static final String WEBJARS_MAVEN_PREFIX = "META-INF/maven/org.webjars";

    private static final Logger log = LoggerFactory.getLogger(RequireJS.class);

    private static String requireConfigJavaScript;
    private static String requireConfigJavaScriptCdn;

    private static Map requireConfigJson;
    private static Map requireConfigJsonCdn;

    /**
     * Returns the JavaScript that is used to setup the RequireJS config.
     * This value is cached in memory so that all of the processing to get the String only has to happen once.
     *
     * @param urlPrefix The URL prefix where the WebJars can be downloaded from with a trailing slash, e.g. /webJars/
     * @return The JavaScript block that can be embedded or loaded in a <script> tag
     */
    public synchronized static String getSetupJavaScript(String urlPrefix) {
        if (requireConfigJavaScript == null) {
            List prefixes = new ArrayList();
            prefixes.add(urlPrefix);

            requireConfigJavaScript = generateSetupJavaScript(prefixes);
        }
        return requireConfigJavaScript;
    }

    /**
     * Returns the JavaScript that is used to setup the RequireJS config.
     * This value is cached in memory so that all of the processing to get the String only has to happen once.
     *
     * @param urlPrefix The URL prefix where the WebJars can be downloaded from with a trailing slash, e.g. /webJars/
     * @param cdnPrefix The optional CDN prefix where the WebJars can be downloaded from
     * @return The JavaScript block that can be embedded or loaded in a <script> tag
     */
    public synchronized static String getSetupJavaScript(String cdnPrefix, String urlPrefix) {
        if (requireConfigJavaScriptCdn == null) {
            List prefixes = new ArrayList();
            prefixes.add(cdnPrefix);
            prefixes.add(urlPrefix);

            requireConfigJavaScriptCdn = generateSetupJavaScript(prefixes);
        }
        return requireConfigJavaScriptCdn;
    }

    /**
     * Returns the JavaScript that is used to setup the RequireJS config.
     * This value is not cached.
     *
     * @param prefixes A list of the prefixes to use in the `paths` part of the RequireJS config.
     * @return The JavaScript block that can be embedded or loaded in a <script> tag.
     */
    public static String generateSetupJavaScript(List prefixes) {
        Map webJars = new WebJarAssetLocator().getWebJars();

        return generateSetupJavaScript(prefixes, webJars);
    }

    /**
     * Generate the JavaScript that is used to setup the RequireJS config.
     * This value is not cached.
     * This uses nasty stuff that is really not maintainable or testable.  So this has been deprecated and the implementation will eventually be replaced with something better.
     *
     * @param prefixes A list of the prefixes to use in the `paths` part of the RequireJS config.
     * @param webJars  The WebJars (artifactId -> version) to use
     * @return The JavaScript block that can be embedded or loaded in a <script> tag.
     */
    @Deprecated
    public static String generateSetupJavaScript(List prefixes, Map webJars) {

        List> prefixesWithVersion = new ArrayList>();
        for (String prefix : prefixes) {
            prefixesWithVersion.add(new AbstractMap.SimpleEntry(prefix, true));
        }

        ObjectMapper mapper = new ObjectMapper();

        ObjectNode webJarsVersions = mapper.createObjectNode();

        StringBuilder webJarConfigsString = new StringBuilder();

        if (webJars.isEmpty()) {
            log.warn("Can't find any WebJars in the classpath, RequireJS configuration will be empty.");
        } else {
            for (Map.Entry webJar : webJars.entrySet()) {

                // assemble the WebJar versions string
                webJarsVersions.put(webJar.getKey(), webJar.getValue());

                // assemble the WebJar config string

                // default to the new pom.xml meta-data way
                ObjectNode webJarObjectNode = getWebJarSetupJson(webJar, prefixesWithVersion);
                if ((webJarObjectNode != null ? webJarObjectNode.size() : 0) != 0) {
                    webJarConfigsString.append("\n").append("requirejs.config(").append(webJarObjectNode.toString()).append(")");
                } else {
                    webJarConfigsString.append("\n").append(getWebJarConfig(webJar));
                }
            }
        }

        String webJarBasePath = "webJarId + '/' + webjars.versions[webJarId] + '/' + path";

        StringBuilder webJarPath = new StringBuilder("[");

        for (String prefix : prefixes) {
            webJarPath.append("'").append(prefix).append("' + ").append(webJarBasePath).append(",\n");
        }

        //webJarBasePath.
        webJarPath.delete(webJarPath.lastIndexOf(",\n"), webJarPath.lastIndexOf(",\n") + 2);

        webJarPath.append("]");

        return "var webjars = {\n" +
                "    versions: " + webJarsVersions.toString() + ",\n" +
                "    path: function(webJarId, path) {\n" +
                "        console.error('The webjars.path() method of getting a WebJar path has been deprecated.  The RequireJS config in the ' + webJarId + ' WebJar may need to be updated.  Please file an issue: http://github.com/webjars/' + webJarId + '/issues/new');\n" +
                "        return " + webJarPath.toString() + ";\n" +
                "    }\n" +
                "};\n" +
                "\n" +
                "var require = {\n" +
                "    callback: function() {\n" +
                "        // Deprecated WebJars RequireJS plugin loader\n" +
                "        define('webjars', function() {\n" +
                "            return {\n" +
                "                load: function(name, req, onload, config) {\n" +
                "                    if (name.indexOf('.js') >= 0) {\n" +
                "                        console.warn('Detected a legacy file name (' + name + ') as the thing to load.  Loading via file name is no longer supported so the .js will be dropped in an effort to resolve the module name instead.');\n" +
                "                        name = name.replace('.js', '');\n" +
                "                    }\n" +
                "                    console.error('The webjars plugin loader (e.g. webjars!' + name + ') has been deprecated.  The RequireJS config in the ' + name + ' WebJar may need to be updated.  Please file an issue: http://github.com/webjars/webjars/issues/new');\n" +
                "                    req([name], function() {;\n" +
                "                        onload();\n" +
                "                    });\n" +
                "                }\n" +
                "            }\n" +
                "        });\n" +
                "\n" +
                "        // All of the WebJar configs\n\n" +
                webJarConfigsString +
                "    }\n" +
                "}";
    }

    /**
     * Returns the JSON that is used to setup the RequireJS config.
     * This value is cached in memory so that all of the processing to get the JSON only has to happen once.
     *
     * @param urlPrefix The URL prefix where the WebJars can be downloaded from with a trailing slash, e.g. /webJars/
     * @return The JSON structured config
     */
    public synchronized static Map getSetupJson(String urlPrefix) {
        if (requireConfigJson == null) {

            List> prefixes = new ArrayList>();
            prefixes.add(new AbstractMap.SimpleEntry(urlPrefix, true));

            requireConfigJson = generateSetupJson(prefixes);
        }
        return requireConfigJson;
    }

    /**
     * Returns the JSON that is used to setup the RequireJS config.
     * This value is cached in memory so that all of the processing to get the JSON only has to happen once.
     *
     * @param cdnPrefix The CDN prefix where the WebJars can be downloaded from
     * @param urlPrefix The URL prefix where the WebJars can be downloaded from with a trailing slash, e.g. /webJars/
     * @return The JSON structured config
     */
    public synchronized static Map getSetupJson(String cdnPrefix, String urlPrefix) {
        if (requireConfigJsonCdn == null) {

            List> prefixes = new ArrayList>();
            prefixes.add(new AbstractMap.SimpleEntry(cdnPrefix, true));
            prefixes.add(new AbstractMap.SimpleEntry(urlPrefix, true));

            requireConfigJsonCdn = generateSetupJson(prefixes);
        }
        return requireConfigJsonCdn;
    }

    /**
     * Returns the JSON used to setup the RequireJS config for each WebJar in the CLASSPATH.
     * This value is not cached.
     *
     * @param prefixes A list of the prefixes to use in the `paths` part of the RequireJS config with a boolean flag
     *                 indicating whether or not to include the version.
     * @return The JSON structured config for each WebJar.
     */
    public static Map generateSetupJson(List> prefixes) {
        Map webJars = new WebJarAssetLocator().getWebJars();

        Map jsonConfigs = new HashMap();

        for (Map.Entry webJar : webJars.entrySet()) {
            jsonConfigs.put(webJar.getKey(), getWebJarSetupJson(webJar, prefixes));
        }

        return jsonConfigs;
    }

    private static ObjectNode getWebJarSetupJson(Map.Entry webJar, List> prefixes) {

        if (RequireJS.class.getClassLoader().getResource("META-INF/maven/org.webjars.npm/" + webJar.getKey() + "/pom.xml") != null) {
            // create the requirejs config from the package.json
            return getNpmWebJarRequireJsConfig(webJar, prefixes);
        }
        else if (RequireJS.class.getClassLoader().getResource("META-INF/maven/org.webjars.bower/" + webJar.getKey() + "/pom.xml") != null) {
            // create the requirejs config from the bower.json
            return getBowerWebJarRequireJsConfig(webJar, prefixes);
        }
        else if (RequireJS.class.getClassLoader().getResource("META-INF/maven/org.webjars/" + webJar.getKey() + "/pom.xml") != null) {
            // get the requirejs config from the pom
            return getWebJarRequireJsConfig(webJar, prefixes);
        }

        return null;
    }

    /**
     * Returns the JSON RequireJS config for a given WebJar
     *
     * @param webJar   A tuple (artifactId -> version) representing the WebJar.
     * @param prefixes A list of the prefixes to use in the `paths` part of the RequireJS config.
     * @return The JSON RequireJS config for the WebJar based on the meta-data in the WebJar's pom.xml file.
     */
    public static ObjectNode getWebJarRequireJsConfig(Map.Entry webJar, List> prefixes) {
        String rawRequireJsConfig = getRawWebJarRequireJsConfig(webJar);

        ObjectMapper mapper = new ObjectMapper()
            .configure(ALLOW_UNQUOTED_FIELD_NAMES, true)
            .configure(ALLOW_SINGLE_QUOTES, true);

        // default to just an empty object
        ObjectNode webJarRequireJsNode = mapper.createObjectNode();

        try {
            JsonNode maybeRequireJsConfig = mapper.readTree(rawRequireJsConfig);
            if (maybeRequireJsConfig.isObject()) {
                // The provided config was parseable, now lets fix the paths

                webJarRequireJsNode = (ObjectNode) maybeRequireJsConfig;


                if (webJarRequireJsNode.isObject()) {

                    // update the paths

                    ObjectNode pathsNode = (ObjectNode) webJarRequireJsNode.get("paths");

                    ObjectNode newPaths = mapper.createObjectNode();

                    if (pathsNode != null) {
                        Iterator> paths = pathsNode.fields();
                        while (paths.hasNext()) {
                            Map.Entry pathNode = paths.next();

                            String originalPath = null;

                            if (pathNode.getValue().isArray()) {
                                ArrayNode nodePaths = (ArrayNode) pathNode.getValue();
                                // lets just assume there is only 1 for now
                                originalPath = nodePaths.get(0).asText();
                            } else if (pathNode.getValue().isTextual()) {
                                TextNode nodePath = (TextNode) pathNode.getValue();
                                originalPath = nodePath.textValue();
                            }

                            if (originalPath != null) {
                                ArrayNode newPathsNode = newPaths.putArray(pathNode.getKey());
                                for (Map.Entry prefix : prefixes) {
                                    String newPath = prefix.getKey() + webJar.getKey();
                                    if (prefix.getValue()) {
                                        newPath += "/" + webJar.getValue();
                                    }
                                    newPath += "/" + originalPath;
                                    newPathsNode.add(newPath);
                                }
                                newPathsNode.add(originalPath);
                            } else {
                                log.error("Strange... The path could not be parsed.  Here is what was provided: " + pathNode.getValue().toString());
                            }
                        }
                    }

                    webJarRequireJsNode.replace("paths", newPaths);


                    // update the location in the packages node
                    ArrayNode packagesNode = webJarRequireJsNode.withArray("packages");

                    ArrayNode newPackages = mapper.createArrayNode();

                    if (packagesNode != null) {
                        for (JsonNode packageJson : packagesNode) {
                            String originalLocation = packageJson.get("location").textValue();
                            if (prefixes.size() > 0) {
                                // this picks the last prefix assuming that it is the right one
                                // not sure of a better way to do this since I don't think we want the CDN prefix
                                // maybe this can be an array like paths?
                                Map.Entry prefix = prefixes.get(prefixes.size() - 1);
                                String newLocation = prefix.getKey() + webJar.getKey();
                                if (prefix.getValue()) {
                                    newLocation += "/" + webJar.getValue();
                                }
                                newLocation += "/" + originalLocation;

                                ((ObjectNode) packageJson).put("location", newLocation);
                            }

                            newPackages.add(packageJson);
                        }
                    }

                    webJarRequireJsNode.replace("packages", newPackages);
                }

            } else {
                log.error(requireJsConfigErrorMessage(webJar));
            }
        } catch (IOException e) {
            log.warn(requireJsConfigErrorMessage(webJar));
            if (rawRequireJsConfig.length() > 0) {
                // only show the error if there was a config to parse
                log.error(e.getMessage());
            }
        }

        return webJarRequireJsNode;
    }


    /**
     * Returns the JSON RequireJS config for a given Bower WebJar
     *
     * @param webJar   A tuple (artifactId -> version) representing the WebJar.
     * @param prefixes A list of the prefixes to use in the `paths` part of the RequireJS config.
     * @return The JSON RequireJS config for the WebJar based on the meta-data in the WebJar's pom.xml file.
     */
    public static ObjectNode getBowerWebJarRequireJsConfig(Map.Entry webJar, List> prefixes) {

        String bowerJsonPath = WebJarAssetLocator.WEBJARS_PATH_PREFIX + "/" + webJar.getKey() + "/" + webJar.getValue() + "/" + "bower.json";

        return getWebJarRequireJsConfigFromMainConfig(webJar, prefixes, bowerJsonPath);
    }

    /**
     * Returns the JSON RequireJS config for a given Bower WebJar
     *
     * @param webJar   A tuple (artifactId -> version) representing the WebJar.
     * @param prefixes A list of the prefixes to use in the `paths` part of the RequireJS config.
     * @return The JSON RequireJS config for the WebJar based on the meta-data in the WebJar's pom.xml file.
     */
    public static ObjectNode getNpmWebJarRequireJsConfig(Map.Entry webJar, List> prefixes) {

        String packageJsonPath = WebJarAssetLocator.WEBJARS_PATH_PREFIX + "/" + webJar.getKey() + "/" + webJar.getValue() + "/" + "package.json";

        return getWebJarRequireJsConfigFromMainConfig(webJar, prefixes, packageJsonPath);
    }

    private static ObjectNode getWebJarRequireJsConfigFromMainConfig(Map.Entry webJar, List> prefixes, String path) {
        InputStream inputStream = RequireJS.class.getClassLoader().getResourceAsStream(path);

        if (inputStream != null) {
            try {
                ObjectMapper mapper = new ObjectMapper()
                        .configure(ALLOW_UNQUOTED_FIELD_NAMES, true)
                        .configure(ALLOW_SINGLE_QUOTES, true);

                ObjectNode requireConfig = mapper.createObjectNode();
                ObjectNode requireConfigPaths = requireConfig.putObject("paths");

                JsonNode jsonNode = mapper.readTree(inputStream);

                String name = jsonNode.get("name").asText();

                if (jsonNode.get("main").getNodeType() == JsonNodeType.STRING) {
                    String main = jsonNode.get("main").asText();
                    requireConfigPaths.put(name, mainJsToPathJson(webJar, main, prefixes));
                }
                else if (jsonNode.get("main").getNodeType() == JsonNodeType.ARRAY) {
                    ArrayList mainList = new ArrayList<>();
                    for (JsonNode mainJsonNode : jsonNode.withArray("main")) {
                        mainList.add(mainJsonNode.asText());
                    }
                    String main = getBowerBestMatchFromMainArray(mainList, name);
                    requireConfigPaths.put(name, mainJsToPathJson(webJar, main, prefixes));
                }

                // todo add dependency shims

                return requireConfig;

            } catch (JsonProcessingException e) {
                e.printStackTrace();
                log.warn("Could not create the RequireJS config for the " + webJar.getKey() + " " + webJar.getValue() + " WebJar" + " from " + path + "\n" +
                        "Please file a bug at: http://github.com/webjars/webjars-locator/issues/new");
            } catch (IOException e) {
                e.printStackTrace();
                log.warn("Could not create the RequireJS config for the " + webJar.getKey() + " " + webJar.getValue() + " WebJar" + " from " + path + "\n" +
                        "Please file a bug at: http://github.com/webjars/webjars-locator/issues/new");
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // what-evs
                }
            }

        }

        return null;
    }

    /*
     * Heuristic approach to find the 'best' candidate which most likely is the main script of a package.
     */

    private static String getBowerBestMatchFromMainArray(ArrayList items, String name) {
        if(items.size() == 1) // not really much choice here
            return items.get(0);

        ArrayList filteredList = new ArrayList<>();

        // first idea: only look at .js files

        for(String item : items) {
            if(item.toLowerCase().endsWith(".js")) {
                filteredList.add(item);
            }
        }

        // ... if there are any
        if(filteredList.size() == 0)
            filteredList = items;

        final HashMap distanceMap = new HashMap<>();
        final String nameForComparisons = name.toLowerCase();

        // second idea: most scripts are named after the project's name
        // sort all script files by their Levenshtein-distance
        // and return the one which is most similar to the project's name

        Collections.sort(filteredList, new Comparator() {

            public Integer getDistance(String value) {
                int distance;
                value = value.toLowerCase();
                if (distanceMap.containsKey(value)) {
                    distance = distanceMap.get(value);
                } else {
                    distance = StringUtils.getLevenshteinDistance(nameForComparisons, value);
                    distanceMap.put(value, distance);
                }
                return distance;
            }

            @Override
            public int compare(String o1, String o2) {
                return getDistance(o1).compareTo(getDistance(o2));
            }
        });

        return filteredList.get(0);
    }

    private static JsonNode mainJsToPathJson(Map.Entry webJar, String main, List> prefixes) {
        String requireJsStyleMain = main;
        if (main.endsWith(".js")) {
            requireJsStyleMain = main.substring(0, main.lastIndexOf(".js"));
        }

        if (requireJsStyleMain.startsWith("./")) {
            requireJsStyleMain = requireJsStyleMain.substring(2);
        }

        String unprefixedMain = webJar.getKey() + "/" + webJar.getValue() + "/" + requireJsStyleMain;

        ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);

        for (Map.Entry prefix : prefixes) {
            arrayNode.add(prefix.getKey() + unprefixedMain);
        }

        return arrayNode;
    }

    /**
     * A generic error message for when the RequireJS config could not be parsed out of the WebJar's pom.xml meta-data.
     *
     * @param webJar A tuple (artifactId -> version) representing the WebJar.
     * @return The error message.
     */
    private static String requireJsConfigErrorMessage(Map.Entry webJar) {
        return "Could not read WebJar RequireJS config for: " + webJar.getKey() + " " + webJar.getValue() + "\n" +
                "Please file a bug at: http://github.com/webjars/" + webJar.getKey() + "/issues/new";
    }

    /**
     * @param webJar A tuple (artifactId -> version) representing the WebJar.
     * @return The raw RequireJS config string from the WebJar's pom.xml meta-data.
     */
    public static String getRawWebJarRequireJsConfig(Map.Entry webJar) {
        String filename = WEBJARS_MAVEN_PREFIX + "/" + webJar.getKey() + "/pom.xml";
        InputStream inputStream = RequireJS.class.getClassLoader().getResourceAsStream(filename);

        if (inputStream != null) {
            // try to parse: { /* some json */ }
            try {
                DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
                Document doc = dBuilder.parse(inputStream);
                doc.getDocumentElement().normalize();

                NodeList propertiesNodes = doc.getElementsByTagName("properties");
                for (int i = 0; i < propertiesNodes.getLength(); i++) {
                    NodeList propertyNodes = propertiesNodes.item(i).getChildNodes();
                    for (int j = 0; j < propertyNodes.getLength(); j++) {
                        Node node = propertyNodes.item(j);
                        if (node.getNodeName().equals("requirejs")) {
                            return node.getTextContent();
                        }
                    }
                }

            } catch (ParserConfigurationException e) {
                log.warn(requireJsConfigErrorMessage(webJar));
            } catch (IOException e) {
                log.warn(requireJsConfigErrorMessage(webJar));
            } catch (SAXException e) {
                log.warn(requireJsConfigErrorMessage(webJar));
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // what-evs
                }
            }

        } else {
            log.warn(requireJsConfigErrorMessage(webJar));
        }

        return "";
    }

    /**
     * The legacy webJars-requirejs.js based RequireJS config for a WebJar.
     *
     * @param webJar A tuple (artifactId -> version) representing the WebJar.
     * @return The contents of the webJars-requirejs.js file.
     */
    @Deprecated
    public static String getWebJarConfig(Map.Entry webJar) {
        String webJarConfig = "";

        // read the webJarConfigs
        String filename = WebJarAssetLocator.WEBJARS_PATH_PREFIX + "/" + webJar.getKey() + "/" + webJar.getValue() + "/" + "webjars-requirejs.js";
        InputStream inputStream = RequireJS.class.getClassLoader().getResourceAsStream(filename);
        if (inputStream != null) {
            log.warn("The " + webJar.getKey() + " " + webJar.getValue() + " WebJar is using the legacy RequireJS config.\n" +
                    "Please try a new version of the WebJar or file or file an issue at:\n" +
                    "http://github.com/webjars/" + webJar.getKey() + "/issues/new");

            StringBuilder webJarConfigBuilder = new StringBuilder("// WebJar config for " + webJar.getKey() + "\n");
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            try {
                String line;

                while ((line = br.readLine()) != null) {
                    webJarConfigBuilder.append(line).append("\n");
                }

                webJarConfig = webJarConfigBuilder.toString();
            } catch (IOException e) {
                log.warn(filename + " could not be read.");
            } finally {
                try {
                    br.close();
                } catch (IOException e) {
                    // really?
                }
            }
        }

        return webJarConfig;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy