diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinCompilationUnit.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinCompilationUnit.java new file mode 100644 index 00000000..19389110 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinCompilationUnit.java @@ -0,0 +1,50 @@ +/* + * 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.language.kotlin; + +import java.util.ArrayList; +import java.util.List; + +import io.spring.initializr.generator.language.CompilationUnit; + +/** + * A Kotlin-specific {@link CompilationUnit}. + * + * @author Stephane Nicoll + */ +public class KotlinCompilationUnit extends CompilationUnit { + + private final List topLevelFunctions = new ArrayList<>(); + + KotlinCompilationUnit(String packageName, String name) { + super(packageName, name); + } + + @Override + protected KotlinTypeDeclaration doCreateTypeDeclaration(String name) { + return new KotlinTypeDeclaration(name); + } + + public void addTopLevelFunction(KotlinFunctionDeclaration function) { + this.topLevelFunctions.add(function); + } + + public List getTopLevelFunctions() { + return this.topLevelFunctions; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinExpression.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinExpression.java new file mode 100644 index 00000000..2ffb5dfa --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinExpression.java @@ -0,0 +1,26 @@ +/* + * 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.language.kotlin; + +/** + * A Kotlin expression. + * + * @author Stephane Nicoll + */ +public class KotlinExpression { + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinExpressionStatement.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinExpressionStatement.java new file mode 100644 index 00000000..2bc0c046 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinExpressionStatement.java @@ -0,0 +1,36 @@ +/* + * 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.language.kotlin; + +/** + * A statement that contains a single expression. + * + * @author Stephane Nicoll + */ +public class KotlinExpressionStatement extends KotlinStatement { + + private final KotlinExpression expression; + + public KotlinExpressionStatement(KotlinExpression expression) { + this.expression = expression; + } + + public KotlinExpression getExpression() { + return this.expression; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinFunctionDeclaration.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinFunctionDeclaration.java new file mode 100644 index 00000000..04214c40 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinFunctionDeclaration.java @@ -0,0 +1,130 @@ +/* + * 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.language.kotlin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.spring.initializr.generator.language.Annotatable; +import io.spring.initializr.generator.language.Annotation; +import io.spring.initializr.generator.language.Parameter; + +/** + * Declaration of a function written in Kotlin. + * + * @author Stephane Nicoll + */ +public final class KotlinFunctionDeclaration implements Annotatable { + + private final List annotations = new ArrayList<>(); + + private final String name; + + private final String returnType; + + private final List modifiers; + + private final List parameters; + + private final List statements; + + private KotlinFunctionDeclaration(String name, String returnType, + List modifiers, List parameters, + List statements) { + this.name = name; + this.returnType = returnType; + this.modifiers = modifiers; + this.parameters = parameters; + this.statements = statements; + } + + public static Builder function(String name) { + return new Builder(name); + } + + String getName() { + return this.name; + } + + String getReturnType() { + return this.returnType; + } + + List getParameters() { + return this.parameters; + } + + List getModifiers() { + return this.modifiers; + } + + public List getStatements() { + return this.statements; + } + + @Override + public void annotate(Annotation annotation) { + this.annotations.add(annotation); + } + + @Override + public List getAnnotations() { + return Collections.unmodifiableList(this.annotations); + } + + /** + * Builder for creating a {@link KotlinFunctionDeclaration}. + */ + public static final class Builder { + + private final String name; + + private List parameters = new ArrayList<>(); + + private List modifiers = new ArrayList<>(); + + private String returnType; + + private Builder(String name) { + this.name = name; + } + + public Builder modifiers(KotlinModifier... modifiers) { + this.modifiers = Arrays.asList(modifiers); + return this; + } + + public Builder returning(String returnType) { + this.returnType = returnType; + return this; + } + + public Builder parameters(Parameter... parameters) { + this.parameters = Arrays.asList(parameters); + return this; + } + + public KotlinFunctionDeclaration body(KotlinStatement... statements) { + return new KotlinFunctionDeclaration(this.name, this.returnType, + this.modifiers, this.parameters, Arrays.asList(statements)); + } + + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinFunctionInvocation.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinFunctionInvocation.java new file mode 100644 index 00000000..8789ebd4 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinFunctionInvocation.java @@ -0,0 +1,53 @@ +/* + * 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.language.kotlin; + +import java.util.Arrays; +import java.util.List; + +/** + * An invocation of a function. + * + * @author Stephane Nicoll + */ +public class KotlinFunctionInvocation extends KotlinExpression { + + private final String target; + + private final String name; + + private final List arguments; + + public KotlinFunctionInvocation(String target, String name, String... arguments) { + this.target = target; + this.name = name; + this.arguments = Arrays.asList(arguments); + } + + public String getTarget() { + return this.target; + } + + public String getName() { + return this.name; + } + + public List getArguments() { + return this.arguments; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguage.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguage.java new file mode 100644 index 00000000..899b3272 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguage.java @@ -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.language.kotlin; + +import io.spring.initializr.generator.language.AbstractLanguage; +import io.spring.initializr.generator.language.Language; + +/** + * Kotlin {@link Language}. + * + * @author Stephane Nicoll + */ +public final class KotlinLanguage extends AbstractLanguage { + + /** + * Kotlin {@link Language} identifier. + */ + public static final String ID = "kotlin"; + + public KotlinLanguage() { + this(DEFAULT_JVM_VERSION); + } + + public KotlinLanguage(String jvmVersion) { + super(ID, jvmVersion); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguageFactory.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguageFactory.java new file mode 100644 index 00000000..84723901 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguageFactory.java @@ -0,0 +1,37 @@ +/* + * 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.language.kotlin; + +import io.spring.initializr.generator.language.Language; +import io.spring.initializr.generator.language.LanguageFactory; + +/** + * A {@link LanguageFactory} for Kotlin. + * + * @author Stephane Nicoll + */ +class KotlinLanguageFactory implements LanguageFactory { + + @Override + public Language createLanguage(String id, String jvmVersion) { + if (KotlinLanguage.ID.equals(id)) { + return new KotlinLanguage(jvmVersion); + } + return null; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinModifier.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinModifier.java new file mode 100644 index 00000000..01a97fdb --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinModifier.java @@ -0,0 +1,61 @@ +/* + * 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.language.kotlin; + +/** + * Basic modifiers for Kotlin. + * + * @author Stephane Nicoll + */ +public enum KotlinModifier { + + /** + * Visible to anyone. + */ + PUBLIC, + + /** + * Visible inside that class and any subclass. + */ + PROTECTED, + + /** + * Visible inside this class only. + */ + PRIVATE, + + /** + * Final modifier. + */ + FINAL, + + /** + * Allow to override a member. + */ + OPEN, + + /** + * Declare a member without an implementation (infers open). + */ + ABSTRACT, + + /** + * Override a member. + */ + OVERRIDE + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinReifiedFunctionInvocation.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinReifiedFunctionInvocation.java new file mode 100644 index 00000000..f28218eb --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinReifiedFunctionInvocation.java @@ -0,0 +1,54 @@ +/* + * 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.language.kotlin; + +import java.util.Arrays; +import java.util.List; + +/** + * Invocation of a function with a reified type parameter. + * + * @author Stephane Nicoll + */ +public class KotlinReifiedFunctionInvocation extends KotlinExpression { + + private final String name; + + private final String targetClass; + + private final List arguments; + + public KotlinReifiedFunctionInvocation(String name, String targetClass, + String... arguments) { + this.name = name; + this.targetClass = targetClass; + this.arguments = Arrays.asList(arguments); + } + + public String getName() { + return this.name; + } + + public String getTargetClass() { + return this.targetClass; + } + + public List getArguments() { + return this.arguments; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinReturnStatement.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinReturnStatement.java new file mode 100644 index 00000000..30acc21d --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinReturnStatement.java @@ -0,0 +1,36 @@ +/* + * 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.language.kotlin; + +/** + * A return statement. + * + * @author Andy Wilkinson + */ +public class KotlinReturnStatement extends KotlinStatement { + + private final KotlinExpression expression; + + public KotlinReturnStatement(KotlinExpression expression) { + this.expression = expression; + } + + public KotlinExpression getExpression() { + return this.expression; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCode.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCode.java new file mode 100644 index 00000000..6c253ddf --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCode.java @@ -0,0 +1,33 @@ +/* + * 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.language.kotlin; + +import io.spring.initializr.generator.language.SourceCode; + +/** + * Kotlin {@link SourceCode}. + * + * @author Stephane Nicoll + */ +public class KotlinSourceCode + extends SourceCode { + + public KotlinSourceCode() { + super(KotlinCompilationUnit::new); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java new file mode 100644 index 00000000..99e2e044 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java @@ -0,0 +1,337 @@ +/* + * 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.language.kotlin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.spring.initializr.generator.io.IndentingWriter; +import io.spring.initializr.generator.io.IndentingWriterFactory; +import io.spring.initializr.generator.language.Annotatable; +import io.spring.initializr.generator.language.Annotation; +import io.spring.initializr.generator.language.Parameter; +import io.spring.initializr.generator.language.SourceCode; +import io.spring.initializr.generator.language.SourceCodeWriter; + +/** + * A {@link SourceCodeWriter} that writes {@link SourceCode} in Kotlin. + * + * @author Stephane Nicoll + */ +public class KotlinSourceCodeWriter implements SourceCodeWriter { + + private final IndentingWriterFactory indentingWriterFactory; + + public KotlinSourceCodeWriter(IndentingWriterFactory indentingWriterFactory) { + this.indentingWriterFactory = indentingWriterFactory; + } + + @Override + public void writeTo(Path directory, KotlinSourceCode sourceCode) throws IOException { + if (!Files.exists(directory)) { + Files.createDirectories(directory); + } + for (KotlinCompilationUnit compilationUnit : sourceCode.getCompilationUnits()) { + writeTo(directory, compilationUnit); + } + } + + private void writeTo(Path directory, KotlinCompilationUnit compilationUnit) + throws IOException { + Path output = fileForCompilationUnit(directory, compilationUnit); + Files.createDirectories(output.getParent()); + try (IndentingWriter writer = this.indentingWriterFactory + .createIndentingWriter("kotlin", Files.newBufferedWriter(output))) { + writer.println("package " + compilationUnit.getPackageName()); + writer.println(); + Set imports = determineImports(compilationUnit); + if (!imports.isEmpty()) { + for (String importedType : imports) { + writer.println("import " + importedType); + } + writer.println(); + } + for (KotlinTypeDeclaration type : compilationUnit.getTypeDeclarations()) { + writeAnnotations(writer, type); + writer.print("class " + type.getName()); + if (type.getExtends() != null) { + writer.print(" : " + getUnqualifiedName(type.getExtends()) + "()"); + } + List functionDeclarations = type + .getFunctionDeclarations(); + if (!functionDeclarations.isEmpty()) { + writer.println(" {"); + writer.println(); + writer.indented(() -> { + for (KotlinFunctionDeclaration functionDeclaration : functionDeclarations) { + writeFunction(writer, functionDeclaration); + } + }); + writer.println("}"); + } + else { + writer.println(""); + } + writer.println(""); + } + List topLevelFunctions = compilationUnit + .getTopLevelFunctions(); + if (!topLevelFunctions.isEmpty()) { + for (KotlinFunctionDeclaration topLevelFunction : topLevelFunctions) { + writeFunction(writer, topLevelFunction); + } + } + + } + } + + private void writeFunction(IndentingWriter writer, + KotlinFunctionDeclaration functionDeclaration) { + writeAnnotations(writer, functionDeclaration); + writeMethodModifiers(writer, functionDeclaration); + writer.print("fun "); + writer.print(functionDeclaration.getName() + "("); + List parameters = functionDeclaration.getParameters(); + if (!parameters.isEmpty()) { + writer.print(parameters.stream() + .map((parameter) -> parameter.getName() + ": " + + getUnqualifiedName(parameter.getType())) + .collect(Collectors.joining(", "))); + } + writer.print(")"); + if (functionDeclaration.getReturnType() != null) { + writer.print(": " + getUnqualifiedName(functionDeclaration.getReturnType())); + } + writer.println(" {"); + List statements = functionDeclaration.getStatements(); + writer.indented(() -> { + for (KotlinStatement statement : statements) { + if (statement instanceof KotlinExpressionStatement) { + writeExpression(writer, + ((KotlinExpressionStatement) statement).getExpression()); + } + else if (statement instanceof KotlinReturnStatement) { + writer.print("return "); + writeExpression(writer, + ((KotlinReturnStatement) statement).getExpression()); + } + writer.println(""); + } + }); + writer.println("}"); + writer.println(); + } + + private void writeAnnotations(IndentingWriter writer, Annotatable annotatable) { + for (Annotation annotation : annotatable.getAnnotations()) { + writeAnnotation(writer, annotation); + } + } + + private void writeAnnotation(IndentingWriter writer, Annotation annotation) { + writer.print("@" + getUnqualifiedName(annotation.getName())); + List attributes = annotation.getAttributes(); + if (!attributes.isEmpty()) { + writer.print("("); + if (attributes.size() == 1 && attributes.get(0).getName().equals("value")) { + writer.print(formatAnnotationAttribute(attributes.get(0))); + } + else { + writer.print(attributes.stream() + .map((attribute) -> attribute.getName() + " = " + + formatAnnotationAttribute(attribute)) + .collect(Collectors.joining(", "))); + } + writer.print(")"); + } + writer.println(); + } + + private String formatAnnotationAttribute(Annotation.Attribute attribute) { + List values = attribute.getValues(); + if (attribute.getType().equals(Class.class)) { + return formatValues(values, + (value) -> String.format("%s::class", getUnqualifiedName(value))); + } + if (Enum.class.isAssignableFrom(attribute.getType())) { + return formatValues(values, (value) -> { + String enumValue = value.substring(value.lastIndexOf(".") + 1); + String enumClass = value.substring(0, value.lastIndexOf(".")); + return String.format("%s.%s", getUnqualifiedName(enumClass), enumValue); + }); + } + if (attribute.getType().equals(String.class)) { + return formatValues(values, (value) -> String.format("\"%s\"", value)); + } + return formatValues(values, (value) -> String.format("%s", value)); + } + + private String formatValues(List values, Function formatter) { + String result = values.stream().map(formatter).collect(Collectors.joining(", ")); + return (values.size() > 1) ? "[" + result + "]" : result; + } + + private void writeMethodModifiers(IndentingWriter writer, + KotlinFunctionDeclaration functionDeclaration) { + String modifiers = functionDeclaration.getModifiers().stream() + .filter((entry) -> !entry.equals(KotlinModifier.PUBLIC)).sorted() + .map((entry) -> entry.toString().toLowerCase(Locale.ENGLISH)) + .collect(Collectors.joining(" ")); + if (!modifiers.isEmpty()) { + writer.print(modifiers); + writer.print(" "); + } + } + + private void writeExpression(IndentingWriter writer, KotlinExpression expression) { + if (expression instanceof KotlinFunctionInvocation) { + writeFunctionInvocation(writer, (KotlinFunctionInvocation) expression); + } + else if (expression instanceof KotlinReifiedFunctionInvocation) { + writeReifiedFunctionInvocation(writer, + (KotlinReifiedFunctionInvocation) expression); + } + } + + private void writeFunctionInvocation(IndentingWriter writer, + KotlinFunctionInvocation functionInvocation) { + writer.print(getUnqualifiedName(functionInvocation.getTarget()) + "." + + functionInvocation.getName() + "(" + + String.join(", ", functionInvocation.getArguments()) + ")"); + } + + private void writeReifiedFunctionInvocation(IndentingWriter writer, + KotlinReifiedFunctionInvocation functionInvocation) { + writer.print(getUnqualifiedName(functionInvocation.getName()) + "<" + + getUnqualifiedName(functionInvocation.getTargetClass()) + ">(" + + String.join(", ", functionInvocation.getArguments()) + ")"); + } + + private Path fileForCompilationUnit(Path directory, + KotlinCompilationUnit compilationUnit) { + return directoryForPackage(directory, compilationUnit.getPackageName()) + .resolve(compilationUnit.getName() + ".kt"); + } + + private Path directoryForPackage(Path directory, String packageName) { + return directory.resolve(packageName.replace('.', '/')); + } + + private Set determineImports(KotlinCompilationUnit compilationUnit) { + List imports = new ArrayList<>(); + for (KotlinTypeDeclaration typeDeclaration : compilationUnit + .getTypeDeclarations()) { + if (requiresImport(typeDeclaration.getExtends())) { + imports.add(typeDeclaration.getExtends()); + } + imports.addAll(getRequiredImports(typeDeclaration.getAnnotations(), + this::determineImports)); + typeDeclaration.getFunctionDeclarations() + .forEach((functionDeclaration) -> imports + .addAll(determineFunctionImports(functionDeclaration))); + } + compilationUnit.getTopLevelFunctions().forEach((functionDeclaration) -> imports + .addAll(determineFunctionImports(functionDeclaration))); + Collections.sort(imports); + return new LinkedHashSet<>(imports); + } + + private Set determineFunctionImports( + KotlinFunctionDeclaration functionDeclaration) { + Set imports = new LinkedHashSet<>(); + if (requiresImport(functionDeclaration.getReturnType())) { + imports.add(functionDeclaration.getReturnType()); + } + imports.addAll(getRequiredImports(functionDeclaration.getAnnotations(), + this::determineImports)); + imports.addAll(getRequiredImports(functionDeclaration.getParameters(), + (parameter) -> Collections.singleton(parameter.getType()))); + imports.addAll(getRequiredImports( + getKotlinExpressions(functionDeclaration) + .filter(KotlinFunctionInvocation.class::isInstance) + .map(KotlinFunctionInvocation.class::cast), + (invocation) -> Collections.singleton(invocation.getTarget()))); + imports.addAll(getRequiredImports( + getKotlinExpressions(functionDeclaration) + .filter(KotlinReifiedFunctionInvocation.class::isInstance) + .map(KotlinReifiedFunctionInvocation.class::cast), + (invocation) -> Collections.singleton(invocation.getName()))); + return imports; + } + + private Collection determineImports(Annotation annotation) { + List imports = new ArrayList<>(); + imports.add(annotation.getName()); + annotation.getAttributes().forEach((attribute) -> { + if (attribute.getType() == Class.class) { + imports.addAll(attribute.getValues()); + } + if (Enum.class.isAssignableFrom(attribute.getType())) { + imports.addAll(attribute.getValues().stream() + .map((value) -> value.substring(0, value.lastIndexOf("."))) + .collect(Collectors.toList())); + } + }); + return imports; + } + + private Stream getKotlinExpressions( + KotlinFunctionDeclaration functionDeclaration) { + return functionDeclaration.getStatements().stream() + .filter(KotlinExpressionStatement.class::isInstance) + .map(KotlinExpressionStatement.class::cast) + .map(KotlinExpressionStatement::getExpression); + } + + private List getRequiredImports(List candidates, + Function> mapping) { + return getRequiredImports(candidates.stream(), mapping); + } + + private List getRequiredImports(Stream candidates, + Function> mapping) { + return candidates.map(mapping).flatMap(Collection::stream) + .filter(this::requiresImport).collect(Collectors.toList()); + } + + private String getUnqualifiedName(String name) { + if (!name.contains(".")) { + return name; + } + return name.substring(name.lastIndexOf(".") + 1); + } + + private boolean requiresImport(String name) { + if (name == null || !name.contains(".")) { + return false; + } + String packageName = name.substring(0, name.lastIndexOf('.')); + return !"java.lang".equals(packageName); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinStatement.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinStatement.java new file mode 100644 index 00000000..48d40077 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinStatement.java @@ -0,0 +1,26 @@ +/* + * 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.language.kotlin; + +/** + * A statement in Kotlin. + * + * @author Stephane Nicoll + */ +public class KotlinStatement { + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinTypeDeclaration.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinTypeDeclaration.java new file mode 100644 index 00000000..a9dd43e5 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinTypeDeclaration.java @@ -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.language.kotlin; + +import java.util.ArrayList; +import java.util.List; + +import io.spring.initializr.generator.language.TypeDeclaration; + +/** + * A {@link TypeDeclaration declaration } of a type written in Kotlin. + * + * @author Stephane Nicoll + */ +public class KotlinTypeDeclaration extends TypeDeclaration { + + private final List functionDeclarations = new ArrayList<>(); + + KotlinTypeDeclaration(String name) { + super(name); + } + + public void addFunctionDeclaration(KotlinFunctionDeclaration methodDeclaration) { + this.functionDeclarations.add(methodDeclaration); + } + + public List getFunctionDeclarations() { + return this.functionDeclarations; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/package-info.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/package-info.java new file mode 100644 index 00000000..81ffdabe --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Kotlin language. Provides a + * {@link io.spring.initializr.generator.language.kotlin.KotlinCompilationUnit compilation + * unit} implementation and a write for Kotlin + * {@link io.spring.initializr.generator.language.kotlin.KotlinSourceCode source code}. + */ +package io.spring.initializr.generator.language.kotlin; diff --git a/initializr-generator/src/main/resources/META-INF/spring.factories b/initializr-generator/src/main/resources/META-INF/spring.factories index 321ce681..e2547013 100644 --- a/initializr-generator/src/main/resources/META-INF/spring.factories +++ b/initializr-generator/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ io.spring.initializr.generator.language.LanguageFactory=\ -io.spring.initializr.generator.language.java.JavaLanguageFactory +io.spring.initializr.generator.language.java.JavaLanguageFactory,\ +io.spring.initializr.generator.language.kotlin.KotlinLanguageFactory diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/LanguageTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/LanguageTests.java index a55ba562..53ece0df 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/language/LanguageTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/LanguageTests.java @@ -17,6 +17,7 @@ package io.spring.initializr.generator.language; import io.spring.initializr.generator.language.java.JavaLanguage; +import io.spring.initializr.generator.language.kotlin.KotlinLanguage; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -38,6 +39,15 @@ class LanguageTests { assertThat(java.jvmVersion()).isEqualTo("11"); } + @Test + void kotlinLanguage() { + Language kotlin = Language.forId("kotlin", null); + assertThat(kotlin).isInstanceOf(KotlinLanguage.class); + assertThat(kotlin.id()).isEqualTo("kotlin"); + assertThat(kotlin.toString()).isEqualTo("kotlin"); + assertThat(kotlin.jvmVersion()).isEqualTo("1.8"); + } + @Test void unknownLanguage() { assertThatIllegalStateException() diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java new file mode 100644 index 00000000..9257894e --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java @@ -0,0 +1,238 @@ +/* + * 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.language.kotlin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import io.spring.initializr.generator.io.IndentingWriterFactory; +import io.spring.initializr.generator.language.Annotation; +import io.spring.initializr.generator.language.Parameter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link KotlinSourceCodeWriter}. + * + * @author Stephane Nicoll + */ +class KotlinSourceCodeWriterTests { + + @TempDir + Path directory; + + private final KotlinSourceCodeWriter writer = new KotlinSourceCodeWriter( + IndentingWriterFactory.withDefaultSettings()); + + @Test + void emptyCompilationUnit() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + sourceCode.createCompilationUnit("com.example", "Test"); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", ""); + } + + @Test + void emptyTypeDeclaration() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + compilationUnit.createTypeDeclaration("Test"); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", "class Test", ""); + } + + @Test + void emptyTypeDeclarationWithSuperClass() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.extend("com.example.build.TestParent"); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", + "import com.example.build.TestParent", "", "class Test : TestParent()", + ""); + } + + @Test + void function() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.addFunctionDeclaration(KotlinFunctionDeclaration.function("reverse") + .returning("java.lang.String") + .parameters(new Parameter("java.lang.String", "echo")) + .body(new KotlinReturnStatement( + new KotlinFunctionInvocation("echo", "reversed")))); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", "class Test {", "", + " fun reverse(echo: String): String {", + " return echo.reversed()", " }", "", "}", ""); + } + + @Test + void functionModifiers() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.addFunctionDeclaration(KotlinFunctionDeclaration.function("toString") + .modifiers(KotlinModifier.OVERRIDE, KotlinModifier.PUBLIC, + KotlinModifier.OPEN) + .returning("java.lang.String").body(new KotlinReturnStatement( + new KotlinFunctionInvocation("super", "toString")))); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", "class Test {", "", + " open override fun toString(): String {", + " return super.toString()", " }", "", "}", ""); + } + + @Test + void springBootApplication() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotate(Annotation + .name("org.springframework.boot.autoconfigure.SpringBootApplication")); + compilationUnit.addTopLevelFunction(KotlinFunctionDeclaration.function("main") + .parameters(new Parameter("Array", "args")) + .body(new KotlinExpressionStatement(new KotlinReifiedFunctionInvocation( + "org.springframework.boot.runApplication", "Test", "*args")))); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", + "import org.springframework.boot.autoconfigure.SpringBootApplication", + "import org.springframework.boot.runApplication", "", + "@SpringBootApplication", "class Test", "", + "fun main(args: Array) {", " runApplication(*args)", "}", + ""); + } + + @Test + void annotationWithSimpleAttribute() throws IOException { + List lines = writeClassAnnotation( + Annotation.name("org.springframework.test.TestApplication", + (builder) -> builder.attribute("counter", Integer.class, "42"))); + assertThat(lines).containsExactly("package com.example", "", + "import org.springframework.test.TestApplication", "", + "@TestApplication(counter = 42)", "class Test", ""); + } + + @Test + void annotationWithSimpleStringAttribute() throws IOException { + List lines = writeClassAnnotation( + Annotation.name("org.springframework.test.TestApplication", + (builder) -> builder.attribute("name", String.class, "test"))); + assertThat(lines).containsExactly("package com.example", "", + "import org.springframework.test.TestApplication", "", + "@TestApplication(name = \"test\")", "class Test", ""); + } + + @Test + void annotationWithOnlyValueAttribute() throws IOException { + List lines = writeClassAnnotation( + Annotation.name("org.springframework.test.TestApplication", + (builder) -> builder.attribute("value", String.class, "test"))); + assertThat(lines).containsExactly("package com.example", "", + "import org.springframework.test.TestApplication", "", + "@TestApplication(\"test\")", "class Test", ""); + } + + @Test + void annotationWithSimpleEnumAttribute() throws IOException { + List lines = writeClassAnnotation( + Annotation.name("org.springframework.test.TestApplication", + (builder) -> builder.attribute("unit", Enum.class, + "java.time.temporal.ChronoUnit.SECONDS"))); + assertThat(lines).containsExactly("package com.example", "", + "import java.time.temporal.ChronoUnit", + "import org.springframework.test.TestApplication", "", + "@TestApplication(unit = ChronoUnit.SECONDS)", "class Test", ""); + } + + @Test + void annotationWithClassArrayAttribute() throws IOException { + List lines = writeClassAnnotation( + Annotation.name("org.springframework.test.TestApplication", + (builder) -> builder.attribute("target", Class.class, + "com.example.One", "com.example.Two"))); + assertThat(lines).containsExactly("package com.example", "", + "import com.example.One", "import com.example.Two", + "import org.springframework.test.TestApplication", "", + "@TestApplication(target = [One::class, Two::class])", "class Test", ""); + } + + @Test + void annotationWithSeveralAttributes() throws IOException { + List lines = writeClassAnnotation(Annotation.name( + "org.springframework.test.TestApplication", + (builder) -> builder.attribute("target", Class.class, "com.example.One") + .attribute("unit", ChronoUnit.class, + "java.time.temporal.ChronoUnit.NANOS"))); + assertThat(lines).containsExactly("package com.example", "", + "import com.example.One", "import java.time.temporal.ChronoUnit", + "import org.springframework.test.TestApplication", "", + "@TestApplication(target = One::class, unit = ChronoUnit.NANOS)", + "class Test", ""); + } + + private List writeClassAnnotation(Annotation annotation) throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotate(annotation); + return writeSingleType(sourceCode, "com/example/Test.kt"); + } + + @Test + void functionWithSimpleAnnotation() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + KotlinFunctionDeclaration function = KotlinFunctionDeclaration + .function("something").body(); + function.annotate(Annotation.name("com.example.test.TestAnnotation")); + test.addFunctionDeclaration(function); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", + "import com.example.test.TestAnnotation", "", "class Test {", "", + " @TestAnnotation", " fun something() {", " }", "", "}", ""); + } + + private List writeSingleType(KotlinSourceCode sourceCode, String location) + throws IOException { + Path source = writeSourceCode(sourceCode).resolve(location); + assertThat(source).isRegularFile(); + return Files.readAllLines(source); + } + + private Path writeSourceCode(KotlinSourceCode sourceCode) throws IOException { + Path projectDirectory = Files.createTempDirectory(this.directory, "project-"); + this.writer.writeTo(projectDirectory, sourceCode); + return projectDirectory; + } + +}