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.report.projectinfo;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.net.MalformedURLException;
26  import java.net.URL;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.validator.routines.UrlValidator;
33  import org.apache.maven.doxia.sink.Sink;
34  import org.apache.maven.doxia.util.DoxiaUtils;
35  import org.apache.maven.model.License;
36  import org.apache.maven.plugins.annotations.Mojo;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.project.ProjectBuilder;
40  import org.apache.maven.reporting.MavenReportException;
41  import org.apache.maven.repository.RepositorySystem;
42  import org.apache.maven.settings.Settings;
43  import org.codehaus.plexus.i18n.I18N;
44  import org.codehaus.plexus.util.StringUtils;
45  
46  /**
47   * Generates the Project Licenses report.
48   *
49   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
50   * @since 2.0
51   */
52  @Mojo(name = "licenses")
53  public class LicensesReport extends AbstractProjectInfoReport {
54      // ----------------------------------------------------------------------
55      // Mojo parameters
56      // ----------------------------------------------------------------------
57  
58      /**
59       * Whether the system is currently offline.
60       */
61      @Parameter(property = "settings.offline")
62      private boolean offline;
63  
64      /**
65       * Whether the only render links to the license documents instead of inlining them.
66       * <br/>
67       * If the system is in {@link #offline} mode, the linkOnly parameter will be always <code>true</code>.
68       *
69       * @since 2.3
70       */
71      @Parameter(defaultValue = "false")
72      private boolean linkOnly;
73  
74      /**
75       * Specifies the input encoding of the project's license file(s).
76       *
77       * @since 2.8
78       */
79      @Parameter
80      private String licenseFileEncoding;
81  
82      @Inject
83      public LicensesReport(RepositorySystem repositorySystem, I18N i18n, ProjectBuilder projectBuilder) {
84          super(repositorySystem, i18n, projectBuilder);
85      }
86  
87      // ----------------------------------------------------------------------
88      // Public methods
89      // ----------------------------------------------------------------------
90  
91      @Override
92      public boolean canGenerateReport() throws MavenReportException {
93          boolean result = super.canGenerateReport();
94          if (result && skipEmptyReport) {
95              result = !isEmpty(getProject().getModel().getLicenses());
96          }
97  
98          if (!result) {
99              return false;
100         }
101 
102         if (!offline) {
103             return true;
104         }
105 
106         for (License license : project.getModel().getLicenses()) {
107             String url = license.getUrl();
108 
109             URL licenseUrl = null;
110             try {
111                 licenseUrl = getLicenseURL(project, url);
112             } catch (IOException e) {
113                 getLog().error(e.getMessage());
114             }
115 
116             if (licenseUrl != null && licenseUrl.getProtocol().equals("file")) {
117                 return true;
118             }
119 
120             if (licenseUrl != null
121                     && (licenseUrl.getProtocol().equals("http")
122                             || licenseUrl.getProtocol().equals("https"))) {
123                 linkOnly = true;
124                 return true;
125             }
126         }
127 
128         return false;
129     }
130 
131     @Override
132     public void executeReport(Locale locale) {
133         LicensesRenderer r = new LicensesRenderer(
134                 getSink(), getProject(), getI18N(locale), locale, settings, linkOnly, licenseFileEncoding);
135 
136         r.render();
137     }
138 
139     /**
140      * {@inheritDoc}
141      */
142     public String getOutputName() {
143         return "licenses";
144     }
145 
146     @Override
147     protected String getI18Nsection() {
148         return "licenses";
149     }
150 
151     /**
152      * @param project not null
153      * @param url     not null
154      * @return a valid URL object from the url string
155      * @throws IOException if any
156      */
157     protected static URL getLicenseURL(MavenProject project, String url) throws IOException {
158         URL licenseUrl;
159         UrlValidator urlValidator = new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES);
160         // UrlValidator does not accept file URLs because the file
161         // URLs do not contain a valid authority (no hostname).
162         // As a workaround accept license URLs that start with the
163         // file scheme.
164         if (urlValidator.isValid(url) || StringUtils.defaultString(url).startsWith("file://")) {
165             try {
166                 licenseUrl = new URL(url);
167             } catch (MalformedURLException e) {
168                 throw new MalformedURLException("The license url '" + url + "' seems to be invalid: " + e.getMessage());
169             }
170         } else {
171             File licenseFile = new File(project.getBasedir(), url);
172             if (!licenseFile.exists()) {
173                 // Workaround to allow absolute path names while
174                 // staying compatible with the way it was...
175                 licenseFile = new File(url);
176             }
177             if (!licenseFile.exists()) {
178                 throw new IOException("Maven can't find the file '" + licenseFile + "' on the system.");
179             }
180             try {
181                 licenseUrl = licenseFile.toURI().toURL();
182             } catch (MalformedURLException e) {
183                 throw new MalformedURLException("The license url '" + url + "' seems to be invalid: " + e.getMessage());
184             }
185         }
186 
187         return licenseUrl;
188     }
189 
190     // ----------------------------------------------------------------------
191     // Private
192     // ----------------------------------------------------------------------
193 
194     /**
195      * Internal renderer class
196      */
197     private static class LicensesRenderer extends AbstractProjectInfoRenderer {
198         private final MavenProject project;
199 
200         private final Settings settings;
201 
202         private final boolean linkOnly;
203 
204         private final String licenseFileEncoding;
205 
206         LicensesRenderer(
207                 Sink sink,
208                 MavenProject project,
209                 I18N i18n,
210                 Locale locale,
211                 Settings settings,
212                 boolean linkOnly,
213                 String licenseFileEncoding) {
214             super(sink, i18n, locale);
215 
216             this.project = project;
217 
218             this.settings = settings;
219 
220             this.linkOnly = linkOnly;
221 
222             this.licenseFileEncoding = licenseFileEncoding;
223         }
224 
225         @Override
226         protected String getI18Nsection() {
227             return "licenses";
228         }
229 
230         @Override
231         protected void renderBody() {
232             List<License> licenses = project.getModel().getLicenses();
233 
234             if (licenses.isEmpty()) {
235                 startSection(getTitle());
236 
237                 paragraph(getI18nString("nolicense"));
238 
239                 endSection();
240 
241                 return;
242             }
243 
244             // Overview
245             startSection(getI18nString("overview.title"));
246 
247             paragraph(getI18nString("overview.intro"));
248 
249             endSection();
250 
251             // License
252             startSection(getI18nString("title"));
253 
254             if (licenses.size() > 1) {
255                 // multiple licenses
256                 paragraph(getI18nString("multiple"));
257 
258                 if (!linkOnly) {
259                     // add an index before licenses content
260                     sink.list();
261                     for (License license : licenses) {
262                         String name = license.getName();
263                         if (name == null || name.isEmpty()) {
264                             name = getI18nString("unnamed");
265                         }
266 
267                         sink.listItem();
268                         link("#" + DoxiaUtils.encodeId(name), name);
269                         sink.listItem_();
270                     }
271                     sink.list_();
272                 }
273             }
274 
275             for (License license : licenses) {
276                 String name = license.getName();
277                 if (name == null || name.isEmpty()) {
278                     name = getI18nString("unnamed");
279                 }
280 
281                 String url = license.getUrl();
282                 String comments = license.getComments();
283 
284                 startSection(name);
285 
286                 if (!(comments == null || comments.isEmpty())) {
287                     paragraph(comments);
288                 }
289 
290                 if (url != null) {
291                     try {
292                         URL licenseUrl = getLicenseURL(project, url);
293 
294                         if (linkOnly) {
295                             link(licenseUrl.toExternalForm(), licenseUrl.toExternalForm());
296                         } else {
297                             renderLicenseContent(licenseUrl);
298                         }
299                     } catch (IOException e) {
300                         // I18N message
301                         paragraph(e.getMessage());
302                     }
303                 }
304 
305                 endSection();
306             }
307 
308             endSection();
309         }
310 
311         /**
312          * Render the license content into the report.
313          *
314          * @param licenseUrl the license URL
315          */
316         private void renderLicenseContent(URL licenseUrl) {
317             try {
318                 // All licenses are supposed to be in English...
319                 String licenseContent = ProjectInfoReportUtils.getContent(licenseUrl, settings, licenseFileEncoding);
320 
321                 // TODO: we should check for a text/html mime type instead, and possibly use a html parser to do this a
322                 // bit more cleanly/reliably.
323                 String licenseContentLC = licenseContent.toLowerCase(Locale.ENGLISH);
324                 int bodyStart = licenseContentLC.indexOf("<body");
325                 int bodyEnd = licenseContentLC.indexOf("</body>");
326 
327                 if ((licenseContentLC.contains("<!doctype html") || licenseContentLC.contains("<html>"))
328                         && ((bodyStart >= 0) && (bodyEnd > bodyStart))) {
329                     bodyStart = licenseContentLC.indexOf('>', bodyStart) + 1;
330                     String body = licenseContent.substring(bodyStart, bodyEnd);
331 
332                     link(licenseUrl.toExternalForm(), getI18nString("originalText"));
333                     paragraph(getI18nString("copy"));
334 
335                     body = replaceRelativeLinks(body, baseURL(licenseUrl).toExternalForm());
336                     sink.rawText(body);
337                 } else {
338                     verbatimText(licenseContent);
339                 }
340             } catch (IOException e) {
341                 paragraph("Can't read the url [" + licenseUrl + "] : " + e.getMessage());
342             }
343         }
344 
345         private static URL baseURL(URL aUrl) {
346             String urlTxt = aUrl.toExternalForm();
347             int lastSlash = urlTxt.lastIndexOf('/');
348             if (lastSlash > -1) {
349                 try {
350                     return new URL(urlTxt.substring(0, lastSlash + 1));
351                 } catch (MalformedURLException e) {
352                     throw new AssertionError(e);
353                 }
354             }
355 
356             return aUrl;
357         }
358 
359         private static String replaceRelativeLinks(String html, String baseURL) {
360             String url = baseURL;
361             if (!url.endsWith("/")) {
362                 url += "/";
363             }
364 
365             String serverURL = url.substring(0, url.indexOf('/', url.indexOf("//") + 2));
366 
367             String content = replaceParts(html, url, serverURL, "[aA]", "[hH][rR][eE][fF]");
368             content = replaceParts(content, url, serverURL, "[iI][mM][gG]", "[sS][rR][cC]");
369             return content;
370         }
371 
372         private static String replaceParts(
373                 String html, String baseURL, String serverURL, String tagPattern, String attributePattern) {
374             Pattern anchor = Pattern.compile(
375                     "(<\\s*" + tagPattern + "\\s+[^>]*" + attributePattern + "\\s*=\\s*\")([^\"]*)\"([^>]*>)");
376             StringBuilder sb = new StringBuilder(html);
377 
378             int indx = 0;
379             boolean done = false;
380             while (!done) {
381                 Matcher mAnchor = anchor.matcher(sb);
382                 if (mAnchor.find(indx)) {
383                     indx = mAnchor.end(3);
384 
385                     if (mAnchor.group(2).startsWith("#")) {
386                         // relative link - don't want to alter this one!
387                     }
388                     if (mAnchor.group(2).startsWith("/")) {
389                         // root link
390                         sb.insert(mAnchor.start(2), serverURL);
391                         indx += serverURL.length();
392                     } else if (mAnchor.group(2).indexOf(':') < 0) {
393                         // relative link
394                         sb.insert(mAnchor.start(2), baseURL);
395                         indx += baseURL.length();
396                     }
397                 } else {
398                     done = true;
399                 }
400             }
401             return sb.toString();
402         }
403     }
404 }