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.report.projectinfo;
20  
21  import javax.inject.Inject;
22  
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Properties;
31  
32  import org.apache.maven.doxia.sink.Sink;
33  import org.apache.maven.model.Contributor;
34  import org.apache.maven.model.Developer;
35  import org.apache.maven.plugins.annotations.Mojo;
36  import org.apache.maven.plugins.annotations.Parameter;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.project.ProjectBuilder;
39  import org.apache.maven.report.projectinfo.avatars.AvatarsProvider;
40  import org.apache.maven.reporting.MavenReportException;
41  import org.apache.maven.repository.RepositorySystem;
42  import org.codehaus.plexus.i18n.I18N;
43  import org.codehaus.plexus.util.StringUtils;
44  
45  /**
46   * Generates the Project Team report.
47   *
48   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton </a>
49   * @since 2.0
50   */
51  @Mojo(name = "team")
52  public class TeamReport extends AbstractProjectInfoReport {
53      /**
54       * Shows avatar images for team members that have a) properties/picUrl set b) An avatar at gravatar.com for their
55       * email address
56       *
57       * @since 2.6
58       */
59      @Parameter(property = "teamlist.showAvatarImages", defaultValue = "true")
60      private boolean showAvatarImages;
61  
62      /**
63       * Indicate if URL should be used for avatar images.
64       * <p>
65       * If set to <code>false</code> images will be downloaded and attached to report during build.
66       * Local path will be used for images.
67       *
68       * @since 3.9.0
69       */
70      @Parameter(property = "teamlist.externalAvatarImages", defaultValue = "true")
71      private boolean externalAvatarImages;
72  
73      /**
74       * Base URL for avatar provider.
75       *
76       * @since 3.9.0
77       */
78      @Parameter(property = "teamlist.avatarBaseUrl", defaultValue = "https://www.gravatar.com/avatar/")
79      private String avatarBaseUrl;
80  
81      /**
82       * Provider name for avatar images.
83       * <p>
84       * Report has one implementation for gravatar.com. Users can provide other by implementing {@link AvatarsProvider}.
85       *
86       * @since 3.9.0
87       */
88      @Parameter(property = "teamlist.avatarProviderName", defaultValue = "gravatar")
89      private String avatarProviderName;
90  
91      private final Map<String, AvatarsProvider> avatarsProviders;
92  
93      @Inject
94      public TeamReport(
95              RepositorySystem repositorySystem,
96              I18N i18n,
97              ProjectBuilder projectBuilder,
98              Map<String, AvatarsProvider> avatarsProviders) {
99          super(repositorySystem, i18n, projectBuilder);
100         this.avatarsProviders = avatarsProviders;
101     }
102 
103     // ----------------------------------------------------------------------
104     // Public methods
105     // ----------------------------------------------------------------------
106 
107     @Override
108     public boolean canGenerateReport() throws MavenReportException {
109         boolean result = super.canGenerateReport();
110         if (result && skipEmptyReport) {
111             result = !isEmpty(getProject().getModel().getDevelopers())
112                     || !isEmpty(getProject().getModel().getContributors());
113         }
114 
115         return result;
116     }
117 
118     @Override
119     public void executeReport(Locale locale) throws MavenReportException {
120 
121         Map<Contributor, String> avatarImages = prepareAvatars();
122 
123         ProjectTeamRenderer renderer =
124                 new ProjectTeamRenderer(getSink(), project, getI18N(locale), locale, showAvatarImages, avatarImages);
125         renderer.render();
126     }
127 
128     private Map<Contributor, String> prepareAvatars() throws MavenReportException {
129 
130         if (!showAvatarImages) {
131             return Collections.emptyMap();
132         }
133 
134         AvatarsProvider avatarsProvider = avatarsProviders.get(avatarProviderName);
135         if (avatarsProvider == null) {
136             throw new MavenReportException("No AvatarsProvider found for name " + avatarProviderName);
137         }
138         avatarsProvider.setBaseUrl(avatarBaseUrl);
139         avatarsProvider.setOutputDirectory(getReportOutputDirectory());
140 
141         Map<Contributor, String> result = new HashMap<>();
142         try {
143             prepareContributorAvatars(result, avatarsProvider, project.getDevelopers());
144             prepareContributorAvatars(result, avatarsProvider, project.getContributors());
145         } catch (IOException e) {
146             throw new MavenReportException("Unable to load avatar images", e);
147         }
148         return result;
149     }
150 
151     private void prepareContributorAvatars(
152             Map<Contributor, String> avatarImages,
153             AvatarsProvider avatarsProvider,
154             List<? extends Contributor> contributors)
155             throws IOException {
156 
157         for (Contributor contributor : contributors) {
158 
159             String picSource = contributor.getProperties().getProperty("picUrl");
160             if (picSource == null || picSource.isEmpty()) {
161                 picSource = externalAvatarImages
162                         ? avatarsProvider.getAvatarUrl(contributor.getEmail())
163                         : avatarsProvider.getLocalAvatarPath(contributor.getEmail());
164             }
165 
166             avatarImages.put(contributor, picSource);
167         }
168     }
169 
170     /**
171      * {@inheritDoc}
172      */
173     @Override
174     public String getOutputName() {
175         return "team";
176     }
177 
178     @Override
179     protected String getI18Nsection() {
180         return "team";
181     }
182 
183     // ----------------------------------------------------------------------
184     // Private
185     // ----------------------------------------------------------------------
186 
187     /**
188      * Internal renderer class
189      */
190     private static class ProjectTeamRenderer extends AbstractProjectInfoRenderer {
191         private static final String PROPERTIES = "properties";
192 
193         private static final String TIME_ZONE = "timeZone";
194 
195         private static final String ROLES = "roles";
196 
197         private static final String ORGANIZATION_URL = "organizationUrl";
198 
199         private static final String ORGANIZATION = "organization";
200 
201         private static final String URL = "url";
202 
203         private static final String EMAIL = "email";
204 
205         private static final String NAME = "name";
206 
207         private static final String IMAGE = "image";
208 
209         private static final String ID = "id";
210 
211         private final MavenProject mavenProject;
212 
213         private final boolean showAvatarImages;
214 
215         private final Map<Contributor, String> avatarImages;
216 
217         ProjectTeamRenderer(
218                 Sink sink,
219                 MavenProject mavenProject,
220                 I18N i18n,
221                 Locale locale,
222                 boolean showAvatarImages,
223                 Map<Contributor, String> avatarImages) {
224             super(sink, i18n, locale);
225 
226             this.mavenProject = mavenProject;
227             this.showAvatarImages = showAvatarImages;
228             this.avatarImages = avatarImages;
229         }
230 
231         @Override
232         protected String getI18Nsection() {
233             return "team";
234         }
235 
236         @Override
237         protected void renderBody() {
238             startSection(getI18nString("intro.title"));
239 
240             // Introduction
241             paragraph(getI18nString("intro.description1"));
242             paragraph(getI18nString("intro.description2"));
243 
244             // Developer section
245             List<Developer> developers = mavenProject.getDevelopers();
246 
247             startSection(getI18nString("developers.title"));
248 
249             if (developers.isEmpty()) {
250                 paragraph(getI18nString("nodeveloper"));
251             } else {
252                 paragraph(getI18nString("developers.intro"));
253 
254                 startTable();
255 
256                 // By default we think that all headers not required: set true for headers that are required
257                 Map<String, Boolean> headersMap = checkRequiredHeaders(developers);
258                 String[] requiredHeaders = getRequiredDevHeaderArray(headersMap);
259 
260                 tableHeader(requiredHeaders);
261 
262                 for (Developer developer : developers) {
263                     renderTeamMember(developer, headersMap);
264                 }
265 
266                 endTable();
267             }
268 
269             endSection();
270 
271             // contributors section
272             List<Contributor> contributors = mavenProject.getContributors();
273 
274             startSection(getI18nString("contributors.title"));
275 
276             if (contributors.isEmpty()) {
277                 paragraph(getI18nString("nocontributor"));
278             } else {
279                 paragraph(getI18nString("contributors.intro"));
280 
281                 startTable();
282 
283                 Map<String, Boolean> headersMap = checkRequiredHeaders(contributors);
284                 String[] requiredHeaders = getRequiredContrHeaderArray(headersMap);
285 
286                 tableHeader(requiredHeaders);
287 
288                 for (Contributor contributor : contributors) {
289                     renderTeamMember(contributor, headersMap);
290                 }
291 
292                 endTable();
293             }
294 
295             endSection();
296 
297             endSection();
298         }
299 
300         private void renderTeamMember(Contributor member, Map<String, Boolean> headersMap) {
301             sink.tableRow();
302 
303             if (headersMap.get(IMAGE) == Boolean.TRUE && showAvatarImages) {
304                 sink.tableCell();
305                 sink.figure();
306                 sink.figureGraphics(avatarImages.get(member));
307                 sink.figure_();
308                 sink.tableCell_();
309             }
310             if (member instanceof Developer) {
311                 if (headersMap.get(ID) == Boolean.TRUE) {
312                     String id = ((Developer) member).getId();
313                     if (id == null) {
314                         tableCell(null);
315                     } else {
316                         tableCell("<a id=\"" + id + "\"></a>" + id, true);
317                     }
318                 }
319             }
320             if (headersMap.get(NAME) == Boolean.TRUE) {
321                 tableCell(member.getName());
322             }
323             if (headersMap.get(EMAIL) == Boolean.TRUE) {
324                 final String link = String.format("mailto:%s", member.getEmail());
325                 tableCell(createLinkPatternedText(member.getEmail(), link));
326             }
327             if (headersMap.get(URL) == Boolean.TRUE) {
328                 tableCellForUrl(member.getUrl());
329             }
330             if (headersMap.get(ORGANIZATION) == Boolean.TRUE) {
331                 tableCell(member.getOrganization());
332             }
333             if (headersMap.get(ORGANIZATION_URL) == Boolean.TRUE) {
334                 tableCellForUrl(member.getOrganizationUrl());
335             }
336             if (headersMap.get(ROLES) == Boolean.TRUE) {
337                 if (member.getRoles() != null) {
338                     // Comma separated roles
339                     List<String> var = member.getRoles();
340                     tableCell(StringUtils.join(var.toArray(new String[var.size()]), ", "));
341                 } else {
342                     tableCell(null);
343                 }
344             }
345             if (headersMap.get(TIME_ZONE) == Boolean.TRUE) {
346                 tableCell(member.getTimezone());
347             }
348 
349             if (headersMap.get(PROPERTIES) == Boolean.TRUE) {
350                 Properties props = member.getProperties();
351                 if (props != null) {
352                     tableCell(propertiesToString(props));
353                 } else {
354                     tableCell(null);
355                 }
356             }
357 
358             sink.tableRow_();
359         }
360 
361         private String[] getRequiredContrHeaderArray(Map<String, Boolean> requiredHeaders) {
362             List<String> requiredArray = new ArrayList<>();
363             String image = getI18nString("contributors.image");
364             String name = getI18nString("contributors.name");
365             String email = getI18nString("contributors.email");
366             String url = getI18nString("contributors.url");
367             String organization = getI18nString("contributors.organization");
368             String organizationUrl = getI18nString("contributors.organizationurl");
369             String roles = getI18nString("contributors.roles");
370             String timeZone = getI18nString("contributors.timezone");
371             String properties = getI18nString("contributors.properties");
372             if (requiredHeaders.get(IMAGE) == Boolean.TRUE && showAvatarImages) {
373                 requiredArray.add(image);
374             }
375             setRequiredArray(
376                     requiredHeaders,
377                     requiredArray,
378                     name,
379                     email,
380                     url,
381                     organization,
382                     organizationUrl,
383                     roles,
384                     timeZone,
385                     properties);
386 
387             return requiredArray.toArray(new String[requiredArray.size()]);
388         }
389 
390         private String[] getRequiredDevHeaderArray(Map<String, Boolean> requiredHeaders) {
391             List<String> requiredArray = new ArrayList<>();
392 
393             String image = getI18nString("developers.image");
394             String id = getI18nString("developers.id");
395             String name = getI18nString("developers.name");
396             String email = getI18nString("developers.email");
397             String url = getI18nString("developers.url");
398             String organization = getI18nString("developers.organization");
399             String organizationUrl = getI18nString("developers.organizationurl");
400             String roles = getI18nString("developers.roles");
401             String timeZone = getI18nString("developers.timezone");
402             String properties = getI18nString("developers.properties");
403 
404             if (requiredHeaders.get(IMAGE) == Boolean.TRUE && showAvatarImages) {
405                 requiredArray.add(image);
406             }
407             if (requiredHeaders.get(ID) == Boolean.TRUE) {
408                 requiredArray.add(id);
409             }
410 
411             setRequiredArray(
412                     requiredHeaders,
413                     requiredArray,
414                     name,
415                     email,
416                     url,
417                     organization,
418                     organizationUrl,
419                     roles,
420                     timeZone,
421                     properties);
422 
423             return requiredArray.toArray(new String[0]);
424         }
425 
426         private static void setRequiredArray(
427                 Map<String, Boolean> requiredHeaders,
428                 List<String> requiredArray,
429                 String name,
430                 String email,
431                 String url,
432                 String organization,
433                 String organizationUrl,
434                 String roles,
435                 String timeZone,
436                 String properties) {
437             if (requiredHeaders.get(NAME) == Boolean.TRUE) {
438                 requiredArray.add(name);
439             }
440             if (requiredHeaders.get(EMAIL) == Boolean.TRUE) {
441                 requiredArray.add(email);
442             }
443             if (requiredHeaders.get(URL) == Boolean.TRUE) {
444                 requiredArray.add(url);
445             }
446             if (requiredHeaders.get(ORGANIZATION) == Boolean.TRUE) {
447                 requiredArray.add(organization);
448             }
449             if (requiredHeaders.get(ORGANIZATION_URL) == Boolean.TRUE) {
450                 requiredArray.add(organizationUrl);
451             }
452             if (requiredHeaders.get(ROLES) == Boolean.TRUE) {
453                 requiredArray.add(roles);
454             }
455             if (requiredHeaders.get(TIME_ZONE) == Boolean.TRUE) {
456                 requiredArray.add(timeZone);
457             }
458 
459             if (requiredHeaders.get(PROPERTIES) == Boolean.TRUE) {
460                 requiredArray.add(properties);
461             }
462         }
463 
464         /**
465          * @param units contributors and developers to check
466          * @return required headers
467          */
468         private static Map<String, Boolean> checkRequiredHeaders(List<? extends Contributor> units) {
469             Map<String, Boolean> requiredHeaders = new HashMap<>();
470 
471             requiredHeaders.put(IMAGE, Boolean.FALSE);
472             requiredHeaders.put(ID, Boolean.FALSE);
473             requiredHeaders.put(NAME, Boolean.FALSE);
474             requiredHeaders.put(EMAIL, Boolean.FALSE);
475             requiredHeaders.put(URL, Boolean.FALSE);
476             requiredHeaders.put(ORGANIZATION, Boolean.FALSE);
477             requiredHeaders.put(ORGANIZATION_URL, Boolean.FALSE);
478             requiredHeaders.put(ROLES, Boolean.FALSE);
479             requiredHeaders.put(TIME_ZONE, Boolean.FALSE);
480             requiredHeaders.put(PROPERTIES, Boolean.FALSE);
481 
482             for (Contributor unit : units) {
483                 if (unit instanceof Developer) {
484                     Developer developer = (Developer) unit;
485                     if (StringUtils.isNotEmpty(developer.getId())) {
486                         requiredHeaders.put(ID, Boolean.TRUE);
487                     }
488                 }
489                 if (StringUtils.isNotEmpty(unit.getName())) {
490                     requiredHeaders.put(NAME, Boolean.TRUE);
491                 }
492                 if (StringUtils.isNotEmpty(unit.getEmail())) {
493                     requiredHeaders.put(EMAIL, Boolean.TRUE);
494                     requiredHeaders.put(IMAGE, Boolean.TRUE);
495                 }
496                 if (StringUtils.isNotEmpty(unit.getUrl())) {
497                     requiredHeaders.put(URL, Boolean.TRUE);
498                 }
499                 if (StringUtils.isNotEmpty(unit.getOrganization())) {
500                     requiredHeaders.put(ORGANIZATION, Boolean.TRUE);
501                 }
502                 if (StringUtils.isNotEmpty(unit.getOrganizationUrl())) {
503                     requiredHeaders.put(ORGANIZATION_URL, Boolean.TRUE);
504                 }
505                 if (!unit.getRoles().isEmpty()) {
506                     requiredHeaders.put(ROLES, Boolean.TRUE);
507                 }
508                 if (StringUtils.isNotEmpty(unit.getTimezone())) {
509                     requiredHeaders.put(TIME_ZONE, Boolean.TRUE);
510                 }
511                 Properties properties = unit.getProperties();
512                 boolean hasPicUrl = properties.containsKey("picUrl");
513                 if (hasPicUrl) {
514                     requiredHeaders.put(IMAGE, Boolean.TRUE);
515                 }
516                 boolean isJustAnImageProperty = properties.size() == 1 && hasPicUrl;
517                 if (!isJustAnImageProperty && !properties.isEmpty()) {
518                     requiredHeaders.put(PROPERTIES, Boolean.TRUE);
519                 }
520             }
521             return requiredHeaders;
522         }
523 
524         /**
525          * Create a table cell with a link to the given url. The url is not validated.
526          */
527         private void tableCellForUrl(String url) {
528             sink.tableCell();
529 
530             if (url == null || url.isEmpty()) {
531                 text(url);
532             } else {
533                 link(url, url);
534             }
535 
536             sink.tableCell_();
537         }
538     }
539 }