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.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.*;
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 }