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