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 java.util.TimeZone;
31  import org.apache.commons.lang.SystemUtils;
32  import org.apache.maven.doxia.sink.Sink;
33  import org.apache.maven.model.Contributor;
34  import org.apache.maven.model.Developer;
35  import org.apache.maven.model.Model;
36  import org.apache.maven.plugin.logging.Log;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.codehaus.plexus.i18n.I18N;
40  import org.codehaus.plexus.util.StringUtils;
41  
42  import org.joda.time.DateTimeZone;
43  
44  /**
45   * Generates the Project Team report.
46   *
47   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton </a>
48   * @version $Id: TeamListReport.java 1402029 2012-10-25 08:58:20Z krosenvold $
49   * @since 2.0
50   */
51  @Mojo( name = "project-team" )
52  public class TeamListReport
53      extends AbstractProjectInfoReport
54  {
55      /**
56       * Shows avatar images for team members that have
57       * a) properties/picUrl set
58       * b) An avatar at gravatar.com for their email address
59       * <p/>
60       * Future versions of this plugin may choose to implement different strategies for resolving
61       * avatar images, possibly using different providers.
62       *
63       * @since 2.6
64       */
65      @Parameter( property = "teamlist.showAvatarImages", defaultValue = "true" )
66      private boolean showAvatarImages;
67  
68      // ----------------------------------------------------------------------
69      // Public methods
70      // ----------------------------------------------------------------------
71  
72      @Override
73      public void executeReport( Locale locale )
74      {
75          TeamListRenderer r = new TeamListRenderer( getSink(), project.getModel(), getI18N( locale ), locale, getLog(),
76                                                     showAvatarImages );
77  
78          r.render();
79      }
80  
81      /**
82       * {@inheritDoc}
83       */
84      public String getOutputName()
85      {
86          return "team-list";
87      }
88  
89      @Override
90      protected String getI18Nsection()
91      {
92          return "team-list";
93      }
94  
95      // ----------------------------------------------------------------------
96      // Private
97      // ----------------------------------------------------------------------
98  
99      /**
100      * Internal renderer class
101      */
102     private static class TeamListRenderer
103         extends AbstractProjectInfoRenderer
104     {
105         private static final String PROPERTIES = "properties";
106 
107         private static final String TIME_ZONE = "timeZone";
108 
109         private static final String ROLES = "roles";
110 
111         private static final String ORGANIZATION_URL = "organizationUrl";
112 
113         private static final String ORGANIZATION = "organization";
114 
115         private static final String URL = "url";
116 
117         private static final String EMAIL = "email";
118 
119         private static final String NAME = "name";
120 
121         private static final String IMAGE = "image";
122 
123         private static final String ID = "id";
124 
125         private final Model model;
126 
127         private final Log log;
128 
129         private final boolean showAvatarImages;
130 
131         private static final String[] EMPTY_STRING_ARRAY = new String[0];
132 
133         TeamListRenderer( Sink sink, Model model, I18N i18n, Locale locale, Log log, boolean showAvatarImages )
134         {
135             super( sink, i18n, locale );
136 
137             this.model = model;
138             this.log = log;
139             this.showAvatarImages = showAvatarImages;
140         }
141 
142         @Override
143         protected String getI18Nsection()
144         {
145             return "team-list";
146         }
147 
148         @Override
149         public void renderBody()
150         {
151             startSection( getI18nString( "intro.title" ) );
152 
153             // To handle JS
154             StringBuilder javascript =
155                 new StringBuilder( "function offsetDate(id, offset) {" ).append( SystemUtils.LINE_SEPARATOR );
156             javascript.append( "    var now = new Date();" ).append( SystemUtils.LINE_SEPARATOR );
157             javascript.append( "    var nowTime = now.getTime();" ).append( SystemUtils.LINE_SEPARATOR );
158             javascript.append( "    var localOffset = now.getTimezoneOffset();" ).append( SystemUtils.LINE_SEPARATOR );
159             javascript.append( "    var developerTime = nowTime + ( offset * 60 * 60 * 1000 )"
160                                    + "+ ( localOffset * 60 * 1000 );" ).append( SystemUtils.LINE_SEPARATOR );
161             javascript.append( "    var developerDate = new Date(developerTime);" ).append(
162                 SystemUtils.LINE_SEPARATOR );
163             javascript.append( SystemUtils.LINE_SEPARATOR );
164             javascript.append( "    document.getElementById(id).innerHTML = developerDate;" ).append(
165                 SystemUtils.LINE_SEPARATOR );
166             javascript.append( "}" ).append( SystemUtils.LINE_SEPARATOR );
167             javascript.append( SystemUtils.LINE_SEPARATOR );
168             javascript.append( "function init(){" ).append( SystemUtils.LINE_SEPARATOR );
169 
170             // Introduction
171             paragraph( getI18nString( "intro.description1" ) );
172             paragraph( getI18nString( "intro.description2" ) );
173 
174             // Developer section
175             List<Developer> developers = model.getDevelopers();
176 
177             startSection( getI18nString( "developers.title" ) );
178 
179             if ( isEmpty( developers ) )
180             {
181                 paragraph( getI18nString( "nodeveloper" ) );
182             }
183             else
184             {
185                 paragraph( getI18nString( "developers.intro" ) );
186 
187                 startTable();
188 
189                 // By default we think that all headers not required: set true for headers that are required
190                 Map<String, Boolean> headersMap = checkRequiredHeaders( developers );
191                 String[] requiredHeaders = getRequiredDevHeaderArray( headersMap );
192 
193                 tableHeader( requiredHeaders );
194 
195                 // To handle JS
196                 int developersRowId = 0;
197                 for ( Developer developer : developers )
198                 {
199                     renderTeamMember( developer, developersRowId, headersMap, javascript );
200 
201                     developersRowId++;
202                 }
203 
204                 endTable();
205             }
206 
207             endSection();
208 
209             // contributors section
210             List<Contributor> contributors = model.getContributors();
211 
212             startSection( getI18nString( "contributors.title" ) );
213 
214             if ( isEmpty( contributors ) )
215             {
216                 paragraph( getI18nString( "nocontributor" ) );
217             }
218             else
219             {
220                 paragraph( getI18nString( "contributors.intro" ) );
221 
222                 startTable();
223 
224                 Map<String, Boolean> headersMap = checkRequiredHeaders( contributors );
225                 String[] requiredHeaders = getRequiredContrHeaderArray( headersMap );
226 
227                 tableHeader( requiredHeaders );
228 
229                 // To handle JS
230                 int contributorsRowId = 0;
231                 for ( Contributor contributor : contributors )
232                 {
233                     renderTeamMember( contributor, contributorsRowId, headersMap, javascript );
234 
235                     contributorsRowId++;
236                 }
237 
238                 endTable();
239             }
240 
241             // To handle JS
242             javascript.append( "}" ).append( SystemUtils.LINE_SEPARATOR ).append( SystemUtils.LINE_SEPARATOR ).append(
243                 "window.onLoad = init();" ).append( SystemUtils.LINE_SEPARATOR );
244             javaScript( javascript.toString() );
245 
246             endSection();
247 
248             endSection();
249         }
250 
251         private void renderTeamMember( Contributor member, int rowId, Map<String, Boolean> headersMap,
252                                        StringBuilder javascript )
253         {
254             sink.tableRow();
255 
256             if ( headersMap.get( IMAGE ) == Boolean.TRUE && showAvatarImages )
257             {
258                 Properties properties = member.getProperties();
259                 String picUrl = properties.getProperty( "picUrl" );
260                 if ( StringUtils.isEmpty( picUrl ) )
261                 {
262                     picUrl = getGravatarUrl( member.getEmail() );
263                 }
264                 if (StringUtils.isEmpty( picUrl ))
265                 {
266                     picUrl = getSpacerGravatarUrl();
267                 }
268                 sink.tableCell();
269                 sink.figure();
270                 sink.figureGraphics( picUrl );
271                 sink.figure_();
272                 sink.tableCell_();
273             }
274             String type = "contributor";
275             if ( member instanceof Developer )
276             {
277                 type = "developer";
278                 if ( headersMap.get( ID ) == Boolean.TRUE )
279                 {
280                     String id = ( (Developer) member ).getId();
281                     if ( id == null )
282                     {
283                         tableCell( null );
284                     }
285                     else
286                     {
287                         tableCell( "<a name=\"" + id + "\"></a>" + id, true );
288                     }
289                 }
290             }
291             if ( headersMap.get( NAME ) == Boolean.TRUE )
292             {
293                 tableCell( member.getName() );
294             }
295             if ( headersMap.get( EMAIL ) == Boolean.TRUE )
296             {
297                 tableCell( createLinkPatternedText( member.getEmail(), member.getEmail() ) );
298             }
299             if ( headersMap.get( URL ) == Boolean.TRUE )
300             {
301                 tableCellForUrl( member.getUrl() );
302             }
303             if ( headersMap.get( ORGANIZATION ) == Boolean.TRUE )
304             {
305                 tableCell( member.getOrganization() );
306             }
307             if ( headersMap.get( ORGANIZATION_URL ) == Boolean.TRUE )
308             {
309                 tableCellForUrl( member.getOrganizationUrl() );
310             }
311             if ( headersMap.get( ROLES ) == Boolean.TRUE )
312             {
313                 if ( member.getRoles() != null )
314                 {
315                     // Comma separated roles
316                     tableCell( StringUtils.join( member.getRoles().toArray( EMPTY_STRING_ARRAY ), ", " ) );
317                 }
318                 else
319                 {
320                     tableCell( null );
321                 }
322             }
323             if ( headersMap.get( TIME_ZONE ) == Boolean.TRUE )
324             {
325                 tableCell( member.getTimezone() );
326 
327                 if ( StringUtils.isNotEmpty( member.getTimezone() ) && ( !ProjectInfoReportUtils.isNumber(
328                     member.getTimezone().trim() ) ) )
329                 {
330                     String tz = member.getTimezone().trim();
331                     try
332                     {
333                         // check if it is a valid timeZone
334                         DateTimeZone.forID( tz );
335 
336                         sink.tableCell();
337                         sink.rawText( "<span id=\"" + type + "-" + rowId + "\">" );
338                         text( tz );
339                         String offSet = String.valueOf( TimeZone.getTimeZone( tz ).getRawOffset() / 3600000 );
340                         javascript.append( "    offsetDate('" ).append( type ).append( "-" ).append( rowId ).append(
341                             "', '" );
342                         javascript.append( offSet ).append( "');" ).append( SystemUtils.LINE_SEPARATOR );
343                         sink.rawText( "</span>" );
344                         sink.tableCell_();
345                     }
346                     catch ( IllegalArgumentException e )
347                     {
348                         log.warn( "The time zone '" + tz + "' for the " + type + " '" + member.getName()
349                                       + "' is not a recognised time zone, use a number in the range -12 and +14 instead of." );
350 
351                         sink.tableCell();
352                         sink.rawText( "<span id=\"" + type + "-" + rowId + "\">" );
353                         text( null );
354                         sink.rawText( "</span>" );
355                         sink.tableCell_();
356                     }
357                 }
358                 else
359                 {
360                     // To handle JS
361                     sink.tableCell();
362                     sink.rawText( "<span id=\"" + type + "-" + rowId + "\">" );
363                     if ( StringUtils.isEmpty( member.getTimezone() ) )
364                     {
365                         text( null );
366                     }
367                     else
368                     {
369                         // check if number is between -12 and +14
370                         float tz = ProjectInfoReportUtils.toFloat( member.getTimezone().trim(), Integer.MIN_VALUE );
371                         if ( tz == Integer.MIN_VALUE || !( tz >= -12 && tz <= 14 ) )
372                         {
373                             text( null );
374                             log.warn( "The time zone '" + member.getTimezone().trim() + "' for the " + type + " '"
375                                           + member.getName()
376                                           + "' is not a recognised time zone, use a number in the range -12 to +14 instead of." );
377                         }
378                         else
379                         {
380                             text( member.getTimezone().trim() );
381                             javascript.append( "    offsetDate('" ).append( type ).append( "-" ).append( rowId ).append(
382                                 "', '" );
383                             javascript.append( member.getTimezone() ).append( "');" ).append(
384                                 SystemUtils.LINE_SEPARATOR );
385                         }
386                     }
387                     sink.rawText( "</span>" );
388                     sink.tableCell_();
389                 }
390             }
391 
392             if ( headersMap.get( PROPERTIES ) == Boolean.TRUE )
393             {
394                 Properties props = member.getProperties();
395                 if ( props != null )
396                 {
397                     tableCell( propertiesToString( props ) );
398                 }
399                 else
400                 {
401                     tableCell( null );
402                 }
403             }
404 
405             sink.tableRow_();
406         }
407 
408         private static final String avatar_size = "s=60";
409 
410         private String getSpacerGravatarUrl()
411         {
412             return "http://www.gravatar.com/avatar/00000000000000000000000000000000?d=blank&f=y&" + avatar_size;
413         }
414 
415         private String getGravatarUrl( String email )
416         {
417             if ( email == null )
418             {
419                 return null;
420             }
421             email = StringUtils.trim( email );
422             email = email.toLowerCase();
423             MessageDigest md;
424             try
425             {
426                 md = MessageDigest.getInstance( "MD5" );
427                 md.update( email.getBytes() );
428                 byte byteData[] = md.digest();
429                 StringBuilder sb = new StringBuilder();
430                 for ( byte aByteData : byteData )
431                 {
432                     sb.append( Integer.toString( ( aByteData & 0xff ) + 0x100, 16 ).substring( 1 ) );
433                 }
434                 return "http://www.gravatar.com/avatar/" + sb.toString() + "?d=mm&" + avatar_size;
435             }
436             catch ( NoSuchAlgorithmException e )
437             {
438                 return null;
439             }
440         }
441 
442         private String img( String src )
443         {
444             if ( src == null )
445             {
446                 return "";
447             }
448             return "<img src='" + src + "'/>";
449         }
450 
451         /**
452          * @param requiredHeaders
453          * @return
454          */
455         private String[] getRequiredContrHeaderArray( Map<String, Boolean> requiredHeaders )
456         {
457             List<String> requiredArray = new ArrayList<String>();
458             String image = getI18nString( "contributors.image" );
459             String name = getI18nString( "contributors.name" );
460             String email = getI18nString( "contributors.email" );
461             String url = getI18nString( "contributors.url" );
462             String organization = getI18nString( "contributors.organization" );
463             String organizationUrl = getI18nString( "contributors.organizationurl" );
464             String roles = getI18nString( "contributors.roles" );
465             String timeZone = getI18nString( "contributors.timezone" );
466             String actualTime = getI18nString( "contributors.actualtime" );
467             String properties = getI18nString( "contributors.properties" );
468             if ( requiredHeaders.get( IMAGE ) == Boolean.TRUE && showAvatarImages)
469             {
470                 requiredArray.add( image );
471             }
472             setRequiredArray( requiredHeaders, requiredArray, image, name, email, url, organization, organizationUrl, roles,
473                               timeZone, actualTime, properties );
474 
475             return requiredArray.toArray( new String[requiredArray.size()] );
476         }
477 
478         /**
479          * @param requiredHeaders
480          * @return
481          */
482         private String[] getRequiredDevHeaderArray( Map<String, Boolean> requiredHeaders )
483         {
484             List<String> requiredArray = new ArrayList<String>();
485 
486             String image = getI18nString( "developers.image" );
487             String id = getI18nString( "developers.id" );
488             String name = getI18nString( "developers.name" );
489             String email = getI18nString( "developers.email" );
490             String url = getI18nString( "developers.url" );
491             String organization = getI18nString( "developers.organization" );
492             String organizationUrl = getI18nString( "developers.organizationurl" );
493             String roles = getI18nString( "developers.roles" );
494             String timeZone = getI18nString( "developers.timezone" );
495             String actualTime = getI18nString( "developers.actualtime" );
496             String properties = getI18nString( "developers.properties" );
497 
498             if ( requiredHeaders.get( IMAGE ) == Boolean.TRUE && showAvatarImages)
499             {
500                 requiredArray.add( image );
501             }
502             if ( requiredHeaders.get( ID ) == Boolean.TRUE )
503             {
504                 requiredArray.add( id );
505             }
506 
507             setRequiredArray( requiredHeaders, requiredArray, image, name, email, url, organization, organizationUrl, roles,
508                               timeZone, actualTime, properties );
509 
510             return requiredArray.toArray( new String[requiredArray.size()] );
511         }
512 
513         /**
514          * @param requiredHeaders
515          * @param requiredArray
516          * @param image
517          * @param name
518          * @param email
519          * @param url
520          * @param organization
521          * @param organizationUrl
522          * @param roles
523          * @param timeZone
524          * @param actualTime
525          * @param properties
526          */
527         private void setRequiredArray( Map<String, Boolean> requiredHeaders, List<String> requiredArray, String image,
528                                        String name, String email, String url, String organization, String organizationUrl,
529                                        String roles, String timeZone, String actualTime, String properties )
530         {
531             if ( requiredHeaders.get( NAME ) == Boolean.TRUE )
532             {
533                 requiredArray.add( name );
534             }
535             if ( requiredHeaders.get( EMAIL ) == Boolean.TRUE )
536             {
537                 requiredArray.add( email );
538             }
539             if ( requiredHeaders.get( URL ) == Boolean.TRUE )
540             {
541                 requiredArray.add( url );
542             }
543             if ( requiredHeaders.get( ORGANIZATION ) == Boolean.TRUE )
544             {
545                 requiredArray.add( organization );
546             }
547             if ( requiredHeaders.get( ORGANIZATION_URL ) == Boolean.TRUE )
548             {
549                 requiredArray.add( organizationUrl );
550             }
551             if ( requiredHeaders.get( ROLES ) == Boolean.TRUE )
552             {
553                 requiredArray.add( roles );
554             }
555             if ( requiredHeaders.get( TIME_ZONE ) == Boolean.TRUE )
556             {
557                 requiredArray.add( timeZone );
558                 requiredArray.add( actualTime );
559             }
560 
561             if ( requiredHeaders.get( PROPERTIES ) == Boolean.TRUE )
562             {
563                 requiredArray.add( properties );
564             }
565         }
566 
567         /**
568          * @param units contributors and developers to check
569          * @return required headers
570          */
571         private Map<String, Boolean> checkRequiredHeaders( List<? extends Contributor> units )
572         {
573             Map<String, Boolean> requiredHeaders = new HashMap<String, Boolean>();
574 
575             requiredHeaders.put( IMAGE, Boolean.FALSE );
576             requiredHeaders.put( ID, Boolean.FALSE );
577             requiredHeaders.put( NAME, Boolean.FALSE );
578             requiredHeaders.put( EMAIL, Boolean.FALSE );
579             requiredHeaders.put( URL, Boolean.FALSE );
580             requiredHeaders.put( ORGANIZATION, Boolean.FALSE );
581             requiredHeaders.put( ORGANIZATION_URL, Boolean.FALSE );
582             requiredHeaders.put( ROLES, Boolean.FALSE );
583             requiredHeaders.put( TIME_ZONE, Boolean.FALSE );
584             requiredHeaders.put( PROPERTIES, Boolean.FALSE );
585 
586             for ( Contributor unit : units )
587             {
588                 if ( unit instanceof Developer )
589                 {
590                     Developer developer = (Developer) unit;
591                     if ( StringUtils.isNotEmpty( developer.getId() ) )
592                     {
593                         requiredHeaders.put( ID, Boolean.TRUE );
594                     }
595                 }
596                 if ( StringUtils.isNotEmpty( unit.getName() ) )
597                 {
598                     requiredHeaders.put( NAME, Boolean.TRUE );
599                 }
600                 if ( StringUtils.isNotEmpty( unit.getEmail() ) )
601                 {
602                     requiredHeaders.put( EMAIL, Boolean.TRUE );
603                     requiredHeaders.put( IMAGE, Boolean.TRUE );
604                 }
605                 if ( StringUtils.isNotEmpty( unit.getUrl() ) )
606                 {
607                     requiredHeaders.put( URL, Boolean.TRUE );
608                 }
609                 if ( StringUtils.isNotEmpty( unit.getOrganization() ) )
610                 {
611                     requiredHeaders.put( ORGANIZATION, Boolean.TRUE );
612                 }
613                 if ( StringUtils.isNotEmpty( unit.getOrganizationUrl() ) )
614                 {
615                     requiredHeaders.put( ORGANIZATION_URL, Boolean.TRUE );
616                 }
617                 if ( !isEmpty( unit.getRoles() ) )
618                 {
619                     requiredHeaders.put( ROLES, Boolean.TRUE );
620                 }
621                 if ( StringUtils.isNotEmpty( unit.getTimezone() ) )
622                 {
623                     requiredHeaders.put( TIME_ZONE, Boolean.TRUE );
624                 }
625                 Properties properties = unit.getProperties();
626                 boolean hasPicUrl = properties.contains( "picUrl" );
627                 if ( hasPicUrl )
628                 {
629                     requiredHeaders.put( IMAGE, Boolean.TRUE );
630                 }
631                 boolean isJustAnImageProperty = properties.size() == 1 && hasPicUrl;
632                 if ( !isJustAnImageProperty && !properties.isEmpty() )
633                 {
634                     requiredHeaders.put( PROPERTIES, Boolean.TRUE );
635                 }
636             }
637             return requiredHeaders;
638         }
639 
640         /**
641          * Create a table cell with a link to the given url. The url is not validated.
642          *
643          * @param url
644          */
645         private void tableCellForUrl( String url )
646         {
647             sink.tableCell();
648 
649             if ( StringUtils.isEmpty( url ) )
650             {
651                 text( url );
652             }
653             else
654             {
655                 link( url, url );
656             }
657 
658             sink.tableCell_();
659         }
660 
661         private boolean isEmpty( List<?> list )
662         {
663             return ( list == null ) || list.isEmpty();
664         }
665     }
666 }