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