/*
 *  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.
/** * 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
/** 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.
/** * 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
/** 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[names.size()]); 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.
Returns:a list of rcs entries as an array
/** * 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[entries.size()]); }
Receive notification about the process writing to standard output.
Params:
  • line – the line to process
/** * 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.
Params:
  • line – the line
/** * 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.
Params:
  • line – the line to process
/** * 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.
Params:
  • line – the line to process
/** * 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.
Params:
  • line – the line to process
/** * 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.
Params:
  • line – the line to process
/** * 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.
/** * 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.
Params:
  • date – the string holding date
Returns:the date object or null if unknown date format
/** * 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.
/** * 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; } }