From d0d4809ee90531e5dcc415f2ae680f5449547ac4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Feb 2019 15:00:52 +0100 Subject: [PATCH] Add Java language support This commit provides a Java language implementation with a writer that can generate a `.java` source file based on a configurable model. See gh-813 Co-authored-by: Stephane Nicoll --- .../language/java/JavaCompilationUnit.java | 37 +++ .../language/java/JavaExpression.java | 26 ++ .../java/JavaExpressionStatement.java | 36 ++ .../generator/language/java/JavaLanguage.java | 43 +++ .../language/java/JavaLanguageFactory.java | 37 +++ .../language/java/JavaMethodDeclaration.java | 130 ++++++++ .../language/java/JavaMethodInvocation.java | 53 +++ .../language/java/JavaReturnStatement.java | 36 ++ .../language/java/JavaSourceCode.java | 32 ++ .../language/java/JavaSourceCodeWriter.java | 312 ++++++++++++++++++ .../language/java/JavaStatement.java | 26 ++ .../language/java/JavaTypeDeclaration.java | 45 +++ .../generator/language/java/package-info.java | 23 ++ .../main/resources/META-INF/spring.factories | 2 + .../generator/language/LanguageTests.java | 48 +++ .../java/JavaSourceCodeWriterTests.java | 228 +++++++++++++ 16 files changed, 1114 insertions(+) create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaCompilationUnit.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpression.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpressionStatement.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguage.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguageFactory.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodDeclaration.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodInvocation.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaReturnStatement.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCode.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriter.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaStatement.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaTypeDeclaration.java create mode 100644 initializr-generator/src/main/java/io/spring/initializr/generator/language/java/package-info.java create mode 100644 initializr-generator/src/main/resources/META-INF/spring.factories create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/language/LanguageTests.java create mode 100644 initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaCompilationUnit.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaCompilationUnit.java new file mode 100644 index 00000000..35c8cbe5 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaCompilationUnit.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.java; + +import io.spring.initializr.generator.language.CompilationUnit; + +/** + * A Java-specific {@link CompilationUnit}. + * + * @author Andy Wilkinson + */ +public class JavaCompilationUnit extends CompilationUnit { + + JavaCompilationUnit(String packageName, String name) { + super(packageName, name); + } + + @Override + protected JavaTypeDeclaration doCreateTypeDeclaration(String name) { + return new JavaTypeDeclaration(name); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpression.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpression.java new file mode 100644 index 00000000..1c0e64c2 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpression.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.java; + +/** + * A Java expression. + * + * @author Andy Wilkinson + */ +public class JavaExpression { + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpressionStatement.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpressionStatement.java new file mode 100644 index 00000000..af8c98de --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaExpressionStatement.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.java; + +/** + * A statement that contains a single expression. + * + * @author Andy Wilkinson + */ +public class JavaExpressionStatement extends JavaStatement { + + private final JavaExpression expression; + + public JavaExpressionStatement(JavaExpression expression) { + this.expression = expression; + } + + public JavaExpression getExpression() { + return this.expression; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguage.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguage.java new file mode 100644 index 00000000..94abb1a0 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguage.java @@ -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.language.java; + +import io.spring.initializr.generator.language.AbstractLanguage; +import io.spring.initializr.generator.language.Language; + +/** + * Java {@link Language}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + */ +public final class JavaLanguage extends AbstractLanguage { + + /** + * Java {@link Language} identifier. + */ + public static final String ID = "java"; + + public JavaLanguage() { + this(DEFAULT_JVM_VERSION); + } + + public JavaLanguage(String jvmVersion) { + super(ID, jvmVersion); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguageFactory.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguageFactory.java new file mode 100644 index 00000000..62a835c2 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguageFactory.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.java; + +import io.spring.initializr.generator.language.Language; +import io.spring.initializr.generator.language.LanguageFactory; + +/** + * A {@link LanguageFactory} for Java. + * + * @author Andy Wilkinson + */ +class JavaLanguageFactory implements LanguageFactory { + + @Override + public Language createLanguage(String id, String jvmVersion) { + if (JavaLanguage.ID.equals(id)) { + return new JavaLanguage(jvmVersion); + } + return null; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodDeclaration.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodDeclaration.java new file mode 100644 index 00000000..d15b99e9 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodDeclaration.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.java; + +import java.lang.reflect.Modifier; +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 method written in Java. + * + * @author Andy Wilkinson + */ +public final class JavaMethodDeclaration implements Annotatable { + + private final List annotations = new ArrayList<>(); + + private final String name; + + private final String returnType; + + private final int modifiers; + + private final List parameters; + + private final List statements; + + private JavaMethodDeclaration(String name, String returnType, int modifiers, + List parameters, List statements) { + this.name = name; + this.returnType = returnType; + this.modifiers = modifiers; + this.parameters = parameters; + this.statements = statements; + } + + public static Builder method(String name) { + return new Builder(name); + } + + String getName() { + return this.name; + } + + String getReturnType() { + return this.returnType; + } + + List getParameters() { + return this.parameters; + } + + int 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 JavaMethodDeclaration}. + */ + public static final class Builder { + + private final String name; + + private List parameters = new ArrayList<>(); + + private String returnType = "void"; + + private int modifiers = Modifier.PUBLIC; + + private Builder(String name) { + this.name = name; + } + + public Builder modifiers(int modifiers) { + this.modifiers = 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 JavaMethodDeclaration body(JavaStatement... statements) { + return new JavaMethodDeclaration(this.name, this.returnType, this.modifiers, + this.parameters, Arrays.asList(statements)); + } + + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodInvocation.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodInvocation.java new file mode 100644 index 00000000..f7f8505d --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaMethodInvocation.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.java; + +import java.util.Arrays; +import java.util.List; + +/** + * An invocation of a method. + * + * @author Andy Wilkinson + */ +public class JavaMethodInvocation extends JavaExpression { + + private final String target; + + private final String name; + + private final List arguments; + + public JavaMethodInvocation(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/java/JavaReturnStatement.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaReturnStatement.java new file mode 100644 index 00000000..b6fe9b5d --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaReturnStatement.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.java; + +/** + * A return statement. + * + * @author Andy Wilkinson + */ +public class JavaReturnStatement extends JavaStatement { + + private final JavaExpression expression; + + public JavaReturnStatement(JavaExpression expression) { + this.expression = expression; + } + + public JavaExpression getExpression() { + return this.expression; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCode.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCode.java new file mode 100644 index 00000000..2b370ec4 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCode.java @@ -0,0 +1,32 @@ +/* + * 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.java; + +import io.spring.initializr.generator.language.SourceCode; + +/** + * Java {@link SourceCode}. + * + * @author Andy Wilkinson + */ +public class JavaSourceCode extends SourceCode { + + public JavaSourceCode() { + super(JavaCompilationUnit::new); + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriter.java new file mode 100644 index 00000000..a6a87eb7 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriter.java @@ -0,0 +1,312 @@ +/* + * 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.java; + +import java.io.IOException; +import java.lang.reflect.Modifier; +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.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +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 Java. + * + * @author Andy Wilkinson + */ +public class JavaSourceCodeWriter implements SourceCodeWriter { + + private static final Map, String> METHOD_MODIFIERS; + + static { + Map, String> methodModifiers = new LinkedHashMap<>(); + methodModifiers.put(Modifier::isPublic, "public"); + methodModifiers.put(Modifier::isProtected, "protected"); + methodModifiers.put(Modifier::isPrivate, "private"); + methodModifiers.put(Modifier::isAbstract, "abstract"); + methodModifiers.put(Modifier::isStatic, "static"); + methodModifiers.put(Modifier::isFinal, "final"); + methodModifiers.put(Modifier::isSynchronized, "synchronized"); + methodModifiers.put(Modifier::isNative, "native"); + methodModifiers.put(Modifier::isStrict, "strictfp"); + METHOD_MODIFIERS = methodModifiers; + } + + private final IndentingWriterFactory indentingWriterFactory; + + public JavaSourceCodeWriter(IndentingWriterFactory indentingWriterFactory) { + this.indentingWriterFactory = indentingWriterFactory; + } + + @Override + public void writeTo(Path directory, JavaSourceCode sourceCode) throws IOException { + if (!Files.exists(directory)) { + Files.createDirectories(directory); + } + for (JavaCompilationUnit compilationUnit : sourceCode.getCompilationUnits()) { + writeTo(directory, compilationUnit); + } + } + + private void writeTo(Path directory, JavaCompilationUnit compilationUnit) + throws IOException { + Path output = fileForCompilationUnit(directory, compilationUnit); + Files.createDirectories(output.getParent()); + try (IndentingWriter writer = this.indentingWriterFactory + .createIndentingWriter("java", 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 (JavaTypeDeclaration type : compilationUnit.getTypeDeclarations()) { + writeAnnotations(writer, type); + writer.print("public class " + type.getName()); + if (type.getExtends() != null) { + writer.print(" extends " + getUnqualifiedName(type.getExtends())); + } + writer.println(" {"); + writer.println(); + List methodDeclarations = type + .getMethodDeclarations(); + if (!methodDeclarations.isEmpty()) { + writer.indented(() -> { + for (JavaMethodDeclaration methodDeclaration : methodDeclarations) { + writeMethodDeclaration(writer, methodDeclaration); + } + }); + } + writer.println("}"); + writer.println(""); + } + } + } + + private void writeAnnotations(IndentingWriter writer, Annotatable annotatable) { + annotatable.getAnnotations() + .forEach((annotation) -> 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 writeMethodDeclaration(IndentingWriter writer, + JavaMethodDeclaration methodDeclaration) { + writeAnnotations(writer, methodDeclaration); + writeMethodModifiers(writer, methodDeclaration); + writer.print(getUnqualifiedName(methodDeclaration.getReturnType()) + " " + + methodDeclaration.getName() + "("); + List parameters = methodDeclaration.getParameters(); + if (!parameters.isEmpty()) { + writer.print(parameters + .stream().map((parameter) -> getUnqualifiedName(parameter.getType()) + + " " + parameter.getName()) + .collect(Collectors.joining(", "))); + } + writer.println(") {"); + writer.indented(() -> { + List statements = methodDeclaration.getStatements(); + for (JavaStatement statement : statements) { + if (statement instanceof JavaExpressionStatement) { + writeExpression(writer, + ((JavaExpressionStatement) statement).getExpression()); + } + else if (statement instanceof JavaReturnStatement) { + writer.print("return "); + writeExpression(writer, + ((JavaReturnStatement) statement).getExpression()); + } + writer.println(";"); + } + }); + writer.println("}"); + writer.println(); + } + + private void writeMethodModifiers(IndentingWriter writer, + JavaMethodDeclaration methodDeclaration) { + String modifiers = METHOD_MODIFIERS.entrySet().stream() + .filter((entry) -> entry.getKey().test(methodDeclaration.getModifiers())) + .map(Entry::getValue).collect(Collectors.joining(" ")); + if (!modifiers.isEmpty()) { + writer.print(modifiers); + writer.print(" "); + } + } + + private void writeExpression(IndentingWriter writer, JavaExpression expression) { + if (expression instanceof JavaMethodInvocation) { + writeMethodInvocation(writer, (JavaMethodInvocation) expression); + } + } + + private void writeMethodInvocation(IndentingWriter writer, + JavaMethodInvocation methodInvocation) { + writer.print(getUnqualifiedName(methodInvocation.getTarget()) + "." + + methodInvocation.getName() + "(" + + String.join(", ", methodInvocation.getArguments()) + ")"); + } + + private Path fileForCompilationUnit(Path directory, + JavaCompilationUnit compilationUnit) { + return directoryForPackage(directory, compilationUnit.getPackageName()) + .resolve(compilationUnit.getName() + ".java"); + } + + private Path directoryForPackage(Path directory, String packageName) { + return directory.resolve(packageName.replace('.', '/')); + } + + private Set determineImports(JavaCompilationUnit compilationUnit) { + List imports = new ArrayList<>(); + for (JavaTypeDeclaration typeDeclaration : compilationUnit + .getTypeDeclarations()) { + if (requiresImport(typeDeclaration.getExtends())) { + imports.add(typeDeclaration.getExtends()); + } + imports.addAll(getRequiredImports(typeDeclaration.getAnnotations(), + this::determineImports)); + for (JavaMethodDeclaration methodDeclaration : typeDeclaration + .getMethodDeclarations()) { + if (requiresImport(methodDeclaration.getReturnType())) { + imports.add(methodDeclaration.getReturnType()); + } + imports.addAll(getRequiredImports(methodDeclaration.getAnnotations(), + this::determineImports)); + imports.addAll(getRequiredImports(methodDeclaration.getParameters(), + (parameter) -> Collections.singletonList(parameter.getType()))); + imports.addAll(getRequiredImports( + methodDeclaration.getStatements().stream() + .filter(JavaExpressionStatement.class::isInstance) + .map(JavaExpressionStatement.class::cast) + .map(JavaExpressionStatement::getExpression) + .filter(JavaMethodInvocation.class::isInstance) + .map(JavaMethodInvocation.class::cast), + (methodInvocation) -> Collections + .singleton(methodInvocation.getTarget()))); + } + } + Collections.sort(imports); + return new LinkedHashSet<>(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 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/java/JavaStatement.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaStatement.java new file mode 100644 index 00000000..0718c930 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaStatement.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.java; + +/** + * A statement in Java. + * + * @author Andy Wilkinson + */ +public class JavaStatement { + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaTypeDeclaration.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaTypeDeclaration.java new file mode 100644 index 00000000..44f4b441 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaTypeDeclaration.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.java; + +import java.util.ArrayList; +import java.util.List; + +import io.spring.initializr.generator.language.TypeDeclaration; + +/** + * A {@link TypeDeclaration declaration } of a type written in Java. + * + * @author Andy Wilkinson + */ +public class JavaTypeDeclaration extends TypeDeclaration { + + private final List methodDeclarations = new ArrayList<>(); + + JavaTypeDeclaration(String name) { + super(name); + } + + public void addMethodDeclaration(JavaMethodDeclaration methodDeclaration) { + this.methodDeclarations.add(methodDeclaration); + } + + public List getMethodDeclarations() { + return this.methodDeclarations; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/package-info.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/package-info.java new file mode 100644 index 00000000..33a86472 --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/java/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. + */ + +/** + * Java language. Provides a + * {@link io.spring.initializr.generator.language.java.JavaCompilationUnit compilation + * unit} implementation and a write for Java + * {@link io.spring.initializr.generator.language.java.JavaSourceCode source code}. + */ +package io.spring.initializr.generator.language.java; diff --git a/initializr-generator/src/main/resources/META-INF/spring.factories b/initializr-generator/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..321ce681 --- /dev/null +++ b/initializr-generator/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +io.spring.initializr.generator.language.LanguageFactory=\ +io.spring.initializr.generator.language.java.JavaLanguageFactory 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 new file mode 100644 index 00000000..a55ba562 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/LanguageTests.java @@ -0,0 +1,48 @@ +/* + * 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; + +import io.spring.initializr.generator.language.java.JavaLanguage; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Language} + * + * @author Stephane Nicoll + */ +class LanguageTests { + + @Test + void javaLanguage() { + Language java = Language.forId("java", "11"); + assertThat(java).isInstanceOf(JavaLanguage.class); + assertThat(java.id()).isEqualTo("java"); + assertThat(java.toString()).isEqualTo("java"); + assertThat(java.jvmVersion()).isEqualTo("11"); + } + + @Test + void unknownLanguage() { + assertThatIllegalStateException() + .isThrownBy(() -> Language.forId("unknown", null)) + .withMessageContaining("Unrecognized language id 'unknown'"); + } + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java new file mode 100644 index 00000000..49edd99e --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java @@ -0,0 +1,228 @@ +/* + * 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.java; + +import java.io.IOException; +import java.lang.reflect.Modifier; +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 JavaSourceCodeWriter}. + * + * @author Andy Wilkinson + */ +class JavaSourceCodeWriterTests { + + @TempDir + Path directory; + + private final JavaSourceCodeWriter writer = new JavaSourceCodeWriter( + IndentingWriterFactory.withDefaultSettings()); + + @Test + void emptyCompilationUnit() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + sourceCode.createCompilationUnit("com.example", "Test"); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", ""); + } + + @Test + void emptyTypeDeclaration() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + compilationUnit.createTypeDeclaration("Test"); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", + "public class Test {", "", "}", ""); + } + + @Test + void emptyTypeDeclarationWithSuperClass() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.extend("com.example.build.TestParent"); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", + "import com.example.build.TestParent;", "", + "public class Test extends TestParent {", "", "}", ""); + } + + @Test + void method() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.addMethodDeclaration( + JavaMethodDeclaration.method("trim").returning("java.lang.String") + .parameters(new Parameter("java.lang.String", "value")) + .body(new JavaReturnStatement( + new JavaMethodInvocation("value", "trim")))); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", + "public class Test {", "", " public String trim(String value) {", + " return value.trim();", " }", "", "}", ""); + } + + @Test + void springBootApplication() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotate(Annotation + .name("org.springframework.boot.autoconfigure.SpringBootApplication")); + test.addMethodDeclaration(JavaMethodDeclaration.method("main") + .modifiers(Modifier.PUBLIC | Modifier.STATIC).returning("void") + .parameters(new Parameter("java.lang.String[]", "args")) + .body(new JavaExpressionStatement(new JavaMethodInvocation( + "org.springframework.boot.SpringApplication", "run", "Test.class", + "args")))); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", + "import org.springframework.boot.SpringApplication;", + "import org.springframework.boot.autoconfigure.SpringBootApplication;", + "", "@SpringBootApplication", "public class Test {", "", + " public static void main(String[] args) {", + " SpringApplication.run(Test.class, 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)", "public 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\")", "public 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\")", "public 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)", "public 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 })", + "public 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)", + "public class Test {", "", "}", ""); + } + + private List writeClassAnnotation(Annotation annotation) throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotate(annotation); + return writeSingleType(sourceCode, "com/example/Test.java"); + } + + @Test + void methodWithSimpleAnnotation() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode + .createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + JavaMethodDeclaration method = JavaMethodDeclaration.method("something") + .returning("void").parameters().body(); + method.annotate(Annotation.name("com.example.test.TestAnnotation")); + test.addMethodDeclaration(method); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", + "import com.example.test.TestAnnotation;", "", "public class Test {", "", + " @TestAnnotation", " public void something() {", " }", "", "}", + ""); + } + + private List writeSingleType(JavaSourceCode sourceCode, String location) + throws IOException { + Path source = writeSourceCode(sourceCode).resolve(location); + assertThat(source).isRegularFile(); + return Files.readAllLines(source); + } + + private Path writeSourceCode(JavaSourceCode sourceCode) throws IOException { + Path projectDirectory = Files.createTempDirectory(this.directory, "project-"); + this.writer.writeTo(projectDirectory, sourceCode); + return projectDirectory; + } + +}