content: /*
   * Copyright 2007-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License").
   * You may not use this file except in compliance with the License.
   * A copy of the License is located at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * or in the "license" file accompanying this file. This file 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 com.amazon.ion.junit;
  
  import java.beans.BeanInfo;
  import java.beans.Introspector;
  import java.beans.PropertyDescriptor;
  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.annotation.Target;
  import java.lang.reflect.Method;
  import java.lang.reflect.Modifier;
  import java.util.ArrayList;
  import java.util.List;
  import org.junit.runner.Runner;
  import org.junit.runner.notification.RunNotifier;
  import org.junit.runners.BlockJUnit4ClassRunner;
  import org.junit.runners.Parameterized;
  import org.junit.runners.Suite;
  import org.junit.runners.model.FrameworkField;
  import org.junit.runners.model.FrameworkMethod;
  import org.junit.runners.model.InitializationError;
  import org.junit.runners.model.Statement;
  import org.junit.runners.model.TestClass;
  
  /**
   * A JUnit 4 {@link Runner} that injects one or more JavaBeans properties of
   * the test fixture with a set of configured values.  This approach is similar
   * to {@link Parameterized} but utilizes setter injection instead of using
   * constructors, so its easier to reuse the injection via inheritance.
   */
  public class Injected
  extends Suite
  {
  
      /**
       * Annotation for a public static field which provides values to be
       * injected into the fixture. The {@code value} element of this annotation
       * must be the name of a writable property of the fixture.
       */
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.FIELD)
      public static @interface Inject
      {
          /**
           * The name of the property to inject.
           */
          String value();
      }
  
  
      private static final class Dimension
      {
          String property;
          PropertyDescriptor descriptor;
          Object[] values;
      }
  
  
      private static class InjectedRunner
      extends BlockJUnit4ClassRunner
      {
          private final Dimension[] myDimensions;
          private final int[] myValueIndices;
          private String myName;
  
          /**
           * @param klass
           * @throws InitializationError
           */
          public InjectedRunner(Class<?> klass,
                                Dimension[] dimensions,
                                int... indices)
          throws InitializationError
          {
              super(klass);
  
              assert dimensions.length == indices.length;
  
              myDimensions = dimensions;
              myValueIndices = indices;
          }
  
          @Override
          public Object createTest()
          throws Exception
          {
              Object test = getTestClass().getOnlyConstructor().newInstance();
              for (int i = 0; i < myDimensions.length; i++)
              {
                  inject(test, myDimensions[i], myValueIndices[i]);
              }
              return test;
          }
  
          public void inject(Object target, Dimension dimension, int valueIndex)
          throws Exception
          {
              Method method = dimension.descriptor.getWriteMethod();
              Object value  = dimension.values[valueIndex];
              method.invoke(target, value);
          }
  
          @Override
          protected synchronized String getName()
          {
              if (myName == null)
              {
                  StringBuilder buf = new StringBuilder("[");
                  for (int i = 0; i < myDimensions.length; i++)
                  {
                      if (i != 0) buf.append(',');
  
                      Dimension dim = myDimensions[i];
                      int valueIndex =  myValueIndices[i];
                      buf.append(dim.values[valueIndex]);
                  }
                  buf.append(']');
                  myName = buf.toString();
              }
              return myName;
          }
  
          @Override
          protected String testName(FrameworkMethod method)
          {
              // Eclipse (Helios) can't display results properly if the names
              // are not unique.
              return method.getName() + getName();
          }
  
          @Override
          protected void validateConstructor(List<Throwable> errors)
          {
              validateOnlyOneConstructor(errors);
          }
  
          @Override
          protected Statement classBlock(RunNotifier notifier)
          {
              return childrenInvoker(notifier);
          }
      }
  
  
  
      /**
       * Only called reflectively. Do not use programmatically.
       */
      public Injected(Class<?> klass)
      throws Throwable
      {
          super(klass, fanout(klass));
      }
  
      private static List<Runner> fanout(Class<?> klass) throws Throwable
      {
          Dimension[] dimensions = findDimensions(new TestClass(klass));
          assert dimensions.length != 0;
  
          return fanout(klass, new ArrayList<Runner>(), dimensions, new int[dimensions.length], 0);
      }
  
      private static List<Runner> fanout(Class<?> klass,
                                         List<Runner> runners,
                                         Dimension[] dimensions,
                                         int[] valueIndices,
                                         int dimensionIndex)
      throws InitializationError
      {
          assert dimensions.length == valueIndices.length;
  
          if (dimensionIndex == dimensions.length)
          {
              InjectedRunner runner =
                  new InjectedRunner(klass, dimensions, valueIndices);
              runners.add(runner);
          }
          else
          {
              Dimension dim = dimensions[dimensionIndex];
              int width = dim.values.length;
              for (int i = 0; i < width; i++)
              {
                  int[] childIndexes = valueIndices.clone();
                  childIndexes[dimensionIndex] = i;
                  fanout(klass, runners, dimensions, childIndexes, dimensionIndex + 1);
              }
          }
          return runners;
      }
  
  
      private static Dimension[] findDimensions(TestClass testClass)
      throws Throwable
      {
          List<FrameworkField> fields =
              testClass.getAnnotatedFields(Inject.class);
          if (fields.isEmpty())
          {
              throw new Exception("No fields of " + testClass.getName()
                                  + " have the @Inject annotation");
          }
  
          BeanInfo beanInfo = Introspector.getBeanInfo(testClass.getJavaClass());
          PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
  
          Dimension[] dimensions = new Dimension[fields.size()];
  
          int i = 0;
          for (FrameworkField field : fields)
          {
              int modifiers = field.getField().getModifiers();
              if (! Modifier.isPublic(modifiers) || ! Modifier.isStatic(modifiers))
              {
                  throw new Exception("@Inject " + testClass.getName() + '.'
                                      + field.getField().getName()
                                      + " must be public static");
              }
  
              Dimension dim = new Dimension();
              dim.property = field.getField().getAnnotation(Inject.class).value();
              dim.descriptor = findDescriptor(testClass, descriptors, field, dim.property);
              dim.values = (Object[]) field.get(null);
              dimensions[i++] = dim;
          }
  
          return dimensions;
      }
  
  
      private static PropertyDescriptor findDescriptor(TestClass testClass,
                                                PropertyDescriptor[] descriptors,
                                                FrameworkField field,
                                                String name)
      throws Exception
      {
          for (PropertyDescriptor d : descriptors)
          {
              if (d.getName().equals(name))
              {
                  if (d.getWriteMethod() == null) break;  // To throw error
                  return d;
              }
          }
  
          throw new Exception("@Inject value '" + name
                              + "' doesn't match a writeable property near "
                              + testClass.getName() + '.'
                              + field.getField().getName());
      }
  }
  