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