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