View Javadoc

1   package org.apache.maven.continuum.buildcontroller;
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 java.util.ArrayList;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.continuum.dao.BuildDefinitionDao;
28  import org.apache.continuum.dao.BuildResultDao;
29  import org.apache.continuum.dao.ProjectDao;
30  import org.apache.continuum.dao.ProjectScmRootDao;
31  import org.apache.continuum.model.project.ProjectScmRoot;
32  import org.apache.continuum.utils.ContinuumUtils;
33  import org.apache.maven.continuum.core.action.AbstractContinuumAction;
34  import org.apache.maven.continuum.core.action.ExecuteBuilderContinuumAction;
35  import org.apache.maven.continuum.execution.ContinuumBuildExecutor;
36  import org.apache.maven.continuum.execution.ContinuumBuildExecutorConstants;
37  import org.apache.maven.continuum.execution.manager.BuildExecutorManager;
38  import org.apache.maven.continuum.model.project.BuildDefinition;
39  import org.apache.maven.continuum.model.project.BuildResult;
40  import org.apache.maven.continuum.model.project.Project;
41  import org.apache.maven.continuum.model.project.ProjectDependency;
42  import org.apache.maven.continuum.model.scm.ChangeFile;
43  import org.apache.maven.continuum.model.scm.ChangeSet;
44  import org.apache.maven.continuum.model.scm.ScmResult;
45  import org.apache.maven.continuum.notification.ContinuumNotificationDispatcher;
46  import org.apache.maven.continuum.project.ContinuumProjectState;
47  import org.apache.maven.continuum.store.ContinuumObjectNotFoundException;
48  import org.apache.maven.continuum.store.ContinuumStoreException;
49  import org.apache.maven.continuum.utils.WorkingDirectoryService;
50  import org.apache.maven.scm.ScmException;
51  import org.apache.maven.scm.repository.ScmRepositoryException;
52  import org.codehaus.plexus.action.ActionManager;
53  import org.codehaus.plexus.action.ActionNotFoundException;
54  import org.codehaus.plexus.taskqueue.execution.TaskExecutionException;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  /**
59   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
60   * @version $Id: DefaultBuildController.java 793475 2009-07-13 07:37:50Z ctan $
61   * @plexus.component role="org.apache.maven.continuum.buildcontroller.BuildController" role-hint="default"
62   */
63  public class DefaultBuildController
64      implements BuildController
65  {
66      private static final Logger log = LoggerFactory.getLogger( DefaultBuildController.class );
67  
68      /**
69       * @plexus.requirement
70       */
71      private BuildDefinitionDao buildDefinitionDao;
72  
73      /**
74       * @plexus.requirement
75       */
76      private BuildResultDao buildResultDao;
77  
78      /**
79       * @plexus.requirement
80       */
81      private ProjectDao projectDao;
82  
83      /**
84       * @plexus.requirement
85       */
86      private ProjectScmRootDao projectScmRootDao;
87  
88      /**
89       * @plexus.requirement
90       */
91      private ContinuumNotificationDispatcher notifierDispatcher;
92  
93      /**
94       * @plexus.requirement
95       */
96      private ActionManager actionManager;
97  
98      /**
99       * @plexus.requirement
100      */
101     private WorkingDirectoryService workingDirectoryService;
102 
103     /**
104      * @plexus.requirement
105      */
106     private BuildExecutorManager buildExecutorManager;
107 
108     // ----------------------------------------------------------------------
109     // BuildController Implementation
110     // ----------------------------------------------------------------------
111 
112     /**
113      * @param projectId
114      * @param buildDefinitionId
115      * @param trigger
116      * @throws TaskExecutionException
117      */
118     public void build( int projectId, int buildDefinitionId, int trigger, ScmResult scmResult )
119         throws TaskExecutionException
120     {
121         log.info( "Initializing build" );
122         BuildContext context = initializeBuildContext( projectId, buildDefinitionId, trigger, scmResult );
123 
124         // ignore this if AlwaysBuild ?
125         if ( !checkScmResult( context ) )
126         {
127             log.info( "Error updating from SCM, not building" );
128             return;
129         }
130 
131         log.info( "Starting build of " + context.getProject().getName() );
132         startBuild( context );
133 
134         try
135         {
136             checkProjectDependencies( context );
137 
138             if ( !shouldBuild( context ) )
139             {
140                 return;
141             }
142 
143             Map<String, Object> actionContext = context.getActionContext();
144 
145             try
146             {
147                 performAction( "update-project-from-working-directory", context );
148             }
149             catch ( TaskExecutionException e )
150             {
151                 updateBuildResult( context, ContinuumUtils.throwableToString( e ) );
152 
153                 //just log the error but don't stop the build from progressing in order not to suppress any build result messages there
154                 log.error( "Error executing action update-project-from-working-directory '", e );
155             }
156 
157             performAction( "execute-builder", context );
158 
159             performAction( "deploy-artifact", context );
160 
161             context.setCancelled( ExecuteBuilderContinuumAction.isCancelled( actionContext ) );
162 
163             String s = AbstractContinuumAction.getBuildId( actionContext, null );
164 
165             if ( s != null && !context.isCancelled() )
166             {
167                 try
168                 {
169                     context.setBuildResult( buildResultDao.getBuildResult( Integer.valueOf( s ) ) );
170                 }
171                 catch ( NumberFormatException e )
172                 {
173                     throw new TaskExecutionException( "Internal error: build id not an integer", e );
174                 }
175                 catch ( ContinuumObjectNotFoundException e )
176                 {
177                     throw new TaskExecutionException( "Internal error: Cannot find build result", e );
178                 }
179                 catch ( ContinuumStoreException e )
180                 {
181                     throw new TaskExecutionException( "Error loading build result", e );
182                 }
183             }
184         }
185         finally
186         {
187             endBuild( context );
188         }
189     }
190 
191     /**
192      * Checks if the build should be marked as ERROR and notifies the end of the build.
193      *
194      * @param context
195      * @throws TaskExecutionException
196      */
197     private void endBuild( BuildContext context )
198         throws TaskExecutionException
199     {
200         Project project = context.getProject();
201 
202         try
203         {
204             if ( project.getState() != ContinuumProjectState.NEW &&
205                 project.getState() != ContinuumProjectState.CHECKEDOUT &&
206                 project.getState() != ContinuumProjectState.OK && project.getState() != ContinuumProjectState.FAILED &&
207                 project.getState() != ContinuumProjectState.ERROR && !context.isCancelled() )
208             {
209                 try
210                 {
211                     String s = AbstractContinuumAction.getBuildId( context.getActionContext(), null );
212 
213                     if ( s != null )
214                     {
215                         BuildResult buildResult = buildResultDao.getBuildResult( Integer.valueOf( s ) );
216                         project.setState( buildResult.getState() );
217                         projectDao.updateProject( project );
218                     }
219                 }
220                 catch ( ContinuumStoreException e )
221                 {
222                     throw new TaskExecutionException( "Error storing the project", e );
223                 }
224             }
225         }
226         finally
227         {
228             if ( !context.isCancelled() )
229             {
230                 notifierDispatcher.buildComplete( project, context.getBuildDefinition(), context.getBuildResult() );
231             }
232         }
233     }
234 
235     private void updateBuildResult( BuildContext context, String error )
236         throws TaskExecutionException
237     {
238         BuildResult build = context.getBuildResult();
239 
240         if ( build == null )
241         {
242             build = makeAndStoreBuildResult( context, error );
243         }
244         else
245         {
246             updateBuildResult( build, context );
247 
248             build.setError( error );
249 
250             try
251             {
252                 buildResultDao.updateBuildResult( build );
253 
254                 build = buildResultDao.getBuildResult( build.getId() );
255 
256                 context.setBuildResult( build );
257             }
258             catch ( ContinuumStoreException e )
259             {
260                 throw new TaskExecutionException( "Error updating build result", e );
261             }
262         }
263 
264         context.getProject().setState( build.getState() );
265 
266         try
267         {
268             projectDao.updateProject( context.getProject() );
269         }
270         catch ( ContinuumStoreException e )
271         {
272             throw new TaskExecutionException( "Error updating project", e );
273         }
274     }
275 
276     private void updateBuildResult( BuildResult build, BuildContext context )
277     {
278         if ( build.getScmResult() == null && context.getScmResult() != null )
279         {
280             build.setScmResult( context.getScmResult() );
281         }
282 
283         if ( build.getModifiedDependencies() == null && context.getModifiedDependencies() != null )
284         {
285             build.setModifiedDependencies( context.getModifiedDependencies() );
286         }
287     }
288 
289     private void startBuild( BuildContext context )
290         throws TaskExecutionException
291     {
292 
293         Project project = context.getProject();
294 
295         project.setOldState( project.getState() );
296 
297         project.setState( ContinuumProjectState.BUILDING );
298 
299         try
300         {
301             projectDao.updateProject( project );
302         }
303         catch ( ContinuumStoreException e )
304         {
305             throw new TaskExecutionException( "Error persisting project", e );
306         }
307 
308         notifierDispatcher.buildStarted( project, context.getBuildDefinition() );
309 
310     }
311 
312     /**
313      * Initializes a BuildContext for the build.
314      *
315      * @param projectId
316      * @param buildDefinitionId
317      * @param trigger
318      * @return
319      * @throws TaskExecutionException
320      */
321     protected BuildContext initializeBuildContext( int projectId, int buildDefinitionId, int trigger,
322                                                    ScmResult scmResult )
323         throws TaskExecutionException
324     {
325         BuildContext context = new BuildContext();
326 
327         context.setStartTime( System.currentTimeMillis() );
328 
329         context.setTrigger( trigger );
330 
331         try
332         {
333             Project project = projectDao.getProject( projectId );
334 
335             context.setProject( project );
336 
337             BuildDefinition buildDefinition = buildDefinitionDao.getBuildDefinition( buildDefinitionId );
338 
339             context.setBuildDefinition( buildDefinition );
340 
341             BuildResult oldBuildResult =
342                 buildResultDao.getLatestBuildResultForBuildDefinition( projectId, buildDefinitionId );
343 
344             context.setOldBuildResult( oldBuildResult );
345 
346             context.setScmResult( scmResult );
347 
348             // CONTINUUM-1871 olamy if continuum is killed during building oldBuildResult will have a endTime 0
349             // this means all changes since the project has been loaded in continuum will be in memory
350             // now we will load all BuildResult with an Id bigger or equals than the oldBuildResult one
351             //if ( oldBuildResult != null )
352             //{
353             //    context.setOldScmResult(
354             //        getOldScmResults( projectId, oldBuildResult.getBuildNumber(), oldBuildResult.getEndTime() ) );
355             //}
356         }
357         catch ( ContinuumStoreException e )
358         {
359             throw new TaskExecutionException( "Error initializing the build context", e );
360         }
361 
362         Map<String, Object> actionContext = context.getActionContext();
363 
364         AbstractContinuumAction.setProjectId( actionContext, projectId );
365 
366         AbstractContinuumAction.setProject( actionContext, context.getProject() );
367 
368         AbstractContinuumAction.setBuildDefinitionId( actionContext, buildDefinitionId );
369 
370         AbstractContinuumAction.setBuildDefinition( actionContext, context.getBuildDefinition() );
371 
372         AbstractContinuumAction.setTrigger( actionContext, trigger );
373 
374         AbstractContinuumAction.setScmResult( actionContext, context.getScmResult() );
375 
376         if ( context.getOldBuildResult() != null )
377         {
378             AbstractContinuumAction.setOldBuildId( actionContext, context.getOldBuildResult().getId() );
379         }
380 
381         return context;
382     }
383 
384     private void performAction( String actionName, BuildContext context )
385         throws TaskExecutionException
386     {
387         String error;
388         TaskExecutionException exception;
389 
390         try
391         {
392             log.info( "Performing action " + actionName );
393             actionManager.lookup( actionName ).execute( context.getActionContext() );
394             return;
395         }
396         catch ( ActionNotFoundException e )
397         {
398             error = ContinuumUtils.throwableToString( e );
399             exception = new TaskExecutionException( "Error looking up action '" + actionName + "'", e );
400         }
401         catch ( ScmRepositoryException e )
402         {
403             error = getValidationMessages( e ) + "\n" + ContinuumUtils.throwableToString( e );
404 
405             exception = new TaskExecutionException( "SCM error while executing '" + actionName + "'", e );
406         }
407         catch ( ScmException e )
408         {
409             error = ContinuumUtils.throwableToString( e );
410 
411             exception = new TaskExecutionException( "SCM error while executing '" + actionName + "'", e );
412         }
413         catch ( Exception e )
414         {
415             exception = new TaskExecutionException( "Error executing action '" + actionName + "'", e );
416             error = ContinuumUtils.throwableToString( exception );
417         }
418 
419         // TODO: clean this up. We catch the original exception from the action, and then update the buildresult
420         // for it - we need to because of the specialized error message for SCM.
421         // If updating the buildresult fails, log the previous error and throw the new one.
422         // If updating the buildresult succeeds, throw the original exception. The build result should NOT
423         // be updated again - a TaskExecutionException is final, no further action should be taken upon it.
424 
425         try
426         {
427             updateBuildResult( context, error );
428         }
429         catch ( TaskExecutionException e )
430         {
431             log.error( "Error updating build result after receiving the following exception: ", exception );
432             throw e;
433         }
434 
435         throw exception;
436     }
437 
438     protected boolean shouldBuild( BuildContext context )
439         throws TaskExecutionException
440     {
441         BuildDefinition buildDefinition = context.getBuildDefinition();
442         if ( buildDefinition.isBuildFresh() )
443         {
444             log.info( "FreshBuild configured, building" );
445             return true;
446         }
447         if ( buildDefinition.isAlwaysBuild() )
448         {
449             log.info( "AlwaysBuild configured, building" );
450             return true;
451         }
452         if ( context.getOldBuildResult() == null )
453         {
454             log.info( "The project was never be built with the current build definition, building" );
455             return true;
456         }
457 
458         Project project = context.getProject();
459 
460         //CONTINUUM-1428
461         if ( project.getOldState() == ContinuumProjectState.ERROR ||
462             context.getOldBuildResult().getState() == ContinuumProjectState.ERROR )
463         {
464             log.info( "Latest state was 'ERROR', building" );
465             return true;
466         }
467 
468         if ( context.getTrigger() == ContinuumProjectState.TRIGGER_FORCED )
469         {
470             log.info( "The project build is forced, building" );
471             return true;
472         }
473 
474         boolean shouldBuild = false;
475 
476         boolean allChangesUnknown = true;
477 
478         if ( project.getOldState() != ContinuumProjectState.NEW &&
479             project.getOldState() != ContinuumProjectState.CHECKEDOUT &&
480             context.getTrigger() != ContinuumProjectState.TRIGGER_FORCED &&
481             project.getState() != ContinuumProjectState.NEW && project.getState() != ContinuumProjectState.CHECKEDOUT )
482         {
483             // Check SCM changes
484             if ( context.getScmResult() != null )
485             {
486                 allChangesUnknown = checkAllChangesUnknown( context.getScmResult().getChanges() );
487             }
488 
489             if ( allChangesUnknown )
490             {
491                 if ( context.getScmResult() != null && !context.getScmResult().getChanges().isEmpty() )
492                 {
493                     log.info(
494                         "The project was not built because all changes are unknown (maybe local modifications or ignored files not defined in your SCM tool." );
495                 }
496                 else
497                 {
498                     log.info(
499                         "The project was not built because no changes were detected in sources since the last build." );
500                 }
501             }
502 
503             // Check dependencies changes
504             if ( context.getModifiedDependencies() != null && !context.getModifiedDependencies().isEmpty() )
505             {
506                 log.info( "Found dependencies changes, building" );
507                 shouldBuild = true;
508             }
509         }
510 
511         // Check changes
512         if ( !shouldBuild && ( ( !allChangesUnknown && context.getScmResult() != null && !context.getScmResult().getChanges().isEmpty() ) ||
513             project.getExecutorId().equals( ContinuumBuildExecutorConstants.MAVEN_TWO_BUILD_EXECUTOR ) ) )
514         {
515             try
516             {
517                 ContinuumBuildExecutor executor = buildExecutorManager.getBuildExecutor( project.getExecutorId() );
518 
519                 if ( executor == null )
520                 {
521                     log.warn( "No continuum build executor found for project " + project.getId() + 
522                               " with executor '" + project.getExecutorId() + "'" );
523                 }
524                 else if ( context.getScmResult() != null )
525                 {
526                     shouldBuild = executor.shouldBuild( context.getScmResult().getChanges(), project,
527                                                         workingDirectoryService.getWorkingDirectory( project ),
528                                                         context.getBuildDefinition() );
529                 }
530             }
531             catch ( Exception e )
532             {
533                 updateBuildResult( context, ContinuumUtils.throwableToString( e ) );
534                 throw new TaskExecutionException( "Can't determine if the project should build or not", e );
535             }
536         }
537 
538         if ( shouldBuild )
539         {
540             log.info( "Changes found in the current project, building" );
541         }
542         else
543         {
544             project.setState( project.getOldState() );
545 
546             project.setOldState( 0 );
547 
548             try
549             {
550                 projectDao.updateProject( project );
551             }
552             catch ( ContinuumStoreException e )
553             {
554                 throw new TaskExecutionException( "Error storing project", e );
555             }
556             log.info( "No changes in the current project, not building" );
557 
558         }
559 
560         return shouldBuild;
561     }
562 
563     private boolean checkAllChangesUnknown( List<ChangeSet> changes )
564     {
565         for ( ChangeSet changeSet : changes )
566         {
567             List<ChangeFile> changeFiles = changeSet.getFiles();
568 
569             for ( ChangeFile changeFile : changeFiles )
570             {
571                 if ( !"unknown".equalsIgnoreCase( changeFile.getStatus() ) )
572                 {
573                     return false;
574                 }
575             }
576         }
577 
578         return true;
579     }
580 
581     private String getValidationMessages( ScmRepositoryException ex )
582     {
583         List<String> messages = ex.getValidationMessages();
584 
585         StringBuffer message = new StringBuffer();
586 
587         if ( messages != null && !messages.isEmpty() )
588         {
589             for ( Iterator<String> i = messages.iterator(); i.hasNext(); )
590             {
591                 message.append( i.next() );
592 
593                 if ( i.hasNext() )
594                 {
595                     message.append( System.getProperty( "line.separator" ) );
596                 }
597             }
598         }
599         return message.toString();
600     }
601 
602     protected void checkProjectDependencies( BuildContext context )
603     {
604         if ( context.getOldBuildResult() == null )
605         {
606             return;
607         }
608 
609         try
610         {
611             Project project = projectDao.getProjectWithDependencies( context.getProject().getId() );
612             List<ProjectDependency> dependencies = project.getDependencies();
613 
614             if ( dependencies == null )
615             {
616                 dependencies = new ArrayList<ProjectDependency>();
617             }
618 
619             if ( project.getParent() != null )
620             {
621                 dependencies.add( project.getParent() );
622             }
623 
624             if ( dependencies.isEmpty() )
625             {
626                 return;
627             }
628 
629             List<ProjectDependency> modifiedDependencies = new ArrayList<ProjectDependency>();
630 
631             for ( ProjectDependency dep : dependencies )
632             {
633                 Project dependencyProject =
634                     projectDao.getProject( dep.getGroupId(), dep.getArtifactId(), dep.getVersion() );
635 
636                 if ( dependencyProject != null )
637                 {
638                     long nbBuild = buildResultDao.getNbBuildResultsInSuccessForProject( dependencyProject.getId(),
639                                                                                         context.getOldBuildResult().getEndTime() );
640                     if ( nbBuild > 0 )
641                     {
642                         log.debug( "Dependency changed: " + dep.getGroupId() + ":" + dep.getArtifactId() + ":" +
643                             dep.getVersion() );
644                         modifiedDependencies.add( dep );
645                     }
646                     else
647                     {
648                         log.debug( "Dependency not changed: " + dep.getGroupId() + ":" + dep.getArtifactId() + ":" +
649                             dep.getVersion() );
650                     }
651                 }
652                 else
653                 {
654                     log.debug( "Skip non Continuum project: " + dep.getGroupId() + ":" + dep.getArtifactId() + ":" +
655                         dep.getVersion() );
656                 }
657             }
658 
659             context.setModifiedDependencies( modifiedDependencies );
660             AbstractContinuumAction.setUpdatedDependencies( context.getActionContext(), modifiedDependencies );
661         }
662         catch ( ContinuumStoreException e )
663         {
664             log.warn( "Can't get the project dependencies", e );
665         }
666     }
667 
668     // ----------------------------------------------------------------------
669     //
670     // ----------------------------------------------------------------------
671 
672     private BuildResult makeAndStoreBuildResult( BuildContext context, String error )
673         throws TaskExecutionException
674     {
675         // Project project, ScmResult scmResult, long startTime, int trigger )
676         // project, scmResult, startTime, trigger );
677 
678         BuildResult build = new BuildResult();
679 
680         build.setState( ContinuumProjectState.ERROR );
681 
682         build.setTrigger( context.getTrigger() );
683 
684         build.setStartTime( context.getStartTime() );
685 
686         build.setEndTime( System.currentTimeMillis() );
687 
688         updateBuildResult( build, context );
689 
690         build.setScmResult( context.getScmResult() );
691 
692         build.setBuildDefinition( context.getBuildDefinition() );
693 
694         if ( error != null )
695         {
696             build.setError( error );
697         }
698 
699         try
700         {
701             buildResultDao.addBuildResult( context.getProject(), build );
702 
703             build = buildResultDao.getBuildResult( build.getId() );
704 
705             context.setBuildResult( build );
706 
707             return build;
708         }
709         catch ( ContinuumStoreException e )
710         {
711             throw new TaskExecutionException( "Error storing build result", e );
712         }
713     }
714 
715     /**
716      * Check to see if there was a error while checking out/updating the project
717      *
718      * @param context The build context
719      * @return true if scm result is ok
720      * @throws TaskExecutionException
721      */
722     private boolean checkScmResult( BuildContext context )
723         throws TaskExecutionException
724     {
725         Project project = context.getProject();
726 
727         int projectGroupId = project.getProjectGroup().getId();
728 
729         List<ProjectScmRoot> scmRoots = projectScmRootDao.getProjectScmRootByProjectGroup( projectGroupId );
730 
731         for ( ProjectScmRoot projectScmRoot : scmRoots )
732         {
733             if ( project.getScmUrl().startsWith( projectScmRoot.getScmRootAddress() ) )
734             {
735                 if ( projectScmRoot.getState() == ContinuumProjectState.UPDATED )
736                 {
737                     return true;
738                 }
739 
740                 break;
741             }
742         }
743 
744         return false;
745     }
746 }