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