1 package org.apache.maven.scm.provider.perforce.command.changelog;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import org.apache.maven.scm.ChangeFile;
23 import org.apache.maven.scm.ChangeSet;
24 import org.apache.maven.scm.ScmException;
25 import org.apache.maven.scm.log.ScmLogger;
26 import org.apache.maven.scm.util.AbstractConsumer;
27
28 import java.util.ArrayList;
29 import java.util.Date;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35
36 /**
37 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
38 *
39 */
40 public class PerforceChangeLogConsumer
41 extends AbstractConsumer
42 {
43 /**
44 * Date formatter for perforce timestamp
45 */
46 private static final String PERFORCE_TIMESTAMP_PATTERN = "yyyy/MM/dd HH:mm:ss";
47
48 private List<ChangeSet> entries = new ArrayList<ChangeSet>();
49
50 /**
51 * State machine constant: expecting revision and/or file information
52 */
53 private static final int GET_REVISION = 1;
54
55 /**
56 * State machine constant: eat the first blank line
57 */
58 private static final int GET_COMMENT_BEGIN = 2;
59
60 /**
61 * State machine constant: expecting comments
62 */
63 private static final int GET_COMMENT = 3;
64
65 /**
66 * The comment section ends with a blank line
67 */
68 private static final String COMMENT_DELIMITER = "";
69
70 /**
71 * A file line begins with two slashes
72 */
73 private static final String FILE_BEGIN_TOKEN = "//";
74
75 /**
76 * Current status of the parser
77 */
78 private int status = GET_REVISION;
79
80 /**
81 * The current log entry being processed by the parser
82 */
83 private ChangeSet currentChange;
84
85 /**
86 * the current file being processed by the parser
87 */
88 private String currentFile;
89
90 /**
91 * The location of files within the Perforce depot that we are processing
92 * e.g. //depot/projects/foo/bar
93 */
94 private String repoPath;
95
96 private Date startDate;
97
98 private Date endDate;
99
100 private String userDatePattern;
101
102 /**
103 * The regular expression used to match header lines
104 */
105 private static final Pattern PATTERN = Pattern.compile( "^\\.\\.\\. #(\\d+) " + // revision number
106 "change (\\d+) .* " + // changelist number
107 "on (.*) " + // date
108 "by (.*)@" ); // author
109
110 public PerforceChangeLogConsumer( String path, Date startDate, Date endDate, String userDatePattern,
111 ScmLogger logger )
112 {
113 super( logger );
114
115 this.startDate = startDate;
116 this.endDate = endDate;
117 this.userDatePattern = userDatePattern;
118 this.repoPath = path;
119 }
120
121 // ----------------------------------------------------------------------
122 //
123 // ----------------------------------------------------------------------
124
125 public List<ChangeSet> getModifications() throws ScmException
126 {
127
128 // Here there are one entry for each couple (changelist,file). We merge
129 // entries to have only one entry per changelist
130
131 // Date > ChangeSet
132 Map<Date, ChangeSet> groupedEntries = new LinkedHashMap<Date, ChangeSet>();
133 for ( int i = 0; i < entries.size(); i++ )
134 {
135 ChangeSet cs = (ChangeSet) entries.get( i );
136 ChangeSet hit = (ChangeSet) groupedEntries.get( cs.getDate() );
137 if ( hit != null )
138 {
139 if ( cs.getFiles().size() != 1 )
140 {
141 throw new ScmException( "Merge of entries failed. Bad entry size: " + cs.getFiles().size() );
142 }
143 hit.addFile( (ChangeFile) cs.getFiles().get( 0 ) );
144 }
145 else
146 {
147 groupedEntries.put( cs.getDate(), cs );
148 }
149 }
150
151 List<ChangeSet> result = new ArrayList<ChangeSet>();
152 result.addAll( groupedEntries.values() );
153
154 return result;
155 }
156
157 // ----------------------------------------------------------------------
158 // StreamConsumer Implementation
159 // ----------------------------------------------------------------------
160
161 /** {@inheritDoc} */
162 public void consumeLine( String line )
163 {
164 switch ( status )
165 {
166 case GET_REVISION:
167 processGetRevision( line );
168 break;
169 case GET_COMMENT_BEGIN:
170 status = GET_COMMENT;
171 break;
172 case GET_COMMENT:
173 processGetComment( line );
174 break;
175 default:
176 throw new IllegalStateException( "Unknown state: " + status );
177 }
178 }
179
180 // ----------------------------------------------------------------------
181 //
182 // ----------------------------------------------------------------------
183
184 /**
185 * Add a change log entry to the list (if it's not already there)
186 * with the given file.
187 *
188 * @param entry a {@link ChangeSet} to be added to the list if another
189 * with the same key (p4 change number) doesn't exist already.
190 * @param file a {@link ChangeFile} to be added to the entry
191 */
192 private void addEntry( ChangeSet entry, ChangeFile file )
193 {
194 // ----------------------------------------------------------------------
195 // Check that we are inside the requested date range
196 // ----------------------------------------------------------------------
197
198 if ( startDate != null && entry.getDate().before( startDate ) )
199 {
200 return;
201 }
202
203 if ( endDate != null && entry.getDate().after( endDate ) )
204 {
205 return;
206 }
207
208 // ----------------------------------------------------------------------
209 //
210 // ----------------------------------------------------------------------
211
212 entry.addFile( file );
213
214 entries.add( entry );
215 }
216
217 /**
218 * Most of the relevant info is on the revision line matching the
219 * 'pattern' string.
220 *
221 * @param line A line of text from the perforce log output
222 */
223 private void processGetRevision( String line )
224 {
225 if ( line.startsWith( FILE_BEGIN_TOKEN ) )
226 {
227 currentFile = line.substring( repoPath.length() + 1 );
228 return;
229 }
230
231 Matcher matcher = PATTERN.matcher( line );
232 if ( !matcher.find() )
233 {
234 return;
235 }
236
237 currentChange = new ChangeSet();
238 currentChange.setRevision( matcher.group( 1 ) );
239 currentChange.setDate( parseDate( matcher.group( 3 ), userDatePattern, PERFORCE_TIMESTAMP_PATTERN ) );
240 currentChange.setAuthor( matcher.group( 4 ) );
241
242 status = GET_COMMENT_BEGIN;
243 }
244
245 /**
246 * Process the current input line in the GET_COMMENT state. This
247 * state gathers all of the comments that are part of a log entry.
248 *
249 * @param line a line of text from the perforce log output
250 */
251 private void processGetComment( String line )
252 {
253 if ( line.equals( COMMENT_DELIMITER ) )
254 {
255 addEntry( currentChange, new ChangeFile( currentFile, currentChange.getRevision() ) );
256
257 status = GET_REVISION;
258 }
259 else
260 {
261 currentChange.setComment( currentChange.getComment() + line + "\n" );
262 }
263 }
264 }