package org.apache.turbine.services.assemblerbroker;


/*
 * 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.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.configuration2.Configuration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.turbine.Turbine;
import org.apache.turbine.TurbineConstants;
import org.apache.turbine.annotation.AnnotationProcessor;
import org.apache.turbine.modules.Assembler;
import org.apache.turbine.modules.Loader;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.assemblerbroker.util.AssemblerFactory;
import org.apache.turbine.util.HttpUtils;
import org.apache.turbine.util.TurbineException;
import org.apache.turbine.util.uri.URIConstants;

/**
 * TurbineAssemblerBrokerService allows assemblers (like screens,
 * actions and layouts) to be loaded from one or more AssemblerFactory
 * classes.  AssemblerFactory classes are registered with this broker
 * by adding them to the TurbineResources.properties file.
 *
 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
 * @version $Id$
 */
public class TurbineAssemblerBrokerService
        extends TurbineBaseService
        implements AssemblerBrokerService
{
    /** Logging */
    private static Logger log
            = LogManager.getLogger(TurbineAssemblerBrokerService.class);

    /** A structure that holds the registered AssemblerFactories */
    private Map<Class<?>, List<?>> factories = null;

    /** A cache that holds the generated Assemblers */
    private ConcurrentMap<String, Assembler> assemblerCache = null;

    /** A cache that holds the Loaders */
    private ConcurrentMap<Class<?>, Loader<? extends Assembler>> loaderCache = null;

    /** Caching on/off */
    private boolean isCaching;

    /**
     * Get a list of AssemblerFactories of a certain type
     *
     * @param type type of Assembler
     *
     * @param <T> the type of the assembler
     *
     * @return list of AssemblerFactories
     */
    @SuppressWarnings("unchecked")
    private <T extends Assembler> List<AssemblerFactory<T>> getFactoryGroup(Class<T> type)
    {
        if (!factories.containsKey(type))
        {
            factories.put(type, new ArrayList<AssemblerFactory<T>>());
        }
        return (List<AssemblerFactory<T>>) factories.get(type);
    }

    /**
     * Utility method to register all factories for a given type.
     *
     * @param type type of Assembler
     * @throws TurbineException if the factory for the given type could not be registered
     */
    private void registerFactories(String type)
        throws TurbineException
    {
        List<Object> names = getConfiguration().getList(type);

        log.info("Registering {} {} factories.", Integer.valueOf(names.size()), type);

        for (Object name2 : names)
        {
            String factory = (String) name2;
            try
            {
                @SuppressWarnings("unchecked")
                AssemblerFactory<? extends Assembler> af =
                    (AssemblerFactory<? extends Assembler>) Class.forName(factory).getDeclaredConstructor().newInstance();
                registerFactory(af);
            }
            // these must be passed to the VM
            catch (ThreadDeath | OutOfMemoryError e)
            {
                throw e;
            }
            // when using Class.forName(), NoClassDefFoundErrors are likely
            // to happen (missing jar files)
            catch (Throwable t)
            {
                throw new TurbineException("Failed registering " + type
                        + " factory: " + factory, t);
            }
        }
    }

    /**
     * Initializes the AssemblerBroker and loads the AssemblerFactory
     * classes registered in TurbineResources.Properties.
     *
     * @throws InitializationException if problems occur while registering the factories
     */
    @Override
    public void init()
        throws InitializationException
    {
        factories = new HashMap<>();

        try
        {
            Configuration conf = getConfiguration();

            for (Iterator<String> i = conf.getKeys(); i.hasNext();)
            {
                String type = i.next();

                if (!"classname".equalsIgnoreCase(type))
                {
                    registerFactories(type);
                }
            }
        }
        catch (TurbineException e)
        {
            throw new InitializationException(
                    "AssemblerBrokerService failed to initialize", e);
        }

        isCaching = Turbine.getConfiguration()
            .getBoolean(TurbineConstants.MODULE_CACHE_KEY,
                        TurbineConstants.MODULE_CACHE_DEFAULT);

        if (isCaching)
        {
            int cacheSize = Turbine.getConfiguration()
                .getInt(TurbineConstants.MODULE_CACHE_SIZE_KEY,
                        TurbineConstants.MODULE_CACHE_SIZE_DEFAULT);

            assemblerCache = new ConcurrentHashMap<>(cacheSize);
            loaderCache = new ConcurrentHashMap<>(cacheSize);
        }

        setInit(true);
    }

    /**
     * Register a new AssemblerFactory
     *
     * @param factory factory to register
     *
     * @param <T> the type of the assembler
     *
     */
    @Override
    public <T extends Assembler> void registerFactory(AssemblerFactory<T> factory)
    {
        getFactoryGroup(factory.getManagedClass()).add(factory);
    }

    /**
     * Attempt to retrieve an Assembler of a given type with
     * a name.  Cycle through all the registered AssemblerFactory
     * classes of type and return the first non-null assembly
     * found.  If an assembly was not found return null.
     *
     * @param type type of Assembler
     * @param name name of the requested Assembler
     *
     * @param <T> the type of the assembler
     *
     * @return an Assembler or null
     * @throws TurbineException if the assembler could not be loaded
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T extends Assembler> T getAssembler(Class<T> type, String name)
        throws TurbineException
    {
        String key = type + ":" + name;
        T assembler = null;
        
        log.debug("Check name in key {} in assembler.", key);
        if (HttpUtils.keyRequiresClean( name ))
        {
            String message = "Assembler key has invalid characters.";
            log.warn("{}. Abort assembling for cleaned key {}:{}.", message, type, HttpUtils.getCleanedKey( name ) );
            throw new TurbineException( message );
        }

        if (isCaching && assemblerCache.containsKey(key))
        {
            assembler = (T) assemblerCache.get(key);
            log.debug("Found {} in the cache!", key);
        }
        else
        {
            log.debug("Loading {}", key);
            List<AssemblerFactory<T>> facs = getFactoryGroup(type);

            for (Iterator<AssemblerFactory<T>> it = facs.iterator(); (assembler == null) && it.hasNext();)
            {
                AssemblerFactory<T> fac = it.next();

                try
                {
                    assembler = fac.getAssembler(name);
                }
                catch (Exception e)
                {
                    throw new TurbineException("Failed to load an assembler for "
                                               + name + " from the "
                                               + type + " factory "
                                               + fac.getClass().getName(), e);
                }

                if (assembler != null)
                {
                    AnnotationProcessor.process(assembler);

                    if (isCaching)
                    {
                        T oldAssembler = (T) assemblerCache.putIfAbsent(key, assembler);
                        if (oldAssembler != null)
                        {
                            assembler = oldAssembler;
                        }
                    }
                }
            }
        }

        return assembler;
    }

    /**
     * Get a Loader for the given assembler type
     *
     * @param type The Type of the Assembler
     *
     * @param <T> the type of the assembler
     *
     * @return A Loader instance for the requested type
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T extends Assembler> Loader<T> getLoader(Class<T> type)
    {
        Loader<T> loader = null;

        if (isCaching && loaderCache.containsKey(type))
        {
            loader = (Loader<T>) loaderCache.get(type);
            log.debug("Found {} loader in the cache!", type);
        }
        else
        {
            log.debug("Getting Loader for {}", type);
            List<AssemblerFactory<T>> facs = getFactoryGroup(type);

            for (Iterator<AssemblerFactory<T>> it = facs.iterator(); (loader == null) && it.hasNext();)
            {
                AssemblerFactory<T> fac = it.next();
                loader = fac.getLoader();
            }

            if (isCaching && loader != null)
            {
                loaderCache.put(type, loader);
            }
        }

        if (loader == null)
        {
            log.warn("Loader for {} is null.", type);
        }

        return loader;
    }
}
