Skip to content
Open
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
47 changes: 47 additions & 0 deletions grails-data-neo4j/GORM_REGISTRY_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Neo4j β†’ GormRegistry: migration plan

`grails-data-neo4j` is a **separate Gradle build** that resolves GORM via a published
`datastoreVersion`, on an older baseline (Groovy 3.0.25 / Grails 6.0.0 / `javax.*`). To wire it to
the GormRegistry O(M+N) work (core-impl, `8.0.0-SNAPSHOT`, Groovy 4 / Java 21 / Jakarta) it must first
be moved onto that baseline. This is split into two PRs.

## PR1 β€” baseline migration (this branch: `build/neo4j-groovy4-baseline`)

Goal: the Neo4j separate build compiles and tests against core-impl's GORM, with **no** GormRegistry
behavioural change yet.

1. **`grails-data-neo4j/gradle.properties`**
- `datastoreVersion` `8.0.4` β†’ `8.0.0-SNAPSHOT` (consume core-impl)
- `groovyVersion` `3.0.25` β†’ 4.0.x (match core-impl)
- `grailsVersion` `6.0.0` β†’ 8.0.x; bump `grailsGradlePluginVersion`, `hibernateDatastoreVersion`,
and Java to 21 as needed
2. **Jakarta migration** (`javax.*` β†’ `jakarta.*`) across the ~8 affected main files:
- `javax.persistence.{FlushModeType,FetchType,LockModeType,CascadeType}` β†’ `jakarta.persistence.*`
- `javax.servlet.{ServletException,http.HttpServletRequest,http.HttpServletResponse}` β†’ `jakarta.servlet.*`
- `javax.annotation.PreDestroy` β†’ `jakarta.annotation.PreDestroy`
3. **Recompile + fix** the Groovy 3β†’4 / Grails 6β†’8 / new-GORM-API fallout across the module
(97 main source files).
4. **Verify locally** (the separate build can't see core-impl's local modules directly):
- from repo root: `./gradlew publishToMavenLocal -PskipTests` (publishes GORM `8.0.0-SNAPSHOT`)
- then: `cd grails-data-neo4j && ./gradlew build`

## PR2 β€” GormRegistry wiring (stacked on PR1)

Goal: route Neo4j through `GormRegistry` instead of the legacy `GormEnhancer` static maps.

1. **Add `Neo4jGormApiFactory`** β€” mirror `MongoGormApiFactory`: extend `DefaultGormApiFactory`,
override `createStaticApi` to return
`new Neo4jGormStaticApi<>(persistentClass, neo4jDatastore, finders, txManager)`.
Without it `GormRegistry` falls back to the generic `GormStaticApi`, breaking the
`(Neo4jGormStaticApi) findStaticApi(...)` casts in `Neo4jEntity` (the exact bug fixed for Mongo).
2. **Register it** in `Neo4jDatastore.initialize(...)` β€” replace the anonymous
`new GormEnhancer(...) { getStaticApi override }` with
`GormRegistry.instance.registerApiFactory(Neo4jDatastore, new Neo4jGormApiFactory())`.
3. **Rewrite the entity traits** (`Neo4jEntity`, `Node`, `Relationship`):
`GormEnhancer.findStaticApi/findDatastore` β†’ `GormRegistry.instance.findStaticApi/apiResolver.findDatastore`.
4. **Verify** as in PR1 step 4.

## Sequencing

