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