io.spring.initializr.generator.ProjectGenerator.java Source code

Java tutorial

Introduction

Here is the source code for io.spring.initializr.generator.ProjectGenerator.java

Source

/*
 * Copyright 2012-2017 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
 *
 *      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 io.spring.initializr.generator;

import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import io.spring.initializr.InitializrException;
import io.spring.initializr.metadata.BillOfMaterials;
import io.spring.initializr.metadata.Dependency;
import io.spring.initializr.metadata.InitializrConfiguration.Env.Maven.ParentPom;
import io.spring.initializr.metadata.InitializrMetadata;
import io.spring.initializr.metadata.InitializrMetadataProvider;
import io.spring.initializr.metadata.MetadataElement;
import io.spring.initializr.util.TemplateRenderer;
import io.spring.initializr.util.Version;
import io.spring.initializr.util.ZipUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;

/**
 * Generate a project based on the configured metadata.
 *
 * @author Dave Syer
 * @author Stephane Nicoll
 * @author Sebastien Deleuze
 */
public class ProjectGenerator {

    private static final Logger log = LoggerFactory.getLogger(ProjectGenerator.class);

    private static final Version VERSION_1_2_0_RC1 = Version.parse("1.2.0.RC1");

    private static final Version VERSION_1_3_0_M1 = Version.parse("1.3.0.M1");

    private static final Version VERSION_1_4_0_M2 = Version.parse("1.4.0.M2");

    private static final Version VERSION_1_4_0_M3 = Version.parse("1.4.0.M3");

    private static final Version VERSION_1_4_2_M1 = Version.parse("1.4.2.M1");

    private static final Version VERSION_1_5_0_M1 = Version.parse("1.5.0.M1");

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private InitializrMetadataProvider metadataProvider;

    @Autowired
    private ProjectRequestResolver requestResolver;

    @Autowired
    private TemplateRenderer templateRenderer = new TemplateRenderer();

    @Autowired
    private ProjectResourceLocator projectResourceLocator = new ProjectResourceLocator();

    @Value("${TMPDIR:.}/initializr")
    private String tmpdir;

    private File temporaryDirectory;
    private transient Map<String, List<File>> temporaryFiles = new LinkedHashMap<>();

    public InitializrMetadataProvider getMetadataProvider() {
        return metadataProvider;
    }

    public void setEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void setMetadataProvider(InitializrMetadataProvider metadataProvider) {
        this.metadataProvider = metadataProvider;
    }

    public void setRequestResolver(ProjectRequestResolver requestResolver) {
        this.requestResolver = requestResolver;
    }

    public void setTemplateRenderer(TemplateRenderer templateRenderer) {
        this.templateRenderer = templateRenderer;
    }

    public void setProjectResourceLocator(ProjectResourceLocator projectResourceLocator) {
        this.projectResourceLocator = projectResourceLocator;
    }

    public void setTmpdir(String tmpdir) {
        this.tmpdir = tmpdir;
    }

    public void setTemporaryDirectory(File temporaryDirectory) {
        this.temporaryDirectory = temporaryDirectory;
    }

    public void setTemporaryFiles(Map<String, List<File>> temporaryFiles) {
        this.temporaryFiles = temporaryFiles;
    }

    /**
     * Generate a Maven pom for the specified {@link ProjectRequest}.
     */
    public byte[] generateMavenPom(ProjectRequest request) {
        try {
            Map<String, Object> model = resolveModel(request);
            if (!isMavenBuild(request)) {
                throw new InvalidProjectRequestException(
                        "Could not generate Maven pom, " + "invalid project type " + request.getType());
            }
            byte[] content = doGenerateMavenPom(model);
            publishProjectGeneratedEvent(request);
            return content;
        } catch (InitializrException ex) {
            publishProjectFailedEvent(request, ex);
            throw ex;
        }
    }

    /**
     * Generate a Gradle build file for the specified {@link ProjectRequest}.
     */
    public byte[] generateGradleBuild(ProjectRequest request) {
        try {
            Map<String, Object> model = resolveModel(request);
            if (!isGradleBuild(request)) {
                throw new InvalidProjectRequestException(
                        "Could not generate Gradle build, " + "invalid project type " + request.getType());
            }
            byte[] content = doGenerateGradleBuild(model);
            publishProjectGeneratedEvent(request);
            return content;
        } catch (InitializrException ex) {
            publishProjectFailedEvent(request, ex);
            throw ex;
        }
    }

