Skip to content

Commit 0cd0f4e

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

File tree

2 files changed

+160
-19
lines changed

2 files changed

+160
-19
lines changed

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

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,24 @@
4242
/**
4343
*
4444
* Note: uses @Typed to limit the types it is available for injection to just ModelProcessor.
45-
*
45+
* <p>
4646
* This is because the ModelProcessor interface extends ModelLocator and ModelReader. If we
4747
* made this component available under all its interfaces then it could end up being injected
4848
* into itself leading to a stack overflow.
49-
*
49+
* <p>
5050
* A side effect of using @Typed is that it translates to explicit bindings in the container.
5151
* So instead of binding the component under a 'wildcard' key it is now bound with an explicit
5252
* key. Since this is a default component; this will be a plain binding of ModelProcessor to
5353
* this implementation type; that is, no hint/name.
54-
*
54+
* <p>
5555
* This leads to a second side effect in that any @Inject request for just ModelProcessor in
5656
* the same injector is immediately matched to this explicit binding, which means extensions
5757
* cannot override this binding. This is because the lookup is always short-circuited in this
5858
* specific situation (plain @Inject request, and plain explicit binding for the same type.)
59-
*
59+
* <p>
6060
* The simplest solution is to use a custom @Named here so it isn't bound under the plain key.
6161
* This is only necessary for default components using @Typed that want to support overriding.
62-
*
62+
* <p>
6363
* As a non-default component this now gets a negative priority relative to other implementations
6464
* of the same interface. Since we want to allow overriding this doesn't matter in this case.
6565
* (if it did we could add @Priority of 0 to match the priority given to default components.)
@@ -77,10 +77,12 @@ public DefaultModelProcessor(ModelXmlFactory modelXmlFactory, @Nullable List<Mod
7777
this.modelParsers = modelParsers;
7878
}
7979

80+
/**
81+
* @implNote
82+
* 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!
8486
Path pom = modelParsers.stream()
8587
.map(m -> m.locate(projectDirectory)
8688
.map(org.apache.maven.api.services.Source::getPath)
@@ -97,32 +99,32 @@ public Path locateExistingPom(Path projectDirectory) {
9799
@Override
98100
public Model read(XmlReaderRequest request) throws IOException {
99101
Objects.requireNonNull(request, "source cannot be null");
100-
Path pomFile = request.getPath();
102+
return readOnSelfOrParent(request, request.getPath());
103+
}
104+
105+
private Model readOnSelfOrParent(XmlReaderRequest request, Path pomFile) {
101106
if (pomFile != null) {
102-
Path projectDirectory = pomFile.getParent();
103107
List<ModelParserException> exceptions = new ArrayList<>();
104108
for (ModelParser parser : modelParsers) {
105109
try {
106-
Optional<Model> model =
107-
parser.locateAndParse(projectDirectory, Map.of(ModelParser.STRICT, request.isStrict()));
110+
Optional<Model> model = locateAndParseParent(request, pomFile, parser);
108111
if (model.isPresent()) {
109112
return model.get().withPomFile(pomFile);
110113
}
111114
} catch (ModelParserException e) {
112115
exceptions.add(e);
113116
}
114117
}
115-
try {
116-
return doRead(request);
117-
} catch (IOException e) {
118-
exceptions.forEach(e::addSuppressed);
119-
throw e;
120-
}
118+
return readFromFactory(request);
121119
} else {
122-
return doRead(request);
120+
return readFromFactory(request);
123121
}
124122
}
125123

124+
private static Optional<Model> locateAndParseParent(XmlReaderRequest request, Path pomFile, ModelParser parser) {
125+
return parser.locateAndParse(pomFile.getParent(), Map.of(ModelParser.STRICT, request.isStrict()));
126+
}
127+
126128
private Path doLocateExistingPom(Path project) {
127129
if (project == null) {
128130
project = Paths.get(System.getProperty("user.dir"));
@@ -137,7 +139,7 @@ private Path doLocateExistingPom(Path project) {
137139
}
138140
}
139141

140-
private Model doRead(XmlReaderRequest request) throws IOException {
142+
private Model readFromFactory(XmlReaderRequest request) {
141143
return modelXmlFactory.read(request);
142144
}
143145
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package org.apache.maven.impl.model;
2+
3+
import org.apache.maven.api.model.Model;
4+
import org.apache.maven.api.services.Source;
5+
import org.apache.maven.api.services.xml.Location;
6+
import org.apache.maven.api.services.xml.ModelXmlFactory;
7+
import org.apache.maven.api.services.xml.XmlReaderException;
8+
import org.apache.maven.api.services.xml.XmlReaderRequest;
9+
import org.apache.maven.api.spi.ModelParser;
10+
import org.apache.maven.api.spi.ModelParserException;
11+
import org.junit.jupiter.api.Test;
12+
13+
import java.io.IOException;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.util.*;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
20+
21+
class DefaultModelProcessorTest {
22+
23+
@Test
24+
void read_withValidParser_shouldReturnModel() throws Exception {
25+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
26+
ModelParser parser = mock(ModelParser.class);
27+
XmlReaderRequest request = mock(XmlReaderRequest.class);
28+
Model model = mock(Model.class);
29+
Path path = Path.of("project/pom.xml");
30+
31+
when(request.getPath()).thenReturn(path);
32+
when(request.isStrict()).thenReturn(true);
33+
when(model.withPomFile(path)).thenReturn(model);
34+
when(parser.locateAndParse(any(), any())).thenReturn(Optional.of(model));
35+
36+
DefaultModelProcessor processor = new DefaultModelProcessor(factory, List.of(parser));
37+
Model result = processor.read(request);
38+
39+
assertNotNull(result);
40+
assertEquals(model, result);
41+
}
42+
43+
@Test
44+
void read_allParsersFail_shouldFallbackToFactory() throws Exception {
45+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
46+
ModelParser parser = mock(ModelParser.class);
47+
XmlReaderRequest request = mock(XmlReaderRequest.class);
48+
Model fallbackModel = mock(Model.class);
49+
Path path = Path.of("project/pom.xml");
50+
51+
when(request.getPath()).thenReturn(path);
52+
when(request.isStrict()).thenReturn(false);
53+
when(parser.locateAndParse(any(), any()))
54+
.thenThrow(new ModelParserException("parse fail"));
55+
56+
when(factory.read(request)).thenReturn(fallbackModel);
57+
58+
DefaultModelProcessor processor = new DefaultModelProcessor(factory, List.of(parser));
59+
Model result = processor.read(request);
60+
61+
assertNotNull(result);
62+
assertEquals(fallbackModel, result);
63+
}
64+
65+
@Test
66+
void read_nullPomPath_shouldUseFactoryDirectly() throws Exception {
67+
ModelXmlFactory factory = mock(ModelXmlFactory.class);
68+
XmlReaderRequest request = mock(XmlReaderRequest.class);
69+
Model model = mock(Model.class);
70+
71+
when(request.getPath()).thenReturn(null);
72+
when(factory.read(request)).thenReturn(model);
73+
74+
DefaultModelProcessor processor = new DefaultModelProcessor(factory, List.of());
75+
Model result = processor.read(request);
76+
77+
assertNotNull(result);
78+
assertEquals(model, result);
79+
}
80+
81+
@Test
82+
void locateExistingPom_withParsers_shouldReturnFirstValid() {
83+
Path expectedPom = Path.of("project/pom.xml");
84+
Source mockSource = mock(Source.class);
85+
when(mockSource.getPath()).thenReturn(expectedPom);
86+
87+
ModelParser parser = mock(ModelParser.class);
88+
when(parser.locate(any())).thenReturn(Optional.of(mockSource));
89+
90+
DefaultModelProcessor processor = new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of(parser));
91+
Path result = processor.locateExistingPom(Path.of("project"));
92+
93+
assertEquals(expectedPom, result);
94+
}
95+
96+
@Test
97+
void locateExistingPom_parserReturnsPathOutsideProject_shouldThrow() {
98+
Path unexpectedPom = Path.of("other/pom.xml");
99+
Source mockSource = mock(Source.class);
100+
when(mockSource.getPath()).thenReturn(unexpectedPom);
101+
102+
ModelParser parser = mock(ModelParser.class);
103+
when(parser.locate(any())).thenReturn(Optional.of(mockSource));
104+
105+
DefaultModelProcessor processor = new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of(parser));
106+
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
107+
processor.locateExistingPom(Path.of("project")));
108+
109+
assertTrue(ex.getMessage().contains("does not belong to the given directory"));
110+
}
111+
112+
@Test
113+
void locateExistingPom_fallbackWithValidPom_shouldReturnPom() throws Exception {
114+
Path projectDir = Files.createTempDirectory("testproject");
115+
Path pomFile = projectDir.resolve("pom.xml");
116+
Files.createFile(pomFile);
117+
118+
DefaultModelProcessor processor = new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of());
119+
Path result = processor.locateExistingPom(projectDir);
120+
121+
assertEquals(pomFile, result);
122+
123+
// Clean up
124+
Files.deleteIfExists(pomFile);
125+
Files.deleteIfExists(projectDir);
126+
}
127+
128+
@Test
129+
void locateExistingPom_fallbackWithFileAsPath_shouldReturnThatFile() throws Exception {
130+
Path tempPom = Files.createTempFile("pom", ".xml");
131+
132+
DefaultModelProcessor processor = new DefaultModelProcessor(mock(ModelXmlFactory.class), List.of());
133+
Path result = processor.locateExistingPom(tempPom);
134+
135+
assertEquals(tempPom, result);
136+
137+
Files.deleteIfExists(tempPom);
138+
}
139+
}

0 commit comments

Comments
 (0)