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