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