Coverage Report - org.apache.maven.report.projectinfo.TeamListReport
 
Classes in this File Line Coverage Branch Coverage Complexity
TeamListReport
100%
7/7
N/A
5
TeamListReport$TeamListRenderer
70%
173/247
51%
63/122
5
 
 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  2
 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  2
         TeamListRenderer r = new TeamListRenderer( getSink(), project.getModel(), getI18N( locale ), locale, getLog(),
 76  
                                                    showAvatarImages );
 77  
 
 78  2
         r.render();
 79  2
     }
 80  
 
 81  
     /**
 82  
      * {@inheritDoc}
 83  
      */
 84  
     public String getOutputName()
 85  
     {
 86  4
         return "team-list";
 87  
     }
 88  
 
 89  
     @Override
 90  
     protected String getI18Nsection()
 91  
     {
 92  2
         return "team-list";
 93  
     }
 94  
 
 95  
     // ----------------------------------------------------------------------
 96  
     // Private
 97  
     // ----------------------------------------------------------------------
 98  
 
 99  
     /**
 100  
      * Internal renderer class
 101  
      */
 102  2
     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  2
         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  2
             super( sink, i18n, locale );
 136  
 
 137  2
             this.model = model;
 138  2
             this.log = log;
 139  2
             this.showAvatarImages = showAvatarImages;
 140  2
         }
 141  
 
 142  
         @Override
 143  
         protected String getI18Nsection()
 144  
         {
 145  38
             return "team-list";
 146  
         }
 147  
 
 148  
         @Override
 149  
         public void renderBody()
 150  
         {
 151  2
             startSection( getI18nString( "intro.title" ) );
 152  
 
 153  
             // To handle JS
 154  2
             StringBuilder javascript =
 155  
                 new StringBuilder( "function offsetDate(id, offset) {" ).append( SystemUtils.LINE_SEPARATOR );
 156  2
             javascript.append( "    var now = new Date();" ).append( SystemUtils.LINE_SEPARATOR );
 157  2
             javascript.append( "    var nowTime = now.getTime();" ).append( SystemUtils.LINE_SEPARATOR );
 158  2
             javascript.append( "    var localOffset = now.getTimezoneOffset();" ).append( SystemUtils.LINE_SEPARATOR );
 159  2
             javascript.append( "    var developerTime = nowTime + ( offset * 60 * 60 * 1000 )"
 160  
                                    + "+ ( localOffset * 60 * 1000 );" ).append( SystemUtils.LINE_SEPARATOR );
 161  2
             javascript.append( "    var developerDate = new Date(developerTime);" ).append(
 162  
                 SystemUtils.LINE_SEPARATOR );
 163  2
             javascript.append( SystemUtils.LINE_SEPARATOR );
 164  2
             javascript.append( "    document.getElementById(id).innerHTML = developerDate;" ).append(
 165  
                 SystemUtils.LINE_SEPARATOR );
 166  2
             javascript.append( "}" ).append( SystemUtils.LINE_SEPARATOR );
 167  2
             javascript.append( SystemUtils.LINE_SEPARATOR );
 168  2
             javascript.append( "function init(){" ).append( SystemUtils.LINE_SEPARATOR );
 169  
 
 170  
             // Introduction
 171  2
             paragraph( getI18nString( "intro.description1" ) );
 172  2
             paragraph( getI18nString( "intro.description2" ) );
 173  
 
 174  
             // Developer section
 175  2
             List<Developer> developers = model.getDevelopers();
 176  
 
 177  2
             startSection( getI18nString( "developers.title" ) );
 178  
 
 179  2
             if ( isEmpty( developers ) )
 180  
             {
 181  0
                 paragraph( getI18nString( "nodeveloper" ) );
 182  
             }
 183  
             else
 184  
             {
 185  2
                 paragraph( getI18nString( "developers.intro" ) );
 186  
 
 187  2
                 startTable();
 188  
 
 189  
                 // By default we think that all headers not required: set true for headers that are required
 190  2
                 Map<String, Boolean> headersMap = checkRequiredHeaders( developers );
 191  2
                 String[] requiredHeaders = getRequiredDevHeaderArray( headersMap );
 192  
 
 193  2
                 tableHeader( requiredHeaders );
 194  
 
 195  
                 // To handle JS
 196  2
                 int developersRowId = 0;
 197  2
                 for ( Developer developer : developers )
 198  
                 {
 199  4
                     renderTeamMember( developer, developersRowId, headersMap, javascript );
 200  
 
 201  4
                     developersRowId++;
 202  
                 }
 203  
 
 204  2
                 endTable();
 205  
             }
 206  
 
 207  2
             endSection();
 208  
 
 209  
             // contributors section
 210  2
             List<Contributor> contributors = model.getContributors();
 211  
 
 212  2
             startSection( getI18nString( "contributors.title" ) );
 213  
 
 214  2
             if ( isEmpty( contributors ) )
 215  
             {
 216  2
                 paragraph( getI18nString( "nocontributor" ) );
 217  
             }
 218  
             else
 219  
             {
 220  0
                 paragraph( getI18nString( "contributors.intro" ) );
 221  
 
 222  0
                 startTable();
 223  
 
 224  0
                 Map<String, Boolean> headersMap = checkRequiredHeaders( contributors );
 225  0
                 String[] requiredHeaders = getRequiredContrHeaderArray( headersMap );
 226  
 
 227  0
                 tableHeader( requiredHeaders );
 228  
 
 229  
                 // To handle JS
 230  0
                 int contributorsRowId = 0;
 231  0
                 for ( Contributor contributor : contributors )
 232  
                 {
 233  0
                     renderTeamMember( contributor, contributorsRowId, headersMap, javascript );
 234  
 
 235  0
                     contributorsRowId++;
 236  
                 }
 237  
 
 238  0
                 endTable();
 239  
             }
 240  
 
 241  
             // To handle JS
 242  2
             javascript.append( "}" ).append( SystemUtils.LINE_SEPARATOR ).append( SystemUtils.LINE_SEPARATOR ).append(
 243  
                 "window.onLoad = init();" ).append( SystemUtils.LINE_SEPARATOR );
 244  2
             javaScript( javascript.toString() );
 245  
 
 246  2
             endSection();
 247  
 
 248  2
             endSection();
 249  2
         }
 250  
 
 251  
         private void renderTeamMember( Contributor member, int rowId, Map<String, Boolean> headersMap,
 252  
                                        StringBuilder javascript )
 253  
         {
 254  4
             sink.tableRow();
 255  
 
 256  4
             if ( headersMap.get( IMAGE ) == Boolean.TRUE && showAvatarImages )
 257  
             {
 258  4
                 Properties properties = member.getProperties();
 259  4
                 String picUrl = properties.getProperty( "picUrl" );
 260  4
                 if ( StringUtils.isEmpty( picUrl ) )
 261  
                 {
 262  4
                     picUrl = getGravatarUrl( member.getEmail() );
 263  
                 }
 264  4
                 if (StringUtils.isEmpty( picUrl ))
 265  
                 {
 266  2
                     picUrl = getSpacerGravatarUrl();
 267  
                 }
 268  4
                 sink.tableCell();
 269  4
                 sink.figure();
 270  4
                 sink.figureGraphics( picUrl );
 271  4
                 sink.figure_();
 272  4
                 sink.tableCell_();
 273  
             }
 274  4
             String type = "contributor";
 275  4
             if ( member instanceof Developer )
 276  
             {
 277  4
                 type = "developer";
 278  4
                 if ( headersMap.get( ID ) == Boolean.TRUE )
 279  
                 {
 280  4
                     String id = ( (Developer) member ).getId();
 281  4
                     if ( id == null )
 282  
                     {
 283  0
                         tableCell( null );
 284  
                     }
 285  
                     else
 286  
                     {
 287  4
                         tableCell( "<a name=\"" + id + "\"></a>" + id, true );
 288  
                     }
 289  
                 }
 290  
             }
 291  4
             if ( headersMap.get( NAME ) == Boolean.TRUE )
 292  
             {
 293  4
                 tableCell( member.getName() );
 294  
             }
 295  4
             if ( headersMap.get( EMAIL ) == Boolean.TRUE )
 296  
             {
 297  4
                 tableCell( createLinkPatternedText( member.getEmail(), member.getEmail() ) );
 298  
             }
 299  4
             if ( headersMap.get( URL ) == Boolean.TRUE )
 300  
             {
 301  0
                 tableCellForUrl( member.getUrl() );
 302  
             }
 303  4
             if ( headersMap.get( ORGANIZATION ) == Boolean.TRUE )
 304  
             {
 305  4
                 tableCell( member.getOrganization() );
 306  
             }
 307  4
             if ( headersMap.get( ORGANIZATION_URL ) == Boolean.TRUE )
 308  
             {
 309  0
                 tableCellForUrl( member.getOrganizationUrl() );
 310  
             }
 311  4
             if ( headersMap.get( ROLES ) == Boolean.TRUE )
 312  
             {
 313  4
                 if ( member.getRoles() != null )
 314  
                 {
 315  
                     // Comma separated roles
 316  4
                     tableCell( StringUtils.join( member.getRoles().toArray( EMPTY_STRING_ARRAY ), ", " ) );
 317  
                 }
 318  
                 else
 319  
                 {
 320  0
                     tableCell( null );
 321  
                 }
 322  
             }
 323  4
             if ( headersMap.get( TIME_ZONE ) == Boolean.TRUE )
 324  
             {
 325  4
                 tableCell( member.getTimezone() );
 326  
 
 327  4
                 if ( StringUtils.isNotEmpty( member.getTimezone() ) && ( !ProjectInfoReportUtils.isNumber(
 328  
                     member.getTimezone().trim() ) ) )
 329  
                 {
 330  0
                     String tz = member.getTimezone().trim();
 331  
                     try
 332  
                     {
 333  
                         // check if it is a valid timeZone
 334  0
                         DateTimeZone.forID( tz );
 335  
 
 336  0
                         sink.tableCell();
 337  0
                         sink.rawText( "<span id=\"" + type + "-" + rowId + "\">" );
 338  0
                         text( tz );
 339  0
                         String offSet = String.valueOf( TimeZone.getTimeZone( tz ).getRawOffset() / 3600000 );
 340  0
                         javascript.append( "    offsetDate('" ).append( type ).append( "-" ).append( rowId ).append(
 341  
                             "', '" );
 342  0
                         javascript.append( offSet ).append( "');" ).append( SystemUtils.LINE_SEPARATOR );
 343  0
                         sink.rawText( "</span>" );
 344  0
                         sink.tableCell_();
 345  
                     }
 346  0
                     catch ( IllegalArgumentException e )
 347  
                     {
 348  0
                         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  0
                         sink.tableCell();
 352  0
                         sink.rawText( "<span id=\"" + type + "-" + rowId + "\">" );
 353  0
                         text( null );
 354  0
                         sink.rawText( "</span>" );
 355  0
                         sink.tableCell_();
 356  0
                     }
 357  0
                 }
 358  
                 else
 359  
                 {
 360  
                     // To handle JS
 361  4
                     sink.tableCell();
 362  4
                     sink.rawText( "<span id=\"" + type + "-" + rowId + "\">" );
 363  4
                     if ( StringUtils.isEmpty( member.getTimezone() ) )
 364  
                     {
 365  0
                         text( null );
 366  
                     }
 367  
                     else
 368  
                     {
 369  
                         // check if number is between -12 and +14
 370  4
                         float tz = ProjectInfoReportUtils.toFloat( member.getTimezone().trim(), Integer.MIN_VALUE );
 371  4
                         if ( tz == Integer.MIN_VALUE || !( tz >= -12 && tz <= 14 ) )
 372  
                         {
 373  0
                             text( null );
 374  0
                             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  4
                             text( member.getTimezone().trim() );
 381  4
                             javascript.append( "    offsetDate('" ).append( type ).append( "-" ).append( rowId ).append(
 382  
                                 "', '" );
 383  4
                             javascript.append( member.getTimezone() ).append( "');" ).append(
 384  
                                 SystemUtils.LINE_SEPARATOR );
 385  
                         }
 386  
                     }
 387  4
                     sink.rawText( "</span>" );
 388  4
                     sink.tableCell_();
 389  
                 }
 390  
             }
 391  
 
 392  4
             if ( headersMap.get( PROPERTIES ) == Boolean.TRUE )
 393  
             {
 394  0
                 Properties props = member.getProperties();
 395  0
                 if ( props != null )
 396  
                 {
 397  0
                     tableCell( propertiesToString( props ) );
 398  
                 }
 399  
                 else
 400  
                 {
 401  0
                     tableCell( null );
 402  
                 }
 403  
             }
 404  
 
 405  4
             sink.tableRow_();
 406  4
         }
 407  
 
 408  
         private static final String avatar_size = "s=60";
 409  
 
 410  
         private String getSpacerGravatarUrl()
 411  
         {
 412  2
             return "http://www.gravatar.com/avatar/00000000000000000000000000000000?d=blank&f=y&" + avatar_size;
 413  
         }
 414  
 
 415  
         private String getGravatarUrl( String email )
 416  
         {
 417  4
             if ( email == null )
 418  
             {
 419  2
                 return null;
 420  
             }
 421  2
             email = StringUtils.trim( email );
 422  2
             email = email.toLowerCase();
 423  
             MessageDigest md;
 424  
             try
 425  
             {
 426  2
                 md = MessageDigest.getInstance( "MD5" );
 427  2
                 md.update( email.getBytes() );
 428  2
                 byte byteData[] = md.digest();
 429  2
                 StringBuilder sb = new StringBuilder();
 430  34
                 for ( byte aByteData : byteData )
 431  
                 {
 432  32
                     sb.append( Integer.toString( ( aByteData & 0xff ) + 0x100, 16 ).substring( 1 ) );
 433  
                 }
 434  2
                 return "http://www.gravatar.com/avatar/" + sb.toString() + "?d=mm&" + avatar_size;
 435  
             }
 436  0
             catch ( NoSuchAlgorithmException e )
 437  
             {
 438  0
                 return null;
 439  
             }
 440  
         }
 441  
 
 442  
         private String img( String src )
 443  
         {
 444  0
             if ( src == null )
 445  
             {
 446  0
                 return "";
 447  
             }
 448  0
             return "<img src='" + src + "'/>";
 449  
         }
 450  
 
 451  
         /**
 452  
          * @param requiredHeaders
 453  
          * @return
 454  
          */
 455  
         private String[] getRequiredContrHeaderArray( Map<String, Boolean> requiredHeaders )
 456  
         {
 457  0
             List<String> requiredArray = new ArrayList<String>();
 458  0
             String image = getI18nString( "contributors.image" );
 459  0
             String name = getI18nString( "contributors.name" );
 460  0
             String email = getI18nString( "contributors.email" );
 461  0
             String url = getI18nString( "contributors.url" );
 462  0
             String organization = getI18nString( "contributors.organization" );
 463  0
             String organizationUrl = getI18nString( "contributors.organizationurl" );
 464  0
             String roles = getI18nString( "contributors.roles" );
 465  0
             String timeZone = getI18nString( "contributors.timezone" );
 466  0
             String actualTime = getI18nString( "contributors.actualtime" );
 467  0
             String properties = getI18nString( "contributors.properties" );
 468  0
             if ( requiredHeaders.get( IMAGE ) == Boolean.TRUE && showAvatarImages)
 469  
             {
 470  0
                 requiredArray.add( image );
 471  
             }
 472  0
             setRequiredArray( requiredHeaders, requiredArray, image, name, email, url, organization, organizationUrl, roles,
 473  
                               timeZone, actualTime, properties );
 474  
 
 475  0
             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  2
             List<String> requiredArray = new ArrayList<String>();
 485  
 
 486  2
             String image = getI18nString( "developers.image" );
 487  2
             String id = getI18nString( "developers.id" );
 488  2
             String name = getI18nString( "developers.name" );
 489  2
             String email = getI18nString( "developers.email" );
 490  2
             String url = getI18nString( "developers.url" );
 491  2
             String organization = getI18nString( "developers.organization" );
 492  2
             String organizationUrl = getI18nString( "developers.organizationurl" );
 493  2
             String roles = getI18nString( "developers.roles" );
 494  2
             String timeZone = getI18nString( "developers.timezone" );
 495  2
             String actualTime = getI18nString( "developers.actualtime" );
 496  2
             String properties = getI18nString( "developers.properties" );
 497  
 
 498  2
             if ( requiredHeaders.get( IMAGE ) == Boolean.TRUE && showAvatarImages)
 499  
             {
 500  2
                 requiredArray.add( image );
 501  
             }
 502  2
             if ( requiredHeaders.get( ID ) == Boolean.TRUE )
 503  
             {
 504  2
                 requiredArray.add( id );
 505  
             }
 506  
 
 507  2
             setRequiredArray( requiredHeaders, requiredArray, image, name, email, url, organization, organizationUrl, roles,
 508  
                               timeZone, actualTime, properties );
 509  
 
 510  2
             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  2
             if ( requiredHeaders.get( NAME ) == Boolean.TRUE )
 532  
             {
 533  2
                 requiredArray.add( name );
 534  
             }
 535  2
             if ( requiredHeaders.get( EMAIL ) == Boolean.TRUE )
 536  
             {
 537  2
                 requiredArray.add( email );
 538  
             }
 539  2
             if ( requiredHeaders.get( URL ) == Boolean.TRUE )
 540  
             {
 541  0
                 requiredArray.add( url );
 542  
             }
 543  2
             if ( requiredHeaders.get( ORGANIZATION ) == Boolean.TRUE )
 544  
             {
 545  2
                 requiredArray.add( organization );
 546  
             }
 547  2
             if ( requiredHeaders.get( ORGANIZATION_URL ) == Boolean.TRUE )
 548  
             {
 549  0
                 requiredArray.add( organizationUrl );
 550  
             }
 551  2
             if ( requiredHeaders.get( ROLES ) == Boolean.TRUE )
 552  
             {
 553  2
                 requiredArray.add( roles );
 554  
             }
 555  2
             if ( requiredHeaders.get( TIME_ZONE ) == Boolean.TRUE )
 556  
             {
 557  2
                 requiredArray.add( timeZone );
 558  2
                 requiredArray.add( actualTime );
 559  
             }
 560  
 
 561  2
             if ( requiredHeaders.get( PROPERTIES ) == Boolean.TRUE )
 562  
             {
 563  0
                 requiredArray.add( properties );
 564  
             }
 565  2
         }
 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  2
             Map<String, Boolean> requiredHeaders = new HashMap<String, Boolean>();
 574  
 
 575  2
             requiredHeaders.put( IMAGE, Boolean.FALSE );
 576  2
             requiredHeaders.put( ID, Boolean.FALSE );
 577  2
             requiredHeaders.put( NAME, Boolean.FALSE );
 578  2
             requiredHeaders.put( EMAIL, Boolean.FALSE );
 579  2
             requiredHeaders.put( URL, Boolean.FALSE );
 580  2
             requiredHeaders.put( ORGANIZATION, Boolean.FALSE );
 581  2
             requiredHeaders.put( ORGANIZATION_URL, Boolean.FALSE );
 582  2
             requiredHeaders.put( ROLES, Boolean.FALSE );
 583  2
             requiredHeaders.put( TIME_ZONE, Boolean.FALSE );
 584  2
             requiredHeaders.put( PROPERTIES, Boolean.FALSE );
 585  
 
 586  2
             for ( Contributor unit : units )
 587  
             {
 588  4
                 if ( unit instanceof Developer )
 589  
                 {
 590  4
                     Developer developer = (Developer) unit;
 591  4
                     if ( StringUtils.isNotEmpty( developer.getId() ) )
 592  
                     {
 593  4
                         requiredHeaders.put( ID, Boolean.TRUE );
 594  
                     }
 595  
                 }
 596  4
                 if ( StringUtils.isNotEmpty( unit.getName() ) )
 597  
                 {
 598  4
                     requiredHeaders.put( NAME, Boolean.TRUE );
 599  
                 }
 600  4
                 if ( StringUtils.isNotEmpty( unit.getEmail() ) )
 601  
                 {
 602  2
                     requiredHeaders.put( EMAIL, Boolean.TRUE );
 603  2
                     requiredHeaders.put( IMAGE, Boolean.TRUE );
 604  
                 }
 605  4
                 if ( StringUtils.isNotEmpty( unit.getUrl() ) )
 606  
                 {
 607  0
                     requiredHeaders.put( URL, Boolean.TRUE );
 608  
                 }
 609  4
                 if ( StringUtils.isNotEmpty( unit.getOrganization() ) )
 610  
                 {
 611  2
                     requiredHeaders.put( ORGANIZATION, Boolean.TRUE );
 612  
                 }
 613  4
                 if ( StringUtils.isNotEmpty( unit.getOrganizationUrl() ) )
 614  
                 {
 615  0
                     requiredHeaders.put( ORGANIZATION_URL, Boolean.TRUE );
 616  
                 }
 617  4
                 if ( !isEmpty( unit.getRoles() ) )
 618  
                 {
 619  2
                     requiredHeaders.put( ROLES, Boolean.TRUE );
 620  
                 }
 621  4
                 if ( StringUtils.isNotEmpty( unit.getTimezone() ) )
 622  
                 {
 623  4
                     requiredHeaders.put( TIME_ZONE, Boolean.TRUE );
 624  
                 }
 625  4
                 Properties properties = unit.getProperties();
 626  4
                 boolean hasPicUrl = properties.contains( "picUrl" );
 627  4
                 if ( hasPicUrl )
 628  
                 {
 629  0
                     requiredHeaders.put( IMAGE, Boolean.TRUE );
 630  
                 }
 631  4
                 boolean isJustAnImageProperty = properties.size() == 1 && hasPicUrl;
 632  4
                 if ( !isJustAnImageProperty && !properties.isEmpty() )
 633  
                 {
 634  0
                     requiredHeaders.put( PROPERTIES, Boolean.TRUE );
 635  
                 }
 636  4
             }
 637  2
             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  0
             sink.tableCell();
 648  
 
 649  0
             if ( StringUtils.isEmpty( url ) )
 650  
             {
 651  0
                 text( url );
 652  
             }
 653  
             else
 654  
             {
 655  0
                 link( url, url );
 656  
             }
 657  
 
 658  0
             sink.tableCell_();
 659  0
         }
 660  
 
 661  
         private boolean isEmpty( List<?> list )
 662  
         {
 663  8
             return ( list == null ) || list.isEmpty();
 664  
         }
 665  
     }
 666  
 }