/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.entitysystem.component; import com.esotericsoftware.reflectasm.MethodAccess; import com.google.common.base.CaseFormat; import com.google.common.base.Converter; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.entitysystem.Component; import org.terasology.valuetype.TypeLibrary; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import java.util.Map; /** * A ComponentManager that generates component implementation classes. This expect component interfaces to define properties with getter and setter methods - */ public class CodeGenComponentManager implements ComponentManager { private static final Logger logger = LoggerFactory.getLogger(CodeGenComponentManager.class); private static final Converter<String, String> TO_LOWER_CAMEL = CaseFormat.UPPER_CAMEL.converterTo(CaseFormat.LOWER_CAMEL); private static final Converter<String, String> TO_UPPER_CAMEL = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL); private Map<Class<? extends Component>, ComponentType> componentMetadataLookup = new MapMaker().concurrencyLevel(4).makeMap(); private Map<Class<? extends Component>, ComponentType> componentImplMetadataLookup = new MapMaker().concurrencyLevel(4).makeMap(); private ClassPool pool; private CtClass parent; private ClassLoader targetLoader; private final TypeLibrary typeLibrary; public CodeGenComponentManager(TypeLibrary typeLibrary, ClassLoader classLoader) { this.typeLibrary = typeLibrary; this.targetLoader = classLoader; ClassPool.doPruning = true; pool = new ClassPool(ClassPool.getDefault()); try { parent = pool.get(SharedComponentImpl.class.getName()); } catch (NotFoundException e) { throw new RuntimeException("Unable to resolve SharedComponentImpl", e); } } @Override @SuppressWarnings("unchecked") public <T extends Component> ComponentType<T> getType(Class<T> type) { if (type.isInterface()) { ComponentType<T> componentMetadata = this.componentMetadataLookup.get(type); if (componentMetadata == null) { componentMetadata = createComponentType(type); this.componentMetadataLookup.put(type, componentMetadata); this.componentImplMetadataLookup.put(componentMetadata.getImplType(), componentMetadata); } return componentMetadata; } else { return this.componentImplMetadataLookup.get(type); } } @Override public <T extends Component> T create(Class<T> type) { ComponentType<T> typeInfo = getType(type); return typeInfo.create(); } @Override @SuppressWarnings("unchecked") public <T extends Component> T copy(T instance) { if (instance != null) { Class<T> componentClass = (Class<T>) instance.getClass(); return copy(instance, getType(componentClass).create()); } return null; } @Override @SuppressWarnings("unchecked") public <T extends Component> T copy(T from, T to) { Preconditions.checkNotNull(from); Preconditions.checkNotNull(to); Preconditions.checkArgument(from.getClass().equals(to.getClass()), "Components from and to must be of the same type"); Class<T> componentClass = (Class<T>) from.getClass(); ComponentType<T> metadata = getType(componentClass); return metadata.copy(from, to); } /** * Generates a component type, constructing an implementation class for the component. * <p> * This uses javassist to generate the implementation of the getters and setters for the component * @param type The interface type for the component * @param <T> The interface type of the component * @return The ComponentType for the component */ @SuppressWarnings("unchecked") private <T extends Component> ComponentType<T> createComponentType(Class<T> type) { try { CtClass componentInterface = pool.get(type.getName()); CtClass componentClass = pool.makeClass(type.getName() + "Imp"); componentClass.setInterfaces(new CtClass[]{componentInterface}); componentClass.setSuperclass(parent); Collection<PropertyAccessor<T, ?>> accessorList = discoverProperties(type); for (PropertyAccessor<T, ?> accessor : accessorList) { CtField ctField = CtField.make("private " + accessor.getPropertyType().getTypeName() + " " + accessor.getName() + ";", componentClass); componentClass.addField(ctField); String getterName; if (Boolean.TYPE.equals(accessor.getPropertyType())) { getterName = "is" + TO_UPPER_CAMEL.convert(accessor.getName()); } else { getterName = "get" + TO_UPPER_CAMEL.convert(accessor.getName()); } CtMethod getter = CtNewMethod.make("public " + accessor.getPropertyType().getTypeName() + " " + getterName + "() { return " + accessor.getName() + "; }", componentClass); componentClass.addMethod(getter); CtMethod setter = CtNewMethod.make("public void set" + TO_UPPER_CAMEL.convert(accessor.getName()) + "(" + accessor.getPropertyType().getTypeName() + " value) { this." + accessor.getName() + " = value; }", componentClass); componentClass.addMethod(setter); } Class<? extends T> implementationClass = componentClass.toClass(targetLoader, type.getProtectionDomain()); ComponentPropertyInfo<T> propertyInfo = new ComponentPropertyInfo<>(accessorList); return new ComponentType<>(() -> { try { return implementationClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException("Failed to instantiate component " + type.getName(), e); } }, type, implementationClass, propertyInfo, new ComponentCopyFunction<>(propertyInfo, typeLibrary)); } catch (CannotCompileException e) { throw new RuntimeException("Error compiling component implementation '" + type.getName() + "'", e); } catch (NotFoundException e) { throw new RuntimeException("Error resolving component interface '" + type.getName() + "'", e); } } /** * Scans a component interface, discovering the properties it declares and creating a property accessor for the component. * @param componentType The type of the component to scan * @param <T> The type of the component to scan * @return A collection of property accessors */ private <T extends Component> Collection<PropertyAccessor<T, ?>> discoverProperties(Class<T> componentType) { List<PropertyAccessor<T, ?>> accessorList = Lists.newArrayList(); for (Method method : componentType.getDeclaredMethods()) { if (method.getGenericReturnType() == Void.TYPE && method.getName().startsWith("set") && method.getParameterCount() == 1) { String propertyName = method.getName().substring(3); Type propertyType = method.getGenericParameterTypes()[0]; String getterMethodName; if (Boolean.TYPE.equals(propertyType)) { getterMethodName = "is" + propertyName; } else { getterMethodName = "get" + propertyName; } Method getter; try { getter = componentType.getDeclaredMethod(getterMethodName); } catch (NoSuchMethodException e) { logger.error("Unable to find getter is{}", propertyName); // TODO: Exception continue; } if (!getter.getReturnType().equals(propertyType)) { logger.error("Property type mismatch for '{}' between getter and setter", TO_LOWER_CAMEL.convert(propertyName)); // TODO: Exception continue; } MethodAccess methodAccess = MethodAccess.get(componentType); int getterIndex = methodAccess.getIndex(getter.getName()); int setterIndex = methodAccess.getIndex(method.getName()); accessorList.add(new PropertyAccessor<>(TO_LOWER_CAMEL.convert(propertyName), componentType, propertyType, t -> methodAccess.invoke(t, getterIndex), (t, o) -> methodAccess.invoke(t, setterIndex, o))); } } return accessorList; } }