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.io.IOException;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.nio.file.Paths;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.Optional;
30
31 import org.apache.maven.api.di.Inject;
32 import org.apache.maven.api.di.Named;
33 import org.apache.maven.api.di.Singleton;
34 import org.apache.maven.api.model.Model;
35 import org.apache.maven.api.services.model.ModelProcessor;
36 import org.apache.maven.api.services.xml.ModelXmlFactory;
37 import org.apache.maven.api.services.xml.XmlReaderRequest;
38 import org.apache.maven.api.spi.ModelParser;
39 import org.apache.maven.api.spi.ModelParserException;
40
41 /**
42 *
43 * Note: uses @Typed to limit the types it is available for injection to just ModelProcessor.
44 *
45 * This is because the ModelProcessor interface extends ModelLocator and ModelReader. If we
46 * made this component available under all its interfaces then it could end up being injected
47 * into itself leading to a stack overflow.
48 *
49 * A side effect of using @Typed is that it translates to explicit bindings in the container.
50 * So instead of binding the component under a 'wildcard' key it is now bound with an explicit
51 * key. Since this is a default component this will be a plain binding of ModelProcessor to
52 * this implementation type, ie. no hint/name.
53 *
54 * This leads to a second side effect in that any @Inject request for just ModelProcessor in
55 * the same injector is immediately matched to this explicit binding, which means extensions
56 * cannot override this binding. This is because the lookup is always short-circuited in this
57 * specific situation (plain @Inject request, and plain explicit binding for the same type.)
58 *
59 * The simplest solution is to use a custom @Named here so it isn't bound under the plain key.
60 * This is only necessary for default components using @Typed that want to support overriding.
61 *
62 * As a non-default component this now gets a negative priority relative to other implementations
63 * of the same interface. Since we want to allow overriding this doesn't matter in this case.
64 * (if it did we could add @Priority of 0 to match the priority given to default components.)
65 */
66 @Named
67 @Singleton
68 public class DefaultModelProcessor implements ModelProcessor {
69
70 private final ModelXmlFactory modelXmlFactory;
71 private final List<ModelParser> modelParsers;
72
73 @Inject
74 public DefaultModelProcessor(ModelXmlFactory modelXmlFactory, List<ModelParser> modelParsers) {
75 this.modelXmlFactory = modelXmlFactory;
76 this.modelParsers = modelParsers;
77 }
78
79 @Override
80 public Path locateExistingPom(Path projectDirectory) {
81 // Note that the ModelProcessor#locatePom never returns null
82 // while the ModelParser#locatePom needs to return an existing path!
83 Path pom = modelParsers.stream()
84 .map(m -> m.locate(projectDirectory)
85 .map(org.apache.maven.api.services.Source::getPath)
86 .orElse(null))
87 .filter(Objects::nonNull)
88 .findFirst()
89 .orElseGet(() -> doLocateExistingPom(projectDirectory));
90 if (pom != null && !pom.equals(projectDirectory) && !pom.getParent().equals(projectDirectory)) {
91 throw new IllegalArgumentException("The POM found does not belong to the given directory: " + pom);
92 }
93 return pom;
94 }
95
96 @Override
97 public Model read(XmlReaderRequest request) throws IOException {
98 Objects.requireNonNull(request, "source cannot be null");
99 Path pomFile = request.getPath();
100 if (pomFile != null) {
101 Path projectDirectory = pomFile.getParent();
102 List<ModelParserException> exceptions = new ArrayList<>();
103 for (ModelParser parser : modelParsers) {
104 try {
105 Optional<Model> model =
106 parser.locateAndParse(projectDirectory, Map.of(ModelParser.STRICT, request.isStrict()));
107 if (model.isPresent()) {
108 return model.get().withPomFile(pomFile);
109 }
110 } catch (ModelParserException e) {
111 exceptions.add(e);
112 }
113 }
114 try {
115 return doRead(request);
116 } catch (IOException e) {
117 exceptions.forEach(e::addSuppressed);
118 throw e;
119 }
120 } else {
121 return doRead(request);
122 }
123 }
124
125 private Path doLocateExistingPom(Path project) {
126 if (project == null) {
127 project = Paths.get(System.getProperty("user.dir"));
128 }
129 if (Files.isDirectory(project)) {
130 Path pom = project.resolve("pom.xml");
131 return Files.isRegularFile(pom) ? pom : null;
132 } else if (Files.isRegularFile(project)) {
133 return project;
134 } else {
135 return null;
136 }
137 }
138
139 private Model doRead(XmlReaderRequest request) throws IOException {
140 return modelXmlFactory.read(request);
141 }
142 }