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