Header.java

  1. /*
  2.  * Copyright (C) 2008-2023 Mycila (mathieu.carbou@gmail.com)
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License").
  5.  * You may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *         https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. package com.mycila.maven.plugin.license.header;

  17. import com.mycila.maven.plugin.license.HeaderSection;
  18. import com.mycila.maven.plugin.license.document.Document;
  19. import com.mycila.maven.plugin.license.util.StringUtils;

  20. import java.io.IOException;
  21. import java.util.ArrayList;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.SortedMap;
  25. import java.util.TreeMap;

  26. import static com.mycila.maven.plugin.license.util.FileUtils.readFirstLines;
  27. import static com.mycila.maven.plugin.license.util.FileUtils.remove;

  28. /**
  29.  * The <code>Header</code> class wraps the license template file, the one which have to be outputted inside the other
  30.  * files.
  31.  */
  32. public final class Header {
  33.   private final HeaderSource location;
  34.   private final String headerContent;
  35.   private final String headerContentOneLine;
  36.   private String[] lines;
  37.   private final HeaderSection[] sections;
  38.   private final int maxLength;

  39.   /**
  40.    * Constructs a <code>Header</code> object pointing to a license template file. In case of the template contains
  41.    * replaceable values (declared as ${<em>valuename</em>}), you can set the map of this values.
  42.    *
  43.    * @param location The license template file location.
  44.    * @param sections Any applicable header sections for this header
  45.    * @throws IllegalArgumentException If the header file location is null or if an error occurred while reading the
  46.    *                                  file content.
  47.    */
  48.   public Header(HeaderSource location, HeaderSection[] sections) {
  49.     this.location = location;
  50.     this.sections = sections;
  51.     try {
  52.       this.headerContent = location.getContent();
  53.       lines = headerContent.replace("\r", "").split("\n");
  54.       headerContentOneLine = remove(headerContent, " ", "\t", "\r", "\n");
  55.     } catch (Exception e) {
  56.       throw new IllegalArgumentException("Cannot read header document " + location + ". Cause: " + e.getMessage(), e);
  57.     }

  58.     int maxLength = 0;
  59.     for (String line : lines) {
  60.       if (line.length() > maxLength) {
  61.         maxLength = line.length();
  62.       }
  63.     }

  64.     this.maxLength = maxLength;
  65.   }

  66.   public String asString() {
  67.     return headerContent;
  68.   }

  69.   public String asOneLineString() {
  70.     return headerContentOneLine;
  71.   }

  72.   public int getLineCount() {
  73.     return lines.length;
  74.   }

  75.   public int getMaxLineLength() {
  76.     return maxLength;
  77.   }

  78.   /**
  79.    * Returns the location of license template file.
  80.    *
  81.    * @return The URL location.
  82.    */
  83.   public HeaderSource getLocation() {
  84.     return location;
  85.   }

  86.   public String eol(boolean unix) {
  87.     return unix ? "\n" : "\r\n";
  88.   }

  89.   public String buildForDefinition(HeaderDefinition type, boolean unix) {
  90.     StringBuilder newHeader = new StringBuilder();
  91.     String unixEndOfLine = eol(unix);
  92.     if (notEmpty(type.getFirstLine())) {
  93.       String firstLine = type.getFirstLine().replace("EOL", unixEndOfLine);
  94.       newHeader.append(firstLine);
  95.       if (!firstLine.equals(unixEndOfLine)) {
  96.         newHeader.append(unixEndOfLine);
  97.       }
  98.     }
  99.     for (String line : getLines()) {
  100.       final String before = type.getBeforeEachLine().replace("EOL", unixEndOfLine);
  101.       final String after = type.getAfterEachLine().replace("EOL", unixEndOfLine);
  102.       final String str;

  103.       if (type.isPadLines()) {
  104.         str = before + StringUtils.padRight(line, maxLength) + after;
  105.       } else {
  106.         str = before + line + after;
  107.       }

  108.       newHeader.append(StringUtils.rtrim(str));
  109.       newHeader.append(unixEndOfLine);
  110.     }
  111.     if (notEmpty(type.getEndLine())) {
  112.       String endLine = type.getEndLine().replace("EOL", unixEndOfLine);
  113.       newHeader.append(endLine);
  114.       if (!endLine.equals(unixEndOfLine)) {
  115.         newHeader.append(unixEndOfLine);
  116.       }
  117.     }
  118.     return newHeader.toString();
  119.   }

  120.   @Override
  121.   public String toString() {
  122.     return asString();
  123.   }

  124.   public String[] getLines() {
  125.     return lines;
  126.   }

  127.   /**
  128.    * Determines if a potential file header (typically, the start of the file
  129.    * plus some buffer space) matches this header, as rendered with the
  130.    * specified {@link HeaderDefinition} and line-ending.
  131.    *
  132.    * @param potentialFileHeader the potential file header, usually with some extra buffer
  133.    *                            lines
  134.    * @param headerDefinition    the header definition to render the header with
  135.    * @param unix                if true, unix line-endings will be used
  136.    * @return true if the header is matched
  137.    */
  138.   public boolean isMatchForText(String potentialFileHeader, HeaderDefinition headerDefinition, boolean unix) {
  139.     String expected = buildForDefinition(headerDefinition, unix);
  140.     return isMatchForText(expected, potentialFileHeader, headerDefinition, unix);
  141.   }

  142.   public boolean isMatchForText(String expected, String potentialFileHeader, HeaderDefinition headerDefinition, boolean unix) {

  143.     SortedMap<Integer, HeaderSection> sectionsByIndex = computeSectionsByIndex(expected);

  144.     if (sectionsByIndex.isEmpty()) {
  145.       return potentialFileHeader.contains(expected);
  146.     }

  147.     List<String> textBetweenSections = buildExpectedTextBetweenSections(expected, sectionsByIndex);
  148.     List<HeaderSection> sectionsInOrder = new ArrayList<>(sectionsByIndex.values());
  149.     return recursivelyFindMatch(potentialFileHeader, headerDefinition, textBetweenSections, sectionsInOrder, 0, 0);
  150.   }

  151.   public boolean isMatchForText(Document d, HeaderDefinition headerDefinition, boolean unix, String encoding) throws IOException {
  152.     String fileHeader = readFirstLines(d.getFile(), getLineCount() + 10, encoding).replaceAll(" *\r?\n", "\n");
  153.     String expected = buildForDefinition(headerDefinition, unix);
  154.     expected = d.mergeProperties(expected);
  155.     return isMatchForText(expected, fileHeader, headerDefinition, unix);
  156.   }

  157.   public String applyDefinitionAndSections(HeaderDefinition headerDefinition, boolean unix) {

  158.     String expected = buildForDefinition(headerDefinition, unix);

  159.     SortedMap<Integer, HeaderSection> sectionsByIndex = computeSectionsByIndex(expected);

  160.     if (sectionsByIndex.isEmpty()) {
  161.       return expected;
  162.     }

  163.     List<String> textBetweenSections = buildExpectedTextBetweenSections(expected, sectionsByIndex);
  164.     List<HeaderSection> sectionsInOrder = new ArrayList<>(sectionsByIndex.values());

  165.     StringBuilder b = new StringBuilder();
  166.     for (int i = 0; i < textBetweenSections.size(); ++i) {
  167.       String textBetween = textBetweenSections.get(i);
  168.       b.append(textBetween);
  169.       if (i < sectionsInOrder.size()) {
  170.         HeaderSection section = sectionsInOrder.get(i);
  171.         String sectionValue = section.getDefaultValue();
  172.         if (notEmpty(sectionValue)) {
  173.           String[] tokens = sectionValue.split(eol(unix));
  174.           for (int j = 0; j < tokens.length; j++) {
  175.             if (j > 0) {
  176.               b.append(eol(unix));
  177.               if (notEmpty(headerDefinition.getBeforeEachLine())) {
  178.                 b.append(headerDefinition.getBeforeEachLine());
  179.               }
  180.               b.append(tokens[j]);
  181.               if (notEmpty(headerDefinition.getAfterEachLine())) {
  182.                 b.append(headerDefinition.getAfterEachLine());
  183.               }
  184.             } else {
  185.               b.append(tokens[j]);
  186.             }
  187.           }
  188.         }
  189.       }
  190.     }
  191.     return b.toString();
  192.   }

  193.   private boolean notEmpty(String str) {
  194.     return str != null && str.length() > 0;
  195.   }

  196.   /**
  197.    * If this Header has any {@link HeaderSection} sections defined, we look
  198.    * for each header key in the expected header text and note the position
  199.    * index of the match.
  200.    *
  201.    * @param expectedHeaderText the expected header text
  202.    * @return a sorted-map of matching HeaderSections, with the key being the
  203.    * index of section in the header text
  204.    */
  205.   private SortedMap<Integer, HeaderSection> computeSectionsByIndex(String expectedHeaderText) {

  206.     SortedMap<Integer, HeaderSection> sectionsByIndex = new TreeMap<>();

  207.     if (sections == null) {
  208.       return sectionsByIndex;
  209.     }

  210.     for (HeaderSection section : sections) {

  211.       String key = section.getKey();
  212.       int index = expectedHeaderText.indexOf(key);
  213.       if (index == -1) {
  214.         // TODO: we need some way to log that a header section key was not found...
  215.         continue;
  216.       }

  217.       /**
  218.        * Verify that the new section doesn't overlap with an existing
  219.        * section
  220.        */
  221.       int indexEnd = index + section.getKey().length();

  222.       for (Map.Entry<Integer, HeaderSection> entry : sectionsByIndex.entrySet()) {

  223.         int existingIndexStart = entry.getKey();
  224.         HeaderSection existingSection = entry.getValue();
  225.         int existingIndexEnd = existingIndexStart + existingSection.getKey().length();

  226.         if (existingIndexStart < indexEnd && index < existingIndexEnd) {
  227.           throw new IllegalArgumentException(String.format(
  228.               "Existing section '%1$s' overlaps with new section '%2$s'", existingSection.getKey(),
  229.               section.getKey()));
  230.         }

  231.         sectionsByIndex.put(index, section);
  232.       }

  233.       sectionsByIndex.put(index, section);
  234.     }

  235.     return sectionsByIndex;
  236.   }

  237.   /**
  238.    * Once we have found the set of header sections indexed in the expected
  239.    * header text, we extract out the remaining header text occurring
  240.    * in-between those header sections and return an ordered list of the
  241.    * segments.
  242.    * <p>
  243.    * As an example, if out text looked like:
  244.    * <p>
  245.    * "My name is NAME_SECTION and I work for COMPANY_SECTION most days."
  246.    * <p>
  247.    * where "NAME_SECTION" and "COMPANY_SECTION" are matched sections, the
  248.    * resulting list should look like:
  249.    * <p>
  250.    * ["My name is ", " and I work for ", " most days."]
  251.    *
  252.    * @param expectedHeaderText the expected header text
  253.    * @param sectionsByIndex    a sorted-map of matching HeaderSections, with the key being
  254.    *                           the index of section in the header text
  255.    * @return an ordered list of the text segments occurring in-between the
  256.    * sections
  257.    */
  258.   private List<String> buildExpectedTextBetweenSections(String expectedHeaderText,
  259.                                                         SortedMap<Integer, HeaderSection> sectionsByIndex) {

  260.     List<String> textBetweenSections = new ArrayList<>();
  261.     int currentIndex = 0;

  262.     for (Map.Entry<Integer, HeaderSection> entry : sectionsByIndex.entrySet()) {
  263.       int index = entry.getKey();
  264.       HeaderSection section = entry.getValue();
  265.       String textBetween = expectedHeaderText.substring(currentIndex, index);
  266.       textBetweenSections.add(textBetween);
  267.       currentIndex = index + section.getKey().length();
  268.     }

  269.     /**
  270.      * Add the tail of the expected text
  271.      */
  272.     String textBetween = expectedHeaderText.substring(currentIndex, expectedHeaderText.length());
  273.     textBetweenSections.add(textBetween);

  274.     return textBetweenSections;
  275.   }

  276.   /**
  277.    * Given a potential file header and our expected segmented header text,
  278.    * this method recursively searches through the expected segments, looking
  279.    * for possible matches.
  280.    * <p>
  281.    * We recursively search through the potential header for each of the
  282.    * expected text section, advancing our current text segment index and our
  283.    * index into the potential header text. Each step of the recursion
  284.    * considers all possible matches for a text segment, such that the
  285.    * recursion tree will eventually consider ALL valid matches. This can be
  286.    * useful when the user specifies a header like:
  287.    * <p>
  288.    * "Copyright YEAR NAME - License"
  289.    * <p>
  290.    * where "YEAR" and "NAME" are sections, meaning that we have to match a " "
  291.    * in-between, which can potentially match in multiple places if the actual
  292.    * values in the potential header contain spaces.
  293.    *
  294.    * @param potentialFileHeader             the potential file header
  295.    * @param headerDefinition                the header definition
  296.    * @param expectedTextBetweenSections     the expected text between sections
  297.    * @param sectionsInOrder                 the sections interleaved with the expected text
  298.    * @param currentTextSegmentIndex         the index of the current expected text segment to search for
  299.    * @param currentPotentialFileHeaderIndex the current search index into the potentialFileHeader
  300.    * @return true if a valid match is found
  301.    */
  302.   private boolean recursivelyFindMatch(String potentialFileHeader, HeaderDefinition headerDefinition,
  303.                                        List<String> expectedTextBetweenSections, List<HeaderSection> sectionsInOrder, int currentTextSegmentIndex,
  304.                                        int currentPotentialFileHeaderIndex) {

  305.     if (currentTextSegmentIndex == expectedTextBetweenSections.size()) {
  306.       return true;
  307.     }

  308.     int currentSearchFromIndex = currentPotentialFileHeaderIndex;

  309.     while (true) {
  310.       String expectedText = expectedTextBetweenSections.get(currentTextSegmentIndex);
  311.       int index = potentialFileHeader.indexOf(expectedText, currentSearchFromIndex);
  312.       if (index == -1) {
  313.         return false;
  314.       }

  315.       if (currentTextSegmentIndex > 0) {
  316.         HeaderSection section = sectionsInOrder.get(currentTextSegmentIndex - 1);
  317.         String sectionValue = potentialFileHeader.substring(currentPotentialFileHeaderIndex, index);
  318.         if (!ensureSectionMatch(headerDefinition, section, sectionValue)) {
  319.           return false;
  320.         }
  321.       }

  322.       if (recursivelyFindMatch(potentialFileHeader, headerDefinition, expectedTextBetweenSections,
  323.           sectionsInOrder, currentTextSegmentIndex + 1, index + expectedText.length())) {
  324.         return true;
  325.       }

  326.       currentSearchFromIndex = index + 1;
  327.     }
  328.   }

  329.   /**
  330.    * If a header section has specified an "ensureMatch" value (see
  331.    * {@link HeaderSection#getEnsureMatch()}), then we verify that the contents
  332.    * of the section in the detected header do indeed match.
  333.    *
  334.    * @param headerDefinition the header definition for the current header match
  335.    * @param section          the header section
  336.    * @param sectionValue     the detected value of the section in the source file header
  337.    * @return false if the detected section value failed the match
  338.    */
  339.   private boolean ensureSectionMatch(HeaderDefinition headerDefinition, HeaderSection section, String sectionValue) {

  340.     String match = section.getEnsureMatch();
  341.     if (!notEmpty(match)) {
  342.       return true;
  343.     }

  344.     String[] lines = sectionValue.split("\n");

  345.     /**
  346.      * We need to clean off any header-specific line-start characters before
  347.      * we perform the match
  348.      */
  349.     String before = headerDefinition.getBeforeEachLine();
  350.     if (notEmpty(before)) {
  351.       for (int i = 0; i < lines.length; ++i) {
  352.         String line = lines[i];
  353.         if (line.startsWith(before)) {
  354.           lines[i] = line.substring(before.length());
  355.         }
  356.       }
  357.     }

  358.     /**
  359.      * If a multi-line match has been specified, we reconstruct the
  360.      * multi-line string (now sans line-start characters) and perform the
  361.      * match on the result
  362.      */
  363.     if (section.isMultiLineMatch()) {
  364.       StringBuilder b = new StringBuilder();
  365.       for (int i = 0; i < lines.length; ++i) {
  366.         if (i > 0) {
  367.           b.append('\n');
  368.         }
  369.         b.append(lines[i]);
  370.       }
  371.       String multiLineValue = b.toString();
  372.       return multiLineValue.matches(match);
  373.     }

  374.     for (String line : lines) {
  375.       if (!line.matches(match)) {
  376.         return false;
  377.       }
  378.     }

  379.     return true;
  380.   }
  381. }