001package org.apache.maven.scm.provider.svn.svnexe.command.changelog; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import org.apache.maven.scm.ChangeFile; 023import org.apache.maven.scm.ChangeSet; 024import org.apache.maven.scm.ScmFileStatus; 025import org.apache.maven.scm.log.ScmLogger; 026import org.apache.maven.scm.provider.svn.SvnChangeSet; 027import org.apache.maven.scm.util.AbstractConsumer; 028 029import java.util.ArrayList; 030import java.util.Date; 031import java.util.List; 032import java.util.regex.Matcher; 033import java.util.regex.Pattern; 034 035/** 036 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a> 037 * 038 */ 039public class SvnChangeLogConsumer 040 extends AbstractConsumer 041{ 042 /** 043 * Date formatter for svn timestamp (after a little massaging) 044 */ 045 private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss zzzzzzzzz"; 046 047 /** 048 * State machine constant: expecting header 049 */ 050 private static final int GET_HEADER = 1; 051 052 /** 053 * State machine constant: expecting file information 054 */ 055 private static final int GET_FILE = 2; 056 057 /** 058 * State machine constant: expecting comments 059 */ 060 private static final int GET_COMMENT = 3; 061 062 /** 063 * There is always action and affected path; when copying/moving, recognize also original path and revision 064 */ 065 private static final Pattern FILE_PATTERN = Pattern.compile( "^\\s\\s\\s([A-Z])\\s(.+)$" ); 066 067 /** 068 * This matches the 'original file info' part of the complete file line. 069 * Note the use of [:alpha:] instead of literal 'from' - this is meant to allow non-English localizations. 070 */ 071 private static final Pattern ORIG_FILE_PATTERN = Pattern.compile( "\\([A-Za-z]+ (.+):(\\d+)\\)" ); 072 073 /** 074 * The file section ends with a blank line 075 */ 076 private static final String FILE_END_TOKEN = ""; 077 078 /** 079 * The comment section ends with a dashed line 080 */ 081 private static final String COMMENT_END_TOKEN = 082 "------------------------------------" + "------------------------------------"; 083 084 /** 085 * Current status of the parser 086 */ 087 private int status = GET_HEADER; 088 089 /** 090 * List of change log entries 091 */ 092 private List<ChangeSet> entries = new ArrayList<ChangeSet>(); 093 094 /** 095 * The current log entry being processed by the parser 096 */ 097 private SvnChangeSet currentChange; 098 099 /** 100 * The current revision of the entry being processed by the parser 101 */ 102 private String currentRevision; 103 104 /** 105 * The current comment of the entry being processed by the parser 106 */ 107 private StringBuilder currentComment; 108 109 /** 110 * The regular expression used to match header lines 111 */ 112 private static final Pattern HEADER_REG_EXP = Pattern.compile( "^(.+) \\| (.+) \\| (.+) \\|.*$" ); 113 114 private static final int REVISION_GROUP = 1; 115 116 private static final int AUTHOR_GROUP = 2; 117 118 private static final int DATE_GROUP = 3; 119 120 private static final Pattern REVISION_REG_EXP1 = Pattern.compile( "rev (\\d+):" ); 121 122 private static final Pattern REVISION_REG_EXP2 = Pattern.compile( "r(\\d+)" ); 123 124 private static final Pattern DATE_REG_EXP = Pattern.compile( "(\\d+-\\d+-\\d+ " + // date 2002-08-24 125 "\\d+:\\d+:\\d+) " + // time 16:01:00 126 "([\\-+])(\\d\\d)(\\d\\d)" ); // gmt offset -0400);) 127 128 private final String userDateFormat; 129 130 /** 131 * Default constructor. 132 */ 133 public SvnChangeLogConsumer( ScmLogger logger, String userDateFormat ) 134 { 135 super( logger ); 136 137 this.userDateFormat = userDateFormat; 138 } 139 140 public List<ChangeSet> getModifications() 141 { 142 return entries; 143 } 144 145 // ---------------------------------------------------------------------- 146 // StreamConsumer Implementation 147 // ---------------------------------------------------------------------- 148 149 /** 150 * {@inheritDoc} 151 */ 152 public void consumeLine( String line ) 153 { 154 if ( getLogger().isDebugEnabled() ) 155 { 156 getLogger().debug( line ); 157 } 158 switch ( status ) 159 { 160 case GET_HEADER: 161 processGetHeader( line ); 162 break; 163 case GET_FILE: 164 processGetFile( line ); 165 break; 166 case GET_COMMENT: 167 processGetComment( line ); 168 break; 169 default: 170 throw new IllegalStateException( "Unknown state: " + status ); 171 } 172 } 173 174 // ---------------------------------------------------------------------- 175 // 176 // ---------------------------------------------------------------------- 177 178 /** 179 * Process the current input line in the GET_HEADER state. The 180 * author, date, and the revision of the entry are gathered. Note, 181 * Subversion does not have per-file revisions, instead, the entire 182 * repository is given a single revision number, which is used for 183 * the revision number of each file. 184 * 185 * @param line A line of text from the svn log output 186 */ 187 private void processGetHeader( String line ) 188 { 189 Matcher matcher = HEADER_REG_EXP.matcher( line ); 190 if ( !matcher.matches() ) 191 { 192 // The header line is not found. Intentionally do nothing. 193 return; 194 } 195 196 currentRevision = getRevision( matcher.group( REVISION_GROUP ) ); 197 198 currentChange = new SvnChangeSet(); 199 200 currentChange.setAuthor( matcher.group( AUTHOR_GROUP ) ); 201 202 currentChange.setDate( getDate( matcher.group( DATE_GROUP ) ) ); 203 204 currentChange.setRevision( currentRevision ); 205 206 status = GET_FILE; 207 } 208 209 /** 210 * Gets the svn revision, from the svn log revision output. 211 * 212 * @param revisionOutput 213 * @return the svn revision 214 */ 215 private String getRevision( final String revisionOutput ) 216 { 217 Matcher matcher; 218 if ( ( matcher = REVISION_REG_EXP1.matcher( revisionOutput ) ).matches() ) 219 { 220 return matcher.group( 1 ); 221 } 222 else if ( ( matcher = REVISION_REG_EXP2.matcher( revisionOutput ) ).matches() ) 223 { 224 return matcher.group( 1 ); 225 } 226 else 227 { 228 throw new IllegalOutputException( revisionOutput ); 229 } 230 } 231 232 /** 233 * Process the current input line in the GET_FILE state. This state 234 * adds each file entry line to the current change log entry. Note, 235 * the revision number for the entire entry is used for the revision 236 * number of each file. 237 * 238 * @param line A line of text from the svn log output 239 */ 240 private void processGetFile( String line ) 241 { 242 Matcher matcher = FILE_PATTERN.matcher( line ); 243 if ( matcher.matches() ) 244 { 245 final String fileinfo = matcher.group( 2 ); 246 String name = fileinfo; 247 String originalName = null; 248 String originalRev = null; 249 final int n = fileinfo.indexOf( " (" ); 250 if ( n > 1 && fileinfo.endsWith( ")" ) ) 251 { 252 final String origFileInfo = fileinfo.substring( n ); 253 Matcher matcher2 = ORIG_FILE_PATTERN.matcher( origFileInfo ); 254 if ( matcher2.find() ) 255 { 256 // if original file is present, we must extract the affected one from the beginning 257 name = fileinfo.substring( 0, n ); 258 originalName = matcher2.group( 1 ); 259 originalRev = matcher2.group( 2 ); 260 } 261 } 262 final String actionStr = matcher.group( 1 ); 263 final ScmFileStatus action; 264 if ( "A".equals( actionStr ) ) 265 { 266 //TODO: this may even change to MOVED if we later explore whole changeset and find matching DELETED 267 action = originalRev == null ? ScmFileStatus.ADDED : ScmFileStatus.COPIED; 268 } 269 else if ( "D".equals( actionStr ) ) 270 { 271 action = ScmFileStatus.DELETED; 272 } 273 else if ( "M".equals( actionStr ) ) 274 { 275 action = ScmFileStatus.MODIFIED; 276 } 277 else if ( "R".equals( actionStr ) ) 278 { 279 action = ScmFileStatus.UPDATED; //== REPLACED in svn terms 280 } 281 else 282 { 283 action = ScmFileStatus.UNKNOWN; 284 } 285 if ( getLogger().isDebugEnabled() ) 286 { 287 getLogger().debug( actionStr + " : " + name ); 288 } 289 final ChangeFile changeFile = new ChangeFile( name, currentRevision ); 290 changeFile.setAction( action ); 291 changeFile.setOriginalName( originalName ); 292 changeFile.setOriginalRevision( originalRev ); 293 currentChange.addFile( changeFile ); 294 295 status = GET_FILE; 296 } 297 else if ( line.equals( FILE_END_TOKEN ) ) 298 { 299 // Create a buffer for the collection of the comment now 300 // that we are leaving the GET_FILE state. 301 currentComment = new StringBuilder(); 302 303 status = GET_COMMENT; 304 } 305 } 306 307 /** 308 * Process the current input line in the GET_COMMENT state. This 309 * state gathers all of the comments that are part of a log entry. 310 * 311 * @param line a line of text from the svn log output 312 */ 313 private void processGetComment( String line ) 314 { 315 if ( line.equals( COMMENT_END_TOKEN ) ) 316 { 317 currentChange.setComment( currentComment.toString() ); 318 319 entries.add( currentChange ); 320 321 status = GET_HEADER; 322 } 323 else 324 { 325 currentComment.append( line ).append( '\n' ); 326 } 327 } 328 329 /** 330 * Converts the date time stamp from the svn output into a date 331 * object. 332 * 333 * @param dateOutput The date output from an svn log command. 334 * @return A date representing the time stamp of the log entry. 335 */ 336 private Date getDate( final String dateOutput ) 337 { 338 Matcher matcher = DATE_REG_EXP.matcher( dateOutput ); 339 if ( !matcher.find() ) 340 { 341 throw new IllegalOutputException( dateOutput ); 342 } 343 344 final StringBuilder date = new StringBuilder(); 345 date.append( matcher.group( 1 ) ); 346 date.append( " GMT" ); 347 date.append( matcher.group( 2 ) ); 348 date.append( matcher.group( 3 ) ); 349 date.append( ':' ); 350 date.append( matcher.group( 4 ) ); 351 352 return parseDate( date.toString(), userDateFormat, SVN_TIMESTAMP_PATTERN ); 353 } 354}