blob: 24797449de7a12b68c833d4d522572069590c808 [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.ignite.internal.configuration.asm;
import java.io.Serializable;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
import com.facebook.presto.bytecode.BytecodeBlock;
import com.facebook.presto.bytecode.BytecodeNode;
import com.facebook.presto.bytecode.ClassDefinition;
import com.facebook.presto.bytecode.ClassGenerator;
import com.facebook.presto.bytecode.FieldDefinition;
import com.facebook.presto.bytecode.MethodDefinition;
import com.facebook.presto.bytecode.ParameterizedType;
import com.facebook.presto.bytecode.Variable;
import com.facebook.presto.bytecode.control.IfStatement;
import com.facebook.presto.bytecode.expression.BytecodeExpression;
import org.apache.ignite.configuration.ConfigurationProperty;
import org.apache.ignite.configuration.ConfigurationValue;
import org.apache.ignite.configuration.NamedConfigurationTree;
import org.apache.ignite.configuration.NamedListView;
import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
import org.apache.ignite.configuration.annotation.InternalConfiguration;
import org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.internal.configuration.ConfigurationChanger;
import org.apache.ignite.internal.configuration.DynamicConfiguration;
import org.apache.ignite.internal.configuration.DynamicProperty;
import org.apache.ignite.internal.configuration.NamedListConfiguration;
import org.apache.ignite.internal.configuration.TypeUtils;
import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
import org.apache.ignite.internal.configuration.tree.ConfigurationVisitor;
import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.apache.ignite.internal.configuration.tree.NamedListNode;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import static com.facebook.presto.bytecode.Access.BRIDGE;
import static com.facebook.presto.bytecode.Access.FINAL;
import static com.facebook.presto.bytecode.Access.PRIVATE;
import static com.facebook.presto.bytecode.Access.PUBLIC;
import static com.facebook.presto.bytecode.Access.STATIC;
import static com.facebook.presto.bytecode.Access.SYNTHETIC;
import static com.facebook.presto.bytecode.Parameter.arg;
import static com.facebook.presto.bytecode.ParameterizedType.type;
import static com.facebook.presto.bytecode.ParameterizedType.typeFromJavaClassName;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantClass;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantNull;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantString;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.inlineIf;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.invokeDynamic;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.invokeStatic;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.isNull;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.newInstance;
import static java.lang.invoke.MethodType.methodType;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.EnumSet.of;
import static java.util.stream.Collectors.toList;
import static org.apache.ignite.internal.configuration.asm.SchemaClassesInfo.changeClassName;
import static org.apache.ignite.internal.configuration.asm.SchemaClassesInfo.configurationClassName;
import static org.apache.ignite.internal.configuration.asm.SchemaClassesInfo.viewClassName;
import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.extensionsFields;
import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isConfigValue;
import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isNamedConfigValue;
import static org.apache.ignite.internal.configuration.util.ConfigurationUtil.isValue;
import static org.apache.ignite.internal.util.CollectionUtils.concat;
import static org.apache.ignite.internal.util.CollectionUtils.union;
import static org.objectweb.asm.Opcodes.H_NEWINVOKESPECIAL;
import static org.objectweb.asm.Type.getMethodDescriptor;
import static org.objectweb.asm.Type.getMethodType;
import static org.objectweb.asm.Type.getType;
/**
* This class is responsible for generating internal implementation classes for configuration schemas. It uses classes
* from {@code bytecode} module to achieve this goal, like {@link ClassGenerator}, for examples.
*/
public class ConfigurationAsmGenerator {
/** {@link LambdaMetafactory#metafactory(Lookup, String, MethodType, MethodType, MethodHandle, MethodType)} */
private static final Method LAMBDA_METAFACTORY;
/** {@link Consumer#accept(Object)}*/
private static final Method ACCEPT;
/** {@link ConfigurationVisitor#visitLeafNode(String, Serializable)} */
private static final Method VISIT_LEAF;
/** {@link ConfigurationVisitor#visitInnerNode(String, InnerNode)} */
private static final Method VISIT_INNER;
/** {@link ConfigurationVisitor#visitNamedListNode(String, NamedListNode)} */
private static final Method VISIT_NAMED;
/** {@link ConfigurationSource#unwrap(Class)} */
private static final Method UNWRAP;
/** {@link ConfigurationSource#descend(ConstructableTreeNode)} */
private static final Method DESCEND;
/** {@link ConstructableTreeNode#copy()} */
private static final Method COPY;
/** {@link DynamicConfiguration#DynamicConfiguration(List, String, RootKey, ConfigurationChanger)} */
private static final Constructor DYNAMIC_CONFIGURATION_CTOR;
/** {@link DynamicConfiguration#add(ConfigurationProperty)} */
private static final Method DYNAMIC_CONFIGURATION_ADD;
/** {@link Objects#requireNonNull(Object, String)} */
private static final Method REQUIRE_NON_NULL;
static {
try {
LAMBDA_METAFACTORY = LambdaMetafactory.class.getDeclaredMethod(
"metafactory",
Lookup.class,
String.class,
MethodType.class,
MethodType.class,
MethodHandle.class,
MethodType.class
);
ACCEPT = Consumer.class.getDeclaredMethod("accept", Object.class);
VISIT_LEAF = ConfigurationVisitor.class.getDeclaredMethod("visitLeafNode", String.class, Serializable.class);
VISIT_INNER = ConfigurationVisitor.class.getDeclaredMethod("visitInnerNode", String.class, InnerNode.class);
VISIT_NAMED = ConfigurationVisitor.class.getDeclaredMethod("visitNamedListNode", String.class, NamedListNode.class);
UNWRAP = ConfigurationSource.class.getDeclaredMethod("unwrap", Class.class);
DESCEND = ConfigurationSource.class.getDeclaredMethod("descend", ConstructableTreeNode.class);
COPY = ConstructableTreeNode.class.getDeclaredMethod("copy");
DYNAMIC_CONFIGURATION_CTOR = DynamicConfiguration.class.getDeclaredConstructor(
List.class,
String.class,
RootKey.class,
ConfigurationChanger.class
);
DYNAMIC_CONFIGURATION_ADD = DynamicConfiguration.class.getDeclaredMethod(
"add",
ConfigurationProperty.class
);
REQUIRE_NON_NULL = Objects.class.getDeclaredMethod("requireNonNull", Object.class, String.class);
}
catch (NoSuchMethodException nsme) {
throw new ExceptionInInitializerError(nsme);
}
}
/** Information about schema classes - bunch of names and dynamically compiled internal classes. */
private final Map<Class<?>, SchemaClassesInfo> schemasInfo = new HashMap<>();
/** Class generator instance. */
private final ClassGenerator generator = ClassGenerator.classGenerator(this.getClass().getClassLoader());
/**
* Creates new instance of {@code *Node} class corresponding to the given Configuration Schema.
* @param schemaClass Configuration Schema class.
* @return Node instance.
*/
public synchronized InnerNode instantiateNode(Class<?> schemaClass) {
SchemaClassesInfo info = schemasInfo.get(schemaClass);
assert info != null && info.nodeClass != null : schemaClass;
try {
Constructor<? extends InnerNode> constructor = info.nodeClass.getConstructor();
assert constructor.canAccess(null);
return constructor.newInstance();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Creates new instance of {@code *Configuration} class corresponding to the given Configuration Schema.
* @param rootKey Root key of the configuration root.
* @param changer Configuration changer instance to pass into constructor.
* @return Configuration instance.
*/
public synchronized DynamicConfiguration<?, ?> instantiateCfg(
RootKey<?, ?> rootKey,
ConfigurationChanger changer
) {
SchemaClassesInfo info = schemasInfo.get(rootKey.schemaClass());
assert info != null && info.cfgImplClass != null;
try {
Constructor<? extends DynamicConfiguration<?, ?>> constructor = info.cfgImplClass.getConstructor(
List.class,
String.class,
RootKey.class,
ConfigurationChanger.class
);
assert constructor.canAccess(null);
return constructor.newInstance(Collections.emptyList(), rootKey.key(), rootKey, changer);
}
catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Generates, defines, loads and initializes all dynamic classes required for the given configuration schema.
*
* @param rootSchemaClass Class of the root configuration schema.
* @param internalSchemaExtensions Internal extensions ({@link InternalConfiguration})
* of configuration schemas ({@link ConfigurationRoot} and {@link Config}).
* Mapping: original schema -> extensions.
*/
public synchronized void compileRootSchema(
Class<?> rootSchemaClass,
Map<Class<?>, Set<Class<?>>> internalSchemaExtensions
) {
if (schemasInfo.containsKey(rootSchemaClass))
return; // Already compiled.
Queue<Class<?>> compileQueue = new LinkedList<>();
compileQueue.add(rootSchemaClass);
schemasInfo.put(rootSchemaClass, new SchemaClassesInfo(rootSchemaClass));
Set<Class<?>> schemas = new HashSet<>();
List<ClassDefinition> definitions = new ArrayList<>();
while (!compileQueue.isEmpty()) {
Class<?> schemaClass = compileQueue.poll();
assert schemaClass.isAnnotationPresent(ConfigurationRoot.class)
|| schemaClass.isAnnotationPresent(Config.class)
: schemaClass + " is not properly annotated";
assert schemasInfo.containsKey(schemaClass) : schemaClass;
Field[] schemaFields = schemaClass.getDeclaredFields();
Set<Class<?>> schemaExtensions = internalSchemaExtensions.getOrDefault(schemaClass, Set.of());
Set<Field> extensionsFields = extensionsFields(schemaExtensions);
for (Field schemaField : concat(asList(schemaFields), extensionsFields)) {
if (isConfigValue(schemaField) || isNamedConfigValue(schemaField)) {
Class<?> subSchemaClass = schemaField.getType();
if (!schemasInfo.containsKey(subSchemaClass)) {
compileQueue.offer(subSchemaClass);
schemasInfo.put(subSchemaClass, new SchemaClassesInfo(subSchemaClass));
}
}
}
schemas.add(schemaClass);
definitions.add(createNodeClass(schemaClass, schemaExtensions, schemaFields, extensionsFields));
definitions.add(createCfgImplClass(schemaClass, schemaExtensions, schemaFields, extensionsFields));
}
Map<String, Class<?>> definedClasses = generator.defineClasses(definitions);
for (Class<?> schemaClass : schemas) {
SchemaClassesInfo info = schemasInfo.get(schemaClass);
info.nodeClass = (Class<? extends InnerNode>)definedClasses.get(info.nodeClassName);
info.cfgImplClass = (Class<? extends DynamicConfiguration<?, ?>>)definedClasses.get(info.cfgImplClassName);
}
}
/**
* Construct a {@link InnerNode} definition for a configuration schema.
*
* @param schemaClass Configuration schema class.
* @param schemaExtensions Internal extensions of the configuration schema.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
* @return Constructed {@link InnerNode} definition for the configuration schema.
*/
private ClassDefinition createNodeClass(
Class<?> schemaClass,
Set<Class<?>> schemaExtensions,
Field[] schemaFields,
Set<Field> extensionsFields
) {
SchemaClassesInfo schemaClassInfo = schemasInfo.get(schemaClass);
// Node class definition.
ClassDefinition classDef = new ClassDefinition(
of(PUBLIC, FINAL),
internalName(schemaClassInfo.nodeClassName),
type(InnerNode.class),
nodeClassInterfaces(schemaClass, schemaExtensions)
);
// Spec fields.
Map<Class<?>, FieldDefinition> specFields = new HashMap<>();
int i = 0;
for (Class<?> clazz : concat(List.of(schemaClass), schemaExtensions))
specFields.put(clazz, classDef.declareField(of(PRIVATE, FINAL), "_spec" + i++, clazz));
// org.apache.ignite.internal.configuration.tree.InnerNode#schemaType
addNodeSchemaTypeMethod(classDef, specFields.get(schemaClass));
// Define the rest of the fields.
Map<String, FieldDefinition> fieldDefs = new HashMap<>();
for (Field schemaField : concat(asList(schemaFields), extensionsFields)) {
assert isValue(schemaField) || isConfigValue(schemaField) || isNamedConfigValue(schemaField) : schemaField;
fieldDefs.put(schemaField.getName(), addNodeField(classDef, schemaField));
}
// Constructor.
addNodeConstructor(classDef, specFields, fieldDefs, schemaFields, extensionsFields);
// VIEW and CHANGE methods.
for (Field schemaField : concat(asList(schemaFields), extensionsFields)) {
String fieldName = schemaField.getName();
FieldDefinition fieldDef = fieldDefs.get(fieldName);
addNodeViewMethod(classDef, schemaField, fieldDef);
// Add change methods.
MethodDefinition changeMtd = addNodeChangeMethod(classDef, schemaField, fieldDef, schemaClassInfo.nodeClassName);
addNodeChangeBridgeMethod(classDef, changeClassName(schemaField.getDeclaringClass()), changeMtd);
}
// traverseChildren
addNodeTraverseChildrenMethod(classDef, fieldDefs, schemaFields, extensionsFields);
// traverseChild
addNodeTraverseChildMethod(classDef, fieldDefs, schemaFields, extensionsFields);
// construct
addNodeConstructMethod(classDef, fieldDefs, schemaFields, extensionsFields);
// constructDefault
addNodeConstructDefaultMethod(classDef, specFields, fieldDefs, schemaFields, extensionsFields);
return classDef;
}
/**
* Add {@link InnerNode#schemaType()} method implementation to the class. It looks like the following code:
* <pre><code>
* public Class schemaType() {
* return this._spec.getClass();
* }
* </code></pre>
* @param classDef Class definition.
* @param specField Field definition of the {@code _spec} field.
*/
private void addNodeSchemaTypeMethod(ClassDefinition classDef, FieldDefinition specField) {
MethodDefinition schemaTypeMtd = classDef.declareMethod(of(PUBLIC), "schemaType", type(Class.class));
schemaTypeMtd.getBody().append(
schemaTypeMtd.getThis().getField(specField).invoke("getClass", Class.class)
).retObject();
}
/**
* Declares field that corresponds to configuration value. Depending on the schema, 3 options possible:
* <ul>
* <li>
* {@code @Value public type fieldName}<br/>becomes<br/>
* {@code private BoxedType fieldName}
* </li>
* <li>
* {@code @ConfigValue public MyConfigurationSchema fieldName}<br/>becomes<br/>
* {@code private MyNode fieldName}
* </li>
* <li>
* {@code @NamedConfigValue public type fieldName}<br/>becomes<br/>
* {@code private NamedListNode fieldName}
* </li>
* </ul>
* @param classDef Node class definition.
* @param schemaField Configuration Schema class field.
* @return Declared field definition.
*/
private FieldDefinition addNodeField(ClassDefinition classDef, Field schemaField) {
Class<?> schemaFieldClass = schemaField.getType();
ParameterizedType nodeFieldType;
if (isValue(schemaField))
nodeFieldType = type(box(schemaFieldClass));
else if (isConfigValue(schemaField))
nodeFieldType = typeFromJavaClassName(schemasInfo.get(schemaFieldClass).nodeClassName);
else
nodeFieldType = type(NamedListNode.class);
return classDef.declareField(of(PRIVATE), schemaField.getName(), nodeFieldType);
}
/**
* Implements default constructor for the node class. It initializes {@code _spec} field and every other field
* that represents named list configuration.
*
* @param classDef Node class definition.
* @param specFields Definition of fields for the {@code _spec#} fields of the node class.
* Mapping: configuration schema class -> {@code _spec#} field.
* @param fieldDefs Field definitions for all fields of node class excluding {@code _spec}.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
*/
private void addNodeConstructor(
ClassDefinition classDef,
Map<Class<?>, FieldDefinition> specFields,
Map<String, FieldDefinition> fieldDefs,
Field[] schemaFields,
Set<Field> extensionsFields
) {
MethodDefinition ctor = classDef.declareConstructor(of(PUBLIC));
// super();
ctor.getBody().append(ctor.getThis()).invokeConstructor(InnerNode.class);
// this._spec# = new MyConfigurationSchema();
for (Map.Entry<Class<?>, FieldDefinition> e : specFields.entrySet())
ctor.getBody().append(ctor.getThis().setField(e.getValue(), newInstance(e.getKey())));
for (Field schemaField : concat(asList(schemaFields), extensionsFields)) {
if (!isNamedConfigValue(schemaField))
continue;
NamedConfigValue namedCfgAnnotation = schemaField.getAnnotation(NamedConfigValue.class);
SchemaClassesInfo fieldClassNames = new SchemaClassesInfo(schemaField.getType());
// this.values = new NamedListNode<>(key, ValueNode::new);
ctor.getBody().append(ctor.getThis().setField(
fieldDefs.get(schemaField.getName()),
newInstance(
NamedListNode.class,
constantString(namedCfgAnnotation.syntheticKeyName()),
newNamedListElementLambda(fieldClassNames.nodeClassName)
)
));
}
// return;
ctor.getBody().ret();
}
/**
* Implements getter method from {@code VIEW} interface. It returns field value, possibly unboxed or cloned,
* depending on type.
* @param classDef Node class definition.
* @param schemaField Configuration Schema class field.
* @param fieldDef Field definition.
*/
private void addNodeViewMethod(
ClassDefinition classDef,
Field schemaField,
FieldDefinition fieldDef
) {
Class<?> schemaFieldType = schemaField.getType();
ParameterizedType returnType;
// Return type is either corresponding VIEW type or the same type as declared in schema.
if (isConfigValue(schemaField))
returnType = typeFromJavaClassName(schemasInfo.get(schemaFieldType).viewClassName);
else if (isNamedConfigValue(schemaField))
returnType = type(NamedListView.class);
else
returnType = type(schemaFieldType);
String fieldName = schemaField.getName();
MethodDefinition viewMtd = classDef.declareMethod(
of(PUBLIC),
fieldName,
returnType
);
// result = this.field;
viewMtd.getBody().append(viewMtd.getThis().getField(fieldDef));
// result = Box.boxValue(result); // Unboxing.
if (schemaFieldType.isPrimitive()) {
viewMtd.getBody().invokeVirtual(
box(schemaFieldType),
schemaFieldType.getSimpleName() + "Value",
schemaFieldType
);
}
// retuls = result.clone();
if (schemaFieldType.isArray())
viewMtd.getBody().invokeVirtual(schemaFieldType, "clone", Object.class).checkCast(schemaFieldType);
// return result;
viewMtd.getBody().ret(schemaFieldType);
}
/**
* Implements changer method from {@code CHANGE} interface.
*
* @param classDef Node class definition.
* @param schemaField Configuration Schema class field.
* @param fieldDef Field definition.
* @param nodeClassName Class name for the Node class.
* @return Definition of change method.
*/
private MethodDefinition addNodeChangeMethod(
ClassDefinition classDef,
Field schemaField,
FieldDefinition fieldDef,
String nodeClassName
) {
Class<?> schemaFieldType = schemaField.getType();
MethodDefinition changeMtd = classDef.declareMethod(
of(PUBLIC),
"change" + capitalize(schemaField.getName()),
typeFromJavaClassName(nodeClassName),
// Change argument type is a Consumer for all inner or named fields.
arg("change", isValue(schemaField) ? type(schemaFieldType) : type(Consumer.class))
);
BytecodeBlock changeBody = changeMtd.getBody();
// newValue = change;
BytecodeExpression newValue = changeMtd.getScope().getVariable("change");
if (!schemaFieldType.isPrimitive()) {
// Objects.requireNonNull(newValue, "change");
changeBody.append(invokeStatic(REQUIRE_NON_NULL, newValue, constantString("change")));
}
if (isValue(schemaField)) {
// newValue = Box.valueOf(newValue); // Boxing.
if (schemaFieldType.isPrimitive())
newValue = invokeStatic(fieldDef.getType(), "valueOf", fieldDef.getType(), singleton(newValue));
// newValue = newValue.clone();
if (schemaFieldType.isArray())
newValue = newValue.invoke("clone", Object.class).cast(schemaFieldType);
// this.field = newValue;
changeBody.append(changeMtd.getThis().setField(fieldDef, newValue));
}
else {
// this.field = (this.field == null) ? new ValueNode() : (ValueNode)this.field.copy();
changeBody.append(copyNodeField(changeMtd, fieldDef));
// change.accept(this.field);
changeBody.append(changeMtd.getScope().getVariable("change").invoke(
ACCEPT,
changeMtd.getThis().getField(fieldDef)
));
}
// return this;
changeBody.append(changeMtd.getThis()).retObject();
return changeMtd;
}
/**
* Implements changer bridge method from {@code CHANGE} interface.
*
* @param classDef Node class definition.
* @param changeClassName Class name for the CHANGE class.
* @param changeMtd Definition of change method.
*/
private void addNodeChangeBridgeMethod(
ClassDefinition classDef,
String changeClassName,
MethodDefinition changeMtd
) {
MethodDefinition bridgeMtd = classDef.declareMethod(
of(PUBLIC, SYNTHETIC, BRIDGE),
changeMtd.getName(),
typeFromJavaClassName(changeClassName),
changeMtd.getParameters()
);
Variable changeVar = bridgeMtd.getScope().getVariable("change");
// this.change*(change);
BytecodeExpression invokeChangeMtd = bridgeMtd.getThis().invoke(changeMtd, List.of(changeVar));
// return this.change*(change);
bridgeMtd.getBody().append(invokeChangeMtd).retObject();
}
/**
* Implements {@link InnerNode#traverseChildren(ConfigurationVisitor, boolean)} method.
*
* @param classDef Class definition.
* @param fieldDefs Definitions for all fields in {@code schemaFields}.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
*/
private void addNodeTraverseChildrenMethod(
ClassDefinition classDef,
Map<String, FieldDefinition> fieldDefs,
Field[] schemaFields,
Set<Field> extensionsFields
) {
MethodDefinition traverseChildrenMtd = classDef.declareMethod(
of(PUBLIC),
"traverseChildren",
type(void.class),
arg("visitor", type(ConfigurationVisitor.class)),
arg("includeInternal", type(boolean.class))
).addException(NoSuchElementException.class);
BytecodeBlock mtdBody = traverseChildrenMtd.getBody();
invokeVisitForTraverseChildren(asList(schemaFields), fieldDefs, traverseChildrenMtd)
.forEach(mtdBody::append);
if (!extensionsFields.isEmpty()) {
Variable includeInternalVar = traverseChildrenMtd.getScope().getVariable("includeInternal");
BytecodeBlock includeInternalBlock = new BytecodeBlock();
invokeVisitForTraverseChildren(extensionsFields, fieldDefs, traverseChildrenMtd)
.forEach(includeInternalBlock::append);
mtdBody.append(new IfStatement().condition(includeInternalVar).ifTrue(includeInternalBlock));
}
mtdBody.ret();
}
/**
* Implements {@link InnerNode#traverseChild(String, ConfigurationVisitor, boolean)} method.
*
* @param classDef Class definition.
* @param fieldDefs Definitions for all fields in {@code schemaFields}.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
*/
private void addNodeTraverseChildMethod(
ClassDefinition classDef,
Map<String, FieldDefinition> fieldDefs,
Field[] schemaFields,
Set<Field> extensionsFields
) {
MethodDefinition traverseChildMtd = classDef.declareMethod(
of(PUBLIC),
"traverseChild",
type(Object.class),
arg("key", type(String.class)),
arg("visitor", type(ConfigurationVisitor.class)),
arg("includeInternal", type(boolean.class))
).addException(NoSuchElementException.class);
BytecodeBlock mtdBody = traverseChildMtd.getBody();
if (extensionsFields.isEmpty())
mtdBody.append(invokeVisitForTraverseChild(asList(schemaFields), fieldDefs, traverseChildMtd));
else {
Variable includeInternalVar = traverseChildMtd.getScope().getVariable("includeInternal");
mtdBody.append(
new IfStatement()
.condition(includeInternalVar)
.ifTrue(invokeVisitForTraverseChild(union(extensionsFields, schemaFields), fieldDefs, traverseChildMtd))
.ifFalse(invokeVisitForTraverseChild(asList(schemaFields), fieldDefs, traverseChildMtd))
);
}
}
/**
* Creates bytecode block that invokes one of {@link ConfigurationVisitor}'s methods.
*
* @param mtd Method definition, either {@link InnerNode#traverseChildren(ConfigurationVisitor, boolean)} or
* {@link InnerNode#traverseChild(String, ConfigurationVisitor, boolean)} defined in {@code *Node} class.
* @param schemaField Configuration Schema field to visit.
* @param fieldDef Field definition from current class.
* @return Bytecode block that invokes "visit*" method.
*/
private BytecodeBlock invokeVisit(MethodDefinition mtd, Field schemaField, FieldDefinition fieldDef) {
Method visitMethod;
if (isValue(schemaField))
visitMethod = VISIT_LEAF;
else if (isConfigValue(schemaField))
visitMethod = VISIT_INNER;
else
visitMethod = VISIT_NAMED;
return new BytecodeBlock().append(mtd.getScope().getVariable("visitor").invoke(
visitMethod,
constantString(fieldDef.getName()),
mtd.getThis().getField(fieldDef)
));
}
/**
* Implements {@link ConstructableTreeNode#construct(String, ConfigurationSource, boolean)} method.
*
* @param classDef Class definition.
* @param fieldDefs Definitions for all fields in {@code schemaFields}.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
*/
private void addNodeConstructMethod(
ClassDefinition classDef,
Map<String, FieldDefinition> fieldDefs,
Field[] schemaFields,
Set<Field> extensionsFields
) {
MethodDefinition constructMtd = classDef.declareMethod(
of(PUBLIC),
"construct",
type(void.class),
arg("key", type(String.class)),
arg("src", type(ConfigurationSource.class)),
arg("includeInternal", type(boolean.class))
).addException(NoSuchElementException.class);
BytecodeBlock mtdBody = constructMtd.getBody();
if (extensionsFields.isEmpty())
mtdBody.append(treatSourceForConstruct(asList(schemaFields), fieldDefs, constructMtd)).ret();
else {
Variable includeInternalVar = constructMtd.getScope().getVariable("includeInternal");
mtdBody.append(
new IfStatement()
.condition(includeInternalVar)
.ifTrue(treatSourceForConstruct(union(extensionsFields, schemaFields), fieldDefs, constructMtd))
.ifFalse(treatSourceForConstruct(asList(schemaFields), fieldDefs, constructMtd))
).ret();
}
}
/**
* Implements {@link InnerNode#constructDefault(String)} method.
*
* @param classDef Node class definition.
* @param specFields Definition of fields for the {@code _spec#} fields of the node class.
* Mapping: configuration schema class -> {@code _spec#} field.
* @param fieldDefs Field definitions for all fields of node class excluding {@code _spec}.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
*/
private void addNodeConstructDefaultMethod(
ClassDefinition classDef,
Map<Class<?>, FieldDefinition> specFields,
Map<String, FieldDefinition> fieldDefs,
Field[] schemaFields,
Set<Field> extensionsFields
) {
MethodDefinition constructDfltMtd = classDef.declareMethod(
of(PUBLIC),
"constructDefault",
type(void.class),
arg("key", String.class)
).addException(NoSuchElementException.class);
Variable keyVar = constructDfltMtd.getScope().getVariable("key");
StringSwitchBuilder switchBuilder = new StringSwitchBuilder(constructDfltMtd.getScope())
.expression(keyVar);
for (Field schemaField : concat(asList(schemaFields), extensionsFields)) {
if (!isValue(schemaField))
continue;
if (!schemaField.getAnnotation(Value.class).hasDefault()) {
switchBuilder.addCase(schemaField.getName(), new BytecodeBlock());
continue;
}
FieldDefinition fieldDef = fieldDefs.get(schemaField.getName());
Class<?> schemaFieldType = schemaField.getType();
// defaultValue = _spec#.field;
FieldDefinition specField = specFields.get(schemaField.getDeclaringClass());
BytecodeExpression defaultValue = constructDfltMtd.getThis().getField(specField).getField(schemaField);
// defaultValue = Box.valueOf(defaultValue); // Boxing.
if (schemaFieldType.isPrimitive()) {
defaultValue = invokeStatic(
fieldDef.getType(),
"valueOf",
fieldDef.getType(),
singleton(defaultValue)
);
}
// defaultValue = defaultValue.clone();
if (schemaFieldType.isArray())
defaultValue = defaultValue.invoke("clone", Object.class).cast(schemaFieldType);
// this.field = defaultValue;
BytecodeBlock caseClause = new BytecodeBlock()
.append(constructDfltMtd.getThis().setField(fieldDef, defaultValue));
switchBuilder.addCase(schemaField.getName(), caseClause);
}
// Default option is to throw "NoSuchElementException(key)".
switchBuilder.defaultCase(new BytecodeBlock()
.append(newInstance(NoSuchElementException.class, keyVar))
.throwObject()
);
constructDfltMtd.getBody().append(switchBuilder.build()).ret();
}
/**
* Copies field into itself or instantiates it if the field is null.
* @param mtd Method definition.
* @param fieldDef Field definition.
* @return Bytecode expression.
*/
@NotNull private BytecodeExpression copyNodeField(MethodDefinition mtd, FieldDefinition fieldDef) {
return mtd.getThis().setField(fieldDef, inlineIf(
isNull(mtd.getThis().getField(fieldDef)),
newInstance(fieldDef.getType()),
mtd.getThis().getField(fieldDef).invoke(COPY).cast(fieldDef.getType())
));
}
/**
* Creates {@code *Node::new} lambda expression with {@link Supplier} type.
* @param nodeClassName Name of the {@code *Node} class.
* @return InvokeDynamic bytecode expression.
*/
@NotNull private BytecodeExpression newNamedListElementLambda(String nodeClassName) {
return invokeDynamic(
LAMBDA_METAFACTORY,
asList(
getMethodType(getType(Object.class)),
new Handle(
H_NEWINVOKESPECIAL,
internalName(nodeClassName),
"<init>",
getMethodDescriptor(Type.VOID_TYPE),
false
),
getMethodType(typeFromJavaClassName(nodeClassName).getAsmType())
),
"get",
methodType(Supplier.class)
);
}
/**
* Construct a {@link DynamicConfiguration} definition for a configuration schema.
*
* @param schemaClass Configuration schema class.
* @param schemaExtensions Internal extensions of the configuration schema.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
* @return Constructed {@link DynamicConfiguration} definition for the configuration schema.
*/
private ClassDefinition createCfgImplClass(
Class<?> schemaClass,
Set<Class<?>> schemaExtensions,
Field[] schemaFields,
Set<Field> extensionsFields
) {
SchemaClassesInfo schemaClassInfo = schemasInfo.get(schemaClass);
// Configuration impl class definition.
ClassDefinition classDef = new ClassDefinition(
of(PUBLIC, FINAL),
internalName(schemaClassInfo.cfgImplClassName),
type(DynamicConfiguration.class),
configClassInterfaces(schemaClass, schemaExtensions)
);
// Fields.
Map<String, FieldDefinition> fieldDefs = new HashMap<>();
for (Field schemaField : concat(asList(schemaFields), extensionsFields))
fieldDefs.put(schemaField.getName(), addConfigurationImplField(classDef, schemaField));
// Constructor
addConfigurationImplConstructor(classDef, schemaClassInfo, fieldDefs, schemaFields, extensionsFields);
for (Field schemaField : concat(asList(schemaFields), extensionsFields))
addConfigurationImplGetMethod(classDef, schemaClass, fieldDefs, schemaField);
return classDef;
}
/**
* Declares field that corresponds to configuration value. Depending on the schema, 3 options possible:
* <ul>
* <li>
* {@code @Value public type fieldName}<br/>becomes<br/>
* {@code private DynamicProperty fieldName}
* </li>
* <li>
* {@code @ConfigValue public MyConfigurationSchema fieldName}<br/>becomes<br/>
* {@code private MyConfiguration fieldName}
* </li>
* <li>
* {@code @NamedConfigValue public type fieldName}<br/>becomes<br/>
* {@code private NamedListConfiguration fieldName}
* </li>
* </ul>
* @param classDef Configuration impl class definition.
* @param schemaField Configuration Schema class field.
* @return Declared field definition.
*/
private FieldDefinition addConfigurationImplField(ClassDefinition classDef, Field schemaField) {
ParameterizedType fieldType;
if (isConfigValue(schemaField))
fieldType = typeFromJavaClassName(schemasInfo.get(schemaField.getType()).cfgClassName);
else if (isNamedConfigValue(schemaField))
fieldType = type(NamedListConfiguration.class);
else
fieldType = type(DynamicProperty.class);
return classDef.declareField(of(PRIVATE), schemaField.getName(), fieldType);
}
/**
* Implements default constructor for the configuration class. It initializes all fields and adds them to members
* collection.
* @param classDef Configuration impl class definition.
* @param schemaClassInfo Configuration Schema class info.
* @param fieldDefs Field definitions for all fields of configuration impl class.
* @param schemaFields Fields of the schema class.
* @param extensionsFields Fields of internal extensions of the configuration schema.
*/
private void addConfigurationImplConstructor(
ClassDefinition classDef,
SchemaClassesInfo schemaClassInfo,
Map<String, FieldDefinition> fieldDefs,
Field[] schemaFields,
Set<Field> extensionsFields
) {
MethodDefinition ctor = classDef.declareConstructor(
of(PUBLIC),
arg("prefix", List.class),
arg("key", String.class),
arg("rootKey", RootKey.class),
arg("changer", ConfigurationChanger.class)
);
BytecodeBlock ctorBody = ctor.getBody()
.append(ctor.getThis())
.append(ctor.getScope().getVariable("prefix"))
.append(ctor.getScope().getVariable("key"))
.append(ctor.getScope().getVariable("rootKey"))
.append(ctor.getScope().getVariable("changer"))
.invokeConstructor(DYNAMIC_CONFIGURATION_CTOR);
int newIdx = 0;
for (Field schemaField : concat(asList(schemaFields), extensionsFields)) {
FieldDefinition fieldDef = fieldDefs.get(schemaField.getName());
BytecodeExpression newValue;
if (isValue(schemaField)) {
// newValue = new DynamicProperty(super.keys, fieldName, rootKey, changer);
newValue = newInstance(
DynamicProperty.class,
ctor.getThis().getField("keys", List.class),
constantString(schemaField.getName()),
ctor.getScope().getVariable("rootKey"),
ctor.getScope().getVariable("changer")
);
}
else {
SchemaClassesInfo fieldInfo = schemasInfo.get(schemaField.getType());
ParameterizedType cfgImplParameterizedType = typeFromJavaClassName(fieldInfo.cfgImplClassName);
if (isConfigValue(schemaField)) {
// newValue = new MyConfigurationImpl(super.keys, fieldName, rootKey, changer);
newValue = newInstance(
cfgImplParameterizedType,
ctor.getThis().getField("keys", List.class),
constantString(schemaField.getName()),
ctor.getScope().getVariable("rootKey"),
ctor.getScope().getVariable("changer")
);
}
else {
// We have to create method "$new$<idx>" to reference it in lambda expression. That's the way it
// works, it'll invoke constructor with all 4 arguments, not just 2 as in BiFunction.
MethodDefinition newMtd = classDef.declareMethod(
of(PRIVATE, STATIC, SYNTHETIC),
"$new$" + newIdx++,
typeFromJavaClassName(fieldInfo.cfgClassName),
arg("rootKey", RootKey.class),
arg("changer", ConfigurationChanger.class),
arg("prefix", List.class),
arg("key", String.class)
);
// newValue = new NamedListConfiguration(super.keys, fieldName, rootKey, changer, (p, k) ->
// new ValueConfigurationImpl(p, k, rootKey, changer)
// );
newValue = newInstance(
NamedListConfiguration.class,
ctor.getThis().getField("keys", List.class),
constantString(schemaField.getName()),
ctor.getScope().getVariable("rootKey"),
ctor.getScope().getVariable("changer"),
invokeDynamic(
LAMBDA_METAFACTORY,
asList(
getMethodType(getType(Object.class), getType(Object.class), getType(Object.class)),
new Handle(
Opcodes.H_INVOKESTATIC,
internalName(schemaClassInfo.cfgImplClassName),
newMtd.getName(),
newMtd.getMethodDescriptor(),
false
),
getMethodType(
typeFromJavaClassName(fieldInfo.cfgClassName).getAsmType(),
getType(List.class),
getType(String.class)
)
),
"apply",
BiFunction.class,
ctor.getScope().getVariable("rootKey"),
ctor.getScope().getVariable("changer")
)
);
newMtd.getBody()
.append(newInstance(
cfgImplParameterizedType,
newMtd.getScope().getVariable("prefix"),
newMtd.getScope().getVariable("key"),
newMtd.getScope().getVariable("rootKey"),
newMtd.getScope().getVariable("changer")
))
.retObject();
}
}
// this.field = newValue;
ctorBody.append(ctor.getThis().setField(fieldDef, newValue));
// add(this.field);
ctorBody.append(ctor.getThis().invoke(DYNAMIC_CONFIGURATION_ADD, ctor.getThis().getField(fieldDef)));
}
ctorBody.ret();
}
/**
* Implements accessor method in configuration impl class.
* @param classDef Configuration impl class definition.
* @param schemaClass Configuration Schema class.
* @param fieldDefs Field definitions for all fields of configuration impl class.
* @param schemaField Configuration Schema class field.
*/
private void addConfigurationImplGetMethod(
ClassDefinition classDef,
Class<?> schemaClass,
Map<String, FieldDefinition> fieldDefs,
Field schemaField
) {
Class<?> schemaFieldType = schemaField.getType();
String fieldName = schemaField.getName();
FieldDefinition fieldDef = fieldDefs.get(fieldName);
ParameterizedType returnType;
if (isConfigValue(schemaField))
returnType = typeFromJavaClassName(new SchemaClassesInfo(schemaFieldType).cfgClassName);
else if (isNamedConfigValue(schemaField))
returnType = type(NamedConfigurationTree.class);
else {
assert isValue(schemaField) : schemaClass;
returnType = type(ConfigurationValue.class);
}
MethodDefinition viewMtd = classDef.declareMethod(
of(PUBLIC),
fieldName,
returnType
);
BytecodeBlock viewBody = viewMtd.getBody();
viewBody
.append(viewMtd.getThis())
.getField(fieldDef)
.retObject();
}
/**
* Replaces first letter in string with its upper-cased variant.
* @param name Some string.
* @return Capitalized version of passed string.
*/
private static String capitalize(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
/**
* Returns internalized version of class name, replacing dots with slashes.
* @param className Class name (with package).
* @return Internal class name.
* @see Type#getInternalName(java.lang.Class)
*/
@NotNull private static String internalName(String className) {
return className.replace('.', '/');
}
/**
* Creates boxed version of the class. Types that it can box: {@code boolean}, {@code int}, {@code long} and
* {@code double}. Other primitive types are not supported by configuration framework.
* @param clazz Maybe primitive class.
* @return Not primitive class that represents parameter class.
*/
private static Class<?> box(Class<?> clazz) {
Class<?> boxed = TypeUtils.boxed(clazz);
return boxed == null ? clazz : boxed;
}
/**
* Create bytecode blocks that invokes of {@link ConfigurationVisitor}'s methods for
* {@link InnerNode#traverseChildren(ConfigurationVisitor, boolean)}.
*
* @param schemaFields Fields of the schema.
* @param fieldDefs Definitions for all fields in {@code schemaFields}.
* @param traverseChildrenMtd Method definition {@link InnerNode#traverseChildren(ConfigurationVisitor, boolean)}
* defined in {@code *Node} class.
* @return Created bytecode blocks that invokes of {@link ConfigurationVisitor}'s methods for fields.
*/
private Collection<BytecodeNode> invokeVisitForTraverseChildren(
Collection<Field> schemaFields,
Map<String, FieldDefinition> fieldDefs,
MethodDefinition traverseChildrenMtd
) {
if (schemaFields.isEmpty())
return List.of();
else {
return schemaFields.stream()
.map(field -> invokeVisit(traverseChildrenMtd, field, fieldDefs.get(field.getName())).pop())
.collect(toList());
}
}
/**
* Created switch bytecode block that invokes of {@link ConfigurationVisitor}'s methods for
* {@link InnerNode#traverseChild(String, ConfigurationVisitor, boolean)}.
*
* @param schemaFields Fields of the schema.
* @param fieldDefs Definitions for all fields in {@code schemaFields}.
* @param traverseChildMtd Method definition {@link InnerNode#traverseChild(String, ConfigurationVisitor, boolean)}}
* defined in {@code *Node} class.
* @return Created switch bytecode block that invokes of {@link ConfigurationVisitor}'s methods for fields.
*/
private BytecodeNode invokeVisitForTraverseChild(
Collection<Field> schemaFields,
Map<String, FieldDefinition> fieldDefs,
MethodDefinition traverseChildMtd
) {
Variable keyVar = traverseChildMtd.getScope().getVariable("key");
StringSwitchBuilder switchBuilder = new StringSwitchBuilder(traverseChildMtd.getScope()).expression(keyVar);
for (Field schemaField : schemaFields) {
String fieldName = schemaField.getName();
FieldDefinition fieldDef = fieldDefs.get(fieldName);
// Visit result should be immediately returned.
switchBuilder.addCase(fieldName, invokeVisit(traverseChildMtd, schemaField, fieldDef).retObject());
}
// Default option is to throw "NoSuchElementException(key)".
switchBuilder.defaultCase(new BytecodeBlock()
.append(newInstance(NoSuchElementException.class, keyVar))
.throwObject()
);
return switchBuilder.build();
}
/**
* Created switch bytecode block that invokes of construct methods for
* {@link InnerNode#construct(String, ConfigurationSource, boolean)}.
*
* @param schemaFields Fields of the schema.
* @param fieldDefs Definitions for all fields in {@code schemaFields}.
* @param constructMtd Method definition {@link InnerNode#construct(String, ConfigurationSource, boolean)}
* defined in {@code *Node} class.
* @return Created switch bytecode block that invokes of construct methods for fields.
*/
private BytecodeNode treatSourceForConstruct(
Collection<Field> schemaFields,
Map<String, FieldDefinition> fieldDefs,
MethodDefinition constructMtd
) {
Variable keyVar = constructMtd.getScope().getVariable("key");
Variable srcVar = constructMtd.getScope().getVariable("src");
StringSwitchBuilder switchBuilder = new StringSwitchBuilder(constructMtd.getScope()).expression(keyVar);
for (Field schemaField : schemaFields) {
FieldDefinition fieldDef = fieldDefs.get(schemaField.getName());
BytecodeBlock caseClause = new BytecodeBlock();
switchBuilder.addCase(schemaField.getName(), caseClause);
// this.field = src == null ? null : src.unwrap(FieldType.class);
if (isValue(schemaField)) {
caseClause.append(constructMtd.getThis().setField(fieldDef, inlineIf(
isNull(srcVar),
constantNull(fieldDef.getType()),
srcVar.invoke(UNWRAP, constantClass(fieldDef.getType())).cast(fieldDef.getType())
)));
}
// this.field = src == null ? null : src.descend(field = (field == null ? new FieldType() : field.copy()));
else if (isConfigValue(schemaField)) {
caseClause.append(new IfStatement()
.condition(isNull(srcVar))
.ifTrue(constructMtd.getThis().setField(fieldDef, constantNull(fieldDef.getType())))
.ifFalse(new BytecodeBlock()
.append(copyNodeField(constructMtd, fieldDef))
.append(srcVar.invoke(DESCEND, constructMtd.getThis().getField(fieldDef)))
)
);
}
// this.field = src == null ? new NamedListNode<>(key, ValueNode::new) : src.descend(field = field.copy()));
else {
NamedConfigValue namedCfgAnnotation = schemaField.getAnnotation(NamedConfigValue.class);
String fieldNodeClassName = schemasInfo.get(schemaField.getType()).nodeClassName;
caseClause.append(new IfStatement()
.condition(isNull(srcVar))
.ifTrue(constructMtd.getThis().setField(
fieldDef,
newInstance(
NamedListNode.class,
constantString(namedCfgAnnotation.syntheticKeyName()),
newNamedListElementLambda(fieldNodeClassName)
)
))
.ifFalse(new BytecodeBlock()
.append(constructMtd.getThis().setField(
fieldDef,
constructMtd.getThis().getField(fieldDef).invoke(COPY).cast(fieldDef.getType())
))
.append(srcVar.invoke(DESCEND, constructMtd.getThis().getField(fieldDef)))
)
);
}
}
// Default option is to throw "NoSuchElementException(key)".
switchBuilder.defaultCase(new BytecodeBlock()
.append(newInstance(NoSuchElementException.class, keyVar))
.throwObject()
);
return switchBuilder.build();
}
/**
* Get interfaces for {@link InnerNode} definition for a configuration schema.
*
* @param schemaClass Configuration schema class.
* @param schemaExtensions Internal extensions of the configuration schema.
* @return Interfaces for {@link InnerNode} definition for a configuration schema.
*/
private static ParameterizedType[] nodeClassInterfaces(Class<?> schemaClass, Set<Class<?>> schemaExtensions) {
return Stream.concat(Stream.of(schemaClass), schemaExtensions.stream())
.flatMap(cls -> Stream.of(viewClassName(cls), changeClassName(cls)))
.map(ParameterizedType::typeFromJavaClassName)
.toArray(ParameterizedType[]::new);
}
/**
* Get interfaces for {@link DynamicConfiguration} definition for a configuration schema.
*
* @param schemaClass Configuration schema class.
* @param schemaExtensions Internal extensions of the configuration schema.
* @return Interfaces for {@link DynamicConfiguration} definition for a configuration schema.
*/
private static ParameterizedType[] configClassInterfaces(Class<?> schemaClass, Set<Class<?>> schemaExtensions) {
return Stream.concat(Stream.of(schemaClass), schemaExtensions.stream())
.map(cls -> typeFromJavaClassName(configurationClassName(cls)))
.toArray(ParameterizedType[]::new);
}
}