View Javadoc
1   package org.apache.maven.plugin.doap;
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.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.lang.reflect.Method;
27  import java.net.MalformedURLException;
28  import java.net.SocketTimeoutException;
29  import java.net.URL;
30  import java.text.DateFormat;
31  import java.util.ArrayList;
32  import java.util.Collection;
33  import java.util.Date;
34  import java.util.HashMap;
35  import java.util.LinkedList;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Map;
39  import java.util.StringTokenizer;
40  import java.util.WeakHashMap;
41  import java.util.Map.Entry;
42  import java.util.regex.Matcher;
43  import java.util.regex.Pattern;
44  import java.util.Set;
45  import java.util.Properties;
46  
47  import org.apache.commons.httpclient.Credentials;
48  import org.apache.commons.httpclient.HttpClient;
49  import org.apache.commons.httpclient.HttpStatus;
50  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
51  import org.apache.commons.httpclient.UsernamePasswordCredentials;
52  import org.apache.commons.httpclient.auth.AuthScope;
53  import org.apache.commons.httpclient.methods.GetMethod;
54  import org.apache.commons.httpclient.params.HttpClientParams;
55  import org.apache.commons.httpclient.params.HttpMethodParams;
56  import org.apache.maven.model.Contributor;
57  import org.apache.maven.project.MavenProject;
58  import org.apache.maven.settings.Proxy;
59  import org.apache.maven.settings.Settings;
60  import org.apache.maven.wagon.proxy.ProxyInfo;
61  import org.apache.maven.wagon.proxy.ProxyUtils;
62  import org.codehaus.plexus.i18n.I18N;
63  import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
64  import org.codehaus.plexus.interpolation.InterpolationException;
65  import org.codehaus.plexus.interpolation.ObjectBasedValueSource;
66  import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
67  import org.codehaus.plexus.interpolation.PropertiesBasedValueSource;
68  import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
69  import org.codehaus.plexus.util.IOUtil;
70  import org.codehaus.plexus.util.StringUtils;
71  import org.codehaus.plexus.util.introspection.ClassMap;
72  import org.codehaus.plexus.util.xml.XMLWriter;
73  import org.codehaus.plexus.util.xml.XmlWriterUtil;
74  
75  import com.hp.hpl.jena.rdf.model.Model;
76  import com.hp.hpl.jena.rdf.model.ModelFactory;
77  import com.hp.hpl.jena.rdf.model.RDFReader;
78  import com.hp.hpl.jena.rdf.model.impl.RDFDefaultErrorHandler;
79  
80  /**
81   * Utility class for {@link DoapMojo} class.
82   *
83   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
84   * @version $Id: DoapUtil.java 1642247 2014-11-27 23:17:01Z hboutemy $
85   * @since 1.0
86   */
87  public class DoapUtil
88  {
89      /** Email regex */
90      private static final String EMAIL_REGEX =
91          "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
92  
93      /** Email pattern */
94      private static final Pattern EMAIL_PATTERN = Pattern.compile( EMAIL_REGEX );
95  
96      /** Magic number to repeat '=' */
97      private static final int REPEAT_EQUALS = 21;
98  
99      /** The default timeout used when fetching url, i.e. 2000. */
100     public static final int DEFAULT_TIMEOUT = 2000;
101 
102     /** RDF resource attribute */
103     protected static final String RDF_RESOURCE = "rdf:resource";
104 
105     /** RDF nodeID attribute */
106     protected static final String RDF_NODE_ID = "rdf:nodeID";
107 
108     /** DoaP Organizations stored by name */
109     private static Map<String, DoapUtil.Organization> organizations = new HashMap<String, DoapUtil.Organization>();
110 
111     /**
112      * Write comments in the DOAP file header
113      *
114      * @param writer not null
115      */
116     public static void writeHeader( XMLWriter writer )
117     {
118         XmlWriterUtil.writeLineBreak( writer );
119 
120         XmlWriterUtil.writeCommentLineBreak( writer );
121         XmlWriterUtil.writeComment( writer, StringUtils.repeat( "=", REPEAT_EQUALS ) + " - DO NOT EDIT THIS FILE! - "
122             + StringUtils.repeat( "=", REPEAT_EQUALS ) );
123         XmlWriterUtil.writeCommentLineBreak( writer );
124         XmlWriterUtil.writeComment( writer, " " );
125         XmlWriterUtil.writeComment( writer, "Any modifications will be overwritten." );
126         XmlWriterUtil.writeComment( writer, " " );
127         DateFormat dateFormat = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, Locale.US );
128         XmlWriterUtil.writeComment( writer, "Generated by Maven Doap Plugin " + getPluginVersion() + " on "
129             + dateFormat.format( new Date( System.currentTimeMillis() ) ) );
130         XmlWriterUtil.writeComment( writer, "See: http://maven.apache.org/plugins/maven-doap-plugin/" );
131         XmlWriterUtil.writeComment( writer, " " );
132         XmlWriterUtil.writeCommentLineBreak( writer );
133 
134         XmlWriterUtil.writeLineBreak( writer );
135     }
136 
137     /**
138      * Write comment.
139      *
140      * @param writer not null
141      * @param comment not null
142      * @throws IllegalArgumentException if comment is null or empty
143      * @since 1.1
144      */
145     public static void writeComment( XMLWriter writer, String comment )
146         throws IllegalArgumentException
147     {
148         if ( StringUtils.isEmpty( comment ) )
149         {
150             throw new IllegalArgumentException( "comment should be defined" );
151         }
152 
153         XmlWriterUtil.writeLineBreak( writer );
154         XmlWriterUtil.writeCommentText( writer, comment, 2 );
155     }
156 
157     /**
158      * @param writer not null
159      * @param xmlnsPrefix could be null
160      * @param name not null
161      * @param value could be null. In this case, the element is not written.
162      * @throws IllegalArgumentException if name is null or empty
163      */
164     public static void writeElement( XMLWriter writer, String xmlnsPrefix, String name, String value )
165         throws IllegalArgumentException
166     {
167         if ( StringUtils.isEmpty( name ) )
168         {
169             throw new IllegalArgumentException( "name should be defined" );
170         }
171 
172         if ( value != null )
173         {
174             writeStartElement( writer, xmlnsPrefix, name );
175             writer.writeText( value );
176             writer.endElement();
177         }
178     }
179 
180     /**
181      * @param writer not null
182      * @param xmlnsPrefix could be null
183      * @param name not null
184      * @param lang not null
185      * @param value could be null. In this case, the element is not written.
186      * @throws IllegalArgumentException if name is null or empty
187      */
188     public static void writeElement( XMLWriter writer, String xmlnsPrefix, String name, String value, String lang )
189         throws IllegalArgumentException
190     {
191         if ( StringUtils.isEmpty( lang ) )
192         {
193             writeElement( writer, xmlnsPrefix, name, value );
194             return;
195         }
196 
197         if ( StringUtils.isEmpty( name ) )
198         {
199             throw new IllegalArgumentException( "name should be defined" );
200         }
201 
202         if ( value != null )
203         {
204             writeStartElement( writer, xmlnsPrefix, name );
205             writer.addAttribute( "xml:lang", lang );
206             writer.writeText( value );
207             writer.endElement();
208         }
209     }
210 
211     /**
212      * @param writer not null
213      * @param xmlnsPrefix could be null
214      * @param name not null
215      * @throws IllegalArgumentException if name is null or empty
216      * @since 1.1
217      */
218     public static void writeStartElement( XMLWriter writer, String xmlnsPrefix, String name )
219         throws IllegalArgumentException
220     {
221         if ( StringUtils.isEmpty( name ) )
222         {
223             throw new IllegalArgumentException( "name should be defined" );
224         }
225 
226         if ( StringUtils.isNotEmpty( xmlnsPrefix ) )
227         {
228             writer.startElement( xmlnsPrefix + ":" + name );
229         }
230         else
231         {
232             writer.startElement( name );
233         }
234     }
235 
236     /**
237      * @param writer not null
238      * @param xmlnsPrefix could be null
239      * @param name not null
240      * @param value could be null. In this case, the element is not written.
241      * @throws IllegalArgumentException if name is null or empty
242      */
243     public static void writeRdfResourceElement( XMLWriter writer, String xmlnsPrefix, String name, String value )
244         throws IllegalArgumentException
245     {
246         if ( StringUtils.isEmpty( name ) )
247         {
248             throw new IllegalArgumentException( "name should be defined" );
249         }
250 
251         if ( value != null )
252         {
253             writeStartElement( writer, xmlnsPrefix, name );
254             writer.addAttribute( RDF_RESOURCE, value );
255             writer.endElement();
256         }
257     }
258 
259     /**
260      * @param writer not null
261      * @param name not null
262      * @param value could be null. In this case, the element is not written.
263      * @throws IllegalArgumentException if name is null or empty
264      */
265     public static void writeRdfNodeIdElement( XMLWriter writer, String xmlnsPrefix, String name, String value )
266         throws IllegalArgumentException
267     {
268         if ( StringUtils.isEmpty( name ) )
269         {
270             throw new IllegalArgumentException( "name should be defined" );
271         }
272 
273         if ( value != null )
274         {
275             writeStartElement( writer, xmlnsPrefix, name );
276             writer.addAttribute( RDF_NODE_ID, value );
277             writer.endElement();
278         }
279     }
280 
281     /**
282      * @param i18n the internationalization component
283      * @param developersOrContributors list of <code>{@link Contributor}</code>
284      * @return a none null list of <code>{@link Contributor}</code> which have a <code>developer</code> DOAP role.
285      */
286     public static List<Contributor> getContributorsWithDeveloperRole( I18N i18n,
287                                                                       List<Contributor> developersOrContributors )
288     {
289         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "developers" );
290     }
291 
292     /**
293      * @param i18n the internationalization component
294      * @param developersOrContributors list of <code>{@link Contributor}</code>
295      * @return a none null list of <code>{@link Contributor}</code> which have a <code>documenter</code> DOAP role.
296      */
297     public static List<Contributor> getContributorsWithDocumenterRole( I18N i18n,
298                                                                        List<Contributor> developersOrContributors )
299     {
300         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "documenters" );
301     }
302 
303     /**
304      * @param i18n the internationalization component
305      * @param developersOrContributors list of <code>{@link Contributor}</code>
306      * @return a none null list of <code>{@link Contributor}</code> which have an <code>helper</code> DOAP role.
307      */
308     public static List<Contributor> getContributorsWithHelperRole( I18N i18n,
309                                                                    List<Contributor> developersOrContributors )
310     {
311         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "helpers" );
312     }
313 
314     /**
315      * @param i18n the internationalization component
316      * @param developersOrContributors list of <code>{@link Contributor}</code>
317      * @return a none null list of <code>{@link Contributor}</code> which have a <code>maintainer</code> DOAP role.
318      */
319     public static List<Contributor> getContributorsWithMaintainerRole( I18N i18n,
320                                                                        List<Contributor> developersOrContributors )
321     {
322         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "maintainers" );
323     }
324 
325     /**
326      * @param i18n the internationalization component
327      * @param developersOrContributors list of <code>{@link Contributor}</code>
328      * @return a none null list of <code>{@link Contributor}</code> which have a <code>tester</code> DOAP role.
329      */
330     public static List<Contributor> getContributorsWithTesterRole( I18N i18n,
331                                                                    List<Contributor> developersOrContributors )
332     {
333         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "testers" );
334     }
335 
336     /**
337      * @param i18n the internationalization component
338      * @param developersOrContributors list of <code>{@link Contributor}</code>
339      * @return a none null list of <code>{@link Contributor}</code> which have a <code>translator</code> DOAP role.
340      */
341     public static List<Contributor> getContributorsWithTranslatorRole( I18N i18n,
342                                                                        List<Contributor> developersOrContributors )
343     {
344         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "translators" );
345     }
346 
347     /**
348      * @param i18n the internationalization component
349      * @param developersOrContributors list of <code>{@link Contributor}</code>
350      * @return a none null list of <code>{@link Contributor}</code> which have an <code>unknown</code> DOAP role.
351      */
352     public static List<Contributor> getContributorsWithUnknownRole( I18N i18n,
353                                                                     List<Contributor> developersOrContributors )
354     {
355         return filterContributorsByDoapRoles( i18n, developersOrContributors ).get( "unknowns" );
356     }
357 
358     /**
359      * Utility class for keeping track of DOAP organizations in the DoaP mojo.
360      *
361      * @author <a href="mailto:t.fliss@gmail.com">Tim Fliss</a>
362      * @version $Id: DoapUtil.java 1642247 2014-11-27 23:17:01Z hboutemy $
363      * @since 1.1
364      */
365     public static class Organization
366     {
367         private String name;
368 
369         private String url;
370 
371         private List<String> members = new LinkedList<String>();
372 
373         public Organization( String name, String url )
374         {
375             this.name = name;
376             this.url = url;
377         }
378 
379         public void setName( String name )
380         {
381             this.name = name;
382         }
383 
384         public String getName()
385         {
386             return name;
387         }
388 
389         public void setUrl( String url )
390         {
391             this.url = url;
392         }
393 
394         public String getUrl()
395         {
396             return url;
397         }
398 
399         public void addMember( String nodeId )
400         {
401             members.add( nodeId );
402         }
403 
404         public List<String> getMembers()
405         {
406             return members;
407         }
408     }
409 
410     /**
411      * put an organization from the pom file in the organization list.
412      *
413      * @param name from the pom file (e.g. Yoyodyne)
414      * @param url from the pom file (e.g. http://yoyodyne.example.org/about)
415      * @return the existing organization if a duplicate, or a new one.
416      */
417     public static DoapUtil.Organization addOrganization( String name, String url )
418     {
419         Organization organization = organizations.get( name );
420 
421         if ( organization == null )
422         {
423             organization = new DoapUtil.Organization( name, url );
424         }
425 
426         organizations.put( name, organization );
427 
428         return organization;
429     }
430 
431     // unique RDF blank node index scoped internal to the DOAP file
432     private static int nodeNumber = 1;
433 
434     /**
435      * get a unique (within the DoaP file) RDF blank node ID
436      *
437      * @return the nodeID
438      * @see <a href="http://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-blank-nodes">
439      *      http://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-blank-nodes</a>
440      */
441     public static String getNodeId()
442     {
443         return "b" + nodeNumber++;
444     }
445 
446     /**
447      * get the set of Organizations that people are members of
448      *
449      * @return Map.EntrySet of DoapUtil.Organization
450      */
451     public static Set<Entry<String, DoapUtil.Organization>> getOrganizations()
452     {
453         return organizations.entrySet();
454     }
455 
456     /**
457      * Validate the given DOAP file.
458      *
459      * @param doapFile not null and should exists.
460      * @return an empty list if the DOAP file is valid, otherwise a list of errors.
461      * @since 1.1
462      */
463     public static List<String> validate( File doapFile )
464     {
465         if ( doapFile == null || !doapFile.isFile() )
466         {
467             throw new IllegalArgumentException( "The DOAP file should exist" );
468         }
469 
470         Model model = ModelFactory.createDefaultModel();
471         RDFReader r = model.getReader( "RDF/XML" );
472         r.setProperty( "error-mode", "strict-error" );
473         final List<String> errors = new ArrayList<String>();
474         r.setErrorHandler( new RDFDefaultErrorHandler()
475         {
476             @Override
477             public void error( Exception e )
478             {
479                 errors.add( e.getMessage() );
480             }
481         } );
482 
483         try
484         {
485             r.read( model, doapFile.toURI().toURL().toString() );
486         }
487         catch ( MalformedURLException e )
488         {
489             // ignored
490         }
491 
492         return errors;
493     }
494 
495     /**
496      * @param str not null
497      * @return <code>true</code> if the str parameter is a valid email, <code>false</code> otherwise.
498      * @since 1.1
499      */
500     public static boolean isValidEmail( String str )
501     {
502         if ( StringUtils.isEmpty( str ) )
503         {
504             return false;
505         }
506 
507         Matcher matcher = EMAIL_PATTERN.matcher( str );
508         return matcher.matches();
509     }
510 
511     /**
512      * Fetch an URL
513      *
514      * @param settings the user settings used to fetch the url with an active proxy, if defined.
515      * @param url the url to fetch
516      * @throws IOException if any
517      * @see #DEFAULT_TIMEOUT
518      * @since 1.1
519      */
520     public static void fetchURL( Settings settings, URL url )
521         throws IOException
522     {
523         if ( url == null )
524         {
525             throw new IllegalArgumentException( "The url is null" );
526         }
527 
528         if ( "file".equals( url.getProtocol() ) )
529         {
530             InputStream in = null;
531             try
532             {
533                 in = url.openStream();
534             }
535             finally
536             {
537                 IOUtil.close( in );
538             }
539 
540             return;
541         }
542 
543         // http, https...
544         HttpClient httpClient = new HttpClient( new MultiThreadedHttpConnectionManager() );
545         httpClient.getHttpConnectionManager().getParams().setConnectionTimeout( DEFAULT_TIMEOUT );
546         httpClient.getHttpConnectionManager().getParams().setSoTimeout( DEFAULT_TIMEOUT );
547         httpClient.getParams().setBooleanParameter( HttpClientParams.ALLOW_CIRCULAR_REDIRECTS, true );
548 
549         // Some web servers don't allow the default user-agent sent by httpClient
550         httpClient.getParams().setParameter( HttpMethodParams.USER_AGENT,
551                                              "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)" );
552 
553         if ( settings != null && settings.getActiveProxy() != null )
554         {
555             Proxy activeProxy = settings.getActiveProxy();
556 
557             ProxyInfo proxyInfo = new ProxyInfo();
558             proxyInfo.setNonProxyHosts( activeProxy.getNonProxyHosts() );
559 
560             if ( StringUtils.isNotEmpty( activeProxy.getHost() )
561                 && !ProxyUtils.validateNonProxyHosts( proxyInfo, url.getHost() ) )
562             {
563                 httpClient.getHostConfiguration().setProxy( activeProxy.getHost(), activeProxy.getPort() );
564 
565                 if ( StringUtils.isNotEmpty( activeProxy.getUsername() ) && activeProxy.getPassword() != null )
566                 {
567                     Credentials credentials =
568                         new UsernamePasswordCredentials( activeProxy.getUsername(), activeProxy.getPassword() );
569 
570                     httpClient.getState().setProxyCredentials( AuthScope.ANY, credentials );
571                 }
572             }
573         }
574 
575         GetMethod getMethod = new GetMethod( url.toString() );
576         try
577         {
578             int status;
579             try
580             {
581                 status = httpClient.executeMethod( getMethod );
582             }
583             catch ( SocketTimeoutException e )
584             {
585                 // could be a sporadic failure, one more retry before we give up
586                 status = httpClient.executeMethod( getMethod );
587             }
588 
589             if ( status != HttpStatus.SC_OK )
590             {
591                 throw new FileNotFoundException( url.toString() );
592             }
593         }
594         finally
595         {
596             getMethod.releaseConnection();
597         }
598     }
599 
600     /**
601      * Interpolate a string with project and settings.
602      *
603      * @param value could be null
604      * @param project not null
605      * @param settings could be null
606      * @return the value trimmed and interpolated or null if the interpolation doesn't work.
607      * @since 1.1
608      */
609     public static String interpolate( String value, final MavenProject project, Settings settings )
610     {
611         if ( project == null )
612         {
613             throw new IllegalArgumentException( "project is required" );
614         }
615 
616         if ( value == null )
617         {
618             return value;
619         }
620 
621         if ( !value.contains( "${" ) )
622         {
623             return value.trim();
624         }
625 
626         RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
627         try
628         {
629             interpolator.addValueSource( new EnvarBasedValueSource() );
630         }
631         catch ( IOException e )
632         {
633             // ignore
634         }
635         interpolator.addValueSource( new PropertiesBasedValueSource( System.getProperties() ) );
636         interpolator.addValueSource( new PropertiesBasedValueSource( project.getProperties() ) );
637         interpolator.addValueSource( new PrefixedObjectValueSource( "project", project ) );
638         interpolator.addValueSource( new PrefixedObjectValueSource( "pom", project ) );
639         interpolator.addValueSource( new ObjectBasedValueSource( project )
640         {
641             @Override
642             public Object getValue( String expression )
643             {
644                 try
645                 {
646                     return ReflectionValueExtractor.evaluate( expression, project, true );
647                 }
648                 catch ( Exception e )
649                 {
650                     addFeedback( "Failed to extract \'" + expression + "\' from: " + project, e );
651                 }
652 
653                 return null;
654             }
655         } );
656 
657         if ( settings != null )
658         {
659             interpolator.addValueSource( new PrefixedObjectValueSource( "settings", settings ) );
660         }
661 
662         String interpolatedValue = value;
663         try
664         {
665             interpolatedValue = interpolator.interpolate( value ).trim();
666         }
667         catch ( InterpolationException e )
668         {
669             // ignore
670         }
671 
672         if ( interpolatedValue.startsWith( "${" ) )
673         {
674             return null;
675         }
676 
677         return interpolatedValue;
678     }
679 
680     // ----------------------------------------------------------------------
681     // Private methods
682     // ----------------------------------------------------------------------
683 
684     /**
685      * Filter the developers/contributors roles by the keys from {@link I18N#getBundle()}. <br/>
686      * I18N roles supported in DOAP, i.e. <code>maintainer</code>, <code>developer</code>, <code>documenter</code>,
687      * <code>translator</code>, <code>tester</code>, <code>helper</code>. <br/>
688      * <b>Note:</b> Actually, only English keys are used.
689      *
690      * @param i18n i18n component
691      * @param developersOrContributors list of <code>{@link Contributor}</code>
692      * @return a none null map with <code>maintainers</code>, <code>developers</code>, <code>documenters</code>,
693      *         <code>translators</code>, <code>testers</code>, <code>helpers</code>, <code>unknowns</code> as keys and
694      *         list of <code>{@link Contributor}</code> as value.
695      */
696     private static Map<String, List<Contributor>> filterContributorsByDoapRoles( I18N i18n,
697                                                                            List<Contributor> developersOrContributors )
698     {
699         Map<String, List<Contributor>> returnMap = new HashMap<String, List<Contributor>>( 7 );
700         returnMap.put( "maintainers", new ArrayList<Contributor>() );
701         returnMap.put( "developers", new ArrayList<Contributor>() );
702         returnMap.put( "documenters", new ArrayList<Contributor>() );
703         returnMap.put( "translators", new ArrayList<Contributor>() );
704         returnMap.put( "testers", new ArrayList<Contributor>() );
705         returnMap.put( "helpers", new ArrayList<Contributor>() );
706         returnMap.put( "unknowns", new ArrayList<Contributor>() );
707 
708         if ( developersOrContributors == null || developersOrContributors.isEmpty() )
709         {
710             return returnMap;
711         }
712 
713         for ( Contributor contributor : developersOrContributors )
714         {
715             List<String> roles = contributor.getRoles();
716 
717             if ( roles != null && roles.size() != 0 )
718             {
719                 for ( String role : roles )
720                 {
721                     role = role.toLowerCase( Locale.ENGLISH );
722                     if ( role.contains( getLowerCaseString( i18n, "doap.maintainer" ) ) )
723                     {
724                         if ( !returnMap.get( "maintainers" ).contains( contributor ) )
725                         {
726                             returnMap.get( "maintainers" ).add( contributor );
727                         }
728                     }
729                     else if ( role.contains( getLowerCaseString( i18n, "doap.developer" ) ) )
730                     {
731                         if ( !returnMap.get( "developers" ).contains( contributor ) )
732                         {
733                             returnMap.get( "developers" ).add( contributor );
734                         }
735                     }
736                     else if ( role.contains( getLowerCaseString( i18n, "doap.documenter" ) ) )
737                     {
738                         if ( !returnMap.get( "documenters" ).contains( contributor ) )
739                         {
740                             returnMap.get( "documenters" ).add( contributor );
741                         }
742                     }
743                     else if ( role.contains( getLowerCaseString( i18n, "doap.translator" ) ) )
744                     {
745                         if ( !returnMap.get( "translators" ).contains( contributor ) )
746                         {
747                             returnMap.get( "translators" ).add( contributor );
748                         }
749                     }
750                     else if ( role.contains( getLowerCaseString( i18n, "doap.tester" ) ) )
751                     {
752                         if ( !returnMap.get( "testers" ).contains( contributor ) )
753                         {
754                             returnMap.get( "testers" ).add( contributor );
755                         }
756                     }
757                     else if ( role.contains( getLowerCaseString( i18n, "doap.helper" ) ) )
758                     {
759                         if ( !returnMap.get( "helpers" ).contains( contributor ) )
760                         {
761                             returnMap.get( "helpers" ).add( contributor );
762                         }
763                     }
764                     else if ( role.contains( getLowerCaseString( i18n, "doap.emeritus" ) ) )
765                     {
766                         // Don't add as developer nor as contributor as the person is no longer involved
767                     }
768                     else
769                     {
770                         if ( !returnMap.get( "unknowns" ).contains( contributor ) )
771                         {
772                             returnMap.get( "unknowns" ).add( contributor );
773                         }
774                     }
775                 }
776             }
777             else
778             {
779                 if ( !returnMap.get( "unknowns" ).contains( contributor ) )
780                 {
781                     returnMap.get( "unknowns" ).add( contributor );
782                 }
783             }
784         }
785 
786         return returnMap;
787     }
788 
789     /**
790      * @param i18n not null
791      * @param key not null
792      * @return lower case value for the key in the i18n bundle.
793      */
794     private static String getLowerCaseString( I18N i18n, String key )
795     {
796         return i18n.getString( "doap-person", Locale.ENGLISH, key ).toLowerCase( Locale.ENGLISH );
797     }
798 
799     /**
800      * @return the Maven artefact version.
801      */
802     private static String getPluginVersion()
803     {
804         Properties pomProperties = new Properties();
805         InputStream is = null;
806         try
807         {
808             is =
809                 DoapUtil.class.getResourceAsStream( "/META-INF/maven/org.apache.maven.plugins/"
810                     + "maven-doap-plugin/pom.properties" );
811             if ( is == null )
812             {
813                 return "<unknown>";
814             }
815 
816             pomProperties.load( is );
817 
818             return pomProperties.getProperty( "version", "<unknown>" );
819         }
820         catch ( IOException e )
821         {
822             return "<unknown>";
823         }
824         finally
825         {
826             IOUtil.close( is );
827         }
828     }
829 
830     /**
831      * Fork of {@link org.codehaus.plexus.interpolation.reflection.ReflectionValueExtractor} to care of list or arrays.
832      */
833     static class ReflectionValueExtractor
834     {
835         @SuppressWarnings( "rawtypes" )
836         private static final Class[] CLASS_ARGS = new Class[0];
837 
838         private static final Object[] OBJECT_ARGS = new Object[0];
839 
840         /**
841          * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected. This approach prevents permgen
842          * space overflows due to retention of discarded classloaders.
843          */
844         @SuppressWarnings( "rawtypes" )
845         private static final Map<Class, ClassMap> CLASS_MAPS = new WeakHashMap<Class, ClassMap>();
846 
847         private ReflectionValueExtractor()
848         {
849         }
850 
851         public static Object evaluate( String expression, Object root )
852             throws Exception
853         {
854             return evaluate( expression, root, true );
855         }
856 
857         // TODO: don't throw Exception
858         public static Object evaluate( String expression, Object root, boolean trimRootToken )
859             throws Exception
860         {
861             // if the root token refers to the supplied root object parameter, remove it.
862             if ( trimRootToken )
863             {
864                 expression = expression.substring( expression.indexOf( '.' ) + 1 );
865             }
866 
867             Object value = root;
868 
869             // ----------------------------------------------------------------------
870             // Walk the dots and retrieve the ultimate value desired from the
871             // MavenProject instance.
872             // ----------------------------------------------------------------------
873 
874             StringTokenizer parser = new StringTokenizer( expression, "." );
875 
876             while ( parser.hasMoreTokens() )
877             {
878                 String token = parser.nextToken();
879                 if ( value == null )
880                 {
881                     return null;
882                 }
883 
884                 StringTokenizer parser2 = new StringTokenizer( token, "[]" );
885                 int index = -1;
886                 if ( parser2.countTokens() > 1 )
887                 {
888                     token = parser2.nextToken();
889                     try
890                     {
891                         index = Integer.valueOf( parser2.nextToken() ).intValue();
892                     }
893                     catch ( NumberFormatException e )
894                     {
895                         // ignore
896                     }
897                 }
898 
899                 final ClassMap classMap = getClassMap( value.getClass() );
900 
901                 final String methodBase = StringUtils.capitalizeFirstLetter( token );
902 
903                 String methodName = "get" + methodBase;
904 
905                 Method method = classMap.findMethod( methodName, CLASS_ARGS );
906 
907                 if ( method == null )
908                 {
909                     // perhaps this is a boolean property??
910                     methodName = "is" + methodBase;
911 
912                     method = classMap.findMethod( methodName, CLASS_ARGS );
913                 }
914 
915                 if ( method == null )
916                 {
917                     return null;
918                 }
919 
920                 value = method.invoke( value, OBJECT_ARGS );
921                 if ( value == null )
922                 {
923                     return null;
924                 }
925                 if ( Collection.class.isAssignableFrom( value.getClass() ) )
926                 {
927                     ClassMap classMap2 = getClassMap( value.getClass() );
928 
929                     Method method2 = classMap2.findMethod( "toArray", CLASS_ARGS );
930 
931                     value = method2.invoke( value, OBJECT_ARGS );
932                 }
933                 if ( value.getClass().isArray() )
934                 {
935                     value = ( (Object[]) value )[index];
936                 }
937             }
938 
939             return value;
940         }
941 
942         private static ClassMap getClassMap( Class<? extends Object> clazz )
943         {
944             ClassMap classMap = CLASS_MAPS.get( clazz );
945 
946             if ( classMap == null )
947             {
948                 classMap = new ClassMap( clazz );
949 
950                 CLASS_MAPS.put( clazz, classMap );
951             }
952 
953             return classMap;
954         }
955     }
956 }