/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/** Task script that is called by Ant's build.xml file:
 * Checks that there are no @author javadoc tags, tabs, 
 * svn keywords, javadoc-style licenses, or nocommits.
 */

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.rat.Defaults;
import org.apache.rat.document.impl.FileDocument;
import org.apache.rat.api.MetaData;

def extensions = [
  'java', 'jflex', 'py', 'pl', 'g4', 'jj', 'html', 'js',
  'css', 'xml', 'xsl', 'vm', 'sh', 'cmd', 'bat', 'policy',
  'properties', 'mdtext', 'groovy',
  'template', 'adoc', 'json', 'md'
];
def invalidPatterns = [
  (~$/@author\b/$) : '@author javadoc tag',
  (~$/(?i)\bno(n|)commit\b/$) : 'nocommit',
  (~$/\bTOOD:/$) : 'TOOD instead TODO',
  (~$/\t/$) : 'tabs instead spaces',
  (~$/\Q/**\E((?:\s)|(?:\*))*\Q{@inheritDoc}\E((?:\s)|(?:\*))*\Q*/\E/$) : '{@inheritDoc} on its own is unnecessary',
  (~$/\$$(?:LastChanged)?Date\b/$) : 'svn keyword',
  (~$/\$$(?:(?:LastChanged)?Revision|Rev)\b/$) : 'svn keyword',
  (~$/\$$(?:LastChangedBy|Author)\b/$) : 'svn keyword',
  (~$/\$$(?:Head)?URL\b/$) : 'svn keyword',
  (~$/\$$Id\b/$) : 'svn keyword',
  (~$/\$$Header\b/$) : 'svn keyword',
  (~$/\$$Source\b/$) : 'svn keyword',
  (~$/^\uFEFF/$) : 'UTF-8 byte order mark',
  (~$/import java\.lang\.\w+;/$) : 'java.lang import is unnecessary'
]

// Python and others merrily use var declarations, this is a problem _only_ in Java at least for 8x where we're forbidding var declarations
def invalidJavaOnlyPatterns = [
  (~$/\n\s*var\s+.*=.*<>.*/$) : 'Diamond operators should not be used with var',
  (~$/\n\s*var\s+/$) : 'var is not allowed in until we stop development on the 8x code line'
]


def baseDir = properties['basedir'];
def baseDirLen = baseDir.length() + 1;

def found = 0;
def violations = new TreeSet();
def reportViolation = { f, name ->
  task.log(name + ': ' + f.toString().substring(baseDirLen).replace(File.separatorChar, (char)'/'), Project.MSG_ERR);
  violations.add(name);
  found++;
}

