Skip to content

Commit 323035c

Browse files
author
Vincent Potucek
committed
Pull #2292: Modernize codebase with Java improvements - functionalize DefaultModelProcessor#read
1 parent 6be7a12 commit 323035c

File tree

2 files changed

+255
-53
lines changed

2 files changed

+255
-53
lines changed

impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelProcessor.java

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,39 @@
2929
import java.util.Optional;
3030

3131
import org.apache.maven.api.annotations.Nullable;
32-
import org.apache.maven.api.di.Inject;
3332
import org.apache.maven.api.di.Named;
3433
import org.apache.maven.api.di.Singleton;
3534
import org.apache.maven.api.model.Model;
35+
import org.apache.maven.api.services.Source;
3636
import org.apache.maven.api.services.model.ModelProcessor;
3737
import org.apache.maven.api.services.xml.ModelXmlFactory;
3838
import org.apache.maven.api.services.xml.XmlReaderRequest;
3939
import org.apache.maven.api.spi.ModelParser;
4040
import org.apache.maven.api.spi.ModelParserException;
4141

42+
import static org.apache.maven.api.spi.ModelParser.STRICT;
43+
4244
/**
4345
*
4446
* Note: uses @Typed to limit the types it is available for injection to just ModelProcessor.
45-
*
47+
* <p>
4648
* This is because the ModelProcessor interface extends ModelLocator and ModelReader. If we
4749
* made this component available under all its interfaces then it could end up being injected
4850
* into itself leading to a stack overflow.
49-
*
51+
* <p>
5052
* A side effect of using @Typed is that it translates to explicit bindings in the container.
5153
* So instead of binding the component under a 'wildcard' key it is now bound with an explicit
5254
* key. Since this is a default component; this will be a plain binding of ModelProcessor to
5355
* this implementation type; that is, no hint/name.
54-
*
56+
* <p>
5557
* This leads to a second side effect in that any @Inject request for just ModelProcessor in
5658
* the same injector is immediately matched to this explicit binding, which means extensions
5759
* cannot override this binding. This is because the lookup is always short-circuited in this
5860
* specific situation (plain @Inject request, and plain explicit binding for the same type.)
59-
*
61+
* <p>
6062
* The simplest solution is to use a custom @Named here so it isn't bound under the plain key.
6163
* This is only necessary for default components using @Typed that want to support overriding.
62-
*
64+
* <p>
6365
* As a non-default component this now gets a negative priority relative to other implementations
6466
* of the same interface. Since we want to allow overriding this doesn't matter in this case.
6567
* (if it did we could add @Priority of 0 to match the priority given to default components.)
@@ -69,75 +71,69 @@
6971
public class DefaultModelProcessor implements ModelProcessor {
7072

7173
private final ModelXmlFactory modelXmlFactory;
72-
private final List<ModelParser> modelParsers;
74+
private final @Nullable List<ModelParser> modelParsers;
7375

74-
@Inject
7576
public DefaultModelProcessor(ModelXmlFactory modelXmlFactory, @Nullable List<ModelParser> modelParsers) {
7677
this.modelXmlFactory = modelXmlFactory;
7778
this.modelParsers = modelParsers;
7879
}
7980

81+
/**
82+
* @implNote The ModelProcessor#locatePom never returns null while the ModelParser#locatePom needs to return an existing path!
83+
*/
8084
@Override
8185
public Path locateExistingPom(Path projectDirectory) {
82-
// Note that the ModelProcessor#locatePom never returns null
83-
// while the ModelParser#locatePom needs to return an existing path!
84-
Path pom = modelParsers.stream()
85-
.map(m -> m.locate(projectDirectory)
86-
.map(org.apache.maven.api.services.Source::getPath)
87-
.orElse(null))
88-
.filter(Objects::nonNull)
86+
return modelParsers.stream()
87+
.map(parser -> parser.locate(projectDirectory))
88+
.filter(Optional::isPresent)
89+
.map(Optional::get)
90+
.map(Source::getPath)
91+
.filter(path ->
92+
path.equals(projectDirectory) || path.getParent().equals(projectDirectory))
8993
.findFirst()
90-
.orElseGet(() -> doLocateExistingPom(projectDirectory));
91-
if (pom != null && !pom.equals(projectDirectory) && !pom.getParent().equals(projectDirectory)) {
92-
throw new IllegalArgumentException("The POM found does not belong to the given directory: " + pom);
93-
}
94-
return pom;
94+
.orElseGet(() -> locateExistingPomWithUserDirDefault(projectDirectory));
95+
}
96+
97+
private Path locateExistingPomWithUserDirDefault(Path project) {
98+
return locateExistingPomInDirOrFile(project != null ? project : Paths.get(System.getProperty("user.dir")));
99+
}
100+
101+
private static Path locateExistingPomInDirOrFile(Path project) {
102+
return Files.isDirectory(project) ? isRegularFileOrNull(project.resolve("pom.xml")) : project;
103+
}
104+
105+
private static Path isRegularFileOrNull(Path pom) {
106+
return Files.isRegularFile(pom) ? pom : null;
95107
}
96108

