/*
* 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.catalog.internal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.catalog.internal.CatalogClasspathDo.CatalogScanningModes;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.javalang.AggregateClassLoader;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.time.CountdownTimer;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
public class CatalogDo {
private static final Logger log = LoggerFactory.getLogger(CatalogDo.class);
volatile boolean isLoaded = false;
final CatalogDto dto;
ManagementContext mgmt = null;
CatalogDo parent = null;
List<CatalogDo> childrenCatalogs = new ArrayList<CatalogDo>();
CatalogClasspathDo classpath;
private Map<String, CatalogItemDo<?,?>> cacheById;
AggregateClassLoader childrenClassLoader = AggregateClassLoader.newInstanceWithNoLoaders();
ClassLoader recursiveClassLoader;
protected CatalogDo(CatalogDto dto) {
this.dto = Preconditions.checkNotNull(dto);
}
public CatalogDo(ManagementContext mgmt, CatalogDto dto) {
this(dto);
this.mgmt = mgmt;
}
boolean isLoaded() {
return isLoaded;
}
/** Calls {@link #load(CatalogDo)} with a null parent. */
public CatalogDo load() {
return load(null);
}
/** Calls {@link #load(ManagementContext, CatalogDo)} with the catalog's existing management context. */
public CatalogDo load(CatalogDo parent) {
return load(mgmt, parent);
}
/** Calls {@link #load(ManagementContext, CatalogDo, boolean)} failing on load errors. */
public synchronized CatalogDo load(ManagementContext mgmt, CatalogDo parent) {
return load(mgmt, parent, true);
}
/** causes all URL-based catalogs to have their manifests loaded,
* and all scanning-based classpaths to scan the classpaths
* (but does not load all JARs)
*/
public synchronized CatalogDo load(ManagementContext mgmt, CatalogDo parent, boolean failOnLoadError) {
if (isLoaded()) {
if (mgmt!=null && !Objects.equal(mgmt, this.mgmt)) {
throw new IllegalStateException("Cannot set mgmt "+mgmt+" on "+this+" after catalog is loaded");
}
log.debug("Catalog "+this+" is already loaded");
return this;
}
loadThisCatalog(mgmt, parent, failOnLoadError);
loadChildrenCatalogs(failOnLoadError);
buildCaches();
return this;
}
protected synchronized void loadThisCatalog(ManagementContext mgmt, CatalogDo parent, boolean failOnLoadError) {
if (isLoaded()) return;
CatalogUtils.logDebugOrTraceIfRebinding(log, "Loading catalog {} into {}", this, parent);
if (this.parent!=null && !this.parent.equals(parent))
log.warn("Catalog "+this+" being initialised with different parent "+parent+" when already parented by "+this.parent, new Throwable("source of reparented "+this));
if (this.mgmt!=null && !this.mgmt.equals(mgmt))
log.warn("Catalog "+this+" being initialised with different mgmt "+mgmt+" when already managed by "+this.mgmt, new Throwable("source of reparented "+this));
this.parent = parent;
this.mgmt = mgmt;
dto.populate();
loadCatalogClasspath();
loadCatalogItems(failOnLoadError);
isLoaded = true;
synchronized (this) {
notifyAll();
}
}
private void loadCatalogClasspath() {
try {
classpath = new CatalogClasspathDo(this);
classpath.load();
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
log.error("Unable to load catalog "+this+" (ignoring): "+e);
log.info("Trace for failure to load "+this+": "+e, e);
}
}
private void loadCatalogItems(boolean failOnLoadError) {
Iterable<CatalogItemDtoAbstract<?, ?>> entries = dto.getUniqueEntries();
if (entries!=null) {
for (CatalogItemDtoAbstract<?,?> entry : entries) {
try {
CatalogUtils.installLibraries(mgmt, entry.getLibraries());
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
if (failOnLoadError) {
Exceptions.propagate(e);
} else {
log.error("Loading bundles for catalog item " + entry + " failed: " + e.getMessage(), e);
}
}
}
}
}
public boolean blockIfNotLoaded(Duration timeout) throws InterruptedException {
if (isLoaded()) return true;
synchronized (this) {
if (isLoaded()) return true;
CountdownTimer timer = CountdownTimer.newInstanceStarted(timeout);
while (!isLoaded())
if (!timer.waitOnForExpiry(this))
return false;
return true;
}
}
protected void loadChildrenCatalogs(boolean failOnLoadError) {
if (dto.catalogs!=null) {
for (CatalogDto child: dto.catalogs) {
loadCatalog(child, failOnLoadError);
}
}
}
CatalogDo loadCatalog(CatalogDto child, boolean failOnLoadError) {
CatalogDo childL = new CatalogDo(child);
childrenCatalogs.add(childL);
childL.load(mgmt, this, failOnLoadError);
childrenClassLoader.addFirst(childL.getRecursiveClassLoader());
clearCache(false);
return childL;
}
protected Map<String, CatalogItemDo<?,?>> getIdCache() {
Map<String, CatalogItemDo<?,?>> cache = this.cacheById;
if (cache==null) cache = buildCaches();
return cache;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected synchronized Map<String, CatalogItemDo<?,?>> buildCaches() {
if (cacheById != null) return cacheById;
CatalogUtils.logDebugOrTraceIfRebinding(log, "Building cache for {}", this);
if (!isLoaded())
log.debug("Catalog not fully loaded when loading cache of "+this);
Map<String, CatalogItemDo<?,?>> cache = new LinkedHashMap<String, CatalogItemDo<?,?>>();
// build the cache; first from children catalogs, then from local entities
// so that root and near-root takes precedence over deeper items;
// and go through in reverse order so that things at the top of the file take precedence
// (both in the cache and in the aggregate class loader);
// however anything added _subsequently_ will take precedence (again in both)
if (dto.catalogs!=null) {
List<CatalogDo> catalogsReversed = new ArrayList<CatalogDo>(childrenCatalogs);
Collections.reverse(catalogsReversed);
for (CatalogDo child: catalogsReversed) {
cache.putAll(child.getIdCache());
}
}
if (dto.getUniqueEntries()!=null) {
List<CatalogItemDtoAbstract<?,?>> entriesReversed = MutableList.copyOf(dto.getUniqueEntries());
Collections.reverse(entriesReversed);
for (CatalogItemDtoAbstract<?,?> entry: entriesReversed)
cache.put(entry.getId(), new CatalogItemDo(this, entry));
}
this.cacheById = cache;
return cache;
}
protected synchronized void clearCache(boolean deep) {
this.cacheById = null;
if (deep) {
for (CatalogDo child : childrenCatalogs) {
child.clearCache(true);
}
}
clearParentCache();
}
protected void clearParentCache() {
if (this.parent!=null)
this.parent.clearCache(false);
}
/**
* Adds the given entry to the catalog, with no enrichment.
* Callers may prefer {@link CatalogClasspathDo#addCatalogEntry(CatalogItemDtoAbstract, Class)}
*/
public synchronized void addEntry(CatalogItemDtoAbstract<?,?> entry) {
dto.addEntry(entry);
// could do clearCache(false); but this is slightly more efficient...
if (cacheById != null) {
@SuppressWarnings({ "unchecked", "rawtypes" })
CatalogItemDo<?, ?> cdo = new CatalogItemDo(this, entry);
cacheById.put(entry.getId(), cdo);
}
clearParentCache();
if (mgmt != null) {
mgmt.getRebindManager().getChangeListener().onManaged(entry);
}
}
/**
* Removes the given entry from the catalog.
*/
public synchronized void deleteEntry(CatalogItemDtoAbstract<?, ?> entry) {
dto.removeEntry(entry);
// could do clearCache(false); but this is slightly more efficient...
if (cacheById != null) {
cacheById.remove(entry.getId());
}
clearParentCache();
if (mgmt != null) {
// TODO: Can the entry be in more than one catalogue? The management context has no notion of
// catalogue hierarchy so this will effectively remove it from all catalogues.
// (YES- we're assuming ID's are unique across all catalogues; if not, things get out of sync;
// however see note at top of BasicBrooklynCatalog --
// manualCatalog and OSGi is used for everything now except legacy XML trees)
mgmt.getRebindManager().getChangeListener().onUnmanaged(entry);
}
}
/** returns loaded catalog, if this has been loaded */
CatalogDo addCatalog(CatalogDto child) {
if (dto.catalogs == null)
dto.catalogs = new ArrayList<CatalogDto>();
dto.catalogs.add(child);
if (!isLoaded())
return null;
return loadCatalog(child, true);
}
/** adds the given urls; filters out any nulls supplied */
public synchronized void addToClasspath(String ...urls) {
if (dto.classpath == null)
dto.classpath = new CatalogClasspathDto();
for (String url: urls) {
if (url!=null)
dto.classpath.addEntry(url);
}
if (isLoaded())
throw new IllegalStateException("dynamic classpath entry value update not supported");
// easy enough to add, just support unload+reload (and can also allow dynamic setScan below)
// but more predictable if we don't; the one exception is in the manualAdditionsCatalog
// where BasicBrooklynCatalog reaches in and updates the DTO and/or CompositeClassLoader directly, if necessary
// for (String url: urls)
// loadedClasspath.addEntry(url);
}
public synchronized void setClasspathScanForEntities(CatalogScanningModes value) {
if (dto.classpath == null)
dto.classpath = new CatalogClasspathDto();
dto.classpath.scan = value;
if (isLoaded())
throw new IllegalStateException("dynamic classpath scan value update not supported");
// easy enough to add, see above
}
@Override
public String toString() {
String size = cacheById == null ? "not yet loaded" : "size " + cacheById.size();
return "Loaded:" + dto + "(" + size + ")";
}
/** is "local" if it and all ancestors are not based on any remote urls */
public boolean isLocal() {
if (dto.url != null) {
String proto = Urls.getProtocol(dto.url);
if (proto != null) {
// 'file' is the only protocol accepted as "local"
if (!"file".equals(proto)) return false;
}
}
return parent == null || parent.isLocal();
}
/** classloader for only the entries in this catalog's classpath */
public ClassLoader getLocalClassLoader() {
if (classpath != null) return classpath.getLocalClassLoader();
return null;
}
/** recursive classloader is the local classloader plus all children catalog's classloader */
public ClassLoader getRecursiveClassLoader() {
if (recursiveClassLoader == null) loadRecursiveClassLoader();
return recursiveClassLoader;
}
protected synchronized void loadRecursiveClassLoader() {
if (recursiveClassLoader!=null) return;
AggregateClassLoader cl = AggregateClassLoader.newInstanceWithNoLoaders();
cl.addFirst(childrenClassLoader);
ClassLoader local = getLocalClassLoader();
if (local != null) cl.addFirst(local);
if (parent == null) {
// we are root. include the mgmt base classloader and/or standard class loaders
ClassLoader base = mgmt != null ? ((ManagementContextInternal)mgmt).getBaseClassLoader() : null;
if (base != null) cl.addFirst(base);
else {
cl.addFirst(getClass().getClassLoader());
cl.addFirst(Object.class.getClassLoader());
}
}
recursiveClassLoader = cl;
}
/** the root classloader is the recursive CL from the outermost catalog
* (which includes the base classloader from the mgmt context, if set) */
public ClassLoader getRootClassLoader() {
if (parent != null) return parent.getRootClassLoader();
return getRecursiveClassLoader();
}
}