/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.
 */

package org.apache.velocity.spring;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.runtime.RuntimeConstants;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
 * Factory that configures a VelocityEngine. Can be used standalone,
 * but typically you will either use {@link VelocityEngineFactoryBean}
 * for preparing a VelocityEngine as bean reference, or
 * <a href="https://docs.spring.io/spring-framework/docs/4.3.29.RELEASE/javadoc-api/org/springframework/web/servlet/view/velocity/VelocityConfigurer.html">org.springframework.web.servlet.view.velocity.VelocityConfigurer</a>
 * for web views.
 *
 * <p>The optional "configLocation" property sets the location of the Velocity
 * properties file, within the current application. Velocity properties can be
 * overridden via "velocityProperties", or even completely specified locally,
 * avoiding the need for an external properties file.
 *
 * <p>The "resourceLoaderPath" property can be used to specify the Velocity
 * resource loader path via Spring's Resource abstraction, possibly relative
 * to the Spring application context.
 *
 * <p>The simplest way to use this class is to specify a
 * {@link #setResourceLoaderPath(String) "resourceLoaderPath"}; the
 * VelocityEngine typically then does not need any further configuration.
 *
 * @author Juergen Hoeller
 * @author Claude Brisson
 * @since 2020-05-29
 * @see #setConfigLocation
 * @see #setVelocityProperties
 * @see #setResourceLoaderPath
 * @see #createVelocityEngine
 * @see VelocityEngineFactoryBean
 * @see <a href="https://docs.spring.io/spring-framework/docs/4.3.29.RELEASE/javadoc-api/org/springframework/web/servlet/view/velocity/VelocityConfigurer.html">org.springframework.web.servlet.view.velocity.VelocityConfigurer</a>
 * @see org.apache.velocity.app.VelocityEngine
 */
public class VelocityEngineFactory {

    protected static final Logger logger = LoggerFactory.getLogger(VelocityEngineFactory.class);

    private Resource configLocation;

    private final Map<String, Object> velocityProperties = new HashMap<String, Object>();

    private String resourceLoaderPath;

    private ResourceLoader resourceLoader = new DefaultResourceLoader();

    private boolean preferFileSystemAccess = true;


    /**
     * Set the location of the Velocity config file.
     * Alternatively, you can specify all properties locally.
     * 
     * @param configLocation config resource
     * 
     * @see #setVelocityProperties
     * @see #setResourceLoaderPath
     */
    public void setConfigLocation(Resource configLocation) {
        this.configLocation = configLocation;
    }

    /**
     * Set Velocity properties, like "file.resource.loader.path".
     * Can be used to override values in a Velocity config file,
     * or to specify all necessary properties locally.
     * 
     * <p>Note that the Velocity resource loader path also be set to any
     * Spring resource location via the "resourceLoaderPath" property.
     * Setting it here is just necessary when using a non-file-based
     * resource loader.</p>
     * 
     * @param velocityProperties engine properties to include
     *  
     * @see #setVelocityPropertiesMap
     * @see #setConfigLocation
     * @see #setResourceLoaderPath
     */
    public void setVelocityProperties(Properties velocityProperties) {
        CollectionUtils.mergePropertiesIntoMap(velocityProperties, this.velocityProperties);
    }

    /**
     * Set Velocity properties as Map, to allow for non-String values
     * like "ds.resource.loader.instance".
     * 
     * @param velocityPropertiesMap engine properties to include
     *  
     * @see #setVelocityProperties
     */
    public void setVelocityPropertiesMap(Map<String, Object> velocityPropertiesMap) {
        if (velocityPropertiesMap != null) {
            this.velocityProperties.putAll(velocityPropertiesMap);
        }
    }

    /**
     * Set the Velocity resource loader path via a Spring resource location.
     * Accepts multiple locations in Velocity's comma-separated path style.
     * <p>When populated via a String, standard URLs like "file:" and "classpath:"
     * pseudo URLs are supported, as understood by ResourceLoader. Allows for
     * relative paths when running in an ApplicationContext.
     * 
     * <p>Will define a path for the default Velocity resource loader with the name
     * "file". If the specified resource cannot be resolved to a {@code java.io.File},
     * a generic SpringResourceLoader will be used under the name "spring", without
     * modification detection.</p>
     * 
     * <p>Note that resource caching will be enabled in any case. With the file
     * resource loader, the last-modified timestamp will be checked on access to
     * detect changes. With SpringResourceLoader, the resource will be cached
     * forever (for example for class path resources).</p>
     * 
     * <p>To specify a modification check interval for files, use Velocity's
     * standard "file.resource.loader.modificationCheckInterval" property. By default,
     * the file timestamp is checked on every access (which is surprisingly fast).
     * Of course, this just applies when loading resources from the file system.
     * <p>To enforce the use of SpringResourceLoader, i.e. to not resolve a path
     * as file system resource in any case, turn off the "preferFileSystemAccess"
     * flag. See the latter's javadoc for details.</p>
     * 
     * @param resourceLoaderPath comma-separated resource paths
     *  
     * @see #setResourceLoader
     * @see #setVelocityProperties
     * @see #setPreferFileSystemAccess
     * @see SpringResourceLoader
     * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
     */
    public void setResourceLoaderPath(String resourceLoaderPath) {
        this.resourceLoaderPath = resourceLoaderPath;
    }

    /**
     * Set the Spring ResourceLoader to use for loading Velocity template files.
     * 
     * <p>The default is DefaultResourceLoader. Will get overridden by the
     * ApplicationContext if running in a context.</p>
     * 
     * @param resourceLoader Spring resource loader to ue 
     * 
     * @see org.springframework.core.io.DefaultResourceLoader
     * @see org.springframework.context.ApplicationContext
     */
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    /**
     * Return the Spring ResourceLoader to use for loading Velocity template files.
     * 
     * @return Spring resource loader to use 
     */
    protected ResourceLoader getResourceLoader() {
        return this.resourceLoader;
    }

    /**
     * Set whether to prefer file system access for template loading.
     * File system access enables hot detection of template changes.
     * 
     * <p>If this is enabled, VelocityEngineFactory will try to resolve the
     * specified "resourceLoaderPath" as file system resources, but only when
     * non-classpath resource paths are included.</p>
     * 
     * <p>Default is "true". Turn this off to always load via SpringResourceLoader
     * (i.e. as stream, without hot detection of template changes), which might
     * be necessary if some of your templates reside in an expanded classes
     * directory while others reside in jar files.</p>
     * 
     * @param preferFileSystemAccess whether to rely on file-based loading when possible
     * 
     * @see #setResourceLoaderPath
     */
    public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
        this.preferFileSystemAccess = preferFileSystemAccess;
    }

    /**
     * Return whether to prefer file system access for template loading.
     * 
     * @return  whether to prefer file system access for template loading
     */
    protected boolean isPreferFileSystemAccess() {
        return this.preferFileSystemAccess;
    }

    /**
     * Prepare the VelocityEngine instance and return it.
     * @return the VelocityEngine instance
     * @throws IOException if the config file wasn't found
     * @throws VelocityException on Velocity initialization failure
     */
    public VelocityEngine createVelocityEngine() throws IOException, VelocityException {
        VelocityEngine velocityEngine = newVelocityEngine();
        Map<String, Object> props = new HashMap<String, Object>();

        // Load config file if set.
        if (this.configLocation != null) {
            logger.info("Loading Velocity config from [{}]", this.configLocation);
            CollectionUtils.mergePropertiesIntoMap(PropertiesLoaderUtils.loadProperties(this.configLocation), props);
        }

        // Merge local properties if set.
        if (!this.velocityProperties.isEmpty()) {
            props.putAll(this.velocityProperties);
        }

        // Set a resource loader path, if required.
        if (this.resourceLoaderPath != null) {
            initVelocityResourceLoader(velocityEngine, this.resourceLoaderPath);
        }

        // Apply properties to VelocityEngine.
        for (Map.Entry<String, Object> entry : props.entrySet()) {
            velocityEngine.setProperty(entry.getKey(), entry.getValue());
        }

        postProcessVelocityEngine(velocityEngine);

        // Perform actual initialization.
        velocityEngine.init();

        return velocityEngine;
    }

    /**
     * Return a new VelocityEngine. Subclasses can override this for
     * custom initialization, or for using a mock object for testing.
     * <p>Called by {@code createVelocityEngine()}.
     * @return the VelocityEngine instance
     * @throws IOException if a config file wasn't found
     * @throws VelocityException on Velocity initialization failure
     * @see #createVelocityEngine()
     */
    protected VelocityEngine newVelocityEngine() throws IOException, VelocityException {
        return new VelocityEngine();
    }

    /**
     * Initialize a Velocity resource loader for the given VelocityEngine:
     * either a standard Velocity FileResourceLoader or a SpringResourceLoader.
     * <p>Called by {@code createVelocityEngine()}.
     * @param velocityEngine the VelocityEngine to configure
     * @param resourceLoaderPath the path to load Velocity resources from
     * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader
     * @see SpringResourceLoader
     * @see #initSpringResourceLoader
     * @see #createVelocityEngine()
     */
    protected void initVelocityResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
        
        final ResourceLoader loader = getResourceLoader();
        if (loader != null && isPreferFileSystemAccess()) {

            // Try to load via the file system, fall back to SpringResourceLoader
            // (for hot detection of template changes, if possible).
            
            final List<String> filePaths = new ArrayList<>();
            final List<String> nonFilePaths = new ArrayList<>();

            String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath);

            // Try to load via the file system, fall back to SpringResourceLoader
            // (for hot detection of template changes, if possible).
            for (int i = 0; i < paths.length; i++) {
                String path = paths[i];
                
                // Don't check classpath: locations for existence, they're not file-based.
                // Some containers will expand jars and trigger false positives.
                if (path.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX)) {
                    logger.debug("Using SpringResourceLoader for '{}'", path);
                    nonFilePaths.add(path);
                    continue;
                }

                try {
                    Resource resource = loader.getResource(path);
                    File file = resource.getFile();  // will fail if not resolvable in the file system
                    
                    logger.debug("Resource loader path [{}] resolved to file [{}]", path, file.getAbsolutePath());
                    filePaths.add(file.getAbsolutePath());
                }
                catch (IOException ex) {
                    logger.debug("Cannot resolve resource loader path '{}' to filesystem, will use SpringResourceLoader", path, ex);
                    nonFilePaths.add(path);
                }
            }

            if (!filePaths.isEmpty()) {
                velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "file");
                velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true");
                velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH,
                        StringUtils.collectionToCommaDelimitedString(filePaths));
            }
            
            if (!nonFilePaths.isEmpty()) {
                initSpringResourceLoader(velocityEngine, StringUtils.collectionToCommaDelimitedString(nonFilePaths));
            }
        }
        else {
            // Always load via SpringResourceLoader
            // (without hot detection of template changes).
            logger.debug("File system access not preferred: using SpringResourceLoader");
            initSpringResourceLoader(velocityEngine, resourceLoaderPath);
        }
    }

    /**
     * Initialize a SpringResourceLoader for the given VelocityEngine.
     * <p>Called by {@code initVelocityResourceLoader}.
     * @param velocityEngine the VelocityEngine to configure
     * @param resourceLoaderPath the path to load Velocity resources from
     * @see SpringResourceLoader
     * @see #initVelocityResourceLoader
     */
    protected void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) {
        velocityEngine.addProperty(
                RuntimeConstants.RESOURCE_LOADERS, SpringResourceLoader.NAME);
        velocityEngine.setProperty(
                SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName());
        velocityEngine.setProperty(
                SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true");
        velocityEngine.setApplicationAttribute(
                SpringResourceLoader.SPRING_RESOURCE_LOADER, getResourceLoader());
        velocityEngine.setApplicationAttribute(
                SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath);
    }

    /**
     * To be implemented by subclasses that want to perform custom
     * post-processing of the VelocityEngine after this FactoryBean
     * performed its default configuration (but before VelocityEngine.init).
     * <p>Called by {@code createVelocityEngine()}.
     * @param velocityEngine the current VelocityEngine
     * @throws IOException if a config file wasn't found
     * @throws VelocityException on Velocity initialization failure
     * @see #createVelocityEngine()
     * @see org.apache.velocity.app.VelocityEngine#init
     */
    protected void postProcessVelocityEngine(VelocityEngine velocityEngine)
            throws IOException, VelocityException {
    }

}
