content: /*
   *  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
   *
   *      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 org.apache.tools.ant.taskdefs.cvslib;
  
  import java.text.ParseException;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Date;
  import java.util.Hashtable;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.StringTokenizer;
  import java.util.TimeZone;
  
  import org.apache.tools.ant.taskdefs.AbstractCvsTask;
  import org.apache.tools.ant.taskdefs.AbstractCvsTask.Module;
  
  /**
   * A class used to parse the output of the CVS log command.
   *
   */
  class ChangeLogParser {
      private static final int GET_FILE = 1;
      private static final int GET_DATE = 2;
      private static final int GET_COMMENT = 3;
      private static final int GET_REVISION = 4;
      private static final int GET_PREVIOUS_REV = 5;
  
      /** input format for dates read in from cvs log */
      private final SimpleDateFormat inputDate
          = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US);
      /**
       * New formatter used to parse CVS date/timestamp.
       */
      private final SimpleDateFormat cvs1129InputDate =
          new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
  
      //The following is data used while processing stdout of CVS command
      private String file;
      private String date;
      private String author;
      private String comment;
      private String revision;
      private String previousRevision;
  
      private int status = GET_FILE;
  
      /** rcs entries */
      private final Map<String, CVSEntry> entries = new Hashtable<>();
  
      private final boolean remote;
      private final String[] moduleNames;
      private final int[] moduleNameLengths;
  
      public ChangeLogParser() {
          this(false, "", Collections.emptyList());
      }
  
      public ChangeLogParser(boolean remote, String packageName, List<AbstractCvsTask.Module> modules) {
          this.remote = remote;
  
          List<String> names = new ArrayList<>();
          if (packageName != null) {
              for (StringTokenizer tok = new StringTokenizer(packageName);
                   tok.hasMoreTokens();) {
                  names.add(tok.nextToken());
              }
          }
          modules.stream().map(Module::getName).forEach(names::add);
  
          moduleNames = names.toArray(new String[0]);
          moduleNameLengths = new int[moduleNames.length];
          for (int i = 0; i < moduleNames.length; i++) {
              moduleNameLengths[i] = moduleNames[i].length();
          }
  
          TimeZone utc = TimeZone.getTimeZone("UTC");
          inputDate.setTimeZone(utc);
          cvs1129InputDate.setTimeZone(utc);
      }
  
      /**
       * Get a list of rcs entries as an array.
       *
       * @return a list of rcs entries as an array
       */
      public CVSEntry[] getEntrySetAsArray() {
          return entries.values().toArray(new CVSEntry[0]);
      }
  
      /**
       * Receive notification about the process writing
       * to standard output.
       * @param line the line to process
       */
      public void stdout(final String line) {
          switch (status) {
              case GET_FILE:
                  // make sure attributes are reset when
                  // working on a 'new' file.
                  reset();
                  processFile(line);
                  break;
              case GET_REVISION:
                  processRevision(line);
                  break;
  
              case GET_DATE:
                  processDate(line);
                  break;
  
              case GET_COMMENT:
                  processComment(line);
                  break;
  
              case GET_PREVIOUS_REV:
                  processGetPreviousRevision(line);
                  break;
  
              default:
                  // Do nothing
                  break;
          }
      }
  
      /**
       * Process a line while in "GET_COMMENT" state.
       *
       * @param line the line
       */
      private void processComment(final String line) {
          if ("============================================================================="
              .equals(line)) {
              //We have ended changelog for that particular file
              //so we can save it
              final int end = comment.length() - System.lineSeparator().length(); //was -1
              comment = comment.substring(0, end);
              saveEntry();
              status = GET_FILE;
          } else if ("----------------------------".equals(line)) {
              final int end = comment.length() - System.lineSeparator().length(); //was -1
              comment = comment.substring(0, end);
              status = GET_PREVIOUS_REV;
          } else {
              comment += line + System.lineSeparator();
          }
      }
  
      /**
       * Process a line while in "GET_FILE" state.
       *
       * @param line the line to process
       */
      private void processFile(final String line) {
          if (!remote && line.startsWith("Working file:")) {
              // CheckStyle:MagicNumber OFF
              file = line.substring(14);
              // CheckStyle:MagicNumber ON
              status = GET_REVISION;
          } else if (remote && line.startsWith("RCS file:")) {
              // exclude the part of the RCS filename up to and
              // including the module name (and the path separator)
              int startOfFileName = 0;
              for (int i = 0; i < moduleNames.length; i++) {
                  int index = line.indexOf(moduleNames[i]);
                  if (index >= 0) {
                      startOfFileName = index + moduleNameLengths[i] + 1;
                      break;
                  }
              }
              int endOfFileName = line.indexOf(",v");
              if (endOfFileName == -1) {
                  file = line.substring(startOfFileName);
              } else {
                  file = line.substring(startOfFileName, endOfFileName);
              }
              status = GET_REVISION;
          }
      }
  
      /**
       * Process a line while in "REVISION" state.
       *
       * @param line the line to process
       */
      private void processRevision(final String line) {
          if (line.startsWith("revision")) {
              // CheckStyle:MagicNumber OFF
              revision = line.substring(9);
              // CheckStyle:MagicNumber ON
              status = GET_DATE;
          } else if (line.startsWith("======")) {
              //There were no revisions in this changelog
              //entry so lets move onto next file
              status = GET_FILE;
          }
      }
  
      /**
       * Process a line while in "DATE" state.
       *
       * @param line the line to process
       */
      private void processDate(final String line) {
          if (line.startsWith("date:")) {
              // The date format is using a - format since 1.12.9 so we have:
              // 1.12.9-: 'date: YYYY/mm/dd HH:mm:ss;  author: name;'
              // 1.12.9+: 'date: YYYY-mm-dd HH:mm:ss Z;  author: name'
              int endOfDateIndex = line.indexOf(';');
              date = line.substring("date: ".length(), endOfDateIndex);
  
              int startOfAuthorIndex = line.indexOf("author: ", endOfDateIndex + 1);
              int endOfAuthorIndex = line.indexOf(';', startOfAuthorIndex + 1);
              author = line.substring("author: ".length() + startOfAuthorIndex, endOfAuthorIndex);
  
              status = GET_COMMENT;
  
              //Reset comment to empty here as we can accumulate multiple lines
              //in the processComment method
              comment = "";
          }
      }
  
      /**
       * Process a line while in "GET_PREVIOUS_REVISION" state.
       *
       * @param line the line to process
       */
      private void processGetPreviousRevision(final String line) {
          if (!line.startsWith("revision ")) {
              throw new IllegalStateException("Unexpected line from CVS: "
                  + line);
          }
          previousRevision = line.substring("revision ".length());
  
          saveEntry();
  
          revision = previousRevision;
          status = GET_DATE;
      }
  
      /**
       * Utility method that saves the current entry.
       */
      private void saveEntry() {
          entries.computeIfAbsent(date + author + comment,
                  k -> new CVSEntry(parseDate(date), author, comment)).addFile(file, revision, previousRevision);
      }
  
      /**
       * Parse date out from expected format.
       *
       * @param date the string holding date
       * @return the date object or null if unknown date format
       */
      private Date parseDate(final String date) {
          try {
              return inputDate.parse(date);
          } catch (ParseException e) {
              try {
                  return cvs1129InputDate.parse(date);
              } catch (ParseException e2) {
                  throw new IllegalStateException("Invalid date format: " + date);
              }
          }
      }
  
      /**
       * Reset all internal attributes except status.
       */
      public void reset() {
          this.file = null;
          this.date = null;
          this.author = null;
          this.comment = null;
          this.revision = null;
          this.previousRevision = null;
      }
  }
  