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