blob: bb1918cfef114e30451bbea360a6b544949e9512 [file] [log] [blame]
/*
* 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[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.
*
* @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.
* @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;
}
}