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