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

com.fizzed.rocker.reload.ReloadingRockerBootstrap Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
/*
 * Copyright 2015 Fizzed Inc.
 *
 * 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
 *
 *      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.
 */
package com.fizzed.rocker.reload;

import com.fizzed.rocker.RenderingException;
import com.fizzed.rocker.RockerModel;
import com.fizzed.rocker.TemplateBindException;
import com.fizzed.rocker.TemplateNotFoundException;
import com.fizzed.rocker.runtime.CompileDiagnosticException;
import com.fizzed.rocker.runtime.CompileUnrecoverableException;
import com.fizzed.rocker.compiler.GeneratorException;
import com.fizzed.rocker.runtime.ParserException;
import com.fizzed.rocker.compiler.RockerConfiguration;
import com.fizzed.rocker.compiler.TemplateCompiler;
import com.fizzed.rocker.runtime.DefaultRockerBootstrap;
import com.fizzed.rocker.runtime.DefaultRockerModel;
import com.fizzed.rocker.runtime.DefaultRockerTemplate;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author joelauer
 */

public class ReloadingRockerBootstrap extends DefaultRockerBootstrap {
    static private final Logger log = LoggerFactory.getLogger(ReloadingRockerBootstrap.class);
    
    static public class LoadedTemplate {
        public File file;                           // resolved path to template file
        public long modifiedAt;                     // modified date of version loaded
        public String headerHash;                   // hash of header (interface)
    }
    
    private final RockerConfiguration configuration;
    private final ConcurrentHashMap models;
    private final ConcurrentHashMap templates;
    private RockerClassLoader classLoader;
    
    public ReloadingRockerBootstrap() {
        this.configuration = new RockerConfiguration();
        this.models = new ConcurrentHashMap<>();
        this.templates = new ConcurrentHashMap<>();
        this.classLoader = buildClassLoader();
    }

    public RockerConfiguration getConfiguration() {
        return configuration;
    }
    
    private RockerClassLoader buildClassLoader() {
        return new RockerClassLoader(this, ReloadingRockerBootstrap.class.getClassLoader());
    }

    // views.index$Template
    // views.index$PlainText
    // views.index$Template$1 (if something like an inner class)
    public boolean isReloadableClass(String className) {
        
        if (this.models.containsKey(className)) {
            return true;
        }
        
        // find first occurrence of $
        int pos = className.indexOf('$');
        if (pos < 0) {
            return false;
        }
        
        String modelClassName = className.substring(0, pos);
        
        return this.templates.containsKey(modelClassName);
    }
    
    public File getTemplateFile(String templatePackageName, String templateName) {
        File templateFileDirectory = new File(this.configuration.getTemplateDirectory(), templatePackageName.replace('.', '/'));
        return new File(templateFileDirectory, templateName);
    }
    
    private long getModelClassModifiedAt(Class modelType) throws RenderingException {
        try {
            Method m = modelType.getMethod("getModifiedAt");
            return (long) m.invoke(null);
        } catch (SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            throw new RenderingException("Unable to read getModifiedAt static method from class " + modelType.getName());
        }
    }
    
    private String getModelClassHeaderHash(Class modelType) throws RenderingException {
        try {
            Method m = modelType.getMethod("getHeaderHash");
            return (String)m.invoke(null);
        } catch (NoSuchMethodException | SecurityException | IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
            throw new RenderingException("Unable to read getHeaderHash static method from class " + modelType.getName());
        }
    }
    
    private String getModelClassTemplatePackageName(Class modelType) throws RenderingException {
        try {
            Method m = modelType.getMethod("getTemplatePackageName");
            return (String)m.invoke(null);
        } catch (SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            throw new RenderingException("Unable to read getTemplatePackageName static method from class " + modelType.getName());
        }
    }
    
    private String getModelClassTemplateName(Class modelType) throws RenderingException {
        try {
            Method m = modelType.getMethod("getTemplateName");
            return (String)m.invoke(null);
        } catch (SecurityException | IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RenderingException("Unable to read getTemplateName static method from class " + modelType.getName());
        }
    }
    
