All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.hyperfoil.api.config.BuilderBase Maven / Gradle / Ivy

There is a newer version: 0.27
Show newest version
package io.hyperfoil.api.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Intended base for all builders that might need relocation when the step is copied over.
 */
public interface BuilderBase> {
   default void prepareBuild() {
      Class clz = getClass();
      while (clz != null && clz != Object.class) {
         for (Field f : clz.getDeclaredFields()) {
            if (f.isSynthetic() || Modifier.isStatic(f.getModifiers()) || "parent".equals(f.getName())) {
               continue;
            }
            f.setAccessible(true);
            try {
               tryPrepare(clz, f.getName(), f.getType(), f.get(this));
            } catch (IllegalAccessException e) {
               throw new UnsupportedOperationException(
                     "Cannot get value of " + clz.getName() + "." + f.getName() + " (actual instance: " + this + ")");
            }
         }
         clz = clz.getSuperclass();
      }
   }

   private void tryPrepare(Class clz, String name, Class type, Object value) throws IllegalAccessException {
      if (BuilderBase.class.isAssignableFrom(type)) {
         if (value != null) {
            ((BuilderBase) value).prepareBuild();
         }
      } else if (Collection.class.isAssignableFrom(type)) {
         if (value != null) {
            for (Object item : (Collection) value) {
               if (item != null) {
                  tryPrepare(clz, name, item.getClass(), item);
               }
            }
         }
      } else if (BaseSequenceBuilder.class.isAssignableFrom(type)) {
         if (value != null) {
            ((BaseSequenceBuilder) value).prepareBuild();
         }
      } else if (Map.class.isAssignableFrom(type)) {
         if (value != null) {
            for (Map.Entry entry : ((Map) value).entrySet()) {
               if (entry.getKey() != null) {
                  tryPrepare(clz, name, entry.getKey().getClass(), entry.getKey());
               }
               if (entry.getValue() != null) {
                  tryPrepare(clz, name, entry.getValue().getClass(), entry.getValue());
               }
            }
         }
      } else if (type.isArray()) {
         throw new UnsupportedOperationException(clz.getName() + "." + name + " is an array (actual instance: " + this + ")");
      }
   }

