Skip to content

Latest commit

 

History

History
667 lines (501 loc) · 28.7 KB

README.adoc

File metadata and controls

667 lines (501 loc) · 28.7 KB

OpenRewrite - demo

OpenRewrite allows us to do major refactorings on our source code using (prewritten) recipes. It works by making changes to the Lossless Semantic Trees representing our source code and printing the modifications back to the source code/diffs which we can then compare and commit if we deem them ok.


Use cases

  • fixes: autoformatting, unused imports, applying new conventions using a recipe, …​

  • migrations: log4j ⇒ slf4j, java 8 ⇒ 11 ⇒ 17 ⇒ 21, JUnit 4 ⇒ 5, …​

  • static analysis fixes: resolve common issues reported by SAST tools, code cleanup, …​

  • utility: generate a CycloneDx bill of materials, update GitHub actions, …​

How does OpenRewrite work?

OpenRewrite makes changes to the Lossless Semantic Tree representation of your code using visitors. Visitors are basically event handlers, which deal with what to do, and when to do it that get triggered as OpenRewrite goes through the LST translation of our codebase. These visitors can in turn be gathered into recipes.

Setup

OpenRewrite can be run using the Maven/Gradle build plugin tools or directly from a java main method if a build tool plugin isn’t possible (see for reference)

Both for Maven and Gradle we can run the migrations either by modifying our build files or by running a shell command or init script respectively.

Maven

If we add the plugin to our pom.xml file

<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <version>5.42.2</version>
</plugin>

Gradle

For Gradle, we need to be certain that mavenCentral() is present in our repositories section, then we need to add the following to our build file:

plugins {
    id 'org.openrewrite.rewrite' version '6.25.1'
}

repositories {
  // needed to resolve recipe artifacts
  mavenCentral()
}

rewrite {
    // here we'll place the recipes we wish to use
}

Note: With Gradle. you can either add each dependency with the version number specified or add rewrite-recipe-bom as a bill of materials dependency `rewrite(platform("org.openrewrite.recipe:rewrite-recipe-bom:2.21.0")) `

After which we can try ./mvnw rewrite:discover or ./gradlew rewriteDiscover to discover which recipes we can run from OpenRewrite using this setup. (we can add other sources/write our own).

Usage

Adding a recipe without configuration

Some OpenRewrite recipes require configuration, but we’ll start easy with a standard OpenRewrite which doesn’t need any setup.

For example, if you have a project with a lot of unused imports you can use the org.openrewrite.java.RemoveUnusedImports recipe which is part of the core library.

Maven

a) run mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.RemoveUnusedImports

b) add <recipe>org.openrewrite.java.RemoveUnusedImports</recipe> to the <activeRecipes> in your pom file and perform ./mvnw rewrite:run

Gradle

Add activeRecipe("org.openrewrite.java.RemoveUnusedImports") and perform ./gradlew rewriteRun

If we were to run this one on the current project, and then execute a git diff we’d see:

diff --git a/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java b/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java
index d97b878..8e85aaf 100644
--- a/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java
+++ b/src/main/java/dev/simonverhoeven/openrewritedemo/OpenrewritedemoApplication.java
@@ -3,8 +3,6 @@ package dev.simonverhoeven.openrewritedemo;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;

