/* * 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.entity.drivers.downloads; import static com.google.common.base.Preconditions.checkNotNull; import java.util.List; import java.util.Map; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.drivers.EntityDriver; import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolverManager.DownloadRequirement; import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolverManager.DownloadTargets; import org.apache.brooklyn.config.StringConfigMap; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * Based on the contents of brooklyn properties, sets up rules for resolving where to * download artifacts from, for installing entities. * * By default, these rules override the DOWNLOAD_URL defined on the entities in code. * Global properties can be specified that apply to all entities. Entity-specific properties * can also be specified (which override the global properties for that entity type). * * Below is an example of realistic configuration for an enterprise who have an in-house * repository that must be used for everything, rather than going out to the public internet. * <pre> * {@code * // FIXME Check format for including addonname- only if addonname is non-null? * // FIXME Use this in a testng test case * brooklyn.downloads.all.url=http://downloads.acme.com/brookyn/repository/${simpletype}/${simpletype}-${addon?? addon-}${version}.${fileSuffix!.tar.gz} * } * </pre> * * To illustrate the features and variations one can use, below is an example of global * properties that can be specified. The semicolon-separated list of URLs will be tried in-order * until one succeeds. The fallback url says to use that if all other URLs fail (or no others are * specified). * <pre> * {@code * brooklyn.downloads.all.url=http://myurl1/${simpletype}-${version}.tar.gz; http://myurl2/${simpletype}-${version}.tar.gz * brooklyn.downloads.all.fallbackurl=http://myurl3/${simpletype}-${version}.tar.gz * } * </pre> * * Similarly, entity-specific properties can be defined. All "global properties" will also apply * to this entity type, unless explicitly overridden. * <pre> * {@code * brooklyn.downloads.entity.tomcatserver.url=http://mytomcaturl1/tomcat-${version}.tar.gz * brooklyn.downloads.entity.tomcatserver.fallbackurl=http://myurl2/tomcat-${version}.tar.gz * } * </pre> * * Downloads for entity-specific add-ons can also be defined. All "global properties" will also apply * to this entity type, unless explicitly overridden. * <pre> * {@code * brooklyn.downloads.entity.nginxcontroller.addon.stickymodule.url=http://myurl1/nginx-stickymodule-${version}.tar.gz * brooklyn.downloads.entity.nginxcontroller.addon.stickymodule.fallbackurl=http://myurl2/nginx-stickymodule-${version}.tar.gz * } * </pre> * * If no explicit URLs are supplied, then by default it will use the DOWNLOAD_URL attribute * of the entity (if supplied), followed by the fallbackurl if that fails. * * A URL can be a "template", where things of the form ${version} will be substituted for the value * of "version" provided for that entity. The freemarker template engine is used to convert URLs * (see <a href="http://freemarker.org">http://freemarker.org</a>). For example, one could use the URL: * <pre> * {@code * http://repo.acme.com/${simpletype}-${version}.${fileSuffix!tar.gz} * } * </pre> * The following substitutions are available automatically for a template: * <ul> * <li>entity: the {@link Entity} instance * <li>driver: the {@link EntityDriver} instance being used for the Entity * <li>simpletype: the unqualified name of the entity type * <li>type: the fully qualified name of the entity type * <li>addon: the name of the entity add-on, or null if it's the core entity artifact * <li>version: the version number of the entity to be installed (or of the add-on) * </ul> */ public class DownloadProducerFromProperties implements Function<DownloadRequirement, DownloadTargets> { /* FIXME: expose config for canContinueResolving. * ... then it uses only the overrides in the properties file. This, in combination with * setting something like {@code brooklyn.downloads.all.url=http://acme.com/repo/${simpletype}/${simpletype}-${version}.tar.gz}, * allows an enterprise to ensure that entities never go to the public internet during installation. * * But also need to override things like nginx downlaod url for the stick module and pcre. */ @SuppressWarnings("unused") private static final Logger LOG = LoggerFactory.getLogger(DownloadProducerFromProperties.class); public static final String DOWNLOAD_CONF_PREFIX = "brooklyn.downloads."; private final StringConfigMap config; public DownloadProducerFromProperties(StringConfigMap config) { this.config = config; } public DownloadTargets apply(DownloadRequirement downloadRequirement) { List<Rule> rules = generateRules(); BasicDownloadTargets.Builder result = BasicDownloadTargets.builder(); for (Rule rule : rules) { if (rule.matches(downloadRequirement.getEntityDriver(), downloadRequirement.getAddonName())) { result.addAll(rule.resolve(downloadRequirement)); } } return result.build(); } /** * Produces a set of URL-generating rules, based on the brooklyn properties. These * rules will be applied in-order until one of them returns a non-empty result. */ private List<Rule> generateRules() { List<Rule> result = Lists.newArrayList(); Map<String, String> subconfig = filterAndStripPrefix(config.asMapWithStringKeys(), DOWNLOAD_CONF_PREFIX); // If exists, use things like: // brooklyn.downloads.all.fallbackurl=... // brooklyn.downloads.all.url=... // But only if not overridden by more entity-specify value Map<String, String> forall = filterAndStripPrefix(subconfig, "all."); String fallbackUrlForAll = forall.get("fallbackurl"); String urlForAll = forall.get("url"); // If exists, use things like: // brooklyn.downloads.entity.JBoss7Server.url=... Map<String, String> forSpecificEntities = filterAndStripPrefix(subconfig, "entity."); Map<String, Map<String,String>> splitBySpecificEntity = splitByPrefix(forSpecificEntities); for (Map.Entry<String, Map<String,String>> entry : splitBySpecificEntity.entrySet()) { String entityType = entry.getKey(); Map<String, String> forentity = entry.getValue(); String urlForEntity = forentity.get("url"); if (urlForEntity == null) urlForEntity = urlForAll; String fallbackUrlForEntity = forentity.get("fallbackurl"); if (fallbackUrlForEntity == null) fallbackUrlForEntity = fallbackUrlForAll; result.add(new EntitySpecificRule(entityType, urlForEntity, fallbackUrlForEntity)); // If exists, use things like: // brooklyn.downloads.entity.nginxcontroller.addon.stickymodule.url=... Map<String, String> forSpecificAddons = filterAndStripPrefix(forentity, "addon."); Map<String, Map<String,String>> splitBySpecificAddon = splitByPrefix(forSpecificAddons); for (Map.Entry<String, Map<String,String>> entry2 : splitBySpecificAddon.entrySet()) { String addonName = entry2.getKey(); Map<String, String> foraddon = entry2.getValue(); String urlForAddon = foraddon.get("url"); if (urlForAddon == null) urlForAddon = urlForEntity; String fallbackUrlForAddon = foraddon.get("fallbackurl"); if (fallbackUrlForEntity == null) fallbackUrlForAddon = fallbackUrlForEntity; result.add(new EntityAddonSpecificRule(entityType, addonName, urlForAddon, fallbackUrlForAddon)); } } if (!forall.isEmpty()) { result.add(new UniversalRule(urlForAll, fallbackUrlForAll)); } return result; } /** * Returns a sub-map of config for keys that started with the given prefix, but where the returned * map's keys do not include the prefix. */ private static Map<String,String> filterAndStripPrefix(Map<String,?> config, String prefix) { Map<String,String> result = Maps.newLinkedHashMap(); for (Map.Entry<String,?> entry : config.entrySet()) { String key = entry.getKey(); if (key.startsWith(prefix)) { Object value = entry.getValue(); result.put(key.substring(prefix.length()), (value == null) ? null : value.toString()); } } return result; } /** * Splits the map up into multiple maps, using the key's prefix up to the first dot to * tell which map to include it in. This prefix is used as the key in the map-of-maps, and * is omitted in the contained map. * * For example, given [a.b:v1, a.c:v2, d.e:v3], it will return [ a:[b:v1, c:v2], d:[e:v3] ] */ private static Map<String,Map<String,String>> splitByPrefix(Map<String,String> config) { Map<String,Map<String,String>> result = Maps.newLinkedHashMap(); for (Map.Entry<String,String> entry : config.entrySet()) { String key = entry.getKey(); String keysuffix = key.substring(key.indexOf(".")+1); String keyprefix = key.substring(0, key.length()-keysuffix.length()-1); String value = entry.getValue(); Map<String,String> submap = result.get(keyprefix); if (submap == null) { submap = Maps.newLinkedHashMap(); result.put(keyprefix, submap); } submap.put(keysuffix, value); } return result; } /** * Resolves the download url, given an EntityDriver, with the following rules: * <ol> * <li>If url is not null, split and trim it on ";" and use * <li>If url is null, retrive entity's Attributes.DOWNLOAD_URL and use if non-null * <li>If fallbackUrl is not null, split and trim it on ";" and use * <ol> * * For each of the resulting Strings, transforms them (using freemarker syntax for * substitutions). Returns the list. */ private static abstract class Rule { private final String url; private final String fallbackUrl; Rule(String url, String fallbackUrl) { this.url = url; this.fallbackUrl = fallbackUrl; } abstract boolean matches(EntityDriver driver, String addon); DownloadTargets resolve(DownloadRequirement req) { EntityDriver driver = req.getEntityDriver(); List<String> primaries = Lists.newArrayList(); List<String> fallbacks = Lists.newArrayList(); if (Strings.isEmpty(url)) { String defaulturl = driver.getEntity().getAttribute(Attributes.DOWNLOAD_URL); if (defaulturl != null) primaries.add(defaulturl); } else { String[] parts = url.split(";"); for (String part : parts) { if (!part.isEmpty()) primaries.add(part.trim()); } } if (fallbackUrl != null) { String[] parts = fallbackUrl.split(";"); for (String part : parts) { if (!part.isEmpty()) fallbacks.add(part.trim()); } } BasicDownloadTargets.Builder result = BasicDownloadTargets.builder(); for (String baseurl : primaries) { result.addPrimary(DownloadSubstituters.substitute(req, baseurl)); } for (String baseurl : fallbacks) { result.addFallback(DownloadSubstituters.substitute(req, baseurl)); } return result.build(); } } /** * Rule for generating URLs that applies to all entities, if a more specific rule * did not exist or failed to find a match. */ private static class UniversalRule extends Rule { UniversalRule(String url, String fallbackUrl) { super(url, fallbackUrl); } @Override boolean matches(EntityDriver driver, String addon) { return true; } } /** * Rule for generating URLs that applies to only the entity of the given type. */ private static class EntitySpecificRule extends Rule { private final String entityType; EntitySpecificRule(String entityType, String url, String fallbackUrl) { super(url, fallbackUrl); this.entityType = checkNotNull(entityType, "entityType"); } @Override boolean matches(EntityDriver driver, String addon) { String actualType = driver.getEntity().getEntityType().getName(); String actualSimpleType = actualType.substring(actualType.lastIndexOf(".")+1); return addon == null && entityType.equalsIgnoreCase(actualSimpleType); } } /** * Rule for generating URLs that applies to only the entity of the given type. */ private static class EntityAddonSpecificRule extends Rule { private final String entityType; private final String addonName; EntityAddonSpecificRule(String entityType, String addonName, String url, String fallbackUrl) { super(url, fallbackUrl); this.entityType = checkNotNull(entityType, "entityType"); this.addonName = checkNotNull(addonName, "addonName"); } @Override boolean matches(EntityDriver driver, String addon) { String actualType = driver.getEntity().getEntityType().getName(); String actualSimpleType = actualType.substring(actualType.lastIndexOf(".")+1); return addonName.equals(addon) && entityType.equalsIgnoreCase(actualSimpleType); } } }