001    /*
002     *   Copyright (C) Christian Schulte, 2005-206
003     *   All rights reserved.
004     *
005     *   Redistribution and use in source and binary forms, with or without
006     *   modification, are permitted provided that the following conditions
007     *   are met:
008     *
009     *     o Redistributions of source code must retain the above copyright
010     *       notice, this list of conditions and the following disclaimer.
011     *
012     *     o Redistributions in binary form must reproduce the above copyright
013     *       notice, this list of conditions and the following disclaimer in
014     *       the documentation and/or other materials provided with the
015     *       distribution.
016     *
017     *   THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
018     *   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
019     *   AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
020     *   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
021     *   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
022     *   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
023     *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
024     *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
025     *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
026     *   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027     *
028     *   $JOMC: MergeModulesTask.java 4200 2012-01-25 09:46:13Z schulte2005 $
029     *
030     */
031    package org.jomc.ant;
032    
033    import java.io.ByteArrayOutputStream;
034    import java.io.File;
035    import java.io.IOException;
036    import java.io.InputStream;
037    import java.io.OutputStreamWriter;
038    import java.net.SocketTimeoutException;
039    import java.net.URISyntaxException;
040    import java.net.URL;
041    import java.net.URLConnection;
042    import java.util.ArrayList;
043    import java.util.HashSet;
044    import java.util.Iterator;
045    import java.util.LinkedList;
046    import java.util.List;
047    import java.util.Set;
048    import java.util.logging.Level;
049    import javax.xml.bind.JAXBElement;
050    import javax.xml.bind.JAXBException;
051    import javax.xml.bind.Marshaller;
052    import javax.xml.bind.Unmarshaller;
053    import javax.xml.bind.util.JAXBResult;
054    import javax.xml.bind.util.JAXBSource;
055    import javax.xml.transform.Source;
056    import javax.xml.transform.Transformer;
057    import javax.xml.transform.TransformerConfigurationException;
058    import javax.xml.transform.TransformerException;
059    import javax.xml.transform.stream.StreamSource;
060    import org.apache.tools.ant.BuildException;
061    import org.apache.tools.ant.Project;
062    import org.jomc.ant.types.NameType;
063    import org.jomc.ant.types.ResourceType;
064    import org.jomc.ant.types.TransformerResourceType;
065    import org.jomc.model.Module;
066    import org.jomc.model.Modules;
067    import org.jomc.model.ObjectFactory;
068    import org.jomc.model.modlet.DefaultModelProvider;
069    import org.jomc.modlet.ModelContext;
070    import org.jomc.modlet.ModelException;
071    import org.jomc.modlet.ModelValidationReport;
072    
073    /**
074     * Task for merging module resources.
075     *
076     * @author <a href="mailto:schulte2005@users.sourceforge.net">Christian Schulte</a>
077     * @version $JOMC: MergeModulesTask.java 4200 2012-01-25 09:46:13Z schulte2005 $
078     */
079    public final class MergeModulesTask extends JomcModelTask
080    {
081    
082        /** The encoding of the module resource. */
083        private String moduleEncoding;
084    
085        /** File to write the merged module to. */
086        private File moduleFile;
087    
088        /** The name of the merged module. */
089        private String moduleName;
090    
091        /** The version of the merged module. */
092        private String moduleVersion;
093    
094        /** The vendor of the merged module. */
095        private String moduleVendor;
096    
097        /** Included modules. */
098        private Set<NameType> moduleIncludes;
099    
100        /** Excluded modules. */
101        private Set<NameType> moduleExcludes;
102    
103        /** XSLT documents to use for transforming model objects. */
104        private List<TransformerResourceType> modelObjectStylesheetResources;
105    
106        /** Creates a new {@code MergeModulesTask} instance. */
107        public MergeModulesTask()
108        {
109            super();
110        }
111    
112        /**
113         * Gets the file to write the merged module to.
114         *
115         * @return The file to write the merged module to or {@code null}.
116         *
117         * @see #setModuleFile(java.io.File)
118         */
119        public File getModuleFile()
120        {
121            return this.moduleFile;
122        }
123    
124        /**
125         * Sets the file to write the merged module to.
126         *
127         * @param value The new file to write the merged module to or {@code null}.
128         *
129         * @see #getModuleFile()
130         */
131        public void setModuleFile( final File value )
132        {
133            this.moduleFile = value;
134        }
135    
136        /**
137         * Gets the encoding of the module resource.
138         *
139         * @return The encoding of the module resource.
140         *
141         * @see #setModuleEncoding(java.lang.String)
142         */
143        public String getModuleEncoding()
144        {
145            if ( this.moduleEncoding == null )
146            {
147                this.moduleEncoding = new OutputStreamWriter( new ByteArrayOutputStream() ).getEncoding();
148            }
149    
150            return this.moduleEncoding;
151        }
152    
153        /**
154         * Sets the encoding of the module resource.
155         *
156         * @param value The new encoding of the module resource or {@code null}.
157         *
158         * @see #getModuleEncoding()
159         */
160        public void setModuleEncoding( final String value )
161        {
162            this.moduleEncoding = value;
163        }
164    
165        /**
166         * Gets the name of the merged module.
167         *
168         * @return The name of the merged module or {@code null}.
169         *
170         * @see #setModuleName(java.lang.String)
171         */
172        public String getModuleName()
173        {
174            return this.moduleName;
175        }
176    
177        /**
178         * Sets the name of the merged module.
179         *
180         * @param value The new name of the merged module or {@code null}.
181         *
182         * @see #getModuleName()
183         */
184        public void setModuleName( final String value )
185        {
186            this.moduleName = value;
187        }
188    
189        /**
190         * Gets the version of the merged module.
191         *
192         * @return The version of the merged module or {@code null}.
193         *
194         * @see #setModuleVersion(java.lang.String)
195         */
196        public String getModuleVersion()
197        {
198            return this.moduleVersion;
199        }
200    
201        /**
202         * Sets the version of the merged module.
203         *
204         * @param value The new version of the merged module or {@code null}.
205         *
206         * @see #getModuleVersion()
207         */
208        public void setModuleVersion( final String value )
209        {
210            this.moduleVersion = value;
211        }
212    
213        /**
214         * Gets the vendor of the merged module.
215         *
216         * @return The vendor of the merge module or {@code null}.
217         *
218         * @see #setModuleVendor(java.lang.String)
219         */
220        public String getModuleVendor()
221        {
222            return this.moduleVendor;
223        }
224    
225        /**
226         * Sets the vendor of the merged module.
227         *
228         * @param value The new vendor of the merged module or {@code null}.
229         *
230         * @see #getModuleVendor()
231         */
232        public void setModuleVendor( final String value )
233        {
234            this.moduleVendor = value;
235        }
236    
237        /**
238         * Gets a set of module names to include.
239         * <p>This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
240         * to the returned set will be present inside the object. This is why there is no {@code set} method for the
241         * module includes property.</p>
242         *
243         * @return A set of module names to include.
244         *
245         * @see #createModuleInclude()
246         */
247        public Set<NameType> getModuleIncludes()
248        {
249            if ( this.moduleIncludes == null )
250            {
251                this.moduleIncludes = new HashSet<NameType>();
252            }
253    
254            return this.moduleIncludes;
255        }
256    
257        /**
258         * Creates a new {@code moduleInclude} element instance.
259         *
260         * @return A new {@code moduleInclude} element instance.
261         *
262         * @see #getModuleIncludes()
263         */
264        public NameType createModuleInclude()
265        {
266            final NameType moduleInclude = new NameType();
267            this.getModuleIncludes().add( moduleInclude );
268            return moduleInclude;
269        }
270    
271        /**
272         * Gets a set of module names to exclude.
273         * <p>This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
274         * to the returned set will be present inside the object. This is why there is no {@code set} method for the
275         * module excludes property.</p>
276         *
277         * @return A set of module names to exclude.
278         *
279         * @see #createModuleExclude()
280         */
281        public Set<NameType> getModuleExcludes()
282        {
283            if ( this.moduleExcludes == null )
284            {
285                this.moduleExcludes = new HashSet<NameType>();
286            }
287    
288            return this.moduleExcludes;
289        }
290    
291        /**
292         * Creates a new {@code moduleExclude} element instance.
293         *
294         * @return A new {@code moduleExclude} element instance.
295         *
296         * @see #getModuleExcludes()
297         */
298        public NameType createModuleExclude()
299        {
300            final NameType moduleExclude = new NameType();
301            this.getModuleExcludes().add( moduleExclude );
302            return moduleExclude;
303        }
304    
305        /**
306         * Gets the XSLT documents to use for transforming model objects.
307         * <p>This accessor method returns a reference to the live list, not a snapshot. Therefore any modification you make
308         * to the returned list will be present inside the object. This is why there is no {@code set} method for the
309         * model object stylesheet resources property.</p>
310         *
311         * @return The XSLT documents to use for transforming model objects.
312         *
313         * @see #createModelObjectStylesheetResource()
314         */
315        public List<TransformerResourceType> getModelObjectStylesheetResources()
316        {
317            if ( this.modelObjectStylesheetResources == null )
318            {
319                this.modelObjectStylesheetResources = new LinkedList<TransformerResourceType>();
320            }
321    
322            return this.modelObjectStylesheetResources;
323        }
324    
325        /**
326         * Creates a new {@code modelObjectStylesheetResource} element instance.
327         *
328         * @return A new {@code modelObjectStylesheetResource} element instance.
329         *
330         * @see #getModelObjectStylesheetResources()
331         */
332        public TransformerResourceType createModelObjectStylesheetResource()
333        {
334            final TransformerResourceType modelObjectStylesheetResource = new TransformerResourceType();
335            this.getModelObjectStylesheetResources().add( modelObjectStylesheetResource );
336            return modelObjectStylesheetResource;
337        }
338    
339        /** {@inheritDoc} */
340        @Override
341        public void preExecuteTask() throws BuildException
342        {
343            super.preExecuteTask();
344    
345            this.assertNotNull( "moduleFile", this.getModuleFile() );
346            this.assertNotNull( "moduleName", this.getModuleName() );
347            this.assertNamesNotNull( this.getModuleExcludes() );
348            this.assertNamesNotNull( this.getModuleIncludes() );
349            this.assertLocationsNotNull( this.getModelObjectStylesheetResources() );
350        }
351    
352        /**
353         * Merges module resources.
354         *
355         * @throws BuildException if merging module resources fails.
356         */
357        @Override
358        public void executeTask() throws BuildException
359        {
360            ProjectClassLoader classLoader = null;
361            boolean suppressExceptionOnClose = true;
362    
363            try
364            {
365                this.log( Messages.getMessage( "mergingModules", this.getModel() ) );
366    
367                classLoader = this.newProjectClassLoader();
368                final Modules modules = new Modules();
369                final Set<ResourceType> resources = new HashSet<ResourceType>( this.getModuleResources() );
370                final ModelContext context = this.newModelContext( classLoader );
371                final Marshaller marshaller = context.createMarshaller( this.getModel() );
372                final Unmarshaller unmarshaller = context.createUnmarshaller( this.getModel() );
373    
374                if ( this.isModelResourceValidationEnabled() )
375                {
376                    unmarshaller.setSchema( context.createSchema( this.getModel() ) );
377                }
378    
379                if ( resources.isEmpty() )
380                {
381                    final ResourceType defaultResource = new ResourceType();
382                    defaultResource.setLocation( DefaultModelProvider.getDefaultModuleLocation() );
383                    defaultResource.setOptional( true );
384                    resources.add( defaultResource );
385                }
386    
387                for ( ResourceType resource : resources )
388                {
389                    final URL[] urls = this.getResources( context, resource.getLocation() );
390    
391                    if ( urls.length == 0 )
392                    {
393                        if ( resource.isOptional() )
394                        {
395                            this.logMessage( Level.WARNING, Messages.getMessage( "moduleResourceNotFound",
396                                                                                 resource.getLocation() ) );
397    
398                        }
399                        else
400                        {
401                            throw new BuildException(
402                                Messages.getMessage( "moduleResourceNotFound", resource.getLocation() ),
403                                this.getLocation() );
404    
405                        }
406                    }
407    
408                    for ( int i = urls.length - 1; i >= 0; i-- )
409                    {
410                        InputStream in = null;
411                        suppressExceptionOnClose = true;
412    
413                        try
414                        {
415                            this.logMessage( Level.FINEST, Messages.getMessage( "reading", urls[i].toExternalForm() ) );
416    
417                            final URLConnection con = urls[i].openConnection();
418                            con.setConnectTimeout( resource.getConnectTimeout() );
419                            con.setReadTimeout( resource.getReadTimeout() );
420                            con.connect();
421                            in = con.getInputStream();
422    
423                            final Source source = new StreamSource( in, urls[i].toURI().toASCIIString() );
424    
425                            Object o = unmarshaller.unmarshal( source );
426                            if ( o instanceof JAXBElement<?> )
427                            {
428                                o = ( (JAXBElement<?>) o ).getValue();
429                            }
430    
431                            if ( o instanceof Module )
432                            {
433                                modules.getModule().add( (Module) o );
434                            }
435                            else if ( o instanceof Modules )
436                            {
437                                modules.getModule().addAll( ( (Modules) o ).getModule() );
438                            }
439                            else
440                            {
441                                this.log( Messages.getMessage( "unsupportedModuleResource", urls[i].toExternalForm() ),
442                                          Project.MSG_WARN );
443    
444                            }
445    
446                            suppressExceptionOnClose = false;
447                        }
448                        catch ( final SocketTimeoutException e )
449                        {
450                            String message = Messages.getMessage( e );
451                            message = Messages.getMessage( "resourceTimeout", message != null ? " " + message : "" );
452    
453                            if ( resource.isOptional() )
454                            {
455                                this.getProject().log( message, e, Project.MSG_WARN );
456                            }
457                            else
458                            {
459                                throw new BuildException( message, e, this.getLocation() );
460                            }
461                        }
462                        catch ( final IOException e )
463                        {
464                            String message = Messages.getMessage( e );
465                            message = Messages.getMessage( "resourceFailure", message != null ? " " + message : "" );
466    
467                            if ( resource.isOptional() )
468                            {
469                                this.getProject().log( message, e, Project.MSG_WARN );
470                            }
471                            else
472                            {
473                                throw new BuildException( message, e, this.getLocation() );
474                            }
475                        }
476                        finally
477                        {
478                            try
479                            {
480                                if ( in != null )
481                                {
482                                    in.close();
483                                }
484                            }
485                            catch ( final IOException e )
486                            {
487    
488                                if ( suppressExceptionOnClose )
489                                {
490                                    this.logMessage( Level.SEVERE, Messages.getMessage( e ), e );
491                                }
492                                else
493                                {
494                                    throw new BuildException( Messages.getMessage( e ), e, this.getLocation() );
495                                }
496                            }
497                        }
498                    }
499    
500                    suppressExceptionOnClose = true;
501                }
502    
503                for ( final Iterator<Module> it = modules.getModule().iterator(); it.hasNext(); )
504                {
505                    final Module module = it.next();
506    
507                    if ( !this.isModuleIncluded( module ) || this.isModuleExcluded( module ) )
508                    {
509                        it.remove();
510                        this.log( Messages.getMessage( "excludingModule", module.getName() ) );
511                    }
512                    else
513                    {
514                        this.log( Messages.getMessage( "includingModule", module.getName() ) );
515                    }
516                }
517    
518                Module classpathModule = null;
519                if ( this.isModelObjectClasspathResolutionEnabled() )
520                {
521                    classpathModule = modules.getClasspathModule( Modules.getDefaultClasspathModuleName(), classLoader );
522    
523                    if ( classpathModule != null && modules.getModule( Modules.getDefaultClasspathModuleName() ) == null )
524                    {
525                        modules.getModule().add( classpathModule );
526                    }
527                    else
528                    {
529                        classpathModule = null;
530                    }
531                }
532    
533                final ModelValidationReport validationReport = context.validateModel(
534                    this.getModel(), new JAXBSource( marshaller, new ObjectFactory().createModules( modules ) ) );
535    
536                this.logValidationReport( context, validationReport );
537    
538                if ( !validationReport.isModelValid() )
539                {
540                    throw new ModelException( Messages.getMessage( "invalidModel", this.getModel() ) );
541                }
542    
543                if ( classpathModule != null )
544                {
545                    modules.getModule().remove( classpathModule );
546                }
547    
548                Module mergedModule = modules.getMergedModule( this.getModuleName() );
549                mergedModule.setVendor( this.getModuleVendor() );
550                mergedModule.setVersion( this.getModuleVersion() );
551    
552                for ( int i = 0, s0 = this.getModelObjectStylesheetResources().size(); i < s0; i++ )
553                {
554                    final Transformer transformer =
555                        this.getTransformer( this.getModelObjectStylesheetResources().get( i ) );
556    
557                    if ( transformer != null )
558                    {
559                        final JAXBSource source =
560                            new JAXBSource( marshaller, new ObjectFactory().createModule( mergedModule ) );
561    
562                        final JAXBResult result = new JAXBResult( unmarshaller );
563                        transformer.transform( source, result );
564    
565                        if ( result.getResult() instanceof JAXBElement<?>
566                             && ( (JAXBElement<?>) result.getResult() ).getValue() instanceof Module )
567                        {
568                            mergedModule = (Module) ( (JAXBElement<?>) result.getResult() ).getValue();
569                        }
570                        else
571                        {
572                            throw new BuildException( Messages.getMessage(
573                                "illegalTransformationResult",
574                                this.getModelObjectStylesheetResources().get( i ).getLocation() ), this.getLocation() );
575    
576                        }
577                    }
578                }
579    
580                this.log( Messages.getMessage( "writingEncoded", this.getModuleFile().getAbsolutePath(),
581                                               this.getModuleEncoding() ) );
582    
583                marshaller.setProperty( Marshaller.JAXB_ENCODING, this.getModuleEncoding() );
584                marshaller.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE );
585                marshaller.setSchema( context.createSchema( this.getModel() ) );
586                marshaller.marshal( new ObjectFactory().createModule( mergedModule ), this.getModuleFile() );
587                suppressExceptionOnClose = false;
588            }
589            catch ( final URISyntaxException e )
590            {
591                throw new BuildException( Messages.getMessage( e ), e, this.getLocation() );
592            }
593            catch ( final JAXBException e )
594            {
595                String message = Messages.getMessage( e );
596                if ( message == null )
597                {
598                    message = Messages.getMessage( e.getLinkedException() );
599                }
600    
601                throw new BuildException( message, e, this.getLocation() );
602            }
603            catch ( final TransformerConfigurationException e )
604            {
605                throw new BuildException( Messages.getMessage( e ), e, this.getLocation() );
606            }
607            catch ( final TransformerException e )
608            {
609                throw new BuildException( Messages.getMessage( e ), e, this.getLocation() );
610            }
611            catch ( final ModelException e )
612            {
613                throw new BuildException( Messages.getMessage( e ), e, this.getLocation() );
614            }
615            finally
616            {
617                try
618                {
619                    if ( classLoader != null )
620                    {
621                        classLoader.close();
622                    }
623                }
624                catch ( final IOException e )
625                {
626                    if ( suppressExceptionOnClose )
627                    {
628                        this.logMessage( Level.SEVERE, Messages.getMessage( e ), e );
629                    }
630                    else
631                    {
632                        throw new BuildException( Messages.getMessage( e ), e, this.getLocation() );
633                    }
634                }
635            }
636        }
637    
638        /**
639         * Tests inclusion of a given module based on property {@code moduleIncludes}.
640         *
641         * @param module The module to test.
642         *
643         * @return {@code true}, if {@code module} is included based on property {@code moduleIncludes}.
644         *
645         * @throws NullPointerException if {@code module} is {@code null}.
646         *
647         * @see #getModuleIncludes()
648         */
649        public boolean isModuleIncluded( final Module module )
650        {
651            if ( module == null )
652            {
653                throw new NullPointerException( "module" );
654            }
655    
656            for ( NameType include : this.getModuleIncludes() )
657            {
658                if ( include.getName().equals( module.getName() ) )
659                {
660                    return true;
661                }
662            }
663    
664            return this.getModuleIncludes().isEmpty() ? true : false;
665        }
666    
667        /**
668         * Tests exclusion of a given module based on property {@code moduleExcludes}.
669         *
670         * @param module The module to test.
671         *
672         * @return {@code true}, if {@code module} is excluded based on property {@code moduleExcludes}.
673         *
674         * @throws NullPointerException if {@code module} is {@code null}.
675         *
676         * @see #getModuleExcludes()
677         */
678        public boolean isModuleExcluded( final Module module )
679        {
680            if ( module == null )
681            {
682                throw new NullPointerException( "module" );
683            }
684    
685            for ( NameType exclude : this.getModuleExcludes() )
686            {
687                if ( exclude.getName().equals( module.getName() ) )
688                {
689                    return true;
690                }
691            }
692    
693            return false;
694        }
695    
696        /** {@inheritDoc} */
697        @Override
698        public MergeModulesTask clone()
699        {
700            final MergeModulesTask clone = (MergeModulesTask) super.clone();
701            clone.moduleFile = this.moduleFile != null ? new File( this.moduleFile.getAbsolutePath() ) : null;
702    
703            if ( this.moduleExcludes != null )
704            {
705                clone.moduleExcludes = new HashSet<NameType>( this.moduleExcludes.size() );
706                for ( NameType e : this.moduleExcludes )
707                {
708                    clone.moduleExcludes.add( e.clone() );
709                }
710            }
711    
712            if ( this.moduleIncludes != null )
713            {
714                clone.moduleIncludes = new HashSet<NameType>( this.moduleIncludes.size() );
715                for ( NameType e : this.moduleIncludes )
716                {
717                    clone.moduleIncludes.add( e.clone() );
718                }
719            }
720    
721            if ( this.modelObjectStylesheetResources != null )
722            {
723                clone.modelObjectStylesheetResources =
724                    new ArrayList<TransformerResourceType>( this.modelObjectStylesheetResources.size() );
725    
726                for ( TransformerResourceType e : this.modelObjectStylesheetResources )
727                {
728                    clone.modelObjectStylesheetResources.add( e.clone() );
729                }
730            }
731    
732            return clone;
733        }
734    
735    }