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