Do PR1/PR2 once core-impl (#15780) CI is green, so the migration targets a settled GormRegistry SPI
rather than a moving one. Tracking: this is the Neo4j follow-up referenced from #15780.
8 changes: 4 additions & 4 deletions grails-data-neo4j/boot-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,25 @@

dependencies {
compileOnly "org.springframework.boot:spring-boot-cli:$springBootVersion", {
exclude group:'org.codehaus.groovy', module:'groovy'
exclude group:'org.apache.groovy', module:'groovy'
exclude group:'jline', module:'jline'
}

api "org.springframework.boot:spring-boot-autoconfigure:$springBootVersion"

api "org.codehaus.groovy:groovy"
api "org.apache.groovy:groovy:$groovyVersion"
api project(":grails-datastore-gorm-neo4j")
api "org.springframework:spring-tx:$springVersion"

testRuntimeOnly "org.neo4j.test:neo4j-harness:$neo4jVersion"
testImplementation ("org.spockframework:spock-core:$spockVersion") {
exclude group: 'junit', module: 'junit-dep'
exclude group: 'org.codehaus.groovy', module: 'groovy-all'
exclude group: 'org.apache.groovy', module: 'groovy-all'
exclude group: 'org.hamcrest', module: 'hamcrest-core'
transitive = false
}
testImplementation "org.springframework.boot:spring-boot-cli:$springBootVersion", {
exclude group:'org.codehaus.groovy', module:'groovy'
exclude group:'org.apache.groovy', module:'groovy'
exclude group:'jline', module:'jline'
}
}
28 changes: 18 additions & 10 deletions grails-data-neo4j/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

buildscript {
repositories {
// Scoped to org.apache.grails* so mavenLocal is only consulted for locally-published
// Grails/GORM snapshots (grails-gradle-plugins); everything else still resolves remotely.
mavenLocal {
content { includeGroupByRegex "org\\.apache\\.grails.*" }
}
maven { url "https://repo.grails.org/grails/restricted" }
maven { url "https://plugins.gradle.org/m2/" }
}
Expand All @@ -27,6 +32,7 @@ buildscript {
classpath "org.apache.grails:grails-gradle-plugins:$grailsGradlePluginVersion"
classpath "org.asciidoctor:asciidoctor-gradle-jvm:4.0.5"
classpath "com.github.erdi:webdriver-binaries-gradle-plugin:3.2"
classpath "org.gradle:test-retry-gradle-plugin:1.6.2"
}
}

Expand Down Expand Up @@ -78,7 +84,11 @@ subprojects { subproject ->
ext['junit-jupiter.version'] = junitJupiterVersion

repositories {
mavenLocal()
// Scoped to org.apache.grails* so mavenLocal is only consulted for locally-published
// Grails/GORM snapshots; everything else still resolves from the remote repositories.
mavenLocal {
content { includeGroupByRegex "org\\.apache\\.grails.*" }
}
maven { url "https://repo.grails.org/grails/restricted" }
}

Expand Down Expand Up @@ -127,7 +137,7 @@ subprojects { subproject ->

dependencies {
testImplementation "org.hibernate:hibernate-validator:$hibernateValidatorVersion"
testImplementation "org.codehaus.groovy:groovy-test-junit5"
testImplementation "org.apache.groovy:groovy-test-junit5:$groovyVersion"
testImplementation "org.spockframework:spock-core:$spockVersion", { transitive = false }
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion"
testImplementation "org.junit.platform:junit-platform-suite:$junitPlatformVersion"
Expand Down Expand Up @@ -202,17 +212,17 @@ subprojects { subproject ->
apply plugin: "java-library"
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'org.gradle.test-retry'

if(isGrails3PluginProject) {
apply plugin: 'org.apache.grails.gradle.grails-plugin'
} else {
apply plugin:"groovy"
}

sourceCompatibility = "1.11"
targetCompatibility = "1.11"

java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withJavadocJar()
withSourcesJar()
}
Expand All @@ -227,9 +237,7 @@ subprojects { subproject ->
'grails-datamapping-async',
'grails-datamapping-rx',
'grails-datamapping-support',
'grails-datamapping-tck-tests',
'grails-datamapping-tck-base',
'grails-datamapping-tck-domains',
'grails-datamapping-tck',
'grails-datamapping-core-test',
'grails-datamapping-validation',
'grails-datastore-web']
Expand All @@ -245,8 +253,8 @@ subprojects { subproject ->


dependencies {
api "org.codehaus.groovy:groovy"
testImplementation "org.codehaus.groovy:groovy-test-junit5"
api "org.apache.groovy:groovy:$groovyVersion"
testImplementation "org.apache.groovy:groovy-test-junit5:$groovyVersion"
testImplementation "org.spockframework:spock-core:$spockVersion", { transitive = false }
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion"
testImplementation "org.junit.platform:junit-platform-suite:$junitPlatformVersion"
Expand Down
16 changes: 8 additions & 8 deletions grails-data-neo4j/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@

assetPipelineVersion=3.3.4
cglibNodepVersion=3.3.0
datastoreVersion=8.0.4
datastoreVersion=8.0.0-SNAPSHOT
gebVersion=2.3
gparsVersion=1.2.1
grailsVersion=6.0.0
grailsGradlePluginVersion=6.1.2
groovyVersion=3.0.25
grailsVersion=8.0.0-SNAPSHOT
grailsGradlePluginVersion=8.0.0-SNAPSHOT
groovyVersion=5.0.7
h2Version=1.4.200
hibernateDatastoreVersion=8.0.2
hibernateDatastoreVersion=8.0.0-SNAPSHOT
hibernateEhcacheVersion=5.5.7.Final
hibernateValidatorVersion=6.2.5.Final
jansiVersion=2.4.1
Expand All @@ -40,9 +40,9 @@ projectVersion=8.1.0-SNAPSHOT
seleniumSafariDriverVersion=3.14.0
seleniumVersion=3.14.0
servletApiVersion=4.0.1
spockVersion=2.1-groovy-3.0
springBootVersion=2.7.18
springVersion=5.3.31
spockVersion=2.4-groovy-5.0
springBootVersion=4.1.0
springVersion=7.0.8

org.gradle.caching=true
org.gradle.daemon=true
Expand Down
53 changes: 47 additions & 6 deletions grails-data-neo4j/grails-datastore-gorm-neo4j/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,66 @@ dependencies {
implementation "org.javassist:javassist:$javassistVersion"
testImplementation "org.neo4j.test:neo4j-harness:$neo4jVersion"
testImplementation "org.apache.grails.data:grails-datamapping-core-test:$datastoreVersion"
testImplementation "org.apache.grails.data:grails-datamapping-tck-tests:$datastoreVersion"
testImplementation "org.apache.grails.data:grails-datamapping-tck:$datastoreVersion"
testImplementation "jakarta.validation:jakarta.validation-api:3.1.1"
testImplementation "org.hibernate:hibernate-validator:$hibernateValidatorVersion"
testImplementation "org.codehaus.gpars:gpars:$gparsVersion"
testImplementation "cglib:cglib-nodep:$cglibNodepVersion"
testImplementation "org.objenesis:objenesis:${objenesisVersion}"

// Spock's Mock() support needs this at runtime but spock-core doesn't declare it as a dependency
testRuntimeOnly "io.leangen.geantyref:geantyref:1.3.15"
testRuntimeOnly "net.bytebuddy:byte-buddy:1.18.10" // Required by Spock's mocking support (cglib doesn't work on JDK 21+)
testRuntimeOnly "org.springframework:spring-aop:$springVersion"
testRuntimeOnly "ch.qos.logback:logback-classic:1.4.14"
testRuntimeOnly "javax.el:javax.el-api:3.0.0"
testRuntimeOnly "org.glassfish.web:el-impl:2.2.1-b05"
testRuntimeOnly "jakarta.el:jakarta.el-api:5.0.1"
testRuntimeOnly "org.glassfish.expressly:expressly:5.0.0"

}

// The Spring Boot BOM (pulled in transitively via grails-bom) force-upgrades Jetty to a
// 12.x platform version and neo4j-java-driver to 6.x.
def neo4jHarnessJettyVersion = '9.4.43.v20210629'

// The embedded Neo4j 3.5.x test harness (neo4j-harness, test-only) is compiled against Jetty
// 9.4 and is binary-incompatible with Jetty 12's restructured handler/server APIs. Scoped to
// the test classpaths only, since main code never touches Jetty directly.
[configurations.testCompileClasspath, configurations.testRuntimeClasspath].each {
it.resolutionStrategy {
force "org.eclipse.jetty:jetty-server:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-servlet:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-webapp:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-security:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-http:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-io:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-util:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-util-ajax:$neo4jHarnessJettyVersion",
"org.eclipse.jetty:jetty-xml:$neo4jHarnessJettyVersion"
}
}

// This module's own main code (e.g. Neo4jQuery#executeQuery) calls Driver#defaultTypeSystem(),
// which driver 6.x removed - so this force applies to all configurations, main and test alike.
configurations.all {
resolutionStrategy {
force "org.neo4j.driver:neo4j-java-driver:$neo4jDriverVersion"
}
}

test {
useJUnitPlatform()
maxParallelForks = configuredTestParallel
systemProperty('neo4j.gorm.suite', System.getProperty('neo4j.gorm.suite') ?: true)
maxParallelForks = (findProperty('maxTestParallel') as Integer) ?: 1
forkEvery = 10

jvmArgs = ['-Xmx1028M']
// The embedded Neo4j 3.5.x test harness reflectively pokes JDK internals (Throwable's
// message field, sun.nio.ch.FileChannelImpl's lock accessors) at startup; JDK 9+ strong
// encapsulation blocks that without these opens.
jvmArgs = [
'-Xmx1028M',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED',
]
afterSuite {
System.out.print('.')
System.out.flush()
Expand All @@ -76,7 +117,7 @@ test.doFirst {
.resolvedConfiguration
.getResolvedArtifacts()
.find { resolved ->
resolved.moduleVersion.id.name == 'grails-datamapping-tck-tests'
resolved.moduleVersion.id.name == 'grails-datamapping-tck'
}.file

def tckClassesDir = project.file("${project.buildDir}/tck")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class GraphClassMapping extends AbstractClassMapping<NodeConfig> {

@Override
NodeConfig getMappedForm() {
((GraphPersistentEntity)entity).mappedForm
// Explicit field access (.@) - entity.mappedForm would resolve to the getter,
// which recurses back here via PersistentEntity#getMappedForm()'s default method.
((GraphPersistentEntity)entity).@mappedForm
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import org.grails.datastore.gorm.neo4j.mapping.config.NodeConfig
import org.grails.datastore.gorm.neo4j.mapping.config.RelationshipConfig
import org.grails.datastore.mapping.config.AbstractGormMappingFactory
import org.grails.datastore.mapping.config.Entity
import org.grails.datastore.mapping.config.Property
import org.grails.datastore.mapping.model.ClassMapping
import org.grails.datastore.mapping.model.DefaultIdentityMapping
import org.grails.datastore.mapping.model.IdentityMapping
import org.grails.datastore.mapping.model.MappingFactory
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.ValueGenerator

/**
* A {@link org.grails.datastore.mapping.model.MappingFactory} for Neo4j
Expand Down Expand Up @@ -60,6 +66,32 @@ class GraphGormMappingFactory extends AbstractGormMappingFactory {
return super.createMappedForm(entity)
}
}

/**
* {@link MappingFactory#createDefaultIdentityMapping} resolves {@code generator} via
* {@link ValueGenerator#valueOf} with no fallback, so any name that isn't one of its built-in
* constants throws IllegalArgumentException. Unlike Hibernate - where the generator name is
* always either a known strategy or a Hibernate generator class - Neo4j also accepts its own
* built-in named strategies (e.g. "snowflake", see {@link IdGenerator.Type}) that aren't classes
* at all and are resolved independently by {@link GraphPersistentEntity}; ValueGenerator here only
* needs to not blow up, so any unrecognized name falls back to CUSTOM rather than throwing.
*/
protected IdentityMapping createDefaultIdentityMapping(ClassMapping classMapping, Property property) {
String targetName = property != null ? property.name : null
String[] identifierNames = targetName != null ? [targetName] as String[] : [MappingFactory.IDENTITY_PROPERTY] as String[]
String generatorName = property != null ? property.generator : null
ValueGenerator generator
if (generatorName != null) {
try {
generator = ValueGenerator.valueOf(generatorName.toUpperCase(Locale.ENGLISH))
} catch (IllegalArgumentException ignored) {
generator = ValueGenerator.CUSTOM
}
} else {
generator = ValueGenerator.AUTO
}
new DefaultIdentityMapping(classMapping, property, identifierNames, generator)
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.env.PropertyResolver;

import javax.annotation.PreDestroy;
import javax.persistence.FlushModeType;
import jakarta.annotation.PreDestroy;
import jakarta.persistence.FlushModeType;
import java.io.Closeable;
import java.io.IOException;
import java.io.Serializable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.persistence.FlushModeType;
import jakarta.persistence.FlushModeType;
import java.io.IOException;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.persistence.CascadeType;
import jakarta.persistence.CascadeType;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
Expand Down Expand Up @@ -729,9 +729,11 @@ private Map<String, Object> readNodePropertiesForInsert(PendingInsert entityInse
return simpleProps;
}

@SuppressWarnings("unchecked")
private void applyCustomType(EntityAccess access, PersistentProperty property, Map<String, Object> simpleProps) {
Custom<Map<String, Object>> custom = (Custom<Map<String, Object>>) property;
final CustomTypeMarshaller<Object, Map<String, Object>, Map<String, Object>> customTypeMarshaller = custom.getCustomTypeMarshaller();
Custom<?> custom = (Custom<?>) property;
final CustomTypeMarshaller<Object, Map<String, Object>, Map<String, Object>> customTypeMarshaller =
(CustomTypeMarshaller<Object, Map<String, Object>, Map<String, Object>>) custom.getCustomTypeMarshaller();
Object value = access.getProperty(property.getName());
customTypeMarshaller.write(custom, value, simpleProps);
}
Expand Down
Loading
Loading