/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library 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 library 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. */ package com.liferay.ant.bnd.npm; import aQute.bnd.header.Attrs; import aQute.bnd.header.Parameters; import aQute.bnd.osgi.Analyzer; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Jar; import aQute.bnd.osgi.Resource; import aQute.bnd.service.AnalyzerPlugin; import aQute.bnd.version.Version; import aQute.lib.json.Decoder; import aQute.lib.json.JSONCodec; import java.io.InputStream; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Raymond Augé */ public class NpmAnalyzerPlugin implements AnalyzerPlugin { public static final String WEB_CONTEXT_PATH = "Web-ContextPath"; @Override public boolean analyzeJar(Analyzer analyzer) throws Exception { Jar jar = analyzer.getJar(); Resource npmJSONResource = jar.getResource("package.json"); if (npmJSONResource == null) { return false; } NpmModule npmModule = processNpmJsonResource(analyzer, npmJSONResource); processDependencies(analyzer, npmModule); return false; } public static class NpmModule { public Map<String, String> dependencies; public String name; public Map<String, String> runtime; public String version; } protected void appendInclusive( StringBuilder sb, String group1, String group2) { sb.append("(&(version>="); Matcher matcher = _versionNamedPattern.matcher(group1); matcher.matches(); String major = matcher.group("major"); String minor = matcher.group("minor"); String micro = matcher.group("micro"); String qualifier = matcher.group("qualifier"); sb.append(toVersion(major, minor, micro, qualifier)); matcher = _versionNamedPattern.matcher(group2); matcher.matches(); major = matcher.group("major"); minor = matcher.group("minor"); micro = matcher.group("micro"); qualifier = matcher.group("qualifier"); if (minor == null) { major = Integer.parseInt(major) + 1 + ""; sb.append(")(!(version>="); sb.append(major); sb.append(".0.0)"); } else if (micro == null) { sb.append(")(version<="); sb.append(major); sb.append("."); sb.append(Integer.parseInt(minor) + 1); sb.append(".0"); } else { sb.append(")(version<="); sb.append(toVersion(major, minor, micro, qualifier)); } sb.append("))"); } protected void appendPrefixRange( StringBuilder sb, String prefix, String version) { Matcher matcher = _versionNamedPattern.matcher(version); matcher.matches(); String major = matcher.group("major"); String minor = matcher.group("minor"); String micro = matcher.group("micro"); String qualifier = matcher.group("qualifier"); if (prefix.equals("v") || prefix.equals("=")) { sb.append("(version="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")"); } else if (prefix.equals("<")) { sb.append("(!(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append("))"); } else if (prefix.equals("<=")) { sb.append("(version<="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")"); } else if (prefix.equals(">")) { sb.append("(&(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")(!(version="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")))"); } else if (prefix.equals(">=")) { sb.append("(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")"); } else if (prefix.equals("~")) { sb.append("(&(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")(!(version>="); if (minor != null) { sb.append(major); sb.append("."); sb.append(Integer.parseInt(minor) + 1); sb.append(".0"); } else { sb.append(Integer.parseInt(major) + 1); sb.append(".0.0"); } sb.append(")))"); } else if (prefix.equals("^")) { sb.append("(&(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")(!(version>="); if (!major.equals("0") || minor.equalsIgnoreCase("x") || minor.equals("*")) { sb.append(Integer.parseInt(major) + 1); sb.append(".0.0"); } else if (!minor.equals("0") || (micro == null) || micro.equalsIgnoreCase("x") || micro.equals("*")) { sb.append("0."); sb.append(Integer.parseInt(desugar(minor)) + 1); sb.append(".0"); } else { sb.append("0.0."); sb.append(Integer.parseInt(desugar(micro)) + 1); } sb.append(")))"); } } protected void appendRange(StringBuilder sb, String group1, String group2) { sb.append("(&"); Matcher matcher = _versionPrefixRangePattern.matcher(group1); matcher.matches(); appendPrefixRange(sb, matcher.group(1), matcher.group(2)); matcher = _versionPrefixRangePattern.matcher(group2); matcher.matches(); appendPrefixRange(sb, matcher.group(1), matcher.group(2)); sb.append(")"); } protected void appendVersion(StringBuilder sb, Matcher matcher) { String major = matcher.group("major"); String minor = matcher.group("minor"); String micro = matcher.group("micro"); String qualifier = matcher.group("qualifier"); if ((minor == null) || minor.equalsIgnoreCase("x") || minor.equals("*")) { sb.append("(&(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")(!(version>="); sb.append(Integer.parseInt(major) + 1); sb.append(".0.0)))"); } else if ((micro == null) || micro.equalsIgnoreCase("x") || micro.equals("*")) { sb.append("(&(version>="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")(!(version>="); sb.append(major); sb.append("."); sb.append(Integer.parseInt(minor) + 1); sb.append(".0)))"); } else { sb.append("(version="); sb.append(toVersion(major, minor, micro, qualifier)); sb.append(")"); } } protected String desugar(String minor) { if ((minor == null) || minor.equalsIgnoreCase("x") || minor.equals("*")) { return "0"; } return minor; } protected NpmModule getNpmModule(InputStream inputStream) throws Exception { JSONCodec jsonCodec = new JSONCodec(); Decoder decoder = jsonCodec.dec(); decoder = decoder.from(inputStream); return decoder.get(NpmModule.class); } protected String getNpmVersionFilter(String version) { StringBuilder sb = new StringBuilder(); String[] comparatorSets = version.split("\\|\\|"); // Comparator sets are OR'd together if (comparatorSets.length > 1) { sb.append("(|"); } for (String comparatorSet : comparatorSets) { comparatorSet = comparatorSet.trim(); if ((comparatorSet.length() == 0) || comparatorSet.equals("*")) { comparatorSet = ">=0"; } Matcher inclusiveMatcher = _versionInclusiveRangePattern.matcher( comparatorSet); Matcher rangeMatcher = _versionRangePattern.matcher(comparatorSet); Matcher prefixRangeMatcher = _versionPrefixRangePattern.matcher( comparatorSet); Matcher versionMatcher = _versionNamedPattern.matcher( comparatorSet); if (inclusiveMatcher.matches()) { appendInclusive( sb, inclusiveMatcher.group(1), inclusiveMatcher.group(9)); } else if (rangeMatcher.matches()) { appendRange(sb, rangeMatcher.group(1), rangeMatcher.group(11)); } else if (prefixRangeMatcher.matches()) { appendPrefixRange( sb, prefixRangeMatcher.group(1), prefixRangeMatcher.group(2)); } else if (versionMatcher.matches()) { appendVersion(sb, versionMatcher); } } if (comparatorSets.length > 1) { sb.append(")"); } return sb.toString(); } protected void processDependencies(Analyzer analyzer, NpmModule npmModule) { if (npmModule.runtime == null) { return; } Parameters parameters = new Parameters(); for (Entry<String, String> entry : npmModule.runtime.entrySet()) { Attrs attrs = new Attrs(); StringBuilder sb = new StringBuilder(); sb.append("(&("); sb.append(_OSGI_WEBRESOURCE); sb.append("="); String name = entry.getKey(); sb.append(name); sb.append(")"); String version = entry.getValue(); sb.append(getNpmVersionFilter(version)); sb.append(")"); attrs.put(Constants.FILTER_DIRECTIVE, sb.toString()); parameters.add(_OSGI_WEBRESOURCE, attrs); } setCapabilities(analyzer, Constants.REQUIRE_CAPABILITY, parameters); } protected NpmModule processNpmJsonResource( Analyzer analyzer, Resource npmJSONResource) throws Exception { NpmModule npmModule = getNpmModule(npmJSONResource.openInputStream()); String bundleVersion = analyzer.getBundleVersion(); if (bundleVersion == null) { Version version = null; try { version = new Version(npmModule.version); } catch (IllegalArgumentException iae) { String sanitizedQualifier = npmModule.version.replaceAll( "[^-_\\da-zA-Z]", ""); version = new Version("0.0.0." + sanitizedQualifier); } analyzer.setBundleVersion(version.toString()); } Parameters parameters = new Parameters(); Attrs attrs = new Attrs(); String npmName = npmModule.name; String webContextPath = analyzer.getProperty(WEB_CONTEXT_PATH); if ((webContextPath == null) && (npmName != null)) { if (npmName.indexOf('/') == 0) { npmName = npmName.substring(1); } analyzer.setProperty( WEB_CONTEXT_PATH, '/' + npmName + "-" + analyzer.getBundleVersion()); } attrs.put(_OSGI_WEBRESOURCE, npmName); attrs.put( Constants.VERSION_ATTRIBUTE + ":Version", analyzer.getBundleVersion()); parameters.add(_OSGI_WEBRESOURCE, attrs); setCapabilities(analyzer, Constants.PROVIDE_CAPABILITY, parameters); return npmModule; } protected void setCapabilities( Analyzer analyzer, String capabilityType, Parameters parameters) { if (parameters.isEmpty()) { return; } Parameters headerParameters = new Parameters( analyzer.getProperty(capabilityType)); if (!headerParameters.isEmpty()) { parameters.mergeWith(headerParameters, false); } analyzer.setProperty(capabilityType, parameters.toString()); } protected String toVersion( String major, String minor, String micro, String qualifier) { StringBuilder sb = new StringBuilder(); sb.append(major); if ((minor == null) || minor.equalsIgnoreCase("x") || minor.equals("*")) { sb.append(".0"); } else { sb.append("."); sb.append(minor); } if ((micro == null) || micro.equalsIgnoreCase("x") || micro.equals("*")) { sb.append(".0"); } else { sb.append("."); sb.append(micro); } if (qualifier == null) { sb.append(""); } else { sb.append("."); sb.append(qualifier); } return sb.toString(); } private static final String _OSGI_WEBRESOURCE = "osgi.webresource"; private static final String _VERSION = "((\\d{1,9})(\\.([\\dx\\*]{1,9})(\\.([\\dx\\*]{1,9})" + "([\\.-]([-_\\da-zA-Z]+))?)?)?)"; private static final String _VERSION_INCLUSIVE_RANGE = _VERSION + "\\s*-\\s*" + _VERSION; private static final String _VERSION_NAMED = "(?<major>\\d{1,9})(\\.(?<minor>[\\dx\\*]{1,9})" + "(\\.(?<micro>[\\dx\\*]{1,9})" + "([\\.-](?<qualifier>[-_\\da-zA-Z]+))?)?)?"; private static final String _VERSION_PREFIX_RANGE = "(<|<=|>|>=|=|~|\\^|v)"; private static final String _VERSION_RANGE = "(" + _VERSION_PREFIX_RANGE + _VERSION + ")\\s+(" + _VERSION_PREFIX_RANGE + _VERSION + ")"; private static final Pattern _versionInclusiveRangePattern = Pattern.compile(_VERSION_INCLUSIVE_RANGE); private static final Pattern _versionNamedPattern = Pattern.compile( _VERSION_NAMED); private static final Pattern _versionPrefixRangePattern = Pattern.compile( _VERSION_PREFIX_RANGE + _VERSION); private static final Pattern _versionRangePattern = Pattern.compile( _VERSION_RANGE); }