blob: 79852d4af6a495f28b971facf4723ebaed859410 [file] [log] [blame]
/*
* 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.cassandra.config;
import java.lang.reflect.Constructor;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Queue;
import com.google.common.collect.Maps;
import org.yaml.snakeyaml.introspector.Property;
/**
* Utility class for working with {@link Property} types.
*/
public final class Properties
{
public static final String DELIMITER = ".";
private Properties()
{
}
/**
* Given two properties (root, leaf), calls first go through root and passed to leaf.
*
* <pre>{@code leaf.get(root.get(o))}</pre>
*
* @param root first property in the chain
* @param leaf last property in the chain
* @param delimiter for joining names
* @return new Property which combines root -> leaf
*/
public static Property andThen(Property root, Property leaf, String delimiter)
{
return new AndThen(root, leaf, delimiter);
}
/**
* Given two properties (root, leaf), calls first go through root and passed to leaf.
*
* <pre>{@code leaf.get(root.get(o))}</pre>
*
* @param root first property in the chain
* @param leaf last property in the chain
* @return new Property which combines root -> leaf
*/
public static Property andThen(Property root, Property leaf)
{
return andThen(root, leaf, DELIMITER);
}
/**
* Given a map of Properties, takes any "nested" property (non primitive, value-type, or collection), and
* expands them, producing 1 or more Properties.
*
* @param loader for mapping type to map of properties
* @param input map to flatten
* @return map of all flattened properties
*/
public static Map<String, Property> flatten(Loader loader, Map<String, Property> input)
{
return flatten(loader, input, DELIMITER);
}
/**
* Given a map of Properties, takes any "nested" property (non primitive, value-type, or collection), and
* expands them, producing 1 or more Properties.
*
* @param loader for mapping type to map of properties
* @param input map to flatten
* @param delimiter for joining names
* @return map of all flattened properties
*/
public static Map<String, Property> flatten(Loader loader, Map<String, Property> input, String delimiter)
{
Queue<Property> queue = new ArrayDeque<>(input.values());
Map<String, Property> output = Maps.newHashMapWithExpectedSize(input.size());
while (!queue.isEmpty())
{
Property prop = queue.poll();
Map<String, Property> children = isPrimitive(prop) || isCollection(prop) ? Collections.emptyMap() : loader.getProperties(prop.getType());
if (children.isEmpty())
{
// not nested, so assume properties can be handled
output.put(prop.getName(), prop);
}
else
{
children.values().stream().map(p -> andThen(prop, p, delimiter)).forEach(queue::add);
}
}
return output;
}
/**
* @return true if proeprty type is a collection
*/
public static boolean isCollection(Property prop)
{
return Collection.class.isAssignableFrom(prop.getType()) || Map.class.isAssignableFrom(prop.getType());
}
/**
* @return true if property type is a primitive, or well known value type (may return false for user defined value types)
*/
public static boolean isPrimitive(Property prop)
{
Class<?> type = prop.getType();
return type.isPrimitive() || type.isEnum() || type.equals(String.class) || Number.class.isAssignableFrom(type) || Boolean.class.equals(type);
}
/**
* @return default implementation of {@link Loader}
*/
public static Loader defaultLoader()
{
return new DefaultLoader();
}
/**
* @return a new property with an updated name
*/
public static Property rename(String newName, Property prop)
{
return new ForwardingProperty(newName, prop);
}
private static final class AndThen extends ForwardingProperty
{
private final Property root;
private final Property leaf;
AndThen(Property root, Property leaf, String delimiter)
{
super(root.getName() + delimiter + leaf.getName(), leaf);
this.root = root;
this.leaf = leaf;
}
@Override
public void set(Object object, Object value) throws Exception
{
Object parent = root.get(object);
if (parent == null)
{
// see: org.yaml.snakeyaml.constructor.BaseConstructor.newInstance(java.lang.Class<?>, org.yaml.snakeyaml.nodes.Node, boolean)
// That method is what creates the types, and it boils down to this call. There is a TypeDescription
// class that we don't use, so boils down to "null" in our existing logic... if we start using TypeDescription
// to build the object, then we will need to rewrite this logic to work with BaseConstructor.
Constructor<?> c = root.getType().getDeclaredConstructor();
c.setAccessible(true);
parent = c.newInstance();
root.set(object, parent);
}
leaf.set(parent, value);
}
@Override
public Object get(Object object)
{
try
{
Object parent = root.get(object);
if (parent == null)
return null;
return leaf.get(parent);
}
catch (Exception e)
{
if (!(root instanceof AndThen))
e.addSuppressed(new RuntimeException("Error calling get() on " + this));
throw e;
}
}
}
}