    /**
     * Generate a project structure for the specified {@link ProjectRequest}. Returns a
     * directory containing the project.
     */
    public File generateProjectStructure(ProjectRequest request) {
        try {
            return doGenerateProjectStructure(request);
        } catch (InitializrException ex) {
            publishProjectFailedEvent(request, ex);
            throw ex;
        }
    }

    protected File doGenerateProjectStructure(ProjectRequest request) {
        Map<String, Object> model = resolveModel(request);

        File rootDir;
        try {
            rootDir = File.createTempFile("tmp", "", getTemporaryDirectory());
        } catch (IOException e) {
            throw new IllegalStateException("Cannot create temp dir", e);
        }
        addTempFile(rootDir.getName(), rootDir);
        rootDir.delete();
        rootDir.mkdirs();

        File dir = initializerProjectDir(rootDir, request);

        if (isGradleBuild(request)) {
            String gradle = new String(doGenerateGradleBuild(model));
            writeText(new File(dir, "build.gradle"), gradle);
            writeGradleWrapper(dir, Version.safeParse(request.getBootVersion()));
        } else {
            String pom = new String(doGenerateMavenPom(model));
            writeText(new File(dir, "pom.xml"), pom);
            writeMavenWrapper(dir);
        }

        generateGitIgnore(dir, request);

        String applicationName = request.getApplicationName();
        String language = request.getLanguage();

        String codeLocation = language;
        File src = new File(new File(dir, "src/main/" + codeLocation), request.getPackageName().replace(".", "/"));
        src.mkdirs();
        String extension = ("kotlin".equals(language) ? "kt" : language);
        write(new File(src, applicationName + "." + extension), "Application." + extension, model);

        if ("war".equals(request.getPackaging())) {
            String fileName = "ServletInitializer." + extension;
            write(new File(src, fileName), fileName, model);
        }

        File test = new File(new File(dir, "src/test/" + codeLocation), request.getPackageName().replace(".", "/"));
        test.mkdirs();
        setupTestModel(request, model);
        write(new File(test, applicationName + "Tests." + extension), "ApplicationTests." + extension, model);

        File resources = new File(dir, "src/main/resources");
        resources.mkdirs();
        writeText(new File(resources, "application.properties"), "");

        if (request.hasWebFacet()) {
            new File(dir, "src/main/resources/templates").mkdirs();
            new File(dir, "src/main/resources/static").mkdirs();
        }

        generateExternalStructure(request, dir, model);
        publishProjectGeneratedEvent(request);
        return rootDir;

    }

    private void generateExternalStructure(ProjectRequest request, File dir, Map<String, Object> model) {
        try {
            log.info("Downloading file " + request.getExternalStructure());
            File file = downloadExternalStructure(request.getExternalStructure(), getTemporaryDirectory());
            log.info("Downloaded file at " + file.getAbsolutePath());
            ZipUtils.unzipArchive(file, dir, resolveModel(request));

        } catch (IOException e) {
            log.error("Exception Downloading file:" + e.getMessage());
        }

    }

    private File downloadExternalStructure(String urlStr, File file) throws IOException {
        URL url = new URL(urlStr);
        String filename = FilenameUtils.getBaseName(urlStr) + "." + FilenameUtils.getExtension(urlStr);
        log.info("Filename: " + filename);
        ReadableByteChannel rbc = Channels.newChannel(url.openStream());
        File f = new File(file, filename);
        FileOutputStream fos = new FileOutputStream(f);
        fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
        fos.close();
        rbc.close();
        return f;
    }

    /**
     * Create a distribution file for the specified project structure directory and
     * extension
     */
    public File createDistributionFile(File dir, String extension) {
        File download = new File(getTemporaryDirectory(), dir.getName() + extension);
        addTempFile(dir.getName(), download);
        return download;
    }

    private File getTemporaryDirectory() {
        if (temporaryDirectory == null) {
            temporaryDirectory = new File(tmpdir, "initializr");
            temporaryDirectory.mkdirs();
        }
        return temporaryDirectory;
    }

