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 import org.apache.regexp.RE;
28 import org.apache.regexp.RESyntaxException;
29
30 import java.util.ArrayList;
31 import java.util.Date;
32 import java.util.LinkedHashMap;
33 import java.util.List;
34 import java.util.Map;
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 /**
97 * The regular expression used to match header lines
98 */
99 private RE revisionRegexp;
100
101 private Date startDate;
102
103 private Date endDate;
104
105 private String userDatePattern;
106
107 private static final String PATTERN = "^\\.\\.\\. #(\\d+) " + // revision number
108 "change (\\d+) .* " + // changelist number
109 "on (.*) " + // date
110 "by (.*)@"; // author
111
112 public PerforceChangeLogConsumer( String path, Date startDate, Date endDate, String userDatePattern,
113 ScmLogger logger )
114 {
115 super( logger );
116
117 this.startDate = startDate;
118 this.endDate = endDate;
119 this.userDatePattern = userDatePattern;
120 this.repoPath = path;
121
122 try
123 {
124 revisionRegexp = new RE( PATTERN );
125 }
126 catch ( RESyntaxException ignored )
127 {
128 if ( getLogger().isErrorEnabled() )
129 {
130 getLogger().error( "Could not create regexp to parse perforce log file", ignored );
131 }
132 }
133 }
134
135 // ----------------------------------------------------------------------
136 //
137 // ----------------------------------------------------------------------
138
139 public List<ChangeSet> getModifications() throws ScmException
140 {
141
142 // Here there are one entry for each couple (changelist,file). We merge
143 // entries to have only one entry per changelist
144
145 // Date > ChangeSet
146 Map<Date,ChangeSet> groupedEntries = new LinkedHashMap<Date,ChangeSet>();
147 for ( int i = 0; i < entries.size(); i++ )
148 {
149 ChangeSet cs = (ChangeSet) entries.get( i );
150 ChangeSet hit = (ChangeSet) groupedEntries.get( cs.getDate() );
151 if ( hit != null )
152 {
153 if ( cs.getFiles().size() != 1 )
154 {
155 throw new ScmException( "Merge of entries failed. Bad entry size: " + cs.getFiles().size() );
156 }
157 hit.addFile( (ChangeFile) cs.getFiles().get( 0 ) );
158 }
159 else
160 {
161 groupedEntries.put( cs.getDate(), cs );
162 }
163 }
164
165 List<ChangeSet> result = new ArrayList<ChangeSet>();
166 result.addAll( groupedEntries.values() );
167
168 return result;
169
170 }
171
172 // ----------------------------------------------------------------------
173 // StreamConsumer Implementation
174 // ----------------------------------------------------------------------
175
176 /** {@inheritDoc} */
177 public void consumeLine( String line )
178 {
179 switch ( status )
180 {
181 case GET_REVISION:
182 processGetRevision( line );
183 break;
184 case GET_COMMENT_BEGIN:
185 status = GET_COMMENT;
186 break;
187 case GET_COMMENT:
188 processGetComment( line );
189 break;
190 default:
191 throw new IllegalStateException( "Unknown state: " + status );
192 }
193 }
194
195 // ----------------------------------------------------------------------
196 //
197 // ----------------------------------------------------------------------
198
199 /**
200 * Add a change log entry to the list (if it's not already there)
201 * with the given file.
202 *
203 * @param entry a {@link ChangeSet} to be added to the list if another
204 * with the same key (p4 change number) doesn't exist already.
205 * @param file a {@link ChangeFile} to be added to the entry
206 */
207 private void addEntry( ChangeSet entry, ChangeFile file )
208 {
209 // ----------------------------------------------------------------------
210 // Check that we are inside the requested date range
211 // ----------------------------------------------------------------------
212
213 if ( startDate != null && entry.getDate().before( startDate ) )
214 {
215 return;
216 }
217
218 if ( endDate != null && entry.getDate().after( endDate ) )
219 {
220 return;
221 }
222
223 // ----------------------------------------------------------------------
224 //
225 // ----------------------------------------------------------------------
226
227 entry.addFile( file );
228
229 entries.add( entry );
230 }
231
232 /**
233 * Most of the relevant info is on the revision line matching the
234 * 'pattern' string.
235 *
236 * @param line A line of text from the perforce log output
237 */
238 private void processGetRevision( String line )
239 {
240 if ( line.startsWith( FILE_BEGIN_TOKEN ) )
241 {
242 currentFile = line.substring( repoPath.length() + 1 );
243 return;
244 }
245
246 if ( !revisionRegexp.match( line ) )
247 {
248 return;
249 }
250
251 currentChange = new ChangeSet();
252 currentChange.setDate( parseDate( revisionRegexp.getParen( 3 ), userDatePattern, PERFORCE_TIMESTAMP_PATTERN ) );
253 currentChange.setAuthor( revisionRegexp.getParen( 4 ) );
254
255 status = GET_COMMENT_BEGIN;
256 }
257
258 /**
259 * Process the current input line in the GET_COMMENT state. This
260 * state gathers all of the comments that are part of a log entry.
261 *
262 * @param line a line of text from the perforce log output
263 */
264 private void processGetComment( String line )
265 {
266 if ( line.equals( COMMENT_DELIMITER ) )
267 {
268 addEntry( currentChange, new ChangeFile( currentFile, revisionRegexp.getParen( 1 ) ) );
269
270 status = GET_REVISION;
271 }
272 else
273 {
274 currentChange.setComment( currentChange.getComment() + line + "\n" );
275 }
276 }
277 }