/*
* Copyright 2013 NGDATA nv
* Copyright 2007 Outerthought bvba and Schaubroeck nv
*
* 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.
*/
package org.lilyproject.runtime.configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.lilyproject.runtime.conf.Conf;
import org.lilyproject.runtime.conf.ConfImpl;
import org.lilyproject.runtime.configuration.ConfSource.CachedConfig;
import org.lilyproject.runtime.rapi.ConfListener;
import org.lilyproject.runtime.rapi.ConfListener.ChangeType;
import org.lilyproject.runtime.rapi.ConfNotFoundException;
import org.lilyproject.runtime.rapi.ConfRegistry;
import org.lilyproject.util.location.LocationImpl;
public class ConfRegistryImpl implements ConfRegistry {
private String name;
private List<ConfSource> sources;
private ConfPath root = new ConfPath();
private List<ListenerHandle> listeners = new ArrayList<ListenerHandle>();
private Log log = LogFactory.getLog(getClass());
private static int INITIAL_CACHE_SIZE = 16;
private static float CACHE_LOAD_FACTOR = .75f;
private static int CACHE_CONCURRENCY_LEVEL = 1;
private static final Conf EMPTY_CONF = new ConfImpl("<empty>", new LocationImpl(null, "<generated>", -1, -1));
public ConfRegistryImpl(String name, List<ConfSource> sources) {
this.name = name;
this.sources = sources;
}
/**
* Checks for configuration changes on disk and reloads as necessary.
*
* <p>If there are listeners to be notified of changes, this will not happen immediately,
* but a Runnable will be returned, executing this Runnable will notify the listeners.
* The purpose of this is that the notification of listeners would not block the
* refreshing performed by the ConfManager (ConfListener's should return quickly,
* but you never know). The ConfManager might decide to either run the Runnable
* after refreshing all ConfRegistry's, possibly executing them on a background thread.
*/
public Runnable refresh() {
// Refresh the lower-level conf registries
for (ConfSource source : sources) {
source.refresh();
}
// Determine list of all available configuration paths
Set<String> paths = new HashSet<String>();
for (ConfSource source : sources) {
paths.addAll(source.getPaths());
}
Map<ChangeType, Set<String>> changesByType = new EnumMap<ChangeType, Set<String>>(ChangeType.class);
for (ChangeType changeType : ChangeType.values()) {
changesByType.put(changeType, new HashSet<String>());
}
// Delete confs which no longer exist
root.removeUnexistingChildren(paths, "", changesByType);
// Update/add the existing/new confs
for (String path : paths) {
String[] parsedPath = path.split("/"); // we assume our sources only deliver clean paths without empty path segments
ConfPath confPath = root.getConfPath(parsedPath, 0);
boolean changes;
if (confPath != null && confPath.conf != null) {
changes = confPath.conf.refresh();
} else {
MergedConfig config = new MergedConfig(path);
config.refresh();
root.addConf(parsedPath, 0, config, changesByType);
changes = true; // a new config is always a change
}
if (changes) {
changesByType.get(ChangeType.CONF_CHANGE).add(path);
}
}
return getListenerNotificationRunnable(changesByType);
}
public Conf getConfiguration(String path) {
return getConfiguration(path, true);
}
public Conf getConfiguration(String path, boolean create) {
return getConfiguration(path, create, true);
}
public Conf getConfiguration(String path, boolean create, boolean silent) {
ConfPath confPath = root.getConfPath(parsePath(path), 0);
MergedConfig mergedConfig = confPath == null ? null : confPath.conf;
if (mergedConfig == null || mergedConfig.conf == null /* will never be the case, but anyway */) {
if (create) {
return EMPTY_CONF;
} else if (silent) {
return null;
} else {
throw new ConfNotFoundException("Configuration \"" + path + "\" not found.");
}
}
return mergedConfig.conf;
}
private String[] parsePath(String path) {
String[] parts = path.split("/");
List<String> result = new ArrayList<String>(parts.length);
for (String part : parts) {
part = part.trim();
if (part.length() > 0) {
result.add(part);
}
}
return result.toArray(new String[result.size()]);
}
private String formatPath(String[] parts, int upTo) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i <= upTo; i++) {
if (i > 0) {
builder.append("/");
}
builder.append(parts[i]);
}
return builder.toString();
}
public Collection<String> getConfigurations(String path) {
ConfPath confPath = root.getConfPath(parsePath(path), 0);
if (confPath != null) {
Collection<String> childConfNames = new ArrayList<String>(confPath.children.size());
for (Map.Entry<String, ConfPath> child : confPath.children.entrySet()) {
if (child.getValue().conf != null) {
childConfNames.add(child.getKey());
}
}
return childConfNames;
} else {
return Collections.emptySet();
}
}
public synchronized void addListener(ConfListener listener, String path, ConfListener.ChangeType... types) {
EnumSet<ChangeType> typesSet = EnumSet.noneOf(ChangeType.class);
typesSet.addAll(Arrays.asList(types));
listeners.add(new ListenerHandle(listener, path, typesSet));
}
public void removeListener(ConfListener listener) {
Iterator<ListenerHandle> listenersIt = listeners.iterator();
while (listenersIt.hasNext()) {
ListenerHandle handle = listenersIt.next();
if (handle.listener == listener) {
listenersIt.remove();
}
}
}
private class MergedConfig {
private Long[] lastModifieds;
private Conf conf;
private String path;
public MergedConfig(String path) {
this.path = path;
this.lastModifieds = new Long[sources.size()];
}
public boolean refresh() {
boolean changes = false;
for (int i = 0; i < sources.size(); i++) {
CachedConfig cachedConfig = sources.get(i).get(path);
if (cachedConfig == null) {
if (lastModifieds[i] != null) {
lastModifieds[i] = null;
changes = true;
}
} else if (lastModifieds[i] == null || cachedConfig.lastModified != lastModifieds[i]) {
lastModifieds[i] = cachedConfig.lastModified;
changes = true;
}
}
if (changes) {
List<ConfImpl> confs = new ArrayList<ConfImpl>();
for (ConfSource source : sources) {
CachedConfig cachedConfig = source.get(path);
if (cachedConfig != null) {
confs.add(cachedConfig.conf);
}
}
// Merge the confs
while (confs.size() >= 2) {
// Replace the last 2 confs in the list by a merged conf.
ConfImpl parent = confs.remove(confs.size() - 1);
ConfImpl child = confs.remove(confs.size() - 1);
child.inherit(parent);
confs.add(child);
}
conf = confs.get(0);
}
return changes;
}
public Conf getConfiguration() {
return conf;
}
}
private class ConfPath {
private Map<String, ConfPath> children = new ConcurrentHashMap<String, ConfPath>(
INITIAL_CACHE_SIZE,CACHE_LOAD_FACTOR, CACHE_CONCURRENCY_LEVEL);
private MergedConfig conf;
public void removeUnexistingChildren(Set<String> availablePaths, String currentPath,
Map<ChangeType, Set<String>> changes) {
Iterator<Map.Entry<String, ConfPath>> childrenIt = children.entrySet().iterator();
while (childrenIt.hasNext()) {
Map.Entry<String, ConfPath> childEntry = childrenIt.next();
ConfPath child = childEntry.getValue();
int oldConfChildCount = child.getConfChildren().size();
String childPath = currentPath + childEntry.getKey();
child.removeUnexistingChildren(availablePaths, childPath + "/", changes);
if (!availablePaths.contains(childPath) && child.conf != null) {
child.conf = null;
changes.get(ChangeType.CONF_CHANGE).add(childPath);
}
if (child.children.size() == 0 && child.conf == null) {
if (oldConfChildCount > 0) {
changes.get(ChangeType.PATH_CHANGE).add(childPath);
}
childrenIt.remove();
} else if (child.getConfChildren().size() != oldConfChildCount) {
changes.get(ChangeType.PATH_CHANGE).add(childPath);
}
}
}
public ConfPath getConfPath(String[] path, int pathPos) {
ConfPath child = children.get(path[pathPos]);
if (child == null) {
return null;
} else if (pathPos == path.length -1) {
return child;
} else {
return child.getConfPath(path, pathPos + 1);
}
}
public void addConf(String[] path, int pathPos, MergedConfig conf, Map<ChangeType, Set<String>> changes) {
if (pathPos == path.length - 1) {
ConfPath confPath = new ConfPath();
confPath.conf = conf;
children.put(path[pathPos], confPath);
} else {
ConfPath child = children.get(path[pathPos]);
if (child == null) {
child = new ConfPath();
children.put(path[pathPos], child);
if (pathPos == path.length - 2) {
// We created a new path which will contain a conf, we need to notify of this
changes.get(ChangeType.PATH_CHANGE).add(formatPath(path, pathPos));
}
}
child.addConf(path, pathPos + 1, conf, changes);
}
}
/**
* Return the names of the children who have a conf, thus are not just a path.
*/
public Collection<String> getConfChildren() {
Collection<String> childConfNames = new ArrayList<String>(children.size());
for (Map.Entry<String, ConfPath> child : children.entrySet()) {
if (child.getValue().conf != null) {
childConfNames.add(child.getKey());
}
}
return childConfNames;
}
}
private static class ListenerHandle {
private String path;
private ConfListener listener;
private Set<ChangeType> types;
public ListenerHandle(ConfListener listener, String path, Set<ChangeType> types) {
this.listener = listener;
this.path = path;
this.types = types;
}
}
private Runnable getListenerNotificationRunnable(final Map<ChangeType, Set<String>> changes) {
boolean anyChanges = false;
for (ChangeType changeType : ChangeType.values()) {
if (changes.get(changeType).size() > 0) {
anyChanges = true;
break;
}
}
if (!anyChanges) {
return null;
}
return new Runnable() {
public void run() {
notifyListeners(changes);
}
};
}
private void notifyListeners(Map<ChangeType, Set<String>> changes) {
if (log.isDebugEnabled()) {
log.debug("There are configuration changes in " + name + ", will notify " + listeners.size() + " listeners.");
}
for (ListenerHandle listener : listeners) {
if (listener.path == null) {
// No path specified, listener wants to listen to changes for all paths
for (ChangeType changeType : ChangeType.values()) {
if (listener.types.contains(changeType)) {
notifyChanges(changes.get(changeType), listener, changeType);
}
}
} else {
for (ChangeType changeType : ChangeType.values()) {
if (listener.types.contains(changeType) && changes.get(changeType).contains(listener.path)) {
notifyChanges(Collections.singleton(listener.path), listener, changeType);
}
}
}
}
}
private void notifyChanges(Set<String> paths, ListenerHandle listener, ChangeType changeType) {
for (String path : paths) {
try {
if (log.isDebugEnabled()) {
log.debug("Notifying changes of type " + changeType + " for path " + path + " in " + name
+ " to listener " + listener.listener);
}
listener.listener.confAltered(path, changeType);
} catch (Throwable t) {
log.error("Error while notifying configuration change of type " + changeType + " for path \""
+ path + "\" in " + name, t);
}
}
}
}