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