All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.qute.runtime.EngineProducer Maven / Gradle / Ivy

There is a newer version: 3.18.0.CR1
Show newest version
package io.quarkus.qute.runtime;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Produces;
import javax.inject.Singleton;
import javax.interceptor.Interceptor;

import org.jboss.logging.Logger;

import io.quarkus.arc.All;
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.qute.Engine;
import io.quarkus.qute.EngineBuilder;
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.Expression;
import io.quarkus.qute.HtmlEscaper;
import io.quarkus.qute.NamespaceResolver;
import io.quarkus.qute.Qute;
import io.quarkus.qute.ReflectionValueResolver;
import io.quarkus.qute.Resolver;
import io.quarkus.qute.Results;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.TemplateInstance.Initializer;
import io.quarkus.qute.TemplateLocator;
import io.quarkus.qute.TemplateLocator.TemplateLocation;
import io.quarkus.qute.UserTagSectionHelper;
import io.quarkus.qute.ValueResolver;
import io.quarkus.qute.ValueResolvers;
import io.quarkus.qute.Variant;
import io.quarkus.qute.runtime.QuteRecorder.QuteContext;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.LocalesBuildTimeConfig;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.Startup;

@Startup(Interceptor.Priority.PLATFORM_BEFORE)
@Singleton
public class EngineProducer {

    public static final String INJECT_NAMESPACE = "inject";
    public static final String CDI_NAMESPACE = "cdi";
    public static final String DEPENDENT_INSTANCES = "q_dep_inst";

    private static final String TAGS = "tags/";

    private static final Logger LOGGER = Logger.getLogger(EngineProducer.class);

    private final Engine engine;
    private final ContentTypes contentTypes;
    private final List tags;
    private final List suffixes;
    private final String basePath;
    private final String tagPath;
    private final Pattern templatePathExclude;
    private final Locale defaultLocale;
    private final Charset defaultCharset;
    private final ArcContainer container;

