001package org.apache.maven.model.inheritance; 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.io.File; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.maven.model.Model; 030import org.apache.maven.model.Plugin; 031import org.apache.maven.model.PluginContainer; 032import org.apache.maven.model.ReportPlugin; 033import org.apache.maven.model.Reporting; 034import org.apache.maven.model.building.ModelBuildingRequest; 035import org.apache.maven.model.building.ModelProblemCollector; 036import org.apache.maven.model.merge.MavenModelMerger; 037import org.codehaus.plexus.component.annotations.Component; 038import org.codehaus.plexus.util.StringUtils; 039 040/** 041 * Handles inheritance of model values. 042 * 043 * @author Benjamin Bentmann 044 */ 045@Component( role = InheritanceAssembler.class ) 046public class DefaultInheritanceAssembler 047 implements InheritanceAssembler 048{ 049 050 private InheritanceModelMerger merger = new InheritanceModelMerger(); 051 052 @Override 053 public void assembleModelInheritance( Model child, Model parent, ModelBuildingRequest request, 054 ModelProblemCollector problems ) 055 { 056 Map<Object, Object> hints = new HashMap<>(); 057 hints.put( MavenModelMerger.CHILD_PATH_ADJUSTMENT, getChildPathAdjustment( child, parent ) ); 058 merger.merge( child, parent, false, hints ); 059 } 060 061 /** 062 * Calculates the relative path from the base directory of the parent to the parent directory of the base directory 063 * of the child. The general idea is to adjust inherited URLs to match the project layout (in SCM). 064 * 065 * <p>This calculation is only a heuristic based on our conventions. 066 * In detail, the algo relies on the following assumptions: <ul> 067 * <li>The parent uses aggregation and refers to the child via the modules section</li> 068 * <li>The module path to the child is considered to 069 * point at the POM rather than its base directory if the path ends with ".xml" (ignoring case)</li> 070 * <li>The name of the child's base directory matches the artifact id of the child.</li> 071 * </ul> 072 * Note that for the sake of independence from the user 073 * environment, the filesystem is intentionally not used for the calculation.</p> 074 * 075 * @param child The child model, must not be <code>null</code>. 076 * @param parent The parent model, may be <code>null</code>. 077 * @return The path adjustment, can be empty but never <code>null</code>. 078 */ 079 private String getChildPathAdjustment( Model child, Model parent ) 080 { 081 String adjustment = ""; 082 083 if ( parent != null ) 084 { 085 String childName = child.getArtifactId(); 086 087 /* 088 * This logic (using filesystem, against wanted independance from the user environment) exists only for the 089 * sake of backward-compat with 2.x (MNG-5000). In general, it is wrong to 090 * base URL inheritance on the module directory names as this information is unavailable for POMs in the 091 * repository. In other words, modules where artifactId != moduleDirName will see different effective URLs 092 * depending on how the model was constructed (from filesystem or from repository). 093 */ 094 File childDirectory = child.getProjectDirectory(); 095 if ( childDirectory != null ) 096 { 097 childName = childDirectory.getName(); 098 } 099 100 for ( String module : parent.getModules() ) 101 { 102 module = module.replace( '\\', '/' ); 103 104 if ( module.regionMatches( true, module.length() - 4, ".xml", 0, 4 ) ) 105 { 106 module = module.substring( 0, module.lastIndexOf( '/' ) + 1 ); 107 } 108 109 String moduleName = module; 110 if ( moduleName.endsWith( "/" ) ) 111 { 112 moduleName = moduleName.substring( 0, moduleName.length() - 1 ); 113 } 114 115 int lastSlash = moduleName.lastIndexOf( '/' ); 116 117 moduleName = moduleName.substring( lastSlash + 1 ); 118 119 if ( moduleName.equals( childName ) && lastSlash >= 0 ) 120 { 121 adjustment = module.substring( 0, lastSlash ); 122 break; 123 } 124 } 125 } 126 127 return adjustment; 128 } 129 130 protected static class InheritanceModelMerger 131 extends MavenModelMerger 132 { 133 134 @Override 135 protected String extrapolateChildUrl( String parentUrl, Map<Object, Object> context ) 136 { 137 Object artifactId = context.get( ARTIFACT_ID ); 138 Object childPathAdjustment = context.get( CHILD_PATH_ADJUSTMENT ); 139 140 if ( artifactId != null && childPathAdjustment != null && StringUtils.isNotBlank( parentUrl ) ) 141 { 142 // append childPathAdjustment and artifactId to parent url 143 return appendPath( parentUrl, artifactId.toString(), childPathAdjustment.toString() ); 144 } 145 else 146 { 147 return parentUrl; 148 } 149 } 150 151 private String appendPath( String parentUrl, String childPath, String pathAdjustment ) 152 { 153 StringBuilder url = new StringBuilder( parentUrl.length() + pathAdjustment.length() + childPath.length() 154 + ( ( pathAdjustment.length() == 0 ) ? 1 : 2 ) ); 155 156 url.append( parentUrl ); 157 concatPath( url, pathAdjustment ); 158 concatPath( url, childPath ); 159 160 return url.toString(); 161 } 162 163 private void concatPath( StringBuilder url, String path ) 164 { 165 if ( path.length() > 0 ) 166 { 167 boolean initialUrlEndsWithSlash = url.charAt( url.length() - 1 ) == '/'; 168 boolean pathStartsWithSlash = path.charAt( 0 ) == '/'; 169 170 if ( pathStartsWithSlash ) 171 { 172 if ( initialUrlEndsWithSlash ) 173 { 174 // 1 extra '/' to remove 175 url.setLength( url.length() - 1 ); 176 } 177 } 178 else if ( !initialUrlEndsWithSlash ) 179 { 180 // add missing '/' between url and path 181 url.append( '/' ); 182 } 183 184 url.append( path ); 185 186 // ensure resulting url ends with slash if initial url was 187 if ( initialUrlEndsWithSlash && !path.endsWith( "/" ) ) 188 { 189 url.append( '/' ); 190 } 191 } 192 } 193 194 @Override 195 protected void mergePluginContainer_Plugins( PluginContainer target, PluginContainer source, 196 boolean sourceDominant, Map<Object, Object> context ) 197 { 198 List<Plugin> src = source.getPlugins(); 199 if ( !src.isEmpty() ) 200 { 201 List<Plugin> tgt = target.getPlugins(); 202 Map<Object, Plugin> master = new LinkedHashMap<>( src.size() * 2 ); 203 204 for ( Plugin element : src ) 205 { 206 if ( element.isInherited() || !element.getExecutions().isEmpty() ) 207 { 208 // NOTE: Enforce recursive merge to trigger merging/inheritance logic for executions 209 Plugin plugin = new Plugin(); 210 plugin.setLocation( "", element.getLocation( "" ) ); 211 plugin.setGroupId( null ); 212 mergePlugin( plugin, element, sourceDominant, context ); 213 214 Object key = getPluginKey( element ); 215 216 master.put( key, plugin ); 217 } 218 } 219 220 Map<Object, List<Plugin>> predecessors = new LinkedHashMap<>(); 221 List<Plugin> pending = new ArrayList<>(); 222 for ( Plugin element : tgt ) 223 { 224 Object key = getPluginKey( element ); 225 Plugin existing = master.get( key ); 226 if ( existing != null ) 227 { 228 mergePlugin( element, existing, sourceDominant, context ); 229 230 master.put( key, element ); 231 232 if ( !pending.isEmpty() ) 233 { 234 predecessors.put( key, pending ); 235 pending = new ArrayList<>(); 236 } 237 } 238 else 239 { 240 pending.add( element ); 241 } 242 } 243 244 List<Plugin> result = new ArrayList<>( src.size() + tgt.size() ); 245 for ( Map.Entry<Object, Plugin> entry : master.entrySet() ) 246 { 247 List<Plugin> pre = predecessors.get( entry.getKey() ); 248 if ( pre != null ) 249 { 250 result.addAll( pre ); 251 } 252 result.add( entry.getValue() ); 253 } 254 result.addAll( pending ); 255 256 target.setPlugins( result ); 257 } 258 } 259 260 @Override 261 protected void mergePlugin( Plugin target, Plugin source, boolean sourceDominant, Map<Object, Object> context ) 262 { 263 if ( source.isInherited() ) 264 { 265 mergeConfigurationContainer( target, source, sourceDominant, context ); 266 } 267 mergePlugin_GroupId( target, source, sourceDominant, context ); 268 mergePlugin_ArtifactId( target, source, sourceDominant, context ); 269 mergePlugin_Version( target, source, sourceDominant, context ); 270 mergePlugin_Extensions( target, source, sourceDominant, context ); 271 mergePlugin_Dependencies( target, source, sourceDominant, context ); 272 mergePlugin_Executions( target, source, sourceDominant, context ); 273 } 274 275 @Override 276 protected void mergeReporting_Plugins( Reporting target, Reporting source, boolean sourceDominant, 277 Map<Object, Object> context ) 278 { 279 List<ReportPlugin> src = source.getPlugins(); 280 if ( !src.isEmpty() ) 281 { 282 List<ReportPlugin> tgt = target.getPlugins(); 283 Map<Object, ReportPlugin> merged = 284 new LinkedHashMap<>( ( src.size() + tgt.size() ) * 2 ); 285 286 for ( ReportPlugin element : src ) 287 { 288 Object key = getReportPluginKey( element ); 289 if ( element.isInherited() ) 290 { 291 // NOTE: Enforce recursive merge to trigger merging/inheritance logic for executions as well 292 ReportPlugin plugin = new ReportPlugin(); 293 plugin.setLocation( "", element.getLocation( "" ) ); 294 plugin.setGroupId( null ); 295 mergeReportPlugin( plugin, element, sourceDominant, context ); 296 297 merged.put( key, plugin ); 298 } 299 } 300 301 for ( ReportPlugin element : tgt ) 302 { 303 Object key = getReportPluginKey( element ); 304 ReportPlugin existing = merged.get( key ); 305 if ( existing != null ) 306 { 307 mergeReportPlugin( element, existing, sourceDominant, context ); 308 } 309 merged.put( key, element ); 310 } 311 312 target.setPlugins( new ArrayList<>( merged.values() ) ); 313 } 314 } 315 } 316 317}