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