View Javadoc
1   package org.apache.maven.model.inheritance;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.inject.Named;
29  import javax.inject.Singleton;
30  
31  import org.apache.maven.api.model.InputLocation;
32  import org.apache.maven.api.model.Model;
33  import org.apache.maven.api.model.ModelBase;
34  import org.apache.maven.api.model.Plugin;
35  import org.apache.maven.api.model.PluginContainer;
36  import org.apache.maven.api.model.ReportPlugin;
37  import org.apache.maven.api.model.Reporting;
38  import org.apache.maven.model.building.ModelBuildingRequest;
39  import org.apache.maven.model.building.ModelProblemCollector;
40  import org.apache.maven.model.merge.MavenModelMerger;
41  import org.codehaus.plexus.util.StringUtils;
42  
43  /**
44   * Handles inheritance of model values.
45   *
46   * @author Benjamin Bentmann
47   */
48  @SuppressWarnings( { "checkstyle:methodname" } )
49  @Named
50  @Singleton
51  public class DefaultInheritanceAssembler
52      implements InheritanceAssembler
53  {
54  
55      private InheritanceModelMerger merger = new InheritanceModelMerger();
56  
57      private static final String CHILD_DIRECTORY = "child-directory";
58  
59      private static final String CHILD_DIRECTORY_PROPERTY = "project.directory";
60  
61      @Override
62      public Model assembleModelInheritance( Model child, Model parent, ModelBuildingRequest request,
63                                            ModelProblemCollector problems )
64      {
65          Map<Object, Object> hints = new HashMap<>();
66          String childPath = child.getProperties().getOrDefault( CHILD_DIRECTORY_PROPERTY, child.getArtifactId() );
67          hints.put( CHILD_DIRECTORY, childPath );
68          hints.put( MavenModelMerger.CHILD_PATH_ADJUSTMENT, getChildPathAdjustment( child, parent, childPath ) );
69          return merger.merge( child, parent, false, hints );
70      }
71  
72      /**
73       * Calculates the relative path from the base directory of the parent to the parent directory of the base directory
74       * of the child. The general idea is to adjust inherited URLs to match the project layout (in SCM).
75       *
76       * <p>This calculation is only a heuristic based on our conventions.
77       * In detail, the algo relies on the following assumptions: <ul>
78       * <li>The parent uses aggregation and refers to the child via the modules section</li>
79       * <li>The module path to the child is considered to
80       * point at the POM rather than its base directory if the path ends with ".xml" (ignoring case)</li>
81       * <li>The name of the child's base directory matches the artifact id of the child.</li>
82       * </ul>
83       * Note that for the sake of independence from the user
84       * environment, the filesystem is intentionally not used for the calculation.</p>
85       *
86       * @param child The child model, must not be <code>null</code>.
87       * @param parent The parent model, may be <code>null</code>.
88       * @param childDirectory The directory defined in child model, may be <code>null</code>.
89       * @return The path adjustment, can be empty but never <code>null</code>.
90       */
91      private String getChildPathAdjustment( Model child, Model parent, String childDirectory )
92      {
93          String adjustment = "";
94  
95          if ( parent != null )
96          {
97              String childName = child.getArtifactId();
98  
99              /*
100              * This logic (using filesystem, against wanted independence from the user environment) exists only for the
101              * sake of backward-compat with 2.x (MNG-5000). In general, it is wrong to
102              * base URL inheritance on the module directory names as this information is unavailable for POMs in the
103              * repository. In other words, modules where artifactId != moduleDirName will see different effective URLs
104              * depending on how the model was constructed (from filesystem or from repository).
105              */
106             if ( child.getProjectDirectory() != null )
107             {
108                 childName = child.getProjectDirectory().getFileName().toString();
109             }
110 
111             for ( String module : parent.getModules() )
112             {
113                 module = module.replace( '\\', '/' );
114 
115                 if ( module.regionMatches( true, module.length() - 4, ".xml", 0, 4 ) )
116                 {
117                     module = module.substring( 0, module.lastIndexOf( '/' ) + 1 );
118                 }
119 
120                 String moduleName = module;
121                 if ( moduleName.endsWith( "/" ) )
122                 {
123                     moduleName = moduleName.substring( 0, moduleName.length() - 1 );
124                 }
125 
126                 int lastSlash = moduleName.lastIndexOf( '/' );
127 
128                 moduleName = moduleName.substring( lastSlash + 1 );
129 
130                 if ( ( moduleName.equals( childName ) || ( moduleName.equals( childDirectory ) ) ) && lastSlash >= 0 )
131                 {
132                     adjustment = module.substring( 0, lastSlash );
133                     break;
134                 }
135             }
136         }
137 
138         return adjustment;
139     }
140 
141     /**
142      * InheritanceModelMerger
143      */
144     protected static class InheritanceModelMerger
145         extends MavenModelMerger
146     {
147 
148         @Override
149         protected String extrapolateChildUrl( String parentUrl, boolean appendPath, Map<Object, Object> context )
150         {
151             Object childDirectory = context.get( CHILD_DIRECTORY );
152             Object childPathAdjustment = context.get( CHILD_PATH_ADJUSTMENT );
153 
154             if ( StringUtils.isBlank( parentUrl ) || childDirectory == null || childPathAdjustment == null
155                 || !appendPath )
156             {
157                 return parentUrl;
158             }
159 
160             // append childPathAdjustment and childDirectory to parent url
161             return appendPath( parentUrl, childDirectory.toString(), childPathAdjustment.toString() );
162         }
163 
164         private String appendPath( String parentUrl, String childPath, String pathAdjustment )
165         {
166             StringBuilder url = new StringBuilder( parentUrl.length() + pathAdjustment.length() + childPath.length()
167                 + ( ( pathAdjustment.length() == 0 ) ? 1 : 2 ) );
168 
169             url.append( parentUrl );
170             concatPath( url, pathAdjustment );
171             concatPath( url, childPath );
172 
173             return url.toString();
174         }
175 
176         private void concatPath( StringBuilder url, String path )
177         {
178             if ( path.length() > 0 )
179             {
180                 boolean initialUrlEndsWithSlash = url.charAt( url.length() - 1 ) == '/';
181                 boolean pathStartsWithSlash = path.charAt( 0 ) ==  '/';
182 
183                 if ( pathStartsWithSlash )
184                 {
185                     if ( initialUrlEndsWithSlash )
186                     {
187                         // 1 extra '/' to remove
188                         url.setLength( url.length() - 1 );
189                     }
190                 }
191                 else if ( !initialUrlEndsWithSlash )
192                 {
193                     // add missing '/' between url and path
194                     url.append( '/' );
195                 }
196 
197                 url.append( path );
198 
199                 // ensure resulting url ends with slash if initial url was
200                 if ( initialUrlEndsWithSlash && !path.endsWith( "/" ) )
201                 {
202                     url.append( '/' );
203                 }
204             }
205         }
206 
207         @Override
208         protected void mergeModelBase_Properties( ModelBase.Builder builder,
209                                                   ModelBase target, ModelBase source, boolean sourceDominant,
210                                                   Map<Object, Object> context )
211         {
212             Map<String, String> merged = new HashMap<>();
213             if ( sourceDominant )
214             {
215                 merged.putAll( target.getProperties() );
216                 putAll( merged, source.getProperties(), CHILD_DIRECTORY_PROPERTY );
217             }
218             else
219             {
220                 putAll( merged, source.getProperties(), CHILD_DIRECTORY_PROPERTY );
221                 merged.putAll( target.getProperties() );
222             }
223             builder.properties( merged );
224             builder.location( "properties",
225                                 InputLocation.merge( target.getLocation( "properties" ),
226                                                      source.getLocation( "properties" ), sourceDominant ) );
227         }
228 
229         private void putAll( Map<String, String> s, Map<String, String> t, Object excludeKey )
230         {
231             for ( Map.Entry<String, String> e : t.entrySet() )
232             {
233                 if ( !e.getKey().equals( excludeKey ) )
234                 {
235                     s.put( e.getKey(), e.getValue() );
236                 }
237             }
238         }
239 
240         @Override
241         protected void mergePluginContainer_Plugins( PluginContainer.Builder builder,
242                                                      PluginContainer target, PluginContainer source,
243                                                      boolean sourceDominant, Map<Object, Object> context )
244         {
245             List<Plugin> src = source.getPlugins();
246             if ( !src.isEmpty() )
247             {
248                 List<Plugin> tgt = target.getPlugins();
249                 Map<Object, Plugin> master = new LinkedHashMap<>( src.size() * 2 );
250 
251                 for ( Plugin element : src )
252                 {
253                     if ( element.isInherited() || !element.getExecutions().isEmpty() )
254                     {
255                         // NOTE: Enforce recursive merge to trigger merging/inheritance logic for executions
256                         Plugin plugin = Plugin.newInstance( false );
257                         plugin = mergePlugin( plugin, element, sourceDominant, context );
258 
259                         Object key = getPluginKey().apply( plugin );
260 
261                         master.put( key, plugin );
262                     }
263                 }
264 
265                 Map<Object, List<Plugin>> predecessors = new LinkedHashMap<>();
266                 List<Plugin> pending = new ArrayList<>();
267                 for ( Plugin element : tgt )
268                 {
269                     Object key = getPluginKey().apply( element );
270                     Plugin existing = master.get( key );
271                     if ( existing != null )
272                     {
273                         element = mergePlugin( element, existing, sourceDominant, context );
274 
275                         master.put( key, element );
276 
277                         if ( !pending.isEmpty() )
278                         {
279                             predecessors.put( key, pending );
280                             pending = new ArrayList<>();
281                         }
282                     }
283                     else
284                     {
285                         pending.add( element );
286                     }
287                 }
288 
289                 List<Plugin> result = new ArrayList<>( src.size() + tgt.size() );
290                 for ( Map.Entry<Object, Plugin> entry : master.entrySet() )
291                 {
292                     List<Plugin> pre = predecessors.get( entry.getKey() );
293                     if ( pre != null )
294                     {
295                         result.addAll( pre );
296                     }
297                     result.add( entry.getValue() );
298                 }
299                 result.addAll( pending );
300 
301                 builder.plugins( result );
302             }
303         }
304 
305         @Override
306         protected Plugin mergePlugin( Plugin target, Plugin source,
307                                       boolean sourceDominant, Map<Object, Object> context )
308         {
309             Plugin.Builder builder = Plugin.newBuilder( target );
310             if ( source.isInherited() )
311             {
312                 mergeConfigurationContainer( builder, target, source, sourceDominant, context );
313             }
314             mergePlugin_GroupId( builder, target, source, sourceDominant, context );
315             mergePlugin_ArtifactId( builder, target, source, sourceDominant, context );
316             mergePlugin_Version( builder, target, source, sourceDominant, context );
317             mergePlugin_Extensions( builder, target, source, sourceDominant, context );
318             mergePlugin_Executions( builder, target, source, sourceDominant, context );
319             mergePlugin_Dependencies( builder, target, source, sourceDominant, context );
320             return builder.build();
321         }
322 
323         @Override
324         protected void mergeReporting_Plugins( Reporting.Builder builder,
325                                                Reporting target, Reporting source, boolean sourceDominant,
326                                                Map<Object, Object> context )
327         {
328             List<ReportPlugin> src = source.getPlugins();
329             if ( !src.isEmpty() )
330             {
331                 List<ReportPlugin> tgt = target.getPlugins();
332                 Map<Object, ReportPlugin> merged =
333                     new LinkedHashMap<>( ( src.size() + tgt.size() ) * 2 );
334 
335                 for ( ReportPlugin element :  src )
336                 {
337                     if ( element.isInherited() )
338                     {
339                         // NOTE: Enforce recursive merge to trigger merging/inheritance logic for executions as well
340                         ReportPlugin plugin = ReportPlugin.newInstance( false );
341                         plugin = mergeReportPlugin( plugin, element, sourceDominant, context );
342 
343                         merged.put( getReportPluginKey().apply( element ), plugin );
344                     }
345                 }
346 
347                 for ( ReportPlugin element : tgt )
348                 {
349                     Object key = getReportPluginKey().apply( element );
350                     ReportPlugin existing = merged.get( key );
351                     if ( existing != null )
352                     {
353                         element = mergeReportPlugin( element, existing, sourceDominant, context );
354                     }
355                     merged.put( key, element );
356                 }
357 
358                 builder.plugins( merged.values() );
359             }
360         }
361     }
362 
363 }