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