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