/*
* 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.location;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ServiceLoader;
import java.util.Set;
import org.apache.brooklyn.api.catalog.BrooklynCatalog;
import org.apache.brooklyn.api.catalog.CatalogItem;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationDefinition;
import org.apache.brooklyn.api.location.LocationRegistry;
import org.apache.brooklyn.api.location.LocationResolver;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.config.ConfigMap;
import org.apache.brooklyn.core.config.ConfigPredicates;
import org.apache.brooklyn.core.config.ConfigUtils;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.apache.brooklyn.core.mgmt.internal.LocalLocationManager;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.guava.Maybe.Absent;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import org.apache.brooklyn.util.text.WildcardGlobs;
import org.apache.brooklyn.util.text.WildcardGlobs.PhraseTreatment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
/**
* See {@link LocationRegistry} for general description.
* <p>
* TODO The relationship between the catalog and the location registry is a bit messy.
* For all existing code, the location registry is the definitive way to resolve
* locations.
* <p>
* Any location item added to the catalog must therefore be registered here.
* Whenever an item is added to the catalog, it will automatically call
* {@link #updateDefinedLocation(RegisteredType)}. Similarly, when a location
* is deleted from the catalog it will call {@link #removeDefinedLocation(RegisteredType)}.
* <p>
* However, the location item in the catalog has an unparsed blob of YAML, which contains
* important things like the type and the config of the location. This is only parsed when
* {@link BrooklynCatalog#createSpec(CatalogItem)} is called. We therefore jump through
* some hoops to wire together the catalog and the registry.
* <p>
* To add a location to the catalog, and then to resolve a location that is in the catalog,
* it goes through the following steps:
*
* <ol>
* <li>Call {@link BrooklynCatalog#addItems(String)}
* <ol>
* <li>This automatically calls {@link #updateDefinedLocation(RegisteredType)}
* <li>A LocationDefinition is creating, using as its id the {@link RegisteredType#getSymbolicName()}.
* The definition's spec is {@code brooklyn.catalog:<symbolicName>:<version>},
* </ol>
* <li>A blueprint can reference the catalog item using its symbolic name,
* such as the YAML {@code location: my-new-location}.
* (this feels similar to the "named locations").
* <ol>
* <li>This automatically calls {@link #resolve(String)}.
* <li>The LocationDefinition is found by lookig up this name.
* <li>The {@link LocationDefiniton.getSpec()} is retrieved; the right {@link LocationResolver} is
* found for it.
* <li>This uses the {@link CatalogLocationResolver}, because the spec starts with {@code brooklyn.catalog:}.
* <li>This resolver extracts from the spec the <symobolicName>:<version>, and looks up the
* location item using the {@link BrooklynTypeRegistry}.
* <li>It then creates a {@link LocationSpec} by calling {@link BrooklynTypeRegistry#createSpec(RegisteredType)}.
* <ol>
* <li>This first tries to use the type (that is in the YAML) as a simple Java class.
* <li>If that fails, it will resolve the type using {@link #resolve(String, Boolean, Map)}, which
* returns an actual location object.
* <li>It extracts from that location object the appropriate metadata to create a {@link LocationSpec},
* returns the spec and discards the location object.
* </ol>
* <li>The resolver creates the {@link Location} from the {@link LocationSpec}
* </ol>
* </ol>
*
* TODO There is no concept of a location version in this registry. The version
* in the catalog is generally ignored.
*/
@SuppressWarnings({"rawtypes","unchecked"})
public class BasicLocationRegistry implements LocationRegistry {
// TODO save / serialize
// (we persist live locations, ie those in the LocationManager, but not "catalog" locations, ie those in this Registry)
public static final Logger log = LoggerFactory.getLogger(BasicLocationRegistry.class);
/**
* Splits a comma-separated list of locations (names or specs) into an explicit list.
* The splitting is very careful to handle commas embedded within specs, to split correctly.
*/
public static List<String> expandCommaSeparateLocations(String locations) {
return WildcardGlobs.getGlobsAfterBraceExpansion("{"+locations+"}", false, PhraseTreatment.INTERIOR_NOT_EXPANDABLE, PhraseTreatment.INTERIOR_NOT_EXPANDABLE);
// don't do this, it tries to expand commas inside parentheses which is not good!
// QuotedStringTokenizer.builder().addDelimiterChars(",").buildList((String)id);
}
private final ManagementContext mgmt;
/** map of defined locations by their ID */
private final Map<String,LocationDefinition> definedLocations = new LinkedHashMap<String, LocationDefinition>();
protected final Map<String,LocationResolver> resolvers = new LinkedHashMap<String, LocationResolver>();
private final Set<String> specsWarnedOnException = Sets.newConcurrentHashSet();
public BasicLocationRegistry(ManagementContext mgmt) {
this.mgmt = checkNotNull(mgmt, "mgmt");
findServices();
updateDefinedLocations();
}
protected void findServices() {
ServiceLoader<LocationResolver> loader = ServiceLoader.load(LocationResolver.class, mgmt.getCatalogClassLoader());
MutableList<LocationResolver> loadedResolvers;
try {
loadedResolvers = MutableList.copyOf(loader);
} catch (Throwable e) {
log.warn("Error loading resolvers (rethrowing): "+e);
throw Exceptions.propagate(e);
}
for (LocationResolver r: loadedResolvers) {
registerResolver(r);
}
if (log.isDebugEnabled()) log.debug("Location resolvers are: "+resolvers);
if (resolvers.isEmpty()) log.warn("No location resolvers detected: is src/main/resources correctly included?");
}
/** Registers the given resolver, invoking {@link LocationResolver#init(ManagementContext)} on the argument
* and returning true, unless the argument indicates false for {@link LocationResolver.EnableableLocationResolver#isEnabled()} */
public boolean registerResolver(LocationResolver r) {
r.init(mgmt);
if (r instanceof LocationResolver.EnableableLocationResolver) {
if (!((LocationResolver.EnableableLocationResolver)r).isEnabled()) {
return false;
}
}
resolvers.put(r.getPrefix(), r);
return true;
}
@Override
public Map<String,LocationDefinition> getDefinedLocations() {
synchronized (definedLocations) {
return ImmutableMap.<String,LocationDefinition>copyOf(definedLocations);
}
}
@Override
public LocationDefinition getDefinedLocationById(String id) {
return definedLocations.get(id);
}
@Override
public LocationDefinition getDefinedLocationByName(String name) {
synchronized (definedLocations) {
for (LocationDefinition l: definedLocations.values()) {
if (l.getName().equals(name)) return l;
}
return null;
}
}
@Override
public void updateDefinedLocation(LocationDefinition l) {
synchronized (definedLocations) {
definedLocations.put(l.getId(), l);
}
}
/**
* Converts the given item from the catalog into a LocationDefinition, and adds it
* to the registry (overwriting anything already registered with the id
* {@link CatalogItem#getCatalogItemId()}.
*/
public void updateDefinedLocation(CatalogItem<Location, LocationSpec<?>> item) {
String id = item.getCatalogItemId();
String symbolicName = item.getSymbolicName();
String spec = CatalogLocationResolver.NAME + ":" + id;
Map<String, Object> config = ImmutableMap.<String, Object>of();
BasicLocationDefinition locDefinition = new BasicLocationDefinition(symbolicName, symbolicName, spec, config);
updateDefinedLocation(locDefinition);
}
/**
* Converts the given item from the catalog into a LocationDefinition, and adds it
* to the registry (overwriting anything already registered with the id
* {@link RegisteredType#getId()}.
*/
public void updateDefinedLocation(RegisteredType item) {
String id = item.getId();
String symbolicName = item.getSymbolicName();
String spec = CatalogLocationResolver.NAME + ":" + id;
Map<String, Object> config = ImmutableMap.<String, Object>of();
BasicLocationDefinition locDefinition = new BasicLocationDefinition(symbolicName, symbolicName, spec, config);
updateDefinedLocation(locDefinition);
}
public void removeDefinedLocation(CatalogItem<Location, LocationSpec<?>> item) {
removeDefinedLocation(item.getSymbolicName());
}
@Override
public void removeDefinedLocation(String id) {
LocationDefinition removed;
synchronized (definedLocations) {
removed = definedLocations.remove(id);
}
if (removed == null && log.isDebugEnabled()) {
log.debug("{} was asked to remove location with id {} but no such location was registered", this, id);
}
}
public void updateDefinedLocations() {
synchronized (definedLocations) {
// first read all properties starting brooklyn.location.named.xxx
// (would be nice to move to a better way, e.g. yaml, then deprecate this approach, but first
// we need ability/format for persisting named locations, and better support for adding+saving via REST/GUI)
int count = 0;
String NAMED_LOCATION_PREFIX = "brooklyn.location.named.";
ConfigMap namedLocationProps = mgmt.getConfig().submap(ConfigPredicates.nameStartsWith(NAMED_LOCATION_PREFIX));
for (String k: namedLocationProps.asMapWithStringKeys().keySet()) {
String name = k.substring(NAMED_LOCATION_PREFIX.length());
// If has a dot, then is a sub-property of a named location (e.g. brooklyn.location.named.prod1.user=bob)
if (!name.contains(".")) {
// this is a new named location
String spec = (String) namedLocationProps.asMapWithStringKeys().get(k);
// make up an ID
String id = Identifiers.makeRandomId(8);
Map<String, Object> config = ConfigUtils.filterForPrefixAndStrip(namedLocationProps.asMapWithStringKeys(), k+".");
definedLocations.put(id, new BasicLocationDefinition(id, name, spec, config));
count++;
}
}
if (log.isDebugEnabled())
log.debug("Found "+count+" defined locations from properties (*.named.* syntax): "+definedLocations.values());
if (getDefinedLocationByName("localhost")==null && !BasicOsDetails.Factory.newLocalhostInstance().isWindows()
&& LocationConfigUtils.isEnabled(mgmt, "brooklyn.location.localhost")) {
log.debug("Adding a defined location for localhost");
// add 'localhost' *first*
ImmutableMap<String, LocationDefinition> oldDefined = ImmutableMap.copyOf(definedLocations);
definedLocations.clear();
String id = Identifiers.makeRandomId(8);
definedLocations.put(id, localhost(id));
definedLocations.putAll(oldDefined);
}
for (RegisteredType item: mgmt.getTypeRegistry().getMatching(RegisteredTypePredicates.IS_LOCATION)) {
updateDefinedLocation(item);
count++;
}
}
}
@VisibleForTesting
void disablePersistence() {
// persistence isn't enabled yet anyway (have to manually save things,
// defining the format and file etc)
}
protected static BasicLocationDefinition localhost(String id) {
return new BasicLocationDefinition(id, "localhost", "localhost", null);
}
/** to catch circular references */
protected ThreadLocal<Set<String>> specsSeen = new ThreadLocal<Set<String>>();
@Override @Deprecated
public boolean canMaybeResolve(String spec) {
return getSpecResolver(spec) != null;
}
@Override
public final Location resolve(String spec) {
return resolve(spec, true, null).get();
}
@Override @Deprecated
public final Location resolveIfPossible(String spec) {
if (!canMaybeResolve(spec)) return null;
return resolve(spec, null, null).orNull();
}
@Deprecated /** since 0.7.0 not used */
public final Maybe<Location> resolve(String spec, boolean manage) {
return resolve(spec, manage, null);
}
public Maybe<Location> resolve(String spec, Boolean manage, Map locationFlags) {
try {
locationFlags = MutableMap.copyOf(locationFlags);
if (manage!=null) {
locationFlags.put(LocalLocationManager.CREATE_UNMANAGED, !manage);
}
Set<String> seenSoFar = specsSeen.get();
if (seenSoFar==null) {
seenSoFar = new LinkedHashSet<String>();
specsSeen.set(seenSoFar);
}
if (seenSoFar.contains(spec))
return Maybe.absent(Suppliers.ofInstance(new IllegalStateException("Circular reference in definition of location '"+spec+"' ("+seenSoFar+")")));
seenSoFar.add(spec);
LocationResolver resolver = getSpecResolver(spec);
if (resolver != null) {
try {
return Maybe.of(resolver.newLocationFromString(locationFlags, spec, this));
} catch (RuntimeException e) {
return Maybe.absent(Suppliers.ofInstance(e));
}
}
// problem: but let's ensure that classpath is sane to give better errors in common IDE bogus case;
// and avoid repeated logging
String errmsg;
if (spec == null || specsWarnedOnException.add(spec)) {
if (resolvers.get("id")==null || resolvers.get("named")==null) {
log.error("Standard location resolvers not installed, location resolution will fail shortly. "
+ "This usually indicates a classpath problem, such as when running from an IDE which "
+ "has not properly copied META-INF/services from src/main/resources. "
+ "Known resolvers are: "+resolvers.keySet());
errmsg = "Unresolvable location '"+spec+"': "
+ "Problem detected with location resolver configuration; "
+ resolvers.keySet()+" are the only available location resolvers. "
+ "More information can be found in the logs.";
} else {
log.debug("Location resolution failed for '"+spec+"' (if this is being loaded it will fail shortly): known resolvers are: "+resolvers.keySet());
errmsg = "Unknown location '"+spec+"': "
+ "either this location is not recognised or there is a problem with location resolver configuration.";
}
} else {
// For helpful log message construction: assumes classpath will not suddenly become wrong; might happen with OSGi though!
if (log.isDebugEnabled()) log.debug("Location resolution failed again for '"+spec+"' (throwing)");
errmsg = "Unknown location '"+spec+"': "
+ "either this location is not recognised or there is a problem with location resolver configuration.";
}
return Maybe.absent(Suppliers.ofInstance(new NoSuchElementException(errmsg)));
} finally {
specsSeen.remove();
}
}
@Override
public final Location resolve(String spec, Map locationFlags) {
return resolve(spec, null, locationFlags).get();
}
protected LocationResolver getSpecResolver(String spec) {
int colonIndex = spec.indexOf(':');
int bracketIndex = spec.indexOf("(");
int dividerIndex = (colonIndex < 0) ? bracketIndex : (bracketIndex < 0 ? colonIndex : Math.min(bracketIndex, colonIndex));
String prefix = dividerIndex >= 0 ? spec.substring(0, dividerIndex) : spec;
LocationResolver resolver = resolvers.get(prefix);
if (resolver == null)
resolver = getSpecDefaultResolver(spec);
return resolver;
}
protected LocationResolver getSpecDefaultResolver(String spec) {
return getSpecFirstResolver(spec, "id", "named", "jclouds");
}
protected LocationResolver getSpecFirstResolver(String spec, String ...resolversToCheck) {
for (String resolverId: resolversToCheck) {
LocationResolver resolver = resolvers.get(resolverId);
if (resolver!=null && resolver.accepts(spec, this))
return resolver;
}
return null;
}
/** providers default impl for {@link LocationResolver#accepts(String, LocationRegistry)} */
public static boolean isResolverPrefixForSpec(LocationResolver resolver, String spec, boolean argumentRequired) {
if (spec==null) return false;
if (spec.startsWith(resolver.getPrefix()+":")) return true;
if (!argumentRequired && spec.equals(resolver.getPrefix())) return true;
return false;
}
@Override
public List<Location> resolve(Iterable<?> spec) {
List<Location> result = new ArrayList<Location>();
for (Object id : spec) {
if (id==null) {
// drop a null entry
} if (id instanceof String) {
result.add(resolve((String) id));
} else if (id instanceof Location) {
result.add((Location) id);
} else {
if (id instanceof Iterable)
throw new IllegalArgumentException("Cannot resolve '"+id+"' to a location; collections of collections not allowed");
throw new IllegalArgumentException("Cannot resolve '"+id+"' to a location; unsupported type "+
(id == null ? "null" : id.getClass().getName()));
}
}
return result;
}
public List<Location> resolveList(Object l) {
if (l==null) l = Collections.emptyList();
if (l instanceof String) l = JavaStringEscapes.unwrapJsonishListIfPossible((String)l);
if (l instanceof Iterable) return resolve((Iterable<?>)l);
throw new IllegalArgumentException("Location list must be supplied as a collection or a string, not "+
JavaClassNames.simpleClassName(l)+"/"+l);
}
@Override
public Location resolve(LocationDefinition ld) {
return resolve(ld, null, null).get();
}
@Override @Deprecated
public Location resolveForPeeking(LocationDefinition ld) {
// TODO should clean up how locations are stored, figuring out whether they are shared or not;
// or maybe better, the API calls to this might just want to get the LocationSpec objects back
// for now we use a 'CREATE_UNMANGED' flag to prevent management (leaks and logging)
return resolve(ld, ConfigBag.newInstance().configure(LocalLocationManager.CREATE_UNMANAGED, true).getAllConfig());
}
@Override @Deprecated
public Location resolve(LocationDefinition ld, Map<?,?> flags) {
return resolveLocationDefinition(ld, flags, null);
}
/** @deprecated since 0.7.0 not used (and optionalName was ignored anyway) */
@Deprecated
public Location resolveLocationDefinition(LocationDefinition ld, Map locationFlags, String optionalName) {
return resolve(ld, null, locationFlags).get();
}
public Maybe<Location> resolve(LocationDefinition ld, Boolean manage, Map locationFlags) {
ConfigBag newLocationFlags = ConfigBag.newInstance(ld.getConfig())
.putAll(locationFlags)
.putIfAbsentAndNotNull(LocationInternal.NAMED_SPEC_NAME, ld.getName())
.putIfAbsentAndNotNull(LocationInternal.ORIGINAL_SPEC, ld.getName());
Maybe<Location> result = resolve(ld.getSpec(), manage, newLocationFlags.getAllConfigRaw());
if (result.isPresent())
return result;
throw new IllegalStateException("Cannot instantiate location '"+ld+"' pointing at "+ld.getSpec()+": "+
Exceptions.collapseText( ((Absent<?>)result).getException() ));
}
@Override
public Map getProperties() {
return mgmt.getConfig().asMapWithStringKeys();
}
@VisibleForTesting
public void putProperties(Map<String, ?> vals) {
((ManagementContextInternal)mgmt).getBrooklynProperties().putAll(vals);
}
@VisibleForTesting
public static void setupLocationRegistryForTesting(ManagementContext mgmt) {
// ensure localhost is added (even on windows)
LocationDefinition l = mgmt.getLocationRegistry().getDefinedLocationByName("localhost");
if (l==null) mgmt.getLocationRegistry().updateDefinedLocation(
BasicLocationRegistry.localhost(Identifiers.makeRandomId(8)) );
((BasicLocationRegistry)mgmt.getLocationRegistry()).disablePersistence();
}
}