/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2015 Wisdom Framework * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.wisdom.bnd.plugins; import aQute.bnd.header.Attrs; import aQute.bnd.osgi.Analyzer; import aQute.bnd.osgi.Descriptors; import aQute.bnd.osgi.Jar; import aQute.bnd.service.AnalyzerPlugin; import aQute.bnd.service.Plugin; import aQute.service.reporter.Reporter; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import org.apache.commons.io.IOUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; /** * A BND plugin checking if one of the import packages is matching a list of known package and fix the imported * versions. This plugin avoid generating ranges for dependencies that are forward compatible and when their major * version is bumped. * <p> * The plugin loads its data from an internal file and also look for a 'src/main/osgi/versions.properties' file in * the project, so each project can configure the versions. * <p> * Files are properties file, where values are optional (this is still a valid properties file). For instance: * <pre> * {@code * com.google.common* * com.acme*: 1.0.0 * com.foo: [1.0.0, 2) * } * </pre> * <p> * In this file, the `com.google.common` packages see their imported versions fixed to the "[xxx,)", where "xxx" is * the (OSGi-compliant) version of the dependency providing the package. Notice that there is no upper bound. The * `com.acme` packages see their imported version set to 1.0.0, while `com.foo` packages are imported using the * specified range. * <p> * These fixes can be done in the `osgi.bnd` version, but 1) it's automatic for Guava, 2) let you have a shared file. */ public class ImportedPackageRangeFixer implements AnalyzerPlugin, Plugin { /** * The name of the property that indicate the version file if any. */ public static final String RANGE_FILE = "file"; /** * The internal version file. */ public static final String INTERNAL_RANGE_FILE_URL = "ranges/versions.properties"; /** * The default path to find the version file. */ public static final String DEFAULT_RANGE_FILE = "src/main/osgi/versions.properties"; private Map<String, String> configuration; private Reporter reporter; private Set<Range> ranges = new TreeSet<>(); /** * Analyzes the jar and update the version range. * * @param analyzer the analyzer * @return {@code false} * @throws Exception if the analaysis fails. */ @Override public boolean analyzeJar(Analyzer analyzer) throws Exception { loadInternalRangeFix(); loadExternalRangeFix(); if (analyzer.getReferred() == null) { return false; } // Data loaded, start analysis for (Map.Entry<Descriptors.PackageRef, Attrs> entry : analyzer.getReferred().entrySet()) { for (Range range : ranges) { if (range.matches(entry.getKey().getFQN())) { String value = range.getRange(analyzer); if (value != null) { reporter.warning("Updating import version of " + range.name + " to " + value); entry.getValue().put("version", value); } } } } return false; } private void loadExternalRangeFix() throws IOException { if (configuration == null) { return; } String file = configuration.get(RANGE_FILE); if (file == null) { file = DEFAULT_RANGE_FILE; } File theFile = new File(file); if (theFile.isFile()) { addToRanges(load(theFile)); } } private void loadInternalRangeFix() throws IOException { URL url = this.getClass().getClassLoader().getResource(INTERNAL_RANGE_FILE_URL); Preconditions.checkNotNull(url); Properties loaded = load(url); addToRanges(loaded); } private void addToRanges(Properties properties) { for (String key : properties.stringPropertyNames()) { String value = properties.getProperty(key); ranges.add(new Range(key, value)); } } /** * Callbacks called by BND with the properties. * * @param map the properties */ @Override public void setProperties(Map<String, String> map) { this.configuration = map; } /** * Callbacks called by BND with the logger. * * @param reporter the logger */ @Override public void setReporter(Reporter reporter) { this.reporter = reporter; } /** * Utility method to load a properties file. * * @param file the file * @return the read properties, empty if the file cannot be found. * @throws IOException if the file cannot be loaded. */ public static Properties load(File file) throws IOException { if (file.isFile()) { return load(file.toURI().toURL()); } return new Properties(); } /** * Utility method to load a properties file pointed by the given url. * * @param url the url * @return the read properties, empty if the file cannot be found. * @throws IOException if the file cannot be loaded. */ public static Properties load(URL url) throws IOException { InputStream fis = null; try { Properties props = new Properties(); fis = url.openStream(); props.load(fis); return props; } finally { IOUtils.closeQuietly(fis); } } private class Range implements Comparable<Range> { final String name; final String value; final Pattern regex; /** * Field acting as a cache storing the version of the jar providing the package. This field is only used if * we have no value. */ private String foundRange; private Range(String name, String value) { this.name = name; this.value = value; this.regex = Pattern.compile(name.trim().replace(".", "\\.").replace("*", ".*")); } private boolean matches(String pck) { return regex.matcher(pck).matches(); } private String getRange(Analyzer analyzer) throws Exception { if (foundRange != null) { return foundRange; } if (Strings.isNullOrEmpty(value)) { for (Jar jar : analyzer.getClasspath()) { if (isProvidedByJar(jar) && jar.getVersion() != null) { foundRange = jar.getVersion(); return jar.getVersion(); } } // Cannot find a provider. reporter.error("Cannot find a dependency providing " + name + " in the classpath"); return null; } else { return value; } } private boolean isProvidedByJar(Jar jar) { for (String s : jar.getPackages()) { if (matches(s)) { return true; } } return false; } /** * Method used to sort range. Longest prefix first. * * @param o the other range * @return 1 if the current range is longer than the given range. */ @Override public int compareTo(Range o) { return Integer.compare(this.regex.pattern().length(), o.regex.pattern().length()); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Range range = (Range) o; return Objects.equal(name, range.name) && Objects.equal(value, range.value); } @Override public int hashCode() { return Objects.hashCode(name, value); } } }