content: /**
   * Copyright (c) 2013, Andre Steingress
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
   * following conditions are met:
   *
   * 1.) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
   * disclaimer.
   * 2.) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
   * disclaimer in the documentation and/or other materials provided with the distribution.
   * 3.) Neither the name of Andre Steingress nor the names of its contributors may be used to endorse or
   * promote products derived from this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
   * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  package org.gcontracts.ast.visitor;
  
  import org.codehaus.groovy.GroovyBugError;
  import org.codehaus.groovy.ast.*;
  import org.codehaus.groovy.ast.expr.*;
  import org.codehaus.groovy.ast.stmt.BlockStatement;
  import org.codehaus.groovy.control.SourceUnit;
  import org.codehaus.groovy.control.io.ReaderSource;
  import org.gcontracts.annotations.meta.ContractElement;
  import org.gcontracts.common.spi.AnnotationProcessor;
  import org.gcontracts.common.spi.ProcessingContextInformation;
  import org.gcontracts.generation.CandidateChecks;
  import org.gcontracts.util.AnnotationUtils;
  import org.gcontracts.util.Validate;
  
  import java.lang.reflect.Modifier;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  
  /**
   * Visits annotations of meta-type {@link ContractElement} and applies the AST transformations of the underlying
   * {@link org.gcontracts.common.spi.AnnotationProcessor} implementation.
   *
   * @see org.gcontracts.common.spi.AnnotationProcessor
   *
   * @author ast
   */
  public class AnnotationProcessorVisitor extends BaseVisitor {
  
      private ProcessingContextInformation pci;
  
      public AnnotationProcessorVisitor(final SourceUnit sourceUnit, final ReaderSource source, final ProcessingContextInformation pci) {
          super(sourceUnit, source);
          Validate.notNull(pci);
  
          this.pci = pci;
      }
  
      @Override
      public void visitClass(ClassNode type) {
          handleClassNode(type);
  
          List<MethodNode> methodNodes = new ArrayList<MethodNode>();
          methodNodes.addAll(type.getMethods());
          methodNodes.addAll(type.getDeclaredConstructors());
  
          for (MethodNode methodNode : methodNodes)  {
              if (!CandidateChecks.isClassInvariantCandidate(type, methodNode) && !CandidateChecks.isPreOrPostconditionCandidate(type, methodNode)) continue;
  
              handleMethodNode(methodNode, AnnotationUtils.hasMetaAnnotations(methodNode, ContractElement.class.getName()));
          }
  
          // visit all interfaces of this class
          visitInterfaces(type, type.getInterfaces());
          visitAbstractBaseClassesForInterfaceMethodNodes(type, type.getSuperClass());
      }
  
      private void visitAbstractBaseClassesForInterfaceMethodNodes(ClassNode origin, ClassNode superClass) {
          if (superClass == null) return;
          if (!Modifier.isAbstract(superClass.getModifiers())) return;
  
          for (ClassNode interfaceClassNode : superClass.getInterfaces())  {
              List<MethodNode> methodNodes = new ArrayList<MethodNode>();
              methodNodes.addAll(interfaceClassNode.getMethods());
  
              for (MethodNode interfaceMethodNode : methodNodes)  {
                  final List<AnnotationNode> annotationNodes = AnnotationUtils.hasMetaAnnotations(interfaceMethodNode, ContractElement.class.getName());
                  if (annotationNodes == null || annotationNodes.isEmpty()) continue;
  
                  MethodNode implementingMethodNode = superClass.getMethod(interfaceMethodNode.getName(), interfaceMethodNode.getParameters());
  
                  // if implementingMethodNode == null, then superClass is abstract and does not implement
                  // the current interface methodNode
                  if (implementingMethodNode != null) continue;
  
                  MethodNode implementationInOriginClassNode = origin.getMethod(interfaceMethodNode.getName(), interfaceMethodNode.getParameters());
                  if (implementationInOriginClassNode == null) continue;
  
                  handleMethodNode(implementationInOriginClassNode, annotationNodes);
              }
          }
      }
  
      private void visitInterfaces(final ClassNode classNode, final ClassNode[] interfaces) {
          for (ClassNode interfaceClassNode : interfaces)  {
              List<MethodNode> methodNodes = new ArrayList<MethodNode>();
              methodNodes.addAll(interfaceClassNode.getMethods());
  
              // @ContractElement annotations are by now only supported on method interfaces
              for (MethodNode interfaceMethodNode : methodNodes)  {
                  MethodNode implementingMethodNode = classNode.getMethod(interfaceMethodNode.getName(), interfaceMethodNode.getParameters());
                  if (implementingMethodNode == null) continue;
  
                  final List<AnnotationNode> annotationNodes = AnnotationUtils.hasMetaAnnotations(interfaceMethodNode, ContractElement.class.getName());
                  handleInterfaceMethodNode(classNode, implementingMethodNode, annotationNodes);
              }
  
              visitInterfaces(classNode, interfaceClassNode.getInterfaces());
          }
      }
  
      private void handleClassNode(final ClassNode classNode)  {
          final List<AnnotationNode> annotationNodes = AnnotationUtils.hasMetaAnnotations(classNode, ContractElement.class.getName());
  
          for (AnnotationNode annotationNode : annotationNodes)  {
              final AnnotationProcessor annotationProcessor = createAnnotationProcessor(annotationNode);
  
              if (annotationProcessor != null && annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME) instanceof ClassExpression)  {
                  final ClassExpression closureClassExpression = (ClassExpression) annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME);
  
                  MethodCallExpression doCall = new MethodCallExpression(
                          new ConstructorCallExpression(closureClassExpression.getType(), new ArgumentListExpression(VariableExpression.THIS_EXPRESSION, VariableExpression.THIS_EXPRESSION)),
                          "doCall",
                          ArgumentListExpression.EMPTY_ARGUMENTS
                  );
                  doCall.setMethodTarget(closureClassExpression.getType().getMethods("doCall").get(0));
  
                  final BooleanExpression booleanExpression = new BooleanExpression(doCall);
                  booleanExpression.setSourcePosition(annotationNode);
  
                  annotationProcessor.process(pci, pci.contract(), classNode, (BlockStatement) closureClassExpression.getNodeMetaData(AnnotationClosureVisitor.META_DATA_ORIGINAL_TRY_CATCH_BLOCK), booleanExpression);
              }
          }
      }
  
      private void handleInterfaceMethodNode(ClassNode type, MethodNode methodNode, List<AnnotationNode> annotationNodes) {
          handleMethodNode(type.getMethod(methodNode.getName(), methodNode.getParameters()), annotationNodes);
      }
  
      private void handleMethodNode(MethodNode methodNode, List<AnnotationNode> annotationNodes) {
          if (methodNode == null) return;
  
          for (AnnotationNode annotationNode : annotationNodes)  {
              final AnnotationProcessor annotationProcessor = createAnnotationProcessor(annotationNode);
  
              if (annotationProcessor != null && annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME) instanceof ClassExpression)  {
                  boolean isPostcondition = AnnotationUtils.hasAnnotationOfType(annotationNode.getClassNode(), org.gcontracts.annotations.meta.Postcondition.class.getName());
  
                  ClassExpression closureClassExpression = (ClassExpression) annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME);
  
                  ArgumentListExpression closureArgumentList = new ArgumentListExpression();
  
                  for (Parameter parameter : methodNode.getParameters())  {
                      closureArgumentList.addExpression(new VariableExpression(parameter));
                  }
  
                  if (methodNode.getReturnType() != ClassHelper.VOID_TYPE && isPostcondition && !(methodNode instanceof ConstructorNode))  {
                      VariableExpression variableExpression = new VariableExpression("result", methodNode.getReturnType());
                      variableExpression.setAccessedVariable(variableExpression);
  
                      closureArgumentList.addExpression(variableExpression);
                  }
  
                  if (isPostcondition && !(methodNode instanceof ConstructorNode)) {
                      VariableExpression variableExpression = new VariableExpression("old", new ClassNode(Map.class));
                      variableExpression.setAccessedVariable(variableExpression);
  
                      closureArgumentList.addExpression(variableExpression);
                  }
  
                  MethodCallExpression doCall = new MethodCallExpression(
                          new ConstructorCallExpression(annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME).getType(), new ArgumentListExpression(VariableExpression.THIS_EXPRESSION, VariableExpression.THIS_EXPRESSION)),
                          "doCall",
                          closureArgumentList
                  );
                  ClassNode type = annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME).getType();
                  doCall.setMethodTarget(type.getMethods("doCall").get(0));
  
                  final BooleanExpression booleanExpression = new BooleanExpression(doCall);
                  booleanExpression.setSourcePosition(annotationNode);
  
                  annotationProcessor.process(pci, pci.contract(), methodNode.getDeclaringClass(), methodNode, (BlockStatement) closureClassExpression.getNodeMetaData(AnnotationClosureVisitor.META_DATA_ORIGINAL_TRY_CATCH_BLOCK), booleanExpression);
  
                  // if the implementation method has no annotation, we need to set a dummy marker in order to find parent pre/postconditions
                  if (!AnnotationUtils.hasAnnotationOfType(methodNode, annotationNode.getClassNode().getName()))  {
                      AnnotationNode annotationMarker = new AnnotationNode(annotationNode.getClassNode());
                      annotationMarker.setMember(CLOSURE_ATTRIBUTE_NAME, annotationNode.getMember(CLOSURE_ATTRIBUTE_NAME));
                      annotationMarker.setRuntimeRetention(true);
                      annotationMarker.setSourceRetention(false);
  
                      methodNode.addAnnotation(annotationMarker);
                  }
              }
          }
      }
  
      private AnnotationProcessor createAnnotationProcessor(AnnotationNode annotationNode) {
          ClassExpression annotationProcessingAnno = null;
  
          List<AnnotationNode> annotations = annotationNode.getClassNode().redirect().getAnnotations();
          for (AnnotationNode anno : annotations)  {
              Class typeClass = anno.getClassNode().getTypeClass();
  
              if (typeClass.getName().equals("org.gcontracts.annotations.meta.AnnotationProcessorImplementation"))  {
                  annotationProcessingAnno = (ClassExpression) anno.getMember("value");
                  break;
              }
          }
  
          if (annotationProcessingAnno == null) throw new GroovyBugError("Annotation processing class could not be found! This indicates a bug in GContracts, please file an issue!");
  
          try {
              final Class clz = Class.forName(annotationProcessingAnno.getType().getTypeClass().getName());
              return (AnnotationProcessor) clz.newInstance();
          } catch (InstantiationException e) {
          } catch (IllegalAccessException e) {
          } catch (ClassNotFoundException e) {}
  
          throw new GroovyBugError("Annotation processing class could not be instantiated! This indicates a bug in GContracts, please file an issue!");
      }
  }
  