/* * 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 * * http://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) { final String lineSeparator = System.getProperty("line.separator"); if ("=============================================================================" .equals(line)) { //We have ended changelog for that particular file //so we can save it final int end = comment.length() - lineSeparator.length(); //was -1 comment = comment.substring(0, end); saveEntry(); status = GET_FILE; } else if ("----------------------------".equals(line)) { final int end = comment.length() - lineSeparator.length(); //was -1 comment = comment.substring(0, end); status = GET_PREVIOUS_REV; } else { comment += line + 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, line.length()); // 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 -> { return 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; } }