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  
31  import org.apache.maven.doxia.sink.Sink;
32  import org.apache.maven.model.Contributor;
33  import org.apache.maven.model.Developer;
34  import org.apache.maven.model.Model;
35  import org.apache.maven.plugins.annotations.Mojo;
36  import org.apache.maven.plugins.annotations.Parameter;
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
48      extends AbstractProjectInfoReport
49  {
50      /**
51       * Shows avatar images for team members that have a) properties/picUrl set b) An avatar at gravatar.com for their
52       * email address
53       * <p/>
54       * Future versions of this plugin may choose to implement different strategies for resolving avatar images, possibly
55       * using different providers.
56       *<p>
57       *<strong>Note</strong>: This property will be renamed to {@code tteam.showAvatarImages} in 3.0.
58       * @since 2.6
59       */
60      @Parameter( property = "teamlist.showAvatarImages", defaultValue = "true" )
61      private boolean showAvatarImages;
62  
63      // ----------------------------------------------------------------------
64      // Public methods
65      // ----------------------------------------------------------------------
66  
67      @Override
68      public boolean canGenerateReport()
69      {
70          boolean result = super.canGenerateReport();
71          if ( result && skipEmptyReport )
72          {
73              result = !isEmpty( getProject().getModel().getDevelopers() )
74                      || !isEmpty( getProject().getModel().getContributors() );
75          }
76  
77          return result;
78      }
79  
80      @Override
81      public void executeReport( Locale locale )
82      {
83          ProjectTeamRenderer r =
84              new ProjectTeamRenderer( getSink(), project.getModel(), getI18N( locale ), locale, showAvatarImages );
85          r.render();
86      }
87  
88      /**
89       * {@inheritDoc}
90       */
91      @Override
92      public String getOutputName()
93      {
94          return "team";
95      }
96  
97      @Override
98      protected String getI18Nsection()
99      {
100         return "team";
101     }
102 
103     // ----------------------------------------------------------------------
104     // Private
105     // ----------------------------------------------------------------------
106 
107     /**
108      * Internal renderer class
109      */
110     private static class ProjectTeamRenderer
111         extends AbstractProjectInfoRenderer
112     {
113         private static final String PROPERTIES = "properties";
114 
115         private static final String TIME_ZONE = "timeZone";
116 
117         private static final String ROLES = "roles";
118 
119         private static final String ORGANIZATION_URL = "organizationUrl";
120 
121         private static final String ORGANIZATION = "organization";
122 
123         private static final String URL = "url";
124 
125         private static final String EMAIL = "email";
126 
127         private static final String NAME = "name";
128 
129         private static final String IMAGE = "image";
130 
131         private static final String ID = "id";
132 
133         private final Model model;
134 
135         private final boolean showAvatarImages;
136 
137         private final String protocol;
138 
139         ProjectTeamRenderer( Sink sink, Model model, I18N i18n, Locale locale, boolean showAvatarImages )
140         {
141             super( sink, i18n, locale );
142 
143             this.model = model;
144             this.showAvatarImages = showAvatarImages;
145 
146             // prepare protocol for gravatar
147             if ( model.getUrl() != null && model.getUrl().startsWith( "https://" ) )
148             {
149                 this.protocol = "https";
150         }
151             else
152             {
153                 this.protocol = "http";
154             }
155         }
156 
157         @Override
158         protected String getI18Nsection()
159         {
160             return "team";
161         }
162 
163         @Override
164         public void renderBody()
165         {
166             startSection( getI18nString( "intro.title" ) );
167 
168             // Introduction
169             paragraph( getI18nString( "intro.description1" ) );
170             paragraph( getI18nString( "intro.description2" ) );
171 
172             // Developer section
173             List<Developer> developers = model.getDevelopers();
174 
175             startSection( getI18nString( "developers.title" ) );
176 
177             if ( isEmpty( developers ) )
178             {
179                 paragraph( getI18nString( "nodeveloper" ) );
180             }
181             else
182             {
183                 paragraph( getI18nString( "developers.intro" ) );
184 
185                 startTable();
186 
187                 // By default we think that all headers not required: set true for headers that are required
188                 Map<String, Boolean> headersMap = checkRequiredHeaders( developers );
189                 String[] requiredHeaders = getRequiredDevHeaderArray( headersMap );
190 
191                 tableHeader( requiredHeaders );
192 
193                 for ( Developer developer : developers )
194                 {
195                     renderTeamMember( developer, headersMap );
196                 }
197 
198                 endTable();
199             }
200 
201             endSection();
202 
203             // contributors section
204             List<Contributor> contributors = model.getContributors();
205 
206             startSection( getI18nString( "contributors.title" ) );
207 
208             if ( isEmpty( contributors ) )
209             {
210                 paragraph( getI18nString( "nocontributor" ) );
211             }
212             else
213             {
214                 paragraph( getI18nString( "contributors.intro" ) );
215 
216                 startTable();
217 
218                 Map<String, Boolean> headersMap = checkRequiredHeaders( contributors );
219                 String[] requiredHeaders = getRequiredContrHeaderArray( headersMap );
220 
221                 tableHeader( requiredHeaders );
222 
223                 for ( Contributor contributor : contributors )
224                 {
225                     renderTeamMember( contributor, headersMap );
226                 }
227 
228                 endTable();
229             }
230 
231             endSection();
232 
233             endSection();
234         }
235 
236         private void renderTeamMember( Contributor member, Map<String, Boolean> headersMap )
237         {
238             sink.tableRow();
239 
240             if ( headersMap.get( IMAGE ) == Boolean.TRUE && showAvatarImages )
241             {
242                 Properties properties = member.getProperties();
243                 String picUrl = properties.getProperty( "picUrl" );
244                 if ( StringUtils.isEmpty( picUrl ) )
245                 {
246                     picUrl = getGravatarUrl( member.getEmail() );
247                 }
248                 if ( StringUtils.isEmpty( picUrl ) )
249                 {
250                     picUrl = getSpacerGravatarUrl();
251                 }
252                 sink.tableCell();
253                 sink.figure();
254                 sink.figureGraphics( picUrl );
255                 sink.figure_();
256                 sink.tableCell_();
257             }
258             if ( member instanceof Developer )
259             {
260                 if ( headersMap.get( ID ) == Boolean.TRUE )
261                 {
262                     String id = ( (Developer) member ).getId();
263                     if ( id == null )
264                     {
265                         tableCell( null );
266                     }
267                     else
268                     {
269                         tableCell( "<a name=\"" + id + "\"></a>" + id, true );
270                     }
271                 }
272             }
273             if ( headersMap.get( NAME ) == Boolean.TRUE )
274             {
275                 tableCell( member.getName() );
276             }
277             if ( headersMap.get( EMAIL ) == Boolean.TRUE )
278             {
279                 final String link = String.format( "mailto:%s", member.getEmail() );
280                 tableCell( createLinkPatternedText( member.getEmail(), link ) );
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 static 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 static 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 static boolean isEmpty( List<?> list )
570         {
571             return ( list == null ) || list.isEmpty();
572         }
573     }
574 }