    /**
     * Clean all the temporary files that are related to this root directory.
     * @see #createDistributionFile
     */
    public void cleanTempFiles(File dir) {
        List<File> tempFiles = temporaryFiles.remove(dir.getName());
        if (!tempFiles.isEmpty()) {
            tempFiles.forEach((File file) -> {
                if (file.isDirectory()) {
                    FileSystemUtils.deleteRecursively(file);
                } else if (file.exists()) {
                    file.delete();
                }
            });
        }
    }

    private void publishProjectGeneratedEvent(ProjectRequest request) {
        ProjectGeneratedEvent event = new ProjectGeneratedEvent(request);
        eventPublisher.publishEvent(event);
    }

    private void publishProjectFailedEvent(ProjectRequest request, Exception cause) {
        ProjectFailedEvent event = new ProjectFailedEvent(request, cause);
        eventPublisher.publishEvent(event);
    }

    /**
     * Generate a {@code .gitignore} file for the specified {@link ProjectRequest}
     * @param dir the root directory of the project
     * @param request the request to handle
     */
    protected void generateGitIgnore(File dir, ProjectRequest request) {
        Map<String, Object> model = new LinkedHashMap<>();
        if (isMavenBuild(request)) {
            model.put("build", "maven");
            model.put("mavenBuild", true);
        } else {
            model.put("build", "gradle");
        }
        write(new File(dir, ".gitignore"), "gitignore.tmpl", model);
    }

    /**
     * Resolve the specified {@link ProjectRequest} and return the model to use to
     * generate the project
     * @param originalRequest the request to handle
     * @return a model for that request
     */
    protected Map<String, Object> resolveModel(ProjectRequest originalRequest) {
        Assert.notNull(originalRequest.getBootVersion(), "boot version must not be null");
        Map<String, Object> model = new LinkedHashMap<>();
        InitializrMetadata metadata = metadataProvider.get();

        ProjectRequest request = requestResolver.resolve(originalRequest, metadata);

        // request resolved so we can log what has been requested
        List<Dependency> dependencies = request.getResolvedDependencies();
        List<String> dependencyIds = dependencies.stream().map(Dependency::getId).collect(Collectors.toList());
        log.info("Processing request{type=" + request.getType() + ", dependencies=" + dependencyIds);

        if (isWar(request)) {
            model.put("war", true);
        }

        if (isMavenBuild(request)) {
            model.put("mavenBuild", true);
            ParentPom parentPom = metadata.getConfiguration().getEnv().getMaven()
                    .resolveParentPom(request.getBootVersion());
            if (parentPom.isIncludeSpringBootBom() && !request.getBoms().containsKey("spring-boot")) {
                request.getBoms().put("spring-boot",
                        metadata.createSpringBootBom(request.getBootVersion(), "spring-boot.version"));
            }

            model.put("mavenParentGroupId", parentPom.getGroupId());
            model.put("mavenParentArtifactId", parentPom.getArtifactId());
            model.put("mavenParentVersion", parentPom.getVersion());
            model.put("includeSpringBootBom", parentPom.isIncludeSpringBootBom());
        }

        model.put("repositoryValues", request.getRepositories().entrySet());
        if (!request.getRepositories().isEmpty()) {
            model.put("hasRepositories", true);
        }

        List<BillOfMaterials> resolvedBoms = request.getBoms().values().stream()
                .sorted(Comparator.comparing(BillOfMaterials::getOrder)).collect(Collectors.toList());
        model.put("resolvedBoms", resolvedBoms);
        ArrayList<BillOfMaterials> reversedBoms = new ArrayList<>(resolvedBoms);
        Collections.reverse(reversedBoms);
        model.put("reversedBoms", reversedBoms);

        model.put("compileDependencies", filterDependencies(dependencies, Dependency.SCOPE_COMPILE));
        model.put("runtimeDependencies", filterDependencies(dependencies, Dependency.SCOPE_RUNTIME));
        model.put("compileOnlyDependencies", filterDependencies(dependencies, Dependency.SCOPE_COMPILE_ONLY));
        model.put("providedDependencies", filterDependencies(dependencies, Dependency.SCOPE_PROVIDED));
        model.put("testDependencies", filterDependencies(dependencies, Dependency.SCOPE_TEST));

        request.getBoms().forEach((k, v) -> {
            if (v.getVersionProperty() != null) {
                request.getBuildProperties().getVersions().computeIfAbsent(v.getVersionProperty(),
                        key -> v::getVersion);
            }
        });

        Map<String, String> versions = new LinkedHashMap<>();
        model.put("buildPropertiesVersions", versions.entrySet());
        request.getBuildProperties().getVersions().forEach((k, v) -> versions.put(k, v.get()));
        Map<String, String> gradle = new LinkedHashMap<>();
        model.put("buildPropertiesGradle", gradle.entrySet());
        request.getBuildProperties().getGradle().forEach((k, v) -> gradle.put(k, v.get()));
        Map<String, String> maven = new LinkedHashMap<>();
        model.put("buildPropertiesMaven", maven.entrySet());
        request.getBuildProperties().getMaven().forEach((k, v) -> maven.put(k, v.get()));

        // Add various versions
        model.put("dependencyManagementPluginVersion",
                metadata.getConfiguration().getEnv().getGradle().getDependencyManagementPluginVersion());
        model.put("kotlinVersion", metadata.getConfiguration().getEnv().getKotlin().getVersion());
        if ("kotlin".equals(request.getLanguage())) {
            model.put("kotlin", true);
        }
        if ("groovy".equals(request.getLanguage())) {
            model.put("groovy", true);
        }

        model.put("isRelease", request.getBootVersion().contains("RELEASE"));
        // @SpringBootApplication available as from 1.2.0.RC1
        model.put("useSpringBootApplication",
                VERSION_1_2_0_RC1.compareTo(Version.safeParse(request.getBootVersion())) <= 0);

        // Gradle plugin has changed as from 1.3.0
        model.put("bootOneThreeAvailable",
                VERSION_1_3_0_M1.compareTo(Version.safeParse(request.getBootVersion())) <= 0);

        // Gradle plugin has changed again as from 1.4.2
        model.put("springBootPluginName",
                (VERSION_1_4_2_M1.compareTo(Version.safeParse(request.getBootVersion())) <= 0
                        ? "org.springframework.boot"
                        : "spring-boot"));

        // New testing stuff
        model.put("newTestInfrastructure", isNewTestInfrastructureAvailable(request));

        // New Servlet Initializer location
        model.put("newServletInitializer", isNewServletInitializerAvailable(request));

        // Java versions
        model.put("isJava6", isJavaVersion(request, "1.6"));
        model.put("isJava7", isJavaVersion(request, "1.7"));
        model.put("isJava8", isJavaVersion(request, "1.8"));

        // Append the project request to the model
        BeanWrapperImpl bean = new BeanWrapperImpl(request);
        for (PropertyDescriptor descriptor : bean.getPropertyDescriptors()) {
            if (bean.isReadableProperty(descriptor.getName())) {
                model.put(descriptor.getName(), bean.getPropertyValue(descriptor.getName()));
            }
        }
        if (!request.getBoms().isEmpty()) {
            model.put("hasBoms", true);
        }

        return model;
    }