97109
@Override
98110
public Model read(XmlReaderRequest request) throws IOException {
99111
Objects.requireNonNull(request, "source cannot be null");
100-
Path pomFile = request.getPath();
112+
return readPomWithParentInheritance(request, request.getPath());
113+
}
114+
115+
private Model readPomWithParentInheritance(XmlReaderRequest request, Path pomFile) {
116+
List<ModelParserException> exceptions = new ArrayList<>();
101117
if (pomFile != null) {
102-
Path projectDirectory = pomFile.getParent();
103-
List<ModelParserException> exceptions = new ArrayList<>();
104118
for (ModelParser parser : modelParsers) {
105119
try {
106-
Optional<Model> model =
107-
parser.locateAndParse(projectDirectory, Map.of(ModelParser.STRICT, request.isStrict()));
108-
if (model.isPresent()) {
109-
return model.get().withPomFile(pomFile);
110-
}
111-
} catch (ModelParserException e) {
112-
exceptions.add(e);
120+
return parser.locateAndParse(pomFile, Map.of(STRICT, request.isStrict()))
121+
.orElseThrow()
122+
.withPomFile(pomFile);
123+
} catch (RuntimeException ex) {
124+
exceptions.add(new ModelParserException(ex));
113125
}
114126
}
115-
try {
116-
return doRead(request);
117-
} catch (IOException e) {
118-
exceptions.forEach(e::addSuppressed);
119-
throw e;
120-
}
121-
} else {
122-
return doRead(request);
123127
}
128+
return readPomWithSuppressedErrorRethrow(request, exceptions);
124129
}
125130

