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.BufferedReader;
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.Collections;
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.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.Restriction;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.doxia.Doxia;
import org.apache.maven.doxia.logging.PlexusLoggerWrapper;
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.site.decoration.PublishDate;
import org.apache.maven.doxia.site.skin.SkinModel;
import org.apache.maven.doxia.site.skin.io.xpp3.SkinXpp3Reader;
import org.apache.maven.doxia.parser.module.ParserModule;
import org.apache.maven.doxia.parser.module.ParserModuleManager;
import org.apache.maven.doxia.parser.module.ParserModuleNotFoundException;
import org.apache.maven.doxia.siterenderer.sink.SiteRendererSink;
import org.apache.maven.doxia.util.XmlValidator;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.ToolManager;
import org.apache.velocity.tools.config.ConfigurationUtils;
import org.apache.velocity.tools.config.EasyFactoryConfiguration;
import org.apache.velocity.tools.config.FactoryConfiguration;
import org.apache.velocity.tools.generic.AlternatorTool;
import org.apache.velocity.tools.generic.ClassTool;
import org.apache.velocity.tools.generic.ComparisonDateTool;
import org.apache.velocity.tools.generic.ContextTool;
import org.apache.velocity.tools.generic.ConversionTool;
import org.apache.velocity.tools.generic.DisplayTool;
import org.apache.velocity.tools.generic.EscapeTool;
import org.apache.velocity.tools.generic.FieldTool;
import org.apache.velocity.tools.generic.LinkTool;
import org.apache.velocity.tools.generic.LoopTool;
import org.apache.velocity.tools.generic.MathTool;
import org.apache.velocity.tools.generic.NumberTool;
import org.apache.velocity.tools.generic.RenderTool;
import org.apache.velocity.tools.generic.ResourceTool;
import org.apache.velocity.tools.generic.SortTool;
import org.apache.velocity.tools.generic.XmlTool;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
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.PropertyUtils;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
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: DefaultSiteRenderer.java 1800812 2017-07-04 19:51:00Z michaelo $
 * @since 1.0
 */
