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