Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@

# An opinionated Java Inner Builder Generator

This is a simple Java Inner Builder Generator IntelliJ plugin that generates a
This is an opinionated but simple Java Inner Builder Generator IntelliJ plugin that generates a
builder for a given class. The builder is an inner class of the class it is building.

Based from [InnerBuilder](https://github.com/analytically/innerbuilder) with stripped down features.

Generates a builder class for a given class with the following features:

1. Generates builder method for final fields that are not initialized and/or static fields
2. Generates static `builder()` method inside the parent class
3. Uses field names as setters in the builder

Optional features:

1. Generates `toBuilder()` method to convert the object to a builder
2. Generates `validate()` method to validate the fields before building the object

<!-- Plugin description end -->

```java
Expand All @@ -25,7 +37,6 @@ public class Person {
return new Builder();
}


public static final class Builder {
private int age;
private String lastName;
Expand All @@ -43,14 +54,18 @@ public class Person {
return this;
}

private void validate() {
}

public Person build() {
validate();
return new Person(this);
}
}
}
```

Supports Java record classes and automatically detects the class visibility
Supports Java record classes

```java
record Address(String street, String city, String state, String country) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.junkfactory.innerbuilder;

import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption;
import com.intellij.codeInsight.generation.PsiFieldMember;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
Expand All @@ -11,7 +12,6 @@
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -34,31 +34,47 @@ class BuilderClassGenerator extends AbstractGenerator {

@Override
public void run() {
var selectedFields = generatorParams.selectedFields();
//builder constructor
var builderConstructor = generateBuilderConstructor();
addMethod(builderClass, null, builderConstructor, false);

//build setters
var selectedFields = generatorParams.psi().selectedFields();
var fieldMembers = new ArrayList<PsiFieldMember>();
PsiElement lastAddedField = null;
for (var fieldMember : selectedFields) {
lastAddedField = findOrCreateField(builderClass, fieldMember, lastAddedField);
fieldMembers.add(fieldMember);
}
//builder constructor
var builderConstructor = generateBuilderConstructor();
addMethod(builderClass, null, builderConstructor, false);

// builder methods
PsiElement lastAddedElement = null;
for (var member : fieldMembers) {
var setterMethod = generateBuilderSetter(builderType, member);
lastAddedElement = addMethod(builderClass, lastAddedElement, setterMethod, false);
}

//build validate method
var options = generatorParams.options();
if (options.contains(JavaInnerBuilderOption.WITH_VALIDATE_METHOD)) {
var validateMethod = generateValidateMethod();
addMethod(builderClass, lastAddedElement, validateMethod, false);
}

// builder.build() method
var buildMethod = generateBuildMethod(targetClass, selectedFields);
addMethod(builderClass, lastAddedElement, buildMethod, targetClass.isRecord());
var buildMethod = generateBuildMethod();
addMethod(builderClass, null, buildMethod, targetClass.isRecord());
}

private PsiMethod generateValidateMethod() {
var psiElementFactory = generatorParams.psi().factory();
var voidType = psiElementFactory.createPrimitiveType("void");
var validateMethod = psiElementFactory.createMethod("validate", voidType);
PsiUtil.setModifierProperty(validateMethod, PsiModifier.PRIVATE, true);
return validateMethod;
}

private PsiMethod generateBuilderConstructor() {
var builderConstructor = generatorParams.psiElementFactory().createConstructor(BUILDER_CLASS_NAME);
var builderConstructor = generatorParams.psi().factory().createConstructor(BUILDER_CLASS_NAME);
PsiUtil.setModifierProperty(builderConstructor, PsiModifier.PRIVATE, true);
return builderConstructor;
}
Expand All @@ -70,7 +86,7 @@ private PsiMethod generateBuilderSetter(final PsiType builderType, final PsiFiel
var fieldName = Utils.hasOneLetterPrefix(field.getName()) ?
Character.toLowerCase(field.getName().charAt(1)) + field.getName().substring(2) : field.getName();

var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var setterMethod = psiElementFactory.createMethod(fieldName, builderType);

setterMethod.getModifierList().setModifierProperty(PsiModifier.PUBLIC, true);
Expand All @@ -86,8 +102,8 @@ private PsiMethod generateBuilderSetter(final PsiType builderType, final PsiFiel
return setterMethod;
}

private PsiMethod generateBuildMethod(final PsiClass targetClass, final List<PsiFieldMember> selectedFields) {
var psiElementFactory = generatorParams.psiElementFactory();
private PsiMethod generateBuildMethod() {
var psiElementFactory = generatorParams.psi().factory();
var targetClassType = psiElementFactory.createType(targetClass);
var buildMethod = psiElementFactory.createMethod("build", targetClassType);

Expand All @@ -98,9 +114,14 @@ private PsiMethod generateBuildMethod(final PsiClass targetClass, final List<Psi
.ifPresent(modifier -> PsiUtil.setModifierProperty(buildMethod, modifier, true));

var buildMethodBody = Objects.requireNonNull(buildMethod.getBody());
if (generatorParams.options().contains(JavaInnerBuilderOption.WITH_VALIDATE_METHOD)) {
var validateCall = psiElementFactory.createStatementFromText("validate();", buildMethod);
buildMethodBody.add(validateCall);
}

final PsiStatement returnStatement;
if (targetClass.isRecord()) {
var recordParameters = selectedFields.stream()
var recordParameters = generatorParams.psi().selectedFields().stream()
.map(m -> m.getElement().getName())
.collect(Collectors.joining(", "));
returnStatement = psiElementFactory.createStatementFromText(String.format(
Expand All @@ -109,6 +130,7 @@ private PsiMethod generateBuildMethod(final PsiClass targetClass, final List<Psi
returnStatement = psiElementFactory.createStatementFromText(String.format(
"return new %s(this);", targetClass.getName()), buildMethod);
}

buildMethodBody.add(returnStatement);
return buildMethod;
}
Expand All @@ -119,12 +141,11 @@ private PsiElement findOrCreateField(final PsiClass builderClass, final PsiField
var fieldName = field.getName();
var fieldType = field.getType();
var existingField = builderClass.findFieldByName(fieldName, false);
if (existingField == null ||
Utils.areTypesPresentableNotEqual(existingField.getType(), fieldType)) {
if (existingField == null || Utils.areTypesPresentableNotEqual(existingField.getType(), fieldType)) {
if (existingField != null) {
existingField.delete();
}
var newField = generatorParams.psiElementFactory().createField(fieldName, fieldType);
var newField = generatorParams.psi().factory().createField(fieldName, fieldType);
if (last != null) {
return builderClass.addAfter(newField, last);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
package com.github.junkfactory.innerbuilder;

import com.intellij.codeInsight.generation.PsiFieldMember;
import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiFile;

import java.util.List;
import java.util.Set;

record GeneratorParams(Project project,
PsiFile file,
Editor editor,
List<PsiFieldMember> selectedFields,
PsiElementFactory psiElementFactory) {
PsiParams psi,
Set<JavaInnerBuilderOption> options) {

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private Project project;
private Editor editor;
private PsiParams psi;
private Set<JavaInnerBuilderOption> options;

private Builder() {
}

public Builder project(Project project) {
this.project = project;
return this;
}

public Builder editor(Editor editor) {
this.editor = editor;
return this;
}

public Builder psi(PsiParams psi) {
this.psi = psi;
return this;
}

public Builder options(Set<JavaInnerBuilderOption> options) {
this.options = options;
return this;
}

GeneratorParams build() {
return new GeneratorParams(project, editor, psi, options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ class InnerBuilderGenerator extends AbstractGenerator {

@Override
public void run() {
var file = generatorParams.file();
var file = generatorParams.psi().file();
var targetClass = Utils.getStaticOrTopLevelClass(file, generatorParams.editor());
if (targetClass == null) {
if (targetClass == null || BUILDER_CLASS_NAME.equals(targetClass.getName())) {
return;
}
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var builderClass = findOrCreateBuilderClass(targetClass);
var builderType = psiElementFactory.createTypeFromText(BUILDER_CLASS_NAME, null);

Expand All @@ -42,9 +42,9 @@ public void run() {
addMethod(targetClass, null, newBuilderMethod, false);

// toBuilder method
var options = JavaInnerBuilderOption.currentOptions();
if (options.contains(JavaInnerBuilderOption.TO_BUILDER)) {
var toBuilderMethod = generateToBuilderMethod(builderType, generatorParams.selectedFields());
var options = generatorParams.options();
if (options.contains(JavaInnerBuilderOption.WITH_TO_BUILDER_METHOD)) {
var toBuilderMethod = generateToBuilderMethod(builderType, generatorParams.psi().selectedFields());
addMethod(targetClass, null, toBuilderMethod, true);
}

Expand All @@ -57,7 +57,7 @@ public void run() {

private PsiMethod generateToBuilderMethod(final PsiType builderType,
final Collection<PsiFieldMember> fields) {
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var toBuilderMethod = psiElementFactory.createMethod(TO_BUILDER_NAME, builderType);
PsiUtil.setModifierProperty(toBuilderMethod, PsiModifier.PUBLIC, true);
var toBuilderBody = Objects.requireNonNull(toBuilderMethod.getBody());
Expand All @@ -74,14 +74,14 @@ private void addCopyBody(final Collection<PsiFieldMember> fields, final PsiMetho
var methodBody = Objects.requireNonNull(method.getBody());
for (final PsiFieldMember member : fields) {
var field = member.getElement();
var assignStatement = generatorParams.psiElementFactory().createStatementFromText(String.format(
var assignStatement = generatorParams.psi().factory().createStatementFromText(String.format(
"%s%2$s = this.%3$s;", "builder.", field.getName(), field.getName()), method);
methodBody.add(assignStatement);
}
}

private PsiMethod generateStaticBuilderMethod(final PsiType builderType) {
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var newBuilderMethod = psiElementFactory.createMethod(BUILDER_METHOD_NAME, builderType);
PsiUtil.setModifierProperty(newBuilderMethod, PsiModifier.STATIC, true);
PsiUtil.setModifierProperty(newBuilderMethod, PsiModifier.PUBLIC, true);
Expand All @@ -94,15 +94,15 @@ private PsiMethod generateStaticBuilderMethod(final PsiType builderType) {
}

private PsiMethod generateConstructor(final PsiClass targetClass, final PsiType builderType) {
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var constructor = psiElementFactory.createConstructor(Objects.requireNonNull(targetClass.getName()));
constructor.getModifierList().setModifierProperty(PsiModifier.PRIVATE, true);

var builderParameter = psiElementFactory.createParameter(BUILDER_METHOD_NAME, builderType);
constructor.getParameterList().add(builderParameter);

var constructorBody = Objects.requireNonNull(constructor.getBody());
for (var member : generatorParams.selectedFields()) {
for (var member : generatorParams.psi().selectedFields()) {
var field = member.getElement();
var setterPrototype = PropertyUtilBase.generateSetterPrototype(field);
var setter = targetClass.findMethodBySignature(setterPrototype, true);
Expand Down Expand Up @@ -143,7 +143,7 @@ private PsiClass findOrCreateBuilderClass(final PsiClass targetClass) {

@NotNull
private PsiClass createBuilderClass(final PsiClass targetClass) {
var builderClass = (PsiClass) targetClass.add(generatorParams.psiElementFactory()
var builderClass = (PsiClass) targetClass.add(generatorParams.psi().factory()
.createClass(BUILDER_CLASS_NAME));
PsiUtil.setModifierProperty(builderClass, PsiModifier.STATIC, true);
PsiUtil.setModifierProperty(builderClass, PsiModifier.FINAL, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.junkfactory.innerbuilder;

import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.LanguageCodeInsightActionHandler;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
Expand All @@ -12,6 +14,9 @@
import com.intellij.psi.PsiJavaFile;
import org.jetbrains.annotations.NotNull;

import java.util.EnumSet;
import java.util.Set;

import static com.github.junkfactory.innerbuilder.FieldCollector.collectFields;
import static com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOptionSelector.selectFieldsAndOptions;

Expand Down Expand Up @@ -64,11 +69,39 @@ public void invoke(@NotNull final Project project, @NotNull final Editor editor,
if (selectedFields.isEmpty()) {
return;
}
var generatorParams = new GeneratorParams(project, file, editor, selectedFields,
JavaPsiFacade.getElementFactory(project));
var psiParams = PsiParams.builder()
.file(file)
.selectedFields(selectedFields)
.factory(JavaPsiFacade.getElementFactory(project))
.build();
var generatorParams = GeneratorParams.builder()
.project(project)
.editor(editor)
.psi(psiParams)
.options(currentOptions())
.build();
var builderGenerator = new InnerBuilderGenerator(generatorParams);
ApplicationManager.getApplication().runWriteAction(builderGenerator);
}
}

private Set<JavaInnerBuilderOption> currentOptions() {
final var options = EnumSet.noneOf(JavaInnerBuilderOption.class);
final var propertiesComponent = PropertiesComponent.getInstance();
for (var option : JavaInnerBuilderOption.values()) {

if (Boolean.TRUE.equals(option.isBooleanProperty())) {
final boolean currentSetting = propertiesComponent.getBoolean(option.getProperty(), false);
if (currentSetting) {
options.add(option);
}
} else {
String currentValue = String.valueOf(propertiesComponent.getValue(option.getProperty()));
if (currentValue != null) {
JavaInnerBuilderOption.findValue(currentValue).ifPresent(options::add);
}
}
}
return options;
}
}
Loading