001    /*
002     * Copyright 2005 Stephen J. McConnell.
003     *
004     * Licensed  under the  Apache License,  Version 2.0  (the "License");
005     * you may not use  this file  except in  compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *   http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed  under the  License is distributed on an "AS IS" BASIS,
012     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
013     * implied.
014     *
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package net.dpml.tools.impl;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.net.URL;
024    import java.net.URI;
025    import java.util.Date;
026    
027    import net.dpml.lang.Plugin;
028    import net.dpml.lang.Part;
029    
030    import net.dpml.library.info.Scope;
031    import net.dpml.library.Library;
032    import net.dpml.library.Resource;
033    import net.dpml.library.Type;
034    import net.dpml.library.Filter;
035    import net.dpml.library.impl.DefaultLibrary;
036    
037    import net.dpml.tools.info.ListenerDirective;
038    
039    import net.dpml.tools.Context;
040    
041    import net.dpml.transit.Artifact;
042    import net.dpml.transit.Transit;
043    import net.dpml.transit.link.LinkManager;
044    import net.dpml.transit.Layout;
045    import net.dpml.transit.ClassicLayout;
046    
047    import net.dpml.util.Logger;
048    import net.dpml.util.DefaultLogger;
049    
050    import org.apache.tools.ant.Project;
051    import org.apache.tools.ant.BuildException;
052    import org.apache.tools.ant.BuildListener;
053    import org.apache.tools.ant.BuildEvent;
054    import org.apache.tools.ant.types.Path;
055    
056    /**
057     * Default implementation of a project context.
058     *
059     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
060     * @version 1.2.0
061     */
062    public final class DefaultContext implements Context
063    {
064        private static final Layout CLASSIC_LAYOUT = new ClassicLayout();
065        
066        private final Project m_project;
067        private final Resource m_resource;
068        
069        private Path m_runtime;
070        private Path m_test;
071        
072       /**
073        * Creation of a new project build context.
074        * @param project the unconfigured Ant project
075        * @exception Exception if an error occurs during context extablishment
076        */
077        public DefaultContext( Project project ) throws Exception
078        {
079            this( 
080              newResource( project.getBaseDir() ), 
081              project );
082        }
083        
084       /**
085        * Creation of a new project build context.
086        * @param resource the resource definition
087        * @param project the Ant project
088        * @exception Exception if an error occurs during context extablishment
089        */
090        public DefaultContext( Resource resource, Project project ) throws Exception
091        {
092            m_project = project;
093            m_resource = resource;
094            
095            Library library = resource.getLibrary();
096            project.addReference( "project.timestamp", new Date() );
097            
098            File basedir = resource.getBaseDir();
099            if( !basedir.equals( project.getBaseDir() ) )
100            {
101                project.setBaseDir( resource.getBaseDir() );
102            }
103            
104            project.addReference( "project.context", this );
105            
106            String[] names = resource.getPropertyNames();
107            for( int i=0; i<names.length; i++ )
108            {
109                String name = names[i];
110                String value = resource.getProperty( name );
111                setProperty( name, value );
112            }
113            
114            setProperty( "project.name", resource.getName() );
115            setProperty( "project.version", resource.getVersion() );
116            setProperty( "project.resource.path", resource.getResourcePath() );
117            setProperty( "project.basedir", resource.getBaseDir().toString() );
118            
119            File cache = Transit.getInstance().getCacheDirectory();
120            String cachePath = cache.getCanonicalPath();
121            setProperty( "project.cache", cachePath );
122            
123            Filter[] filters = resource.getFilters();
124            for( int i=0; i<filters.length; i++ )
125            {
126                Filter filter = filters[i];
127                String token = filter.getToken();
128                try
129                {
130                    String value = filter.getValue( resource );
131                    String resolved = resource.resolve( value );
132                    project.getGlobalFilterSet().addFilter( token, resolved );
133                }
134                catch( Exception e )
135                {
136                    final String error =
137                      "Error while attempting to setup the filter [" + token + "].";
138                    throw new BuildException( error, e );
139                }
140            }
141            
142            setProperty( "project.nl", "\n" );
143            setProperty( 
144              "project.line", 
145              "---------------------------------------------------------------------------\n", false );
146            setProperty( 
147              "project.info", 
148              "---------------------------------------------------------------------------\n"
149              + resource.getResourcePath()
150              + "#"
151              + resource.getVersion()
152              + "\n---------------------------------------------------------------------------", false );
153            
154            setProperty( "project.src.dir", getSrcDirectory().toString() );
155            setProperty( "project.src.main.dir", getSrcMainDirectory().toString() );
156            setProperty( "project.src.test.dir", getSrcTestDirectory().toString() );
157            setProperty( "project.etc.dir", getEtcDirectory().toString() );
158            setProperty( "project.etc.main.dir", getEtcMainDirectory().toString() );
159            setProperty( "project.etc.test.dir", getEtcTestDirectory().toString() );
160            setProperty( "project.etc.data.dir", getEtcDataDirectory().toString() );
161            
162            setProperty( "project.target.dir", getTargetDirectory().toString() );
163            setProperty( "project.target.build.main.dir", getTargetBuildMainDirectory().toString() );
164            setProperty( "project.target.build.test.dir", getTargetBuildTestDirectory().toString() );
165            setProperty( "project.target.classes.main.dir", getTargetClassesMainDirectory().toString() );
166            setProperty( "project.target.classes.test.dir", getTargetClassesTestDirectory().toString() );
167            setProperty( "project.target.deliverables.dir", getTargetDeliverablesDirectory().toString() );
168            setProperty( "project.target.test.dir", getTargetTestDirectory().toString() );
169            setProperty( "project.target.reports.dir", getTargetReportsDirectory().toString() );
170            
171            // add properties dealing with produced types
172            
173            addProductionProperties();
174            
175            // add listeners declared in the builder configuration
176            
177            ListenerDirective[] listeners = StandardBuilder.CONFIGURATION.getListenerDirectives();
178            for( int i=0; i<listeners.length; i++ )
179            {
180                ListenerDirective directive = listeners[i];
181                project.log( "adding listener: " + directive.getName(), Project.MSG_VERBOSE );
182                BuildListener buildListener = loadBuildListener( directive );
183                project.addBuildListener( buildListener );
184                BuildEvent event = new BuildEvent( project );
185                buildListener.buildStarted( event );
186            }
187        }
188        
189        private void addProductionProperties() throws IOException
190        {
191            Resource resource = getResource();
192            Type[] types = resource.getTypes();
193            for( int i=0; i<types.length; i++ )
194            {
195                Type type = types[i];
196                String id = type.getID();
197                File file = getTargetDeliverable( id );
198                String path = file.getCanonicalPath();
199                setProperty( "project.deliverable." + id + ".path", path );
200                File dir = file.getParentFile();
201                String spec = dir.getCanonicalPath();
202                setProperty( "project.deliverable." + id + ".dir", spec );
203                String base = getLayoutBase( id );
204                setProperty( "project.cache." + id + ".dir", base );
205                String address = getLayoutPath( id );
206                setProperty( "project.cache." + id + ".path", address );
207            }
208        }
209    
210        private void setProperty( String key, String value )
211        {
212            setProperty( key, value, true );
213        }
214        
215        private void setProperty( String key, String value, boolean verbose )
216        {
217            Project project = getProject();
218            String v = project.getProperty( key );
219            if( null == v )
220            {
221                if( verbose )
222                {
223                    project.log( "setting property [" + key + "] to [" + value + "]", Project.MSG_VERBOSE );
224                }
225                project.setProperty( key, value ); 
226            }
227            else if( !value.equals( v ) )
228            {
229                if( verbose )
230                {
231                    project.log( "updating property [" + key + "] to [" + value + "]", Project.MSG_VERBOSE );
232                }
233                project.setProperty( key, value );
234            }
235        }
236        
237       /**
238        * Initialize the context during which runtime and test path objects are 
239        * established as project references.
240        */
241        public void init()
242        {
243            if( m_runtime != null )
244            {
245                return;
246            }
247            
248            final Path compileSrcPath = new Path( m_project );
249            File srcMain = getTargetBuildMainDirectory();
250            compileSrcPath.createPathElement().setLocation( srcMain );
251            m_project.addReference( "project.build.src.path", compileSrcPath );
252            m_runtime = createPath( Scope.RUNTIME );
253            m_project.addReference( "project.compile.path", m_runtime );
254            m_test = createPath( Scope.TEST );
255            if( m_resource.isa( "jar" ) )
256            {
257                File deliverables = getTargetDeliverablesDirectory();
258                File jars = new File( deliverables, "jars" );
259                String filename = getLayoutFilename( "jar" );
260                File jar = new File( jars, filename );
261                m_test.createPathElement().setLocation( jar );
262            }
263            final File testClasses = getTargetClassesTestDirectory();
264            m_test.createPathElement().setLocation( testClasses );
265            m_project.addReference( "project.test.path", m_test );
266        }
267        
268       /**
269        * Return the associated project.
270        * @return the ant project
271        */
272        public Project getProject()
273        {
274            return m_project;
275        }
276        
277       /**
278        * Return the value of a property.
279        * @param key the property key
280        * @return the property value or null if undefined
281        */
282        public String getProperty( String key )
283        {
284            return getProperty( key, null );
285        }
286        
287       /**
288        * Return the value of a property. If the project contains a declaration 
289        * for the property then that value will be returned, otherwise the property
290        * will be resolved relative to the current resource.
291        *
292        * @param key the property key
293        * @param value the default value
294        * @return the property value or null if undefined
295        */
296        public String getProperty( String key, String value )
297        {
298            String result = m_project.getProperty( key );
299            if( null != result )
300            {
301                return result;
302            }
303            else
304            {
305                return getResource().getProperty( key, value );
306            }
307        }
308        
309       /**
310        * Return an Ant path suitable for comile or runtime usage. If the supplied scope is 
311        * less than Scope.RUNTIME a runtime path is returned otherwise the test path is 
312        * returned.
313        * @param scope the build scope
314        * @return the path object
315        */
316        public Path getPath( Scope scope )
317        {
318            if( m_runtime == null )
319            {
320                init();
321            }
322            if( scope.isLessThan( Scope.TEST ) )
323            {
324                return m_runtime;
325            }
326            else
327            {
328                return m_test;
329            }
330        }
331        
332       /**
333        * Return the active resource.
334        * @return the resource definition
335        */
336        public Resource getResource()
337        {
338            return m_resource;
339        }
340        
341       /**
342        * Return the resource library.
343        * @return the library
344        */
345        public Library getLibrary()
346        {
347            return m_resource.getLibrary();
348        }
349        
350       /**
351        * Return the project source directory.
352        * @return the directory
353        */
354        public File getSrcDirectory()
355        {
356            return createFile( "src" );
357        }
358        
359       /**
360        * Return the project source main directory.
361        * @return the directory
362        */
363        public File getSrcMainDirectory()
364        {
365            String path = getProperty( "project.src.main", "src/main" );
366            return createFile( path );
367        }
368        
369       /**
370        * Return the project source test directory.
371        * @return the directory
372        */
373        public File getSrcTestDirectory()
374        {
375            String path = getProperty( "project.src.test", "src/test" );
376            return createFile( path );
377        }
378        
379       /**
380        * Return the project source docs directory.
381        * @return the directory
382        */
383        public File getSrcDocsDirectory()
384        {
385            String path = getProperty( "project.src.docs", "src/docs" );
386            return createFile( path );
387        }
388        
389       /**
390        * Return the project etc directory.
391        * @return the directory
392        */
393        public File getEtcDirectory()
394        {
395            String path = getProperty( "project.etc", "etc" );
396            return createFile( path );
397        }
398    
399       /**
400        * Return the project etc/main directory.
401        * @return the directory
402        */
403        public File getEtcMainDirectory()
404        {
405            String path = getProperty( "project.etc.main", "etc/main" );
406            return createFile( path );
407        }
408    
409       /**
410        * Return the project etc/test directory.
411        * @return the directory
412        */
413        public File getEtcTestDirectory()
414        {
415            String path = getProperty( "project.etc.test", "etc/test" );
416            return createFile( path );
417        }
418    
419       /**
420        * Return the project etc/resources directory.
421        * @return the directory
422        */
423        public File getEtcDataDirectory()
424        {
425            String path = getProperty( "project.etc.data", "etc/data" );
426            return createFile( path );
427        }
428    
429       /**
430        * Return the project target directory.
431        * @return the directory
432        */
433        public File getTargetDirectory()
434        {
435            return createFile( "target" );
436        }
437        
438       /**
439        * Return a directory within the target directory.
440        * @param path the path
441        * @return the directory
442        */
443        public File getTargetDirectory( String path )
444        {
445            return new File( getTargetDirectory(), path );
446        }
447        
448       /**
449        * Return the project target temp directory.
450        * @return the directory
451        */
452        public File getTargetTempDirectory()
453        {
454            return new File( getTargetDirectory(), "temp" );
455        }
456        
457       /**
458        * Return the project target build directory.
459        * @return the directory
460        */
461        public File getTargetBuildDirectory()
462        {
463            return new File( getTargetDirectory(), "build" );
464        }
465        
466       /**
467        * Return the project target build main directory.
468        * @return the directory
469        */
470        public File getTargetBuildMainDirectory()
471        {
472            return new File( getTargetBuildDirectory(), "main" );
473        }
474        
475       /**
476        * Return the project target build test directory.
477        * @return the directory
478        */
479        public File getTargetBuildTestDirectory()
480        {
481            return new File( getTargetBuildDirectory(), "test" );
482        }
483        
484       /**
485        * Return the project target build docs directory.
486        * @return the directory
487        */
488        public File getTargetBuildDocsDirectory()
489        {
490            return new File( getTargetBuildDirectory(), "docs" );
491        }
492        
493       /**
494        * Return the project target root classes directory.
495        * @return the directory
496        */
497        public File getTargetClassesDirectory()
498        {
499            return new File( getTargetDirectory(), "classes" );
500        }
501        
502       /**
503        * Return the project target main classes directory.
504        * @return the directory
505        */
506        public File getTargetClassesMainDirectory()
507        {
508            return new File( getTargetClassesDirectory(), "main" );
509        }
510        
511       /**
512        * Return the project target test classes directory.
513        * @return the directory
514        */
515        public File getTargetClassesTestDirectory()
516        {
517            return new File( getTargetClassesDirectory(), "test" );
518        }
519        
520       /**
521        * Return the project target reports directory.
522        * @return the directory
523        */
524        public File getTargetReportsDirectory()
525        {
526            return new File( getTargetDirectory(), "reports" );
527        }
528        
529       /**
530        * Return the project target test reports directory.
531        * @return the directory
532        */
533        public File getTargetReportsTestDirectory()
534        {
535            return new File( getTargetReportsDirectory(), "test" );
536        }
537        
538       /**
539        * Return the project target main reports directory.
540        * @return the directory
541        */
542        public File getTargetReportsMainDirectory()
543        {
544            return new File( getTargetReportsDirectory(), "main" );
545        }
546        
547       /**
548        * Return the project target javadoc reports directory.
549        * @return the directory
550        */
551        public File getTargetReportsJavadocDirectory()
552        {
553            return new File( getTargetReportsDirectory(), "api" );
554        }
555        
556       /**
557        * Return the project target reports docs directory.
558        * @return the directory
559        */
560        public File getTargetDocsDirectory()
561        {
562            return new File( getTargetDirectory(), "docs" );
563        }
564        
565       /**
566        * Return the project target test directory.
567        * @return the directory
568        */
569        public File getTargetTestDirectory()
570        {
571            return new File( getTargetDirectory(), "test" );
572        }
573        
574       /**
575        * Return the project target deliverables directory.
576        * @return the directory
577        */
578        public File getTargetDeliverablesDirectory()
579        {
580            return new File( getTargetDirectory(), "deliverables" );
581        }
582        
583       /**
584        * Return the project target deliverables directory.
585        * @param type the deliverable type
586        * @return the directory
587        */
588        public File getTargetDeliverable( String type )
589        {
590            Artifact artifact = m_resource.getArtifact( type );
591            String path = CLASSIC_LAYOUT.resolveFilename( artifact );
592            String types = type + "s";
593            File root = new File( getTargetDeliverablesDirectory(), types );
594            return new File( root, path );
595        }
596        
597       /**
598        * Create a file relative to the resource basedir.
599        * @param path the relative path
600        * @return the directory
601        */
602        public File createFile( String path )
603        {
604            File basedir = m_resource.getBaseDir();
605            return new File( basedir, path );
606        }
607        
608       /**
609        * Return a filename using the layout strategy employed by the cache.
610        * @param id the artifact type
611        * @return the filename
612        */
613        public String getLayoutFilename( String id )
614        {
615            Artifact artifact = m_resource.getArtifact( id );
616            return Transit.getInstance().getCacheLayout().resolveFilename( artifact );
617        }
618    
619       /**
620        * Return the directory path representing the module structure and type
621        * using the layout strategy employed by the cache.
622        * @param id the artifact type
623        * @return the path from the root of the cache to the directory containing the artifact
624        */
625        public String getLayoutBase( String id )
626        {
627            Artifact artifact = m_resource.getArtifact( id );
628            return Transit.getInstance().getCacheLayout().resolveBase( artifact );
629        }
630        
631       /**
632        * Return the full path to an artifact using the layout employed by the cache.
633        * @param id the artifact type
634        * @return the full path including base path and filename
635        */
636        public String getLayoutPath( String id )
637        {
638            Artifact artifact = m_resource.getArtifact( id );
639            return Transit.getInstance().getCacheLayout().resolvePath( artifact );
640        }
641        
642       /**
643        * Utility operation to construct a new classpath path instance.
644        * @param scope the build scope
645        * @return the path
646        */
647        public Path createPath( Scope scope )
648        {
649            try
650            {
651                Resource[] resources = m_resource.getClasspathProviders( scope );
652                return createPath( resources, true, true );
653            }
654            catch( Exception e )
655            {
656                final String error = 
657                  "Unexpected error while constructing path instance for the scope: " + scope;
658                throw new RuntimeException( error, e );
659            }
660        }
661        
662       /**
663        * Utility operation to construct a new path using a supplied array of resources.
664        * @param resources the resource to use in path construction
665        * @return the path
666        */
667        public Path createPath( Resource[] resources ) 
668        {
669            return createPath( resources, true, false );
670        }
671        
672       /**
673        * Utility operation to construct a new path using a supplied array of resources.
674        * @param resources the resources to use in path construction
675        * @param resolve if true force local caching of the artifact 
676        * @param filter if true restrict path entries to resources that produce jars
677        * @return the path
678        */
679        public Path createPath( Resource[] resources, boolean resolve, boolean filter )
680        {
681            final Path path = new Path( m_project );
682            File cache = (File) m_project.getReference( "dpml.cache" );
683            for( int i=0; i<resources.length; i++ )
684            {
685                Resource resource = resources[i];
686                if( !resource.equals( getResource() ) )
687                {
688                    if( filter && resource.isa( "jar" ) )
689                    {
690                        Artifact artifact = resource.getArtifact( "jar" );
691                        addToPath( cache, path, artifact, resolve );
692                    }
693                    else
694                    {
695                        Type[] types = resource.getTypes();
696                        for( int j=0; j<types.length; j++ )
697                        {
698                            Artifact artifact = resource.getArtifact( types[j].getID() );
699                            addToPath( cache, path, artifact, resolve );
700                        }
701                    }
702                }
703            }
704            return path;
705        }
706    
707        private void addToPath( File cache, Path path, Artifact artifact, boolean resolve )
708        {
709            Artifact target = getTargetArtifact( artifact );
710            String location = Transit.getInstance().getCacheLayout().resolvePath( target );
711            File file = new File( cache, location );
712            path.createPathElement().setLocation( file );
713            
714            if( resolve )
715            {
716                resolveArtifact( artifact );
717            }
718        }
719        
720        private Artifact getTargetArtifact( Artifact artifact )
721        {
722            String scheme = artifact.getScheme();
723            if( !Artifact.LINK.equals( scheme ) )
724            {
725                return artifact;
726            }
727            else
728            {
729                try
730                {
731                    LinkManager manager = Transit.getInstance().getLinkManager();
732                    URI uri = manager.getTargetURI( artifact.toURI() );
733                    return Artifact.createArtifact( uri );
734                }
735                catch( IOException e )
736                {
737                    final String error = 
738                      "Unable to resolve link artifact [" 
739                      + artifact
740                      + "].";
741                    throw new BuildException( error, e );
742                }
743            }
744        }
745        
746        private void resolveArtifact( Artifact artifact )
747        {
748            try
749            {
750                URL url = artifact.toURL();
751                url.openStream();
752            }
753            catch( IOException e )
754            {
755                final String error = 
756                  "Unable to resolve artifact [" 
757                  + artifact
758                  + "].";
759                throw new BuildException( error, e );
760            }
761        }
762    
763        private static Resource newResource( File basedir ) throws Exception
764        {
765            Logger logger = new DefaultLogger();
766            DefaultLibrary library = new DefaultLibrary( logger );
767            return library.locate( basedir.getCanonicalFile() );
768        }
769    
770        private static String flatternDependencies( String[] deps )
771        {
772            if( deps.length == 0 )
773            {
774                return null;
775            }
776            StringBuffer buffer = new StringBuffer();
777            for( int i=0; i<deps.length; i++ )
778            {
779                if( i>0 )
780                {
781                    buffer.append( "," );
782                }
783                String dep = deps[i];
784                buffer.append( dep );
785            }
786            return buffer.toString();
787        }
788        
789        private BuildListener loadBuildListener( ListenerDirective listener )
790        {
791            String name = listener.getName();
792            URI uri = listener.getURI();
793            try
794            {
795                String classname = listener.getClassname();
796                Object object = loadInstance( name, uri, classname );
797                return (BuildListener) object;
798            }
799            catch( ClassCastException e )
800            {
801                final String error = 
802                  "Build listener [" 
803                  + name
804                  + "] from uri ["
805                  + uri
806                  + "] does not implement "
807                  + BuildListener.class.getName();
808                throw new BuilderError( error );
809            }
810        }
811            
812        private Object loadInstance( String name, URI uri, String classname )
813        {
814            if( null == uri )
815            {
816                try
817                {
818                    ClassLoader classloader = getClass().getClassLoader();
819                    Class clazz = classloader.loadClass( classname );
820                    Object[] args = new Object[]{this};
821                    return Plugin.instantiate( clazz, args );
822                }
823                catch( Throwable e )
824                {
825                    final String error = 
826                      "Internal error while attempting to load a local plugin."
827                      + "\nClass: " + classname
828                      + "\nName: " + name;
829                    throw new BuilderError( error, e );
830                }
831            }
832            else
833            {
834                ClassLoader context = Thread.currentThread().getContextClassLoader();
835                try
836                {
837                    ClassLoader classloader = getClass().getClassLoader();
838                    Thread.currentThread().setContextClassLoader( classloader );
839                    Object[] params = new Object[]{this};
840                    Part part = Part.load( uri );
841                    if( null == classname )
842                    {
843                        return part.instantiate( params );
844                    }
845                    else
846                    {
847                        ClassLoader loader = part.getClassLoader();
848                        Class c = loader.loadClass( classname );
849                        return Plugin.instantiate( c, params );
850                    }
851                }
852                catch( Throwable e )
853                {
854                    final String error = 
855                      "Internal error while attempting to load plugin."
856                      + "\nURI: " + uri
857                      + "\nName: " + name;
858                    throw new BuilderError( error, e );
859                }
860                finally
861                {
862                    Thread.currentThread().setContextClassLoader( context );
863                }
864            }
865        }
866    }
867