com.expedia.tesla.compiler.Compiler.java Source code

Java tutorial

Introduction

Here is the source code for com.expedia.tesla.compiler.Compiler.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Expedia 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.expedia.tesla.compiler;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.io.FilenameUtils;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.expedia.tesla.compiler.plugins.CSharpUtils;
import com.expedia.tesla.compiler.plugins.CppUtils;
import com.expedia.tesla.compiler.plugins.JavaTypeMapper;
import com.expedia.tesla.compiler.plugins.JavaUtils;
import com.expedia.tesla.schema.Schema;
import com.expedia.tesla.schema.TeslaSchemaException;

/**
 * Telsa compiler compiles Tesla schema file (TML) into source code. This is the
 * entry point of the compiler tool.
 * 
 * @author <a href="mailto:yzuo@expedia.com">Yunfei Zuo</a>
 */
public class Compiler {

    public static final String LANG_JAVA = "java";
    public static final String LANG_CPP = "cpp";
    public static final String LANG_CSHARP = "csharp";
    private static final Map<String, String> LANG_ALIAS = new HashMap<>();

    @Parameter(names = { "-l", "--language" }, description = "The output code language (Java, C# or C++).")
    private String language = LANG_JAVA;

    @Parameter(names = { "-s", "--serializer" }, description = "The full name the generated serializer class name "
            + "(e.g. com.expedia.sample.AppSchema). Or C++ header file name without extension name.")
    private String appSchemaClassName = null;

    @Parameter(names = { "-o",
            "--output-dir" }, description = "The output directory. Output to current directory by default.")
    private File outputDir = new File(System.getProperty("user.dir"));

    @Parameter(names = { "-c", "--class-template" }, description = "User class source code template.")
    private String classTemplatePath;

    @Parameter(names = { "-m", "--enum-template" }, description = "User enum source code template.")
    private String enumTemplatePath;

    @Parameter(names = { "-v", "--serializer-template" }, description = "User serializer source code template.")
    private String appSchemaTemplatePath;

    @Parameter(names = { "-nu",
            "--not-generate-user-types" }, description = "Not generate source code for user types.")
    private boolean notGenerateUserTypes;

    @Parameter(names = { "-E", "--plugin" }, description = "Create a plugin object of type <class_name> with "
            + "<variable_name>. The plugin object will be available in template enginees, and can be refered by "
            + "variable $variable_name.")
    private Map<String, String> extension = new HashMap<>();

    @Parameter(description = "Tesla schema files (TMLs).", required = true)
    private List<String> schemaFiles = new ArrayList<>();

    @Parameter(names = { "-cp",
            "-classpath" }, description = "Java classpath that compiler will search for user classes.")
    private String classpath;

    @Parameter(names = { "-h", "--help" }, description = "Show this help message.", help = true)
    private boolean help;

    static {
        LANG_ALIAS.put("java", LANG_JAVA);
        LANG_ALIAS.put("c++", LANG_CPP);
        LANG_ALIAS.put("cpp", LANG_CPP);
        LANG_ALIAS.put("csharp", LANG_CSHARP);
        LANG_ALIAS.put("cs", LANG_CSHARP);
        LANG_ALIAS.put("c#", LANG_CSHARP);
    }

    /**
     * Constructor.
     */
    public Compiler() {
    }

    /**
     * Get the name of programming language we are generation source code.
     * 
     * @return the name of programming language we are generation source code.
     */
    public String getLanguage() {
        return language;
    }

    /**
     * Get the full name the generated serializer class name.
     * 
     * @return the full name the generated serializer class name. 
     */
    public String getAppSchemaClassName() {
        return appSchemaClassName;
    }

    /**
     * Get the base output directory of the generated source code.
     *  
     * @return the base output directory of the generated source code.
     */
    public File getOutputDir() {
        return outputDir;
    }

    /**
     * Get the user class source code template.
     * 
     * @return the user class source code template.
     */
    public String getClassTemplatePath() {
        return classTemplatePath;
    }

    /**
     * Get the user enum source code template.
     * 
     * @return the user enum source code template.
     */
    public String getEnumTemplatePath() {
        return enumTemplatePath;
    }

    /**
     * Get the path of user serializer source code template.
     * 
     * @return the path of user serializer source code template.
     */
    public String getAppSchemaTemplatePath() {
        return appSchemaTemplatePath;
    }

    /**
     * Check if it is configured to not generate source code for user types.
     * 
     * @return ture if it is configured to not generate source code for user types. Otherwise, return false.
     */
    public boolean isNotGenerateUserTypes() {
        return notGenerateUserTypes;
    }

