Header.java
- /*
- * Copyright (C) 2008-2023 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.HeaderSection;
- import com.mycila.maven.plugin.license.document.Document;
- import com.mycila.maven.plugin.license.util.StringUtils;
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import java.util.SortedMap;
- import java.util.TreeMap;
- import static com.mycila.maven.plugin.license.util.FileUtils.readFirstLines;
- import static com.mycila.maven.plugin.license.util.FileUtils.remove;
- /**
- * The <code>Header</code> class wraps the license template file, the one which have to be outputted inside the other
- * files.
- */
- public final class Header {
- private final HeaderSource location;
- private final String headerContent;
- private final String headerContentOneLine;
- private String[] lines;
- private final HeaderSection[] sections;
- private final int maxLength;
- /**
- * Constructs a <code>Header</code> object pointing to a license template file. In case of the template contains
- * replaceable values (declared as ${<em>valuename</em>}), you can set the map of this values.
- *
- * @param location The license template file location.
- * @param sections Any applicable header sections for this header
- * @throws IllegalArgumentException If the header file location is null or if an error occurred while reading the
- * file content.
- */
- public Header(HeaderSource location, HeaderSection[] sections) {
- this.location = location;
- this.sections = sections;
- try {
- this.headerContent = location.getContent();
- lines = headerContent.replace("\r", "").split("\n");
- headerContentOneLine = remove(headerContent, " ", "\t", "\r", "\n");
- } catch (Exception e) {
- throw new IllegalArgumentException("Cannot read header document " + location + ". Cause: " + e.getMessage(), e);
- }
- int maxLength = 0;
- for (String line : lines) {
- if (line.length() > maxLength) {
- maxLength = line.length();
- }
- }
- this.maxLength = maxLength;
- }
- public String asString() {
- return headerContent;
- }
- public String asOneLineString() {
- return headerContentOneLine;
- }
- public int getLineCount() {
- return lines.length;
- }
- public int getMaxLineLength() {
- return maxLength;
- }
- /**
- * Returns the location of license template file.
- *
- * @return The URL location.
- */
- public HeaderSource getLocation() {
- return location;
- }
- public String eol(boolean unix) {
- return unix ? "\n" : "\r\n";
- }
- public String buildForDefinition(HeaderDefinition type, boolean unix) {
- StringBuilder newHeader = new StringBuilder();
- String unixEndOfLine = eol(unix);
- if (notEmpty(type.getFirstLine())) {
- String firstLine = type.getFirstLine().replace("EOL", unixEndOfLine);
- newHeader.append(firstLine);
- if (!firstLine.equals(unixEndOfLine)) {
- newHeader.append(unixEndOfLine);
- }
- }
- for (String line : getLines()) {
- final String before = type.getBeforeEachLine().replace("EOL", unixEndOfLine);
- final String after = type.getAfterEachLine().replace("EOL", unixEndOfLine);
- final String str;
- if (type.isPadLines()) {
- str = before + StringUtils.padRight(line, maxLength) + after;
- } else {
- str = before + line + after;
- }
- newHeader.append(StringUtils.rtrim(str));
- newHeader.append(unixEndOfLine);
- }
- if (notEmpty(type.getEndLine())) {
- String endLine = type.getEndLine().replace("EOL", unixEndOfLine);
- newHeader.append(endLine);
- if (!endLine.equals(unixEndOfLine)) {
- newHeader.append(unixEndOfLine);
- }
- }
- return newHeader.toString();
- }
- @Override
- public String toString() {
- return asString();
- }
- public String[] getLines() {
- return lines;
- }
- /**
- * Determines if a potential file header (typically, the start of the file
- * plus some buffer space) matches this header, as rendered with the
- * specified {@link HeaderDefinition} and line-ending.
- *
- * @param potentialFileHeader the potential file header, usually with some extra buffer
- * lines
- * @param headerDefinition the header definition to render the header with
- * @param unix if true, unix line-endings will be used
- * @return true if the header is matched
- */
- public boolean isMatchForText(String potentialFileHeader, HeaderDefinition headerDefinition, boolean unix) {
- String expected = buildForDefinition(headerDefinition, unix);
- return isMatchForText(expected, potentialFileHeader, headerDefinition, unix);
- }
- public boolean isMatchForText(String expected, String potentialFileHeader, HeaderDefinition headerDefinition, boolean unix) {
- SortedMap<Integer, HeaderSection> sectionsByIndex = computeSectionsByIndex(expected);
- if (sectionsByIndex.isEmpty()) {
- return potentialFileHeader.contains(expected);
- }
- List<String> textBetweenSections = buildExpectedTextBetweenSections(expected, sectionsByIndex);
- List<HeaderSection> sectionsInOrder = new ArrayList<>(sectionsByIndex.values());
- return recursivelyFindMatch(potentialFileHeader, headerDefinition, textBetweenSections, sectionsInOrder, 0, 0);
- }
- public boolean isMatchForText(Document d, HeaderDefinition headerDefinition, boolean unix, String encoding) throws IOException {
- String fileHeader = readFirstLines(d.getFile(), getLineCount() + 10, encoding).replaceAll(" *\r?\n", "\n");
- String expected = buildForDefinition(headerDefinition, unix);
- expected = d.mergeProperties(expected);
- return isMatchForText(expected, fileHeader, headerDefinition, unix);
- }
- public String applyDefinitionAndSections(HeaderDefinition headerDefinition, boolean unix) {
- String expected = buildForDefinition(headerDefinition, unix);
- SortedMap<Integer, HeaderSection> sectionsByIndex = computeSectionsByIndex(expected);
- if (sectionsByIndex.isEmpty()) {
- return expected;
- }
- List<String> textBetweenSections = buildExpectedTextBetweenSections(expected, sectionsByIndex);
- List<HeaderSection> sectionsInOrder = new ArrayList<>(sectionsByIndex.values());
- StringBuilder b = new StringBuilder();
- for (int i = 0; i < textBetweenSections.size(); ++i) {
- String textBetween = textBetweenSections.get(i);
- b.append(textBetween);
- if (i < sectionsInOrder.size()) {
- HeaderSection section = sectionsInOrder.get(i);
- String sectionValue = section.getDefaultValue();
- if (notEmpty(sectionValue)) {
- String[] tokens = sectionValue.split(eol(unix));
- for (int j = 0; j < tokens.length; j++) {
- if (j > 0) {
- b.append(eol(unix));
- if (notEmpty(headerDefinition.getBeforeEachLine())) {
- b.append(headerDefinition.getBeforeEachLine());
- }
- b.append(tokens[j]);
- if (notEmpty(headerDefinition.getAfterEachLine())) {
- b.append(headerDefinition.getAfterEachLine());
- }
- } else {
- b.append(tokens[j]);
- }
- }
- }
- }
- }
- return b.toString();
- }
- private boolean notEmpty(String str) {
- return str != null && str.length() > 0;
- }
- /**
- * If this Header has any {@link HeaderSection} sections defined, we look
- * for each header key in the expected header text and note the position
- * index of the match.
- *
- * @param expectedHeaderText the expected header text
- * @return a sorted-map of matching HeaderSections, with the key being the
- * index of section in the header text
- */
- private SortedMap<Integer, HeaderSection> computeSectionsByIndex(String expectedHeaderText) {
- SortedMap<Integer, HeaderSection> sectionsByIndex = new TreeMap<>();
- if (sections == null) {
- return sectionsByIndex;
- }
- for (HeaderSection section : sections) {
- String key = section.getKey();
- int index = expectedHeaderText.indexOf(key);
- if (index == -1) {
- // TODO: we need some way to log that a header section key was not found...
- continue;
- }
- /**
- * Verify that the new section doesn't overlap with an existing
- * section
- */
- int indexEnd = index + section.getKey().length();
- for (Map.Entry<Integer, HeaderSection> entry : sectionsByIndex.entrySet()) {
- int existingIndexStart = entry.getKey();
- HeaderSection existingSection = entry.getValue();
- int existingIndexEnd = existingIndexStart + existingSection.getKey().length();
- if (existingIndexStart < indexEnd && index < existingIndexEnd) {
- throw new IllegalArgumentException(String.format(
- "Existing section '%1$s' overlaps with new section '%2$s'", existingSection.getKey(),
- section.getKey()));
- }
- sectionsByIndex.put(index, section);
- }
- sectionsByIndex.put(index, section);
- }
- return sectionsByIndex;
- }
- /**
- * Once we have found the set of header sections indexed in the expected
- * header text, we extract out the remaining header text occurring
- * in-between those header sections and return an ordered list of the
- * segments.
- * <p>
- * As an example, if out text looked like:
- * <p>
- * "My name is NAME_SECTION and I work for COMPANY_SECTION most days."
- * <p>
- * where "NAME_SECTION" and "COMPANY_SECTION" are matched sections, the
- * resulting list should look like:
- * <p>
- * ["My name is ", " and I work for ", " most days."]
- *
- * @param expectedHeaderText the expected header text
- * @param sectionsByIndex a sorted-map of matching HeaderSections, with the key being
- * the index of section in the header text
- * @return an ordered list of the text segments occurring in-between the
- * sections
- */
- private List<String> buildExpectedTextBetweenSections(String expectedHeaderText,
- SortedMap<Integer, HeaderSection> sectionsByIndex) {
- List<String> textBetweenSections = new ArrayList<>();
- int currentIndex = 0;
- for (Map.Entry<Integer, HeaderSection> entry : sectionsByIndex.entrySet()) {
- int index = entry.getKey();
- HeaderSection section = entry.getValue();
- String textBetween = expectedHeaderText.substring(currentIndex, index);
- textBetweenSections.add(textBetween);
- currentIndex = index + section.getKey().length();
- }
- /**
- * Add the tail of the expected text
- */
- String textBetween = expectedHeaderText.substring(currentIndex, expectedHeaderText.length());
- textBetweenSections.add(textBetween);
- return textBetweenSections;
- }
- /**
- * Given a potential file header and our expected segmented header text,
- * this method recursively searches through the expected segments, looking
- * for possible matches.
- * <p>
- * We recursively search through the potential header for each of the
- * expected text section, advancing our current text segment index and our
- * index into the potential header text. Each step of the recursion
- * considers all possible matches for a text segment, such that the
- * recursion tree will eventually consider ALL valid matches. This can be
- * useful when the user specifies a header like:
- * <p>
- * "Copyright YEAR NAME - License"
- * <p>
- * where "YEAR" and "NAME" are sections, meaning that we have to match a " "
- * in-between, which can potentially match in multiple places if the actual
- * values in the potential header contain spaces.
- *
- * @param potentialFileHeader the potential file header
- * @param headerDefinition the header definition
- * @param expectedTextBetweenSections the expected text between sections
- * @param sectionsInOrder the sections interleaved with the expected text
- * @param currentTextSegmentIndex the index of the current expected text segment to search for
- * @param currentPotentialFileHeaderIndex the current search index into the potentialFileHeader
- * @return true if a valid match is found
- */
- private boolean recursivelyFindMatch(String potentialFileHeader, HeaderDefinition headerDefinition,
- List<String> expectedTextBetweenSections, List<HeaderSection> sectionsInOrder, int currentTextSegmentIndex,
- int currentPotentialFileHeaderIndex) {
- if (currentTextSegmentIndex == expectedTextBetweenSections.size()) {
- return true;
- }
- int currentSearchFromIndex = currentPotentialFileHeaderIndex;
- while (true) {
- String expectedText = expectedTextBetweenSections.get(currentTextSegmentIndex);
- int index = potentialFileHeader.indexOf(expectedText, currentSearchFromIndex);
- if (index == -1) {
- return false;
- }
- if (currentTextSegmentIndex > 0) {
- HeaderSection section = sectionsInOrder.get(currentTextSegmentIndex - 1);
- String sectionValue = potentialFileHeader.substring(currentPotentialFileHeaderIndex, index);
- if (!ensureSectionMatch(headerDefinition, section, sectionValue)) {
- return false;
- }
- }
- if (recursivelyFindMatch(potentialFileHeader, headerDefinition, expectedTextBetweenSections,
- sectionsInOrder, currentTextSegmentIndex + 1, index + expectedText.length())) {
- return true;
- }
- currentSearchFromIndex = index + 1;
- }
- }
- /**
- * If a header section has specified an "ensureMatch" value (see
- * {@link HeaderSection#getEnsureMatch()}), then we verify that the contents
- * of the section in the detected header do indeed match.
- *
- * @param headerDefinition the header definition for the current header match
- * @param section the header section
- * @param sectionValue the detected value of the section in the source file header
- * @return false if the detected section value failed the match
- */
- private boolean ensureSectionMatch(HeaderDefinition headerDefinition, HeaderSection section, String sectionValue) {
- String match = section.getEnsureMatch();
- if (!notEmpty(match)) {
- return true;
- }
- String[] lines = sectionValue.split("\n");
- /**
- * We need to clean off any header-specific line-start characters before
- * we perform the match
- */
- String before = headerDefinition.getBeforeEachLine();
- if (notEmpty(before)) {
- for (int i = 0; i < lines.length; ++i) {
- String line = lines[i];
- if (line.startsWith(before)) {
- lines[i] = line.substring(before.length());
- }
- }
- }
- /**
- * If a multi-line match has been specified, we reconstruct the
- * multi-line string (now sans line-start characters) and perform the
- * match on the result
- */
- if (section.isMultiLineMatch()) {
- StringBuilder b = new StringBuilder();
- for (int i = 0; i < lines.length; ++i) {
- if (i > 0) {
- b.append('\n');
- }
- b.append(lines[i]);
- }
- String multiLineValue = b.toString();
- return multiLineValue.matches(match);
- }
- for (String line : lines) {
- if (!line.matches(match)) {
- return false;
- }
- }
- return true;
- }
- }