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