blob: 7a9cdfafc79d107d01ab8af0fe196e928c154ccd [file] [log] [blame]
package org.apache.maven.doxia.siterenderer;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.maven.doxia.Doxia;
import org.apache.maven.doxia.logging.PlexusLoggerWrapper;
import org.apache.maven.doxia.sink.render.RenderingContext;
import org.apache.maven.doxia.parser.ParseException;
import org.apache.maven.doxia.parser.Parser;
import org.apache.maven.doxia.parser.manager.ParserNotFoundException;
import org.apache.maven.doxia.site.decoration.DecorationModel;
import org.apache.maven.doxia.module.site.SiteModule;
import org.apache.maven.doxia.module.site.manager.SiteModuleManager;
import org.apache.maven.doxia.module.site.manager.SiteModuleNotFoundException;
import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.context.Context;
import org.codehaus.plexus.i18n.I18N;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.PathTool;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.velocity.SiteResourceLoader;
import org.codehaus.plexus.velocity.VelocityComponent;
/**
* <p>DefaultSiteRenderer class.</p>
*
* @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @version $Id$
* @since 1.0
* @plexus.component role-hint="default"
*/
public class DefaultSiteRenderer
extends AbstractLogEnabled
implements Renderer
{
// ----------------------------------------------------------------------
// Requirements
// ----------------------------------------------------------------------
/**
* @plexus.requirement
*/
private VelocityComponent velocity;
/**
* @plexus.requirement
*/
private SiteModuleManager siteModuleManager;
/**
* @plexus.requirement
*/
private Doxia doxia;
/**
* @plexus.requirement
*/
private I18N i18n;
private static final String RESOURCE_DIR = "org/apache/maven/doxia/siterenderer/resources";
private static final String DEFAULT_TEMPLATE = RESOURCE_DIR + "/default-site.vm";
private static final String SKIN_TEMPLATE_LOCATION = "META-INF/maven/site.vm";
// ----------------------------------------------------------------------
// Renderer implementation
// ----------------------------------------------------------------------
/** {@inheritDoc} */
public void render( Collection documents,
SiteRenderingContext siteRenderingContext,
File outputDirectory )
throws RendererException, IOException
{
renderModule( documents, siteRenderingContext, outputDirectory );
for ( Iterator i = siteRenderingContext.getSiteDirectories().iterator(); i.hasNext(); )
{
File siteDirectory = (File) i.next();
copyResources( siteRenderingContext, new File( siteDirectory, "resources" ), outputDirectory );
}
}
/** {@inheritDoc} */
public Map locateDocumentFiles( SiteRenderingContext siteRenderingContext )
throws IOException, RendererException
{
Map files = new LinkedHashMap();
Map moduleExcludes = siteRenderingContext.getModuleExcludes();
for ( Iterator i = siteRenderingContext.getSiteDirectories().iterator(); i.hasNext(); )
{
File siteDirectory = (File) i.next();
if ( siteDirectory.exists() )
{
for ( Iterator j = siteModuleManager.getSiteModules().iterator(); j.hasNext(); )
{
SiteModule module = (SiteModule) j.next();
File moduleBasedir = new File( siteDirectory, module.getSourceDirectory() );
if ( moduleExcludes != null && moduleExcludes.containsKey( module.getParserId() ) )
{
addModuleFiles( moduleBasedir, module, (String) moduleExcludes.get( module.getParserId() ),
files );
}
else
{
addModuleFiles( moduleBasedir, module, null, files );
}
}
}
}
for ( Iterator i = siteRenderingContext.getModules().iterator(); i.hasNext(); )
{
ModuleReference module = (ModuleReference) i.next();
try
{
if ( moduleExcludes != null && moduleExcludes.containsKey( module.getParserId() ) )
{
addModuleFiles( module.getBasedir(), siteModuleManager.getSiteModule( module.getParserId() ),
(String) moduleExcludes.get( module.getParserId() ), files );
}
else
{
addModuleFiles( module.getBasedir(), siteModuleManager.getSiteModule( module.getParserId() ), null,
files );
}
}
catch ( SiteModuleNotFoundException e )
{
throw new RendererException( "Unable to find module: " + e.getMessage(), e );
}
}
return files;
}
private void addModuleFiles( File moduleBasedir,
SiteModule module,
String excludes,
Map files )
throws IOException, RendererException
{
if ( moduleBasedir.exists() )
{
List allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", excludes, false );
String lowerCaseExtension = module.getExtension().toLowerCase( Locale.ENGLISH );
List docs = new LinkedList( allFiles );
// Take care of extension case
for ( Iterator it = docs.iterator(); it.hasNext(); )
{
String name = it.next().toString().trim();
if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( "." + lowerCaseExtension ) )
{
it.remove();
}
}
List velocityFiles = new LinkedList( allFiles );
// *.xml.vm
for ( Iterator it = velocityFiles.iterator(); it.hasNext(); )
{
String name = it.next().toString().trim();
if ( !name.toLowerCase( Locale.ENGLISH ).endsWith( lowerCaseExtension + ".vm" ) )
{
it.remove();
}
}
docs.addAll( velocityFiles );
for ( Iterator k = docs.iterator(); k.hasNext(); )
{
String doc = k.next().toString().trim();
RenderingContext context =
new RenderingContext( moduleBasedir, doc, module.getParserId(), module.getExtension() );
// TODO: DOXIA-111: we need a general filter here that knows how to alter the context
if ( doc.toLowerCase( Locale.ENGLISH ).endsWith( ".vm" ) )
{
context.setAttribute( "velocity", "true" );
}
String key = context.getOutputName();
key = StringUtils.replace( key, "\\", "/" );
if ( files.containsKey( key ) )
{
DocumentRenderer renderer = (DocumentRenderer) files.get( key );
RenderingContext originalContext = renderer.getRenderingContext();
File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );
throw new RendererException( "Files '" + module.getSourceDirectory() + File.separator + doc
+ "' clashes with existing '" + originalDoc + "'." );
}
// -----------------------------------------------------------------------
// Handle key without case differences
// -----------------------------------------------------------------------
for ( Iterator iter = files.entrySet().iterator(); iter.hasNext(); )
{
Map.Entry entry = (Map.Entry) iter.next();
if ( entry.getKey().toString().equalsIgnoreCase( key ) )
{
DocumentRenderer renderer = (DocumentRenderer) files.get( entry.getKey() );
RenderingContext originalContext = renderer.getRenderingContext();
File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );
if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
{
throw new RendererException( "Files '" + module.getSourceDirectory() + File.separator
+ doc + "' clashes with existing '" + originalDoc + "'." );
}
if ( getLogger().isWarnEnabled() )
{
getLogger().warn(
"Files '" + module.getSourceDirectory() + File.separator + doc
+ "' could clashes with existing '" + originalDoc + "'." );
}
}
}
files.put( key, new DoxiaDocumentRenderer( context ) );
}
}
}
private void renderModule( Collection docs,
SiteRenderingContext siteRenderingContext,
File outputDirectory )
throws IOException, RendererException
{
for ( Iterator i = docs.iterator(); i.hasNext(); )
{
DocumentRenderer docRenderer = (DocumentRenderer) i.next();
RenderingContext renderingContext = docRenderer.getRenderingContext();
File outputFile = new File( outputDirectory, docRenderer.getOutputName() );
File inputFile = new File( renderingContext.getBasedir(), renderingContext.getInputName() );
boolean modified = false;
if ( !outputFile.exists() || inputFile.lastModified() > outputFile.lastModified() )
{
modified = true;
}
if ( modified || docRenderer.isOverwrite() )
{
if ( !outputFile.getParentFile().exists() )
{
outputFile.getParentFile().mkdirs();
}
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "Generating " + outputFile );
}
Writer writer = null;
try
{
writer = WriterFactory.newWriter( outputFile, siteRenderingContext.getOutputEncoding() );
docRenderer.renderDocument( writer, this, siteRenderingContext );
}
finally
{
IOUtil.close( writer );
}
}
else
{
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( inputFile + " unchanged, not regenerating..." );
}
}
}
}
/** {@inheritDoc} */
public void renderDocument( Writer writer,
RenderingContext renderingContext,
SiteRenderingContext context )
throws RendererException, FileNotFoundException, UnsupportedEncodingException
{
SiteRendererSink sink = new SiteRendererSink( renderingContext );
File doc = new File( renderingContext.getBasedir(), renderingContext.getInputName() );
Reader reader = null;
try
{
Parser parser = doxia.getParser( renderingContext.getParserId() );
// TODO: DOXIA-111: the filter used here must be checked generally.
if ( renderingContext.getAttribute( "velocity" ) != null )
{
String resource = doc.getAbsolutePath();
try
{
SiteResourceLoader.setResource( resource );
Context vc = createContext( sink, context );
StringWriter sw = new StringWriter();
velocity.getEngine().mergeTemplate( resource, context.getInputEncoding(), vc, sw );
reader = new StringReader( sw.toString() );
}
catch ( Exception e )
{
if ( getLogger().isDebugEnabled() )
{
getLogger().error( "Error parsing " + resource + " as a velocity template, using as text.", e );
}
else
{
getLogger().error( "Error parsing " + resource + " as a velocity template, using as text." );
}
}
}
else
{
switch ( parser.getType() )
{
case Parser.XML_TYPE:
reader = ReaderFactory.newXmlReader( doc );
break;
case Parser.TXT_TYPE:
case Parser.UNKNOWN_TYPE:
default:
reader = ReaderFactory.newReader( doc, context.getInputEncoding() );
}
}
sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );
doxia.parse( reader, renderingContext.getParserId(), sink );
}
catch ( ParserNotFoundException e )
{
throw new RendererException( "Error getting a parser for '" + doc + "': " + e.getMessage(), e );
}
catch ( ParseException e )
{
throw new RendererException( "Error parsing '"
+ doc + "': line [" + e.getLineNumber() + "] " + e.getMessage(), e );
}
catch ( IOException e )
{
throw new RendererException( "IOException when processing '" + doc + "'", e );
}
finally
{
sink.flush();
sink.close();
IOUtil.close( reader );
}
generateDocument( writer, sink, context );
}
private Context createContext( SiteRendererSink sink,
SiteRenderingContext siteRenderingContext )
{
VelocityContext context = new VelocityContext();
// ----------------------------------------------------------------------
// Data objects
// ----------------------------------------------------------------------
RenderingContext renderingContext = sink.getRenderingContext();
context.put( "relativePath", renderingContext.getRelativePath() );
// Add infos from document
context.put( "authors", sink.getAuthors() );
String title = "";
if ( siteRenderingContext.getDecoration().getName() != null )
{
title = siteRenderingContext.getDecoration().getName();
}
else if ( siteRenderingContext.getDefaultWindowTitle() != null )
{
title = siteRenderingContext.getDefaultWindowTitle();
}
if ( title.length() > 0 )
{
title += " - ";
}
title += sink.getTitle();
context.put( "title", title );
context.put( "headContent", sink.getHead() );
context.put( "bodyContent", sink.getBody() );
context.put( "decoration", siteRenderingContext.getDecoration() );
SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
if ( StringUtils.isNotEmpty( sink.getDate() ) )
{
try
{
// we support only ISO-8601 date
context.put( "dateCreation", sdf.format( new SimpleDateFormat( "yyyy-MM-dd" ).parse( sink.getDate() ) ) );
}
catch ( Exception e )
{
// nop
}
}
context.put( "dateRevision", sdf.format( new Date() ) );
context.put( "currentDate", new Date() );
Locale locale = siteRenderingContext.getLocale();
context.put( "dateFormat", DateFormat.getDateInstance( DateFormat.DEFAULT, locale ) );
String currentFileName = renderingContext.getOutputName().replace( '\\', '/' );
context.put( "currentFileName", currentFileName );
context.put( "alignedFileName", PathTool.calculateLink( currentFileName, renderingContext.getRelativePath() ) );
context.put( "locale", locale );
// Add user properties
Map templateProperties = siteRenderingContext.getTemplateProperties();
if ( templateProperties != null )
{
for ( Iterator i = templateProperties.keySet().iterator(); i.hasNext(); )
{
String key = (String) i.next();
context.put( key, templateProperties.get( key ) );
}
}
// ----------------------------------------------------------------------
// Tools
// ----------------------------------------------------------------------
context.put( "PathTool", new PathTool() );
context.put( "FileUtils", new FileUtils() );
context.put( "StringUtils", new StringUtils() );
context.put( "i18n", i18n );
return context;
}
/** {@inheritDoc} */
public void generateDocument( Writer writer,
SiteRendererSink sink,
SiteRenderingContext siteRenderingContext )
throws RendererException
{
Context context = createContext( sink, siteRenderingContext );
writeTemplate( writer, context, siteRenderingContext );
}
private void writeTemplate( Writer writer,
Context context,
SiteRenderingContext siteContext )
throws RendererException
{
ClassLoader old = null;
if ( siteContext.getTemplateClassLoader() != null )
{
// -------------------------------------------------------------------------
// If no template classloader was set we'll just use the context classloader
// -------------------------------------------------------------------------
old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader( siteContext.getTemplateClassLoader() );
}
try
{
processTemplate( siteContext.getTemplateName(), context, writer );
}
finally
{
IOUtil.close( writer );
if ( old != null )
{
Thread.currentThread().setContextClassLoader( old );
}
}
}
/**
* @noinspection OverlyBroadCatchBlock,UnusedCatchParameter
*/
private void processTemplate( String templateName,
Context context,
Writer writer )
throws RendererException
{
Template template;
try
{
template = velocity.getEngine().getTemplate( templateName );
}
catch ( Exception e )
{
throw new RendererException( "Could not find the template '" + templateName, e );
}
try
{
template.merge( context, writer );
}
catch ( Exception e )
{
throw new RendererException( "Error while generating code.", e );
}
}
/** {@inheritDoc} */
public SiteRenderingContext createContextForSkin( File skinFile,
Map attributes,
DecorationModel decoration,
String defaultWindowTitle,
Locale locale )
throws IOException
{
SiteRenderingContext context = new SiteRenderingContext();
// TODO: plexus-archiver, if it could do the excludes
ZipFile zipFile = new ZipFile( skinFile );
try
{
if ( zipFile.getEntry( SKIN_TEMPLATE_LOCATION ) != null )
{
context.setTemplateName( SKIN_TEMPLATE_LOCATION );
context.setTemplateClassLoader( new URLClassLoader( new URL[]{skinFile.toURI().toURL()} ) );
}
else
{
context.setTemplateName( DEFAULT_TEMPLATE );
context.setTemplateClassLoader( getClass().getClassLoader() );
context.setUsingDefaultTemplate( true );
}
}
finally
{
closeZipFile( zipFile );
}
context.setTemplateProperties( attributes );
context.setLocale( locale );
context.setDecoration( decoration );
context.setDefaultWindowTitle( defaultWindowTitle );
context.setSkinJarFile( skinFile );
return context;
}
/** {@inheritDoc} */
public SiteRenderingContext createContextForTemplate( File templateFile,
File skinFile,
Map attributes,
DecorationModel decoration,
String defaultWindowTitle,
Locale locale )
throws MalformedURLException
{
SiteRenderingContext context = new SiteRenderingContext();
context.setTemplateName( templateFile.getName() );
context.setTemplateClassLoader( new URLClassLoader( new URL[]{templateFile.getParentFile().toURI().toURL()} ) );
context.setTemplateProperties( attributes );
context.setLocale( locale );
context.setDecoration( decoration );
context.setDefaultWindowTitle( defaultWindowTitle );
context.setSkinJarFile( skinFile );
return context;
}
private void closeZipFile( ZipFile zipFile )
{
// TODO: move to plexus utils
try
{
zipFile.close();
}
catch ( IOException e )
{
// ignore
}
}
/** {@inheritDoc} */
public void copyResources( SiteRenderingContext siteRenderingContext,
File resourcesDirectory,
File outputDirectory )
throws IOException
{
if ( siteRenderingContext.getSkinJarFile() != null )
{
// TODO: plexus-archiver, if it could do the excludes
ZipFile file = new ZipFile( siteRenderingContext.getSkinJarFile() );
try
{
for ( Enumeration e = file.entries(); e.hasMoreElements(); )
{
ZipEntry entry = (ZipEntry) e.nextElement();
if ( !entry.getName().startsWith( "META-INF/" ) )
{
File destFile = new File( outputDirectory, entry.getName() );
if ( !entry.isDirectory() )
{
destFile.getParentFile().mkdirs();
copyFileFromZip( file, entry, destFile );
}
else
{
destFile.mkdirs();
}
}
}
}
finally
{
file.close();
}
}
if ( siteRenderingContext.isUsingDefaultTemplate() )
{
InputStream resourceList = getClass().getClassLoader()
.getResourceAsStream( RESOURCE_DIR + "/resources.txt" );
if ( resourceList != null )
{
Reader r = null;
try
{
r = ReaderFactory.newReader( resourceList, ReaderFactory.UTF_8 );
LineNumberReader reader = new LineNumberReader( r );
String line = reader.readLine();
while ( line != null )
{
InputStream is = getClass().getClassLoader().getResourceAsStream( RESOURCE_DIR + "/" + line );
if ( is == null )
{
throw new IOException( "The resource " + line + " doesn't exist." );
}
File outputFile = new File( outputDirectory, line );
if ( !outputFile.getParentFile().exists() )
{
outputFile.getParentFile().mkdirs();
}
OutputStream os = null;
try
{
// for the images
os = new FileOutputStream( outputFile );
IOUtil.copy( is, os );
}
finally
{
IOUtil.close( os );
}
IOUtil.close( is );
line = reader.readLine();
}
}
finally
{
IOUtil.close( r );
}
}
}
// Copy extra site resources
if ( resourcesDirectory != null && resourcesDirectory.exists() )
{
copyDirectory( resourcesDirectory, outputDirectory );
}
// Check for the existence of /css/site.css
File siteCssFile = new File( outputDirectory, "/css/site.css" );
if ( !siteCssFile.exists() )
{
// Create the subdirectory css if it doesn't exist, DOXIA-151
File cssDirectory = new File( outputDirectory, "/css/" );
boolean created = cssDirectory.mkdirs();
if ( created && getLogger().isDebugEnabled() )
{
getLogger().debug(
"The directory '" + cssDirectory.getAbsolutePath() + "' did not exist. It was created." );
}
// If the file is not there - create an empty file, DOXIA-86
if ( getLogger().isDebugEnabled() )
{
getLogger().debug(
"The file '" + siteCssFile.getAbsolutePath() + "' does not exists. Creating an empty file." );
}
Writer writer = null;
try
{
writer = WriterFactory.newWriter( siteCssFile, siteRenderingContext.getOutputEncoding() );
//DOXIA-290...the file should not be 0 bytes.
writer.write( "/* You can override this file with your own styles */" );
}
finally
{
IOUtil.close( writer );
}
}
}
private void copyFileFromZip( ZipFile file,
ZipEntry entry,
File destFile )
throws IOException
{
FileOutputStream fos = new FileOutputStream( destFile );
try
{
IOUtil.copy( file.getInputStream( entry ), fos );
}
finally
{
IOUtil.close( fos );
}
}
/**
* Copy the directory
*
* @param source source file to be copied
* @param destination destination file
* @throws java.io.IOException if any
*/
protected void copyDirectory( File source,
File destination )
throws IOException
{
if ( source.exists() )
{
DirectoryScanner scanner = new DirectoryScanner();
String[] includedResources = {"**/**"};
scanner.setIncludes( includedResources );
scanner.addDefaultExcludes();
scanner.setBasedir( source );
scanner.scan();
List includedFiles = Arrays.asList( scanner.getIncludedFiles() );
for ( Iterator j = includedFiles.iterator(); j.hasNext(); )
{
String name = (String) j.next();
File sourceFile = new File( source, name );
File destinationFile = new File( destination, name );
FileUtils.copyFile( sourceFile, destinationFile );
}
}
}
}