    /**
     * Get user extensions. The keys are extension names while the values are full class names of the extensions.
     *  
     * @return the user extensions.
     */
    public Map<String, String> getExtension() {
        return extension;
    }

    /**
     * Get the list of schema file paths.
     *
     * @return the paths of schema files.
     */
    public Collection<String> getSchemaFiles() {
        return new ArrayList<String>(schemaFiles);
    }

    /**
     * Get the classpath where compiler load Java classes for user extensions.
     * 
     * @return the Java the classpath where compiler load Java classes for user extensions.
     */
    public String getClasspath() {
        return classpath;
    }

    /**
     * Set the name of programming language we are generation source code
     * 
     * @param language the name of programming language we are generation source code
     */
    public void setLanguage(String language) {
        this.language = LANG_ALIAS.get(language.toLowerCase());
    }

    /**
     * Set the full name the generated serializer class name.
     * 
     * @param appSchemaClassName 
     *       the full name the generated serializer class name.
     */
    public void setAppSchemaClassName(String appSchemaClassName) {
        this.appSchemaClassName = appSchemaClassName;
    }

    /**
     * Set the base output directory of the generated source code.
     * 
     * @param outputDir 
     *       the base output directory of the generated source code.
     */
    public void setOutputDir(File outputDir) {
        this.outputDir = outputDir;
    }

    /**
     * Set the base output directory of the generated source code.
     * 
     * @param outputDir 
     *       the base output directory of the generated source code.
     */
    public void setOutputDir(String outputDir) {
        setOutputDir(new File(outputDir));
    }

    /**
     * Set the user class source code template.
     * 
     * @param classTemplatePath 
     *       the user class source code template.
     */
    public void setClassTemplatePath(String classTemplatePath) {
        this.classTemplatePath = classTemplatePath;
    }

    /**
     * Set the user enum source code template.
     * 
     * @param enumTemplatePath 
     *       the user enum source code template.
     */
    public void setEnumTemplatePath(String enumTemplatePath) {
        this.enumTemplatePath = enumTemplatePath;
    }

    /**
     * Set the path of user serializer source code template.
     * 
     * @param appSchemaTemplatePath 
     *       the path of user serializer source code template.
     */
    public void setAppSchemaTemplatePath(String appSchemaTemplatePath) {
        this.appSchemaTemplatePath = appSchemaTemplatePath;
    }

    /**
     * Configure compiler to not generate source code for user types. 
     * <p>
     * This is {@code false} by default.
     * 
     * @param notGenerateUserTypes
     *       set this to {@code true} for partial code generation which doesn't generate source code for user 
     *       classes and enums. 
     */
    public void setNotGenerateUserTypes(boolean notGenerateUserTypes) {
        this.notGenerateUserTypes = notGenerateUserTypes;
    }

    /**
     * Set user extensions. 
     * <p>
     * The keys are extension names while the values are full class names of the extensions.
     * 
     * @param extension 
     *       the extension to set
     */
    public void setExtension(Map<String, String> extension) {
        this.extension.clear();
        this.extension.putAll(extension);
    }

    /**
     * Set input schema file paths.
     * 
     * @param schemaFiles
     *     the list of schema file paths.
     */
    public void setSchemaFiles(Collection<String> schemaFiles) {
        this.schemaFiles.clear();
        this.schemaFiles.addAll(schemaFiles);
    }

    /**
     * Set the classpath where compiler load Java classes for user extensions. 
     * <p>
     * Tesla compiler will search for user extensions from both system class path and class paths specified here.
     * 
     * @param classpath 
     *       the Java classpath where compiler load Java classes for user extensions.
     */
    public void setClasspath(String classpath) {
        this.classpath = classpath;
    }

    /**
     * Merge Tesla schemas and generate source code use the current settings. 
     * @throws Exception
     */
    public void compile() throws Exception {
        // normalize language name
        setLanguage(this.language);
        MergedSchema ms = mergeSchemas(schemaFiles);
        if (!this.notGenerateUserTypes) {
            writeClasses(ms.getClasses());
            writeEnums(ms.getEnumes());
        }
        writeAppSchema(ms.getSchemas());
        System.out.println("Tesla compiled schema(s) successfully.");
    }

    private Map<String, Object> loadExtension() throws Exception {
        Map<String, Object> variables = new HashMap<>();
        if (this.extension != null) {
            for (String varName : this.extension.keySet()) {
                String varClassName = this.extension.get(varName);
                java.lang.Class<?> clazz = java.lang.Class.forName(varClassName);
                variables.put(varName, clazz.newInstance());
            }
        }
        return variables;
    }