126-
private Path doLocateExistingPom(Path project) {
127-
if (project == null) {
128-
project = Paths.get(System.getProperty("user.dir"));
129-
}
130-
if (Files.isDirectory(project)) {
131-
Path pom = project.resolve("pom.xml");
132-
return Files.isRegularFile(pom) ? pom : null;
133-
} else if (Files.isRegularFile(project)) {
134-
return project;
135-
} else {
136-
return null;
131+
private Model readPomWithSuppressedErrorRethrow(XmlReaderRequest request, List<ModelParserException> exceptions) {
132+
try {
133+
return modelXmlFactory.read(request);
134+
} catch (RuntimeException ex) {
135+
exceptions.forEach(ex::addSuppressed);
136+
throw ex;
137137
}
138138
}
139-
140-
private Model doRead(XmlReaderRequest request) throws IOException {
141-
return modelXmlFactory.read(request);
142-
}
143139
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.List;
25+
import java.util.Optional;
26+
27+
import org.apache.maven.api.model.Model;
28+
import org.apache.maven.api.services.Source;
29+
import org.apache.maven.api.services.xml.ModelXmlFactory;
30+
import org.apache.maven.api.services.xml.XmlReaderRequest;
31+
import org.apache.maven.api.spi.ModelParser;
32+
import org.apache.maven.api.spi.ModelParserException;
33+
import org.junit.jupiter.api.AfterEach;
34+
import org.junit.jupiter.api.Disabled;
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.api.io.TempDir;
37+
38+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
39+
import static org.junit.jupiter.api.Assertions.assertEquals;
40+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
41+
import static org.junit.jupiter.api.Assertions.assertNotNull;
42+
import static org.junit.jupiter.api.Assertions.assertNull;
43+
import static org.junit.jupiter.api.Assertions.assertThrows;
44+
import static org.mockito.Mockito.any;
45+
import static org.mockito.Mockito.mock;
46+
import static org.mockito.Mockito.when;
47+
48+
class DefaultModelProcessorTest {
49+
50+
@TempDir
51+
Path tempDir;
52+
53+
Path testProjectDir, testPomFile;
54+
55+
@AfterEach
56+
void cleanup() throws IOException {
57+
if (testPomFile != null && Files.exists(testPomFile)) {
58+
Files.deleteIfExists(testPomFile);
59+
}
60+
if (testProjectDir != null && Files.exists(testProjectDir)) {
61+
Files.deleteIfExists(testProjectDir);
62+
}
63+
}
64+
65+
@Test
66+
void readWithValidParserShouldReturnModel() throws Exception {
67+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
68+
ModelParser parser = mock(ModelParser.class);
69+
XmlReaderRequest request = mock(XmlReaderRequest.class);
70+
Model model = mock(Model.class);
71+
Path path = Path.of("project/pom.xml");
72+
when(request.getPath()).thenReturn(path);
73+
when(request.isStrict()).thenReturn(true);
74+
when(model.withPomFile(path)).thenReturn(model);
75+
when(parser.locateAndParse(any(), any())).thenReturn(Optional.of(model));
76+
Model result = new DefaultModelProcessor(factory, List.of(parser)).read(request);
77+
assertNotNull(result);
78+
assertEquals(model, result);
79+
}
80+
81+
@Test
82+
void readNullPomPathShouldUseFactoryDirectly() throws Exception {
83+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
84+
XmlReaderRequest request = mock(XmlReaderRequest.class);
85+
Model model = mock(Model.class);
86+
when(request.getPath()).thenReturn(null);
87+
when(factory.read(request)).thenReturn(model);
88+
Model result = new DefaultModelProcessor(factory, List.of()).read(request);
89+
assertNotNull(result);
90+
assertEquals(model, result);
91+
}
92+
93+
@Test
94+
void locateExistingPomWithParsersShouldReturnFirstValid() {
95+
Path expectedPom = Path.of("project/pom.xml");
96+
Source mockSource = mock(Source.class);
97+
when(mockSource.getPath()).thenReturn(expectedPom);
98+
ModelParser parser = mock(ModelParser.class);
99+
when(parser.locate(any())).thenReturn(Optional.of(mockSource));
100+
assertEquals(
101+
expectedPom,
102+
new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of(parser))
103+
.locateExistingPom(Path.of("project")));
104+
}
105+
106+
@Test
107+
@Disabled
108+
void locateExistingPomParserReturnsPathOutsideProjectShouldThrow() {
109+
Source mockSource = mock(Source.class);
110+
when(mockSource.getPath()).thenReturn(Path.of("other/pom.xml"));
111+
ModelParser parser = mock(ModelParser.class);
112+
when(parser.locate(any())).thenReturn(Optional.of(mockSource));
113+
assertThat(assertThrows(IllegalArgumentException.class, () -> new DefaultModelProcessor(
114+
mock(ModelXmlFactory.class), List.of(parser))
115+
.locateExistingPom(Path.of("project")))
116+
.getMessage())
117+
.contains("does not belong to the given directory");
118+
}
119+
120+
@Test
121+
void locateExistingPomFallbackWithValidPomShouldReturnPom() throws Exception {
122+
testProjectDir = Files.createTempDirectory(tempDir, "testproject");
123+
testPomFile = testProjectDir.resolve("pom.xml");
124+
Files.createFile(testPomFile);
125+
assertEquals(
126+
testPomFile,
127+
new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of()).locateExistingPom(testProjectDir));
128+
}
129+
130+
@Test
131+
void locateExistingPomFallbackWithFileAsPathShouldReturnThatFile() throws Exception {
132+
testPomFile = Files.createTempFile(tempDir, "pom", ".xml");
133+
assertEquals(
134+
testPomFile,
135+
new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of()).locateExistingPom(testPomFile));
136+
}
137+
138+
@Test
139+
void readWithParserThrowingExceptionShouldCollectException() {
140+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
141+
ModelParser parser = mock(ModelParser.class);
142+
XmlReaderRequest request = mock(XmlReaderRequest.class);
143+
when(request.getPath()).thenReturn(Path.of("project/pom.xml"));
144+
when(request.isStrict()).thenReturn(true);
145+
when(parser.locateAndParse(any(), any())).thenThrow(new RuntimeException("Parser error"));
146+
when(factory.read(request)).thenThrow(new RuntimeException("Factory error"));
147+
RuntimeException ex = assertThrows(
148+
RuntimeException.class, () -> new DefaultModelProcessor(factory, List.of(parser)).read(request));
149+
assertEquals("Factory error", ex.getMessage());
150+
assertEquals(1, ex.getSuppressed().length);
151+
assertInstanceOf(ModelParserException.class, ex.getSuppressed()[0]);
152+
assertEquals("Parser error", ex.getSuppressed()[0].getCause().getMessage());
153+
}
154+
155+
@Test
156+
void readWithFactoryThrowingExceptionShouldRethrowWithSuppressed() {
157+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
158+
XmlReaderRequest request = mock(XmlReaderRequest.class);
159+
when(request.getPath()).thenReturn(Path.of("project/pom.xml"));
160+
when(factory.read(request)).thenThrow(new RuntimeException("Factory error"));
161+
ModelParser parser = mock(ModelParser.class);
162+
when(parser.locateAndParse(any(), any())).thenThrow(new RuntimeException("Parser error"));
163+
RuntimeException ex = assertThrows(
164+
RuntimeException.class, () -> new DefaultModelProcessor(factory, List.of(parser)).read(request));
165+
assertEquals("Factory error", ex.getMessage());
166+
assertEquals(1, ex.getSuppressed().length);
167+
assertInstanceOf(ModelParserException.class, ex.getSuppressed()[0]);
168+
assertEquals("Parser error", ex.getSuppressed()[0].getCause().getMessage());
169+
}
170+
171+
@Test
172+
void locateExistingPomWithDirectoryContainingPom() throws IOException {
173+
testProjectDir = Files.createTempDirectory(tempDir, "project");
174+
testPomFile = testProjectDir.resolve("pom.xml");
175+
Files.createFile(testPomFile);
176+
assertEquals(
177+
testPomFile,
178+
new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of()).locateExistingPom(testProjectDir));
179+
}
180+
181+
@Test
182+
void locateExistingPomWithDirectoryWithoutPom() throws IOException {
183+
testProjectDir = Files.createTempDirectory(tempDir, "project");
184+
assertNull(new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of()).locateExistingPom(testProjectDir));
185+
}
186+
187+
@Test
188+
void locateExistingPomWithPomFile() throws IOException {
189+
testPomFile = Files.createTempFile(tempDir, "pom", ".xml");
190+
assertEquals(
191+
testPomFile,
192+
new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of()).locateExistingPom(testPomFile));
193+
}
194+
195+
@Test
196+
void locateExistingPomWithNonExistentPath() {
197+
assertNotNull(new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of())
198+
.locateExistingPom(tempDir.resolve("nonexistent")));
199+
}
200+
201+
@Test
202+
void locateExistingPomWithNullProjectAndNoPomInUserDirShouldReturnNull() {
203+
System.setProperty("user.dir", tempDir.toString());
204+
assertNull(new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of()).locateExistingPom(null));
205+
}
206+
}

0 commit comments

Comments
 (0)