package org.dcache.cells;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.BadVersionException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.AbstractNestablePropertyAccessor;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.beans.PropertyAccessException;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import javax.annotation.concurrent.GuardedBy;
import java.beans.PropertyDescriptor;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import diskCacheV111.util.CacheException;
import dmg.cells.nucleus.CellArgsAware;
import dmg.cells.nucleus.CellCommandListener;
import dmg.cells.nucleus.CellEventListener;
import dmg.cells.nucleus.CellIdentityAware;
import dmg.cells.nucleus.CellInfo;
import dmg.cells.nucleus.CellInfoAware;
import dmg.cells.nucleus.CellInfoProvider;
import dmg.cells.nucleus.CellLifeCycleAware;
import dmg.cells.nucleus.CellMessageReceiver;
import dmg.cells.nucleus.CellMessageSender;
import dmg.cells.nucleus.CellSetupProvider;
import dmg.cells.nucleus.DelayedReply;
import dmg.cells.nucleus.DomainContextAware;
import dmg.cells.nucleus.EnvironmentAware;
import dmg.util.CommandException;
import dmg.util.CommandExitException;
import dmg.util.CommandInterpreter;
import dmg.util.CommandPanicException;
import dmg.util.CommandThrowableException;
import dmg.util.command.Argument;
import dmg.util.command.Command;
import dmg.util.command.Option;
import org.dcache.util.Args;
import org.dcache.util.cli.CommandExecutor;
import org.dcache.vehicles.BeanQueryAllPropertiesMessage;
import org.dcache.vehicles.BeanQueryMessage;
import org.dcache.vehicles.BeanQuerySinglePropertyMessage;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
/**
* Universal cell for building complex cells from simpler components.
*
* This class aims at being a universal cell for dCache. It makes it
* possible to construct complex cells from simpler cell
* components. The Spring Framework is used to manage the lifetime of
* the cell components, as well as the wiring between components. We
* therfore borrow the term "bean" from the Spring Framework and refer
* to cell components as beans.
*
* Beans can get access to core cell functionality by implementing one
* or more of the following interfaces: CellInfoProvider,
* CellCommunicationAware, ThreadFactoryAware, CellCommandListener,
* and CellSetupAware. When instantiated through this class, those
* interfaces are detected and the necessary wiring is performed
* automatically.
*/
public class UniversalSpringCell
extends AbstractCell
implements BeanPostProcessor,
EnvironmentAware
{
private static final Logger LOGGER = LoggerFactory.getLogger(UniversalSpringCell.class);
private static final long WAIT_FOR_FILE_SLEEP = 30000;
private static final Set<Class<?>> PRIMITIVE_TYPES =
Sets.newHashSet(Byte.class, Byte.TYPE, Short.class, Short.TYPE,
Integer.class, Integer.TYPE, Long.class, Long.TYPE,
Float.class, Float.TYPE, Double.class, Double.TYPE,
Character.class, Character.TYPE, Boolean.class,
Boolean.TYPE, String.class);
private static final Class<?>[] TERMINAL_TYPES = new Class<?>[] { Class.class, ApplicationContext.class };
private static final Class<?>[] HIDDEN_TYPES = new Class<?>[] { ApplicationContext.class, AutoCloseable.class };
/**
* Environment map this cell was instantiated in.
*/
private Map<String,Object> _environment = Collections.emptyMap();
/**
* Spring application context. All beans are created through this
* context.
*/
private ConfigurableApplicationContext _context;
/**
* Registered info providers mapped to their bean names. Sorted to
* maintain consistent ordering.
*/
private final Map<String, CellInfoProvider> _infoProviders =
new TreeMap<>();
/**
* List of registered setup providers. Sorted to maintain
* consistent ordering.
*/
private final Map<String, CellSetupProvider> _setupProviders =
new TreeMap<>();
/**
* List of registered life cycle aware beans. Sorted to maintain
* consistent ordering.
*/
private final Map<String, CellLifeCycleAware> _lifeCycleAware =
new TreeMap<>();
/**
* Command interpreter for processing setup files.
*/
private final CommandInterpreter _setupInterpreter = new CommandInterpreter();
/**
* Setup to execute during start and to which to save the setup.
*/
private File _setupFile;
private SetupManager _setupManager;
public UniversalSpringCell(String cellName, String arguments)
{
super(cellName, arguments);
}
@Override
protected Serializable executeCommand(CommandExecutor command, Args args) throws CommandException
{
if (command.getImplementation().isAnnotationPresent(CellSetupProvider.AffectsSetup.class)) {
return _setupManager.execute(command, args);
}
return super.executeCommand(command, args);
}
@Override
public void setEnvironment(Map<String,Object> environment)
{
_environment = environment;
}
@Override
protected void starting() throws Exception
{
super.starting();
/* Process command line arguments.
*/
Args args = getArgs();
checkArgument(args.argc() > 0, "Configuration location missing");
_setupFile =
(!args.hasOption("setupFile"))
? null
: new File(args.getOption("setupFile"));
if (_setupFile != null) {
addCommandListener(new SetupCommandListener());
}
/* The setup may be provided as a setup file on disk or through zookeeper.
*/
if (args.hasOption("setupNode")) {
_setupManager = new ZooKeeperSetupManager(_setupFile, args.getOption("setupNode"));
} else if (_setupFile != null) {
_setupManager = new LocalSetupManager(_setupFile);
} else {
_setupManager = new InMemorySetupManager();
}
/* To ensure that all required file systems are mounted, the
* admin may specify some required files. We will block until
* they become available.
*/
waitForFiles();
/* Instantiate Spring application context. This will
* eagerly instantiate all beans.
*/
createContext();
/* The setup may be provided as static configuration in the
* domain context.
*/
executeSetupContext();
/* Starting the setup manager loads the external setup.
*/
_setupManager.start();
}
@Override
protected void started()
{
/* Run the final initialisation hooks.
*/
for (CellLifeCycleAware bean: _lifeCycleAware.values()) {
bean.afterStart();
}
}
@Override
protected void stopping()
{
super.stopping();
if (_setupManager != null) {
_setupManager.close();
}
for (CellLifeCycleAware bean: _lifeCycleAware.values()) {
bean.beforeStop();
}
}
/**
* Closes the application context, which will shutdown all beans.
*/
@Override
public void stopped()
{
super.stopped();
if (_context != null) {
_context.close();
_context = null;
}
_infoProviders.clear();
_setupProviders.clear();
_lifeCycleAware.clear();
}
private File firstMissing(File[] files)
{
for (File file: files) {
if (!file.exists()) {
return file;
}
}
return null;
}
private void waitForFiles()
throws InterruptedException
{
String s = getArgs().getOpt("waitForFiles");
if (s != null && !s.trim().isEmpty()) {
String[] paths = s.trim().split(":");
File[] files = new File[paths.length];
for (int i = 0; i < paths.length; i++) {
files[i] = new File(paths[i]);
}
File missing;
while ((missing = firstMissing(files)) != null) {
LOGGER.warn("File missing: {}; sleeping {} seconds", missing, WAIT_FOR_FILE_SLEEP / 1000);
Thread.sleep(WAIT_FOR_FILE_SLEEP);
}
}
}
private byte[] loadSetup(File file) throws CommandException
{
try {
byte[] data = readAllBytes(file.toPath());
testSetup(file.toString(), data);
return data;
} catch (IOException e) {
throw new CommandException("Failed to load " + file.toPath() + ": "
+ e.getMessage(), e);
}
}
private void testSetup(String source, byte[] data)
throws CommandException
{
CommandInterpreter mockInterpreter = new CommandInterpreter();
_setupProviders.values().stream().map(CellSetupProvider::mock).forEach(mockInterpreter::addCommandListener);
executeSetup(mockInterpreter, source, data);
}
private void executeSetup(String source, byte[] data)
throws CommandException
{
try {
_lifeCycleAware.values().forEach(CellLifeCycleAware::beforeSetup);
try {
executeSetup(_setupInterpreter, source, data);
} finally {
_lifeCycleAware.values().forEach(CellLifeCycleAware::afterSetup);
}
} catch (RuntimeException e) {
kill();
throw new CommandPanicException("Possible bug detected during setup execution " +
"and service must be restarted: " + e.toString(), e);
}
}
private void executeSetup(CommandInterpreter interpreter, String source, byte[] data)
throws CommandException
{
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(new ByteArrayInputStream(data), UTF_8));
int lineCount = 1;
for (String line = in.readLine(); line != null; line = in.readLine(), lineCount++) {
line = line.trim();
if (line.isEmpty() || line.charAt(0) == '#') {
continue;
}
try {
Serializable result = interpreter.command(new Args(line));
if (result instanceof DelayedReply) {
((DelayedReply)result).take();
}
} catch (InterruptedException e) {
throw new CommandExitException("Error at " + source + ":" + lineCount + ": command interrupted");
} catch (CommandPanicException e) {
throw new CommandPanicException("Error at " + source + ":" + lineCount + ": " + e.getMessage(), e);
} catch (CommandException e) {
throw new CommandThrowableException("Error at " + source + ":" + lineCount + ": " + e.getMessage(), e);
}
}
} catch (IOException e) {
throw new RuntimeException(e); // Should not be possible
}
}
/**
* Returns the singleton bean with a given name. Returns null if
* such a bean does not exist.
*/
private Object getBean(String name)
{
try {
if (_context != null && _context.isSingleton(name)) {
return _context.getBean(name);
}
} catch (NoSuchBeanDefinitionException e) {
}
return null;
}
/**
* Returns the names of beans that depend of a given bean.
*/
private List<String> getDependentBeans(String name)
{
if (_context == null) {
return Collections.emptyList();
} else {
return Arrays.asList(_context.getBeanFactory().getDependentBeans(name));
}
}
/**
* Returns the collection of singleton bean names.
*/
private List<String> getBeanNames()
{
if (_context == null) {
return Collections.emptyList();
} else {
return Arrays.asList(_context.getBeanFactory().getSingletonNames());
}
}
/**
* Collects information from all registered info providers.
*/
@Override
public void getInfo(PrintWriter pw)
{
/* getInfo is called during cell initialization, but the context isn't fully
* initialized until cell initialization is done.
*/
ConfigurableApplicationContext context = _context;
if (context != null) {
ConfigurableListableBeanFactory factory = context.getBeanFactory();
for (Map.Entry<String, CellInfoProvider> entry : _infoProviders.entrySet()) {
String name = entry.getKey();
CellInfoProvider provider = entry.getValue();
try {
BeanDefinition definition = factory.getBeanDefinition(name);
String description = definition.getDescription();
if (description != null) {
pw.println(String.format("--- %s (%s) ---",
name, description));
} else {
pw.println(String.format("--- %s ---", name));
}
} catch (NoSuchBeanDefinitionException e) {
pw.println(String.format("--- %s ---", name));
}
provider.getInfo(pw);
pw.println();
}
}
}
/**
* Collects information about the cell and returns these in a
* CellInfo object. Information is collected from the CellAdapter
* base class and from all beans implementing CellInfoProvider.
*/
@Override
public CellInfo getCellInfo()
{
CellInfo info = super.getCellInfo();
for (CellInfoProvider provider : _infoProviders.values()) {
info = provider.getCellInfo(info);
}
return info;
}
/**
* Collects setup information from all registered setup providers.
*/
protected void printSetup(PrintWriter pw)
{
pw.println("#\n# Created by " + getCellName() + "("
+ getNucleus().getCellClass() + ") at " + (new Date()).toString()
+ "\n#");
for (CellSetupProvider provider: _setupProviders.values()) {
provider.printSetup(pw);
}
}
private static void renameWithBackup(File source, File dest)
throws IOException
{
File backup = new File(dest.getPath() + ".bak");
if (dest.exists()) {
if (!dest.isFile()) {
throw new IOException("Cannot rename " + dest + ": Not a file");
}
if (backup.exists()) {
if (!backup.isFile()) {
throw new IOException("Cannot delete " + backup + ": Not a file");
}
if (!backup.delete()) {
throw new IOException("Failed to delete " + backup);
}
}
if (!dest.renameTo(backup)) {
throw new IOException("Failed to rename " + dest);
}
}
if (!source.renameTo(dest)) {
throw new IOException("Failed to rename" + source);
}
}
public class SetupCommandListener implements CellCommandListener
{
@Command(name = "save", hint = "save service configuration")
public class SaveCommand implements Callable<String>
{
@Option(name = "file", usage = "Path of setup file.")
File file = _setupFile;
@Override
public String call()
throws IOException, IllegalArgumentException
{
File path = file.getAbsoluteFile();
File directory = path.getParentFile();
File temp = File.createTempFile(path.getName(), null, directory);
temp.deleteOnExit();
try (PrintWriter pw = new PrintWriter(new FileWriter(temp))) {
printSetup(pw);
}
renameWithBackup(temp, path);
return "";
}
}
@Command(name = "reload",
hint = "reload service configuration",
description = "This command destroys the current setup and replaces it " +
"by the setup on disk.")
public class ReloadCommand implements Callable<String>
{
@Option(name = "yes", required = true,
usage = "Confirms that the current setup should be destroyed and replaced " +
"with the one on disk.")
boolean confirmed;
@Override
public String call() throws IOException, CommandException, IllegalArgumentException
{
checkArgument(confirmed, "Required option is missing.");
if (_setupFile != null && !_setupFile.exists()) {
return String.format("Setup file [%s] does not exist", _setupFile);
}
_setupManager.load(_setupFile);
return "";
}
}
}
@Command(name = "infox", hint = "show status information about bean")
public class InfoxCommand implements Callable<String>
{
@Argument(metaVar = "bean")
String name;
@Override
public String call()
{
Object bean = getBean(name);
if (CellInfoProvider.class.isInstance(bean)) {
StringWriter s = new StringWriter();
PrintWriter pw = new PrintWriter(s);
((CellInfoProvider)bean).getInfo(pw);
return s.toString();
}
return "No such bean: " + name;
}
}
@Command(name = "bean ls", hint = "list running beans",
description = "Lists the bean in this service. Services are composed of components " +
"called beans. Each bean implements a part of a service.")
public class BeanLsCommand implements Callable<String>
{
@Override
public String call()
{
String format = "%-30s %s\n";
try (Formatter s = new Formatter(new StringBuilder())) {
ConfigurableListableBeanFactory factory = _context.getBeanFactory();
s.format(format, "Bean", "Description");
s.format(format, "----", "-----------");
for (String name : factory.getBeanDefinitionNames()) {
if (!name.startsWith("org.springframework.")) {
BeanDefinition definition = factory.getBeanDefinition(name);
String description = definition.getDescription();
s.format(format, name,
(description != null ? description : "-"));
}
}
return s.toString();
}
}
}
@Command(name = "bean dep", hint = "show bean dependencies",
description = "Shows dependencies between beans. This information is mostly useful " +
"as a debugging aid.")
public class BeanDepCommand implements Callable<String>
{
@Override
public String call()
{
String format = "%-30s %s\n";
try (Formatter s = new Formatter(new StringBuilder())) {
s.format(format, "Bean", "Used by");
s.format(format, "----", "-------");
for (String name : getBeanNames()) {
s.format(format, name, Joiner.on(",").join(getDependentBeans(name)));
}
return s.toString();
}
}
}
/**
* If given a simple bean name, returns that bean (equivalent to
* calling getBean). If given a compound name using the syntax of
* BeanWrapper, then the respective property value is
* returned. E.g., getBeanProperty("foo") returns the bean named
* "foo", whereas getBeanProperty("foo.bar") returns the value of
* the "bar" property of bean "foo".
*/
private Object getBeanProperty(String s)
{
String[] a = s.split("\\.", 2);
Object o = getBean(a[0]);
if (o != null && a.length == 2) {
BeanWrapper bean = new RestrictedBeanWrapper(o);
o = bean.isReadableProperty(a[1])
? bean.getPropertyValue(a[1])
: null;
}
return o;
}
@Command(name = "bean properties", hint = "show properties of a bean",
description = "Shows the properties of a bean and their values. Each bean exposes " +
"some properties that can be read with this command.")
public class BeanPropertiesCommand implements Callable<String>
{
@Argument(metaVar = "bean", usage = "Name of bean or property reference.")
String name;
@Override
public String call() throws InvalidPropertyException
{
Object o = getBeanProperty(name);
if (o != null) {
StringBuilder s = new StringBuilder();
BeanWrapper bean = new RestrictedBeanWrapper(o);
for (PropertyDescriptor p : bean.getPropertyDescriptors()) {
String property = p.getName();
if (bean.isReadableProperty(property)) {
s.append(property).append('=');
try {
s.append(org.dcache.util.Strings.toString(bean.getPropertyValue(property)));
if (!bean.isWritableProperty(property)) {
s.append(" [read-only]");
}
} catch (InvalidPropertyException e) {
s.append(" [invalid]");
}
s.append('\n');
}
}
return s.toString();
}
return "No such bean: " + name;
}
}
@Command(name = "bean property", hint = "show property of a bean")
public class BeanPropertyCommand implements Callable<String>
{
@Argument(metaVar = "property")
String name;
@Override
public String call() throws InvalidPropertyException
{
Object o = getBeanProperty(name);
return (o != null) ? org.dcache.util.Strings.toMultilineString(o) : ("No such property: " + name);
}
}
/** Returns a formatted name of a message class. */
protected String getMessageName(Class<?> c)
{
String name = c.getSimpleName();
int length = name.length();
if ((length > 7) && name.endsWith("Message")) {
name = name.substring(0, name.length() - 7);
} else if ((length > 3) && name.endsWith("Msg")) {
name = name.substring(0, name.length() - 3);
}
return name;
}
@Command(name = "bean messages", hint = "show message types handled by beans",
description = "Shows messages processed by each bean. dCache services communicate " +
"by message passing. Each bean may be able to handle zero or more messages and " +
"this command shows which messages.")
public class BeanMessagesCommand implements Callable<String>
{
@Argument(required = false, metaVar = "bean",
usage = "Bean name. If omitted, the information is displayed for all beans.")
String name;
@Override
public String call()
{
if (name == null) {
Multimap<String,Class<? extends Serializable>> nameToClassMap = ArrayListMultimap.create();
for (String name: getBeanNames()) {
Object bean = getBean(name);
if (CellMessageReceiver.class.isInstance(bean)) {
Collection<Class<? extends Serializable>> types =
_messageDispatcher.getMessageTypes(bean);
nameToClassMap.putAll(name, types);
}
}
Multimap<Class<? extends Serializable>,String> classToNameMap = Multimaps.invertFrom(
nameToClassMap, ArrayListMultimap.create());
final String format = "%-40s %s\n";
Formatter f = new Formatter(new StringBuilder());
f.format(format, "Message", "Receivers");
f.format(format, "-------", "---------");
for (Map.Entry<Class<? extends Serializable>,Collection<String>> e: classToNameMap.asMap().entrySet()) {
f.format(format,
getMessageName(e.getKey()),
Joiner.on(",").join(e.getValue()));
}
return f.toString();
} else {
Object bean = getBean(name);
if (CellMessageReceiver.class.isInstance(bean)) {
StringBuilder s = new StringBuilder();
Collection<Class<? extends Serializable>> types =
_messageDispatcher.getMessageTypes(bean);
for (Class<? extends Serializable> t : types) {
s.append(getMessageName(t)).append('\n');
}
return s.toString();
}
return "No such bean: " + name;
}
}
}
public BeanQueryMessage messageArrived(BeanQueryAllPropertiesMessage message)
throws CacheException
{
Map<String,Object> beans = Maps.newHashMap();
ConfigurableListableBeanFactory factory = _context.getBeanFactory();
for (String name : factory.getBeanDefinitionNames()) {
if (!name.startsWith("org.springframework.")) {
beans.put(name, getBean(name));
}
}
message.setResult(serialize(beans));
return message;
}
public BeanQueryMessage messageArrived(BeanQuerySinglePropertyMessage message)
throws CacheException
{
String[] a = message.getPropertyName().split("\\.", 2);
String name = a[0];
Object o = null;
if (_context != null && _context.isSingleton(name) && _context.containsBeanDefinition(name)) {
o = _context.getBean(name);
if (a.length == 2) {
String propertyName = a[1];
BeanWrapper bean = new RestrictedBeanWrapper(o);
o = bean.isReadableProperty(propertyName)
? bean.getPropertyValue(propertyName)
: null;
}
}
if (o == null) {
throw new CacheException("No such property");
}
message.setResult(serialize(o));
return message;
}
/**
* Returns true if {@code clazz} is assignable to any of the classes in {@code classes}, false
* otherwise.
*/
private boolean isAssignableTo(Class<?> clazz, Class<?>... classes)
{
for (Class<?> aClass : classes) {
if (aClass.isAssignableFrom(clazz)) {
return true;
}
}
return false;
}
private Object serialize(Set<Object> prune, Queue<Map.Entry<String,Object>> queue, Object o)
{
if (o == null || PRIMITIVE_TYPES.contains(o.getClass())) {
return o;
} else if (isAssignableTo(o.getClass(), TERMINAL_TYPES)) {
return o.toString();
} else if (o.getClass().isEnum()) {
return o;
} else if (o.getClass().isArray()) {
int len = Array.getLength(o);
List<Object> values = Lists.newArrayListWithCapacity(len);
for (int i = 0; i < len; i++) {
values.add(serialize(prune, queue, Array.get(o, i)));
}
return values;
} else if (o instanceof Map) {
Map<?,?> map = (Map<?,?>) o;
Map<String,Object> values = Maps.newHashMapWithExpectedSize(map.size());
for (Map.Entry<?,?> e: map.entrySet()) {
values.put(String.valueOf(e.getKey()), serialize(prune, queue, e
.getValue()));
}
return values;
} else if (o instanceof Set) {
Collection<?> collection = (Collection<?>) o;
Set<Object> values = Sets.newHashSetWithExpectedSize(collection.size());
for (Object entry: collection) {
values.add(serialize(prune, queue, entry));
}
return values;
} else if (o instanceof Collection) {
Collection<?> collection = (Collection<?>) o;
List<Object> values = Lists.newArrayListWithCapacity(collection.size());
for (Object entry: collection) {
values.add(serialize(prune, queue, entry));
}
return values;
} else if (o instanceof Iterable) {
Iterable<?> collection = (Iterable<?>) o;
List<Object> values = Lists.newArrayList();
for (Object entry: collection) {
values.add(serialize(prune, queue, entry));
}
return values;
} else if (prune.contains(o)) {
return o.toString();
} else {
prune.add(o);
Map<String,Object> values = Maps.newHashMap();
BeanWrapper bean = new RestrictedBeanWrapper(o);
for (PropertyDescriptor p: bean.getPropertyDescriptors()) {
String property = p.getName();
if (bean.isReadableProperty(property)) {
try {
values.put(property, bean.getPropertyValue(property));
} catch (InvalidPropertyException | PropertyAccessException e) {
LOGGER.debug("Failed to read {} of object of class {}: {}",
property, o.getClass(), e.getMessage());
}
}
}
if (values.isEmpty()) {
return o.toString();
}
queue.addAll(values.entrySet());
return values;
}
}
/**
* Breadth-first serialisation.
*
* Prunes the object tree to produce a tree even if o is a DAG or
* contains cycles. Using a breadth-first search tends to produce
* friendlier results when the object graph is pruned.
*/
private Object serialize(Object o)
{
Set<Object> prune = Sets.newHashSet();
Queue<Map.Entry<String,Object>> queue = new ArrayDeque<>();
Object result = serialize(prune, queue, o);
Map.Entry<String,Object> entry;
while ((entry = queue.poll()) != null) {
entry.setValue(serialize(prune, queue, entry.getValue()));
}
return result;
}
/**
* Registers an info provider. Info providers contribute to the
* result of the <code>getInfo</code> method.
*/
public void addInfoProviderBean(CellInfoProvider bean, String name)
{
_infoProviders.put(name, bean);
}
/**
* Add a message receiver. Message receiver receive messages via
* message handlers (see CellMessageDispatcher).
*/
public void addMessageReceiver(CellMessageReceiver bean)
{
addMessageListener(bean);
}
/**
* Registers a setup provider. Setup providers contribute to the
* result of the <code>save</code> method.
*/
public void addSetupProviderBean(CellSetupProvider bean, String name)
{
_setupProviders.put(name, bean);
_setupInterpreter.addCommandListener(bean);
}
/**
* Registers a life cycle aware bean. Life cycle aware beans are
* notified about cell start and stop events.
*/
public void addLifeCycleAwareBean(CellLifeCycleAware bean, String name)
{
_lifeCycleAware.put(name, bean);
}
/**
* Part of the BeanPostProcessor implementation. Recognizes beans
* implementing CellCommandListener, CellInfoProvider,
* CellCommunicationAware, CellSetupProvider and
* ThreadFactoryAware and performs the necessary wiring.
*/
@Override
public Object postProcessBeforeInitialization(Object bean,
String beanName)
throws BeansException
{
if (bean instanceof EnvironmentAware) {
((EnvironmentAware) bean).setEnvironment(_environment);
}
if (bean instanceof CellArgsAware) {
((CellArgsAware)bean).setCellArgs(getArgs());
}
if (bean instanceof DomainContextAware) {
((DomainContextAware) bean).setDomainContext(getDomainContext());
}
if (bean instanceof CuratorFrameworkAware) {
((CuratorFrameworkAware) bean).setCuratorFramework(getCuratorFramework());
}
if (bean instanceof CellIdentityAware) {
((CellIdentityAware) bean).setCellAddress(getNucleus().getThisAddress());
}
if (bean instanceof CellInfoAware) {
((CellInfoAware) bean).setCellInfoSupplier(this::getCellInfo);
}
if (bean instanceof CellMessageSender) {
((CellMessageSender) bean).setCellEndpoint(this);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean,
String beanName)
throws BeansException
{
if (bean instanceof CellCommandListener) {
addCommandListener(bean);
}
if (bean instanceof CellInfoProvider) {
addInfoProviderBean((CellInfoProvider) bean, beanName);
}
if (bean instanceof CellMessageReceiver) {
addMessageReceiver((CellMessageReceiver) bean);
}
if (bean instanceof CellSetupProvider) {
addSetupProviderBean((CellSetupProvider) bean, beanName);
}
if (bean instanceof CellLifeCycleAware) {
addLifeCycleAwareBean((CellLifeCycleAware) bean, beanName);
}
if (bean instanceof CellEventListener) {
addCellEventListener((CellEventListener) bean);
}
return bean;
}
private void createContext() throws CommandThrowableException
{
ClassPathXmlApplicationContext context;
Args args = getArgs();
try {
context = new ClassPathXmlApplicationContext();
context.setConfigLocations(args.argv(0));
context.addBeanFactoryPostProcessor(
beanFactory -> beanFactory.addBeanPostProcessor(UniversalSpringCell.this));
context.addBeanFactoryPostProcessor(
beanFactory -> beanFactory.registerSingleton(
"message-executor", (Executor) this::invokeOnMessageThread)
);
ConfigurableEnvironment environment = context.getEnvironment();
environment.getPropertySources().addFirst(
new MapPropertySource("environment", _environment));
environment.getPropertySources().addFirst(
new MapPropertySource("options", Maps.newHashMap(args.optionsAsMap())));
environment.getPropertySources().addFirst(
new PropertySource<Object>("arguments")
{
private final String arguments;
{
Args args = new Args(getArgs());
args.shift();
arguments = args.toString().replaceAll("-\\$\\{[0-9]+\\}", "");
}
@Override
public Object getProperty(String name)
{
return name.equals("arguments") ? arguments : null;
}
}
);
if (args.hasOption("profiles")) {
environment.setActiveProfiles(args.getOption("profiles").split(","));
}
context.refresh();
} catch (BeanInstantiationException e) {
Throwable t = e.getMostSpecificCause();
Throwables.throwIfUnchecked(t);
String msg = "Failed to instantiate class " + e.getBeanClass().getName() +
": " + t.getMessage();
throw new CommandThrowableException(msg, t);
} catch (BeanCreationException e) {
Throwable t = e.getMostSpecificCause();
Throwables.throwIfUnchecked(t);
String msg = "Failed to create bean '" + e.getBeanName() +
"' : " + t.getMessage();
throw new CommandThrowableException(msg, t);
}
_context = context;
}
/**
* BeanWrapper that restricts access to certain private properties that should not be exposed.
*/
private class RestrictedBeanWrapper extends BeanWrapperImpl
{
private RestrictedBeanWrapper(Object object)
{
super(object);
}
private RestrictedBeanWrapper(Object object, String nestedPath, Object rootObject)
{
super(object, nestedPath, rootObject);
}
@Override
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath)
{
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
if (pos > -1) {
String nestedProperty = propertyPath.substring(0, pos);
if (!isReadableProperty(nestedProperty)) {
throw new NotReadablePropertyException(getRootClass(), nestedProperty);
}
}
return super.getPropertyAccessorForPropertyPath(propertyPath);
}
@Override
public boolean isReadableProperty(String propertyName)
{
if (isAllowedName(propertyName)) {
try {
PropertyDescriptor pd = getPropertyDescriptor(propertyName);
if (pd.getReadMethod() != null && !pd.isHidden() && isAllowedType(pd.getPropertyType())) {
return true;
}
} catch (InvalidPropertyException e) {
try {
Object value = super.getPropertyValue(propertyName);
if (value == null || isAllowedType(value.getClass())) {
return true;
}
} catch (InvalidPropertyException ignored) {
}
}
}
return false;
}
private boolean isAllowedType(Class<?> clazz)
{
return !isAssignableTo(clazz, HIDDEN_TYPES);
}
private boolean isAllowedName(String propertyName)
{
String s = propertyName.toLowerCase();
return !s.contains("password") && !s.contains("username");
}
@Override
public Object getPropertyValue(String propertyName) throws BeansException
{
if (!isReadableProperty(propertyName)) {
throw new NotReadablePropertyException(getRootClass(), propertyName);
}
return super.getPropertyValue(propertyName);
}
@Override
protected BeanWrapperImpl newNestedPropertyAccessor(Object object, String nestedPath) {
return new RestrictedBeanWrapper(object, nestedPath, this);
}
}
/**
* Setup related operations are delegated to an implementation of this interface.
*/
private interface SetupManager
{
/** Called on startup. Should load the initial setup. */
void start() throws CommandException;
/** Called on shutdown. Should release any resources held. */
void close();
/**
* Called when a setup command is issued. The implementation should execute the given command
* and optionally synchronize it with external representations.
*/
Serializable execute(CommandExecutor command, Args args) throws CommandException;
/**
* Called when the admin issues the reload command. Implementations should parse
* the given file and update the configuration accordingly.
*/
void load(File file) throws CommandException;
}
/**
* Setup manager without any external representation of the setup.
*/
private class InMemorySetupManager implements SetupManager
{
private int version;
@Override
public void start() throws CommandException
{
}
@Override
public void close()
{
}
@Override
public synchronized Serializable execute(CommandExecutor command, Args args) throws CommandException
{
Serializable result = command.execute(args);
notifyListeners();
return result;
}
@Override
public void load(File file) throws CommandException
{
}
@GuardedBy("this")
protected void notifyListeners()
{
version++;
_lifeCycleAware.values().forEach(b -> b.setupChanged(version));
}
}
/**
* Setup manager that uses a local file as a configuration source.
*/
private class LocalSetupManager extends InMemorySetupManager
{
private final File _file;
public LocalSetupManager(File file)
{
_file = file;
}
@Override
public void start() throws CommandException
{
if (_file != null && _file.isFile()) {
load(_file);
}
}
@Override
public synchronized void load(File file) throws CommandException
{
executeSetup(file.toString(), loadSetup(file));
notifyListeners();
}
}
/**
* Setup manager that uses an local file as an initial configuration source and afterwards
* keeps the current setup in sync with a zookeeper node.
*/
private class ZooKeeperSetupManager implements NodeCacheListener, SetupManager
{
private final CuratorFramework _curator;
/**
* Local file containing the persistent setup.
*/
private final File _file;
/**
* Path to ZooKeeper node to keep in sync with the current setup.
*/
private final String _node;
/**
* Cache of the ZooKeeper node identified by {@code _node}.
*/
private final NodeCache _cache;
/**
* Stat of the last value loaded from the ZooKeeper node identified by {@code _node}.
*/
private Stat _current;
public ZooKeeperSetupManager(File file, String node)
{
_curator = getCuratorFramework();
_file = file;
_node = node;
_cache = new NodeCache(_curator, _node);
_cache.getListenable().addListener(this);
}
@Override
public synchronized void start() throws CommandException
{
byte[] data;
if (_file != null && _file.isFile()) {
data = loadSetup(_file);
} else {
data = getCurrentSetup();
}
// Seed setup in zookeeper
try {
_curator.create().creatingParentContainersIfNeeded().forPath(_node, data);
} catch (KeeperException.NodeExistsException e) {
LOGGER.info("Not seeding setup in ZooKeeper as such a setup already exists.");
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new CommandThrowableException("Failed to create " +
"zookeeper node " + _node + ": " + e.getMessage(), e);
}
// Blocking start of the setup node cache - the service fails to start if zookeeper is unavailable
try {
_cache.start(true);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new CommandThrowableException("Failed while waiting for " +
"zookeeper: " + e.getMessage(), e);
}
// Load initial setup from zookeeper
ChildData currentData = _cache.getCurrentData();
if (currentData == null) {
throw new CommandPanicException("Setup reload failed because " + _node + " in ZooKeeper disappeared. Service must be restarted.");
}
apply(currentData);
}
@Override
public void close()
{
CloseableUtils.closeQuietly(_cache);
}
@Override
public synchronized Serializable execute(CommandExecutor command, Args args) throws CommandException
{
Serializable result = command.execute(args);
/* REVISIT: _current is null if start was not called yet - this happens because the setup context is
* executed before starting the setup manager.
*/
if (_current != null) {
try {
byte[] data = getCurrentSetup();
_current = _curator.setData().withVersion(_current.getVersion()).forPath(_node, data);
} catch (BadVersionException e) {
try {
_cache.rebuild();
} catch (Exception suppressed) {
e.addSuppressed(e);
}
rollback(e);
throw new CommandThrowableException(
"Setup command failed due to concurrent update in ZooKeeper.", e);
} catch (NoNodeException e) {
rollback(e);
throw new CommandPanicException(
"Setup command failed because " + _node + " in ZooKeeper disappeared. Service must be restarted.", e);
} catch (KeeperException e) {
rollback(e);
throw new CommandThrowableException(
"Setup command fail due to ZooKeeper failure: " + e.getMessage(), e);
} catch (InterruptedException e) {
rollback(e);
throw new CommandThrowableException("Setup command was interrupted.", e);
} catch (Exception e) {
rollback(e);
throw new CommandPanicException("Setup command failed unexpectedly: " + e, e);
}
}
return result;
}
@Override
public void load(File file) throws CommandException
{
try {
_curator.setData().forPath(_node, loadSetup(file));
} catch (NoNodeException e) {
throw new CommandPanicException("Setup reload failed because " + _node + " in ZooKeeper disappeared. Service must be restarted.", e);
} catch (KeeperException e) {
throw new CommandThrowableException("Setup reload fail due to ZooKeeper failure: " + e.getMessage(), e);
} catch (InterruptedException e) {
throw new CommandThrowableException("Setup reload was interrupted.", e);
} catch (Exception e) {
throw new CommandPanicException("Setup reload failed unexpectedly: " + e, e);
}
}
@Override
public synchronized void nodeChanged() throws Exception
{
ChildData newData = _cache.getCurrentData();
if (newData == null) {
LOGGER.error("Setup node " + _node + " in ZooKeeper disappeared. Service must be restarted.");
kill();
} else if (newData.getStat().getVersion() > _current.getVersion()) {
apply(newData);
}
}
private void rollback(Exception cause) throws CommandPanicException
{
try {
ChildData currentData = _cache.getCurrentData();
if (currentData == null) {
kill();
throw new CommandPanicException("Setup node " + _node + " in ZooKeeper disappeared.");
}
apply(currentData);
} catch (CommandException e) {
e.addSuppressed(cause);
throw new CommandPanicException("Saving setup to ZooKeeper failed (" + cause.getMessage() + "); however the rollback failed too (" + e.getMessage() + "). Service must be restarted.", e);
}
}
private byte[] getCurrentSetup()
{
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
printSetup(pw);
pw.close();
sw.close();
return sw.getBuffer().toString().getBytes(UTF_8);
} catch (IOException e) {
throw new RuntimeException(e); // Should not be possible
}
}
@GuardedBy("this")
private void apply(ChildData setup) throws CommandException
{
testSetup("zookeeper:" + _node, setup.getData());
executeSetup("zookeeper:" + _node, setup.getData());
_current = setup.getStat();
_lifeCycleAware.values().forEach(b -> b.setupChanged(_current.getVersion()));
}
}
}