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