    protected void setupTestModel(ProjectRequest request, Map<String, Object> model) {
        String imports = "";
        String testAnnotations = "";
        boolean newTestInfrastructure = isNewTestInfrastructureAvailable(request);
        if (newTestInfrastructure) {
            imports += String.format(
                    generateImport("org.springframework.boot.test.context.SpringBootTest", request.getLanguage())
                            + "%n");
            imports += String.format(
                    generateImport("org.springframework.test.context.junit4.SpringRunner", request.getLanguage())
                            + "%n");
        } else {
            imports += String.format(generateImport("org.springframework.boot.test.SpringApplicationConfiguration",
                    request.getLanguage()) + "%n");
            imports += String
                    .format(generateImport("org.springframework.test.context.junit4.SpringJUnit4ClassRunner",
                            request.getLanguage()) + "%n");
        }
        if (request.hasWebFacet() && !newTestInfrastructure) {
            imports += String.format(generateImport("org.springframework.test.context.web.WebAppConfiguration",
                    request.getLanguage()) + "%n");
            testAnnotations = String.format("@WebAppConfiguration%n");
        }
        model.put("testImports", imports);
        model.put("testAnnotations", testAnnotations);
    }

    protected String generateImport(String type, String language) {
        String end = ("groovy".equals(language) || "kotlin".equals(language)) ? "" : ";";
        return "import " + type + end;
    }

    private static boolean isGradleBuild(ProjectRequest request) {
        return "gradle".equals(request.getBuild());
    }

    private static boolean isMavenBuild(ProjectRequest request) {
        return "maven".equals(request.getBuild());
    }