    @Override
    public DefaultRockerTemplate template(Class modelType, DefaultRockerModel model) throws RenderingException {
        
        LoadedTemplate template = templates.get(modelType.getName());
        
        if (template == null) {
            // read stored "metadata" compiled with template as static fields
            String templatePackageName = this.getModelClassTemplatePackageName(modelType);
            String templateName = this.getModelClassTemplateName(modelType);
            long modifiedAt = this.getModelClassModifiedAt(modelType);
            String headerHash = this.getModelClassHeaderHash(modelType);
            
            File templateFile = getTemplateFile(templatePackageName, templateName);
            
            if (!templateFile.exists()) {
                log.warn("{}: does not exist for model {}. Unable to check if reload required", templateFile, modelType.getCanonicalName());
                return buildTemplate(modelType, model, this.classLoader);
            }
            
            template = new LoadedTemplate();
            template.file = templateFile;
            template.modifiedAt = modifiedAt;
            template.headerHash = headerHash;
            
            templates.put(modelType.getName(), template);
            
        } else {
            
            if (!template.file.exists()) {
                log.warn("{}: no longer exists for model {} (did you delete it?)", template.file, modelType.getCanonicalName());
                return buildTemplate(modelType, model, this.classLoader);
            }
            
        }

        compileIfNeeded(template, true);
        
        return buildTemplate(modelType, model, this.classLoader);
    }
    
    @Override
    public RockerModel model(String templatePath) throws TemplateNotFoundException, TemplateBindException {
        
        String modelClassName = DefaultRockerBootstrap.templatePathToClassName(templatePath);
        
        LoadedTemplate template = templates.get(modelClassName);
        
        RockerModel initialModel = null;
        
        if (template == null) {
            File templateFile = new File(this.configuration.getTemplateDirectory(), templatePath);
            
            if (!templateFile.exists()) {
                log.warn("{}: does not exist. Unable to check if reload required", templateFile);
                return buildModel(templatePath, this.classLoader);
            }
            
            // load initial model so we can grab the metadata
            template = new LoadedTemplate();
            template.file = templateFile;
            template.modifiedAt = -1;           // maybe its not even compiled yet
            templates.put(modelClassName, template);
            
            // also add to models so that classloader knows to load it
            this.models.put(modelClassName, "");
            
            try {
                // update the template with the initially loaded modifed_at value
                initialModel = buildModel(templatePath, this.classLoader);
                template.modifiedAt = this.getModelClassModifiedAt(initialModel.getClass());
                template.headerHash = this.getModelClassHeaderHash(initialModel.getClass());
            } catch (Exception e) {
                // ignore exceptions here...
            }
            
        } else {
            
            if (!template.file.exists()) {
                log.warn("{}: no longer exists for model {} (did you delete it?)", template.file, modelClassName);
                return buildModel(templatePath, this.classLoader);
            }
            
        }
        
        boolean recompiled = compileIfNeeded(template, false);

        if (initialModel != null && !recompiled) {
            return initialModel;
        } else {
            // build a new one since it was recompiled
            return buildModel(templatePath, this.classLoader);
        }
    }
    
    public boolean compileIfNeeded(LoadedTemplate template, boolean verifyHeaderHash) {
        // recompile needed?
        long modifiedAt = template.file.lastModified();

        if (modifiedAt != template.modifiedAt) {

            log.info("Rocker template change detected [{}]", template.file);

            TemplateCompiler compiler = new TemplateCompiler(this.configuration);

            try {
                long start = System.currentTimeMillis();
                
                List units
                        = compiler.parse(Arrays.asList(template.file));

                // did the interface change?
                TemplateCompiler.CompilationUnit unit = units.get(0);
                String newHeaderHash = unit.getTemplateModel().createHeaderHash()+"";
                
                if (verifyHeaderHash) {
                    if (!newHeaderHash.equals(template.headerHash)) {
                        log.debug("current header hash " + template.headerHash + "; new header hash " + newHeaderHash);
                        
                        // build proper template exception
                        String templatePath = unit.getTemplateModel().getPackageName().replace('.', '/');
                        throw new RenderingException(1, 1, unit.getTemplateModel().getTemplateName(), templatePath, 
                                "Interface (e.g. arguments/imports) were modified. Unable to safely hot reload. Do a fresh project build and JVM restart.", null);
                    }
                }
                
                compiler.generate(units);

                compiler.compile(units);
                
                long stop = System.currentTimeMillis();
                
                log.info("Rocker compiled " + units.size() + " templates in " + (stop - start) + " ms");
                
                // save current modifiedAt & header hash
                template.modifiedAt = modifiedAt;
                template.headerHash = newHeaderHash;   
            } catch (ParserException | CompileDiagnosticException | CompileUnrecoverableException e) {
                throw e;
            } catch (IOException | GeneratorException e) {
                throw new RenderingException("Unable to compile rocker template", e);
            }

            // create new classloader to force a new class load
            this.classLoader = buildClassLoader();
            
            return true;
        }
        
        return false;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy