001package org.apache.maven.scm.provider.jazz.command.status;
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.ScmFile;
023import org.apache.maven.scm.ScmFileStatus;
024import org.apache.maven.scm.log.ScmLogger;
025import org.apache.maven.scm.provider.ScmProviderRepository;
026import org.apache.maven.scm.provider.jazz.command.consumer.AbstractRepositoryConsumer;
027import org.apache.maven.scm.provider.jazz.repository.JazzScmProviderRepository;
028
029import java.util.ArrayList;
030import java.util.List;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034/**
035 * Consume the output of the scm command for the "status" operation.
036 * <p>
037 * It is normally just used to build up a list of ScmFile objects that have
038 * their ScmFileStatus set.
039 * This class has been expanded so that the Workspace, Component and Baseline
040 * are also collected and set back in the JazzScmProviderRepository.
041 * The Workspace and Component names are needed for some other commands (list,
042 * for example), so we can easily get this information here.
043 * <p>
044 * As this class has expanded over time, it has become more and more of a state
045 * machine, one that needs to parse the output of the "scm status --wide" command.
046 * If there are any issues with this provider, I would suggest this is a good
047 * place to start.
048 * 
049 * @author <a href="mailto:ChrisGWarp@gmail.com">Chris Graham</a>
050 */
051public class JazzStatusConsumer
052    extends AbstractRepositoryConsumer
053{
054// We have have a workspace with no flow targets (it points to itself)
055//
056//  Workspace: (1000) "BogusRepositoryWorkspace" <-> (1000) "BogusRepositoryWorkspace"
057//    Component: (1001) "BogusComponent"
058//      Baseline: (1128) 27 "BogusTestJazz-3.0.0.40"
059//      Unresolved:
060//        d-- /BogusTest/pom.xml.releaseBackup
061//        d-- /BogusTest/release.properties
062//
063// Or, we have have one that does have a flow target (ie a stream or another workspace).
064//
065//  Workspace: (1156) "GPDBWorkspace" <-> (1157) "GPDBStream"
066//    Component: (1158) "GPDB" <-> (1157) "GPDBStream"
067//      Baseline: (1159) 1 "Initial Baseline"
068//
069// Note the (%d) numbers are aliases and are only valid for the machine/instance that made the
070// remote calls to the server. They are not to be shared across machines (ie don't make them global, public
071// or persistent).
072//
073// We can also have a changeset with a work item associated with it:
074//
075//  Workspace: (1156) "GPDBWorkspace" <-> (1157) "GPDBStream"
076//    Component: (1158) "GPDB"
077//      Baseline: (2362) 48 "GPDB-1.0.50"
078//        Outgoing:
079//          Change sets:
080//            (2366) *--@  62 "Release the next release of GPDB." - "Man Created Changeset: X.Y.Z" 28-Apr-2015 07:55 PM
081//
082// Or not:
083//
084//  Workspace: (1156) "GPDBWorkspace" <-> (1157) "GPDBStream"
085//    Component: (1158) "GPDB"
086//      Baseline: (2362) 48 "GPDB-1.0.50"
087//        Outgoing:
088//          Change sets:
089//            (2365) ---@  "This is my changeset comment." 26-Apr-2015 09:36 PM
090//
091// We can also have a multiple changesets. These will be seen when a JBE is used to perform
092// the release and has been instructed to create a baseline prior to starting the build.
093// Multiple changesets will also be seen when a maven release process fails (for whatever reason).
094//
095//  Workspace: (1156) "GPDBWorkspace" <-> (1157) "GPDBStream"
096//    Component: (1158) "GPDB"
097//      Baseline: (2362) 48 "GPDB-1.0.50"
098//        Outgoing:
099//          Change sets:
100//            (2366) *--@  62 "Release the next release of GPDB." - "Man Created Changeset: X.Y.Z" 28-Apr-2015 07:55 PM
101//            (2365) ---@  "This is my changeset comment." 26-Apr-2015 09:36 PM
102//
103// We can also have Baselines, of which there may be more than one (especially true if an update (accept changes)
104// has not been done in a while.
105//
106// So the most complete/complex example I can find is something like this:
107//
108//  Workspace: (1756) "Scott's GPDBWorkspace" <-> (1157) "GPDBStream"
109//    Component: (1158) "GPDB"
110//      Baseline: (1718) 25 "GPDB-1.0.25"
111//      Unresolved:
112//        -c- /GPDB/pom.xml
113//      Outgoing:
114//        Change sets:
115//          (2389) *--@  "<No comment>" 23-May-2015 07:09 PM
116//      Incoming:
117//        Change sets:
118//          (2385) ---$ Deb 62 "Release the next release of GPDB." - \
119//             + "[maven-release-plugin] prepare for next development itera..." 02-May-2015 11:01 PM
120//      Baselines:
121//        (2386) 52 "GPDB-1.0.53"
122//        (2387) 51 "GPDB-1.0.52"
123//        (2388) 50 "GPDB-1.0.51"
124//        (2369) 49 "GPDB-MAN-1.0.50"
125//        (2362) 48 "GPDB-1.0.50"
126//        (2357) 47 "GPDB-1.0.49"
127//        (2352) 46 "GPDB-1.0.48"
128//        (2347) 45 "GPDB-1.0.47"
129//        (2292) 44 "GPDB-1.0.46"
130//        (2285) 42 "GPDB-1.0.42"
131//        (2276) 41 "GPDB-1.0.41"
132//        (2259) 40 "GPDB-1.0.40"
133//        (2250) 39 "GPDB-1.0.39"
134//        (2241) 38 "GPDB-1.0.38"
135//        (2232) 37 "GPDB-1.0.37"
136//        (2222) 36 "GPDB-1.0.36"
137//        (2212) 35 "GPDB-1.0.35"
138//        (2202) 34 "GPDB-1.0.34"
139//        (2191) 33 "GPDB-1.0.33"
140//        (2181) 32 "GPDB-1.0.32"
141//        (2171) 31 "GPDB-1.0.31"
142//        (2160) 30 "GPDB-1.0.30"
143//        (2147) 29 "GPDB-1.0.29"
144//        (2079) 28 "GPDB-1.0.28"
145//        (1851) 27 "GPDB-1.0.27"
146//        (1807) 26 "GPDB-1.0.26"
147//
148// Because the "Change sets:" line exists by itself, and it is followed by the changeset
149// lines, we need to implement a state machine... (seenIncomingChangeSets and seenOutgoingChangeSets)
150//
151// We can also have collisions:
152//
153//  Workspace: (8551) "myNewWorkspace" <-> (8552) "stream19_test_max_results_1256765247692134"
154//    Component: (8553) "Flux Capacitor"
155//      Baseline: (8554) 1 "Initial Baseline"
156//      Outgoing:
157//        Change sets:
158//          (8617) -#@ "Update from November planning meeting"
159//            Changes:
160//              -#-c /flux.capacitor/requirements.txt
161//      Incoming:
162//        Change sets:
163//          (8616) -#$ "Results of initial trials"
164//            Changes:
165//              -#-c /flux.capacitor/requirements.txt
166
167    //  Workspace: (1000) "BogusRepositoryWorkspace" <-> (1000) "BogusRepositoryWorkspace"
168    //  Workspace: (1156) "GPDBWorkspace" <-> (1157) "GPDBStream"
169    private static final Pattern WORKSPACE_PATTERN =
170        Pattern.compile( "\\((\\d+)\\) \"(.*)\" <-> \\((\\d+)\\) \"(.*)\"" );
171
172    //  Component: (1001) "BogusComponent"
173    private static final Pattern COMPONENT_PATTERN1 = Pattern.compile( "\\((\\d+)\\) \"(.*)\"" );
174
175    //  Component: (1158) "GPDB" <-> (1157) "GPDBStream"
176    //  Component: (1002) "FireDragon" <-> (1005) "MavenR3Stream Workspace" (outgoing addition)
177    private static final Pattern COMPONENT_PATTERN2 = Pattern.compile( "\\((\\d+)\\) \"(.*)\" <.*>" );
178
179    //  Baseline: (1128) 27 "BogusTestJazz-3.0.0.40"
180    private static final Pattern BASELINE_PATTERN = Pattern.compile( "\\((\\d+)\\) (\\d+) \"(.*)\"" );
181
182    // (2365) ---@  "This is my changeset comment." 26-Apr-2015 09:36 PM
183    private static final Pattern CHANGESET_PATTERN = Pattern.compile( "\\((\\d+)\\) (.*)" );
184
185    //
186    // Additional data we collect. (eye catchers)
187    //
188    
189    /**
190     * The "Status" command output line that contains the "Workspace" name.
191     */
192    public static final String STATUS_CMD_WORKSPACE = "Workspace:";
193
194    /**
195     * The "Status" command output line that contains the "Component" name.
196     */
197    public static final String STATUS_CMD_COMPONENT = "Component:";
198
199    /**
200     * The "Status" command output line that contains the "Baseline" name.
201     */
202    public static final String STATUS_CMD_BASELINE = "Baseline:";
203
204    /**
205     * The "Status" command output line that contains the "Outgoing" eye catcher.
206     */
207    public static final String STATUS_CMD_OUTGOING = "Outgoing:";
208
209    /**
210     * The "Status" command output line that contains the "Incoming" eye catcher.
211     */
212    public static final String STATUS_CMD_INCOMING = "Incoming:";
213
214    /**
215     * The "Status" command output line that contains the line "Change sets:".
216     * This will be followed by the change set lines themselves. 
217     */
218    public static final String STATUS_CMD_CHANGE_SETS = "Change sets:";
219
220    /**
221     * The "Status" command output line that contains the "Baselines" eye catcher.
222     */
223    public static final String STATUS_CMD_BASELINES = "Baselines:";
224    
225    // File Status Commands (eye catchers)
226
227    /**
228     * The "Status" command status flag for a resource that has been added.
229     */
230    public static final String STATUS_CMD_ADD_FLAG = "a-";
231
232    /**
233     * The "Status" command status flag for when the content or properties of
234     * a file have been modified, or the properties of a directory have changed.
235     */
236    public static final String STATUS_CMD_CHANGE_FLAG = "-c";
237
238    /**
239     * The "Status" command status flag for a resource that has been deleted.
240     */
241    public static final String STATUS_CMD_DELETE_FLAG = "d-";
242
243    /**
244     * The "Status" command status flag for a resource that has been renamed or moved.
245     */
246    public static final String STATUS_CMD_MOVED_FLAG = "m-";
247
248    /**
249     * A List of ScmFile objects that have their ScmFileStatus set.
250     */
251    private List<ScmFile> fChangedFiles = new ArrayList<ScmFile>();
252
253    /**
254     * Implement a simple state machine: Have we seen the "Change sets:" (outgoing) line or not?
255     */
256    private boolean seenOutgoingChangeSets = false;
257
258    /**
259     * Implement a simple state machine: Have we seen the "Change sets:" (incoming) line or not?
260     */
261    private boolean seenIncomingChangeSets = false;
262
263    /**
264     * Constructor for our "scm status" consumer.
265     *
266     * @param repo   The JazzScmProviderRepository being used.
267     * @param logger The ScmLogger to use.
268     */
269    public JazzStatusConsumer( ScmProviderRepository repo, ScmLogger logger )
270    {
271        super( repo, logger );
272    }
273
274    /**
275     * Process one line of output from the execution of the "scm status" command.
276     *
277     * @param line The line of output from the external command that has been pumped to us.
278     * @see org.codehaus.plexus.util.cli.StreamConsumer#consumeLine(java.lang.String)
279     */
280    public void consumeLine( String line )
281    {
282        super.consumeLine( line );
283        if ( containsWorkspace( line ) )
284        {
285            extractWorkspace( line );
286        }
287        if ( containsComponent( line ) )
288        {
289            extractComponent( line );
290        }
291        if ( containsBaseline( line ) )
292        {
293            extractBaseline( line );
294        }
295        if ( containsStatusFlag( line ) )
296        {
297            extractChangedFile( line );
298        }
299        if ( containsOutgoing( line ) )
300        {
301            // Now looking for outgoing, not incoming
302            seenOutgoingChangeSets = true;
303            seenIncomingChangeSets = false;
304        }
305        if ( containsIncoming( line ) )
306        {
307            // Now looking for incoming, not outgoing
308            seenOutgoingChangeSets = false;
309            seenIncomingChangeSets = true;
310        }
311        if ( containsBaselines( line ) )
312        {
313            // Got to baselines, stop looking for all changesets
314            seenOutgoingChangeSets = false;
315            seenIncomingChangeSets = false;
316        }
317        if ( seenOutgoingChangeSets )
318        {
319            Integer changeSetAlias = extractChangeSetAlias( line );
320            if ( changeSetAlias != null )
321            {
322                // We are now supporting multiple change sets, as this allows
323                // us to cater for multiple changeset caused by previous failed
324                // release attempts.
325                // Our starting point should always be a clean slate of a workspace
326                // or sandbox, however, if something fails, then we will have some
327                // changesets already created, so we need to be able to deal with them effectively.
328                JazzScmProviderRepository jazzRepository = (JazzScmProviderRepository) getRepository();
329                jazzRepository.getOutgoingChangeSetAliases().add( new Integer( changeSetAlias ) );
330            }
331        }
332        if ( seenIncomingChangeSets )
333        {
334            Integer changeSetAlias = extractChangeSetAlias( line );
335            if ( changeSetAlias != null )
336            {
337                // We are now supporting multiple change sets, as this allows
338                // us to cater for multiple changeset caused by previous failed
339                // release attempts.
340                // Our starting point should always be a clean slate of a workspace
341                // or sandbox, however, if something fails, then we will have some
342                // changesets already created, so we need to be able to deal with them effectively.
343                JazzScmProviderRepository jazzRepository = (JazzScmProviderRepository) getRepository();
344                jazzRepository.getIncomingChangeSetAliases().add( new Integer( changeSetAlias ) );
345            }
346        }
347    }
348
349    private boolean containsWorkspace( String line )
350    {
351        return line.trim().startsWith( STATUS_CMD_WORKSPACE );
352    }
353
354    private void extractWorkspace( String line )
355    {
356        // With no stream (flow target):
357        //   Workspace: (1000) "BogusRepositoryWorkspace" <-> (1000) "BogusRepositoryWorkspace"
358        // With a stream:
359        //   Workspace: (1156) "GPDBWorkspace" <-> (1157) "GPDBStream"
360
361        Matcher matcher = WORKSPACE_PATTERN.matcher( line );
362        if ( matcher.find() )
363        {
364            JazzScmProviderRepository jazzRepository = (JazzScmProviderRepository) getRepository();
365
366            int workspaceAlias = Integer.parseInt( matcher.group( 1 ) );
367            String workspace = matcher.group( 2 );
368            int streamAlias = Integer.parseInt( matcher.group( 3 ) );
369            String stream = matcher.group( 4 );
370            if ( getLogger().isDebugEnabled() )
371            {
372                getLogger().debug( "Successfully parsed \"Workspace:\" line:" );
373                getLogger().debug( "  workspaceAlias = " + workspaceAlias );
374                getLogger().debug( "  workspace      = " + workspace );
375                getLogger().debug( "  streamAlias    = " + streamAlias );
376                getLogger().debug( "  stream         = " + stream );
377            }
378            jazzRepository.setWorkspaceAlias( workspaceAlias );
379            jazzRepository.setWorkspace( workspace );
380            jazzRepository.setFlowTargetAlias( streamAlias );
381            jazzRepository.setFlowTarget( stream );
382        }
383    }
384
385    private boolean containsComponent( String line )
386    {
387        return line.trim().startsWith( STATUS_CMD_COMPONENT );
388    }
389
390    private void extractComponent( String line )
391    {
392        // With no stream (flow target):
393        //     Component: (1001) "BogusComponent"
394        // With a stream:
395        //     Component: (1158) "GPDB" <-> (1157) "GPDBStream"
396        // With some additional information:
397        //     Component: (1002) "FireDragon" <-> (1005) "MavenR3Stream Workspace" (outgoing addition)
398
399        Matcher matcher = COMPONENT_PATTERN1.matcher( line );
400        if ( matcher.find() )
401        {
402            //     Component: (1001) "BogusComponent"
403            JazzScmProviderRepository jazzRepository = (JazzScmProviderRepository) getRepository();
404            int componentAlias = Integer.parseInt( matcher.group( 1 ) );
405            String component = matcher.group( 2 );
406            if ( getLogger().isDebugEnabled() )
407            {
408                getLogger().debug( "Successfully parsed \"Component:\" line:" );
409                getLogger().debug( "  componentAlias = " + componentAlias );
410                getLogger().debug( "  component      = " + component );
411            }
412            jazzRepository.setComponent( component );
413        }
414
415        matcher = COMPONENT_PATTERN2.matcher( line );
416        if ( matcher.find() )
417        {
418            //     Component: (1158) "GPDB" <-> (1157) "GPDBStream"
419            JazzScmProviderRepository jazzRepository = (JazzScmProviderRepository) getRepository();
420            int componentAlias = Integer.parseInt( matcher.group( 1 ) );
421            String component = matcher.group( 2 );
422            if ( getLogger().isDebugEnabled() )
423            {
424                getLogger().debug( "Successfully parsed \"Component:\" line:" );
425                getLogger().debug( "  componentAlias = " + componentAlias );
426                getLogger().debug( "  component      = " + component );
427            }
428            jazzRepository.setComponent( component );
429        }
430    }
431
432    private boolean containsBaseline( String line )
433    {
434        return line.trim().startsWith( STATUS_CMD_BASELINE );
435    }
436
437    private void extractBaseline( String line )
438    {
439        // Baseline: (1128) 27 "BogusTestJazz-3.0.0.40"
440
441        Matcher matcher = BASELINE_PATTERN.matcher( line );
442        if ( matcher.find() )
443        {
444            JazzScmProviderRepository jazzRepository = (JazzScmProviderRepository) getRepository();
445
446            int baselineAlias = Integer.parseInt( matcher.group( 1 ) );
447            int baselineId = Integer.parseInt( matcher.group( 2 ) );
448            String baseline = matcher.group( 3 );
449            if ( getLogger().isDebugEnabled() )
450            {
451                getLogger().debug( "Successfully parsed \"Baseline:\" line:" );
452                getLogger().debug( "  baselineAlias  = " + baselineAlias );
453                getLogger().debug( "  baselineId     = " + baselineId );
454                getLogger().debug( "  baseline       = " + baseline );
455            }
456            jazzRepository.setBaseline( baseline );
457        }
458    }
459
460    private boolean containsStatusFlag( String line )
461    {
462        boolean containsStatusFlag = false;
463
464        if ( line.trim().length() > 2 )
465        {
466            String flag = line.trim().substring( 0, 2 );
467            if ( STATUS_CMD_ADD_FLAG.equals( flag ) || STATUS_CMD_CHANGE_FLAG.equals( flag )
468                || STATUS_CMD_DELETE_FLAG.equals( flag ) )
469            {
470                containsStatusFlag = true;
471            }
472        }
473        return containsStatusFlag;
474    }
475
476    private void extractChangedFile( String line )
477    {
478        String flag = line.trim().substring( 0, 2 );
479        String filePath = line.trim().substring( 3 ).trim();
480        ScmFileStatus status = ScmFileStatus.UNKNOWN;
481
482        if ( STATUS_CMD_ADD_FLAG.equals( flag ) )
483        {
484            status = ScmFileStatus.ADDED;
485        }
486
487        if ( STATUS_CMD_CHANGE_FLAG.equals( flag ) )
488        {
489            status = ScmFileStatus.MODIFIED;
490        }
491
492        if ( STATUS_CMD_DELETE_FLAG.equals( flag ) )
493        {
494            status = ScmFileStatus.DELETED;
495        }
496
497        if ( getLogger().isDebugEnabled() )
498        {
499            getLogger().debug( " Extracted filePath  : '" + filePath + "'" );
500            getLogger().debug( " Extracted     flag  : '" + flag + "'" );
501            getLogger().debug( " Extracted   status  : '" + status + "'" );
502        }
503
504        fChangedFiles.add( new ScmFile( filePath, status ) );
505    }
506
507    public List<ScmFile> getChangedFiles()
508    {
509        return fChangedFiles;
510    }
511
512    private boolean containsOutgoing( String line )
513    {
514        return line.trim().startsWith( STATUS_CMD_OUTGOING );
515    }
516
517    private boolean containsIncoming( String line )
518    {
519        return line.trim().startsWith( STATUS_CMD_INCOMING );
520    }
521
522    private boolean containsBaselines( String line )
523    {
524        return line.trim().startsWith( STATUS_CMD_BASELINES );
525    }
526
527    /**
528     * Extract and return an Integer of a change set alias, from both
529     * incoming and outgoing changesets.
530     * @param line The line to extract the change sets from.
531     * @return A parsed Integer value, or null if not able to parse.
532     */
533    private Integer extractChangeSetAlias( String line )
534    {
535        // (2365) ---@  "This is my changeset comment." 26-Apr-2015 09:36 PM
536
537        Matcher matcher = CHANGESET_PATTERN.matcher( line );
538        if ( matcher.find() )
539        {
540            int changeSetAlias = Integer.parseInt( matcher.group( 1 ) );
541            if ( getLogger().isDebugEnabled() )
542            {
543                getLogger().debug( "Successfully parsed post \"Change sets:\" line:" );
544                getLogger().debug( "  changeSetAlias = " + changeSetAlias );
545            }
546            return new Integer( changeSetAlias );
547        }
548        else
549        {
550            return null;
551        }
552    }
553}