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