001package org.apache.maven.model.merge;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.ArrayList;
023import java.util.LinkedHashMap;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.maven.model.BuildBase;
030import org.apache.maven.model.CiManagement;
031import org.apache.maven.model.Contributor;
032import org.apache.maven.model.Dependency;
033import org.apache.maven.model.DeploymentRepository;
034import org.apache.maven.model.Developer;
035import org.apache.maven.model.DistributionManagement;
036import org.apache.maven.model.Exclusion;
037import org.apache.maven.model.Extension;
038import org.apache.maven.model.InputLocation;
039import org.apache.maven.model.IssueManagement;
040import org.apache.maven.model.License;
041import org.apache.maven.model.MailingList;
042import org.apache.maven.model.Model;
043import org.apache.maven.model.ModelBase;
044import org.apache.maven.model.Organization;
045import org.apache.maven.model.Plugin;
046import org.apache.maven.model.PluginExecution;
047import org.apache.maven.model.ReportPlugin;
048import org.apache.maven.model.ReportSet;
049import org.apache.maven.model.Repository;
050import org.apache.maven.model.RepositoryBase;
051import org.apache.maven.model.Scm;
052import org.apache.maven.model.Site;
053
054/**
055 * The domain-specific model merger for the Maven POM, overriding generic code from parent class when necessary with
056 * more adapted algorithms.
057 *
058 * @author Benjamin Bentmann
059 */
060public class MavenModelMerger
061    extends ModelMerger
062{
063
064    /**
065     * The hint key for the child path adjustment used during inheritance for URL calculations.
066     */
067    public static final String CHILD_PATH_ADJUSTMENT = "child-path-adjustment";
068
069    /**
070     * The context key for the artifact id of the target model.
071     */
072    private static final String ARTIFACT_ID = "artifact-id";
073
074    @Override
075    protected void mergeModel( Model target, Model source, boolean sourceDominant, Map<Object, Object> context )
076    {
077        context.put( ARTIFACT_ID, target.getArtifactId() );
078
079        super.mergeModel( target, source, sourceDominant, context );
080    }
081
082    @Override
083    protected void mergeModel_Name( Model target, Model source, boolean sourceDominant, Map<Object, Object> context )
084    {
085        String src = source.getName();
086        if ( src != null )
087        {
088            if ( sourceDominant )
089            {
090                target.setName( src );
091                target.setLocation( "name", source.getLocation( "name" ) );
092            }
093        }
094    }
095
096    @Override
097    protected void mergeModel_Url( Model target, Model source, boolean sourceDominant, Map<Object, Object> context )
098    {
099        String src = source.getUrl();
100        if ( src != null )
101        {
102            if ( sourceDominant )
103            {
104                target.setUrl( src );
105                target.setLocation( "url", source.getLocation( "url" ) );
106            }
107            else if ( target.getUrl() == null )
108            {
109                target.setUrl( appendPath( src, context ) );
110                target.setLocation( "url", source.getLocation( "url" ) );
111            }
112        }
113    }
114
115    /*
116     * TODO: Whether the merge continues recursively into an existing node or not could be an option for the generated
117     * merger
118     */
119    @Override
120    protected void mergeModel_Organization( Model target, Model source, boolean sourceDominant,
121                                            Map<Object, Object> context )
122    {
123        Organization src = source.getOrganization();
124        if ( src != null )
125        {
126            Organization tgt = target.getOrganization();
127            if ( tgt == null )
128            {
129                tgt = new Organization();
130                tgt.setLocation( "", src.getLocation( "" ) );
131                target.setOrganization( tgt );
132                mergeOrganization( tgt, src, sourceDominant, context );
133            }
134        }
135    }
136
137    @Override
138    protected void mergeModel_IssueManagement( Model target, Model source, boolean sourceDominant,
139                                               Map<Object, Object> context )
140    {
141        IssueManagement src = source.getIssueManagement();
142        if ( src != null )
143        {
144            IssueManagement tgt = target.getIssueManagement();
145            if ( tgt == null )
146            {
147                tgt = new IssueManagement();
148                tgt.setLocation( "", src.getLocation( "" ) );
149                target.setIssueManagement( tgt );
150                mergeIssueManagement( tgt, src, sourceDominant, context );
151            }
152        }
153    }
154
155    @Override
156    protected void mergeModel_CiManagement( Model target, Model source, boolean sourceDominant,
157                                            Map<Object, Object> context )
158    {
159        CiManagement src = source.getCiManagement();
160        if ( src != null )
161        {
162            CiManagement tgt = target.getCiManagement();
163            if ( tgt == null )
164            {
165                tgt = new CiManagement();
166                tgt.setLocation( "", src.getLocation( "" ) );
167                target.setCiManagement( tgt );
168                mergeCiManagement( tgt, src, sourceDominant, context );
169            }
170        }
171    }
172
173    @Override
174    protected void mergeModel_ModelVersion( Model target, Model source, boolean sourceDominant,
175                                            Map<Object, Object> context )
176    {
177        // neither inherited nor injected
178    }
179
180    @Override
181    protected void mergeModel_ArtifactId( Model target, Model source, boolean sourceDominant,
182                                          Map<Object, Object> context )
183    {
184        // neither inherited nor injected
185    }
186
187    @Override
188    protected void mergeModel_Profiles( Model target, Model source, boolean sourceDominant,
189                                        Map<Object, Object> context )
190    {
191        // neither inherited nor injected
192    }
193
194    @Override
195    protected void mergeModel_Prerequisites( Model target, Model source, boolean sourceDominant,
196                                             Map<Object, Object> context )
197    {
198        // neither inherited nor injected
199    }
200
201    @Override
202    protected void mergeModel_Licenses( Model target, Model source, boolean sourceDominant,
203                                        Map<Object, Object> context )
204    {
205        if ( target.getLicenses().isEmpty() )
206        {
207            target.setLicenses( new ArrayList<License>( source.getLicenses() ) );
208        }
209    }
210
211    @Override
212    protected void mergeModel_Developers( Model target, Model source, boolean sourceDominant,
213                                          Map<Object, Object> context )
214    {
215        if ( target.getDevelopers().isEmpty() )
216        {
217            target.setDevelopers( new ArrayList<Developer>( source.getDevelopers() ) );
218        }
219    }
220
221    @Override
222    protected void mergeModel_Contributors( Model target, Model source, boolean sourceDominant,
223                                            Map<Object, Object> context )
224    {
225        if ( target.getContributors().isEmpty() )
226        {
227            target.setContributors( new ArrayList<Contributor>( source.getContributors() ) );
228        }
229    }
230
231    @Override
232    protected void mergeModel_MailingLists( Model target, Model source, boolean sourceDominant,
233                                            Map<Object, Object> context )
234    {
235        if ( target.getMailingLists().isEmpty() )
236        {
237            target.setMailingLists( new ArrayList<MailingList>( source.getMailingLists() ) );
238        }
239    }
240
241    @Override
242    protected void mergeModelBase_Modules( ModelBase target, ModelBase source, boolean sourceDominant,
243                                           Map<Object, Object> context )
244    {
245        List<String> src = source.getModules();
246        if ( !src.isEmpty() && sourceDominant )
247        {
248            List<Integer> indices = new ArrayList<Integer>();
249            List<String> tgt = target.getModules();
250            Set<String> excludes = new LinkedHashSet<String>( tgt );
251            List<String> merged = new ArrayList<String>( tgt.size() + src.size() );
252            merged.addAll( tgt );
253            for ( int i = 0, n = tgt.size(); i < n; i++ )
254            {
255                indices.add( i );
256            }
257            for ( int i = 0, n = src.size(); i < n; i++ )
258            {
259                String s = src.get( i );
260                if ( !excludes.contains( s ) )
261                {
262                    merged.add( s );
263                    indices.add( ~i );
264                }
265            }
266            target.setModules( merged );
267            target.setLocation( "modules", InputLocation.merge( target.getLocation( "modules" ),
268                                                                source.getLocation( "modules" ), indices ) );
269        }
270    }
271
272    /*
273     * TODO: The order of the merged list could be controlled by an attribute in the model association: target-first,
274     * source-first, dominant-first, recessive-first
275     */
276    @Override
277    protected void mergeModelBase_Repositories( ModelBase target, ModelBase source, boolean sourceDominant,
278                                                Map<Object, Object> context )
279    {
280        List<Repository> src = source.getRepositories();
281        if ( !src.isEmpty() )
282        {
283            List<Repository> tgt = target.getRepositories();
284            Map<Object, Repository> merged = new LinkedHashMap<Object, Repository>( ( src.size() + tgt.size() ) * 2 );
285
286            List<Repository> dominant, recessive;
287            if ( sourceDominant )
288            {
289                dominant = src;
290                recessive = tgt;
291            }
292            else
293            {
294                dominant = tgt;
295                recessive = src;
296            }
297
298            for ( Repository element : dominant )
299            {
300                Object key = getRepositoryKey( element );
301                merged.put( key, element );
302            }
303
304            for ( Repository element : recessive )
305            {
306                Object key = getRepositoryKey( element );
307                if ( !merged.containsKey( key ) )
308                {
309                    merged.put( key, element );
310                }
311            }
312
313            target.setRepositories( new ArrayList<Repository>( merged.values() ) );
314        }
315    }
316
317    protected void mergeModelBase_PluginRepositories( ModelBase target, ModelBase source, boolean sourceDominant,
318                                                      Map<Object, Object> context )
319    {
320        List<Repository> src = source.getPluginRepositories();
321        if ( !src.isEmpty() )
322        {
323            List<Repository> tgt = target.getPluginRepositories();
324            Map<Object, Repository> merged = new LinkedHashMap<Object, Repository>( ( src.size() + tgt.size() ) * 2 );
325
326            List<Repository> dominant, recessive;
327            if ( sourceDominant )
328            {
329                dominant = src;
330                recessive = tgt;
331            }
332            else
333            {
334                dominant = tgt;
335                recessive = src;
336            }
337
338            for ( Repository element : dominant )
339            {
340                Object key = getRepositoryKey( element );
341                merged.put( key, element );
342            }
343
344            for ( Repository element : recessive )
345            {
346                Object key = getRepositoryKey( element );
347                if ( !merged.containsKey( key ) )
348                {
349                    merged.put( key, element );
350                }
351            }
352
353            target.setPluginRepositories( new ArrayList<Repository>( merged.values() ) );
354        }
355    }
356
357    /*
358     * TODO: Whether duplicates should be removed looks like an option for the generated merger.
359     */
360    @Override
361    protected void mergeBuildBase_Filters( BuildBase target, BuildBase source, boolean sourceDominant,
362                                           Map<Object, Object> context )
363    {
364        List<String> src = source.getFilters();
365        if ( !src.isEmpty() )
366        {
367            List<String> tgt = target.getFilters();
368            Set<String> excludes = new LinkedHashSet<String>( tgt );
369            List<String> merged = new ArrayList<String>( tgt.size() + src.size() );
370            merged.addAll( tgt );
371            for ( String s : src )
372            {
373                if ( !excludes.contains( s ) )
374                {
375                    merged.add( s );
376                }
377            }
378            target.setFilters( merged );
379        }
380    }
381
382    @Override
383    protected void mergeBuildBase_Resources( BuildBase target, BuildBase source, boolean sourceDominant,
384                                             Map<Object, Object> context )
385    {
386        if ( sourceDominant || target.getResources().isEmpty() )
387        {
388            super.mergeBuildBase_Resources( target, source, sourceDominant, context );
389        }
390    }
391
392    @Override
393    protected void mergeBuildBase_TestResources( BuildBase target, BuildBase source, boolean sourceDominant,
394                                                 Map<Object, Object> context )
395    {
396        if ( sourceDominant || target.getTestResources().isEmpty() )
397        {
398            super.mergeBuildBase_TestResources( target, source, sourceDominant, context );
399        }
400    }
401
402    @Override
403    protected void mergeDistributionManagement_Repository( DistributionManagement target,
404                                                           DistributionManagement source, boolean sourceDominant,
405                                                           Map<Object, Object> context )
406    {
407        DeploymentRepository src = source.getRepository();
408        if ( src != null )
409        {
410            DeploymentRepository tgt = target.getRepository();
411            if ( sourceDominant || tgt == null )
412            {
413                tgt = new DeploymentRepository();
414                tgt.setLocation( "", src.getLocation( "" ) );
415                target.setRepository( tgt );
416                mergeDeploymentRepository( tgt, src, sourceDominant, context );
417            }
418        }
419    }
420
421    @Override
422    protected void mergeDistributionManagement_SnapshotRepository( DistributionManagement target,
423                                                                   DistributionManagement source,
424                                                                   boolean sourceDominant,
425                                                                   Map<Object, Object> context )
426    {
427        DeploymentRepository src = source.getSnapshotRepository();
428        if ( src != null )
429        {
430            DeploymentRepository tgt = target.getSnapshotRepository();
431            if ( sourceDominant || tgt == null )
432            {
433                tgt = new DeploymentRepository();
434                tgt.setLocation( "", src.getLocation( "" ) );
435                target.setSnapshotRepository( tgt );
436                mergeDeploymentRepository( tgt, src, sourceDominant, context );
437            }
438        }
439    }
440
441    @Override
442    protected void mergeDistributionManagement_Site( DistributionManagement target, DistributionManagement source,
443                                                     boolean sourceDominant, Map<Object, Object> context )
444    {
445        Site src = source.getSite();
446        if ( src != null )
447        {
448            Site tgt = target.getSite();
449            if ( sourceDominant || tgt == null )
450            {
451                tgt = new Site();
452                tgt.setLocation( "", src.getLocation( "" ) );
453                target.setSite( tgt );
454                mergeSite( tgt, src, sourceDominant, context );
455            }
456        }
457    }
458
459    @Override
460    protected void mergeSite_Url( Site target, Site source, boolean sourceDominant, Map<Object, Object> context )
461    {
462        String src = source.getUrl();
463        if ( src != null )
464        {
465            if ( sourceDominant )
466            {
467                target.setUrl( src );
468                target.setLocation( "url", source.getLocation( "url" ) );
469            }
470            else if ( target.getUrl() == null )
471            {
472                target.setUrl( appendPath( src, context ) );
473                target.setLocation( "url", source.getLocation( "url" ) );
474            }
475        }
476    }
477
478    @Override
479    protected void mergeScm_Url( Scm target, Scm source, boolean sourceDominant, Map<Object, Object> context )
480    {
481        String src = source.getUrl();
482        if ( src != null )
483        {
484            if ( sourceDominant )
485            {
486                target.setUrl( src );
487                target.setLocation( "url", source.getLocation( "url" ) );
488            }
489            else if ( target.getUrl() == null )
490            {
491                target.setUrl( appendPath( src, context ) );
492                target.setLocation( "url", source.getLocation( "url" ) );
493            }
494        }
495    }
496
497    @Override
498    protected void mergeScm_Connection( Scm target, Scm source, boolean sourceDominant, Map<Object, Object> context )
499    {
500        String src = source.getConnection();
501        if ( src != null )
502        {
503            if ( sourceDominant )
504            {
505                target.setConnection( src );
506                target.setLocation( "connection", source.getLocation( "connection" ) );
507            }
508            else if ( target.getConnection() == null )
509            {
510                target.setConnection( appendPath( src, context ) );
511                target.setLocation( "connection", source.getLocation( "connection" ) );
512            }
513        }
514    }
515
516    @Override
517    protected void mergeScm_DeveloperConnection( Scm target, Scm source, boolean sourceDominant,
518                                                 Map<Object, Object> context )
519    {
520        String src = source.getDeveloperConnection();
521        if ( src != null )
522        {
523            if ( sourceDominant )
524            {
525                target.setDeveloperConnection( src );
526                target.setLocation( "developerConnection", source.getLocation( "developerConnection" ) );
527            }
528            else if ( target.getDeveloperConnection() == null )
529            {
530                target.setDeveloperConnection( appendPath( src, context ) );
531                target.setLocation( "developerConnection", source.getLocation( "developerConnection" ) );
532            }
533        }
534    }
535
536    @Override
537    protected void mergePlugin_Executions( Plugin target, Plugin source, boolean sourceDominant,
538                                           Map<Object, Object> context )
539    {
540        List<PluginExecution> src = source.getExecutions();
541        if ( !src.isEmpty() )
542        {
543            List<PluginExecution> tgt = target.getExecutions();
544            Map<Object, PluginExecution> merged =
545                new LinkedHashMap<Object, PluginExecution>( ( src.size() + tgt.size() ) * 2 );
546
547            for ( PluginExecution element : src )
548            {
549                if ( sourceDominant
550                                || ( element.getInherited() != null ? element.isInherited() : source.isInherited() ) )
551                {
552                    Object key = getPluginExecutionKey( element );
553                    merged.put( key, element );
554                }
555            }
556
557            for ( PluginExecution element : tgt )
558            {
559                Object key = getPluginExecutionKey( element );
560                PluginExecution existing = merged.get( key );
561                if ( existing != null )
562                {
563                    mergePluginExecution( element, existing, sourceDominant, context );
564                }
565                merged.put( key, element );
566            }
567
568            target.setExecutions( new ArrayList<PluginExecution>( merged.values() ) );
569        }
570    }
571
572    @Override
573    protected void mergePluginExecution_Goals( PluginExecution target, PluginExecution source, boolean sourceDominant,
574                                               Map<Object, Object> context )
575    {
576        List<String> src = source.getGoals();
577        if ( !src.isEmpty() )
578        {
579            List<String> tgt = target.getGoals();
580            Set<String> excludes = new LinkedHashSet<String>( tgt );
581            List<String> merged = new ArrayList<String>( tgt.size() + src.size() );
582            merged.addAll( tgt );
583            for ( String s : src )
584            {
585                if ( !excludes.contains( s ) )
586                {
587                    merged.add( s );
588                }
589            }
590            target.setGoals( merged );
591        }
592    }
593
594    @Override
595    protected void mergeReportPlugin_ReportSets( ReportPlugin target, ReportPlugin source, boolean sourceDominant,
596                                                 Map<Object, Object> context )
597    {
598        List<ReportSet> src = source.getReportSets();
599        if ( !src.isEmpty() )
600        {
601            List<ReportSet> tgt = target.getReportSets();
602            Map<Object, ReportSet> merged = new LinkedHashMap<Object, ReportSet>( ( src.size() + tgt.size() ) * 2 );
603
604            for ( ReportSet element : src )
605            {
606                if ( sourceDominant || ( element.getInherited() != null ? element.isInherited() : source.isInherited() ) )
607                {
608                    Object key = getReportSetKey( element );
609                    merged.put( key, element );
610                }
611            }
612
613            for ( ReportSet element : tgt )
614            {
615                Object key = getReportSetKey( element );
616                ReportSet existing = merged.get( key );
617                if ( existing != null )
618                {
619                    mergeReportSet( element, existing, sourceDominant, context );
620                }
621                merged.put( key, element );
622            }
623
624            target.setReportSets( new ArrayList<ReportSet>( merged.values() ) );
625        }
626    }
627
628    @Override
629    protected Object getDependencyKey( Dependency dependency )
630    {
631        return dependency.getManagementKey();
632    }
633
634    @Override
635    protected Object getPluginKey( Plugin plugin )
636    {
637        return plugin.getKey();
638    }
639
640    @Override
641    protected Object getPluginExecutionKey( PluginExecution pluginExecution )
642    {
643        return pluginExecution.getId();
644    }
645
646    @Override
647    protected Object getReportPluginKey( ReportPlugin reportPlugin )
648    {
649        return reportPlugin.getKey();
650    }
651
652    @Override
653    protected Object getReportSetKey( ReportSet reportSet )
654    {
655        return reportSet.getId();
656    }
657
658    @Override
659    protected Object getRepositoryBaseKey( RepositoryBase repositoryBase )
660    {
661        return repositoryBase.getId();
662    }
663
664    @Override
665    protected Object getExtensionKey( Extension extension )
666    {
667        return extension.getGroupId() + ':' + extension.getArtifactId();
668    }
669
670    @Override
671    protected Object getExclusionKey( Exclusion exclusion )
672    {
673        return exclusion.getGroupId() + ':' + exclusion.getArtifactId();
674    }
675
676    private String appendPath( String parentPath, Map<Object, Object> context )
677    {
678        Object artifactId = context.get( ARTIFACT_ID );
679        Object childPathAdjustment = context.get( CHILD_PATH_ADJUSTMENT );
680
681        if ( artifactId != null && childPathAdjustment != null )
682        {
683            return appendPath( parentPath, artifactId.toString(), childPathAdjustment.toString() );
684        }
685        else
686        {
687            return parentPath;
688        }
689    }
690
691    private String appendPath( String parentPath, String childPath, String pathAdjustment )
692    {
693        String path = parentPath;
694        path = concatPath( path, pathAdjustment );
695        path = concatPath( path, childPath );
696        return path;
697    }
698
699    private String concatPath( String base, String path )
700    {
701        String result = base;
702
703        if ( path != null && path.length() > 0 )
704        {
705            if ( ( result.endsWith( "/" ) && !path.startsWith( "/" ) )
706                || ( !result.endsWith( "/" ) && path.startsWith( "/" ) ) )
707            {
708                result += path;
709            }
710            else if ( result.endsWith( "/" ) && path.startsWith( "/" ) )
711            {
712                result += path.substring( 1 );
713            }
714            else
715            {
716                result += '/';
717                result += path;
718            }
719            if ( base.endsWith( "/" ) && !result.endsWith( "/" ) )
720            {
721                result += '/';
722            }
723        }
724
725        return result;
726    }
727
728}