-import java.math.BigDecimal;
-
 @SpringBootApplication
 public class OpenrewritedemoApplication {

Adding a recipe with a configuration

Some recipes require a configuration. Let’s start with an easy one. For example, your organisation changes its name, and suddenly you need to rewrite your package names.

For this, we can use the org.openrewrite.java.ChangePackage recipe.

To set it up we’ll need to create a rewrite.yml in which we’ll define a recipe name, optionally a display name, and the recipe list with the parameters. In this case, we’ll be renaming dev.simonverhoeven.openrewritedemo.oldorgname to dev.simonverhoeven.openrewritedemo.neworgname

---
type: specs.openrewrite.org/v1beta/recipe
name: dev.simonverhoeven.sampleRecipe
displayName: A simple recipe
recipeList:
  - org.openrewrite.java.ChangePackage:
      oldPackageName: dev.simonverhoeven.openrewritedemo.oldorgname
      newPackageName: dev.simonverhoeven.openrewritedemo.neworgname
      recursive: null

Then we’ll add dev.simonverhoeven.sampleRecipe to our active recipes.

When we then run the rewrite we’ll see that our oldorgname has been renamed to neworgname and that the package statement in our Sample file has also been adapted.

Preconditions

As of Rewrite version 8.9.0 we can once again write Preconditions for our recipes like in Rewrite 7.

Those conditions allow us to introduce some conditionality to our recipes, such as for example only applying a recipe in case of certain Java versions using org.openrewrite.java.search.HasJavaVersion. In case we define multiple conditions then a file must meet them all before any changes are applied.

Note
Precondition recipes can make changes to determine whether the condition is met, but are not included in the final result.

For example:

---
type: specs.openrewrite.org/v1beta/recipe
name: dev.simonverhoeven.preconditionExample
preconditions:
  - org.openrewrite.java.search.HasJavaVersion:
      version: 8.X
recipeList:
  - org.openrewrite.text.FindAndReplace:
      find: somethingold
      replace: somethingnew
      filePattern: '**/application.properties'

Our FindAndReplace recipe will only be applied in case the HasJavaVersion recipe condition is met.

Without build tool plugins

It is possible to use OpenRewrite without the build tool plugins, the hardest part is determining the applicable classpath for each set of files. A brief overview of the approach is documented at running rewrite without build tool plugins on the OpenRewrite website.

Writing our own recipe

Java recipe

We can write a Java refactoring recipe quite easily, it just needs

  • Fully qualified name

  • Serializable constructor

  • getDisplayName() method

  • getDescription() method

We can easily add a RewriteTest for it, which at its basics just applies a rewriteRun to compare the before and after.

@Override
public void defaults(RecipeSpec spec) {
    spec.recipe(new MyRecipe("com.my.SomeClass"));
}

@Test
void someTest() {
    rewriteRun(
        java(
            """
                beforeClassState
            """,
            """
                afterClassState
            """
        )
    );
}

A Recipe itself makes use of a Recipe and the Visitor. For example to migrate from JUnit 4’s Enclosed to Jupiter’s @Nested:

@Value
@EqualsAndHashCode(callSuper = false)
public class EnclosedToNested extends Recipe {
    private static final String ENCLOSED = "org.junit.experimental.runners.Enclosed";
    private static final String RUN_WITH = "org.junit.runner.RunWith";
    private static final String NESTED = "org.junit.jupiter.api.Nested";
    private static final String TEST_JUNIT4 = "org.junit.Test";
    private static final String TEST_JUNIT_JUPITER = "org.junit.jupiter.api.Test";

    @Override
    public String getDisplayName() {
        return "JUnit 4 `@RunWith(Enclosed.class)` to JUnit Jupiter `@Nested`";
    }

    @Override
    public String getDescription() {
        return "Removes the `Enclosed` specification from a class, and adds `Nested` to its inner classes.";
    }

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        return Preconditions.check(new UsesType<>(ENCLOSED, false), new JavaIsoVisitor<ExecutionContext>() {
            @SuppressWarnings("ConstantConditions")
            @Override
            public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
                J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
                String runwithEnclosed = String.format("@%s(%s.class)", RUN_WITH, ENCLOSED);
                if (!FindAnnotations.find(cd.withBody(null), runwithEnclosed).isEmpty()) {
                    cd = (J.ClassDeclaration) new RemoveAnnotationVisitor(new AnnotationMatcher(runwithEnclosed)).visit(cd, ctx);
                    cd = cd.withBody((J.Block) new AddNestedAnnotationVisitor().visit(cd.getBody(), ctx, updateCursor(cd)));

                    maybeRemoveImport(ENCLOSED);
                    maybeRemoveImport(RUN_WITH);
                    maybeAddImport(NESTED);
                }
                return cd;
            }
        });
    }

    public static class AddNestedAnnotationVisitor extends JavaIsoVisitor<ExecutionContext> {
        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
            J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
            if (hasTestMethods(cd)) {
                cd = JavaTemplate.builder("@Nested")
                        .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "junit-jupiter-api-5.9"))
                        .imports(NESTED)
                        .build()
                        .apply(getCursor(), cd.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));
                cd.getModifiers().removeIf(modifier -> modifier.getType() == J.Modifier.Type.Static);
                return maybeAutoFormat(classDecl, cd, ctx);
            }
            return cd;
        }

        private boolean hasTestMethods(final J.ClassDeclaration cd) {
            return !FindAnnotations.find(cd, "@" + TEST_JUNIT4).isEmpty() ||
                   !FindAnnotations.find(cd, "@" + TEST_JUNIT_JUPITER).isEmpty();
        }
    }
}

