/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ package org.apache.brooklyn.core; import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.Attributes; import javax.annotation.Nullable; import org.osgi.framework.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import java.util.Arrays; import java.util.Dictionary; import java.util.Hashtable; import java.util.List; import org.apache.brooklyn.api.catalog.CatalogItem; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.core.mgmt.classloading.OsgiBrooklynClassLoadingContext; import org.apache.brooklyn.core.mgmt.ha.OsgiManager; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.rt.felix.ManifestHelper; import org.apache.brooklyn.util.core.osgi.Osgis; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.stream.Streams; import org.apache.brooklyn.util.text.Strings; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; /** * Wraps the version of Brooklyn. * <p> * Also retrieves the SHA-1 from any OSGi bundle, and checks that the maven and osgi versions match. */ public class BrooklynVersion { private static final Logger log = LoggerFactory.getLogger(BrooklynVersion.class); private static final String MVN_VERSION_RESOURCE_FILE = "META-INF/maven/org.apache.brooklyn/brooklyn-core/pom.properties"; private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; private static final String BROOKLYN_CORE_SYMBOLIC_NAME = "org.apache.brooklyn.core"; private static final String MVN_VERSION_PROPERTY_NAME = "version"; private static final String OSGI_VERSION_PROPERTY_NAME = Attributes.Name.IMPLEMENTATION_VERSION.toString(); private static final String OSGI_SHA1_PROPERTY_NAME = "Implementation-SHA-1"; // may be useful: // private static final String OSGI_BRANCH_PROPERTY_NAME = "Implementation-Branch"; private final static String VERSION_FROM_STATIC = "0.9.0-SNAPSHOT"; // BROOKLYN_VERSION private static final AtomicReference<Boolean> IS_DEV_ENV = new AtomicReference<Boolean>(); private static final String BROOKLYN_FEATURE_PREFIX = "Brooklyn-Feature-"; public static final BrooklynVersion INSTANCE = new BrooklynVersion(); private final Properties versionProperties = new Properties(); private BrooklynVersion() { // we read the maven pom metadata and osgi metadata and make sure it's sensible // everything is put into a single map for now (good enough, but should be cleaned up) readPropertiesFromMavenResource(BrooklynVersion.class.getClassLoader()); readPropertiesFromOsgiResource(); // TODO there is also build-metadata.properties used in ServerResource /v1/server/version endpoint // see comments on that about folding it into this class instead checkVersions(); } public void checkVersions() { String mvnVersion = getVersionFromMavenProperties(); if (mvnVersion != null && !VERSION_FROM_STATIC.equals(mvnVersion)) { throw new IllegalStateException("Version error: maven " + mvnVersion + " / code " + VERSION_FROM_STATIC); } String osgiVersion = versionProperties.getProperty(OSGI_VERSION_PROPERTY_NAME); // TODO does the OSGi version include other slightly differ gubbins/style ? if (osgiVersion != null && !VERSION_FROM_STATIC.equals(osgiVersion)) { throw new IllegalStateException("Version error: osgi " + osgiVersion + " / code " + VERSION_FROM_STATIC); } } /** * Returns version as inferred from classpath/osgi, if possible, or 0.0.0-SNAPSHOT. * See also {@link #getVersionFromMavenProperties()} and {@link #getVersionFromOsgiManifest()}. * * @deprecated since 0.7.0, in favour of the more specific methods (and does anyone need that default value?) */ @Deprecated public String getVersionFromClasspath() { String v = getVersionFromMavenProperties(); if (Strings.isNonBlank(v)) return v; v = getVersionFromOsgiManifest(); if (Strings.isNonBlank(v)) return v; return "0.0.0-SNAPSHOT"; } @Nullable public String getVersionFromMavenProperties() { return versionProperties.getProperty(MVN_VERSION_PROPERTY_NAME); } @Nullable public String getVersionFromOsgiManifest() { return versionProperties.getProperty(OSGI_VERSION_PROPERTY_NAME); } @Nullable /** SHA1 of the last commit to brooklyn at the time this build was made. * For SNAPSHOT builds of course there may have been further non-committed changes. */ public String getSha1FromOsgiManifest() { return versionProperties.getProperty(OSGI_SHA1_PROPERTY_NAME); } public String getVersion() { return VERSION_FROM_STATIC; } public boolean isSnapshot() { return (getVersion().indexOf("-SNAPSHOT") >= 0); } private void readPropertiesFromMavenResource(ClassLoader resourceLoader) { InputStream versionStream = null; try { versionStream = resourceLoader.getResourceAsStream(MVN_VERSION_RESOURCE_FILE); if (versionStream == null) { if (isDevelopmentEnvironment()) { // allowed for dev env log.trace("No maven resource file " + MVN_VERSION_RESOURCE_FILE + " available"); } else { log.warn("No maven resource file " + MVN_VERSION_RESOURCE_FILE + " available"); } return; } versionProperties.load(checkNotNull(versionStream)); } catch (IOException e) { log.warn("Error reading maven resource file " + MVN_VERSION_RESOURCE_FILE + ": " + e, e); } finally { Streams.closeQuietly(versionStream); } } /** * Reads main attributes properties from brooklyn-core's bundle manifest. */ private void readPropertiesFromOsgiResource() { if (Osgis.isBrooklynInsideFramework()) { Dictionary<String, String> headers = FrameworkUtil.getBundle(BrooklynVersion.class).getHeaders(); for (Enumeration<String> keys = headers.keys(); keys.hasMoreElements();) { String key = keys.nextElement(); versionProperties.put(key, headers.get(key)); } } else { Enumeration<URL> paths; try { paths = BrooklynVersion.class.getClassLoader().getResources(MANIFEST_PATH); } catch (IOException e) { // shouldn't happen throw Exceptions.propagate(e); } while (paths.hasMoreElements()) { URL u = paths.nextElement(); InputStream us = null; try { us = u.openStream(); ManifestHelper mh = ManifestHelper.forManifest(us); if (BROOKLYN_CORE_SYMBOLIC_NAME.equals(mh.getSymbolicName())) { Attributes attrs = mh.getManifest().getMainAttributes(); for (Object key : attrs.keySet()) { // key is an Attribute.Name; toString converts to string versionProperties.put(key.toString(), attrs.getValue(key.toString())); } return; } } catch (Exception e) { Exceptions.propagateIfFatal(e); log.warn("Error reading OSGi manifest from " + u + " when determining version properties: " + e, e); } finally { Streams.closeQuietly(us); } } if (isDevelopmentEnvironment()) { // allowed for dev env log.trace("No OSGi manifest available to determine version properties"); } else { log.warn("No OSGi manifest available to determine version properties"); } } } /** * Returns whether this is a Brooklyn dev environment, * specifically core/target/classes/ is on the classpath for the org.apache.brooklyn.core project. * <p/> * In a packaged or library build of Brooklyn (normal usage) this should return false, * and all OSGi components should be available. * <p/> * There is no longer any way to force this, * such as the old BrooklynDevelopmentMode class; * but that could easily be added if required (eg as a system property). */ public static boolean isDevelopmentEnvironment() { Boolean isDevEnv = IS_DEV_ENV.get(); if (isDevEnv != null) return isDevEnv; synchronized (IS_DEV_ENV) { isDevEnv = computeIsDevelopmentEnvironment(); IS_DEV_ENV.set(isDevEnv); return isDevEnv; } } private static boolean computeIsDevelopmentEnvironment() { Enumeration<URL> paths; try { paths = BrooklynVersion.class.getClassLoader().getResources("org/apache/brooklyn/core/BrooklynVersion.class"); } catch (IOException e) { // shouldn't happen throw Exceptions.propagate(e); } while (paths.hasMoreElements()) { URL u = paths.nextElement(); // running fram a classes directory (including coverage-classes for cobertura) triggers dev env if (u.getPath().endsWith("org/apache/brooklyn/core/BrooklynVersion.class")) { try { log.debug("Brooklyn dev/src environment detected: BrooklynVersion class is at: " + u); return true; } catch (Exception e) { Exceptions.propagateIfFatal(e); log.warn("Error reading manifest to determine whether this is a development environment: " + e, e); } } } return false; } public void logSummary() { log.debug("Brooklyn version " + getVersion() + " (git SHA1 " + getSha1FromOsgiManifest() + ")"); } /** * @deprecated since 0.7.0, redundant with {@link #get()} */ @Deprecated public static String getVersionFromStatic() { return VERSION_FROM_STATIC; } public static String get() { return INSTANCE.getVersion(); } /** * @param mgmt The context to search for features. * @return An iterable containing all features found in the management context's classpath and catalogue. */ public static Iterable<BrooklynFeature> getFeatures(ManagementContext mgmt) { if (Osgis.isBrooklynInsideFramework()) { List<Bundle> bundles = Arrays.asList( FrameworkUtil.getBundle(BrooklynVersion.class) .getBundleContext() .getBundles() ); Maybe<OsgiManager> osgi = ((ManagementContextInternal)mgmt).getOsgiManager(); for (CatalogItem<?, ?> catalogItem : mgmt.getCatalog().getCatalogItems()) { if (osgi.isPresentAndNonNull()) { for (CatalogItem.CatalogBundle catalogBundle : catalogItem.getLibraries()) { Maybe<Bundle> osgiBundle = osgi.get().findBundle(catalogBundle); if (osgiBundle.isPresentAndNonNull()) { bundles.add(osgiBundle.get()); } } } } // Set over list in case a bundle is reported more than once (e.g. from classpath and from OSGi). // Not sure of validity of this approach over just reporting duplicates. ImmutableSet.Builder<BrooklynFeature> features = ImmutableSet.builder(); for(Bundle bundle : bundles) { Optional<BrooklynFeature> fs = BrooklynFeature.newFeature(bundle.getHeaders()); if (fs.isPresent()) { features.add(fs.get()); } } return features.build(); } else { Iterable<URL> manifests = ResourceUtils.create(mgmt).getResources(MANIFEST_PATH); for (CatalogItem<?, ?> catalogItem : mgmt.getCatalog().getCatalogItems()) { OsgiBrooklynClassLoadingContext osgiContext = new OsgiBrooklynClassLoadingContext( mgmt, catalogItem.getCatalogItemId(), catalogItem.getLibraries()); manifests = Iterables.concat(manifests, osgiContext.getResources(MANIFEST_PATH)); } // Set over list in case a bundle is reported more than once (e.g. from classpath and from OSGi). // Not sure of validity of this approach over just reporting duplicates. ImmutableSet.Builder<BrooklynFeature> features = ImmutableSet.builder(); for (URL manifest : manifests) { ManifestHelper mh = null; try { mh = ManifestHelper.forManifest(manifest); } catch (Exception e) { Exceptions.propagateIfFatal(e); log.debug("Error reading OSGi manifest from " + manifest + " when determining version properties: " + e, e); } if (mh == null) continue; Attributes attrs = mh.getManifest().getMainAttributes(); Optional<BrooklynFeature> fs = BrooklynFeature.newFeature(attrs); if (fs.isPresent()) { features.add(fs.get()); } } return features.build(); } } public static class BrooklynFeature { private final String name; private final String symbolicName; private final String version; private final String lastModified; private final Map<String, String> additionalData; BrooklynFeature(String name, String symbolicName, String version, String lastModified, Map<String, String> additionalData) { this.symbolicName = checkNotNull(symbolicName, "symbolicName"); this.name = name; this.version = version; this.lastModified = lastModified; this.additionalData = ImmutableMap.copyOf(additionalData); } private static Optional<BrooklynFeature> newFeature(Attributes attrs) { // unfortunately Attributes is a Map<Object,Object> Dictionary<String,String> headers = new Hashtable<>(); for (Map.Entry<Object, Object> entry : attrs.entrySet()) { headers.put(entry.getKey().toString(), entry.getValue().toString()); } return newFeature(headers); } /** @return Present if any attribute name begins with {@link #BROOKLYN_FEATURE_PREFIX}, absent otherwise. */ private static Optional<BrooklynFeature> newFeature(Dictionary<String,String> headers) { Map<String, String> additionalData = Maps.newHashMap(); for (Enumeration<String> keys = headers.keys(); keys.hasMoreElements();) { String key = keys.nextElement(); if (key.startsWith(BROOKLYN_FEATURE_PREFIX)) { String value = headers.get(key); if (!Strings.isBlank(value)) { additionalData.put(key, value); } } } if (additionalData.isEmpty()) { return Optional.absent(); } // Name is special cased as it a useful way to indicate a feature without String nameKey = BROOKLYN_FEATURE_PREFIX + "Name"; String name = Optional.fromNullable(additionalData.remove(nameKey)) .or(Optional.fromNullable(Constants.BUNDLE_NAME)) .or(headers.get(Constants.BUNDLE_SYMBOLICNAME)); return Optional.of(new BrooklynFeature( name, headers.get(Constants.BUNDLE_SYMBOLICNAME), headers.get(Constants.BUNDLE_VERSION), headers.get("Bnd-LastModified"), additionalData)); } public String getLastModified() { return lastModified; } public String getName() { return name; } public String getSymbolicName() { return symbolicName; } public String getVersion() { return version; } /** @return an unmodifiable map */ public Map<String, String> getAdditionalData() { return additionalData; } @Override public String toString() { return getClass().getSimpleName() + "{" + symbolicName + (version != null ? ":" + version : "") + "}"; } @Override public int hashCode() { return Objects.hashCode(symbolicName, version); } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; BrooklynFeature that = (BrooklynFeature) other; if (!symbolicName.equals(that.symbolicName)) { return false; } else if (version != null ? !version.equals(that.version) : that.version != null) { return false; } return true; } } }