   /**
    * Some scenarios copy its parts from one place to another, either during parsing
    * phase (e.g. through YAML anchors) or in {@link #prepareBuild()}.
    * In order to make sure that modification in one place does not accidentally change
    * the original one we require defining a deep copy method on each builder. The only
    * exception is when the builder is immutable (including potential children builder);
    * in that case the deep copy is not necessary and this method can return this.
    * 

* The default implementation uses reflection to create a deep copy of all collections and maps, * calling copy() on all objects implementing {@link BuilderBase}. * * @param newParent Object passed to a matching constructor. * @return Deep copy of this object. */ @SuppressWarnings("unchecked") default S copy(Object newParent) { if (getClass().isSynthetic()) { // This is most likely a lambda supplier of the instance (which should be immutable anyway) assert getClass().getSimpleName().contains("$$Lambda$"); return (S) this; } try { ThrowingSupplier> constructor = null; for (Constructor ctor : getClass().getConstructors()) { // parameterless constructor has lowest priority if (ctor.getParameterCount() == 0 && constructor == null) { constructor = () -> (BuilderBase) ctor.newInstance(); } else if (ctor.getParameterCount() == 1) { Class parameterType = ctor.getParameterTypes()[0]; if (parameterType == getClass()) { constructor = () -> (BuilderBase) ctor.newInstance(this); // copy constructor has highest priority break; } else if (newParent != null && parameterType.isAssignableFrom(newParent.getClass())) { constructor = () -> (BuilderBase) ctor.newInstance(newParent); } } } if (constructor == null) { throw new NoSuchMethodException("No constructor for " + getClass().getName()); } BuilderBase copy = constructor.get(); Class cls = getClass(); while (cls != null && cls != BuilderBase.class) { for (Field f : cls.getDeclaredFields()) { f.setAccessible(true); if (Modifier.isStatic(f.getModifiers())) { // do not copy static fields } else if (f.isAnnotationPresent(IgnoreCopy.class)) { // field is intentionally omitted } else if (Modifier.isFinal(f.getModifiers())) { Object thisValue = f.get(this); Object copyValue = f.get(copy); if (thisValue == copyValue) { // usually happens when the value is null } else if (copyValue instanceof Collection) { // final collections can only get the elements Collection copyCollection = (Collection) copyValue; copyCollection.clear(); copyCollection.addAll((Collection) CopyUtil.deepCopy(thisValue, copy)); } else if (f.getName().equals("parent")) { // Fluent builders often require parent element reference; in YAML configuration these are not used. } else if (copyValue instanceof BaseSequenceBuilder && thisValue instanceof BaseSequenceBuilder) { List> newSteps = ((BaseSequenceBuilder) copyValue).steps; ((BaseSequenceBuilder) thisValue).steps.forEach(sb -> newSteps.add(sb.copy(copyValue))); } else { // This could be e.g. final list and we wouldn't copy it throw new UnsupportedOperationException( cls.getName() + "." + f.getName() + " is final (actual instance: " + this + ")"); } } else if (f.getType().isPrimitive()) { if (f.getType() == boolean.class) { f.setBoolean(copy, f.getBoolean(this)); } else if (f.getType() == int.class) { f.setInt(copy, f.getInt(this)); } else if (f.getType() == long.class) { f.setLong(copy, f.getLong(this)); } else if (f.getType() == double.class) { f.setDouble(copy, f.getDouble(this)); } else if (f.getType() == float.class) { f.setFloat(copy, f.getFloat(this)); } else if (f.getType() == byte.class) { f.setByte(copy, f.getByte(this)); } else if (f.getType() == char.class) { f.setChar(copy, f.getChar(this)); } else if (f.getType() == short.class) { f.setShort(copy, f.getShort(this)); } else { throw new UnsupportedOperationException("Unknown primitive: " + f.getType()); } } else if (f.getType().isArray()) { if (f.getType().getComponentType() == byte.class) { byte[] bytes = (byte[]) f.get(this); f.set(copy, bytes == null ? null : Arrays.copyOf(bytes, bytes.length)); } else { // use list in builders throw new UnsupportedOperationException( cls.getName() + "." + f.getName() + " is an array (actual instance: " + this + ")"); } } else { f.set(copy, CopyUtil.deepCopy(f.get(this), copy)); } } cls = cls.getSuperclass(); } return (S) copy; } catch (ReflectiveOperationException e) { throw new BenchmarkDefinitionException("Default deep copy failed", e); } } interface ThrowingSupplier { T get() throws ReflectiveOperationException; } class CopyUtil { private static Object deepCopy(Object o, Object newParent) throws ReflectiveOperationException { if (o == null) { return null; } else if (BuilderBase.class.isAssignableFrom(o.getClass())) { return ((BuilderBase) o).copy(newParent); } else if (Collection.class.isAssignableFrom(o.getClass())) { Collection thisCollection = (Collection) o; @SuppressWarnings("unchecked") Collection newCollection = thisCollection.getClass().getConstructor().newInstance(); for (Object item : thisCollection) { newCollection.add(deepCopy(item, newParent)); } return newCollection; } else if (Map.class.isAssignableFrom(o.getClass())) { Map thisMap = (Map) o; @SuppressWarnings("unchecked") Map newMap = thisMap.getClass().getConstructor().newInstance(); for (Map.Entry entry : thisMap.entrySet()) { newMap.put(deepCopy(entry.getKey(), null), deepCopy(entry.getValue(), newParent)); } return newMap; } else { return o; } } } /** * Used to ignore copying the field (e.g. when it's final, or we don't want to call it 'parent'). */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface IgnoreCopy { } }