001package org.apache.maven.scm.provider.jazz.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.ScmProviderRepository;
027import org.apache.maven.scm.provider.jazz.command.consumer.AbstractRepositoryConsumer;
028import org.apache.regexp.RE;
029import org.apache.regexp.RESyntaxException;
030
031import java.util.ArrayList;
032import java.util.Calendar;
033import java.util.Date;
034import java.util.List;
035import java.util.Locale;
036
037/**
038 * Consume the output of the scm command for the "list changesets" operation.
039 * <p/>
040 * This parses the contents of the output and uses it to fill in the remaining
041 * information in the <code>entries</code> list.
042 *
043 * @author <a href="mailto:ChrisGWarp@gmail.com">Chris Graham</a>
044 */
045public class JazzListChangesetConsumer
046    extends AbstractRepositoryConsumer
047{
048//Change sets:
049//  (1589)  ---$ Deb "[maven-release-plugin] prepare for next development iteration"
050//    Component: (1158) "GPDB"
051//    Modified: Feb 25, 2012 10:15 PM (Yesterday)
052//    Changes:
053//      ---c- (1170) \GPDB\GPDBEAR\pom.xml
054//      ---c- (1171) \GPDB\GPDBResources\pom.xml
055//      ---c- (1167) \GPDB\GPDBWeb\pom.xml
056//      ---c- (1165) \GPDB\pom.xml
057//  (1585)  ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
058//    Component: (1158) "GPDB"
059//    Modified: Feb 25, 2012 10:13 PM (Yesterday)
060//    Changes:
061//      ---c- (1170) \GPDB\GPDBEAR\pom.xml
062//      ---c- (1171) \GPDB\GPDBResources\pom.xml
063//      ---c- (1167) \GPDB\GPDBWeb\pom.xml
064//      ---c- (1165) \GPDB\pom.xml
065//  (1584)  ---$ Deb "This is my first changeset (2)"
066//    Component: (1158) "GPDB"
067//    Modified: Feb 25, 2012 10:13 PM (Yesterday)
068//  (1583)  ---$ Deb "This is my first changeset (1)"
069//    Component: (1158) "GPDB"
070//    Modified: Feb 25, 2012 10:13 PM (Yesterday)
071//  (1323)  ---$ Deb <No comment>
072//    Component: (1158) "GPDB"
073//    Modified: Feb 24, 2012 11:04 PM (Last Week)
074//    Changes:
075//      ---c- (1170) \GPDB\GPDBEAR\pom.xml
076//      ---c- (1171) \GPDB\GPDBResources\pom.xml
077//      ---c- (1167) \GPDB\GPDBWeb\pom.xml
078//      ---c- (1165) \GPDB\pom.xml
079//  (1319)  ---$ Deb <No comment>
080//    Component: (1158) "GPDB"
081//    Modified: Feb 24, 2012 11:03 PM (Last Week)
082//    Changes:
083//      ---c- (1170) \GPDB\GPDBEAR\pom.xml
084//      ---c- (1171) \GPDB\GPDBResources\pom.xml
085//      ---c- (1167) \GPDB\GPDBWeb\pom.xml
086//      ---c- (1165) \GPDB\pom.xml
087//
088// NOTE: If the change sets originate on the current date, the date is not
089//       displayed, only the time is.
090// EG:
091//Change sets:
092//  (1809)  ---$ Deb "[maven-release-plugin] prepare for next development iteration"
093//    Component: (1158) "GPDB"
094//    Modified: 6:20 PM (5 minutes ago)
095//    Changes:
096//      ---c- (1170) \GPDB\GPDBEAR\pom.xml
097//      ---c- (1171) \GPDB\GPDBResources\pom.xml
098//      ---c- (1167) \GPDB\GPDBWeb\pom.xml
099//      ---c- (1165) \GPDB\pom.xml
100//  (1801)  ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.26"
101//    Component: (1158) "GPDB"
102//    Modified: 6:18 PM (10 minutes ago)
103//    Changes:
104//      ---c- (1170) \GPDB\GPDBEAR\pom.xml
105//      ---c- (1171) \GPDB\GPDBResources\pom.xml
106//      ---c- (1167) \GPDB\GPDBWeb\pom.xml
107//  (1799)  ---$ Deb <No comment>
108//    Component: (1158) "GPDB"
109//    Modified: 6:18 PM (10 minutes ago)
110//    Changes:
111//      ---c- (1165) \GPDB\pom.xml
112//  (1764)  ---$ Deb <No comment>
113//    Component: (1158) "GPDB"
114//    Modified: Mar 1, 2012 2:34 PM
115//    Changes:
116//      ---c- (1165) \GPDB\pom.xml
117
118
119    // State Machine Definitions
120    private static final int STATE_CHANGE_SETS = 0;
121
122    private static final int STATE_CHANGE_SET = 1;
123
124    private static final int STATE_COMPONENT = 2;
125
126    private static final int STATE_MODIFIED = 3;
127
128    private static final int STATE_CHANGES = 4;
129
130    // Header definitions. 
131    private static final String HEADER_CHANGE_SETS = "Change sets:";
132
133    private static final String HEADER_CHANGE_SET = "(";
134
135    private static final String HEADER_COMPONENT = "Component:";
136
137    private static final String HEADER_MODIFIED = "Modified:";
138
139    private static final String HEADER_CHANGES = "Changes:";
140
141    private static final String JAZZ_TIMESTAMP_PATTERN = "MMM d, yyyy h:mm a";
142    // Actually: DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.SHORT );
143
144    private static final String JAZZ_TIMESTAMP_PATTERN_TIME = "h:mm a";
145    // Only seen when the data = today. Only the time is displayed.
146
147    //  (1589)  ---$ Deb "[maven-release-plugin] prepare for next development iteration"
148    //  (1585)  ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
149    private static final String CHANGESET_PATTERN = "\\((\\d+)\\)  (....) (\\w+) (.*)";
150
151    /**
152     * @see #CHANGESET_PATTERN
153     */
154    private RE changeSetRegExp;
155
156    //      ---c- (1170) \GPDB\GPDBEAR\pom.xml
157    //      ---c- (1171) \GPDB\GPDBResources\pom.xml
158    //      ---c- (1167) \GPDB\GPDBWeb\pom.xml
159    //      ---c- (1165) \GPDB\pom.xml
160    private static final String CHANGES_PATTERN = "(.....) \\((\\d+)\\) (.*)";
161
162    /**
163     * @see #CHANGES_PATTERN
164     */
165    private RE changesRegExp;
166
167
168    private List<ChangeSet> entries;
169
170    private final String userDateFormat;
171
172    // This is incremented at the beginning of every change set line. So we start at -1 (to get zero on first processing)
173    private int currentChangeSetIndex = -1;
174
175    private int currentState = STATE_CHANGE_SETS;
176
177    /**
178     * Constructor for our "scm list changeset" consumer.
179     *
180     * @param repo    The JazzScmProviderRepository being used.
181     * @param logger  The ScmLogger to use.
182     * @param entries The List of ChangeSet entries that we will populate.
183     */
184    public JazzListChangesetConsumer( ScmProviderRepository repo, ScmLogger logger, List<ChangeSet> entries,
185                                      String userDateFormat )
186    {
187        super( repo, logger );
188        this.entries = entries;
189        this.userDateFormat = userDateFormat;
190
191        try
192        {
193            changeSetRegExp = new RE( CHANGESET_PATTERN );
194            changesRegExp = new RE( CHANGES_PATTERN );
195        }
196        catch ( RESyntaxException ex )
197        {
198            throw new RuntimeException(
199                "INTERNAL ERROR: Could not create regexp to parse jazz scm history output. This shouldn't happen. Something is probably wrong with the oro installation.",
200                ex );
201        }
202    }
203
204    /**
205     * Process one line of output from the execution of the "scm list changeset" command.
206     *
207     * @param line The line of output from the external command that has been pumped to us.
208     * @see org.codehaus.plexus.util.cli.StreamConsumer#consumeLine(java.lang.String)
209     */
210    public void consumeLine( String line )
211    {
212        super.consumeLine( line );
213
214        // Process the "Change sets:" line - do nothing
215        if ( line.trim().startsWith( HEADER_CHANGE_SETS ) )
216        {
217            currentState = STATE_CHANGE_SETS;
218        }
219        else
220        {
221            if ( line.trim().startsWith( HEADER_CHANGE_SET ) )
222            {
223                currentState = STATE_CHANGE_SET;
224            }
225            else
226            {
227                if ( line.trim().startsWith( HEADER_COMPONENT ) )
228                {
229                    currentState = STATE_COMPONENT;
230                }
231                else
232                {
233                    if ( line.trim().startsWith( HEADER_MODIFIED ) )
234                    {
235                        currentState = STATE_MODIFIED;
236                    }
237                    else
238                    {
239                        if ( line.trim().startsWith( HEADER_CHANGES ) )
240                        {
241                            // Note: processChangesLine() will also be passed the "Changes:" line
242                            // So, it needs to be able to deal with that.
243                            // Changes:
244                            //   ---c- (1170) \GPDB\GPDBEAR\pom.xml
245                            //   ---c- (1171) \GPDB\GPDBResources\pom.xml
246                            //   ---c- (1167) \GPDB\GPDBWeb\pom.xml
247                            //   ---c- (1165) \GPDB\pom.xml
248                            currentState = STATE_CHANGES;
249                        }
250                    }
251                }
252            }
253        }
254
255        switch ( currentState )
256        {
257            case STATE_CHANGE_SETS:
258                // Nothing to do.
259                break;
260
261            case STATE_CHANGE_SET:
262                processChangeSetLine( line );
263                break;
264
265            case STATE_COMPONENT:
266                // Nothing to do. Not used (Yet?)
267                break;
268
269            case STATE_MODIFIED:
270                processModifiedLine( line );
271                break;
272
273            case STATE_CHANGES:
274                processChangesLine( line );
275                break;
276        }
277
278    }
279
280    private void processChangeSetLine( String line )
281    {
282        // Process the headerless change set line - starts with a '(', eg:
283        // (1589)  ---$ Deb "[maven-release-plugin] prepare for next development iteration"
284        // (1585)  ---$ Deb "[maven-release-plugin] prepare release GPDB-1.0.21"
285        if ( changeSetRegExp.match( line ) )
286        {
287            // This is the only place this gets incremented.
288            // It starts at -1, and on first execution is incremented to 0 - which is correct.
289            currentChangeSetIndex++;
290            ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
291
292            // Init the file of files, so it is not null, but it can be empty!
293            List<ChangeFile> files = new ArrayList<ChangeFile>();
294            currentChangeSet.setFiles( files );
295
296            String changesetAlias = changeSetRegExp.getParen( 1 );
297            String changeFlags = changeSetRegExp.getParen( 2 );     // Not used.
298            String author = changeSetRegExp.getParen( 3 );
299            String comment = changeSetRegExp.getParen( 4 );
300
301            if ( getLogger().isDebugEnabled() )
302            {
303                getLogger().debug( "  Parsing ChangeSet Line : " + line );
304                getLogger().debug( "    changesetAlias : " + changesetAlias );
305                getLogger().debug( "    changeFlags    : " + changeFlags );
306                getLogger().debug( "    author         : " + author );
307                getLogger().debug( "    comment        : " + comment );
308            }
309
310            // Sanity check.
311            if ( currentChangeSet.getRevision() != null && !currentChangeSet.getRevision().equals( changesetAlias ) )
312            {
313                getLogger().warn( "Warning! The indexes appear to be out of sequence! " +
314                                      "For currentChangeSetIndex = " + currentChangeSetIndex + ", we got '" +
315                                      changesetAlias + "' and not '" + currentChangeSet.getRevision()
316                                      + "' as expected." );
317            }
318
319            comment = stripDelimiters( comment );
320            currentChangeSet.setAuthor( author );
321            currentChangeSet.setComment( comment );
322        }
323    }
324
325    private void processModifiedLine( String line )
326    {
327        // Process the "Modified: ..." line, eg:
328        // Modified: Feb 25, 2012 10:15 PM (Yesterday)
329        // Modified: Feb 25, 2012 10:13 PM (Yesterday)
330        // Modified: Feb 24, 2012 11:03 PM (Last Week)
331        // Modified: Mar 1, 2012 2:34 PM
332        // Modified: 6:20 PM (5 minutes ago)
333
334        if ( getLogger().isDebugEnabled() )
335        {
336            getLogger().debug( "  Parsing Modified Line : " + line );
337        }
338
339        int colonPos = line.indexOf( ":" );
340        int parenPos = line.indexOf( "(" );
341
342        String date = null;
343
344        if ( colonPos != -1 && parenPos != -1 )
345        {
346            date = line.substring( colonPos + 2, parenPos - 1 );
347        }
348        else
349        {
350            if ( colonPos != -1 && parenPos == -1 )
351            {
352                // No trailing bracket
353                date = line.substring( colonPos + 2 );
354            }
355        }
356
357        if ( date != null )
358        {
359            Date changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN );
360            // try again forcing en locale
361            if ( changesetDate == null )
362            {
363                changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN, Locale.ENGLISH );
364            }
365            if ( changesetDate == null )
366            {
367                // changesetDate will be null when the date is not given, it only has just the time. The date is today.
368                changesetDate = parseDate( date.toString(), userDateFormat, JAZZ_TIMESTAMP_PATTERN_TIME );
369                // Get today's time/date. Used to get the date.
370                Calendar today = Calendar.getInstance();
371                // Get a working one.
372                Calendar changesetCal = Calendar.getInstance();
373                // Set the date/time. Used to set the time.
374                changesetCal.setTimeInMillis( changesetDate.getTime() );
375                // Now set the date (today).
376                changesetCal.set( today.get( Calendar.YEAR ), today.get( Calendar.MONTH ),
377                                  today.get( Calendar.DAY_OF_MONTH ) );
378                // Now get the date of the combined results.
379                changesetDate = changesetCal.getTime();
380            }
381
382            if ( getLogger().isDebugEnabled() )
383            {
384                getLogger().debug( "    date           : " + date );
385                getLogger().debug( "    changesetDate  : " + changesetDate );
386            }
387
388            ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
389            currentChangeSet.setDate( changesetDate );
390        }
391    }
392
393    private void processChangesLine( String line )
394    {
395        // Process the changes line, eg:
396        //      ---c- (1170) \GPDB\GPDBEAR\pom.xml
397        //      ---c- (1171) \GPDB\GPDBResources\pom.xml
398        //      ---c- (1167) \GPDB\GPDBWeb\pom.xml
399        //      ---c- (1165) \GPDB\pom.xml
400        if ( changesRegExp.match( line ) )
401        {
402            ChangeSet currentChangeSet = entries.get( currentChangeSetIndex );
403
404            String changeFlags = changesRegExp.getParen( 1 );     // Not used.
405            String fileAlias = changesRegExp.getParen( 2 );
406            String file = changesRegExp.getParen( 3 );
407
408            if ( getLogger().isDebugEnabled() )
409            {
410                getLogger().debug( "  Parsing Changes Line : " + line );
411                getLogger().debug(
412                    "    changeFlags    : " + changeFlags + " Translated to : " + parseFileChangeState( changeFlags ) );
413                getLogger().debug( "    filetAlias     : " + fileAlias );
414                getLogger().debug( "    file           : " + file );
415            }
416
417            ChangeFile changeFile = new ChangeFile( file );
418            ScmFileStatus status = parseFileChangeState( changeFlags );
419            changeFile.setAction( status );
420            currentChangeSet.getFiles().add( changeFile );
421        }
422    }
423
424    /**
425     * String the leading/trailing ", < and > from the text.
426     *
427     * @param text The text to process.
428     * @return The striped text.
429     */
430    protected String stripDelimiters( String text )
431    {
432        if ( text == null )
433        {
434            return null;
435        }
436        String workingText = text;
437        if ( workingText.startsWith( "\"" ) || workingText.startsWith( "<" ) )
438        {
439            workingText = workingText.substring( 1 );
440        }
441        if ( workingText.endsWith( "\"" ) || workingText.endsWith( ">" ) )
442        {
443            workingText = workingText.substring( 0, workingText.length() - 1 );
444        }
445
446        return workingText;
447    }
448
449    /**
450     * Parse the change state file flags from Jazz and map them to the maven SCM ones.
451     * <p/>
452     * "----" Character positions 0-3.
453     * <p/>
454     * [0] is '*' or '-'    Indicates that this is the current change set ('*') or not ('-').   STATE_CHANGESET_CURRENT
455     * [1] is '!' or '-'    Indicates a Potential Conflict ('!') or not ('-').                  STATE_POTENTIAL_CONFLICT
456     * [2] is '#' or '-'    Indicates a Conflict ('#') or not ('-').                            STATE_CONFLICT
457     * [3] is '@' or '$'    Indicates whether the changeset is active ('@') or not ('$').       STATE_CHANGESET_ACTIVE
458     *
459     * @param state The 5 character long state string
460     * @return The ScmFileStatus value.
461     */
462    private ScmFileStatus parseChangeSetChangeState( String state )
463    {
464        if ( state.length() != 4 )
465        {
466            throw new IllegalArgumentException( "Change State string must be 4 chars long!" );
467        }
468
469        // This is not used, but is here for potential future usage and for documentation purposes.
470        return ScmFileStatus.UNKNOWN;
471    }
472
473    /**
474     * Parse the change state file flags from Jazz and map them to the maven SCM ones.
475     * <p/>
476     * "-----" Character positions 0-4. The default is '-'.
477     * <p/>
478     * [0] is '-' or '!'    Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT
479     * [1] is '-' or '#'    Indicates a Conflict.           STATE_CONFLICT
480     * [2] is '-' or 'a'    Indicates an addition.          STATE_ADD
481     * or 'd'    Indicates a deletion.           STATE_DELETE
482     * or 'm'    Indicates a move.               STATE_MOVE
483     * [3] is '-' or 'c'    Indicates a content change.     STATE_CONTENT_CHANGE
484     * [4] is '-' or 'p'    Indicates a property change.    STATE_PROPERTY_CHANGE
485     * <p/>
486     * NOTE: [3] and [4] can only be set it [2] is NOT 'a' or 'd'.
487     *
488     * @param state The 5 character long state string
489     * @return The SCMxxx value.
490     */
491    private ScmFileStatus parseFileChangeState( String state )
492    {
493        if ( state.length() != 5 )
494        {
495            throw new IllegalArgumentException( "Change State string must be 5 chars long!" );
496        }
497
498        // NOTE: We have an impedance mismatch here. The Jazz file change flags represent
499        // many different states. However, we can only return *ONE* ScmFileStatus value,
500        // so we need to be careful as to the precedence that we give to them.
501
502        ScmFileStatus status = ScmFileStatus.UNKNOWN;   // Probably not a valid initial default value.
503
504        // [0] is '-' or '!'    Indicates a Potential Conflict. STATE_POTENTIAL_CONFLICT
505        if ( state.charAt( 0 ) == '!' )
506        {
507            status = ScmFileStatus.CONFLICT;
508        }
509        // [1] is '-' or '#'    Indicates a Conflict.           STATE_CONFLICT
510        if ( state.charAt( 1 ) == '#' )
511        {
512            status = ScmFileStatus.CONFLICT;
513        }
514
515        // [2] is '-' or 'a'    Indicates an addition.          STATE_ADD
516        //            or 'd'    Indicates a deletion.           STATE_DELETE
517        //            or 'm'    Indicates a move.               STATE_MOVE
518        if ( state.charAt( 2 ) == 'a' )
519        {
520            status = ScmFileStatus.ADDED;
521        }
522        else
523        {
524            if ( state.charAt( 2 ) == 'd' )
525            {
526                status = ScmFileStatus.DELETED;
527            }
528            else
529            {
530                if ( state.charAt( 2 ) == 'm' )
531                {
532                    status = ScmFileStatus.RENAMED;     // Has been renamed or moved.
533                }
534
535                // [3] is '-' or 'c'    Indicates a content change.     STATE_CONTENT_CHANGE
536                if ( state.charAt( 3 ) == 'c' )
537                {
538                    status = ScmFileStatus.MODIFIED;    // The file has been modified in the working tree.
539                }
540
541                // [4] is '-' or 'p'    Indicates a property change.    STATE_PROPERTY_CHANGE
542                if ( state.charAt( 4 ) == 'p' )
543                {
544                    status =
545                        ScmFileStatus.MODIFIED;    // ScmFileStatus has no concept of property or meta data changes.
546                }
547            }
548        }
549
550        return status;
551    }
552}