View Javadoc
1   package org.apache.maven.plugins.site.render;
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 static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
23  
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.OutputStream;
27  import java.io.Writer;
28  import java.io.File;
29  import java.lang.reflect.InvocationTargetException;
30  import java.lang.reflect.Method;
31  import java.util.ArrayList;
32  import java.util.Locale;
33  import java.util.List;
34  
35  import org.apache.maven.doxia.sink.Sink;
36  import org.apache.maven.doxia.sink.SinkFactory;
37  import org.apache.maven.doxia.siterenderer.DocumentRenderer;
38  import org.apache.maven.doxia.siterenderer.Renderer;
39  import org.apache.maven.doxia.siterenderer.RendererException;
40  import org.apache.maven.doxia.siterenderer.RenderingContext;
41  import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
42  import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
43  import org.apache.maven.doxia.tools.MojoLogWrapper;
44  import org.apache.maven.plugin.logging.Log;
45  import org.apache.maven.reporting.MavenMultiPageReport;
46  import org.apache.maven.reporting.MavenReport;
47  import org.apache.maven.reporting.MavenReportException;
48  import org.apache.maven.reporting.exec.MavenReportExecution;
49  import org.codehaus.plexus.util.PathTool;
50  import org.codehaus.plexus.util.StringUtils;
51  import org.codehaus.plexus.util.WriterFactory;
52  
53  /**
54   * Renders a Maven report in a Doxia site.
55   *
56   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
57   * @see org.apache.maven.doxia.siterenderer.DoxiaDocumentRenderer
58   */
59  public class ReportDocumentRenderer
60      implements DocumentRenderer
61  {
62      private final MavenReport report;
63  
64      private final RenderingContext renderingContext;
65  
66      private final String reportMojoInfo;
67  
68      private final ClassLoader classLoader;
69  
70      private final Log log;
71  
72      public ReportDocumentRenderer( MavenReportExecution mavenReportExecution, RenderingContext renderingContext,
73                                     Log log )
74      {
75          this.report = mavenReportExecution.getMavenReport();
76  
77          this.renderingContext = renderingContext;
78  
79          if ( mavenReportExecution.getPlugin() == null )
80          {
81              // Maven 2: report has been prepared in Maven Core, MavenReportExecution contains only the report
82              this.reportMojoInfo = getPluginInfo( report );
83          }
84          else
85          {
86              // Maven 3: full MavenReportExecution prepared by maven-reporting-impl
87              this.reportMojoInfo =
88                  mavenReportExecution.getPlugin().getArtifactId() + ':' + mavenReportExecution.getPlugin().getVersion()
89                      + ':' + mavenReportExecution.getGoal();
90          }
91  
92          this.classLoader = mavenReportExecution.getClassLoader();
93  
94          this.log = log;
95      }
96  
97      /**
98       * Get plugin information from report's Manifest.
99       *
100      * @param report the Maven report
101      * @return plugin information as Specification Title followed by Specification Version if set in Manifest and
102      *         supported by JVM
103      */
104     private String getPluginInfo( MavenReport report )
105     {
106         Package pkg = report.getClass().getPackage();
107 
108         if ( pkg != null )
109         {
110             String title = pkg.getSpecificationTitle();
111             String version = pkg.getSpecificationVersion();
112 
113             if ( title == null )
114             {
115                 return version;
116             }
117             else if ( version == null )
118             {
119                 return title;
120             }
121             else
122             {
123                 return title + ' ' + version;
124             }
125         }
126 
127         return null;
128     }
129 
130     private static class MultiPageSubSink
131         extends SiteRendererSink
132     {
133         private File outputDir;
134 
135         private String outputName;
136 
137         MultiPageSubSink( File outputDir, String outputName, RenderingContext context )
138         {
139             super( context );
140             this.outputName = outputName;
141             this.outputDir = outputDir;
142         }
143 
144         public String getOutputName()
145         {
146             return outputName;
147         }
148 
149         public File getOutputDir()
150         {
151             return outputDir;
152         }
153 
154     }
155 
156     private static class MultiPageSinkFactory
157         implements SinkFactory
158     {
159         /**
160          * The report that is (maybe) generating multiple pages
161          */
162         private MavenReport report;
163 
164         /**
165          * The main RenderingContext, which is the base for the RenderingContext of subpages
166          */
167         private RenderingContext context;
168 
169         /**
170          * List of sinks (subpages) associated to this report
171          */
172         private List<MultiPageSubSink> sinks = new ArrayList<MultiPageSubSink>();
173 
174         MultiPageSinkFactory( MavenReport report, RenderingContext context )
175         {
176             this.report = report;
177             this.context = context;
178         }
179 
180         @Override
181         public Sink createSink( File outputDir, String outputName )
182         {
183             // Create a new context, similar to the main one, but with a different output name
184             String outputRelativeToTargetSite = PathTool.getRelativeFilePath(
185                 report.getReportOutputDirectory().getPath(),
186                 new File( outputDir, outputName ).getPath()
187             );
188 
189             RenderingContext subSinkContext = new RenderingContext(
190                 context.getBasedir(),
191                 context.getBasedirRelativePath(),
192                 outputRelativeToTargetSite,
193                 context.getParserId(),
194                 context.getExtension(),
195                 context.isEditable(),
196                 context.getGenerator()
197             );
198 
199             // Create a sink for this subpage, based on this new context
200             MultiPageSubSink sink = new MultiPageSubSink( outputDir, outputName, subSinkContext );
201 
202             // Add it to the list of sinks associated to this report
203             sinks.add( sink );
204 
205             return sink;
206         }
207 
208         @Override
209         public Sink createSink( File arg0, String arg1, String arg2 )
210             throws IOException
211         {
212             // Not used
213             return null;
214         }
215 
216         @Override
217         public Sink createSink( OutputStream arg0 )
218             throws IOException
219         {
220             // Not used
221             return null;
222         }
223 
224         @Override
225         public Sink createSink( OutputStream arg0, String arg1 )
226             throws IOException
227         {
228             // Not used
229             return null;
230         }
231 
232         public List<MultiPageSubSink> sinks()
233         {
234             return sinks;
235         }
236     }
237 
238     @Override
239     public void renderDocument( Writer writer, Renderer renderer, SiteRenderingContext siteRenderingContext )
240         throws RendererException, FileNotFoundException
241     {
242         Locale locale = siteRenderingContext.getLocale();
243         String localReportName = report.getName( locale );
244 
245         String msg = "Generating \"" + buffer().strong( localReportName ) + "\" report";
246         // CHECKSTYLE_OFF: MagicNumber
247         log.info( reportMojoInfo == null ? ( msg + '.' )
248                         : ( StringUtils.rightPad( msg, 40 ) + buffer().strong( " --- " ).mojo( reportMojoInfo ) ) );
249         // CHECKSTYLE_ON: MagicNumber
250 
251         // main sink
252         SiteRendererSink mainSink = new SiteRendererSink( renderingContext );
253         // sink factory, for multi-page reports that need sub-sinks
254         MultiPageSinkFactory multiPageSinkFactory = new MultiPageSinkFactory( report, renderingContext );
255 
256         ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
257         try
258         {
259             if ( classLoader != null )
260             {
261                 Thread.currentThread().setContextClassLoader( classLoader );
262             }
263 
264             if ( report instanceof MavenMultiPageReport )
265             {
266                 // extended multi-page API
267                 ( (MavenMultiPageReport) report ).generate( mainSink, multiPageSinkFactory, locale );
268             }
269             else if ( generateMultiPage( locale, multiPageSinkFactory, mainSink ) )
270             {
271                 // extended multi-page API for Maven 2.2, only accessible by reflection API
272             }
273             else
274             {
275                 // old single-page-only API
276                 report.generate( mainSink, locale );
277             }
278         }
279         catch ( MavenReportException e )
280         {
281             String report = ( reportMojoInfo == null ) ? ( '"' + localReportName + '"' ) : reportMojoInfo;
282             throw new RendererException( "Error generating " + report + " report", e );
283         }
284         catch ( RuntimeException re )
285         {
286             // MSITE-836: if report generation throws a RuntimeException, transform to RendererException
287             String report = ( reportMojoInfo == null ) ? ( '"' + localReportName + '"' ) : reportMojoInfo;
288             throw new RendererException( "Error generating " + report + " report", re );
289         }
290         catch ( LinkageError e )
291         {
292             String report = ( reportMojoInfo == null ) ? ( '"' + localReportName + '"' ) : reportMojoInfo;
293             log.warn( "An issue has occurred with " + report + " report, skipping LinkageError "
294                           + e.getMessage() + ", please report an issue to Maven dev team.", e );
295         }
296         finally
297         {
298             if ( classLoader != null )
299             {
300                 Thread.currentThread().setContextClassLoader( originalClassLoader );
301             }
302             mainSink.close();
303         }
304 
305         if ( report.isExternalReport() )
306         {
307             // external reports are rendered from their own: no Doxia site rendering needed
308             return;
309         }
310 
311         // render main sink document content
312         renderer.mergeDocumentIntoSite( writer, mainSink, siteRenderingContext );
313 
314         // render sub-sinks, eventually created by multi-page reports
315         String outputName = "";
316         try
317         {
318             List<MultiPageSubSink> sinks = multiPageSinkFactory.sinks();
319 
320             log.debug( "Multipage report: " + sinks.size() + " subreports" );
321 
322             for ( MultiPageSubSink mySink : sinks )
323             {
324                 mySink.enableLogging( new MojoLogWrapper( log ) );
325 
326                 outputName = mySink.getOutputName();
327                 log.debug( "  Rendering " + outputName );
328 
329                 // Create directories if necessary
330                 if ( !mySink.getOutputDir().exists() )
331                 {
332                     mySink.getOutputDir().mkdirs();
333                 }
334 
335                 File outputFile = new File( mySink.getOutputDir(), outputName );
336 
337                 try ( Writer out = WriterFactory.newWriter( outputFile, siteRenderingContext.getOutputEncoding() ) )
338                 {
339                     renderer.mergeDocumentIntoSite( out, mySink, siteRenderingContext );
340                     mySink.close();
341                     mySink = null;
342                 }
343                 finally
344                 {
345                     if ( mySink != null )
346                     {
347                         mySink.close();
348                     }
349                 }
350             }
351         }
352         catch ( IOException e )
353         {
354             throw new RendererException( "Cannot create writer to " + outputName, e );
355         }
356     }
357 
358     /**
359      * Try to generate report with extended multi-page API.
360      *
361      * @return <code>true</code> if the report was compatible with the extended API
362      */
363     private boolean generateMultiPage( Locale locale, SinkFactory sf, Sink sink )
364         throws MavenReportException
365     {
366         try
367         {
368             // MavenMultiPageReport is not in Maven Core, then the class is different in site plugin and in each report
369             // plugin: only reflection can let us invoke its method
370             Method generate =
371                 report.getClass().getMethod( "generate", Sink.class, SinkFactory.class, Locale.class );
372 
373             generate.invoke( report, sink, sf, locale );
374 
375             return true;
376         }
377         catch ( SecurityException se )
378         {
379             return false;
380         }
381         catch ( NoSuchMethodException nsme )
382         {
383             return false;
384         }
385         catch ( IllegalArgumentException | IllegalAccessException | InvocationTargetException ite )
386         {
387             throw new MavenReportException( "error while invoking generate on " + report.getClass(), ite );
388         }
389     }
390 
391     @Override
392     public String getOutputName()
393     {
394         return renderingContext.getOutputName();
395     }
396 
397     @Override
398     public RenderingContext getRenderingContext()
399     {
400         return renderingContext;
401     }
402 
403     @Override
404     public boolean isOverwrite()
405     {
406         // TODO: would be nice to query the report to see if it is modified
407         return true;
408     }
409 
410     /**
411      * @return true if the current report is external, false otherwise
412      */
413     @Override
414     public boolean isExternalReport()
415     {
416         return report.isExternalReport();
417     }
418 }