content: /**
   * ﻿Copyright (C) 2007 - 2019 52°North Initiative for Geospatial Open Source
   * Software GmbH
   *
   * 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.n52.wps.algorithm.annotation;
  
  import java.lang.reflect.AccessibleObject;
  import java.lang.reflect.Constructor;
  import java.lang.reflect.Field;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Member;
  import java.lang.reflect.Method;
  import java.lang.reflect.Modifier;
  import java.lang.reflect.ParameterizedType;
  import java.lang.reflect.Type;
  import java.lang.reflect.WildcardType;
  import java.util.ArrayList;
  import java.util.List;
  import org.n52.wps.algorithm.descriptor.BoundDescriptor;
  import org.n52.wps.algorithm.descriptor.InputDescriptor;
  import org.n52.wps.algorithm.descriptor.OutputDescriptor;
  import org.n52.wps.algorithm.util.ClassUtil;
  import org.n52.wps.io.data.IData;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   *
   * @author tkunicki
   */
  public abstract class AnnotationBinding<M extends AccessibleObject & Member> {
  
      private final static Logger LOGGER = LoggerFactory.getLogger(AnnotationBinding.class);
      
      private M member;
  
      public AnnotationBinding(M member) {
          this.member = member;
      }
  
      public M getMember() {
          return member;
      }
      
      protected boolean checkModifier() {
          return (getMember().getModifiers() & Modifier.PUBLIC) != 0;
      }
      
      public abstract boolean validate();
      
      public static class ExecuteMethodBinding extends AnnotationBinding<Method> {
  
          public ExecuteMethodBinding(Method method) {
              super(method);
          }
  
          @Override
          public boolean validate() {
              if (!checkModifier()) {
                  LOGGER.error("Method {} with Execute annotation can't be used, not public.", getMember());
                  return false;
              }
              // eh, do we really need to care about this?
              if (!getMember().getReturnType().equals(void.class)) {
                  LOGGER.error("Method {} with Execute annotation can't be used, return type not void", getMember());
                  return false;
              }
              if (getMember().getParameterTypes().length != 0) {
                  LOGGER.error("Method {} with Execute annotation can't be used, method parameter count is > 0.", getMember());
                  return false;
              }
              return true;
          }
          
          public void execute(Object annotatedInstance) {
              try {
                  getMember().invoke(annotatedInstance);
              }catch (IllegalAccessException ex) {
                  throw new RuntimeException("Internal error executing process", ex);
              } catch (IllegalArgumentException ex) {
                  throw new RuntimeException("Internal error executing process", ex);
              } catch (InvocationTargetException ex) {
                  Throwable cause = ex.getCause() == null ? ex : ex.getCause();
                  throw new RuntimeException(cause.getMessage(), cause);
              }
          }
      }
      
      public static abstract class DataBinding<M extends AccessibleObject & Member, D extends BoundDescriptor> extends AnnotationBinding<M> {
          
          private D descriptor;
          
          public DataBinding(M member) {
              super(member);
          }
          
          public void setDescriptor(D descriptor) {
              this.descriptor = descriptor;
          }
          
          public D getDescriptor() {
              return descriptor;
          }
          
          public abstract Type getMemberType();
  
          public Type getType() {
              return getMemberType();
          }
  
          public Type getPayloadType() {
              Type type = getType();
              if (isTypeEnum()) {
                  return String.class;
              }
              if (type instanceof Class<?>) {
                  Class<?> inputClass = (Class<?>) type;
                  if (inputClass.isPrimitive()) {
                      return ClassUtil.wrap(inputClass);
                  }
              }
              return type;
          }
  
          public boolean isTypeEnum() {
              Type inputType = getType();
              return (inputType instanceof Class<?>) && ((Class<?>) inputType).isEnum();
          }
      }
  
      public static abstract class InputBinding<M extends AccessibleObject & Member, D extends InputDescriptor> extends DataBinding<M,D> {
          
          public InputBinding(M member) {
              super(member);
          }
  
          @Override
          public Type getType() {
              Type memberType = getMemberType();
              Type inputType = memberType;
              if (memberType instanceof Class<?>) {
                  Class<?> memberClass = (Class<?>) memberType;
                  if (List.class.isAssignableFrom(memberClass)) {
                      // We treat List as List<? extends Object>
                      inputType = NOT_PARAMETERIZED_TYPE;
                  }
              } else if (memberType instanceof ParameterizedType) {
                  ParameterizedType parameterizedMemberType = (ParameterizedType) memberType;
                  Class<?> rawClass = (Class<?>) parameterizedMemberType.getRawType();
                  if (List.class.isAssignableFrom(rawClass)) {
                      inputType = parameterizedMemberType.getActualTypeArguments()[0];
                  }
              } else {
                  LOGGER.error("Unable to infer concrete type information for " + getMember());
              }
              return inputType;
          }
  
          public boolean isMemberTypeList() {
              Type memberType = getMemberType();
              if (memberType instanceof Class<?>) {
                  return List.class.isAssignableFrom((Class<?>) memberType);
              } else if (memberType instanceof ParameterizedType) {
                  Class<?> rawClass = (Class<?>) ((ParameterizedType) memberType).getRawType();
                  return List.class.isAssignableFrom(rawClass);
              } else {
                  LOGGER.error("Unable to infer concrete type information for " + getMember());
              }
              return false;
          }
          
          protected boolean checkType() {
              Type inputPayloadType = getPayloadType();
              Class<? extends IData> bindingClass = getDescriptor().getBinding();
              try {
                  Class<?> bindingPayloadClass = bindingClass.getMethod("getPayload", (Class<?>[]) null).getReturnType();
                  if (inputPayloadType instanceof Class<?>) {
                      return ((Class<?>) inputPayloadType).isAssignableFrom(bindingPayloadClass);
                  } else if (inputPayloadType instanceof ParameterizedType) {
                      // i.e. List<FeatureCollection<SimpleFeatureType,SimpleFeature>>
                      return ((Class<?>) ((ParameterizedType) inputPayloadType).getRawType()).isAssignableFrom(bindingPayloadClass);
                  } else if (inputPayloadType instanceof WildcardType) {
                      // i.e. List<? extends String> or List<? super String>
                      WildcardType inputTypeWildcardType = (WildcardType) inputPayloadType;
                      Type[] lowerBounds = inputTypeWildcardType.getLowerBounds();
                      Type[] upperBounds = inputTypeWildcardType.getUpperBounds();
                      Class<?> lowerBoundClass = null;
                      Class<?> upperBoundClass = null;
                      if (lowerBounds != null && lowerBounds.length > 0) {
                          if (lowerBounds[0] instanceof Class<?>) {
                              lowerBoundClass = (Class<?>) lowerBounds[0];
                          } else if (lowerBounds[0] instanceof ParameterizedType) {
                              lowerBoundClass = (Class<?>) ((ParameterizedType) lowerBounds[0]).getRawType();
                          }
                      }
                      if (upperBounds != null && upperBounds.length > 0) {
                          if (upperBounds[0] instanceof Class<?>) {
                              upperBoundClass = (Class<?>) upperBounds[0];
                          } else if (upperBounds[0] instanceof ParameterizedType) {
                              upperBoundClass = (Class<?>) ((ParameterizedType) upperBounds[0]).getRawType();
                          }
                      }
                      return (upperBoundClass == null || upperBoundClass.isAssignableFrom(bindingPayloadClass)) && (lowerBounds == null || bindingPayloadClass.isAssignableFrom(lowerBoundClass));
                  } else {
                      LOGGER.error("Unable to infer assignability from type for " + getMember());
                  }
              } catch (NoSuchMethodException e) {
                  return false;
              }
              return false;
          }
          
          public Object unbindInput(List<IData> boundValueList) {
              Object value = null;
              if (boundValueList != null && boundValueList.size() > 0) {
                  if (isMemberTypeList()) {
                      List valueList = new ArrayList(boundValueList.size());
                      for (IData bound : boundValueList) {
                          value = bound.getPayload();
                          if (isTypeEnum()) {
                              value = Enum.valueOf((Class<? extends Enum>)getType(), (String)value);
                          }
                          valueList.add(value);
                      }
                      value = valueList;
                  } else if (boundValueList.size() == 1) {
                      value = boundValueList.get(0).getPayload();
                      if (isTypeEnum()) {
                          value = Enum.valueOf((Class<? extends Enum>)getType(), (String)value);
                      }
                  }
              }
              return value;
          }
          
          public abstract void set(Object annotatedObject, List<IData> boundInputList);
      }
  
      public static abstract class OutputBinding<M extends AccessibleObject & Member,  D extends OutputDescriptor> extends DataBinding<M,D> {
          
          private Constructor<? extends IData> bindingConstructor;
          
          public OutputBinding(M member) {
              super(member);
          }
          
          protected boolean checkType( ) {
              return getConstructor() != null;
          }
          
          public IData bindOutputValue(Object outputValue) {
              try {
                  if (isTypeEnum()) {
                      outputValue = ((Enum<?>)outputValue).name();
                  }
                  return getConstructor().newInstance(outputValue);
              } catch (InstantiationException ex) {
                  throw new RuntimeException("Internal error processing outputs", ex);
              } catch (SecurityException ex) {
                  throw new RuntimeException("Internal error processing outputs", ex);
              } catch (IllegalAccessException ex) {
                  throw new RuntimeException("Internal error processing outputs", ex);
              } catch (InvocationTargetException ex) {
                  Throwable cause = ex.getCause() == null ? ex : ex.getCause();
                  throw new RuntimeException(cause.getMessage(), cause);
              }
          }
          
          public abstract IData get(Object annotatedInstance);
          
          private synchronized Constructor<? extends IData> getConstructor() {
              if (bindingConstructor == null ){
                  try {
                      Class<? extends IData> bindingClass = getDescriptor().getBinding();
                      Class<?> outputPayloadClass = bindingClass.getMethod("getPayload", (Class<?>[]) null).getReturnType();
                      Type bindingPayloadType = getPayloadType();
                      if (bindingPayloadType instanceof Class<?>) {
                          Class<?> bindingPayloadClass = (Class<?>) bindingPayloadType;
                          if (bindingPayloadClass.isAssignableFrom(outputPayloadClass)) {
                              bindingConstructor = bindingClass.getConstructor(bindingPayloadClass);
                          }
                      }
                  }  catch (NoSuchMethodException e) {
                      // error handling on fall-through
                  }
              }
              return bindingConstructor;
          }
      }
  
      public static class InputFieldBinding<D extends InputDescriptor> extends InputBinding<Field, D> {
  
          public InputFieldBinding(Field field) {
              super(field);
          }
  
          @Override
          public Type getMemberType() {
              return getMember().getGenericType();
          }
          
          @Override
          public boolean validate() {
              if (!checkModifier()) {
                  LOGGER.error("Field {} with input annotation can't be used, not public.", getMember());
                  return false;
              }
              if (!(getDescriptor().getMaxOccurs().intValue() < 2 || isMemberTypeList())) {
                  LOGGER.error("Field {} with input annotation can't be used, maxOccurs > 1 and field is not of type List", getMember());
                  return false;
              }
              if (!checkType()) {
                  LOGGER.error("Field {} with input annotation can't be used, unable to safely assign field using binding payload type", getMember());
                  return false;
              }
              return true;
          }
          
          @Override
          public void set(Object annotatedObject, List<IData> boundInputList) {
              try {
                  getMember().set(annotatedObject, unbindInput(boundInputList));
              } catch (IllegalArgumentException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              } catch (IllegalAccessException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              }
          }
      }
  
      public static class InputMethodBinding<D extends InputDescriptor> extends InputBinding<Method, D> {
  
          public InputMethodBinding(Method method) {
              super(method);
          }
  
          @Override
          public Type getMemberType() {
              Type[] genericParameterTypes = getMember().getGenericParameterTypes();
              return (genericParameterTypes.length == 0) ? Void.class : genericParameterTypes[0];
          }
          
          @Override
          public boolean validate() {
              if (!checkModifier()) {
                  LOGGER.error("Field {} with input annotation can't be used, not public.", getMember());
                  return false;
              }
              if (!(getDescriptor().getMaxOccurs().intValue() < 2 || isMemberTypeList())) {
                  LOGGER.error("Field {} with input annotation can't be used, maxOccurs > 1 and field is not of type List", getMember());
                  return false;
              }
              if (!checkType()) {
                  LOGGER.error("Field {} with input annotation can't be used, unable to safely assign field using binding payload type", getMember());
                  return false;
              }
              return true;
          }
          
          @Override
          public void set(Object annotatedObject, List<IData> boundInputList) {
              try {
                  getMember().invoke(annotatedObject, unbindInput(boundInputList));
              } catch (IllegalAccessException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              } catch (IllegalArgumentException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              } catch (InvocationTargetException ex) {
                  Throwable cause = ex.getCause() == null ? ex : ex.getCause();
                  throw new RuntimeException(cause.getMessage(), cause);
              }
          }
      }
  
      public static class OutputFieldBinding<D extends OutputDescriptor> extends OutputBinding<Field, D> {
  
          public OutputFieldBinding(Field field) {
              super(field);
          }
  
          @Override
          public Type getMemberType() {
              return getMember().getGenericType();
          }
          
          @Override
          public boolean validate() {
              if (!checkModifier()) {
                  LOGGER.error("Field {} with output annotation can't be used, not public.", getMember());
                  return false;
              }
              if (!checkType()) {
                  LOGGER.error("Field {} with output annotation can't be used, unable to safely construct binding using field type", getMember());
                  return false;
              }
              return true;
          }
          
          @Override
          public IData get(Object annotatedInstance) {
              Object value;
              try {
                  value = getMember().get(annotatedInstance);
              } catch (IllegalArgumentException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              } catch (IllegalAccessException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              }
              return value == null ? null : bindOutputValue(value);
          }
      }
  
      public static class OutputMethodBinding<D extends OutputDescriptor> extends OutputBinding<Method, D> {
  
          public OutputMethodBinding(Method method) {
              super(method);
          }
  
          @Override
          public Type getMemberType() {
              return getMember().getGenericReturnType();
          }
          
          @Override
          public boolean validate() {
              Method method = getMember();
              if (method.getParameterTypes().length != 0) {
                  LOGGER.error("Method {} with output annotation can't be used, parameter count != 0", getMember());
                  return false;
              }
              if (!checkModifier()) {
                  LOGGER.error("Method {} with output annotation can't be used, not public", getMember());
                  return false;
              }
              if (!checkType()) {
                  LOGGER.error("Method {} with output annotation can't be used, unable to safely construct binding using method return type", getMember());
                  return false;
              }
              return true;
          }
          
          @Override
          public IData get(Object annotatedInstance) {
              Object value;
              try {
                  value = getMember().invoke(annotatedInstance);
              } catch (IllegalAccessException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              } catch (IllegalArgumentException ex) {
                  throw new RuntimeException("Internal error processing inputs", ex);
              } catch (InvocationTargetException ex) {
                  Throwable cause = ex.getCause() == null ? ex : ex.getCause();
                  throw new RuntimeException(cause.getMessage(), cause);
              }
              return value == null ? null : bindOutputValue(value);
          }
      }
      
      // for example, a type reprecenting the <? extends Object> for types of List<? extends Object> or List
      public final Type NOT_PARAMETERIZED_TYPE = new WildcardType() {
          @Override public Type[] getUpperBounds() { return new Type[]{Object.class}; }
          @Override public Type[] getLowerBounds() { return new Type[0]; }  
      };
  }
  