/* * Copyright 2002-2004 The Apache Software Foundation * * 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 * * 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 hudson.org.apache.tools.ant.taskdefs.cvslib; // patched to work around http://issues.apache.org/bugzilla/show_bug.cgi?id=38583 import org.apache.tools.ant.Project; import org.apache.commons.io.FileUtils; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.Map.Entry; import java.util.TimeZone; import java.util.logging.Logger; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.io.File; import java.io.IOException; /** * A class used to parse the output of the CVS log command. * * @version $Revision$ $Date$ */ class ChangeLogParser { //private static final int GET_ENTRY = 0; 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; private static final int GET_SYMBOLIC_NAMES = 6; /** * input format for dates read in from cvs log. * * Some users reported that they see different formats, * so this is extended from original Ant version to cover different formats. * * <p> * KK: {@link SimpleDateFormat} is not thread safe, so make it per-instance. */ private final SimpleDateFormat[] c_inputDate = new SimpleDateFormat[]{ new SimpleDateFormat("yyyy/MM/dd HH:mm:ss Z"), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"), new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), }; { TimeZone utc = TimeZone.getTimeZone("UTC"); for (SimpleDateFormat df : c_inputDate) { df.setTimeZone(utc); } } //The following is data used while processing stdout of CVS command private String m_file; private String m_fullName; private String m_date; private String m_author; private String m_comment; private String m_revision; private String m_previousRevision; /** * All branches available on the current file. * Keyed by branch revision prefix (like "1.2.3." if files in the branch have revision numbers like * "1.2.3.4") and the value is the branch name. */ private final Map<String,String> branches = new HashMap<String,String>(); /** * True if the log record indicates deletion; */ private boolean m_dead; private int m_status = GET_FILE; /** rcs entries */ private final Hashtable<String,CVSEntry> m_entries = new Hashtable<String,CVSEntry>(); private final ChangeLogTask owner; public ChangeLogParser(ChangeLogTask owner) { this.owner = owner; } /** * Get a list of rcs entries as an array. * * @return a list of rcs entries as an array */ CVSEntry[] getEntrySetAsArray() { final CVSEntry[] array = new CVSEntry[ m_entries.size() ]; Enumeration e = m_entries.elements(); int i = 0; while (e.hasMoreElements()) { array[i++] = (CVSEntry) e.nextElement(); } return array; } private boolean dead = false; private String previousLine = null; /** * Receive notification about the process writing * to standard output. */ public void stdout(final String line) { if(dead) return; try { switch(m_status) { case GET_FILE: // make sure attributes are reset when // working on a 'new' file. reset(); processFile(line); break; case GET_SYMBOLIC_NAMES: processSymbolicName(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; } } catch (Exception e) { // we don't know how to handle the input any more. don't accept any more input dead = true; } } /** * 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 (line.startsWith("======")) { //We have ended changelog for that particular file //so we can save it final int end = m_comment.length() - lineSeparator.length(); //was -1 m_comment = m_comment.substring(0, end); saveEntry(); m_status = GET_FILE; } else if (null != previousLine && previousLine.startsWith("----------------------------")) { if (line.startsWith("revision")) { final int end = m_comment.length() - lineSeparator.length(); //was -1 m_comment = m_comment.substring(0, end); m_status = GET_PREVIOUS_REV; processGetPreviousRevision(line); } else { m_comment += previousLine + lineSeparator + line + lineSeparator; } previousLine = null; } else if (line.startsWith("----------------------------")) { if (null != previousLine) { m_comment += previousLine + lineSeparator; } previousLine = line; } else { m_comment += line + lineSeparator; } } /** * Process a line while in "GET_FILE" state. * * @param line the line */ private void processFile(final String line) { if (line.startsWith("Working file:")) { m_file = line.substring(14, line.length()); File repo = new File(new File(owner.getDir(), m_file).getParentFile(), "CVS/Repository"); try { String module = FileUtils.readFileToString(repo, null);// not sure what encoding CVS uses. String simpleName = m_file.substring(m_file.lastIndexOf('/')+1); m_fullName = '/'+module.trim()+'/'+simpleName; } catch (IOException e) { // failed to read LOGGER.log(Level.WARNING, "Failed to read CVS/Repository at "+repo,e); m_fullName = null; } m_status = GET_SYMBOLIC_NAMES; } } /** * Obtains the revision name list */ private void processSymbolicName(String line) { if (line.startsWith("\t")) { line = line.trim(); int idx = line.lastIndexOf(':'); if(idx<0) { // ??? return; } String symbol = line.substring(0,idx); Matcher m = DOT_PATTERN.matcher(line.substring(idx + 2)); if(!m.matches()) return; // not a branch name branches.put(m.group(1)+m.group(3)+'.',symbol); } else if (line.startsWith("keyword substitution:")) { m_status = GET_REVISION; } } private static final Pattern DOT_PATTERN = Pattern.compile("(([0-9]+\\.)+)0\\.([0-9]+)"); /** * Process a line while in "REVISION" state. * * @param line the line */ private void processRevision(final String line) { if (line.startsWith("revision")) { m_revision = line.substring(9); m_status = GET_DATE; } else if (line.startsWith("======")) { //There was no revisions in this changelog //entry so lets move unto next file m_status = GET_FILE; } } /** * Process a line while in "DATE" state. * * @param line the line */ private void processDate(final String line) { if (line.startsWith("date:")) { int idx = line.indexOf(";"); m_date = line.substring(6, idx); String lineData = line.substring(idx + 1); m_author = lineData.substring(10, lineData.indexOf(";")); m_status = GET_COMMENT; m_dead = lineData.indexOf("state: dead;")!=-1; //Reset comment to empty here as we can accumulate multiple lines //in the processComment method m_comment = ""; } } /** * Process a line while in "GET_PREVIOUS_REVISION" state. * * @param line the line */ private void processGetPreviousRevision(final String line) { if (!line.startsWith("revision")) { throw new IllegalStateException("Unexpected line from CVS: " + line); } m_previousRevision = line.substring(9); saveEntry(); m_revision = m_previousRevision; m_status = GET_DATE; } /** * Utility method that saves the current entry. */ private void saveEntry() { final String entryKey = m_date + m_author + m_comment; CVSEntry entry; if (!m_entries.containsKey(entryKey)) { entry = new CVSEntry(parseDate(m_date), m_author, m_comment); m_entries.put(entryKey, entry); } else { entry = m_entries.get(entryKey); } String branch = findBranch(m_revision); owner.log("Recorded a change: "+m_date+','+m_author+','+m_revision+"(branch="+branch+"),"+m_comment,Project.MSG_VERBOSE); entry.addFile(m_file, m_fullName, m_revision, m_previousRevision, branch, m_dead); } /** * Finds the branch name that matches the revision, or null if not found. */ private String findBranch(String revision) { if(revision==null) return null; // defensive check for (Entry<String,String> e : branches.entrySet()) { if(revision.startsWith(e.getKey()) && revision.substring(e.getKey().length()).indexOf('.')==-1) return e.getValue(); } return null; } /** * Parse date out from expected format. * * @param date the string holding dat * @return the date object or null if unknown date format */ private Date parseDate(String date) { for (SimpleDateFormat df : c_inputDate) { try { return df.parse(date); } catch (ParseException e) { // try next if one fails } } // nothing worked owner.log("Failed to parse "+date+"\n", Project.MSG_ERR); //final String message = REZ.getString( "changelog.bat-date.error", date ); //getContext().error( message ); return null; } /** * reset all internal attributes except status. */ private void reset() { m_file = null; m_fullName = null; m_date = null; m_author = null; m_comment = null; m_revision = null; m_previousRevision = null; m_dead = false; branches.clear(); } private static final Logger LOGGER = Logger.getLogger(ChangeLogParser.class.getName()); }