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.plugin.logging.Log;
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   * @version $Id: TeamListReport.html 935177 2015-01-05 21:05:55Z michaelo $
45   * @since 2.0
46   */
47  @Mojo( name = "project-team" )
48  public class TeamListReport
49      extends AbstractProjectInfoReport
50  {
51      /**
52       * Shows avatar images for team members that have a) properties/picUrl set b) An avatar at gravatar.com for their
53       * email address
54       * <p/>
55       * Future versions of this plugin may choose to implement different strategies for resolving avatar images, possibly
56       * using different providers.
57       *
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          // CHECKSTYLE_OFF: LineLength
84          TeamListRenderer r =
85              new TeamListRenderer( getSink(), project.getModel(), getI18N( locale ), locale, getLog(), showAvatarImages );
86          // CHECKSTYLE_ON: LineLength
87  
88          r.render();
89      }
90  
91      /**
92       * {@inheritDoc}
93       */
94      public String getOutputName()
95      {
96          return "team-list";
97      }
98  
99      @Override
100     protected String getI18Nsection()
101     {
102         return "team-list";
103     }
104 
105     // ----------------------------------------------------------------------
106     // Private
107     // ----------------------------------------------------------------------
108 
109     /**
110      * Internal renderer class
111      */
112     private static class TeamListRenderer
113         extends AbstractProjectInfoRenderer
114     {
115         private static final String PROPERTIES = "properties";
116 
117         private static final String TIME_ZONE = "timeZone";
118 
119         private static final String ROLES = "roles";
120 
121         private static final String ORGANIZATION_URL = "organizationUrl";
122 
123         private static final String ORGANIZATION = "organization";
124 
125         private static final String URL = "url";
126 
127         private static final String EMAIL = "email";
128 
129         private static final String NAME = "name";
130 
131         private static final String IMAGE = "image";
132 
133         private static final String ID = "id";
134 
135         private final Model model;
136 
137         private final Log log;
138 
139         private final boolean showAvatarImages;
140 
141         TeamListRenderer( Sink sink, Model model, I18N i18n, Locale locale, Log log, boolean showAvatarImages )
142         {
143             super( sink, i18n, locale );
144 
145             this.model = model;
146             this.log = log;
147             this.showAvatarImages = showAvatarImages;
148         }
149 
150         @Override
151         protected String getI18Nsection()
152         {
153             return "team-list";
154         }
155 
156         @Override
157         public void renderBody()
158         {
159             startSection( getI18nString( "intro.title" ) );
160 
161             // To handle JS
162             StringBuilder javascript = new StringBuilder();
163 
164             // Introduction
165             paragraph( getI18nString( "intro.description1" ) );
166             paragraph( getI18nString( "intro.description2" ) );
167 
168             // Developer section
169             List<Developer> developers = model.getDevelopers();
170 
171             startSection( getI18nString( "developers.title" ) );
172 
173             if ( isEmpty( developers ) )
174             {
175                 paragraph( getI18nString( "nodeveloper" ) );
176             }
177             else
178             {
179                 paragraph( getI18nString( "developers.intro" ) );
180 
181                 startTable();
182 
183                 // By default we think that all headers not required: set true for headers that are required
184                 Map<String, Boolean> headersMap = checkRequiredHeaders( developers );
185                 String[] requiredHeaders = getRequiredDevHeaderArray( headersMap );
186 
187                 tableHeader( requiredHeaders );
188 
189                 // To handle JS
190                 int developersRowId = 0;
191                 for ( Developer developer : developers )
192                 {
193                     renderTeamMember( developer, developersRowId, headersMap, javascript );
194 
195                     developersRowId++;
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                 // To handle JS
224                 int contributorsRowId = 0;
225                 for ( Contributor contributor : contributors )
226                 {
227                     renderTeamMember( contributor, contributorsRowId, headersMap, javascript );
228 
229                     contributorsRowId++;
230                 }
231 
232                 endTable();
233             }
234 
235             endSection();
236 
237             endSection();
238         }
239 
240         private void renderTeamMember( Contributor member, int rowId, Map<String, Boolean> headersMap,
241                                        StringBuilder javascript )
242         {
243             sink.tableRow();
244 
245             if ( headersMap.get( IMAGE ) == Boolean.TRUE && showAvatarImages )
246             {
247                 Properties properties = member.getProperties();
248                 String picUrl = properties.getProperty( "picUrl" );
249                 if ( StringUtils.isEmpty( picUrl ) )
250                 {
251                     picUrl = getGravatarUrl( member.getEmail() );
252                 }
253                 if ( StringUtils.isEmpty( picUrl ) )
254                 {
255                     picUrl = getSpacerGravatarUrl();
256                 }
257                 sink.tableCell();
258                 sink.figure();
259                 sink.figureGraphics( picUrl );
260                 sink.figure_();
261                 sink.tableCell_();
262             }
263             String type = "contributor";
264             if ( member instanceof Developer )
265             {
266                 type = "developer";
267                 if ( headersMap.get( ID ) == Boolean.TRUE )
268                 {
269                     String id = ( (Developer) member ).getId();
270                     if ( id == null )
271                     {
272                         tableCell( null );
273                     }
274                     else
275                     {
276                         tableCell( "<a name=\"" + id + "\"></a>" + id, true );
277                     }
278                 }
279             }
280             if ( headersMap.get( NAME ) == Boolean.TRUE )
281             {
282                 tableCell( member.getName() );
283             }
284             if ( headersMap.get( EMAIL ) == Boolean.TRUE )
285             {
286                 tableCell( createLinkPatternedText( member.getEmail(), member.getEmail() ) );
287             }
288             if ( headersMap.get( URL ) == Boolean.TRUE )
289             {
290                 tableCellForUrl( member.getUrl() );
291             }
292             if ( headersMap.get( ORGANIZATION ) == Boolean.TRUE )
293             {
294                 tableCell( member.getOrganization() );
295             }
296             if ( headersMap.get( ORGANIZATION_URL ) == Boolean.TRUE )
297             {
298                 tableCellForUrl( member.getOrganizationUrl() );
299             }
300             if ( headersMap.get( ROLES ) == Boolean.TRUE )
301             {
302                 if ( member.getRoles() != null )
303                 {
304                     // Comma separated roles
305                     List<String> var = member.getRoles();
306                     tableCell( StringUtils.join( var.toArray( new String[var.size()] ), ", " ) );
307                 }
308                 else
309                 {
310                     tableCell( null );
311                 }
312             }
313             if ( headersMap.get( TIME_ZONE ) == Boolean.TRUE )
314             {
315                 tableCell( member.getTimezone() );
316             }
317 
318             if ( headersMap.get( PROPERTIES ) == Boolean.TRUE )
319             {
320                 Properties props = member.getProperties();
321                 if ( props != null )
322                 {
323                     tableCell( propertiesToString( props ) );
324                 }
325                 else
326                 {
327                     tableCell( null );
328                 }
329             }
330 
331             sink.tableRow_();
332         }
333 
334         private static final String AVATAR_SIZE = "s=60";
335 
336         private String getSpacerGravatarUrl()
337         {
338             return "http://www.gravatar.com/avatar/00000000000000000000000000000000?d=blank&f=y&" + AVATAR_SIZE;
339         }
340 
341         private String getGravatarUrl( String email )
342         {
343             if ( email == null )
344             {
345                 return null;
346             }
347             email = StringUtils.trim( email );
348             email = email.toLowerCase();
349             MessageDigest md;
350             try
351             {
352                 md = MessageDigest.getInstance( "MD5" );
353                 md.update( email.getBytes() );
354                 byte byteData[] = md.digest();
355                 StringBuilder sb = new StringBuilder();
356                 final int lowerEightBitsOnly = 0xff;
357                 for ( byte aByteData : byteData )
358                 {
359                     sb.append( Integer.toString( ( aByteData & lowerEightBitsOnly ) + 0x100, 16 ).substring( 1 ) );
360                 }
361                 return "http://www.gravatar.com/avatar/" + sb.toString() + "?d=mm&" + AVATAR_SIZE;
362             }
363             catch ( NoSuchAlgorithmException e )
364             {
365                 return null;
366             }
367         }
368 
369         /**
370          * @param requiredHeaders
371          * @return
372          */
373         private String[] getRequiredContrHeaderArray( Map<String, Boolean> requiredHeaders )
374         {
375             List<String> requiredArray = new ArrayList<String>();
376             String image = getI18nString( "contributors.image" );
377             String name = getI18nString( "contributors.name" );
378             String email = getI18nString( "contributors.email" );
379             String url = getI18nString( "contributors.url" );
380             String organization = getI18nString( "contributors.organization" );
381             String organizationUrl = getI18nString( "contributors.organizationurl" );
382             String roles = getI18nString( "contributors.roles" );
383             String timeZone = getI18nString( "contributors.timezone" );
384             String actualTime = getI18nString( "contributors.actualtime" );
385             String properties = getI18nString( "contributors.properties" );
386             if ( requiredHeaders.get( IMAGE ) == Boolean.TRUE && showAvatarImages )
387             {
388                 requiredArray.add( image );
389             }
390             setRequiredArray( requiredHeaders, requiredArray, image, name, email, url, organization, organizationUrl,
391                               roles, timeZone, actualTime, properties );
392 
393             return requiredArray.toArray( new String[requiredArray.size()] );
394         }
395 
396         /**
397          * @param requiredHeaders
398          * @return
399          */
400         private String[] getRequiredDevHeaderArray( Map<String, Boolean> requiredHeaders )
401         {
402             List<String> requiredArray = new ArrayList<String>();
403 
404             String image = getI18nString( "developers.image" );
405             String id = getI18nString( "developers.id" );
406             String name = getI18nString( "developers.name" );
407             String email = getI18nString( "developers.email" );
408             String url = getI18nString( "developers.url" );
409             String organization = getI18nString( "developers.organization" );
410             String organizationUrl = getI18nString( "developers.organizationurl" );
411             String roles = getI18nString( "developers.roles" );
412             String timeZone = getI18nString( "developers.timezone" );
413             String actualTime = getI18nString( "developers.actualtime" );
414             String properties = getI18nString( "developers.properties" );
415 
416             if ( requiredHeaders.get( IMAGE ) == Boolean.TRUE && showAvatarImages )
417             {
418                 requiredArray.add( image );
419             }
420             if ( requiredHeaders.get( ID ) == Boolean.TRUE )
421             {
422                 requiredArray.add( id );
423             }
424 
425             setRequiredArray( requiredHeaders, requiredArray, image, name, email, url, organization, organizationUrl,
426                               roles, timeZone, actualTime, properties );
427 
428             return requiredArray.toArray( new String[requiredArray.size()] );
429         }
430 
431         /**
432          * @param requiredHeaders
433          * @param requiredArray
434          * @param image
435          * @param name
436          * @param email
437          * @param url
438          * @param organization
439          * @param organizationUrl
440          * @param roles
441          * @param timeZone
442          * @param actualTime
443          * @param properties
444          */
445         private void setRequiredArray( Map<String, Boolean> requiredHeaders, List<String> requiredArray, String image,
446                                        String name, String email, String url, String organization,
447                                        String organizationUrl, String roles, String timeZone, String actualTime,
448                                        String properties )
449         {
450             if ( requiredHeaders.get( NAME ) == Boolean.TRUE )
451             {
452                 requiredArray.add( name );
453             }
454             if ( requiredHeaders.get( EMAIL ) == Boolean.TRUE )
455             {
456                 requiredArray.add( email );
457             }
458             if ( requiredHeaders.get( URL ) == Boolean.TRUE )
459             {
460                 requiredArray.add( url );
461             }
462             if ( requiredHeaders.get( ORGANIZATION ) == Boolean.TRUE )
463             {
464                 requiredArray.add( organization );
465             }
466             if ( requiredHeaders.get( ORGANIZATION_URL ) == Boolean.TRUE )
467             {
468                 requiredArray.add( organizationUrl );
469             }
470             if ( requiredHeaders.get( ROLES ) == Boolean.TRUE )
471             {
472                 requiredArray.add( roles );
473             }
474             if ( requiredHeaders.get( TIME_ZONE ) == Boolean.TRUE )
475             {
476                 requiredArray.add( timeZone );
477             }
478 
479             if ( requiredHeaders.get( PROPERTIES ) == Boolean.TRUE )
480             {
481                 requiredArray.add( properties );
482             }
483         }
484 
485         /**
486          * @param units contributors and developers to check
487          * @return required headers
488          */
489         private Map<String, Boolean> checkRequiredHeaders( List<? extends Contributor> units )
490         {
491             Map<String, Boolean> requiredHeaders = new HashMap<String, Boolean>();
492 
493             requiredHeaders.put( IMAGE, Boolean.FALSE );
494             requiredHeaders.put( ID, Boolean.FALSE );
495             requiredHeaders.put( NAME, Boolean.FALSE );
496             requiredHeaders.put( EMAIL, Boolean.FALSE );
497             requiredHeaders.put( URL, Boolean.FALSE );
498             requiredHeaders.put( ORGANIZATION, Boolean.FALSE );
499             requiredHeaders.put( ORGANIZATION_URL, Boolean.FALSE );
500             requiredHeaders.put( ROLES, Boolean.FALSE );
501             requiredHeaders.put( TIME_ZONE, Boolean.FALSE );
502             requiredHeaders.put( PROPERTIES, Boolean.FALSE );
503 
504             for ( Contributor unit : units )
505             {
506                 if ( unit instanceof Developer )
507                 {
508                     Developer developer = (Developer) unit;
509                     if ( StringUtils.isNotEmpty( developer.getId() ) )
510                     {
511                         requiredHeaders.put( ID, Boolean.TRUE );
512                     }
513                 }
514                 if ( StringUtils.isNotEmpty( unit.getName() ) )
515                 {
516                     requiredHeaders.put( NAME, Boolean.TRUE );
517                 }
518                 if ( StringUtils.isNotEmpty( unit.getEmail() ) )
519                 {
520                     requiredHeaders.put( EMAIL, Boolean.TRUE );
521                     requiredHeaders.put( IMAGE, Boolean.TRUE );
522                 }
523                 if ( StringUtils.isNotEmpty( unit.getUrl() ) )
524                 {
525                     requiredHeaders.put( URL, Boolean.TRUE );
526                 }
527                 if ( StringUtils.isNotEmpty( unit.getOrganization() ) )
528                 {
529                     requiredHeaders.put( ORGANIZATION, Boolean.TRUE );
530                 }
531                 if ( StringUtils.isNotEmpty( unit.getOrganizationUrl() ) )
532                 {
533                     requiredHeaders.put( ORGANIZATION_URL, Boolean.TRUE );
534                 }
535                 if ( !isEmpty( unit.getRoles() ) )
536                 {
537                     requiredHeaders.put( ROLES, Boolean.TRUE );
538                 }
539                 if ( StringUtils.isNotEmpty( unit.getTimezone() ) )
540                 {
541                     requiredHeaders.put( TIME_ZONE, Boolean.TRUE );
542                 }
543                 Properties properties = unit.getProperties();
544                 boolean hasPicUrl = properties.containsKey( "picUrl" );
545                 if ( hasPicUrl )
546                 {
547                     requiredHeaders.put( IMAGE, Boolean.TRUE );
548                 }
549                 boolean isJustAnImageProperty = properties.size() == 1 && hasPicUrl;
550                 if ( !isJustAnImageProperty && !properties.isEmpty() )
551                 {
552                     requiredHeaders.put( PROPERTIES, Boolean.TRUE );
553                 }
554             }
555             return requiredHeaders;
556         }
557 
558         /**
559          * Create a table cell with a link to the given url. The url is not validated.
560          *
561          * @param url
562          */
563         private void tableCellForUrl( String url )
564         {
565             sink.tableCell();
566 
567             if ( StringUtils.isEmpty( url ) )
568             {
569                 text( url );
570             }
571             else
572             {
573                 link( url, url );
574             }
575 
576             sink.tableCell_();
577         }
578 
579         private boolean isEmpty( List<?> list )
580         {
581             return ( list == null ) || list.isEmpty();
582         }
583     }
584 }