Refaster recipes

For straightforward refactorings we can also make use of Refaster templates. These replacements which offer compiler and type support allow us to do clear before & after replacements, with the caveat they’re whitespace agnostic. We could for example use them to facilitate replacing StringUtils.equals(..) with Objects.equals(..).

A Refaster template recipe is an imperative recipe that’s generated automatically when we build our Java classes that container on or more Refaster templates.

There are a couple of requirements for a class to be considered a Refaster template:

  • All argument types and names must match

  • There is exactly 1 @AfterTemplate annotated method, all other methods must be annotated with @BeforeTemplate

  • There are multiple methods with the same return type

import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;

public class StringIsEmpty {
  @BeforeTemplate
  boolean equalsEmptyString(String string) {
    return string.equals("");
  }

  @BeforeTemplate
  boolean lengthEquals0(String string) {
    return string.length() == 0;
  }

  @AfterTemplate
  boolean optimizedMethod(String string) {
    return string.isEmpty();
  }
}

A couple of things to keep in mind in regard to a Refaster recipe:

  • It should contain one or more @BeforeTemplate

  • It should contain one @AfterTemplate

  • If there are multiple templates in the Recipe, the file name should be in the plural

  • When creating a recipe we’ll need to add an annotation processor (org.openrewrite:rewrite-templating)

The real power

For now, we’ve used 2 quite basic recipes, which had relatively limited impact. Now let’s take a leap forward to Java 21 & Spring Boot 3.3.

Migration

Hamcrest ⇒ AssertJ

Now taking a look at our project, we stumble upon an issue. We’re still using Hamcrest, which is no longer actively being supported, and we’ve encountered some challenges with using it. So a migration to a different framework such as AssertJ seems apt.

OpenRewrite has a lot of individual recipes for this, but we can also use org.openrewrite.recipe:rewrite-testing-frameworks:2.20.1org.openrewrite.java.testing.hamcrest.MigrateHamcrestToAssertJ which has no required input.

So we can just add this one to our pom.xml or build.gradle, or execute it directly from the mvn command line.

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-testing-frameworks:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.testing.hamcrest.MigrateHamcrestToAssertJ

After running this command you can see that this recipe has managed to fully replace all usages of Hamcrest. So if desired one can remove the library.

Modernization

And we'd love to finally start using `spring-boot-starter-test`.

Now we’d like to take the sensible approach and make certain that all of our tests run properly using this library. Now here’s where we stumble upon a small hiccup. For some reason, our project’s using JUnit 4, not 5 and since Spring Boot 2.2 the backward compatibility with Spring JUnit 4 has been dropped.

JUnit 4 ⇒ JUnit 5

