HeaderParser.java

/*
 * Copyright (C) 2008-2022 Mycila (mathieu.carbou@gmail.com)
 *
 * 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
 *
 *         https://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 com.mycila.maven.plugin.license.header;

import com.mycila.maven.plugin.license.util.FileContent;
import com.mycila.maven.plugin.license.util.StringUtils;

/**
 * The <code>HeaderParser</code> class is used to get header information about the current header defined in the given
 * file. The achieve this it will use the <code>HeaderDefinition</code> associated to the type of the given file.
 * <p>
 * Important: is considered a license header a header which contains the word <em>copyright</em> (case insensitive)
 * within a section of the file which match the given <code>HeaderDefinition</code> associated to this
 * <code>HeaderParser</code>.
 *
 * @author Mathieu Carbou (mathieu.carbou@gmail.com)
 * @see com.mycila.maven.plugin.license.header.HeaderDefinition
 */
public final class HeaderParser {

  private final int beginPosition;
  private final int endPosition;
  private final boolean existingHeader;
  private final FileContent fileContent;
  private final String[] keywords;
  private HeaderDefinition headerDefinition;

  private String line;

  /**
   * Creates a <code>HeaderParser</code> object linked to the given file content and the associated header definition
   * based on the file type.
   *
   * @param fileContent      The file content.
   * @param headerDefinition The associated header definition to use.
   * @throws IllegalArgumentException If the file content is null or if the header definition is null.
   */
  public HeaderParser(FileContent fileContent, HeaderDefinition headerDefinition, String[] keywords) {
    if (fileContent == null) {
      throw new IllegalArgumentException("Cannot create a header parser for null file content");
    }
    if (headerDefinition == null) {
      throw new IllegalArgumentException("Cannot work on file header if the header definition is null");
    }
    this.keywords = keywords.clone();
    this.headerDefinition = headerDefinition;
    this.fileContent = fileContent;
    beginPosition = findBeginPosition();
    existingHeader = hasHeader();
    endPosition = existingHeader ? findEndPosition() : -1;
  }

  /**
   * Returns the index position in the content where the header effectively starts.
   *
   * @return The index in the content.
   */
  public int getBeginPosition() {
    return beginPosition;
  }

  /**
   * Returns the index position in the content where the header effectively ends.
   *
   * @return The index in the content.
   */
  public int getEndPosition() {
    return endPosition;
  }

  /**
   * Tells if the given file already contains a license header.
   *
   * @return true if a license header has been detect or false.
   */
  public boolean gotAnyHeader() {
    return existingHeader;
  }

  /**
   * Returns the file content.
   *
   * @return The content.
   */
  public FileContent getFileContent() {
    return fileContent;
  }

  /**
   * Returns the header definition associated to this header parser (itself bounded to a file).
   *
   * @return The associated header definition.
   */
  public HeaderDefinition getHeaderDefinition() {
    return headerDefinition;
  }

  private int findBeginPosition() {
    int beginPos = 0;
    line = fileContent.nextLine();
    if (headerDefinition.getSkipLinePattern() == null) {
      return beginPos;
    }

    // the format expect to find lines to be skipped
    while (line != null && !headerDefinition.isSkipLine(line)) {
      beginPos = fileContent.getPosition();
      line = fileContent.nextLine();
    }

    // at least we have found the line to skip or we are the end of the file
    // this time we are going to skip next lines if they match the skip pattern
    while (line != null && headerDefinition.isSkipLine(line)) {
      beginPos = fileContent.getPosition();
      line = fileContent.nextLine();
    }

    if (line == null) {
      // After skipping everything we are at the end of the file
      // Header has to be at the file beginning
      beginPos = 0;
      fileContent.reset();
      line = fileContent.nextLine();
    }

    return beginPos;
  }

  private boolean hasHeader() {
    // skip blank lines
    while (line != null && "".equals(line.trim())) {
      line = fileContent.nextLine();
    }
    // check if there is already a header
    boolean gotHeader = false;
    if (headerDefinition.isFirstHeaderLine(line)) {
      StringBuilder inPlaceHeader = new StringBuilder();
      inPlaceHeader.append(line.toLowerCase());

      line = fileContent.nextLine();

      // skip blank lines before header text
      if (headerDefinition.allowBlankLines()) {
        while (line != null && "".equals(line.trim())) {
          line = fileContent.nextLine();
        }
      }

      // first header detected line & potential blank lines have been detected
      // following lines should be header lines
      if (line == null) {
        // we detected previously a one line comment block that matches the header detection
        // it is not an header it is a comment
        return false;

      } else {
        inPlaceHeader.append(line.toLowerCase());
      }

      String before = StringUtils.rtrim(headerDefinition.getBeforeEachLine());
      if ("".equals(before) && !headerDefinition.isMultiLine()) {
        before = headerDefinition.getBeforeEachLine();
      }

      boolean foundEnd = false;
      if (headerDefinition.isMultiLine() && headerDefinition.isLastHeaderLine(line)) {
        foundEnd = true;

      } else {
        while ((line = fileContent.nextLine()) != null && line.startsWith(before)) {
          inPlaceHeader.append(line.toLowerCase());
          if (headerDefinition.isMultiLine() && headerDefinition.isLastHeaderLine(line)) {
            foundEnd = true;
            break;
          }
        }
      }

      // skip blank lines after header text
      if (headerDefinition.isMultiLine() && headerDefinition.allowBlankLines() && !foundEnd) {
        do {
          line = fileContent.nextLine();
        } while (line != null && "".equals(line.trim()));
        fileContent.rewind();

      } else if (!headerDefinition.isMultiLine() && !foundEnd) {
        fileContent.rewind();
      }

      if (!headerDefinition.isMultiLine()) {
        // keep track of the position for headers where the end line is the same as the before each line
        int pos = fileContent.getPosition();
        // check if the line is the end line
        while (line != null
            && !headerDefinition.isLastHeaderLine(line)
            && (headerDefinition.allowBlankLines() || !"".equals(line.trim()))
            && line.startsWith(before)) {
          line = fileContent.nextLine();
        }
        if (line == null) {
          fileContent.resetTo(pos);
        }
      } else if (line != null) {
        // we could end up there if we still have some lines, but not matching "before".
        // This can be the last line in a multi line header
        int pos = fileContent.getPosition();
        line = fileContent.nextLine();
        if (line == null || !headerDefinition.isLastHeaderLine(line)) {
          fileContent.resetTo(pos);
        }
      }
      gotHeader = true;
      for (String keyword : keywords) {
        if (inPlaceHeader.indexOf(keyword.toLowerCase()) == -1) {
          gotHeader = false;
          break;
        }
      }
    }
    return gotHeader;
  }

  private int findEndPosition() {
    // we check if there is a header, if the next line is the blank line of the header
    int end = fileContent.getPosition();
    line = fileContent.nextLine();
    if (beginPosition == 0) {
      while (line != null && "".equals(line.trim())) {
        end = fileContent.getPosition();
        line = fileContent.nextLine();
      }
    }
    if (headerDefinition.getEndLine().endsWith("EOL") && line != null && "".equals(line.trim())) {
      end = fileContent.getPosition();
    }
    return end;
  }
}