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;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  
24  import java.io.File;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Objects;
32  import java.util.Optional;
33  import java.util.function.Function;
34  import java.util.function.Predicate;
35  import java.util.stream.Collectors;
36  
37  import org.apache.maven.artifact.ArtifactUtils;
38  import org.apache.maven.execution.MavenSession;
39  import org.apache.maven.model.Model;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.repository.internal.MavenWorkspaceReader;
42  import org.eclipse.aether.artifact.Artifact;
43  import org.eclipse.aether.repository.WorkspaceRepository;
44  import org.eclipse.aether.util.artifact.ArtifactIdUtils;
45  
46  import static java.util.function.Function.identity;
47  import static java.util.stream.Collectors.groupingBy;
48  import static java.util.stream.Collectors.toMap;
49  
50  /**
51   * An implementation of a workspace reader that knows how to search the Maven reactor for artifacts, either as packaged
52   * jar if it has been built, or only compile output directory if packaging hasn't happened yet.
53   *
54   * @author Jason van Zyl
55   */
56  @Named(ReactorReader.HINT)
57  @SessionScoped
58  class ReactorReader implements MavenWorkspaceReader {
59      public static final String HINT = "reactor";
60  
61      private static final Collection<String> COMPILE_PHASE_TYPES =
62              Arrays.asList("jar", "ejb-client", "war", "rar", "ejb3", "par", "sar", "wsr", "har", "app-client");
63  
64      private final MavenSession session;
65      private final Map<String, MavenProject> projectsByGAV;
66      private final Map<String, List<MavenProject>> projectsByGA;
67      private final WorkspaceRepository repository;
68  
69      private Function<MavenProject, String> projectIntoKey =
70              s -> ArtifactUtils.key(s.getGroupId(), s.getArtifactId(), s.getVersion());
71  
72      private Function<MavenProject, String> projectIntoVersionlessKey =
73              s -> ArtifactUtils.versionlessKey(s.getGroupId(), s.getArtifactId());
74  
75      @Inject
76      ReactorReader(MavenSession session) {
77          this.session = session;
78          this.projectsByGAV = session.getProjects().stream().collect(toMap(projectIntoKey, identity()));
79  
80          this.projectsByGA = projectsByGAV.values().stream().collect(groupingBy(projectIntoVersionlessKey));
81  
82          repository = new WorkspaceRepository("reactor", new HashSet<>(projectsByGAV.keySet()));
83      }
84  
85      //
86      // Public API
87      //
88  
89      public WorkspaceRepository getRepository() {
90          return repository;
91      }
92  
93      public File findArtifact(Artifact artifact) {
94          String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
95  
96          MavenProject project = projectsByGAV.get(projectKey);
97  
98          if (project != null) {
99              File file = find(project, artifact);
100             if (file == null && project != project.getExecutionProject()) {
101                 file = find(project.getExecutionProject(), artifact);
102             }
103             return file;
104         }
105 
106         return null;
107     }
108 
109     public List<String> findVersions(Artifact artifact) {
110         String key = ArtifactUtils.versionlessKey(artifact.getGroupId(), artifact.getArtifactId());
111 
112         return Optional.ofNullable(projectsByGA.get(key)).orElse(Collections.emptyList()).stream()
113                 .filter(s -> Objects.nonNull(find(s, artifact)))
114                 .map(MavenProject::getVersion)
115                 .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
116     }
117 
118     @Override
119     public Model findModel(Artifact artifact) {
120         String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
121         MavenProject project = projectsByGAV.get(projectKey);
122         return project == null ? null : project.getModel();
123     }
124 
125     //
126     // Implementation
127     //
128 
129     private File find(MavenProject project, Artifact artifact) {
130         if ("pom".equals(artifact.getExtension())) {
131             return project.getFile();
132         }
133 
134         Artifact projectArtifact = findMatchingArtifact(project, artifact);
135 
136         if (hasArtifactFileFromPackagePhase(projectArtifact)) {
137             return projectArtifact.getFile();
138         } else if (!hasBeenPackaged(project)) {
139             // fallback to loose class files only if artifacts haven't been packaged yet
140             // and only for plain old jars. Not war files, not ear files, not anything else.
141 
142             if (isTestArtifact(artifact)) {
143                 if (project.hasLifecyclePhase("test-compile")) {
144                     return new File(project.getBuild().getTestOutputDirectory());
145                 }
146             } else {
147                 String type = artifact.getProperty("type", "");
148                 if (project.hasLifecyclePhase("compile") && COMPILE_PHASE_TYPES.contains(type)) {
149                     return new File(project.getBuild().getOutputDirectory());
150                 }
151             }
152         }
153 
154         // The fall-through indicates that the artifact cannot be found;
155         // for instance if package produced nothing or classifier problems.
156         return null;
157     }
158 
159     private File determinePreviouslyPackagedArtifactFile(MavenProject project, Artifact artifact) {
160         if (artifact == null) {
161             return null;
162         }
163 
164         String fileName = String.format("%s.%s", project.getBuild().getFinalName(), artifact.getExtension());
165         return new File(project.getBuild().getDirectory(), fileName);
166     }
167 
168     private boolean hasArtifactFileFromPackagePhase(Artifact projectArtifact) {
169         return projectArtifact != null
170                 && projectArtifact.getFile() != null
171                 && projectArtifact.getFile().exists();
172     }
173 
174     private boolean hasBeenPackaged(MavenProject project) {
175         return project.hasLifecyclePhase("package")
176                 || project.hasLifecyclePhase("install")
177                 || project.hasLifecyclePhase("deploy");
178     }
179 
180     /**
181      * Tries to resolve the specified artifact from the artifacts of the given project.
182      *
183      * @param project The project to try to resolve the artifact from, must not be <code>null</code>.
184      * @param requestedArtifact The artifact to resolve, must not be <code>null</code>.
185      * @return The matching artifact from the project or <code>null</code> if not found. Note that this
186      */
187     private Artifact findMatchingArtifact(MavenProject project, Artifact requestedArtifact) {
188         String requestedRepositoryConflictId = ArtifactIdUtils.toVersionlessId(requestedArtifact);
189 
190         Artifact mainArtifact = RepositoryUtils.toArtifact(project.getArtifact());
191         if (requestedRepositoryConflictId.equals(ArtifactIdUtils.toVersionlessId(mainArtifact))) {
192             return mainArtifact;
193         }
194 
195         return RepositoryUtils.toArtifacts(project.getAttachedArtifacts()).stream()
196                 .filter(isRequestedArtifact(requestedArtifact))
197                 .findFirst()
198                 .orElse(null);
199     }
200 
201     /**
202      * We are taking as much as we can from the DefaultArtifact.equals(). The requested artifact has no file, so we want
203      * to remove that from the comparison.
204      *
205      * @param requestArtifact checked against the given artifact.
206      * @return true if equals, false otherwise.
207      */
208     private Predicate<Artifact> isRequestedArtifact(Artifact requestArtifact) {
209         return s -> s.getArtifactId().equals(requestArtifact.getArtifactId())
210                 && s.getGroupId().equals(requestArtifact.getGroupId())
211                 && s.getVersion().equals(requestArtifact.getVersion())
212                 && s.getExtension().equals(requestArtifact.getExtension())
213                 && s.getClassifier().equals(requestArtifact.getClassifier());
214     }
215 
216     /**
217      * Determines whether the specified artifact refers to test classes.
218      *
219      * @param artifact The artifact to check, must not be {@code null}.
220      * @return {@code true} if the artifact refers to test classes, {@code false} otherwise.
221      */
222     private static boolean isTestArtifact(Artifact artifact) {
223         return ("test-jar".equals(artifact.getProperty("type", "")))
224                 || ("jar".equals(artifact.getExtension()) && "tests".equals(artifact.getClassifier()));
225     }
226 }