Add support for customizable project generation

This commit adds a project generation infrastructure based on the
abstraction defined thus far. Each project is described by a
`ProjectDescription` that provides the basic information about the
project such as language, build system, packaging, platform version
and more.

Each project runs in a dedicated `ProjectApplicationContext` where
contributors and customizers are elected to generate the project.

Customizers are meant to update the model based on the
`ProjectDescription` and other factors while contributors consume
models to generate project assets (build files, source files, etc).

Because project generation runs in a dedicated context, components can
be flagged with special conditions that enable them only when necessary.
Several conditions are provided in this commit to enable a component
based on the language, build system, packaging, platform version or
requested dependency.

See gh-340

Co-authored-by: Andy Wilkinson <awilkinson@pivotal.io>
Co-authored-by: Madhura Bhave <mbhave@pivotal.io>
This commit is contained in:
Stephane Nicoll 2019-02-07 15:59:37 +01:00
parent 2cb1f3e647
commit 0d5efd24ba
37 changed files with 2439 additions and 0 deletions

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2019 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.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import io.spring.initializr.generator.buildsystem.BuildSystem;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches when a generated project will use a particular
* {@link BuildSystem}.
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnBuildSystemCondition.class)
public @interface ConditionalOnBuildSystem {
String value();
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2019 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.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import io.spring.initializr.generator.language.Language;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches when a generated project will be written using a particular
* {@link Language}.
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnLanguageCondition.class)
public @interface ConditionalOnLanguage {
String value();
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2019 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.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import io.spring.initializr.generator.packaging.Packaging;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches when a generated project will use a particular
* {@link Packaging}.
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPackagingCondition.class)
public @interface ConditionalOnPackaging {
String value();
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2012-2019 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.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches when a generated project is using a matching version of the
* platform.
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPlatformVersionCondition.class)
public @interface ConditionalOnPlatformVersion {
/**
* The version range to match.
* @return the version range
*/
String value();
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-2019 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.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches when a {@link ResolvedProjectDescription} defines a particular
* dependency. A generated project may ultimately define a different set of dependencies
* according to the contributors that have been executed. To contribute to the project
* according to the real set, prefer querying the model itself rather than using this
* condition.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnRequestedDependencyCondition.class)
public @interface ConditionalOnRequestedDependency {
/**
* The identifier of the dependency.
* @return the dependency ID
*/
String value();
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.buildsystem.BuildSystem;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link ProjectGenerationCondition Condition} implementation for
* {@link ConditionalOnBuildSystem}.
*
* @author Andy Wilkinson
*/
class OnBuildSystemCondition extends ProjectGenerationCondition {
@Override
protected boolean matches(ResolvedProjectDescription projectDescription,
ConditionContext context, AnnotatedTypeMetadata metadata) {
String buildSystemId = (String) metadata
.getAllAnnotationAttributes(ConditionalOnBuildSystem.class.getName())
.getFirst("value");
BuildSystem buildSystem = BuildSystem.forId(buildSystemId);
return projectDescription.getBuildSystem().id().equals(buildSystem.id());
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.language.Language;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link ProjectGenerationCondition Condition} implementation for
* {@link ConditionalOnLanguage}.
*
* @author Andy Wilkinson
*/
class OnLanguageCondition extends ProjectGenerationCondition {
@Override
protected boolean matches(ResolvedProjectDescription projectDescription,
ConditionContext context, AnnotatedTypeMetadata metadata) {
if (projectDescription.getLanguage() == null) {
return false;
}
String languageId = (String) metadata
.getAllAnnotationAttributes(ConditionalOnLanguage.class.getName())
.getFirst("value");
Language language = Language.forId(languageId, null);
return projectDescription.getLanguage().id().equals(language.id());
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.packaging.Packaging;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link ProjectGenerationCondition Condition} implementation for
* {@link ConditionalOnPackaging}.
*
* @author Andy Wilkinson
*/
class OnPackagingCondition extends ProjectGenerationCondition {
@Override
protected boolean matches(ResolvedProjectDescription projectDescription,
ConditionContext context, AnnotatedTypeMetadata metadata) {
if (projectDescription.getPackaging() == null) {
return false;
}
String packagingId = (String) metadata
.getAllAnnotationAttributes(ConditionalOnPackaging.class.getName())
.getFirst("value");
Packaging packaging = Packaging.forId(packagingId);
return projectDescription.getPackaging().id().equals(packaging.id());
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import io.spring.initializr.generator.version.VersionParser;
import io.spring.initializr.generator.version.VersionRange;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link ProjectGenerationCondition} implementation for
* {@link ConditionalOnPlatformVersion}.
*
* @author Andy Wilkinson
*/
class OnPlatformVersionCondition extends ProjectGenerationCondition {
@Override
protected boolean matches(ResolvedProjectDescription projectDescription,
ConditionContext context, AnnotatedTypeMetadata metadata) {
if (projectDescription.getPlatformVersion() == null) {
return false;
}
VersionRange range = VersionParser.DEFAULT.parseRange((String) metadata
.getAnnotationAttributes(ConditionalOnPlatformVersion.class.getName())
.get("value"));
return range.match(projectDescription.getPlatformVersion());
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link ProjectGenerationCondition} implementation for
* {@link ConditionalOnRequestedDependency}.
*
* @author Andy Wilkinson
*/
class OnRequestedDependencyCondition extends ProjectGenerationCondition {
@Override
protected boolean matches(ResolvedProjectDescription projectDescription,
ConditionContext context, AnnotatedTypeMetadata metadata) {
String id = (String) metadata
.getAnnotationAttributes(ConditionalOnRequestedDependency.class.getName())
.get("value");
return projectDescription.getRequestedDependencies().containsKey(id);
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* Base class for all project generation {@link Condition Conditions}.
*
* @author Andy Wilkinson
*/
public abstract class ProjectGenerationCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
ResolvedProjectDescription projectDescription = context.getBeanFactory()
.getBean(ResolvedProjectDescription.class);
return matches(projectDescription, context, metadata);
}
protected abstract boolean matches(ResolvedProjectDescription projectDescription,
ConditionContext context, AnnotatedTypeMetadata metadata);
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Project generation {@code Condition} annotations and supporting classes.
*/
package io.spring.initializr.generator.condition;

View File

@ -0,0 +1,64 @@
/*
* Copyright 2012-2019 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.project;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import io.spring.initializr.generator.project.contributor.ProjectContributor;
/**
* The default {@link ProjectAssetGenerator}. Generates a directory structure with all
* available {@link ProjectContributor project contributors}.
*
* @author Stephane Nicoll
*/
public class DefaultProjectAssetGenerator implements ProjectAssetGenerator<Path> {
@Override
public Path generate(ProjectGenerationContext context) throws IOException {
ResolvedProjectDescription resolvedProjectDescription = context
.getBean(ResolvedProjectDescription.class);
Path projectRoot = context.getBean(ProjectDirectoryFactory.class)
.createProjectDirectory(resolvedProjectDescription);
Path projectDirectory = initializerProjectDirectory(projectRoot,
resolvedProjectDescription);
List<ProjectContributor> contributors = context
.getBeanProvider(ProjectContributor.class).orderedStream()
.collect(Collectors.toList());
for (ProjectContributor contributor : contributors) {
contributor.contribute(projectDirectory);
}
return projectRoot;
}
private Path initializerProjectDirectory(Path rootDir,
ResolvedProjectDescription description) throws IOException {
if (description.getBaseDirectory() != null) {
Path dir = rootDir.resolve(description.getBaseDirectory());
Files.createDirectories(dir);
return dir;
}
else {
return rootDir;
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-2019 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.project;
import java.io.IOException;
/**
* Generate project assets using a {@link ProjectGenerationContext}.
*
* @param <T> the type that gathers the project assets
* @author Stephane Nicoll
*/
@FunctionalInterface
public interface ProjectAssetGenerator<T> {
/**
* Generate project assets using the specified {@link ProjectGenerationContext}.
* @param context the context to use
* @return the type that gathers the project assets
* @throws IOException if writing project assets failed
*/
T generate(ProjectGenerationContext context) throws IOException;
}

View File

@ -0,0 +1,164 @@
/*
* Copyright 2012-2019 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.project;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import io.spring.initializr.generator.buildsystem.BuildSystem;
import io.spring.initializr.generator.buildsystem.Dependency;
import io.spring.initializr.generator.language.Language;
import io.spring.initializr.generator.packaging.Packaging;
import io.spring.initializr.generator.version.Version;
/**
* Description of a project to generate.
*
* @author Andy Wilkinson
*/
public class ProjectDescription {
private Version platformVersion;
private BuildSystem buildSystem;
private Packaging packaging;
private Language language;
private final Map<String, Dependency> requestedDependencies = new LinkedHashMap<>();
private String groupId;
private String artifactId;
private String name;
private String description;
private String applicationName;
private String packageName;
private String baseDirectory;
/**
* Resolve the state of this instance to a {@link ResolvedProjectDescription}.
* @return an immutable description.
*/
public ResolvedProjectDescription resolve() {
return new ResolvedProjectDescription(this);
}
public Version getPlatformVersion() {
return this.platformVersion;
}
public void setPlatformVersion(Version platformVersion) {
this.platformVersion = platformVersion;
}
public BuildSystem getBuildSystem() {
return this.buildSystem;
}
public void setBuildSystem(BuildSystem buildSystem) {
this.buildSystem = buildSystem;
}
public Packaging getPackaging() {
return this.packaging;
}
public void setPackaging(Packaging packaging) {
this.packaging = packaging;
}
public Language getLanguage() {
return this.language;
}
public void setLanguage(Language language) {
this.language = language;
}
public Dependency addDependency(String id, Dependency dependency) {
return this.requestedDependencies.put(id, dependency);
}
public Map<String, Dependency> getRequestedDependencies() {
return Collections.unmodifiableMap(this.requestedDependencies);
}
public String getGroupId() {
return this.groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getArtifactId() {
return this.artifactId;
}
public void setArtifactId(String artifactId) {
this.artifactId = artifactId;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public String getApplicationName() {
return this.applicationName;
}
public void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}
public String getPackageName() {
return this.packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getBaseDirectory() {
return this.baseDirectory;
}
public void setBaseDirectory(String baseDirectory) {
this.baseDirectory = baseDirectory;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-2019 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.project;
import org.springframework.core.Ordered;
/**
* Callback for customizing a {@link ProjectDescription}. Invoked with an {@link Ordered
* order} of {@code 0} by default, considering overriding {@link #getOrder()} to customize
* this behaviour.
*
* @author Stephane Nicoll
*/
@FunctionalInterface
public interface ProjectDescriptionCustomizer extends Ordered {
void customize(ProjectDescription description);
@Override
default int getOrder() {
return 0;
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2012-2019 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.project;
import java.io.IOException;
import java.nio.file.Path;
/**
* A factory of project directory.
*
* @author Stephane Nicoll
*/
@FunctionalInterface
public interface ProjectDirectoryFactory {
/**
* Create a dedicated project directory for the specified
* {@link ResolvedProjectDescription}.
* @param description the description of a project to generate
* @return a dedicated existing directory
* @throws IOException if creating the directory failed
*/
Path createProjectDirectory(ResolvedProjectDescription description)
throws IOException;
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-2019 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.project;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Configuration;
/**
* Specialization of {@link Configuration} for configuration of project generation.
*
* @author Andy Wilkinson
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface ProjectGenerationConfiguration {
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2012-2019 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.project;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* Provide configuration and infrastructure to generate a project.
*
* @author Stephane Nicoll
*/
public class ProjectGenerationContext extends AnnotationConfigApplicationContext {
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2012-2019 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.project;
/**
* Thrown when a project generation failure occurs.
*
* @author Stephane Nicoll
*/
@SuppressWarnings("serial")
public class ProjectGenerationException extends RuntimeException {
public ProjectGenerationException(String message) {
super(message);
}
public ProjectGenerationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2012-2019 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.project;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotationMetadata;
/**
* Main entry point for project generation.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
public class ProjectGenerator {
private final Consumer<ProjectGenerationContext> projectGenerationContext;
/**
* Create an instance with a customizer for the project generator application context.
* @param projectGenerationContext a consumer of the project generation context before
* it is refreshed.
*/
public ProjectGenerator(Consumer<ProjectGenerationContext> projectGenerationContext) {
this.projectGenerationContext = projectGenerationContext;
}
/**
* Generate project assets using the specified {@link ProjectAssetGenerator}.
* @param description the description of the project to generate
* @param projectAssetGenerator the {@link ProjectAssetGenerator} to invoke
* @param <T> the type that gathers the project assets
* @return the generated content
* @throws ProjectGenerationException if an error occurs while generating the project
*/
public <T> T generate(ProjectDescription description,
ProjectAssetGenerator<T> projectAssetGenerator)
throws ProjectGenerationException {
try (ProjectGenerationContext context = new ProjectGenerationContext()) {
context.registerBean(ResolvedProjectDescription.class,
resolve(description, context));
context.register(CoreConfiguration.class);
this.projectGenerationContext.accept(context);
context.refresh();
try {
return projectAssetGenerator.generate(context);
}
catch (IOException ex) {
throw new ProjectGenerationException("Failed to generate project", ex);
}
}
}
private Supplier<ResolvedProjectDescription> resolve(ProjectDescription description,
ProjectGenerationContext context) {
return () -> {
context.getBeanProvider(ProjectDescriptionCustomizer.class).orderedStream()
.forEach((customizer) -> customizer.customize(description));
return new ResolvedProjectDescription(description);
};
}
/**
* Configuration used to bootstrap the application context used for project
* generation.
*/
@Configuration
@Import(ProjectGenerationImportSelector.class)
static class CoreConfiguration {
}
/**
* {@link ImportSelector} for loading classes configured in {@code spring.factories}
* using the
* {@code io.spring.initializr.generator.project.ProjectGenerationConfiguration} key.
*/
static class ProjectGenerationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> factories = SpringFactoriesLoader.loadFactoryNames(
ProjectGenerationConfiguration.class, getClass().getClassLoader());
return factories.toArray(new String[0]);
}
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright 2012-2019 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.project;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import io.spring.initializr.generator.buildsystem.BuildSystem;
import io.spring.initializr.generator.buildsystem.Dependency;
import io.spring.initializr.generator.language.Language;
import io.spring.initializr.generator.packaging.Packaging;
import io.spring.initializr.generator.version.Version;
import org.springframework.util.StringUtils;
/**
* An immutable description of a project that is being generated.
*
* @author Madhura Bhave
*/
public final class ResolvedProjectDescription {
private final Map<String, Dependency> requestedDependencies;
private final Version platformVersion;
private final BuildSystem buildSystem;
private final Packaging packaging;
private final Language language;
private final String groupId;
private final String artifactId;
private final String name;
private final String description;
private final String applicationName;
private final String packageName;
private final String baseDirectory;
public ResolvedProjectDescription(ProjectDescription description) {
this.platformVersion = description.getPlatformVersion();
this.buildSystem = description.getBuildSystem();
this.packaging = description.getPackaging();
this.language = description.getLanguage();
this.groupId = description.getGroupId();
this.artifactId = description.getArtifactId();
this.name = description.getName();
this.description = description.getDescription();
this.applicationName = description.getApplicationName();
this.packageName = getPackageName(description);
this.baseDirectory = description.getBaseDirectory();
Map<String, Dependency> requestedDependencies = new LinkedHashMap<>(
description.getRequestedDependencies());
this.requestedDependencies = Collections.unmodifiableMap(requestedDependencies);
}
private String getPackageName(ProjectDescription description) {
if (StringUtils.hasText(description.getPackageName())) {
return description.getPackageName();
}
if (StringUtils.hasText(description.getGroupId())
&& StringUtils.hasText(description.getArtifactId())) {
return description.getGroupId() + "." + description.getArtifactId();
}
return null;
}
public Map<String, Dependency> getRequestedDependencies() {
return this.requestedDependencies;
}
public Version getPlatformVersion() {
return this.platformVersion;
}
public BuildSystem getBuildSystem() {
return this.buildSystem;
}
public Packaging getPackaging() {
return this.packaging;
}
public Language getLanguage() {
return this.language;
}
public String getGroupId() {
return this.groupId;
}
public String getArtifactId() {
return this.artifactId;
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public String getApplicationName() {
return this.applicationName;
}
public String getPackageName() {
return this.packageName;
}
public String getBaseDirectory() {
return this.baseDirectory;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2012-2019 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.project.contributor;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Predicate;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.FileCopyUtils;
/**
* A {@link ProjectContributor} that contributes all of the resources found beneath a root
* location to a generated project.
*
* @author Andy Wilkinson
* @see PathMatchingResourcePatternResolver
*/
public class MultipleResourcesProjectContributor implements ProjectContributor {
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
private final String rootResource;
private final Predicate<String> executable;
public MultipleResourcesProjectContributor(String rootResource) {
this(rootResource, (filename) -> false);
}
public MultipleResourcesProjectContributor(String rootResource,
Predicate<String> executable) {
this.rootResource = rootResource;
this.executable = executable;
}
@Override
public void contribute(Path projectRoot) throws IOException {
Resource root = this.resolver.getResource(this.rootResource);
Resource[] resources = this.resolver.getResources(this.rootResource + "/**");
for (Resource resource : resources) {
String filename = resource.getURI().toString()
.substring(root.getURI().toString().length() + 1);
if (resource.isReadable()) {
Path output = projectRoot.resolve(filename);
Files.createDirectories(output.getParent());
Files.createFile(output);
FileCopyUtils.copy(resource.getInputStream(),
Files.newOutputStream(output));
// TODO Set executable using NIO
output.toFile().setExecutable(this.executable.test(filename));
}
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2019 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.project.contributor;
import java.io.IOException;
import java.nio.file.Path;
import org.springframework.core.Ordered;
/**
* A callback for contributing on a generated project. Invoked with an {@link Ordered
* order} of {@code 0} by default, considering overriding {@link #getOrder()} to customize
* this behaviour.
*
* @author Andy Wilkinson
*/
@FunctionalInterface
public interface ProjectContributor extends Ordered {
/**
* Contribute additional resources to the project in the specified root directory.
* @param projectRoot the root directory of the project
* @throws IOException if contributing a resource failed
*/
void contribute(Path projectRoot) throws IOException;
@Override
default int getOrder() {
return 0;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2012-2019 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.project.contributor;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.FileCopyUtils;
/**
* {@link ProjectContributor} that contributes a single file, identified by a resource
* pattern, to a generated project.
*
* @author Andy Wilkinson
* @see PathMatchingResourcePatternResolver
*/
public class SingleResourceProjectContributor implements ProjectContributor {
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
private final String filename;
private final String resourcePattern;
public SingleResourceProjectContributor(String filename, String resourcePattern) {
this.filename = filename;
this.resourcePattern = resourcePattern;
}
@Override
public void contribute(Path projectRoot) throws IOException {
Path output = projectRoot.resolve(this.filename);
if (!Files.exists(output)) {
Files.createDirectories(output.getParent());
Files.createFile(output);
}
Resource resource = this.resolver.getResource(this.resourcePattern);
FileCopyUtils.copy(resource.getInputStream(),
Files.newOutputStream(output, StandardOpenOption.APPEND));
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Project contribution abstraction.
*/
package io.spring.initializr.generator.project.contributor;

View File

@ -0,0 +1,22 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Project generation support classes.
*
* @see io.spring.initializr.generator.project.ProjectGenerator
*/
package io.spring.initializr.generator.project;

View File

@ -0,0 +1,82 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.buildsystem.gradle.GradleBuildSystem;
import io.spring.initializr.generator.buildsystem.maven.MavenBuildSystem;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConditionalOnBuildSystem}.
*
* @author Stephane Nicoll
*/
class ConditionalOnBuildSystemTests {
private final ProjectAssetTester projectTester = new ProjectAssetTester()
.withConfiguration(BuildSystemTestConfiguration.class);
@Test
void outcomeWithMavenBuildSystem() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setBuildSystem(new MavenBuildSystem());
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("testMaven");
}
@Test
void outcomeWithGradleBuildSystem() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setBuildSystem(new GradleBuildSystem());
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("testGradle");
}
private String outcomeFor(ProjectDescription projectDescription) {
return this.projectTester.generate(projectDescription,
(projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class))
.hasSize(1);
return projectGenerationContext.getBean(String.class);
});
}
@Configuration
static class BuildSystemTestConfiguration {
@Bean
@ConditionalOnBuildSystem("gradle")
public String gradle() {
return "testGradle";
}
@Bean
@ConditionalOnBuildSystem("maven")
public String maven() {
return "testMaven";
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.language.groovy.GroovyLanguage;
import io.spring.initializr.generator.language.java.JavaLanguage;
import io.spring.initializr.generator.language.kotlin.KotlinLanguage;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConditionalOnLanguage}.
*
* @author Stephane Nicoll
*/
class ConditionalOnLanguageTests {
private final ProjectAssetTester projectTester = new ProjectAssetTester()
.withConfiguration(LanguageTestConfiguration.class);
@Test
void outcomeWithJavaLanguage() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setLanguage(new JavaLanguage());
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("testJava");
}
@Test
void outcomeWithGroovyBuildSystem() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setLanguage(new GroovyLanguage());
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("testGroovy");
}
@Test
void outcomeWithNoMatch() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setLanguage(new KotlinLanguage());
this.projectTester.generate(projectDescription, (projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class)).isEmpty();
return null;
});
}
@Test
void outcomeWithNoAvailableLanguage() {
ProjectDescription projectDescription = new ProjectDescription();
this.projectTester.generate(projectDescription, (projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class)).isEmpty();
return null;
});
}
private String outcomeFor(ProjectDescription projectDescription) {
return this.projectTester.generate(projectDescription,
(projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class))
.hasSize(1);
return projectGenerationContext.getBean(String.class);
});
}
@Configuration
static class LanguageTestConfiguration {
@Bean
@ConditionalOnLanguage("java")
public String java() {
return "testJava";
}
@Bean
@ConditionalOnLanguage("groovy")
public String groovy() {
return "testGroovy";
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.packaging.jar.JarPackaging;
import io.spring.initializr.generator.packaging.war.WarPackaging;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConditionalOnPackaging}.
*
* @author Stephane Nicoll
*/
class ConditionalOnPackagingTests {
private final ProjectAssetTester projectTester = new ProjectAssetTester()
.withConfiguration(PackagingTestConfiguration.class);
@Test
void outcomeWithJarPackaging() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setPackaging(new JarPackaging());
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("testJar");
}
@Test
void outcomeWithWarPackaging() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setPackaging(new WarPackaging());
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("testWar");
}
@Test
void outcomeWithNoAvailablePackaging() {
ProjectDescription projectDescription = new ProjectDescription();
this.projectTester.generate(projectDescription, (projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class)).isEmpty();
return null;
});
}
private String outcomeFor(ProjectDescription projectDescription) {
return this.projectTester.generate(projectDescription,
(projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class))
.hasSize(1);
return projectGenerationContext.getBean(String.class);
});
}
@Configuration
static class PackagingTestConfiguration {
@Bean
@ConditionalOnPackaging("jar")
public String jar() {
return "testJar";
}
@Bean
@ConditionalOnPackaging("war")
public String war() {
return "testWar";
}
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import io.spring.initializr.generator.version.Version;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConditionalOnPlatformVersion}.
*
* @author Stephane Nicoll
*/
class ConditionalOnPlatformVersionTests {
private final ProjectAssetTester projectTester = new ProjectAssetTester()
.withConfiguration(PlatformVersionTestConfiguration.class);
@Test
void outcomeWithMatchingRange() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setPlatformVersion(Version.parse("1.2.0.RELEASE"));
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("one");
}
@Test
void outcomeWithMatchingOpenRange() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setPlatformVersion(Version.parse("2.0.1.RELEASE"));
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("two");
}
@Test
void outcomeWithMatchingStartOfOpenRange() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setPlatformVersion(Version.parse("2.0.0.M1"));
String bean = outcomeFor(projectDescription);
assertThat(bean).isEqualTo("two");
}
@Test
void outcomeWithNoMatch() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.setPlatformVersion(Version.parse("0.1.0"));
this.projectTester.generate(projectDescription, (projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class)).isEmpty();
return null;
});
}
@Test
void outcomeWithNoAvailablePlatformVersion() {
ProjectDescription projectDescription = new ProjectDescription();
this.projectTester.generate(projectDescription, (projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class)).isEmpty();
return null;
});
}
private String outcomeFor(ProjectDescription projectDescription) {
return this.projectTester.generate(projectDescription,
(projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class))
.hasSize(1);
return projectGenerationContext.getBean(String.class);
});
}
@Configuration
static class PlatformVersionTestConfiguration {
@Bean
@ConditionalOnPlatformVersion("[1.0.0.RELEASE, 2.0.0.M1)")
public String first() {
return "one";
}
@Bean
@ConditionalOnPlatformVersion("2.0.0.M1")
public String second() {
return "two";
}
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2012-2019 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.condition;
import io.spring.initializr.generator.buildsystem.Dependency;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.test.project.ProjectAssetTester;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ConditionalOnRequestedDependency}.
*
* @author Stephane Nicoll
*/
class ConditionalOnRequestedDependencyTests {
private final ProjectAssetTester projectTester = new ProjectAssetTester()
.withConfiguration(RequestedDependencyTestConfiguration.class);
@Test
void outcomeWithMatchingDependency() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.addDependency("web", mock(Dependency.class));
String bean = this.projectTester.generate(projectDescription,
(projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class))
.hasSize(1);
return projectGenerationContext.getBean(String.class);
});
assertThat(bean).isEqualTo("webDependency");
}
@Test
void outcomeWithNoMatch() {
ProjectDescription projectDescription = new ProjectDescription();
projectDescription.addDependency("another", mock(Dependency.class));
this.projectTester.generate(projectDescription, (projectGenerationContext) -> {
assertThat(projectGenerationContext.getBeansOfType(String.class)).isEmpty();
return null;
});
}
@Configuration
static class RequestedDependencyTestConfiguration {
@Bean
@ConditionalOnRequestedDependency("web")
public String webActive() {
return "webDependency";
}
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright 2012-2019 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.project;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
import io.spring.initializr.generator.buildsystem.maven.MavenBuildSystem;
import io.spring.initializr.generator.project.contributor.ProjectContributor;
import io.spring.initializr.generator.test.project.ProjectGeneratorTester;
import io.spring.initializr.generator.version.Version;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ProjectGenerator}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
class ProjectGeneratorTests {
private final ProjectGeneratorTester projectTester = new ProjectGeneratorTester()
.withDescriptionCustomizer((description) -> {
description.setBuildSystem(new MavenBuildSystem());
description.setPlatformVersion(Version.parse("2.1.0.RELEASE"));
});
@Test
void generateInvokedProcessor() {
ProjectDescription description = new ProjectDescription();
description.setBuildSystem(new MavenBuildSystem());
Version platformVersion = Version.parse("2.1.0.RELEASE");
description.setPackageName("com.example.test");
ResolvedProjectDescription resolvedProjectDescription = this.projectTester
.generate(description,
(projectGenerationContext) -> projectGenerationContext
.getBean(ResolvedProjectDescription.class));
assertThat(resolvedProjectDescription.getPlatformVersion())
.isEqualTo(platformVersion);
assertThat(resolvedProjectDescription.getPackageName())
.isEqualTo("com.example.test");
}
@Test
void generateInvokesCustomizers() {
ProjectGeneratorTester tester = this.projectTester
.withContextInitializer((context) -> {
context.registerBean("customizer1",
TestProjectDescriptionCustomizer.class,
() -> new TestProjectDescriptionCustomizer(5,
(description) -> description.setName("Test")));
context.registerBean("customizer2",
TestProjectDescriptionCustomizer.class,
() -> new TestProjectDescriptionCustomizer(3,
(description) -> {
description.setName("First");
description.setGroupId("com.acme");
}));
});
ProjectDescription description = new ProjectDescription();
description.setGroupId("com.example.demo");
description.setName("Original");
ResolvedProjectDescription resolvedProjectDescription = tester.generate(
description, (projectGenerationContext) -> projectGenerationContext
.getBean(ResolvedProjectDescription.class));
assertThat(resolvedProjectDescription.getGroupId()).isEqualTo("com.acme");
assertThat(resolvedProjectDescription.getName()).isEqualTo("Test");
}
@Test
void generateInvokeProjectContributors(@TempDir Path directory) {
ProjectGeneratorTester tester = this.projectTester.withDirectory(directory)
.withContextInitializer((context) -> {
context.registerBean("contributor1", ProjectContributor.class,
() -> (projectDirectory) -> Files
.createFile(projectDirectory.resolve("test.text")));
context.registerBean("contributor2", ProjectContributor.class,
() -> (projectDirectory) -> {
Path subDir = projectDirectory.resolve("src/main/test");
Files.createDirectories(subDir);
Files.createFile(subDir.resolve("Test.src"));
});
});
List<String> relativePaths = tester.generate(new ProjectDescription())
.getRelativePathsOfProjectFiles();
assertThat(relativePaths).containsOnly("test.text", "src/main/test/Test.src");
}
private static class TestProjectDescriptionCustomizer
implements ProjectDescriptionCustomizer {
private final Integer order;
private final Consumer<ProjectDescription> projectDescription;
TestProjectDescriptionCustomizer(Integer order,
Consumer<ProjectDescription> projectDescription) {
this.order = order;
this.projectDescription = projectDescription;
}
@Override
public void customize(ProjectDescription description) {
this.projectDescription.accept(description);
}
@Override
public int getOrder() {
return this.order;
}
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright 2012-2019 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.test.project;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import io.spring.initializr.generator.io.IndentingWriterFactory;
import io.spring.initializr.generator.io.SimpleIndentStrategy;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.project.ProjectDirectoryFactory;
import io.spring.initializr.generator.project.ProjectGenerationContext;
import io.spring.initializr.generator.project.ProjectGenerationException;
/**
* Base tester for project generation.
*
* @author Stephane Nicoll
*/
public abstract class AbstractProjectGenerationTester<SELF extends AbstractProjectGenerationTester<SELF>> {
private final Map<Class<?>, Supplier<?>> beanDefinitions;
private final Consumer<ProjectGenerationContext> contextInitializer;
private final Consumer<ProjectDescription> descriptionCustomizer;
protected AbstractProjectGenerationTester(
Map<Class<?>, Supplier<?>> beanDefinitions) {
this(beanDefinitions, defaultContextInitializer(),
defaultDescriptionCustomizer());
}
protected AbstractProjectGenerationTester(Map<Class<?>, Supplier<?>> beanDefinitions,
Consumer<ProjectGenerationContext> contextInitializer,
Consumer<ProjectDescription> descriptionCustomizer) {
this.beanDefinitions = new LinkedHashMap<>(beanDefinitions);
this.descriptionCustomizer = descriptionCustomizer;
this.contextInitializer = contextInitializer;
}
protected abstract SELF newInstance(Map<Class<?>, Supplier<?>> beanDefinitions,
Consumer<ProjectGenerationContext> contextInitializer,
Consumer<ProjectDescription> descriptionCustomizer);
public <T> SELF withBean(Class<T> beanType, Supplier<T> beanDefinition) {
LinkedHashMap<Class<?>, Supplier<?>> beans = new LinkedHashMap<>(
this.beanDefinitions);
beans.put(beanType, beanDefinition);
return newInstance(beans, this.contextInitializer, this.descriptionCustomizer);
}
public SELF withDirectory(Path directory) {
return withBean(ProjectDirectoryFactory.class,
() -> (description) -> Files.createTempDirectory(directory, "project-"));
}
public SELF withIndentingWriterFactory() {
return withBean(IndentingWriterFactory.class,
() -> IndentingWriterFactory.create(new SimpleIndentStrategy(" ")));
}
public SELF withContextInitializer(Consumer<ProjectGenerationContext> context) {
return newInstance(this.beanDefinitions, this.contextInitializer.andThen(context),
this.descriptionCustomizer);
}
public SELF withDescriptionCustomizer(Consumer<ProjectDescription> description) {
return newInstance(this.beanDefinitions, this.contextInitializer,
this.descriptionCustomizer.andThen(description));
}
protected static Consumer<ProjectGenerationContext> defaultContextInitializer() {
return (context) -> {
};
}
protected static Consumer<ProjectDescription> defaultDescriptionCustomizer() {
return (projectDescription) -> {
if (projectDescription.getGroupId() == null) {
projectDescription.setGroupId("com.example");
}
if (projectDescription.getArtifactId() == null) {
projectDescription.setArtifactId("demo");
}
if (projectDescription.getApplicationName() == null) {
projectDescription.setApplicationName("DemoApplication");
}
};
}
protected <T> T invokeProjectGeneration(ProjectDescription description,
ProjectGenerationInvoker<T> invoker) {
this.descriptionCustomizer.accept(description);
try {
return invoker.generate(beansConfigurer().andThen(this.contextInitializer));
}
catch (IOException ex) {
throw new ProjectGenerationException("Failed to generated project", ex);
}
}
private Consumer<ProjectGenerationContext> beansConfigurer() {
return (context) -> {
this.beanDefinitions.forEach(
(type, definition) -> register(context, type, definition.get()));
};
}
// Restore proper generic signature to make sure the context resolve the bean properly
private <T> void register(ProjectGenerationContext context, Class<T> type,
Object instance) {
T bean = type.cast(instance);
context.registerBean(type, () -> bean);
}
protected interface ProjectGenerationInvoker<T> {
T generate(Consumer<ProjectGenerationContext> contextInitializer)
throws IOException;
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright 2012-2019 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.test.project;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.spring.initializr.generator.project.ProjectAssetGenerator;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.project.ProjectDirectoryFactory;
import io.spring.initializr.generator.project.ProjectGenerationContext;
import io.spring.initializr.generator.project.ResolvedProjectDescription;
import io.spring.initializr.generator.project.contributor.ProjectContributor;
/**
* A tester for project asset that does not detect available {@link ProjectContributor
* contributors}. By default, no contributor is available and can be added using a
* {@link #withConfiguration(Class[]) configuration class} or a
* {@link #withContextInitializer(Consumer) customization of the project generation
* context}.
*
* @author Stephane Nicoll
*/
public class ProjectAssetTester
extends AbstractProjectGenerationTester<ProjectAssetTester> {
public ProjectAssetTester() {
super(Collections.emptyMap());
}
private ProjectAssetTester(Map<Class<?>, Supplier<?>> beanDefinitions,
Consumer<ProjectGenerationContext> contextInitializer,
Consumer<ProjectDescription> descriptionCustomizer) {
super(beanDefinitions, contextInitializer, descriptionCustomizer);
}
@Override
protected ProjectAssetTester newInstance(Map<Class<?>, Supplier<?>> beanDefinitions,
Consumer<ProjectGenerationContext> contextInitializer,
Consumer<ProjectDescription> descriptionCustomizer) {
return new ProjectAssetTester(beanDefinitions, contextInitializer,
descriptionCustomizer);
}
public ProjectAssetTester withConfiguration(Class<?>... configurationClasses) {
return withContextInitializer(
(context) -> context.register(configurationClasses));
}
/**
* Generate a project asset using the specified {@link ProjectAssetGenerator}.
* @param description the description of the project to generate
* @param projectAssetGenerator the {@link ProjectAssetGenerator} to invoke
* @param <T> the project asset type
* @return the project asset
* @see #withConfiguration(Class[])
*/
public <T> T generate(ProjectDescription description,
ProjectAssetGenerator<T> projectAssetGenerator) {
return invokeProjectGeneration(description, (contextInitializer) -> {
try (ProjectGenerationContext context = new ProjectGenerationContext()) {
ResolvedProjectDescription resolvedProjectDescription = new ResolvedProjectDescription(
description);
context.registerBean(ResolvedProjectDescription.class,
() -> resolvedProjectDescription);
contextInitializer.accept(context);
context.refresh();
return projectAssetGenerator.generate(context);
}
});
}
/**
* Generate a project structure using only explicitly configured
* {@link ProjectContributor contributors}.
* @param description the description of the project to generateProject
* @return the {@link ProjectStructure} of the generated project
* @see #withConfiguration(Class[])
*/
public ProjectStructure generate(ProjectDescription description) {
return generate(description, runAllAvailableContributors());
}
private ProjectAssetGenerator<ProjectStructure> runAllAvailableContributors() {
return (context) -> {
Path projectDirectory = context.getBean(ProjectDirectoryFactory.class)
.createProjectDirectory(
context.getBean(ResolvedProjectDescription.class));
List<ProjectContributor> projectContributors = context
.getBeanProvider(ProjectContributor.class).orderedStream()
.collect(Collectors.toList());
for (ProjectContributor projectContributor : projectContributors) {
projectContributor.contribute(projectDirectory);
}
return new ProjectStructure(projectDirectory);
};
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2012-2019 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.test.project;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import io.spring.initializr.generator.io.IndentingWriterFactory;
import io.spring.initializr.generator.io.SimpleIndentStrategy;
import io.spring.initializr.generator.project.DefaultProjectAssetGenerator;
import io.spring.initializr.generator.project.ProjectAssetGenerator;
import io.spring.initializr.generator.project.ProjectDescription;
import io.spring.initializr.generator.project.ProjectGenerationContext;
import io.spring.initializr.generator.project.ProjectGenerator;
/**
* A tester class for {@link ProjectGenerator}.
*
* @author Stephane Nicoll
*/
public class ProjectGeneratorTester
extends AbstractProjectGenerationTester<ProjectGeneratorTester> {
private ProjectGeneratorTester(Map<Class<?>, Supplier<?>> beanDefinitions,
Consumer<ProjectGenerationContext> contextInitializer,
Consumer<ProjectDescription> descriptionCustomizer) {
super(beanDefinitions, contextInitializer, descriptionCustomizer);
}
public ProjectGeneratorTester() {
super(defaultBeans());
}
private static Map<Class<?>, Supplier<?>> defaultBeans() {
return Collections.singletonMap(IndentingWriterFactory.class,
() -> IndentingWriterFactory.create(new SimpleIndentStrategy(" ")));
}
@Override
protected ProjectGeneratorTester newInstance(
Map<Class<?>, Supplier<?>> beanDefinitions,
Consumer<ProjectGenerationContext> contextInitializer,
Consumer<ProjectDescription> descriptionCustomizer) {
return new ProjectGeneratorTester(beanDefinitions, contextInitializer,
descriptionCustomizer);
}
public ProjectStructure generate(ProjectDescription description) {
return invokeProjectGeneration(description, (contextInitializer) -> {
Path directory = new ProjectGenerator(contextInitializer)
.generate(description, new DefaultProjectAssetGenerator());
return new ProjectStructure(directory);
});
}
public <T> T generate(ProjectDescription description,
ProjectAssetGenerator<T> projectAssetGenerator) {
return invokeProjectGeneration(description,
(contextInitializer) -> new ProjectGenerator(contextInitializer)
.generate(description, projectAssetGenerator));
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2012-2019 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.test.project;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
/**
* Test helper to assert content of a generated project structure.
*
* @author Stephane Nicoll
*/
public class ProjectStructure {
private final Path projectDirectory;
/**
* Create an instance based on the specified project {@link Path directory}.
* @param projectDirectory the project's root directory
*/
public ProjectStructure(Path projectDirectory) {
this.projectDirectory = projectDirectory;
}
/**
* Return the project directory.
* @return the project directory
*/
public Path getProjectDirectory() {
return this.projectDirectory;
}
/**
* Resolve a {@link Path} relative to the project directory
* @param other the path string to resolve against the root of the project structure
* @return the resulting path
* @see Path#resolve(String)
*/
public Path resolve(String other) {
return this.projectDirectory.resolve(other);
}
/**
* Return the relative paths of all files.
* @return the relative path of all files
*/
public List<String> getRelativePathsOfProjectFiles() {
List<String> relativePaths = new ArrayList<>();
try {
Files.walkFileTree(this.projectDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
relativePaths.add(ProjectStructure.this.projectDirectory
.relativize(file).toString());
return FileVisitResult.CONTINUE;
}
});
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
return relativePaths;
}
}