@Component( role = Renderer.class )
public class DefaultSiteRenderer
    extends AbstractLogEnabled
    implements Renderer
{
    // ----------------------------------------------------------------------
    // Requirements
    // ----------------------------------------------------------------------

    @Requirement
    private VelocityComponent velocity;

    @Requirement
    private ParserModuleManager parserModuleManager;

    @Requirement
    private Doxia doxia;

    @Requirement
    private I18N i18n;

    @Requirement
    private PlexusContainer plexus;

    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";

    private static final String TOOLS_LOCATION = "META-INF/maven/site-tools.xml";

    // ----------------------------------------------------------------------
    // Renderer implementation
    // ----------------------------------------------------------------------

    /** {@inheritDoc} */
    public Map<String, DocumentRenderer> locateDocumentFiles( SiteRenderingContext siteRenderingContext )
            throws IOException, RendererException
    {
        Map<String, DocumentRenderer> files = new LinkedHashMap<String, DocumentRenderer>();
        Map<String, String> moduleExcludes = siteRenderingContext.getModuleExcludes();

        // look in every site directory (in general src/site or target/generated-site)
        for ( File siteDirectory : siteRenderingContext.getSiteDirectories() )
        {
            if ( siteDirectory.exists() )
            {
                Collection<ParserModule> modules = parserModuleManager.getParserModules();
                // use every Doxia parser module
                for ( ParserModule module : modules )
                {
                    File moduleBasedir = new File( siteDirectory, module.getSourceDirectory() );

                    String excludes = ( moduleExcludes == null ) ? null : moduleExcludes.get( module.getParserId() );

                    addModuleFiles( moduleBasedir, module, excludes, files );
                }
            }
        }

        // look in specific modules directories (used for old Maven 1.x site layout: xdoc and fml docs in /xdocs)
        for ( ExtraDoxiaModuleReference module : siteRenderingContext.getModules() )
        {
            try
            {
                ParserModule parserModule = parserModuleManager.getParserModule( module.getParserId() );

                String excludes = ( moduleExcludes == null ) ? null : moduleExcludes.get( module.getParserId() );

                addModuleFiles( module.getBasedir(), parserModule, excludes, files );
            }
            catch ( ParserModuleNotFoundException e )
            {
                throw new RendererException( "Unable to find module: " + e.getMessage(), e );
            }
        }
        return files;
    }

    private List<String> filterExtensionIgnoreCase( List<String> fileNames, String extension )
    {
        List<String> filtered = new LinkedList<String>( fileNames );
        for ( Iterator<String> it = filtered.iterator(); it.hasNext(); )
        {
            String name = it.next();

            // Take care of extension case
            if ( !endsWithIgnoreCase( name, extension ) )
            {
                it.remove();
            }
        }
        return filtered;
    }

    private void addModuleFiles( File moduleBasedir, ParserModule module, String excludes,
                                 Map<String, DocumentRenderer> files )
            throws IOException, RendererException
    {
        if ( !moduleBasedir.exists() || ArrayUtils.isEmpty( module.getExtensions() ) )
        {
            return;
        }

        List<String> allFiles = FileUtils.getFileNames( moduleBasedir, "**/*.*", excludes, false );

        for ( String extension : module.getExtensions() )
        {
            String fullExtension = "." + extension;

            List<String> docs = filterExtensionIgnoreCase( allFiles, fullExtension );

            // *.<extension>.vm
            List<String> velocityFiles = filterExtensionIgnoreCase( allFiles, fullExtension + ".vm" );

            docs.addAll( velocityFiles );

            for ( String doc : docs )
            {
                RenderingContext context =
                        new RenderingContext( moduleBasedir, doc, module.getParserId(), extension );

                // TODO: DOXIA-111: we need a general filter here that knows how to alter the context
                if ( endsWithIgnoreCase( doc, ".vm" ) )
                {
                    context.setAttribute( "velocity", "true" );
                }

                String key = context.getOutputName();
                key = StringUtils.replace( key, "\\", "/" );

                if ( files.containsKey( key ) )
                {
                    DocumentRenderer renderer = files.get( key );

                    RenderingContext originalContext = renderer.getRenderingContext();

                    File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );

                    throw new RendererException( "File '" + module.getSourceDirectory() + File.separator + doc
                        + "' clashes with existing '" + originalDoc + "'." );
                }
                // -----------------------------------------------------------------------
                // Handle key without case differences
                // -----------------------------------------------------------------------
                for ( Map.Entry<String, DocumentRenderer> entry : files.entrySet() )
                {
                    if ( entry.getKey().equalsIgnoreCase( key ) )
                    {
                        RenderingContext originalContext = entry.getValue().getRenderingContext();

                        File originalDoc = new File( originalContext.getBasedir(), originalContext.getInputName() );

                        if ( Os.isFamily( Os.FAMILY_WINDOWS ) )
                        {
                            throw new RendererException( "File '" + module.getSourceDirectory() + File.separator
                                + doc + "' clashes with existing '" + originalDoc + "'." );
                        }

                        if ( getLogger().isWarnEnabled() )
                        {
                            getLogger().warn( "File '" + module.getSourceDirectory() + File.separator + doc
                                + "' could clash with existing '" + originalDoc + "'." );
                        }
                    }
                }

                files.put( key, new DoxiaDocumentRenderer( context ) );
            }
        }
    }

    /** {@inheritDoc} */
    public void render( Collection<DocumentRenderer> documents, SiteRenderingContext siteRenderingContext,
                        File outputDirectory )
        throws RendererException, IOException
    {
        for ( DocumentRenderer docRenderer : documents )
        {
            RenderingContext renderingContext = docRenderer.getRenderingContext();

            File outputFile = new File( outputDirectory, docRenderer.getOutputName() );

            File inputFile = new File( renderingContext.getBasedir(), renderingContext.getInputName() );

            boolean modified = !outputFile.exists() || ( inputFile.lastModified() > outputFile.lastModified() )
                || ( siteRenderingContext.getDecoration().getLastModified() > outputFile.lastModified() );

            if ( modified || docRenderer.isOverwrite() )
            {
                if ( !outputFile.getParentFile().exists() )
                {
                    outputFile.getParentFile().mkdirs();
                }

                if ( getLogger().isDebugEnabled() )
                {
                    getLogger().debug( "Generating " + outputFile );
                }

                Writer writer = null;
                try
                {
                    if ( !docRenderer.isExternalReport() )
                    {
                        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 siteContext )
            throws RendererException, FileNotFoundException, UnsupportedEncodingException
    {
        SiteRendererSink sink = new SiteRendererSink( renderingContext );

        File doc = new File( renderingContext.getBasedir(), renderingContext.getInputName() );

        Reader reader = null;
        try
        {
            String resource = doc.getAbsolutePath();

            Parser parser = doxia.getParser( renderingContext.getParserId() );
            // DOXIASITETOOLS-146 don't render comments from source markup
            parser.setEmitComments( false );

            // TODO: DOXIA-111: the filter used here must be checked generally.
            if ( renderingContext.getAttribute( "velocity" ) != null )
            {
                getLogger().debug( "Processing Velocity for " + renderingContext.getInputName() );
                try
                {
                    Context vc = createDocumentVelocityContext( renderingContext, siteContext );

                    StringWriter sw = new StringWriter();

                    velocity.getEngine().mergeTemplate( resource, siteContext.getInputEncoding(), vc, sw );

                    String doxiaContent = sw.toString();

                    if ( siteContext.getProcessedContentOutput() != null )
                    {
                        // save Velocity processing result, ie the Doxia content that will be parsed after
                        if ( !siteContext.getProcessedContentOutput().exists() )
                        {
                            siteContext.getProcessedContentOutput().mkdirs();
                        }

                        String input = renderingContext.getInputName();
                        File outputFile = new File( siteContext.getProcessedContentOutput(),
                                                    input.substring( 0, input.length() - 3 ) );

                        File outputParent = outputFile.getParentFile();
                        if ( !outputParent.exists() )
                        {
                            outputParent.mkdirs();
                        }

                        FileUtils.fileWrite( outputFile, siteContext.getInputEncoding(), doxiaContent );
                    }

                    reader = new StringReader( doxiaContent );
                }
                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." );
                    }
                }

                if ( parser.getType() == Parser.XML_TYPE && siteContext.isValidate() )
                {
                    reader = validate( reader, resource );
                }
            }
            else
            {
                switch ( parser.getType() )
                {
                    case Parser.XML_TYPE:
                        reader = ReaderFactory.newXmlReader( doc );
                        if ( siteContext.isValidate() )
                        {
                            reader = validate( reader, resource );
                        }
                        break;

                    case Parser.TXT_TYPE:
                    case Parser.UNKNOWN_TYPE:
                    default:
                        reader = ReaderFactory.newReader( doc, siteContext.getInputEncoding() );
                }
            }
            sink.enableLogging( new PlexusLoggerWrapper( getLogger() ) );

            if ( reader == null ) // can happen if velocity throws above
            {
                throw new RendererException( "Error getting a parser for '" + doc + "'" );
            }
            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, siteContext );
    }

    /**
     * Creates a Velocity Context with all generic tools configured wit the site rendering context.
     *
     * @param siteRenderingContext the site rendering context
     * @return a Velocity tools managed context
     */
    protected Context createToolManagedVelocityContext( SiteRenderingContext siteRenderingContext )
    {
        Locale locale = siteRenderingContext.getLocale();
        String dateFormat = siteRenderingContext.getDecoration().getPublishDate().getFormat();

        EasyFactoryConfiguration config = new EasyFactoryConfiguration( false );
        config.property( "safeMode", Boolean.FALSE );
        config.toolbox( Scope.REQUEST )
            .tool( ContextTool.class )
            .tool( LinkTool.class )
            .tool( LoopTool.class )
            .tool( RenderTool.class );
        config.toolbox( Scope.APPLICATION ).property( "locale", locale )
            .tool( AlternatorTool.class )
            .tool( ClassTool.class )
            .tool( ComparisonDateTool.class ).property( "format", dateFormat )
            .tool( ConversionTool.class ).property( "dateFormat", dateFormat )
            .tool( DisplayTool.class )
            .tool( EscapeTool.class )
            .tool( FieldTool.class )
            .tool( MathTool.class )
            .tool( NumberTool.class )
            .tool( ResourceTool.class ).property( "bundles", new String[] { "site-renderer" } )
            .tool( SortTool.class )
            .tool( XmlTool.class );

        FactoryConfiguration customConfig = ConfigurationUtils.findInClasspath( TOOLS_LOCATION );

        if ( customConfig != null )
        {
            config.addConfiguration( customConfig );
        }

        ToolManager manager = new ToolManager( false, false );
        manager.configure( config );

        return manager.createContext();
    }

    /**
     * Create a Velocity Context for a Doxia document, containing every information about rendered document.
     *
     * @param sink the site renderer sink for the document
     * @param siteRenderingContext the site rendering context
     * @return
     */
    protected Context createDocumentVelocityContext( RenderingContext renderingContext,
                                                     SiteRenderingContext siteRenderingContext )
    {
        Context context = createToolManagedVelocityContext( siteRenderingContext );
        // ----------------------------------------------------------------------
        // Data objects
        // ----------------------------------------------------------------------

        context.put( "relativePath", renderingContext.getRelativePath() );

        String currentFileName = renderingContext.getOutputName().replace( '\\', '/' );
        context.put( "currentFileName", currentFileName );

        context.put( "alignedFileName", PathTool.calculateLink( currentFileName, renderingContext.getRelativePath() ) );

        context.put( "decoration", siteRenderingContext.getDecoration() );

        Locale locale = siteRenderingContext.getLocale();
        context.put( "locale", locale );
        context.put( "supportedLocales", Collections.unmodifiableList( siteRenderingContext.getSiteLocales() ) );

        context.put( "currentDate", new Date() );
        SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
        context.put( "dateRevision", sdf.format( new Date() ) );

        context.put( "publishDate", siteRenderingContext.getPublishDate() );

        PublishDate publishDate = siteRenderingContext.getDecoration().getPublishDate();
        DateFormat dateFormat = new SimpleDateFormat( publishDate.getFormat(), locale );
        context.put( "dateFormat", dateFormat );

        // doxiaSiteRendererVersion
        InputStream inputStream = this.getClass().getResourceAsStream( "/META-INF/"
            + "maven/org.apache.maven.doxia/doxia-site-renderer/pom.properties" );
        Properties properties = PropertyUtils.loadProperties( inputStream );
        if ( inputStream == null )
        {
            getLogger().debug( "pom.properties for doxia-site-renderer could not be found." );
        }
        else if ( properties == null )
        {
            getLogger().debug( "Failed to load pom.properties, so doxiaVersion is not available"
                + " in the Velocity context." );
        }
        else
        {
            context.put( "doxiaSiteRendererVersion", properties.getProperty( "version" ) );
        }

        // Add user properties
        Map<String, ?> templateProperties = siteRenderingContext.getTemplateProperties();

        if ( templateProperties != null )
        {
            for ( Map.Entry<String, ?> entry : templateProperties.entrySet() )
            {
                context.put( entry.getKey(), entry.getValue() );
            }
        }

        // ----------------------------------------------------------------------
        // Tools
        // ----------------------------------------------------------------------

        context.put( "PathTool", new PathTool() );

        context.put( "FileUtils", new FileUtils() );

        context.put( "StringUtils", new StringUtils() );

        context.put( "i18n", i18n );

        context.put( "plexus", plexus );
        return context;
    }

    /**
     * Create a Velocity Context for the site template decorating the document. In addition to all the informations
     * from the document, this context contains data gathered in {@link SiteRendererSink} during document rendering.
     *
     * @param siteRendererSink the site renderer sink for the document
     * @param siteRenderingContext the site rendering context
     * @return
     */
    protected Context createSiteTemplateVelocityContext( SiteRendererSink siteRendererSink,
                                                         SiteRenderingContext siteRenderingContext )
    {
        // first get the context from document
        Context context = createDocumentVelocityContext( siteRendererSink.getRenderingContext(), siteRenderingContext );

        // then add data objects from rendered document

        // Add infos from document
        context.put( "authors", siteRendererSink.getAuthors() );

        context.put( "shortTitle", siteRendererSink.getTitle() );

        // DOXIASITETOOLS-70: Prepend the project name to the title, if any
        String title = "";
        if ( siteRenderingContext.getDecoration() != null
                && siteRenderingContext.getDecoration().getName() != null )
        {
            title = siteRenderingContext.getDecoration().getName();
        }
        else if ( siteRenderingContext.getDefaultWindowTitle() != null )
        {
            title = siteRenderingContext.getDefaultWindowTitle();
        }

        if ( title.length() > 0 )
        {
            title += " &#x2013; "; // Symbol Name: En Dash, Html Entity: &ndash;
        }
        title += siteRendererSink.getTitle();

        context.put( "title", title );

        context.put( "headContent", siteRendererSink.getHead() );

        context.put( "bodyContent", siteRendererSink.getBody() );

        // document date (got from Doxia Sink date() API)
        String documentDate = siteRendererSink.getDate();
        if ( StringUtils.isNotEmpty( documentDate ) )
        {
            context.put( "documentDate", documentDate );

            // deprecated variables that rework the document date, suppose one semantics over others
            // (ie creation date, while it may be last modification date if the document writer decided so)
            // see DOXIASITETOOLS-20 for the beginning and DOXIASITETOOLS-164 for the end of this story
            try
            {
                // we support only ISO 8601 date
                Date creationDate = new SimpleDateFormat( "yyyy-MM-dd" ).parse( documentDate );

                context.put( "creationDate", creationDate );
                SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
                context.put( "dateCreation", sdf.format( creationDate ) );
            }
            catch ( java.text.ParseException e )
            {
                getLogger().warn( "Could not parse date '" + documentDate + "' from "
                    + siteRendererSink.getRenderingContext().getInputName()
                    + " (expected yyyy-MM-dd format), ignoring!" );
            }
        }

        return context;
    }

    /** {@inheritDoc} */
    public void generateDocument( Writer writer, SiteRendererSink sink, SiteRenderingContext siteRenderingContext )
            throws RendererException
    {
        String templateName = siteRenderingContext.getTemplateName();

        getLogger().debug( "Processing Velocity for template " + templateName + " on "
            + sink.getRenderingContext().getInputName() );

        Context context = createSiteTemplateVelocityContext( sink, siteRenderingContext );

        ClassLoader old = null;

        if ( siteRenderingContext.getTemplateClassLoader() != null )
        {
            // -------------------------------------------------------------------------
            // If no template classloader was set we'll just use the context classloader
            // -------------------------------------------------------------------------

            old = Thread.currentThread().getContextClassLoader();

            Thread.currentThread().setContextClassLoader( siteRenderingContext.getTemplateClassLoader() );
        }

        try
        {
            Template template;
            Artifact skin = siteRenderingContext.getSkin();

            try
            {
                SkinModel skinModel = siteRenderingContext.getSkinModel();
                String encoding = ( skinModel == null ) ? null : skinModel.getEncoding();

                template = ( encoding == null ) ? velocity.getEngine().getTemplate( templateName )
                                : velocity.getEngine().getTemplate( templateName, encoding );
            }
            catch ( ParseErrorException pee )
            {
                throw new RendererException( "Velocity parsing error while reading the site decoration template "
                    + ( ( skin == null ) ? ( "'" + templateName + "'" ) : ( "from " + skin.getId() + " skin" ) ),
                                             pee );
            }
            catch ( ResourceNotFoundException rnfe )
            {
                throw new RendererException( "Could not find the site decoration template "
                    + ( ( skin == null ) ? ( "'" + templateName + "'" ) : ( "from " + skin.getId() + " skin" ) ),
                                             rnfe );
            }

            try
            {
                StringWriter sw = new StringWriter();
                template.merge( context, sw );
                writer.write( sw.toString().replaceAll( "\r?\n", SystemUtils.LINE_SEPARATOR ) );
            }
            catch ( VelocityException ve )
            {
                throw new RendererException( "Velocity error while merging site decoration template.", ve );
            }
            catch ( IOException ioe )
            {
                throw new RendererException( "IO exception while merging site decoration template.", ioe );
            }
        }
        finally
        {
            IOUtil.close( writer );

            if ( old != null )
            {
                Thread.currentThread().setContextClassLoader( old );
            }
        }
    }

    private SiteRenderingContext createSiteRenderingContext( Map<String, ?> attributes, DecorationModel decoration,
                                                             String defaultWindowTitle, Locale locale )
    {
        SiteRenderingContext context = new SiteRenderingContext();

        context.setTemplateProperties( attributes );
        context.setLocale( locale );
        context.setDecoration( decoration );
        context.setDefaultWindowTitle( defaultWindowTitle );

        return context;
    }

    /** {@inheritDoc} */
    public SiteRenderingContext createContextForSkin( Artifact skin, Map<String, ?> attributes,
                                                      DecorationModel decoration, String defaultWindowTitle,
                                                      Locale locale )
            throws IOException, RendererException
    {
        SiteRenderingContext context = createSiteRenderingContext( attributes, decoration, defaultWindowTitle, locale );

        context.setSkin( skin );

        ZipFile zipFile = getZipFile( skin.getFile() );
        InputStream in = null;

        try
        {
            if ( zipFile.getEntry( SKIN_TEMPLATE_LOCATION ) != null )
            {
                context.setTemplateName( SKIN_TEMPLATE_LOCATION );
                context.setTemplateClassLoader( new URLClassLoader( new URL[]{skin.getFile().toURI().toURL()} ) );
            }
            else
            {
                context.setTemplateName( DEFAULT_TEMPLATE );
                context.setTemplateClassLoader( getClass().getClassLoader() );
                context.setUsingDefaultTemplate( true );
            }

            ZipEntry skinDescriptorEntry = zipFile.getEntry( SkinModel.SKIN_DESCRIPTOR_LOCATION );
            if ( skinDescriptorEntry != null )
            {
                in = zipFile.getInputStream( skinDescriptorEntry );

                SkinModel skinModel = new SkinXpp3Reader().read( in );
                context.setSkinModel( skinModel );

                String toolsPrerequisite =
                    skinModel.getPrerequisites() == null ? null : skinModel.getPrerequisites().getDoxiaSitetools();

                Package p = DefaultSiteRenderer.class.getPackage();
                String current = ( p == null ) ? null : p.getImplementationVersion();

                if ( StringUtils.isNotBlank( toolsPrerequisite ) && ( current != null )
                    && !matchVersion( current, toolsPrerequisite ) )
                {
                    throw new RendererException( "Cannot use skin: has " + toolsPrerequisite
                        + " Doxia Sitetools prerequisite, but current is " + current );
                }
            }
        }
        catch ( XmlPullParserException e )
        {
            throw new RendererException( "Failed to parse " + SkinModel.SKIN_DESCRIPTOR_LOCATION
                + " skin descriptor from " + skin.getId() + " skin", e );
        }
        finally
        {
            IOUtil.close( in );
            closeZipFile( zipFile );
        }

        return context;
    }

    boolean matchVersion( String current, String prerequisite )
        throws RendererException
    {
        try
        {
            ArtifactVersion v = new DefaultArtifactVersion( current );
            VersionRange vr = VersionRange.createFromVersionSpec( prerequisite );

            boolean matched = false;
            ArtifactVersion recommendedVersion = vr.getRecommendedVersion();
            if ( recommendedVersion == null )
            {
                List<Restriction> restrictions = vr.getRestrictions();
                for ( Restriction restriction : restrictions )
                {
                    if ( restriction.containsVersion( v ) )
                    {
                        matched = true;
                        break;
                    }
                }
            }
            else
            {
                // only singular versions ever have a recommendedVersion
                @SuppressWarnings( "unchecked" )
                int compareTo = recommendedVersion.compareTo( v );
                matched = ( compareTo <= 0 );
            }

            if ( getLogger().isDebugEnabled() )
            {
                getLogger().debug( "Skin doxia-sitetools prerequisite: " + prerequisite + ", current: " + current
                    + ", matched = " + matched );
            }

            return matched;
        }
        catch ( InvalidVersionSpecificationException e )
        {
            throw new RendererException( "Invalid skin doxia-sitetools prerequisite: " + prerequisite, e );
        }
    }

    /** {@inheritDoc} */
    @Deprecated
    public SiteRenderingContext createContextForTemplate( File templateFile, Map<String, ?> attributes,
                                                          DecorationModel decoration, String defaultWindowTitle,
                                                          Locale locale )
            throws MalformedURLException
    {
        SiteRenderingContext context = createSiteRenderingContext( attributes, decoration, defaultWindowTitle, locale );

        context.setTemplateName( templateFile.getName() );
        context.setTemplateClassLoader( new URLClassLoader( new URL[]{templateFile.getParentFile().toURI().toURL()} ) );

        return context;
    }

    /** {@inheritDoc} */
    public void copyResources( SiteRenderingContext siteRenderingContext, File resourcesDirectory,
                               File outputDirectory )
        throws IOException
    {
        throw new AssertionError( "copyResources( SiteRenderingContext, File, File ) is deprecated." );
    }

    /** {@inheritDoc} */
    public void copyResources( SiteRenderingContext siteRenderingContext, File outputDirectory )
        throws IOException
    {
        if ( siteRenderingContext.getSkin() != null )
        {
            ZipFile file = getZipFile( siteRenderingContext.getSkin().getFile() );

            try
            {
                for ( Enumeration<? extends ZipEntry> e = file.entries(); e.hasMoreElements(); )
                {
                    ZipEntry entry = e.nextElement();

                    if ( !entry.getName().startsWith( "META-INF/" ) )
                    {
                        File destFile = new File( outputDirectory, entry.getName() );
                        if ( !entry.isDirectory() )
                        {
                            if ( destFile.exists() )
                            {
                                // don't override existing content: avoids extra rewrite with same content or extra site
                                // resource
                                continue;
                            }

                            destFile.getParentFile().mkdirs();

                            copyFileFromZip( file, entry, destFile );
                        }
                        else
                        {
                            destFile.mkdirs();
                        }
                    }
                }
            }
            finally
            {
                closeZipFile( file );
            }
        }

        if ( siteRenderingContext.isUsingDefaultTemplate() )
        {
            InputStream resourceList = getClass().getClassLoader()
                    .getResourceAsStream( RESOURCE_DIR + "/resources.txt" );

            if ( resourceList != null )
            {
                Reader r = null;
                LineNumberReader reader = null;
                try
                {
                    r = ReaderFactory.newReader( resourceList, ReaderFactory.UTF_8 );
                    reader = new LineNumberReader( r );

                    String line;

                    while ( ( line = reader.readLine() ) != null )
                    {
                        if ( line.startsWith( "#" ) || line.trim().length() == 0 )
                        {
                            continue;
                        }

                        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.exists() )
                        {
                            // don't override existing content: avoids extra rewrite with same content or extra site
                            // resource
                            continue;
                        }

                        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 );
                    }
                }
                finally
                {
                    IOUtil.close( reader );
                    IOUtil.close( r );
                }
            }
        }

        // Copy extra site resources
        for ( File siteDirectory : siteRenderingContext.getSiteDirectories() )
        {
            File resourcesDirectory = new File( siteDirectory, "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 exist. 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 static 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<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() );

            for ( String name : includedFiles )
            {
                File sourceFile = new File( source, name );

                File destinationFile = new File( destination, name );

                FileUtils.copyFile( sourceFile, destinationFile );
            }
        }
    }

    private Reader validate( Reader source, String resource )
            throws ParseException, IOException
    {
        getLogger().debug( "Validating: " + resource );

        try
        {
            String content = IOUtil.toString( new BufferedReader( source ) );

            new XmlValidator( new PlexusLoggerWrapper( getLogger() ) ).validate( content );

            return new StringReader( content );
        }
        finally
        {
            IOUtil.close( source );
        }
    }

    // TODO replace with StringUtils.endsWithIgnoreCase() from maven-shared-utils 0.7
    static boolean endsWithIgnoreCase( String str, String searchStr )
    {
        if ( str.length() < searchStr.length() )
        {
            return false;
        }

        return str.regionMatches( true, str.length() - searchStr.length(), searchStr, 0, searchStr.length() );
    }

    private static ZipFile getZipFile( File file )
        throws IOException
    {
        if ( file == null )
        {
            throw new IOException( "Error opening ZipFile: null" );
        }

        try
        {
            // TODO: plexus-archiver, if it could do the excludes
            return new ZipFile( file );
        }
        catch ( ZipException ex )
        {
            IOException ioe = new IOException( "Error opening ZipFile: " + file.getAbsolutePath() );
            ioe.initCause( ex );
            throw ioe;
        }
    }

    private static void closeZipFile( ZipFile zipFile )
    {
        // TODO: move to plexus utils
        try
        {
            zipFile.close();
        }
        catch ( IOException e )
        {
            // ignore
        }
    }
}