    private void writeEnums(List<MergedEnum> enums) throws Exception {
        TemplateEngine te = new TemplateEngine(this.language, this.enumTemplatePath, "enum.vm");
        for (MergedEnum me : enums) {
            Map<String, Object> variables = new TreeMap<String, Object>();
            variables.put("enum", me);
            variables.put("util", createUtilObject());
            variables.putAll(loadExtension());

            String path = getOutputPath(me.getName());
            try (Writer writer = new FileWriter(path)) {
                System.out.println("Writing source file " + path);
                te.merge(variables, writer);
            }
        }
    }

    private String getOutputPath(String fullName) throws IOException {
        int idx = fullName.lastIndexOf('.');
        String shortName = idx == -1 ? fullName : fullName.substring(idx + 1);
        String fileName = shortName;
        switch (this.language) {
        case LANG_JAVA:
            fileName = fullName.replace('.', File.separatorChar) + ".java";
            break;
        case LANG_CSHARP:
            fileName += ".cs";
            break;
        case LANG_CPP:
            fileName += ".h";
            break;
        default:
            fileName += '.' + this.language;
            break;
        }
        File file = new File(FilenameUtils.concat(this.outputDir.getAbsolutePath(), fileName));
        Util.forceMkdirParent(file);
        return file.getAbsolutePath();
    }

    private void writeClasses(List<MergedClass> classes) throws Exception {
        TemplateEngine te = new TemplateEngine(this.language, this.classTemplatePath, "class.vm");
        for (MergedClass mc : classes) {
            Map<String, Object> variables = new TreeMap<String, Object>();
            variables.put("class", mc);
            variables.put("util", createUtilObject());
            variables.put("serializerName", this.appSchemaClassName);
            variables.putAll(loadExtension());

            String path = getOutputPath(mc.getName());
            try (Writer writer = new FileWriter(path)) {
                System.out.println("Writing source file " + path);
                te.merge(variables, writer);
            }
        }
    }

    private Object createUtilObject() {
        if (this.language.equals(LANG_JAVA)) {
            return new JavaUtils();
        } else if (this.language.equals(LANG_CPP)) {
            return new CppUtils();
        } else if (this.language.equals(LANG_CSHARP)) {
            return new CSharpUtils();
        }
        return null;
    }

    private void writeAppSchema(Collection<Schema> collection) throws IOException {
        if (this.appSchemaClassName == null || this.appSchemaClassName.isEmpty()) {
            return;
        }

        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        ve.init();

        String appSchemaClassShortName = this.appSchemaClassName;
        String appSchemaClassNamespace = null;
        if (this.appSchemaClassName.matches(".+\\..+")) {
            int pos = this.appSchemaClassName.lastIndexOf('.');
            appSchemaClassShortName = this.appSchemaClassName.substring(pos + 1);
            appSchemaClassNamespace = this.appSchemaClassName.substring(0, pos);
        }

        String defaultTemplate = this.language.equals(LANG_JAVA) ? "serializer.vm" : "application_schema.vm";
        TemplateEngine te = new TemplateEngine(this.language, this.appSchemaTemplatePath, defaultTemplate);
        Map<String, Object> variables = new HashMap<>();
        variables.put("schemas", collection);
        variables.put("className", appSchemaClassShortName);
        variables.put("namespace", appSchemaClassNamespace);
        variables.put("util", createUtilObject());
        variables.put("mapper", new JavaTypeMapper());

        String path = getOutputPath(this.appSchemaClassName);
        try (Writer writer = new FileWriter(path)) {
            System.out.println("Writing source file " + path);
            te.merge(variables, writer);
            writer.flush();
        }
    }

    private MergedSchema mergeSchemas(Collection<String> values) throws TeslaSchemaException, IOException {
        List<Schema> schemas = new ArrayList<>();
        for (String path : Util.expandWildcard(values)) {
            schemas.add(Schema.build(path));
        }
        return new MergedSchema(schemas);
    }

    public static void main(String[] args) throws Exception {
        JCommander jc = null;
        try {
            Compiler compiler = new Compiler();
            jc = new JCommander(compiler);
            jc.parse(args);
            if (compiler.help) {
                jc.usage();
            } else {
                compiler.compile();
            }
        } catch (ParameterException e) {
            System.err.printf("Error: %s\nUse -h or --help option to show usage help.\n", e.getMessage());
            System.exit(1);
        }
    }

}