001    package 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    
022    import org.apache.maven.scm.ChangeFile;
023    import org.apache.maven.scm.ChangeSet;
024    import org.apache.maven.scm.ScmFileStatus;
025    import org.apache.maven.scm.log.ScmLogger;
026    import org.apache.maven.scm.provider.ScmProviderRepository;
027    import org.apache.maven.scm.provider.jazz.command.consumer.AbstractRepositoryConsumer;
028    import org.apache.regexp.RE;
029    import org.apache.regexp.RESyntaxException;
030    
031    import java.util.ArrayList;
032    import java.util.Calendar;
033    import java.util.Date;
034    import java.util.List;
035    import 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     */
045    public 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    }