    private static boolean isWar(ProjectRequest request) {
        return "war".equals(request.getPackaging());
    }

    private static boolean isNewTestInfrastructureAvailable(ProjectRequest request) {
        return VERSION_1_4_0_M2.compareTo(Version.safeParse(request.getBootVersion())) <= 0;
    }

    private static boolean isNewServletInitializerAvailable(ProjectRequest request) {
        return VERSION_1_4_0_M3.compareTo(Version.safeParse(request.getBootVersion())) <= 0;
    }

    private static boolean isGradle3Available(Version bootVersion) {
        return VERSION_1_5_0_M1.compareTo(bootVersion) <= 0;
    }

    private static boolean isJavaVersion(ProjectRequest request, String version) {
        return request.getJavaVersion().equals(version);
    }

    private byte[] doGenerateMavenPom(Map<String, Object> model) {
        return templateRenderer.process("starter-pom.xml", model).getBytes();
    }

    private byte[] doGenerateGradleBuild(Map<String, Object> model) {
        return templateRenderer.process("starter-build.gradle", model).getBytes();
    }

    private void writeGradleWrapper(File dir, Version bootVersion) {
        String gradlePrefix = isGradle3Available(bootVersion) ? "gradle3" : "gradle";
        writeTextResource(dir, "gradlew.bat", gradlePrefix + "/gradlew.bat");
        writeTextResource(dir, "gradlew", gradlePrefix + "/gradlew");

        File wrapperDir = new File(dir, "gradle/wrapper");
        wrapperDir.mkdirs();
        writeTextResource(wrapperDir, "gradle-wrapper.properties",
                gradlePrefix + "/gradle/wrapper/gradle-wrapper.properties");
        writeBinaryResource(wrapperDir, "gradle-wrapper.jar", gradlePrefix + "/gradle/wrapper/gradle-wrapper.jar");
    }

    private void writeMavenWrapper(File dir) {
        writeTextResource(dir, "mvnw.cmd", "maven/mvnw.cmd");
        writeTextResource(dir, "mvnw", "maven/mvnw");

        File wrapperDir = new File(dir, ".mvn/wrapper");
        wrapperDir.mkdirs();
        writeTextResource(wrapperDir, "maven-wrapper.properties", "maven/wrapper/maven-wrapper.properties");
        writeBinaryResource(wrapperDir, "maven-wrapper.jar", "maven/wrapper/maven-wrapper.jar");
    }

    private File writeBinaryResource(File dir, String name, String location) {
        return doWriteProjectResource(dir, name, location, true);
    }

    private File writeTextResource(File dir, String name, String location) {
        return doWriteProjectResource(dir, name, location, false);
    }

    private File doWriteProjectResource(File dir, String name, String location, boolean binary) {
        File target = new File(dir, name);
        if (binary) {
            writeBinary(target, projectResourceLocator.getBinaryResource("classpath:project/" + location));
        } else {
            writeText(target, projectResourceLocator.getTextResource("classpath:project/" + location));
        }
        return target;
    }

    private File initializerProjectDir(File rootDir, ProjectRequest request) {
        if (request.getBaseDir() != null) {
            File dir = new File(rootDir, request.getBaseDir());
            dir.mkdirs();
            return dir;
        } else {
            return rootDir;
        }
    }

    public void write(File target, String templateName, Map<String, Object> model) {
        String body = templateRenderer.process(templateName, model);
        writeText(target, body);
    }

    private void writeText(File target, String body) {
        try (OutputStream stream = new FileOutputStream(target)) {
            StreamUtils.copy(body, Charset.forName("UTF-8"), stream);
        } catch (Exception e) {
            throw new IllegalStateException("Cannot write file " + target, e);
        }
    }

    private void writeBinary(File target, byte[] body) {
        try (OutputStream stream = new FileOutputStream(target)) {
            StreamUtils.copy(body, stream);
        } catch (Exception e) {
            throw new IllegalStateException("Cannot write file " + target, e);
        }
    }

    private void addTempFile(String group, File file) {
        temporaryFiles.computeIfAbsent(group, key -> new ArrayList<>()).add(file);
    }

    private static List<Dependency> filterDependencies(List<Dependency> dependencies, String scope) {
        return dependencies.stream().filter(dep -> scope.equals(dep.getScope()))
                .sorted(Comparator.comparing(MetadataElement::getId)).collect(Collectors.toList());
    }

}