As documented the upgrade to JUnit 5 entails a couple of steps for which there are recipes

  • @Ignore@Disabled: org.openrewrite.java.testing.junit5.IgnoreToDisabled

  • org.junit.Assertorg.junit.jupiter.api.Assertions: org.openrewrite.java.test.junit5.AssertToAssertions

  • org.junit.Testorg.junit.jupiter.api.Test: org.openrewrite.java.test.junit5.UpdateTestAnnotation

  • @Junit 4’s @Rule ExpectedException ⇒ JUnit 5’s `Assertions.assertThrows(): org.openrewrite.java.testing.junit5.ExpectedExceptionToAssertThrows

  • …​

And that’s the premise behind OpenRewrite, large migrations in small steps.

One of the recipes we can use for this is org.openrewrite.java.testing.junit5.JUnit4to5Migration for which we’ll need a dependency on org.openrewrite.recipe:rewrite-testing-frameworks:2.20.1.

When we execute this recipe we’ll get

[WARNING] Changes have been made to pom.xml by:
[WARNING]     org.openrewrite.java.testing.junit5.JUnit4to5Migration
[WARNING]         org.openrewrite.java.dependencies.RemoveDependency: {groupId=junit, artifactId=junit}
[WARNING]             org.openrewrite.maven.RemoveDependency: {groupId=junit, artifactId=junit}
[WARNING]         org.openrewrite.java.dependencies.AddDependency: {groupId=org.junit.jupiter, artifactId=junit-jupiter, version=5.x, onlyIfUsing=org.junit.jupiter.api.Test, acceptTransitive=true}
[WARNING]             org.openrewrite.maven.AddDependency: {groupId=org.junit.jupiter, artifactId=junit-jupiter, version=5.x, onlyIfUsing=org.junit.jupiter.api.Test, acceptTransitive=true}
[WARNING] Changes have been made to src\test\java\dev\simonverhoeven\openrewritedemo\JunitTest.java by:
[WARNING]     org.openrewrite.java.testing.junit5.JUnit4to5Migration
[WARNING]         org.openrewrite.java.testing.junit5.IgnoreToDisabled
[WARNING]             org.openrewrite.java.ChangeType: {oldFullyQualifiedTypeName=org.junit.Ignore, newFullyQualifiedTypeName=org.junit.jupiter.api.Disabled}
[WARNING]         org.openrewrite.java.testing.junit5.AssertToAssertions
[WARNING]         org.openrewrite.java.testing.junit5.CategoryToTag
[WARNING]         org.openrewrite.java.testing.junit5.TemporaryFolderToTempDir
[WARNING]         org.openrewrite.java.testing.junit5.UpdateBeforeAfterAnnotations
[WARNING]         org.openrewrite.java.testing.junit5.UpdateTestAnnotation
[WARNING]         org.openrewrite.java.testing.junit5.ExpectedExceptionToAssertThrows

If we then run a git diff to see the changes that were made we’ll notice that our pom.xml has been upgraded, our imports are now from the jupiter hierarchy, @Ignore@Disabled, Assert.Assertions., …​

note: there are multiple recipes that can be used from this. For example, there’s also org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration which is a superset of the JUnit 4 to 5 & Mockito 1 to 3 recipes.

Now we can run those tests, and everything looks fine and dandy.

Java 8 ⇒ 17 ⇒ 21 & Spring Boot 2.17 ⇒ 3.3

Let’s take the next step, and try a migration to Java 17 and spring boot.

In our pom.xml:

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>5.42.2</version>
    <configuration>
        <activeRecipes>
            <recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3</recipe>
        </activeRecipes>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.openrewrite.recipe</groupId>
            <artifactId>rewrite-spring</artifactId>
            <version>5.21.0</version>
        </dependency>
    </dependencies>
</plugin>

or build.gradle:

plugins {
    id("org.openrewrite.rewrite") version("6.25.1")
}

rewrite {
    activeRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3")
}

repositories {
    mavenCentral()
}

dependencies {
    rewrite("org.openrewrite.recipe:rewrite-spring:5.21.0")
}

After running ./mvnw rewrite:run or ./gradlew rewriteRun we can use git diff to take a look at the results.

And we can see a lot of interesting changes:

  • our outdated spring properties have been migrated

  • our Java version has been upgraded from java 8 to 17 (the new spring boot 3 baseline), including improvements such as:

    • using the BigDecimal RoundingMode enum rather than an int

    • !emptyOptional.isPresent();emptyOptional.isEmpty()

    • concatenated text has been replaced with a text block

    • updated String formatting

  • JUnit 4 ⇒ JUnit 5

  • …​

We got all this thanks to the recipe list of UpgradeSpringBoot_3_3

It’s quite amazing to see what we can achieve with just this simple action.

We can also get conditional upgrades such as the one provided by Enable virtual threads which will enable Virtual Threads in case we’re running Java 21.

¿Guava?

One will quite likely encounter Guava in a lot of older projects, it offered us a lot of functionality that wasn’t part of the JDK. Over the years a lot of this functionality has become part of it though, and after all the effort we’ve done to upgrade our project we’d like to use the standard JDK as much as possible.

For example, in our SampleService we’ll see that a lot of things are being done using the Guava library.

OpenRewrite has a lot of individual recipes for this, but we can also use org.openrewrite.recipe:rewrite-migrate-java:2.27.0org.openrewrite.java.migrate.guava.NoGuava which has no required input.

So we can just add this one to our pom.xml or build.gradle, or execute it directly from the mvn command line.

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.migrate.guava.NoGuava

After running this command you can see that this recipe has managed to fully replace all usages of Guava. So if desired one can remove the library.

Static analysis fixes

Now that we’ve done all this, we’re finally starting to reach our target. The next thing we’d like to tackle are the results we got from our upgraded Sonar instance. Whilst some of these will of course require human intervention, OpenRewrite offers a lot of (composite) recipes which will help us clean up the common issues which can be found at static analysis.

We can run a lot of recipes manually, such as org.openrewrite.staticanalysis.MissingOverrideAnnotation, but our eye swiftly gets drawn to org.openrewrite.staticanalysis.CommonStaticAnalysis which is part of org.openrewrite.recipe:rewrite-static-analysis:1.18.0 and has no required input.

So we can just do

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-static-analysis:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.staticanalysis.CommonStaticAnalysis

And we’ll notice that a lot of complaints such as:

  • missing serialVersionUID

  • inverted boolean checks

  • catch should do more than just rethrow

  • modifier order

  • missing braces

  • Strings not using .equals

  • unnecessary String#toString()

  • no double variable declaration

  • …​

are resolved for us

In our console we’ll see:

[WARNING]     org.openrewrite.staticanalysis.CommonStaticAnalysis
[WARNING]         org.openrewrite.staticanalysis.BigDecimalRoundingConstantsToEnums
[WARNING] Changes have been made to src\main\java\dev\simonverhoeven\openrewritedemo\oldorgname\SampleController.java by:
[WARNING]     org.openrewrite.staticanalysis.CommonStaticAnalysis
[WARNING]         org.openrewrite.staticanalysis.AddSerialVersionUidToSerializable
[WARNING]         org.openrewrite.staticanalysis.BooleanChecksNotInverted
[WARNING]         org.openrewrite.staticanalysis.CaseInsensitiveComparisonsDoNotChangeCase
[WARNING]         org.openrewrite.staticanalysis.DefaultComesLast
[WARNING]         org.openrewrite.staticanalysis.EmptyBlock
[WARNING]         org.openrewrite.staticanalysis.FinalizePrivateFields
[WARNING]         org.openrewrite.staticanalysis.FinalClass
[WARNING]         org.openrewrite.staticanalysis.ForLoopIncrementInUpdate
[WARNING]         org.openrewrite.staticanalysis.ModifierOrder
[WARNING]         org.openrewrite.staticanalysis.MultipleVariableDeclarations
[WARNING]         org.openrewrite.staticanalysis.NoToStringOnStringType
[WARNING]         org.openrewrite.staticanalysis.RemoveExtraSemicolons
[WARNING]         org.openrewrite.staticanalysis.RenamePrivateFieldsToCamelCase
[WARNING]         org.openrewrite.staticanalysis.UseDiamondOperator
[WARNING]         org.openrewrite.staticanalysis.InlineVariable

And looking at SampleController will reveal a lot of changes

Utility

Now OpenRewrite goes beyond just rewriting one’s codebase. There are a lot of other convenient features:

GitHub actions

There are quite a bit of recipes to help you manage your GitHub workflows.

For example, there’s setup-java which updates your setup-java action if needed (and is part of the upgrade to Spring Boot 3.3 recipe for example)

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-github-actions:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.github.SetupJavaUpgradeJavaVersion

Or say if one wants to bulk update the used runners there’s the replacerunners recipe.

Cloud suitability analysis

OpenRewrite offers a lot of recipes at cloud suitability to help you determine the cloud suitability of your project

One nice example is findunsuitablecode

Which will scan for items that may potentially cause issues such as:

  • usage of ehcache

  • usage of corba

  • hardcoded IP addresses

  • remote method invocation

  • unhandled term signals

  • …​

Secrets

Hopefully one will never need these, but there are recipes to scan for different types of secrets within your codes.

For example one can spot that in our SampleController we have:

private static final String ACCOUNT_KEY = "lJzRc1YdHaAA2KCNJJ1tkYwF/+mKK6Ygw0NGe170Xu592euJv2wYUtBlV8z+qnlcNQSnIYVTkLWntUO1F8j8rQ==";

After running:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-java-security:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.security.secrets.FindAzureSecrets

We’ll see that it has been transformed to:

private static final String ACCOUNT_KEY = /*~~(Azure access key)~~>*/"lJzRc1YdHaAA2KCNJJ1tkYwF/+mKK6Ygw0NGe170Xu592euJv2wYUtBlV8z+qnlcNQSnIYVTkLWntUO1F8j8rQ==";

Which makes it a lot easier for us to find these kind of issues.

Generating a Bill of Materials (BOM)

You might be asked to provide a list of your (transitive) project dependencies, this can easily be achieved using the cyclonedx goal.

etc…​

OpenRewrite has so many more interesting recipes, and I’d invite you to take a gander at their recipe list.

A last one I wanted to point out which showcases a way in which OpenRewrite can help with the readability of your codebase is the formatsql one

Which automatically transforms this:

class Test {
    String query = """
            SELECT b.book_id, b.title, COUNT(r.review_id) AS num_reviews,AVG(r.rating) AS median_rating FROM books b
            JOIN reads rd ON b.book_id = rd.book_id JOIN readers
            re ON rd.reader_id = re.reader_id
            JOIN reviews r ON b.book_id = r.book_id
            GROUP BY b.book_id, b.title ORDER
            BY num_reviews DESC;\
            """;
}

to

class Test {
    String query = """
            SELECT
              b.book_id,
              b.title,
              COUNT(r.review_id) AS num_reviews,
              AVG(r.rating) AS median_rating
            FROM
              books b
              JOIN reads rd ON b.book_id = rd.book_id
              JOIN readers re ON rd.reader_id = re.reader_id
              JOIN reviews r ON b.book_id = r.book_id
            GROUP BY
              b.book_id,
              b.title
            ORDER BY
              num_reviews DESC;\
            """;
}

Generative AI

Proper expectation management is needed, as it’s still very much a work in progress. Initially even the generated recipes didn’t compile

Currently the scope has been reduced to limit the use cases, yet still adding value such as:

AI-assisted refactoring

The Moderne platform now supports AI to help us compose, search for, and be recommended recipes.

References

Notes

If you have a multi-module maven project you might run into errors when using the maven plugin, a workaround & more information is documented at using multi-module maven.