def javadocsPattern = ~$/(?sm)^\Q/**\E(.*?)\Q*/\E/$;
def javaCommentPattern = ~$/(?sm)^\Q/*\E(.*?)\Q*/\E/$;
def xmlCommentPattern = ~$/(?sm)\Q<!--\E(.*?)\Q-->\E/$;
def lineSplitter = ~$/[\r\n]+/$;
def singleLineSplitter = ~$/\r?\n/$;
def licenseMatcher = Defaults.createDefaultMatcher();
def validLoggerPattern = ~$/(?s)\b(private\s|static\s|final\s){3}+\s*Logger\s+\p{javaJavaIdentifierStart}+\s+=\s+\QLoggerFactory.getLogger(MethodHandles.lookup().lookupClass());\E/$;
def validLoggerNamePattern = ~$/(?s)\b(private\s|static\s|final\s){3}+\s*Logger\s+log+\s+=\s+\QLoggerFactory.getLogger(MethodHandles.lookup().lookupClass());\E/$;
def packagePattern = ~$/(?m)^\s*package\s+org\.apache.*;/$;
def xmlTagPattern = ~$/(?m)\s*<[a-zA-Z].*/$;
def sourceHeaderPattern = ~$/\[source\b.*/$;
def blockBoundaryPattern = ~$/----\s*/$;
def blockTitlePattern = ~$/\..*/$;
def unescapedSymbolPattern = ~$/(?<=[^\\]|^)([-=]>|<[-=])/$; // SOLR-10883
def extendsLuceneTestCasePattern = ~$/public.*?class.*?extends.*?LuceneTestCase[^\n]*?\n/$;
def validSPINameJavadocTag = ~$/(?s)\s*\*\s*@lucene\.spi\s+\{@value #NAME\}/$;

def isLicense = { matcher, ratDocument ->
  licenseMatcher.reset();
  return lineSplitter.split(matcher.group(1)).any{ licenseMatcher.match(ratDocument, it) };
}

def checkLicenseHeaderPrecedes = { f, description, contentPattern, commentPattern, text, ratDocument ->
  def contentMatcher = contentPattern.matcher(text);
  if (contentMatcher.find()) {
    def contentStartPos = contentMatcher.start();
    def commentMatcher = commentPattern.matcher(text);
    while (commentMatcher.find()) {
      if (isLicense(commentMatcher, ratDocument)) {
        if (commentMatcher.start() < contentStartPos) {
          break; // This file is all good, so break loop: license header precedes 'description' definition
        } else {
          reportViolation(f, description+' declaration precedes license header');
        }
      }
    }
  }
}

def checkMockitoAssume = { f, text ->
  if (text.contains("mockito") && !text.contains("assumeWorkingMockito()")) {
    reportViolation(f, 'File uses Mockito but has no assumeWorkingMockito() call');
  }
}

def checkForUnescapedSymbolSubstitutions = { f, text ->
  def inCodeBlock = false;
  def underSourceHeader = false;
  def lineNumber = 0;
  singleLineSplitter.split(text).each {
    ++lineNumber;
    if (underSourceHeader) { // This line is either a single source line, or the boundary of a code block
      inCodeBlock = blockBoundaryPattern.matcher(it).matches();
      if ( ! blockTitlePattern.matcher(it).matches()) {
        underSourceHeader = false;
      }
    } else {
      if (inCodeBlock) {
        inCodeBlock = ! blockBoundaryPattern.matcher(it).matches();
      } else {
        underSourceHeader = sourceHeaderPattern.matcher(it).lookingAt();
        if ( ! underSourceHeader) {
          def unescapedSymbolMatcher = unescapedSymbolPattern.matcher(it);
          if (unescapedSymbolMatcher.find()) {
            reportViolation(f, 'Unescaped symbol "' + unescapedSymbolMatcher.group(1) + '" on line #' + lineNumber);
          }
        }
      }
    }
  }
}

ant.fileScanner{
  fileset(dir: baseDir){
    extensions.each{
      include(name: 'lucene/**/*.' + it)
      include(name: 'solr/**/*.' + it)
      include(name: 'dev-tools/**/*.' + it)
      include(name: '*.' + it)
    }
    // TODO: For now we don't scan txt files, so we
    // check licenses in top-level folders separately:
    include(name: '*.txt')
    include(name: '*/*.txt')
    // excludes:
    exclude(name: '**/build/**')
    exclude(name: '**/dist/**')
    exclude(name: 'lucene/benchmark/work/**')
    exclude(name: 'lucene/benchmark/temp/**')
    exclude(name: '**/CheckLoggingConfiguration.java')
    exclude(name: 'lucene/tools/src/groovy/check-source-patterns.groovy') // ourselves :-)
    exclude(name: 'solr/core/src/test/org/apache/hadoop/**')
  }
}.each{ f ->
  task.log('Scanning file: ' + f, Project.MSG_VERBOSE);
  def text = f.getText('UTF-8');
  invalidPatterns.each{ pattern,name ->
    if (pattern.matcher(text).find()) {
      reportViolation(f, name);
    }
  }
  def javadocsMatcher = javadocsPattern.matcher(text);
  def ratDocument = new FileDocument(f);
  while (javadocsMatcher.find()) {
    if (isLicense(javadocsMatcher, ratDocument)) {
      reportViolation(f, String.format(Locale.ENGLISH, 'javadoc-style license header [%s]',
        ratDocument.getMetaData().value(MetaData.RAT_URL_LICENSE_FAMILY_NAME)));
    }
  }
  if (f.name.endsWith('.java')) {
    if (text.contains('org.slf4j.LoggerFactory')) {
      if (!validLoggerPattern.matcher(text).find()) {
        reportViolation(f, 'invalid logging pattern [not private static final, uses static class name]');
      }
      if (!validLoggerNamePattern.matcher(text).find()) {
        reportViolation(f, 'invalid logger name [log, uses static class name, not specialized logger]')
      }
    }
    // make sure that SPI names of all tokenizers/charfilters/tokenfilters are documented
    if (!f.name.contains("Test") && !f.name.contains("Mock") && !text.contains("abstract class") &&
        !f.name.equals("TokenizerFactory.java") && !f.name.equals("CharFilterFactory.java") && !f.name.equals("TokenFilterFactory.java") &&
        (f.name.contains("TokenizerFactory") && text.contains("extends TokenizerFactory") ||
            f.name.contains("CharFilterFactory") && text.contains("extends CharFilterFactory") ||
            f.name.contains("FilterFactory") && text.contains("extends TokenFilterFactory"))) {
      if (!validSPINameJavadocTag.matcher(text).find()) {
        reportViolation(f, 'invalid spi name documentation')
      }
    }
    checkLicenseHeaderPrecedes(f, 'package', packagePattern, javaCommentPattern, text, ratDocument);
    if (f.name.contains("Test")) {
      checkMockitoAssume(f, text);
    }

    if (f.path.substring(baseDirLen).contains("solr/")
        && f.name.equals("SolrTestCase.java") == false
        && f.name.equals("TestXmlQParser.java") == false) {
      if (extendsLuceneTestCasePattern.matcher(text).find()) {
        reportViolation(f, "Solr test cases should extend SolrTestCase rather than LuceneTestCase");
      }
    }
    invalidJavaOnlyPatterns.each { pattern,name ->
      if (pattern.matcher(text).find()) {
        reportViolation(f, name);
      }
    }
  }
  if (f.name.endsWith('.xml') || f.name.endsWith('.xml.template')) {
    checkLicenseHeaderPrecedes(f, '<tag>', xmlTagPattern, xmlCommentPattern, text, ratDocument);
  }
  if (f.name.endsWith('.adoc')) {
    checkForUnescapedSymbolSubstitutions(f, text);
  }
};

if (found) {
  throw new BuildException(String.format(Locale.ENGLISH, 'Found %d violations in source files (%s).',
    found, violations.join(', ')));
}