    public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig runtimeConfig,
            Event builderReady, Event engineReady, ContentTypes contentTypes,
            LaunchMode launchMode, LocalesBuildTimeConfig locales, @All List locators) {
        this.contentTypes = contentTypes;
        this.suffixes = config.suffixes;
        this.basePath = "templates/";
        this.tagPath = basePath + TAGS;
        this.tags = context.getTags();
        this.templatePathExclude = config.templatePathExclude;
        this.defaultLocale = locales.defaultLocale;
        this.defaultCharset = config.defaultCharset;
        this.container = Arc.container();

        LOGGER.debugf("Initializing Qute [templates: %s, tags: %s, resolvers: %s", context.getTemplatePaths(), tags,
                context.getResolverClasses());

        EngineBuilder builder = Engine.builder();

        // We don't register the map resolver because of param declaration validation
        builder.addValueResolver(ValueResolvers.thisResolver());
        builder.addValueResolver(ValueResolvers.orResolver());
        builder.addValueResolver(ValueResolvers.trueResolver());
        builder.addValueResolver(ValueResolvers.collectionResolver());
        builder.addValueResolver(ValueResolvers.mapperResolver());
        builder.addValueResolver(ValueResolvers.mapEntryResolver());
        // foo.string.raw returns a RawString which is never escaped
        builder.addValueResolver(ValueResolvers.rawResolver());
        builder.addValueResolver(ValueResolvers.logicalAndResolver());
        builder.addValueResolver(ValueResolvers.logicalOrResolver());
        builder.addValueResolver(ValueResolvers.orEmpty());
        // Note that arrays are handled specifically during validation
        builder.addValueResolver(ValueResolvers.arrayResolver());

        // Enable/disable strict rendering
        if (runtimeConfig.strictRendering) {
            builder.strictRendering(true);
        } else {
            builder.strictRendering(false);
            // If needed, use a specific result mapper for the selected strategy
            if (runtimeConfig.propertyNotFoundStrategy.isPresent()) {
                switch (runtimeConfig.propertyNotFoundStrategy.get()) {
                    case THROW_EXCEPTION:
                        builder.addResultMapper(new PropertyNotFoundThrowException());
                        break;
                    case NOOP:
                        builder.addResultMapper(new PropertyNotFoundNoop());
                        break;
                    case OUTPUT_ORIGINAL:
                        builder.addResultMapper(new PropertyNotFoundOutputOriginal());
                        break;
                    default:
                        // Use the default strategy
                        break;
                }
            } else {
                // Throw an exception in the development mode
                if (launchMode == LaunchMode.DEVELOPMENT) {
                    builder.addResultMapper(new PropertyNotFoundThrowException());
                }
            }
        }

        // Escape some characters for HTML/XML templates
        builder.addResultMapper(new HtmlEscaper(List.copyOf(config.escapeContentTypes)));

        // Fallback reflection resolver
        builder.addValueResolver(new ReflectionValueResolver());

        // Remove standalone lines if desired
        builder.removeStandaloneLines(runtimeConfig.removeStandaloneLines);

        // Iteration metadata prefix
        builder.iterationMetadataPrefix(config.iterationMetadataPrefix);

        // Default section helpers
        builder.addDefaultSectionHelpers();

        // Allow anyone to customize the builder
        builderReady.fire(builder);

        // Resolve @Named beans
        builder.addNamespaceResolver(NamespaceResolver.builder(INJECT_NAMESPACE).resolve(this::resolveInject).build());
        builder.addNamespaceResolver(NamespaceResolver.builder(CDI_NAMESPACE).resolve(this::resolveInject).build());

        // Add generated resolvers
        for (String resolverClass : context.getResolverClasses()) {
            Resolver resolver = createResolver(resolverClass);
            if (resolver instanceof NamespaceResolver) {
                builder.addNamespaceResolver((NamespaceResolver) resolver);
            } else {
                builder.addValueResolver((ValueResolver) resolver);
            }
            LOGGER.debugf("Added generated value resolver: %s", resolverClass);
        }
        // Add tags
        for (String tag : tags) {
            // Strip suffix, item.html -> item
            String tagName = tag.contains(".") ? tag.substring(0, tag.indexOf('.')) : tag;
            String tagTemplateId = TAGS + tagName;
            LOGGER.debugf("Registered UserTagSectionHelper for %s [%s]", tagName, tagTemplateId);
            builder.addSectionHelper(new UserTagSectionHelper.Factory(tagName, tagTemplateId));
        }
        // Add locator
        builder.addLocator(this::locate);
        registerCustomLocators(builder, locators);

        // Add a special parser hook for Qute.fmt() methods
        builder.addParserHook(new Qute.IndexedArgumentsParserHook());

        // Add template initializers
        for (String initializerClass : context.getTemplateInstanceInitializerClasses()) {
            builder.addTemplateInstanceInitializer(createInitializer(initializerClass));
        }

        // Add a special initializer for templates that contain an inject/cdi namespace expressions
        Map discoveredInjectTemplates = new HashMap<>();
        builder.addTemplateInstanceInitializer(new Initializer() {

            @Override
            public void accept(TemplateInstance instance) {
                Boolean hasInject = discoveredInjectTemplates.get(instance.getTemplate().getGeneratedId());
                if (hasInject == null) {
                    hasInject = hasInjectExpression(instance.getTemplate());
                }
                if (hasInject) {
                    // Add dependent beans map if the template contains a cdi namespace expression
                    instance.setAttribute(DEPENDENT_INSTANCES, new ConcurrentHashMap<>());
                    // Add a close action to destroy all dependent beans
                    instance.onRendered(new Runnable() {
                        @Override
                        public void run() {
                            Object dependentInstances = instance.getAttribute(EngineProducer.DEPENDENT_INSTANCES);
                            if (dependentInstances != null) {
                                @SuppressWarnings("unchecked")
                                ConcurrentMap> existing = (ConcurrentMap>) dependentInstances;
                                if (!existing.isEmpty()) {
                                    for (InstanceHandle handle : existing.values()) {
                                        handle.close();
                                    }
                                }
                            }
                        }
                    });
                }
            }
        });

        builder.timeout(runtimeConfig.timeout);
        builder.useAsyncTimeout(runtimeConfig.useAsyncTimeout);

        engine = builder.build();

        // Load discovered template files
        Map> discovered = new HashMap<>();
        for (String path : context.getTemplatePaths()) {
            Template template = engine.getTemplate(path);
            if (template != null) {
                for (String suffix : config.suffixes) {
                    if (path.endsWith(suffix)) {
                        String pathNoSuffix = path.substring(0, path.length() - (suffix.length() + 1));
                        List