You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by dw...@apache.org on 2021/12/19 11:05:20 UTC

[lucene] 01/03: Revert "Reverting back to 9d19b49038b."

This is an automated email from the ASF dual-hosted git repository.

dweiss pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/lucene.git

commit c3d3d0703b1b4b027e411a3f474a75825a144dd7
Author: Dawid Weiss <da...@carrotsearch.com>
AuthorDate: Sun Dec 19 11:59:13 2021 +0100

    Revert "Reverting back to 9d19b49038b."
    
    This reverts commit d2a022f49bb84a409c96307febd314da6eb22353.
---
 build.gradle                                       |   9 +-
 gradle/documentation/render-javadoc.gradle         |  68 +++--
 gradle/ide/eclipse.gradle                          |   6 +-
 gradle/ide/intellij-idea.gradle                    |   2 +-
 gradle/java/jar-manifest.gradle                    |  32 ---
 gradle/java/javac.gradle                           |   3 +-
 gradle/java/modules-debugging.gradle               |  84 ++++++
 gradle/java/modules.gradle                         | 254 ++++++++++++++++++
 gradle/maven/publications.gradle                   |  10 +-
 gradle/testing/randomization.gradle                |   4 +-
 gradle/validation/ecj-lint.gradle                  |  60 ++++-
 gradle/validation/ecj-lint/ecj.javadocs.prefs      |   8 +-
 lucene/CHANGES.txt                                 |   3 +
 lucene/analysis/common/build.gradle                |   2 +-
 lucene/analysis/common/src/java/module-info.java   | 222 ++++++++++++++++
 lucene/analysis/icu/build.gradle                   |   6 +-
 lucene/analysis/icu/src/java/module-info.java      |  37 +++
 lucene/analysis/kuromoji/build.gradle              |   4 +-
 lucene/analysis/kuromoji/src/java/module-info.java |  40 +++
 .../{kuromoji => morfologik.tests}/build.gradle    |  11 +-
 .../src/test/module-info.java}                     |  18 +-
 .../morfologik/tests/TestMorfologikAnalyzer.java   |  47 ++++
 lucene/analysis/morfologik/build.gradle            |  11 +-
 .../{build.gradle => src/java/module-info.java}    |  24 +-
 .../analysis/uk/UkrainianMorfologikAnalyzer.java   | 104 +++++---
 lucene/analysis/nori/build.gradle                  |   4 +-
 .../src/java/module-info.java}                     |  22 +-
 lucene/analysis/opennlp/build.gradle               |   6 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/analysis/phonetic/build.gradle              |   6 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/analysis/smartcn/build.gradle               |   4 +-
 .../src/java/module-info.java}                     |  17 +-
 lucene/analysis/stempel/build.gradle               |   4 +-
 .../src/java/module-info.java}                     |  18 +-
 lucene/backward-codecs/build.gradle                |   4 +-
 lucene/backward-codecs/src/java/module-info.java   |  47 ++++
 lucene/benchmark/build.gradle                      |  24 +-
 lucene/benchmark/src/java/module-info.java         |  40 +++
 lucene/classification/build.gradle                 |   6 +-
 .../src/java/module-info.java}                     |  18 +-
 lucene/codecs/build.gradle                         |   2 +-
 lucene/codecs/src/java/module-info.java            |  39 +++
 lucene/core/src/java/module-info.java              |  72 +++++
 lucene/demo/build.gradle                           |  12 +-
 .../src/java/module-info.java}                     |  24 +-
 lucene/distribution.tests/build.gradle             |  62 +++++
 .../lucene/distribution/TestModularLayer.java      | 295 +++++++++++++++++++++
 lucene/distribution/binary-release.gradle          |  59 +++--
 .../distribution/src/binary-release/bin/luke.cmd   |   2 +-
 lucene/distribution/src/binary-release/bin/luke.sh |   2 +-
 lucene/expressions/build.gradle                    |  17 +-
 .../src/java/module-info.java}                     |  19 +-
 lucene/facet/build.gradle                          |   6 +-
 .../src/java/module-info.java}                     |  25 +-
 lucene/grouping/build.gradle                       |   4 +-
 .../src/java/module-info.java}                     |  12 +-
 lucene/highlighter/build.gradle                    |   6 +-
 .../src/java/module-info.java}                     |  19 +-
 lucene/join/build.gradle                           |   2 +-
 .../src/java/module-info.java}                     |  11 +-
 lucene/licenses/asm-analysis-7.2.jar.sha1          |   1 +
 lucene/licenses/asm-tree-7.2.jar.sha1              |   1 +
 lucene/licenses/assertj-core-3.21.0.jar.sha1       |   1 +
 lucene/licenses/assertj-core-LICENSE-ASL.txt       | 201 ++++++++++++++
 lucene/licenses/assertj-core-NOTICE.txt            |   0
 lucene/luke/build.gradle                           |  53 ++--
 .../src/java/module-info.java}                     |  20 +-
 .../apache/lucene/luke/app/desktop/LukeMain.java   |  30 ++-
 .../lucene/luke/app/desktop/util/FontUtils.java    |   6 +-
 .../app/desktop/{font => util}/ElegantIcons.ttf    | Bin
 lucene/memory/build.gradle                         |   2 +-
 .../src/java/module-info.java}                     |  11 +-
 lucene/misc/build.gradle                           |   2 +-
 .../src/java/module-info.java}                     |  20 +-
 lucene/monitor/build.gradle                        |   6 +-
 .../src/java/module-info.java}                     |  14 +-
 lucene/queries/build.gradle                        |   2 +-
 .../src/java/module-info.java}                     |  24 +-
 lucene/queryparser/build.gradle                    |   6 +-
 lucene/queryparser/src/java/module-info.java       |  52 ++++
 lucene/replicator/build.gradle                     |   8 +-
 .../src/java/module-info.java}                     |  20 +-
 lucene/sandbox/build.gradle                        |   4 +-
 .../src/java/module-info.java}                     |  23 +-
 lucene/spatial-extras/build.gradle                 |  10 +-
 .../src/java/module-info.java}                     |  28 +-
 lucene/spatial3d/build.gradle                      |   2 +-
 .../src/java/module-info.java}                     |  12 +-
 lucene/suggest/build.gradle                        |   4 +-
 lucene/suggest/src/java/module-info.java           |  36 +++
 settings.gradle                                    |   8 +-
 versions.lock                                      |   5 +-
 versions.props                                     |   3 +-
 94 files changed, 2142 insertions(+), 498 deletions(-)

diff --git a/build.gradle b/build.gradle
index bf2f34d..220ada7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -110,6 +110,10 @@ apply from: file('buildSrc/scriptDepVersions.gradle')
 
 apply from: file('gradle/generation/local-settings.gradle')
 
+// IDE support, settings and specials.
+apply from: file('gradle/ide/intellij-idea.gradle')
+apply from: file('gradle/ide/eclipse.gradle')
+
 // Set up defaults and configure aspects for certain modules or functionality
 // (java, tests)
 apply from: file('gradle/java/folder-layout.gradle')
@@ -119,14 +123,11 @@ apply from: file('gradle/testing/randomization.gradle')
 apply from: file('gradle/testing/fail-on-no-tests.gradle')
 apply from: file('gradle/testing/alternative-jdk-support.gradle')
 apply from: file('gradle/java/jar-manifest.gradle')
+apply from: file('gradle/java/modules.gradle')
 
 // Maven artifact publishing.
 apply from: file('gradle/maven/publications.gradle')
 
-// IDE support, settings and specials.
-apply from: file('gradle/ide/intellij-idea.gradle')
-apply from: file('gradle/ide/eclipse.gradle')
-
 // Validation tasks
 apply from: file('gradle/validation/measure-task-times.gradle')
 apply from: file('gradle/validation/error-prone.gradle')
diff --git a/gradle/documentation/render-javadoc.gradle b/gradle/documentation/render-javadoc.gradle
index 11499be..9579c5d 100644
--- a/gradle/documentation/render-javadoc.gradle
+++ b/gradle/documentation/render-javadoc.gradle
@@ -59,20 +59,28 @@ allprojects {
       outputDir = project.javadoc.destinationDir
     }
 
-    task renderSiteJavadoc(type: RenderJavadocTask) {
-      description "Generates Javadoc API documentation for the site (relative links)."
-      group "documentation"
-
-      taskResources = resources
-      dependsOn sourceSets.main.compileClasspath
-      classpath = sourceSets.main.compileClasspath;
-      srcDirSet = sourceSets.main.java;
-
-      relativeProjectLinks = true
-
-      // Place the documentation under Lucene or Solr's documentation directory.
-      // docroot is defined in 'documentation.gradle'
-      outputDir = project.docroot.toPath().resolve(project.relativeDocPath).toFile()
+    if (project.path == ':lucene:luke' || project.path.endsWith(".tests")) {
+      // These projects are not part of the public API so we don't render their javadocs
+      // as part of the site's creation. A side-effect of this is that javadocs would not
+      // be linted for these projects. To avoid this, we connect the regular javadoc task
+      // to check so that everything is validated.
+      project.tasks.getByName("check").dependsOn renderJavadoc
+    } else {
+      task renderSiteJavadoc(type: RenderJavadocTask) {
+        description "Generates Javadoc API documentation for the site (relative links)."
+        group "documentation"
+
+        taskResources = resources
+        dependsOn sourceSets.main.compileClasspath
+        classpath = sourceSets.main.compileClasspath
+        srcDirSet = sourceSets.main.java
+
+        relativeProjectLinks = true
+
+        // Place the documentation under the documentation directory.
+        // docroot is defined in 'documentation.gradle'
+        outputDir = project.docroot.toPath().resolve(project.relativeDocPath).toFile()
+      }
     }
   }
 }
@@ -257,10 +265,6 @@ configure(subprojects) {
   }
 }
 
-configure(project(':lucene:luke')) {
-  project.tasks.matching { it.name == 'renderSiteJavadoc' }.configureEach { it.enabled = false }
-}
-
 class RenderJavadocTask extends DefaultTask {
   @InputFiles
   @SkipWhenEmpty
@@ -329,16 +333,18 @@ class RenderJavadocTask extends DefaultTask {
 
   @TaskAction
   public void render() {
-    def srcDirs = srcDirSet.srcDirs.findAll { dir -> dir.exists() }
+    def srcDirs = srcDirSet.sourceDirectories.filter { dir -> dir.exists() }
+
     def optionsFile = project.file("${getTemporaryDir()}/javadoc-options.txt")
     
     // create the directory, so relative link calculation knows that it's a directory:
     outputDir.mkdirs();
     
     def opts = []
-    opts << [ '-overview', project.file("${srcDirs[0]}/overview.html") ]
-    opts << [ '-sourcepath', srcDirs.join(File.pathSeparator) ]
-    opts << [ '-subpackages', project.path.startsWith(':lucene') ? 'org.apache.lucene' : 'org.apache.solr' ]
+
+    def overviewSourceSetDir = srcDirs.filter { dir -> project.file("${dir}/overview.html").exists() }.singleFile
+    opts << [ '-overview', project.file("${overviewSourceSetDir}/overview.html") ]
+
     opts << [ '-d', outputDir ]
     opts << '-protected'
     opts << [ '-encoding', 'UTF-8' ]
@@ -430,6 +436,24 @@ class RenderJavadocTask extends DefaultTask {
     def jOpts = opts.findAll { opt -> opt instanceof String && opt.startsWith("-J") }
     opts.removeAll(jOpts)
 
+    // Collect all source files, for now excluding module descriptors.
+    opts.addAll(
+        srcDirs.collectMany { dir ->
+          project.fileTree(dir: dir, include: "**/*.java", exclude: "**/module-info.java").files
+        }.collect { it.toString() }
+    )
+
+    // handle doc-files manually since in explicit source file mode javadoc does not copy them.
+    srcDirs.each { File dir ->
+      project.copy {
+        into outputDir
+
+        from(dir, {
+          include "**/doc-files/**"
+        })
+      }
+    }
+
     // Temporary file that holds all javadoc options for the current task (except jOpts)
     optionsFile.withWriter("UTF-8", { writer ->
       // escapes an option with single quotes or whitespace to be passed in the options.txt file for
diff --git a/gradle/ide/eclipse.gradle b/gradle/ide/eclipse.gradle
index 0a5b608..4db9ae3 100644
--- a/gradle/ide/eclipse.gradle
+++ b/gradle/ide/eclipse.gradle
@@ -58,7 +58,11 @@ configure(rootProject) {
             jars += prj.configurations.testCompileClasspath.resolve()
           }
 
-          classpath.entries += sources.sort().collect {name -> new SourceFolder(name, "build/eclipse/" + name) }
+          classpath.entries += sources.sort().collect { name -> 
+            def sourceFolder = new SourceFolder(name, "build/eclipse/" + name) 
+            sourceFolder.setExcludes(["module-info.java"])
+            return sourceFolder
+          }
           classpath.entries += jars.unique().findAll { location -> location.isFile() }.collect { location ->
             new LibEntry(location.toString())
           }
diff --git a/gradle/ide/intellij-idea.gradle b/gradle/ide/intellij-idea.gradle
index 1d5b8d2..589aaec 100644
--- a/gradle/ide/intellij-idea.gradle
+++ b/gradle/ide/intellij-idea.gradle
@@ -16,7 +16,7 @@
  */
 
 // Try to detect IntelliJ model loader ("reimport") early.
-def isIdea = System.getProperty("idea.active") != null ||
+rootProject.ext.isIdea = System.getProperty("idea.active") != null ||
     gradle.startParameter.taskNames.contains('idea') ||
     gradle.startParameter.taskNames.contains('cleanIdea')
 
diff --git a/gradle/java/jar-manifest.gradle b/gradle/java/jar-manifest.gradle
index d4ab812..359b579 100644
--- a/gradle/java/jar-manifest.gradle
+++ b/gradle/java/jar-manifest.gradle
@@ -69,12 +69,6 @@ subprojects {
               "X-Build-OS"            : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
           ]
 
-          // Only apply automatic module name to jar task.
-          if (task.name in ["jar"]) {
-            manifestAttrs["Automatic-Module-Name"] =
-              "${->  project.path.replaceFirst(/^:lucene/, Matcher.quoteReplacement(project.group)).replace(':', '.').replace('-', '_')}"
-          }
-
           manifest {
             attributes(manifestAttrs)
           }
@@ -88,29 +82,3 @@ subprojects {
           }
       }
 }
-
-configure(rootProject) {
-  tasks.register("showModuleNames", { showModuleTask ->
-    def allJarTasks = []
-
-    rootProject.subprojects.each { subproject ->
-      subproject.tasks.matching { it.name == 'jar' }.all {
-        allJarTasks.add it
-      }
-    }
-
-    dependsOn allJarTasks
-
-    doFirst {
-      allJarTasks.each { jarTask ->
-        File jarFile = jarTask.outputs.files.singleFile
-        try (def jar = new JarFile(jarFile)) {
-          logger.lifecycle(String.format(Locale.ROOT,
-              "%-50s -> %s",
-              jarFile.name,
-              jar.manifest.mainAttributes.getValue("Automatic-Module-Name")))
-        }
-      }
-    }
-  })
-}
diff --git a/gradle/java/javac.gradle b/gradle/java/javac.gradle
index 78c5fb4..25f66c3 100644
--- a/gradle/java/javac.gradle
+++ b/gradle/java/javac.gradle
@@ -39,7 +39,8 @@ allprojects {
         "-Xlint:dep-ann",
         "-Xlint:divzero",
         "-Xlint:empty",
-        "-Xlint:exports",
+        // TODO: uh-oh we have broken APIs.
+        "-Xlint:-exports",
         "-Xlint:fallthrough",
         "-Xlint:finally",
         "-Xlint:opens",
diff --git a/gradle/java/modules-debugging.gradle b/gradle/java/modules-debugging.gradle
new file mode 100644
index 0000000..52bc6fe
--- /dev/null
+++ b/gradle/java/modules-debugging.gradle
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Debugging/ validation utilities and helpers to aid transition
+// to java modules.
+
+allprojects {
+  plugins.withType(JavaPlugin) {
+    // Show all non-empty package names
+    tasks.register("showPackageNames", { task ->
+      doFirst {
+        listPackageNames(sourceSets).each { println(it) }
+      }
+    })
+
+    tasks.register("showServiceProviders", { task ->
+      doFirst {
+        def services = listServices(sourceSets)
+        services.each { entry -> {
+          println(entry.key)
+          entry.value.each { println("  ${it}") }
+        }}
+      }
+    })
+  }
+}
+
+/* Utility method to collect all package names in a source sets. */
+static def listPackageNames(SourceSetContainer sourceSets) {
+  var pkgNameSet = [] as Set<String>
+  sourceSets.main.each { sourceSet ->
+    var dirs = sourceSet.allJava.srcDirTrees.collect { it.dir.toPath() }
+    var pattern = new PatternSet()
+        .include('**/*.java')
+        .exclude('module-info.java')
+        .exclude('**/package-info.java')
+    sourceSet.allJava.matching(pattern).each {srcFile ->
+      var srcPath = srcFile.toPath()
+      var dir = dirs.find { srcPath.startsWith(it) }
+      var pkgName = srcPath.subpath(dir.nameCount, srcPath.nameCount).parent.stream().map(Object::toString).collect(Collectors.joining('.'))
+      pkgNameSet.add(pkgName)
+    }
+  }
+  var pkgNames = pkgNameSet as List<String>
+  pkgNames.sort()
+  return pkgNames
+}
+
+/* Utility method to collect all service providers in a source sets. */
+static def listServices(SourceSetContainer sourceSets) {
+  def services = [:] as Map<String, List<String>>
+  sourceSets.main.each {sourceSet ->
+    var pattern = new PatternSet().include('META-INF/services/*')
+    sourceSet.resources.matching(pattern).each {file ->
+      def serviceName = file.name
+      def providers = []
+      file.withReader { reader -> {
+        reader.lines().each { l ->
+          def line = l.trim()
+          if (line != "" && !line.startsWith("#")) {
+            def provider = line.replace('$', '.')
+            providers.add(provider)
+          }
+        }
+      }}
+      services.put(serviceName, providers)
+    }
+  }
+  return services
+}
diff --git a/gradle/java/modules.gradle b/gradle/java/modules.gradle
new file mode 100644
index 0000000..45405a2
--- /dev/null
+++ b/gradle/java/modules.gradle
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Configure miscellaneous aspects required for supporting the java module system layer.
+
+allprojects {
+  plugins.withType(JavaPlugin) {
+    // We won't be using gradle's built-in automatic module finder.
+    java {
+      modularity.inferModulePath.set(false)
+    }
+
+    // Map convention configuration names to "modular" corresponding configurations.
+    Closure<String> moduleConfigurationNameFor = { String configurationName ->
+      return "module" + configurationName.capitalize().replace("Classpath", "Path")
+    }
+
+    //
+    // For each source set, create explicit configurations for declaring modular dependencies.
+    // These "modular" configurations correspond 1:1 to Gradle's conventions but have a 'module' prefix
+    // and a capitalized remaining part of the conventional name. For example, an 'api' configuration in
+    // the main source set would have a corresponding 'moduleApi' configuration for declaring modular
+    // dependencies.
+    //
+    // Gradle's java plugin "convention" configurations extend from their modular counterparts
+    // so all dependencies end up on classpath by default for backward compatibility with other
+    // tasks and gradle infrastructure.
+    //
+    // At the same time, we also know which dependencies (and their transitive graph of dependencies!)
+    // should be placed on module-path only.
+    //
+    // Note that an explicit configuration of modular dependencies also opens up the possibility of automatically
+    // validating whether the dependency configuration for a gradle project is consistent with the information in
+    // the module-info descriptor because there is a (nearly?) direct correspondence between the two:
+    //
+    // moduleApi            - 'requires transitive'
+    // moduleImplementation - 'requires'
+    // moduleCompileOnly    - 'requires static'
+    //
+    project.sourceSets.all { SourceSet sourceSet ->
+      ConfigurationContainer configurations = project.configurations
+
+      // Create modular configurations for convention configurations.
+      Closure<Configuration> createModuleConfigurationForConvention = { String configurationName ->
+        Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
+        Configuration moduleConfiguration = configurations.maybeCreate(moduleConfigurationNameFor(configurationName))
+        moduleConfiguration.canBeConsumed(false)
+        moduleConfiguration.canBeResolved(false)
+        conventionConfiguration.extendsFrom(moduleConfiguration)
+
+        project.logger.info("Created module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
+        return moduleConfiguration
+      }
+
+      Configuration moduleApi = createModuleConfigurationForConvention(sourceSet.apiConfigurationName)
+      Configuration moduleImplementation = createModuleConfigurationForConvention(sourceSet.implementationConfigurationName)
+      moduleImplementation.extendsFrom(moduleApi)
+      Configuration moduleRuntimeOnly = createModuleConfigurationForConvention(sourceSet.runtimeOnlyConfigurationName)
+      Configuration moduleCompileOnly = createModuleConfigurationForConvention(sourceSet.compileOnlyConfigurationName)
+      // sourceSet.compileOnlyApiConfigurationName  // This seems like a very esoteric use case, leave out.
+
+      // Set up compilation module path configuration combining corresponding convention configurations.
+      Closure<Configuration> createResolvableModuleConfiguration = { String configurationName ->
+        Configuration conventionConfiguration = configurations.maybeCreate(configurationName)
+        Configuration moduleConfiguration = configurations.maybeCreate(
+            moduleConfigurationNameFor(conventionConfiguration.name))
+        moduleConfiguration.canBeConsumed(false)
+        moduleConfiguration.canBeResolved(true)
+        moduleConfiguration.attributes {
+          // Prefer class folders over JARs. The exception is made for tests projects which require a composition
+          // of classes and resources, otherwise split into two folders.
+          if (project.name.endsWith(".tests")) {
+            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
+          } else {
+            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.CLASSES))
+          }
+        }
+
+        project.logger.info("Created resolvable module configuration for '${conventionConfiguration.name}': ${moduleConfiguration.name}")
+        return moduleConfiguration
+      }
+
+      Configuration compileModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.compileClasspathConfigurationName)
+      compileModulePathConfiguration.extendsFrom(moduleCompileOnly, moduleImplementation)
+
+      Configuration runtimeModulePathConfiguration = createResolvableModuleConfiguration(sourceSet.runtimeClasspathConfigurationName)
+      runtimeModulePathConfiguration.extendsFrom(moduleRuntimeOnly, moduleImplementation)
+
+      // Create and register a source set extension for manipulating classpath/ module-path
+      ModularPathsExtension modularPaths = new ModularPathsExtension(project, sourceSet,
+          compileModulePathConfiguration,
+          runtimeModulePathConfiguration)
+      sourceSet.extensions.add("modularPaths", modularPaths)
+
+      // Customized the JavaCompile for this source set so that it has proper module path.
+      tasks.named(sourceSet.getCompileJavaTaskName()).configure({ JavaCompile task ->
+        task.dependsOn modularPaths.compileModulePathConfiguration
+
+        // Add modular dependencies and their transitive dependencies to module path.
+        task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) {
+          def modularPathFiles = modularPaths.compileModulePathConfiguration.files
+          def extraArgs = []
+          if (!modularPathFiles.isEmpty()) {
+            if (!modularPaths.hasModuleDescriptor()) {
+              // We're compiling a non-module so we'll bring everything on module path in
+              // otherwise things wouldn't be part of the resolved module graph.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            }
+
+            extraArgs += ["--module-path", modularPathFiles.join(File.pathSeparator)]
+          }
+
+          task.logger.info("Module path for ${task.path}:\n  " + modularPathFiles.sort().join("\n  "))
+
+          return extraArgs
+        })
+
+        // LUCENE-10304: if we modify the classpath here, IntelliJ no longer sees the dependencies as compile-time
+        // dependencies, don't know why.
+        if (!rootProject.ext.isIdea) {
+          // Modify the default classpath by removing anything already placed on module path.
+          // This could be done in a fancier way but a set difference is just fine for us here. Use a lazy
+          // provider to delay computation of the actual path.
+          task.classpath = files({ ->
+            def trimmedClasspath = sourceSet.compileClasspath - modularPaths.compileModulePathConfiguration
+            task.logger.info("Class path for ${task.path}:\n  " + trimmedClasspath.files.sort().join("\n  "))
+            return trimmedClasspath
+          })
+        }
+      })
+    }
+
+    //
+    // Configure the (default) test task to use module paths.
+    //
+    // There is no explicit connection between source sets and test tasks so there is no way (?)
+    // to do this automatically, convention-style.
+
+    // This closure can be used to configure a different task, with a different source set, should we
+    // have the need for it.
+    Closure<Void> configureTestTaskForSourceSet = { Test task, SourceSet sourceSet ->
+      task.configure {
+        Configuration modulePath = task.project.configurations.maybeCreate(
+            moduleConfigurationNameFor(sourceSet.getRuntimeClasspathConfigurationName()))
+
+        task.dependsOn modulePath
+
+        // Add modular dependencies and their transitive dependencies to module path.
+        task.jvmArgumentProviders.add((CommandLineArgumentProvider) {
+          def extraArgs = []
+
+          // Determine whether the source set classes themselves should be appended
+          // to classpath or module path.
+          boolean sourceSetIsAModule = sourceSet.modularPaths.hasModuleDescriptor()
+
+          if (!modulePath.isEmpty() || sourceSetIsAModule) {
+            if (sourceSetIsAModule) {
+              // Add source set outputs to module path.
+              extraArgs += ["--module-path", (modulePath + sourceSet.output.classesDirs).files.join(File.pathSeparator)]
+              // Ideally, we should only add the sourceset's module here, everything else would be resolved via the
+              // module descriptor. But this would require parsing the module descriptor and may cause JVM version conflicts
+              // so keeping it simple.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            } else {
+              extraArgs += ["--module-path", modulePath.files.join(File.pathSeparator)]
+              // In this case we're running a non-module against things on the module path so let's bring in
+              // everything on module path into the resolution graph.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            }
+          }
+
+          task.logger.info("Module path for ${task.path}:\n  " + modulePath.files.sort().join("\n  "))
+
+          return extraArgs
+        })
+
+
+        // Modify the default classpath by removing anything already placed on module path.
+        // This could be done in a fancier way but a set difference is just fine for us here. Use a lazy
+        // provider to delay computation of the actual path.
+        task.classpath = files({ ->
+          def trimmedClasspath = sourceSet.runtimeClasspath - modulePath
+
+          boolean sourceSetIsAModule = sourceSet.modularPaths.hasModuleDescriptor()
+          if (sourceSetIsAModule) {
+            // also subtract the sourceSet's output directories.
+            trimmedClasspath = trimmedClasspath - sourceSet.output.classesDirs
+          }
+
+          task.logger.info("Class path for ${task.path}:\n  " + trimmedClasspath.files.sort().join("\n  "))
+          return trimmedClasspath
+        })
+      }
+    }
+
+    // Configure (tasks.test, sourceSets.test)
+    tasks.matching { it.name == "test" }.all { Test task ->
+      configureTestTaskForSourceSet(task, task.project.sourceSets.test)
+    }
+
+    // Configure module versions.
+    tasks.withType(JavaCompile).configureEach { task ->
+      // TODO: LUCENE-10267: workaround for gradle bug. Remove when the corresponding issue is fixed.
+      task.options.compilerArgumentProviders.add((CommandLineArgumentProvider) { ->
+        if (task.getClasspath().isEmpty()) {
+          return ["--module-version", project.version.toString()]
+        } else {
+          return []
+        }
+      })
+
+      task.options.javaModuleVersion.set(provider {
+        return project.version.toString()
+      })
+    }
+  }
+}
+
+
+class ModularPathsExtension {
+  Project project
+  SourceSet sourceSet
+  Configuration compileModulePathConfiguration
+  Configuration runtimeModulePathConfiguration
+
+  ModularPathsExtension(Project project, SourceSet sourceSet,
+                        Configuration compileModulePathConfiguration,
+                        Configuration runtimeModulePathConfiguration) {
+    this.project =  project
+    this.sourceSet = sourceSet
+    this.compileModulePathConfiguration = compileModulePathConfiguration
+    this.runtimeModulePathConfiguration = runtimeModulePathConfiguration
+  }
+
+  boolean hasModuleDescriptor() {
+    return sourceSet.allJava.srcDirs.stream()
+        .map(dir -> new File(dir, "module-info.java"))
+        .anyMatch(file -> file.exists())
+  }
+}
\ No newline at end of file
diff --git a/gradle/maven/publications.gradle b/gradle/maven/publications.gradle
index 32422cd..cc9bc96 100644
--- a/gradle/maven/publications.gradle
+++ b/gradle/maven/publications.gradle
@@ -31,15 +31,19 @@
 configure(rootProject) {
   ext {
     mavenProjects = project(":lucene").subprojects.findAll {subproject ->
-      return !(subproject.path in [
-          // Exclude distribution assembly & documentation.
+      def excluded = [
+          // Exclude distribution assembly, tests & documentation.
           ":lucene:distribution",
           ":lucene:documentation",
           // Exclude the parent container project for analysis modules (no artifacts).
           ":lucene:analysis",
           // Exclude the native module.
           ":lucene:misc:native"
-      ])
+      ]
+
+      // Exclude all subprojects that are modular test projects and those explicitly
+      // excluded above.
+      return !(subproject.path.endsWith(".tests") || subproject.path in excluded)
     }
   }
 }
diff --git a/gradle/testing/randomization.gradle b/gradle/testing/randomization.gradle
index fc6c991..938ca39 100644
--- a/gradle/testing/randomization.gradle
+++ b/gradle/testing/randomization.gradle
@@ -164,7 +164,9 @@ allprojects {
         // Enable security manager, if requested. We could move the selection of security manager and security policy
         // to each project's build/ configuration but it seems compact enough to keep it here for now.
         if (Boolean.parseBoolean(testOptionsResolved["tests.useSecurityManager"])) {
-          if (project.path == ":lucene:replicator") {
+          if (project.path.endsWith(".tests")) {
+            // LUCENE-10301: for now, do not use the security manager for modular tests (test framework is not available).
+          } else if (project.path == ":lucene:replicator") {
             systemProperty 'java.security.manager', "org.apache.lucene.util.TestSecurityManager"
             systemProperty 'java.security.policy', file("${resources}/policies/replicator-tests.policy")
           } else if (project.path.startsWith(":lucene")) {
diff --git a/gradle/validation/ecj-lint.gradle b/gradle/validation/ecj-lint.gradle
index 398c26e..591eb84 100644
--- a/gradle/validation/ecj-lint.gradle
+++ b/gradle/validation/ecj-lint.gradle
@@ -35,9 +35,10 @@ allprojects {
     // with a non-empty java.srcDirs. These tasks are then
     // attached to project's "ecjLint" task.
     def lintTasks = sourceSets.collect { sourceSet ->
-      def srcDirs = sourceSet.java.srcDirs.findAll { dir -> dir.exists() }
+      def srcDirs = sourceSet.java.sourceDirectories
+          .filter { dir -> dir.exists() }
 
-      tasks.create(sourceSet.getTaskName("ecjLint", null), JavaExec, {task ->
+      tasks.create(sourceSet.getTaskName("ecjLint", null), JavaExec, {JavaExec task ->
         // This dependency is on a configuration; technically it causes
         // all dependencies to be resolved before this task executes
         // (this includes scheduling tasks that compile the
@@ -60,6 +61,24 @@ allprojects {
         def tmpDst = getTemporaryDir()
         workingDir tmpDst
 
+        // Place input files in an external file to dodge command line argument
+        // limits. We could pass a directory but ecj seems to be buggy: when it
+        // encounters a module-info.java file it no longer compiles other source files.
+        def inputsFile = file("${tmpDst}/ecj-inputs.txt")
+        // escape filename accoring to ECJ's rules:
+        // https://github.com/eclipse/aspectj.eclipse.jdt.core/blob/a05312e746b9bc2b48b4b039f6e7b5e061b5b393/org.eclipse.jdt.core/batch/org/eclipse/jdt/internal/compiler/batch/Main.java#L1533-L1537
+        // Basically surround all whitespace by quotes:
+        def escapeFileName = { String s -> s.replaceAll(/ +/, /"$0"/) }
+        inputsFile.setText(
+            srcDirs.collectMany { dir ->
+              project.fileTree(dir: dir, include: "**/*.java" ).files
+            }
+            // Try to sort all input files; a side-effect of this should be that module-info.java
+            // is placed first on the list, which works around ECJ bug:
+            // https://bugs.eclipse.org/bugs/show_bug.cgi?id=569833
+            .sort()
+            .collect {file -> escapeFileName(file.absolutePath.toString())}.join("\n"), "UTF-8")
+
         args += [ "-d", "none" ]
 
         // Compilation environment.
@@ -72,22 +91,41 @@ allprojects {
         args += [ "-enableJavadoc" ]
         args += [ "-properties", file("${resources}/ecj.javadocs.prefs").absolutePath ]
 
-        doFirst {
-          tmpDst.mkdirs()
+        // We depend on modular paths.
+        def modularPaths = sourceSet.modularPaths
+        dependsOn modularPaths.compileModulePathConfiguration
+
+        task.argumentProviders.add((CommandLineArgumentProvider) {
+          // Add modular dependencies and their transitive dependencies to module path.
+          def modularPathFiles = modularPaths.compileModulePathConfiguration.files
+          def extraArgs = []
+          if (!modularPathFiles.isEmpty()) {
+            if (!modularPaths.hasModuleDescriptor()) {
+              // We're compiling a non-module so we'll bring everything on module path in
+              // otherwise things wouldn't be part of the resolved module graph.
+              extraArgs += ["--add-modules", "ALL-MODULE-PATH"]
+            }
+
+            extraArgs += ["--module-path", modularPathFiles.join(File.pathSeparator)]
+          }
 
-          // Add classpath locations at execution time (can't resolve the
+          // Add classpath locations in a lazy provider (can't resolve the
           // configuration at evaluation time). Filter out non-existing entries
           // (output folders for non-existing input source dirs like resources).
           def cpath = sourceSet.compileClasspath.filter { p -> p.exists() }
+          cpath = cpath - modularPathFiles
           if (!cpath.isEmpty()) {
-            args += ["-classpath", cpath.asPath]
+            extraArgs += ["-classpath", cpath.join(File.pathSeparator)]
           }
 
-          // Add source location(s). Ideally we'd provide a set of files as in:
-          // args += sourceSet.java.files
-          // but this exceeds max allowed command line size. So we pass source
-          // directories instead:
-          args += srcDirs
+          // Add source location(s) in an external file to avoid command line argument limits.
+          extraArgs += ["@" + inputsFile.absolutePath]
+
+          return extraArgs
+        })
+
+        doFirst {
+          tmpDst.mkdirs()
         }
       })
     }
diff --git a/gradle/validation/ecj-lint/ecj.javadocs.prefs b/gradle/validation/ecj-lint/ecj.javadocs.prefs
index a5352aa..25d527a 100644
--- a/gradle/validation/ecj-lint/ecj.javadocs.prefs
+++ b/gradle/validation/ecj-lint/ecj.javadocs.prefs
@@ -17,7 +17,8 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.doc.comment.support=enabled
-org.eclipse.jdt.core.compiler.problem.APILeak=error
+# TODO: disabled because we do have api leaks in modules
+org.eclipse.jdt.core.compiler.problem.APILeak=ignore
 org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=error
 org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=error
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
@@ -120,7 +121,8 @@ org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=error
 org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
 org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
 org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
-org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=error
+# TODO: ideally, we shouldn't rely on these... but we do.
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=ignore
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
@@ -144,4 +146,4 @@ org.eclipse.jdt.core.compiler.release=disabled
 org.eclipse.jdt.core.compiler.source=11
 org.eclipse.jdt.core.compiler.taskCaseSensitive=enabled
 org.eclipse.jdt.core.compiler.taskPriorities=HIGH
-org.eclipse.jdt.core.compiler.taskTags=nocommit
+org.eclipse.jdt.core.compiler.taskTags=nocommit
\ No newline at end of file
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 6460edd..0584b68 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -14,6 +14,9 @@ API Changes
 New Features
 ---------------------
 
+* LUCENE-10255: Lucene JARs are now proper modules, with module descriptors and dependency information.
+  (Chris Hegarty, Uwe Schindler, Tomoko Uchida, Dawid Weiss)
+
 * LUCENE-10223: Add interval function support to StandardQueryParser. Add min-should-match operator
   support to StandardQueryParser. Update and clean up package documentation in flexible query parser
   module. (Dawid Weiss, Alan Woodward)
diff --git a/lucene/analysis/common/build.gradle b/lucene/analysis/common/build.gradle
index 6fba70a..1815041 100644
--- a/lucene/analysis/common/build.gradle
+++ b/lucene/analysis/common/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Analyzers for indexing content in different languages and domains'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 }
 
diff --git a/lucene/analysis/common/src/java/module-info.java b/lucene/analysis/common/src/java/module-info.java
new file mode 100644
index 0000000..e736a95
--- /dev/null
+++ b/lucene/analysis/common/src/java/module-info.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Lucene Analysis Common. */
+module org.apache.lucene.analysis.common {
+  requires java.xml;
+  requires org.apache.lucene.core;
+
+  exports org.apache.lucene.analysis.ar;
+  exports org.apache.lucene.analysis.bg;
+  exports org.apache.lucene.analysis.bn;
+  exports org.apache.lucene.analysis.boost;
+  exports org.apache.lucene.analysis.br;
+  exports org.apache.lucene.analysis.ca;
+  exports org.apache.lucene.analysis.charfilter;
+  exports org.apache.lucene.analysis.cjk;
+  exports org.apache.lucene.analysis.ckb;
+  exports org.apache.lucene.analysis.classic;
+  exports org.apache.lucene.analysis.commongrams;
+  exports org.apache.lucene.analysis.compound.hyphenation;
+  exports org.apache.lucene.analysis.compound;
+  exports org.apache.lucene.analysis.core;
+  exports org.apache.lucene.analysis.custom;
+  exports org.apache.lucene.analysis.cz;
+  exports org.apache.lucene.analysis.da;
+  exports org.apache.lucene.analysis.de;
+  exports org.apache.lucene.analysis.el;
+  exports org.apache.lucene.analysis.email;
+  exports org.apache.lucene.analysis.en;
+  exports org.apache.lucene.analysis.es;
+  exports org.apache.lucene.analysis.et;
+  exports org.apache.lucene.analysis.eu;
+  exports org.apache.lucene.analysis.fa;
+  exports org.apache.lucene.analysis.fi;
+  exports org.apache.lucene.analysis.fr;
+  exports org.apache.lucene.analysis.ga;
+  exports org.apache.lucene.analysis.gl;
+  exports org.apache.lucene.analysis.hi;
+  exports org.apache.lucene.analysis.hu;
+  exports org.apache.lucene.analysis.hunspell;
+  exports org.apache.lucene.analysis.hy;
+  exports org.apache.lucene.analysis.id;
+  exports org.apache.lucene.analysis.in;
+  exports org.apache.lucene.analysis.it;
+  exports org.apache.lucene.analysis.lt;
+  exports org.apache.lucene.analysis.lv;
+  exports org.apache.lucene.analysis.minhash;
+  exports org.apache.lucene.analysis.miscellaneous;
+  exports org.apache.lucene.analysis.ne;
+  exports org.apache.lucene.analysis.ngram;
+  exports org.apache.lucene.analysis.nl;
+  exports org.apache.lucene.analysis.no;
+  exports org.apache.lucene.analysis.path;
+  exports org.apache.lucene.analysis.pattern;
+  exports org.apache.lucene.analysis.payloads;
+  exports org.apache.lucene.analysis.pt;
+  exports org.apache.lucene.analysis.query;
+  exports org.apache.lucene.analysis.reverse;
+  exports org.apache.lucene.analysis.ro;
+  exports org.apache.lucene.analysis.ru;
+  exports org.apache.lucene.analysis.shingle;
+  exports org.apache.lucene.analysis.sinks;
+  exports org.apache.lucene.analysis.snowball;
+  exports org.apache.lucene.analysis.sr;
+  exports org.apache.lucene.analysis.sv;
+  exports org.apache.lucene.analysis.synonym;
+  exports org.apache.lucene.analysis.ta;
+  exports org.apache.lucene.analysis.te;
+  exports org.apache.lucene.analysis.th;
+  exports org.apache.lucene.analysis.tr;
+  exports org.apache.lucene.analysis.util;
+  exports org.apache.lucene.analysis.wikipedia;
+  exports org.apache.lucene.collation.tokenattributes;
+  exports org.apache.lucene.collation;
+  exports org.tartarus.snowball.ext;
+  exports org.tartarus.snowball;
+
+  provides org.apache.lucene.analysis.CharFilterFactory with
+      org.apache.lucene.analysis.charfilter.HTMLStripCharFilterFactory,
+      org.apache.lucene.analysis.charfilter.MappingCharFilterFactory,
+      org.apache.lucene.analysis.cjk.CJKWidthCharFilterFactory,
+      org.apache.lucene.analysis.fa.PersianCharFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternReplaceCharFilterFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.tr.ApostropheFilterFactory,
+      org.apache.lucene.analysis.ar.ArabicNormalizationFilterFactory,
+      org.apache.lucene.analysis.ar.ArabicStemFilterFactory,
+      org.apache.lucene.analysis.bg.BulgarianStemFilterFactory,
+      org.apache.lucene.analysis.boost.DelimitedBoostTokenFilterFactory,
+      org.apache.lucene.analysis.bn.BengaliNormalizationFilterFactory,
+      org.apache.lucene.analysis.bn.BengaliStemFilterFactory,
+      org.apache.lucene.analysis.br.BrazilianStemFilterFactory,
+      org.apache.lucene.analysis.cjk.CJKBigramFilterFactory,
+      org.apache.lucene.analysis.cjk.CJKWidthFilterFactory,
+      org.apache.lucene.analysis.ckb.SoraniNormalizationFilterFactory,
+      org.apache.lucene.analysis.ckb.SoraniStemFilterFactory,
+      org.apache.lucene.analysis.classic.ClassicFilterFactory,
+      org.apache.lucene.analysis.commongrams.CommonGramsFilterFactory,
+      org.apache.lucene.analysis.commongrams.CommonGramsQueryFilterFactory,
+      org.apache.lucene.analysis.compound.DictionaryCompoundWordTokenFilterFactory,
+      org.apache.lucene.analysis.compound.HyphenationCompoundWordTokenFilterFactory,
+      org.apache.lucene.analysis.core.DecimalDigitFilterFactory,
+      org.apache.lucene.analysis.core.LowerCaseFilterFactory,
+      org.apache.lucene.analysis.core.StopFilterFactory,
+      org.apache.lucene.analysis.core.TypeTokenFilterFactory,
+      org.apache.lucene.analysis.core.UpperCaseFilterFactory,
+      org.apache.lucene.analysis.cz.CzechStemFilterFactory,
+      org.apache.lucene.analysis.de.GermanLightStemFilterFactory,
+      org.apache.lucene.analysis.de.GermanMinimalStemFilterFactory,
+      org.apache.lucene.analysis.de.GermanNormalizationFilterFactory,
+      org.apache.lucene.analysis.de.GermanStemFilterFactory,
+      org.apache.lucene.analysis.el.GreekLowerCaseFilterFactory,
+      org.apache.lucene.analysis.el.GreekStemFilterFactory,
+      org.apache.lucene.analysis.en.EnglishMinimalStemFilterFactory,
+      org.apache.lucene.analysis.en.EnglishPossessiveFilterFactory,
+      org.apache.lucene.analysis.en.KStemFilterFactory,
+      org.apache.lucene.analysis.en.PorterStemFilterFactory,
+      org.apache.lucene.analysis.es.SpanishLightStemFilterFactory,
+      org.apache.lucene.analysis.es.SpanishMinimalStemFilterFactory,
+      org.apache.lucene.analysis.es.SpanishPluralStemFilterFactory,
+      org.apache.lucene.analysis.fa.PersianNormalizationFilterFactory,
+      org.apache.lucene.analysis.fi.FinnishLightStemFilterFactory,
+      org.apache.lucene.analysis.fr.FrenchLightStemFilterFactory,
+      org.apache.lucene.analysis.fr.FrenchMinimalStemFilterFactory,
+      org.apache.lucene.analysis.ga.IrishLowerCaseFilterFactory,
+      org.apache.lucene.analysis.gl.GalicianMinimalStemFilterFactory,
+      org.apache.lucene.analysis.gl.GalicianStemFilterFactory,
+      org.apache.lucene.analysis.hi.HindiNormalizationFilterFactory,
+      org.apache.lucene.analysis.hi.HindiStemFilterFactory,
+      org.apache.lucene.analysis.hu.HungarianLightStemFilterFactory,
+      org.apache.lucene.analysis.hunspell.HunspellStemFilterFactory,
+      org.apache.lucene.analysis.id.IndonesianStemFilterFactory,
+      org.apache.lucene.analysis.in.IndicNormalizationFilterFactory,
+      org.apache.lucene.analysis.it.ItalianLightStemFilterFactory,
+      org.apache.lucene.analysis.lv.LatvianStemFilterFactory,
+      org.apache.lucene.analysis.minhash.MinHashFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.CapitalizationFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.CodepointCountFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ConcatenateGraphFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.DateRecognizerFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.DelimitedTermFrequencyTokenFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.DropIfFlaggedFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.FingerprintFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.FixBrokenOffsetsFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.HyphenatedWordsFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.KeepWordFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.KeywordMarkerFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.KeywordRepeatFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LengthFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LimitTokenCountFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LimitTokenOffsetFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.LimitTokenPositionFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.RemoveDuplicatesTokenFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.StemmerOverrideFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ProtectedTermFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.TrimFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.TruncateTokenFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.TypeAsSynonymFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.WordDelimiterFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ScandinavianFoldingFilterFactory,
+      org.apache.lucene.analysis.miscellaneous.ScandinavianNormalizationFilterFactory,
+      org.apache.lucene.analysis.ngram.EdgeNGramFilterFactory,
+      org.apache.lucene.analysis.ngram.NGramFilterFactory,
+      org.apache.lucene.analysis.no.NorwegianLightStemFilterFactory,
+      org.apache.lucene.analysis.no.NorwegianMinimalStemFilterFactory,
+      org.apache.lucene.analysis.no.NorwegianNormalizationFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternReplaceFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternCaptureGroupFilterFactory,
+      org.apache.lucene.analysis.pattern.PatternTypingFilterFactory,
+      org.apache.lucene.analysis.payloads.DelimitedPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.payloads.NumericPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.payloads.TokenOffsetPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.payloads.TypeAsPayloadTokenFilterFactory,
+      org.apache.lucene.analysis.pt.PortugueseLightStemFilterFactory,
+      org.apache.lucene.analysis.pt.PortugueseMinimalStemFilterFactory,
+      org.apache.lucene.analysis.pt.PortugueseStemFilterFactory,
+      org.apache.lucene.analysis.reverse.ReverseStringFilterFactory,
+      org.apache.lucene.analysis.ru.RussianLightStemFilterFactory,
+      org.apache.lucene.analysis.shingle.ShingleFilterFactory,
+      org.apache.lucene.analysis.shingle.FixedShingleFilterFactory,
+      org.apache.lucene.analysis.snowball.SnowballPorterFilterFactory,
+      org.apache.lucene.analysis.sr.SerbianNormalizationFilterFactory,
+      org.apache.lucene.analysis.sv.SwedishLightStemFilterFactory,
+      org.apache.lucene.analysis.sv.SwedishMinimalStemFilterFactory,
+      org.apache.lucene.analysis.synonym.SynonymFilterFactory,
+      org.apache.lucene.analysis.synonym.SynonymGraphFilterFactory,
+      org.apache.lucene.analysis.core.FlattenGraphFilterFactory,
+      org.apache.lucene.analysis.te.TeluguNormalizationFilterFactory,
+      org.apache.lucene.analysis.te.TeluguStemFilterFactory,
+      org.apache.lucene.analysis.tr.TurkishLowerCaseFilterFactory,
+      org.apache.lucene.analysis.util.ElisionFilterFactory;
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.classic.ClassicTokenizerFactory,
+      org.apache.lucene.analysis.core.KeywordTokenizerFactory,
+      org.apache.lucene.analysis.core.LetterTokenizerFactory,
+      org.apache.lucene.analysis.core.WhitespaceTokenizerFactory,
+      org.apache.lucene.analysis.email.UAX29URLEmailTokenizerFactory,
+      org.apache.lucene.analysis.ngram.EdgeNGramTokenizerFactory,
+      org.apache.lucene.analysis.ngram.NGramTokenizerFactory,
+      org.apache.lucene.analysis.path.PathHierarchyTokenizerFactory,
+      org.apache.lucene.analysis.pattern.PatternTokenizerFactory,
+      org.apache.lucene.analysis.pattern.SimplePatternSplitTokenizerFactory,
+      org.apache.lucene.analysis.pattern.SimplePatternTokenizerFactory,
+      org.apache.lucene.analysis.th.ThaiTokenizerFactory,
+      org.apache.lucene.analysis.wikipedia.WikipediaTokenizerFactory;
+}
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/icu/build.gradle
index 2eab963..e76b327 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/icu/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Analysis integration with ICU (International Components for Unicode)'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
-  api 'com.ibm.icu:icu4j'
+  moduleApi 'com.ibm.icu:icu4j'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/src/java/module-info.java b/lucene/analysis/icu/src/java/module-info.java
new file mode 100644
index 0000000..c901af4
--- /dev/null
+++ b/lucene/analysis/icu/src/java/module-info.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Analysis integration with ICU */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.icu {
+  requires com.ibm.icu;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+
+  exports org.apache.lucene.analysis.icu;
+  exports org.apache.lucene.analysis.icu.segmentation;
+  exports org.apache.lucene.analysis.icu.tokenattributes;
+
+  provides org.apache.lucene.analysis.CharFilterFactory with
+      org.apache.lucene.analysis.icu.ICUNormalizer2CharFilterFactory;
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.icu.segmentation.ICUTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.icu.ICUFoldingFilterFactory,
+      org.apache.lucene.analysis.icu.ICUNormalizer2FilterFactory,
+      org.apache.lucene.analysis.icu.ICUTransformFilterFactory;
+}
diff --git a/lucene/analysis/kuromoji/build.gradle b/lucene/analysis/kuromoji/build.gradle
index af2bfa5..07fc0b0 100644
--- a/lucene/analysis/kuromoji/build.gradle
+++ b/lucene/analysis/kuromoji/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Japanese Morphological Analyzer'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/kuromoji/src/java/module-info.java b/lucene/analysis/kuromoji/src/java/module-info.java
new file mode 100644
index 0000000..7c829fd
--- /dev/null
+++ b/lucene/analysis/kuromoji/src/java/module-info.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Japanese Morphological Analyzer */
+module org.apache.lucene.analysis.kuromoji {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+
+  exports org.apache.lucene.analysis.ja;
+  exports org.apache.lucene.analysis.ja.completion;
+  exports org.apache.lucene.analysis.ja.dict;
+  exports org.apache.lucene.analysis.ja.tokenattributes;
+  exports org.apache.lucene.analysis.ja.util;
+
+  provides org.apache.lucene.analysis.CharFilterFactory with
+      org.apache.lucene.analysis.ja.JapaneseIterationMarkCharFilterFactory;
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.ja.JapaneseTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.ja.JapaneseBaseFormFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseCompletionFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseKatakanaStemFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseNumberFilterFactory,
+      org.apache.lucene.analysis.ja.JapanesePartOfSpeechStopFilterFactory,
+      org.apache.lucene.analysis.ja.JapaneseReadingFormFilterFactory;
+}
diff --git a/lucene/analysis/kuromoji/build.gradle b/lucene/analysis/morfologik.tests/build.gradle
similarity index 76%
copy from lucene/analysis/kuromoji/build.gradle
copy to lucene/analysis/morfologik.tests/build.gradle
index af2bfa5..9cd6720 100644
--- a/lucene/analysis/kuromoji/build.gradle
+++ b/lucene/analysis/morfologik.tests/build.gradle
@@ -17,11 +17,12 @@
 
 apply plugin: 'java-library'
 
-description = 'Japanese Morphological Analyzer'
+description = 'Module tests for :lucene:analysis:morfologik'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  testImplementation project(':lucene:test-framework')
+  moduleTestImplementation project(':lucene:analysis:morfologik')
+  moduleTestImplementation("junit:junit", {
+    exclude group: "org.hamcrest"
+  })
+  moduleTestImplementation "org.hamcrest:hamcrest"
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/morfologik.tests/src/test/module-info.java
similarity index 68%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/morfologik.tests/src/test/module-info.java
index 2eab963..15642a9 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/morfologik.tests/src/test/module-info.java
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Test module for {@code org.apache.lucene.analysis.morfologik}. */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.morfologik.tests {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.analysis.morfologik;
+  requires junit;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.analysis.morfologik.tests;
 }
diff --git a/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java b/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
new file mode 100644
index 0000000..c2933fc
--- /dev/null
+++ b/lucene/analysis/morfologik.tests/src/test/org/apache/lucene/analysis/morfologik/tests/TestMorfologikAnalyzer.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.lucene.analysis.morfologik.tests;
+
+import org.apache.lucene.analysis.morfologik.MorfologikAnalyzer;
+import org.apache.lucene.analysis.uk.UkrainianMorfologikAnalyzer;
+import org.apache.lucene.index.IndexWriter;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestMorfologikAnalyzer {
+  @Test
+  public void testMorfologikAnalyzerLoads() {
+    var analyzer = new MorfologikAnalyzer();
+    Assert.assertNotNull(analyzer);
+  }
+
+  @Test
+  public void testUkrainianMorfologikAnalyzerLoads() {
+    var analyzer = new UkrainianMorfologikAnalyzer();
+    Assert.assertNotNull(analyzer);
+  }
+
+  @Test
+  public void testWeAreModule() {
+    Assert.assertTrue(this.getClass().getModule().isNamed());
+  }
+
+  @Test
+  public void testLuceneIsAModule() {
+    Assert.assertTrue(IndexWriter.class.getModule().isNamed());
+  }
+}
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/morfologik/build.gradle
index 153515f..03d7c09 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/morfologik/build.gradle
@@ -20,13 +20,12 @@ apply plugin: 'java-library'
 description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
+  moduleApi 'org.carrot2:morfologik-stemming'
 
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
+  moduleImplementation 'org.carrot2:morfologik-polish'
+  moduleImplementation 'ua.net.nlp:morfologik-ukrainian-search'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/morfologik/src/java/module-info.java
similarity index 57%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/analysis/morfologik/src/java/module-info.java
index 153515f..3268d4b 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/morfologik/src/java/module-info.java
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for dictionary stemming, built-in Polish dictionary */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.morfologik {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.carrot2.morfologik.stemming;
+  requires org.carrot2.morfologik.polish;
+  requires morfologik.ukrainian.search;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
+  exports org.apache.lucene.analysis.morfologik;
+  exports org.apache.lucene.analysis.uk;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.morfologik.MorfologikFilterFactory;
 }
diff --git a/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java b/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java
index 425a55c6..b80ccb6 100644
--- a/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java
+++ b/lucene/analysis/morfologik/src/java/org/apache/lucene/analysis/uk/UkrainianMorfologikAnalyzer.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Objects;
 import morfologik.stemming.Dictionary;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.CharArraySet;
@@ -42,12 +43,9 @@ import org.apache.lucene.util.IOUtils;
  * @since 6.2.0
  */
 public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
-
+  private final Dictionary dictionary;
   private final CharArraySet stemExclusionSet;
 
-  /** File containing default Ukrainian stopwords. */
-  public static final String DEFAULT_STOPWORD_FILE = "stopwords.txt";
-
   private static final NormalizeCharMap NORMALIZER_MAP;
 
   static {
@@ -67,47 +65,72 @@ public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
     NORMALIZER_MAP = builder.build();
   }
 
-  /**
-   * Returns an unmodifiable instance of the default stop words set.
-   *
-   * @return default stop words set.
-   */
-  public static CharArraySet getDefaultStopSet() {
-    return DefaultSetHolder.DEFAULT_STOP_SET;
+  /** Returns a lazy singleton with the default Ukrainian resources. */
+  private static volatile DefaultResources defaultResources;
+
+  private static DefaultResources getDefaultResources() {
+    if (defaultResources == null) {
+      synchronized (DefaultResources.class) {
+        try {
+          CharArraySet wordList;
+          try (var is = UkrainianMorfologikAnalyzer.class.getResourceAsStream("stopwords.txt")) {
+            if (is == null) {
+              throw new IOException("Could not locate the required stopwords resource.");
+            }
+            wordList =
+                WordlistLoader.getSnowballWordSet(
+                    IOUtils.getDecodingReader(is, StandardCharsets.UTF_8));
+          }
+
+          // First, try to look up the resource module by name.
+          Dictionary dictionary;
+          Module ourModule = DefaultResources.class.getModule();
+          if (ourModule.isNamed() && ourModule.getLayer() != null) {
+            var module =
+                ourModule
+                    .getLayer()
+                    .findModule("morfologik.ukrainian.search")
+                    .orElseThrow(
+                        () ->
+                            new IOException(
+                                "Can't find the resource module: morfologik.ukrainian.search"));
+
+            try (var fsaStream = module.getResourceAsStream("ua/net/nlp/ukrainian.dict");
+                var metaStream = module.getResourceAsStream("ua/net/nlp/ukrainian.info")) {
+              dictionary = Dictionary.read(fsaStream, metaStream);
+            }
+          } else {
+            dictionary =
+                Dictionary.read(
+                    Objects.requireNonNull(
+                        UkrainianMorfologikAnalyzer.class
+                            .getClassLoader()
+                            .getResource("ua/net/nlp/ukrainian.dict"),
+                        "Could not locate the required Ukrainian dictionary resource."));
+          }
+          defaultResources = new DefaultResources(wordList, dictionary);
+        } catch (IOException e) {
+          throw new UncheckedIOException(
+              "Could not load the required resources for the Ukrainian analyzer.", e);
+        }
+      }
+    }
+    return defaultResources;
   }
 
-  /**
-   * Atomically loads the DEFAULT_STOP_SET and DICTIONARY in a lazy fashion once the outer class
-   * accesses the static final set the first time.;
-   */
-  private static class DefaultSetHolder {
-    static final CharArraySet DEFAULT_STOP_SET;
-    static final Dictionary DICTIONARY;
-
-    static {
-      try {
-        DEFAULT_STOP_SET =
-            WordlistLoader.getSnowballWordSet(
-                IOUtils.getDecodingReader(
-                    UkrainianMorfologikAnalyzer.class,
-                    DEFAULT_STOPWORD_FILE,
-                    StandardCharsets.UTF_8));
-        DICTIONARY =
-            Dictionary.read(
-                UkrainianMorfologikAnalyzer.class
-                    .getClassLoader()
-                    .getResource("ua/net/nlp/ukrainian.dict"));
-      } catch (IOException ex) {
-        // default set should always be present as it is part of the
-        // distribution (JAR)
-        throw new UncheckedIOException("Unable to load analyzer resources", ex);
-      }
+  private static class DefaultResources {
+    final CharArraySet stopSet;
+    final Dictionary dictionary;
+
+    private DefaultResources(CharArraySet stopSet, Dictionary dictionary) {
+      this.stopSet = stopSet;
+      this.dictionary = dictionary;
     }
   }
 
-  /** Builds an analyzer with the default stop words: {@link #DEFAULT_STOPWORD_FILE}. */
+  /** Builds an analyzer with the default stop words. */
   public UkrainianMorfologikAnalyzer() {
-    this(DefaultSetHolder.DEFAULT_STOP_SET);
+    this(getDefaultResources().stopSet);
   }
 
   /**
@@ -129,6 +152,7 @@ public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
   public UkrainianMorfologikAnalyzer(CharArraySet stopwords, CharArraySet stemExclusionSet) {
     super(stopwords);
     this.stemExclusionSet = CharArraySet.unmodifiableSet(CharArraySet.copy(stemExclusionSet));
+    this.dictionary = getDefaultResources().dictionary;
   }
 
   @Override
@@ -155,7 +179,7 @@ public final class UkrainianMorfologikAnalyzer extends StopwordAnalyzerBase {
       result = new SetKeywordMarkerFilter(result, stemExclusionSet);
     }
 
-    result = new MorfologikFilter(result, DefaultSetHolder.DICTIONARY);
+    result = new MorfologikFilter(result, dictionary);
     return new TokenStreamComponents(source, result);
   }
 }
diff --git a/lucene/analysis/nori/build.gradle b/lucene/analysis/nori/build.gradle
index 430673c..079cf50 100644
--- a/lucene/analysis/nori/build.gradle
+++ b/lucene/analysis/nori/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Korean Morphological Analyzer'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
   
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/nori/src/java/module-info.java
similarity index 54%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/nori/src/java/module-info.java
index 2eab963..9dd085b 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/nori/src/java/module-info.java
@@ -15,15 +15,19 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Korean Morphological Analyzer */
+module org.apache.lucene.analysis.nori {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.ko;
+  exports org.apache.lucene.analysis.ko.dict;
+  exports org.apache.lucene.analysis.ko.tokenattributes;
+  exports org.apache.lucene.analysis.ko.util;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.ko.KoreanTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.ko.KoreanPartOfSpeechStopFilterFactory,
+      org.apache.lucene.analysis.ko.KoreanReadingFormFilterFactory;
 }
diff --git a/lucene/analysis/opennlp/build.gradle b/lucene/analysis/opennlp/build.gradle
index c4672c0..3fee61a 100644
--- a/lucene/analysis/opennlp/build.gradle
+++ b/lucene/analysis/opennlp/build.gradle
@@ -20,9 +20,9 @@ apply plugin: 'java-library'
 description = 'OpenNLP Library Integration'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-  api 'org.apache.opennlp:opennlp-tools'
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
+  moduleApi 'org.apache.opennlp:opennlp-tools'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/opennlp/src/java/module-info.java
similarity index 52%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/opennlp/src/java/module-info.java
index 2eab963..c63ecfa 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/opennlp/src/java/module-info.java
@@ -15,15 +15,20 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** OpenNLP Library Integration */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.opennlp {
+  requires org.apache.opennlp.tools;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.opennlp;
+  exports org.apache.lucene.analysis.opennlp.tools;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.opennlp.OpenNLPTokenizerFactory;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.opennlp.OpenNLPChunkerFilterFactory,
+      org.apache.lucene.analysis.opennlp.OpenNLPLemmatizerFilterFactory,
+      org.apache.lucene.analysis.opennlp.OpenNLPPOSFilterFactory;
 }
diff --git a/lucene/analysis/phonetic/build.gradle b/lucene/analysis/phonetic/build.gradle
index 0f716e8..e5595cb 100644
--- a/lucene/analysis/phonetic/build.gradle
+++ b/lucene/analysis/phonetic/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Analyzer for indexing phonetic signatures (for sounds-alike search)'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
-  implementation 'commons-codec:commons-codec'
+  moduleImplementation 'commons-codec:commons-codec'
 
   testImplementation project(':lucene:test-framework')
 } 
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/analysis/phonetic/src/java/module-info.java
similarity index 58%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/analysis/phonetic/src/java/module-info.java
index 153515f..706251a 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/analysis/phonetic/src/java/module-info.java
@@ -15,18 +15,17 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for indexing phonetic signatures */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.analysis.phonetic {
+  requires org.apache.commons.codec;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
+  exports org.apache.lucene.analysis.phonetic;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.phonetic.BeiderMorseFilterFactory,
+      org.apache.lucene.analysis.phonetic.DoubleMetaphoneFilterFactory,
+      org.apache.lucene.analysis.phonetic.PhoneticFilterFactory;
 }
diff --git a/lucene/analysis/smartcn/build.gradle b/lucene/analysis/smartcn/build.gradle
index 8d5eeb1..960be70 100644
--- a/lucene/analysis/smartcn/build.gradle
+++ b/lucene/analysis/smartcn/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Analyzer for indexing Chinese'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
   testImplementation project(':lucene:test-framework')
 } 
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/smartcn/src/java/module-info.java
similarity index 67%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/smartcn/src/java/module-info.java
index 2eab963..44f4eb5 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/smartcn/src/java/module-info.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for indexing Chinese */
+module org.apache.lucene.analysis.smartcn {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.cn.smart;
+  exports org.apache.lucene.analysis.cn.smart.hhmm;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory;
 }
diff --git a/lucene/analysis/stempel/build.gradle b/lucene/analysis/stempel/build.gradle
index 3b80f9f..3299159 100644
--- a/lucene/analysis/stempel/build.gradle
+++ b/lucene/analysis/stempel/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Analyzer for indexing Polish'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/analysis/stempel/src/java/module-info.java
similarity index 66%
copy from lucene/analysis/icu/build.gradle
copy to lucene/analysis/stempel/src/java/module-info.java
index 2eab963..d559548 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/analysis/stempel/src/java/module-info.java
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Analyzer for indexing Polish */
+module org.apache.lucene.analysis.stempel {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
+  exports org.apache.lucene.analysis.pl;
+  exports org.apache.lucene.analysis.stempel;
+  exports org.egothor.stemmer;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.analysis.stempel.StempelPolishStemFilterFactory;
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/backward-codecs/build.gradle
index aa47302..d5e25d9 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/backward-codecs/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 
 description = 'Codecs for older versions of Lucene'
 
-dependencies { 
-  api project(':lucene:core')
+dependencies {
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/backward-codecs/src/java/module-info.java b/lucene/backward-codecs/src/java/module-info.java
new file mode 100644
index 0000000..8e01546
--- /dev/null
+++ b/lucene/backward-codecs/src/java/module-info.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Codecs for older versions of Lucene */
+module org.apache.lucene.backward_codecs {
+  requires org.apache.lucene.core;
+
+  exports org.apache.lucene.backward_codecs;
+  exports org.apache.lucene.backward_codecs.lucene40.blocktree;
+  exports org.apache.lucene.backward_codecs.lucene50;
+  exports org.apache.lucene.backward_codecs.lucene50.compressing;
+  exports org.apache.lucene.backward_codecs.lucene60;
+  exports org.apache.lucene.backward_codecs.lucene70;
+  exports org.apache.lucene.backward_codecs.lucene80;
+  exports org.apache.lucene.backward_codecs.lucene84;
+  exports org.apache.lucene.backward_codecs.lucene86;
+  exports org.apache.lucene.backward_codecs.lucene87;
+  exports org.apache.lucene.backward_codecs.packed;
+  exports org.apache.lucene.backward_codecs.store;
+
+  provides org.apache.lucene.codecs.DocValuesFormat with
+      org.apache.lucene.backward_codecs.lucene70.Lucene70DocValuesFormat,
+      org.apache.lucene.backward_codecs.lucene80.Lucene80DocValuesFormat;
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.backward_codecs.lucene50.Lucene50PostingsFormat,
+      org.apache.lucene.backward_codecs.lucene84.Lucene84PostingsFormat;
+  provides org.apache.lucene.codecs.Codec with
+      org.apache.lucene.backward_codecs.lucene70.Lucene70Codec,
+      org.apache.lucene.backward_codecs.lucene80.Lucene80Codec,
+      org.apache.lucene.backward_codecs.lucene84.Lucene84Codec,
+      org.apache.lucene.backward_codecs.lucene86.Lucene86Codec,
+      org.apache.lucene.backward_codecs.lucene87.Lucene87Codec;
+}
diff --git a/lucene/benchmark/build.gradle b/lucene/benchmark/build.gradle
index 67b1644..9da51c3 100644
--- a/lucene/benchmark/build.gradle
+++ b/lucene/benchmark/build.gradle
@@ -22,23 +22,23 @@ plugins {
 description = 'Lucene benchmarking module'
 
 dependencies {  
-  implementation project(':lucene:core')
+  moduleImplementation project(':lucene:core')
 
-  implementation project(':lucene:analysis:common')
-  implementation project(':lucene:facet')
-  implementation project(':lucene:highlighter')
-  implementation project(':lucene:queries')
-  implementation project(':lucene:spatial-extras')
-  implementation project(':lucene:queryparser')
+  moduleImplementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:facet')
+  moduleImplementation project(':lucene:highlighter')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:spatial-extras')
+  moduleImplementation project(':lucene:queryparser')
 
-  implementation "org.apache.commons:commons-compress"
-  implementation "com.ibm.icu:icu4j"
-  implementation "org.locationtech.spatial4j:spatial4j"
-  implementation("net.sourceforge.nekohtml:nekohtml", {
+  moduleImplementation "org.apache.commons:commons-compress"
+  moduleImplementation "com.ibm.icu:icu4j"
+  moduleImplementation "org.locationtech.spatial4j:spatial4j"
+  moduleImplementation ("net.sourceforge.nekohtml:nekohtml", {
     exclude module: "xml-apis"
   })
 
-  runtimeOnly project(':lucene:analysis:icu')
+  moduleRuntimeOnly project(':lucene:analysis:icu')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/benchmark/src/java/module-info.java b/lucene/benchmark/src/java/module-info.java
new file mode 100644
index 0000000..f3dfac9
--- /dev/null
+++ b/lucene/benchmark/src/java/module-info.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.benchmark {
+  requires java.xml;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.facet;
+  requires org.apache.lucene.highlighter;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.queryparser;
+  requires org.apache.lucene.spatial_extras;
+  requires spatial4j;
+
+  exports org.apache.lucene.benchmark;
+  exports org.apache.lucene.benchmark.byTask;
+  exports org.apache.lucene.benchmark.byTask.feeds;
+  exports org.apache.lucene.benchmark.byTask.programmatic;
+  exports org.apache.lucene.benchmark.byTask.stats;
+  exports org.apache.lucene.benchmark.byTask.tasks;
+  exports org.apache.lucene.benchmark.byTask.utils;
+  exports org.apache.lucene.benchmark.quality;
+  exports org.apache.lucene.benchmark.quality.trec;
+  exports org.apache.lucene.benchmark.quality.utils;
+  exports org.apache.lucene.benchmark.utils;
+}
diff --git a/lucene/classification/build.gradle b/lucene/classification/build.gradle
index 736dfb3..8566cdb 100644
--- a/lucene/classification/build.gradle
+++ b/lucene/classification/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Classification module for Lucene'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:queries')
-  implementation project(':lucene:grouping')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:grouping')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:analysis:common')
diff --git a/lucene/analysis/icu/build.gradle b/lucene/classification/src/java/module-info.java
similarity index 70%
copy from lucene/analysis/icu/build.gradle
copy to lucene/classification/src/java/module-info.java
index 2eab963..4817d8a 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/classification/src/java/module-info.java
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Classification module for Lucene */
+module org.apache.lucene.classification {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.grouping;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.classification;
+  exports org.apache.lucene.classification.document;
+  exports org.apache.lucene.classification.utils;
 }
diff --git a/lucene/codecs/build.gradle b/lucene/codecs/build.gradle
index ad26aae..92e0782 100644
--- a/lucene/codecs/build.gradle
+++ b/lucene/codecs/build.gradle
@@ -20,6 +20,6 @@ apply plugin: 'java-library'
 description = 'Lucene codecs and postings formats'
 
 dependencies {
-    implementation project(':lucene:core')
+    moduleImplementation project(':lucene:core')
     testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/codecs/src/java/module-info.java b/lucene/codecs/src/java/module-info.java
new file mode 100644
index 0000000..73f53fb
--- /dev/null
+++ b/lucene/codecs/src/java/module-info.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Lucene codecs and postings formats */
+module org.apache.lucene.codecs {
+  requires org.apache.lucene.core;
+
+  exports org.apache.lucene.codecs.blockterms;
+  exports org.apache.lucene.codecs.blocktreeords;
+  exports org.apache.lucene.codecs.bloom;
+  exports org.apache.lucene.codecs.memory;
+  exports org.apache.lucene.codecs.simpletext;
+  exports org.apache.lucene.codecs.uniformsplit;
+  exports org.apache.lucene.codecs.uniformsplit.sharedterms;
+
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.codecs.blocktreeords.BlockTreeOrdsPostingsFormat,
+      org.apache.lucene.codecs.bloom.BloomFilteringPostingsFormat,
+      org.apache.lucene.codecs.memory.DirectPostingsFormat,
+      org.apache.lucene.codecs.memory.FSTPostingsFormat,
+      org.apache.lucene.codecs.uniformsplit.UniformSplitPostingsFormat,
+      org.apache.lucene.codecs.uniformsplit.sharedterms.STUniformSplitPostingsFormat;
+  provides org.apache.lucene.codecs.Codec with
+      org.apache.lucene.codecs.simpletext.SimpleTextCodec;
+}
diff --git a/lucene/core/src/java/module-info.java b/lucene/core/src/java/module-info.java
new file mode 100644
index 0000000..931d4cb
--- /dev/null
+++ b/lucene/core/src/java/module-info.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Lucene Core. */
+module org.apache.lucene.core {
+  requires jdk.unsupported; // this is needed for MMapDirectory to unmap
+
+  exports org.apache.lucene.analysis;
+  exports org.apache.lucene.analysis.standard;
+  exports org.apache.lucene.analysis.tokenattributes;
+  exports org.apache.lucene.codecs;
+  exports org.apache.lucene.codecs.compressing;
+  exports org.apache.lucene.codecs.lucene90;
+  exports org.apache.lucene.codecs.lucene90.blocktree;
+  exports org.apache.lucene.codecs.lucene90.compressing;
+  exports org.apache.lucene.codecs.perfield;
+  exports org.apache.lucene.document;
+  exports org.apache.lucene.geo;
+  exports org.apache.lucene.index;
+  exports org.apache.lucene.search;
+  exports org.apache.lucene.search.comparators;
+  exports org.apache.lucene.search.similarities;
+  exports org.apache.lucene.store;
+  exports org.apache.lucene.util;
+  exports org.apache.lucene.util.automaton;
+  exports org.apache.lucene.util.bkd;
+  exports org.apache.lucene.util.compress;
+  exports org.apache.lucene.util.fst;
+  exports org.apache.lucene.util.graph;
+  exports org.apache.lucene.util.hnsw;
+  exports org.apache.lucene.util.hppc;
+  exports org.apache.lucene.util.mutable;
+  exports org.apache.lucene.util.packed;
+
+  provides org.apache.lucene.analysis.TokenizerFactory with
+      org.apache.lucene.analysis.standard.StandardTokenizerFactory;
+  provides org.apache.lucene.codecs.Codec with
+      org.apache.lucene.codecs.lucene90.Lucene90Codec;
+  provides org.apache.lucene.codecs.DocValuesFormat with
+      org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat;
+  provides org.apache.lucene.codecs.KnnVectorsFormat with
+      org.apache.lucene.codecs.lucene90.Lucene90HnswVectorsFormat;
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.codecs.lucene90.Lucene90PostingsFormat;
+  provides org.apache.lucene.index.SortFieldProvider with
+      org.apache.lucene.search.SortField.Provider,
+      org.apache.lucene.search.SortedNumericSortField.Provider,
+      org.apache.lucene.search.SortedSetSortField.Provider;
+
+  uses org.apache.lucene.analysis.CharFilterFactory;
+  uses org.apache.lucene.analysis.TokenFilterFactory;
+  uses org.apache.lucene.analysis.TokenizerFactory;
+  uses org.apache.lucene.codecs.Codec;
+  uses org.apache.lucene.codecs.DocValuesFormat;
+  uses org.apache.lucene.codecs.KnnVectorsFormat;
+  uses org.apache.lucene.codecs.PostingsFormat;
+  uses org.apache.lucene.index.SortFieldProvider;
+}
diff --git a/lucene/demo/build.gradle b/lucene/demo/build.gradle
index 3624328..70fd1df 100644
--- a/lucene/demo/build.gradle
+++ b/lucene/demo/build.gradle
@@ -20,12 +20,12 @@ apply plugin: 'java-library'
 description = 'Simple example code for Apache Lucene'
 
 dependencies {
-  implementation project(':lucene:core')
-  implementation project(':lucene:facet')
-  implementation project(':lucene:queries')
-  implementation project(':lucene:analysis:common')
-  implementation project(':lucene:queryparser')
-  implementation project(':lucene:expressions')
+  moduleImplementation project(':lucene:core')
+  moduleImplementation project(':lucene:facet')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:queryparser')
+  moduleImplementation project(':lucene:expressions')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/demo/src/java/module-info.java
similarity index 65%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/demo/src/java/module-info.java
index 153515f..13549a5 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/demo/src/java/module-info.java
@@ -15,18 +15,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Simple example code for Apache Lucene */
+module org.apache.lucene.demo {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.facet;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.queryparser;
+  requires org.apache.lucene.expressions;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.demo;
+  exports org.apache.lucene.demo.facet;
+  exports org.apache.lucene.demo.knn;
 }
diff --git a/lucene/distribution.tests/build.gradle b/lucene/distribution.tests/build.gradle
new file mode 100644
index 0000000..30c4ae1
--- /dev/null
+++ b/lucene/distribution.tests/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+plugins {
+  id 'java-library'
+}
+
+configurations {
+  // This is a configuration that references an 'folder-expanded' binary distribution
+  // tests will run against. The distribution is slightly trimmed (no docs, licenses)
+  // because we don't test these parts of the distribution anyway.
+  binaryDistribution
+}
+
+dependencies {
+  binaryDistribution project(path: ":lucene:distribution", configuration: "binaryDirForTests")
+
+  moduleTestImplementation("com.carrotsearch.randomizedtesting:randomizedtesting-runner", {
+    exclude group: "junit"
+  })
+
+  moduleTestImplementation("junit:junit", {
+    exclude group: "org.hamcrest"
+  })
+  moduleTestImplementation "org.hamcrest:hamcrest"
+  moduleTestImplementation "org.assertj:assertj-core"
+}
+
+test {
+  dependsOn configurations.binaryDistribution
+
+  // We need to pass the system property using a lazy provider, not supported at the moment:
+  // https://github.com/gradle/gradle/issues/12247
+  // so we'll use a workaround and pass command-line arguments directly.
+  jvmArgumentProviders.add(new CommandLineArgumentProvider() {
+    @Override
+    Iterable<String> asArguments() {
+      return [
+          "-Dlucene.distribution.dir=${configurations.binaryDistribution.singleFile.absolutePath }",
+          "-Dlucene.distribution.version=${project.version}"
+      ]
+    }
+  })
+
+  doFirst {
+    logger.lifecycle("Testing binary distribution at: ${configurations.binaryDistribution.singleFile}")
+  }
+}
\ No newline at end of file
diff --git a/lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java b/lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java
new file mode 100644
index 0000000..5627089
--- /dev/null
+++ b/lucene/distribution.tests/src/test/org/apache/lucene/distribution/TestModularLayer.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.lucene.distribution;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReader;
+import java.lang.module.ModuleReference;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.Assumptions;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Sanity checks concerning the distribution's binary artifacts (modules).
+ *
+ * <p>We do <em>not</em> want this module to depend on any Lucene classes (including the test
+ * framework) so that there is no risk of accidental classpath space pollution. This also means the
+ * default {@code LuceneTestCase} configuration setup is not used (you have to annotate test for
+ * JUnit, for example).
+ */
+public class TestModularLayer {
+  /** A path to a directory with an expanded Lucene distribution. */
+  private static final String DISTRIBUTION_PROPERTY = "lucene.distribution.dir";
+
+  /** The expected distribution version of Lucene modules. */
+  private static final String VERSION_PROPERTY = "lucene.distribution.version";
+
+  /** Only core Lucene modules, no third party modules. */
+  private static Set<ModuleReference> allCoreModules;
+
+  /** {@link ModuleFinder} resolving only the Lucene modules. */
+  private static ModuleFinder coreModulesFinder;
+
+  /** Ensure Lucene classes are not directly visible. */
+  @BeforeClass
+  public static void checkLuceneNotInClasspath() {
+    Assertions.assertThatThrownBy(
+            () -> {
+              Class.forName("org.apache.lucene.index.IndexWriter");
+            })
+        .isInstanceOf(ClassNotFoundException.class);
+  }
+
+  /**
+   * We accept external properties that point to the assembled set of distribution modules and to
+   * their expected version. These properties are collected and passed by gradle but can be provided
+   * manually (for IDE launches).
+   */
+  @BeforeClass
+  public static void checkModulePathProvided() {
+    String modulesPropertyValue = System.getProperty(DISTRIBUTION_PROPERTY);
+    if (modulesPropertyValue == null) {
+      throw new AssertionError(DISTRIBUTION_PROPERTY + " property is required for this test.");
+    }
+
+    Path modulesPath = Paths.get(modulesPropertyValue).resolve("modules");
+    if (!Files.isDirectory(modulesPath)) {
+      throw new AssertionError(
+          DISTRIBUTION_PROPERTY
+              + " property does not point to a directory where this path is present: "
+              + modulesPath.toAbsolutePath());
+    }
+
+    Path thirdPartyModulesPath = Paths.get(modulesPropertyValue).resolve("modules-thirdparty");
+    if (!Files.isDirectory(thirdPartyModulesPath)) {
+      throw new AssertionError(
+          DISTRIBUTION_PROPERTY
+              + " property does not point to a directory where this path is present: "
+              + thirdPartyModulesPath.toAbsolutePath());
+    }
+
+    coreModulesFinder = ModuleFinder.of(modulesPath);
+    allCoreModules = coreModulesFinder.findAll();
+  }
+
+  @AfterClass
+  public static void cleanup() {
+    allCoreModules = null;
+    coreModulesFinder = null;
+  }
+
+  /** Make sure all published module names remain constant, even if we reorganize the build. */
+  @Test
+  public void testExpectedDistributionModuleNames() {
+    Assertions.assertThat(
+            allCoreModules.stream().map(module -> module.descriptor().name()).sorted())
+        .containsExactly(
+            "org.apache.lucene.analysis.common",
+            "org.apache.lucene.analysis.icu",
+            "org.apache.lucene.analysis.kuromoji",
+            "org.apache.lucene.analysis.morfologik",
+            "org.apache.lucene.analysis.nori",
+            "org.apache.lucene.analysis.opennlp",
+            "org.apache.lucene.analysis.phonetic",
+            "org.apache.lucene.analysis.smartcn",
+            "org.apache.lucene.analysis.stempel",
+            "org.apache.lucene.backward_codecs",
+            "org.apache.lucene.benchmark",
+            "org.apache.lucene.classification",
+            "org.apache.lucene.codecs",
+            "org.apache.lucene.core",
+            "org.apache.lucene.demo",
+            "org.apache.lucene.expressions",
+            "org.apache.lucene.facet",
+            "org.apache.lucene.grouping",
+            "org.apache.lucene.highlighter",
+            "org.apache.lucene.join",
+            "org.apache.lucene.luke",
+            "org.apache.lucene.memory",
+            "org.apache.lucene.misc",
+            "org.apache.lucene.monitor",
+            "org.apache.lucene.queries",
+            "org.apache.lucene.queryparser",
+            "org.apache.lucene.replicator",
+            "org.apache.lucene.sandbox",
+            "org.apache.lucene.spatial3d",
+            "org.apache.lucene.spatial_extras",
+            "org.apache.lucene.suggest");
+  }
+
+  /** Make sure we don't publish automatic modules. */
+  @Test
+  public void testAllCoreModulesAreNamedModules() {
+    Assertions.assertThat(allCoreModules)
+        .allSatisfy(
+            module -> {
+              Assertions.assertThat(module.descriptor().isAutomatic())
+                  .as(module.descriptor().name())
+                  .isFalse();
+            });
+  }
+
+  /** Ensure all modules have the same (expected) version. */
+  @Test
+  public void testAllModulesHaveExpectedVersion() {
+    String luceneBuildVersion = System.getProperty(VERSION_PROPERTY);
+    Assumptions.assumeThat(luceneBuildVersion).isNotNull();
+    for (var module : allCoreModules) {
+      Assertions.assertThat(module.descriptor().rawVersion().orElse(null))
+          .as("Version of module: " + module.descriptor().name())
+          .isEqualTo(luceneBuildVersion);
+    }
+  }
+
+  /** Ensure SPIs are equal for the module and classpath layer. */
+  @Test
+  public void testModularAndClasspathProvidersAreConsistent() throws IOException {
+    for (var module : allCoreModules) {
+      TreeMap<String, TreeSet<String>> modularProviders = getModularServiceProviders(module);
+      TreeMap<String, TreeSet<String>> classpathProviders = getClasspathServiceProviders(module);
+
+      // Compare services first so that the exception is shorter.
+      Assertions.assertThat(modularProviders.keySet())
+          .as("Modular services in module: " + module.descriptor().name())
+          .containsExactlyInAnyOrderElementsOf(classpathProviders.keySet());
+
+      // We're sure the services correspond to each other. Now, for each service, compare the
+      // providers.
+      for (var service : modularProviders.keySet()) {
+        Assertions.assertThat(modularProviders.get(service))
+            .as(
+                "Modular providers of service "
+                    + service
+                    + " in module: "
+                    + module.descriptor().name())
+            .containsExactlyInAnyOrderElementsOf(classpathProviders.get(service));
+      }
+    }
+  }
+
+  private TreeMap<String, TreeSet<String>> getClasspathServiceProviders(ModuleReference module)
+      throws IOException {
+    TreeMap<String, TreeSet<String>> services = new TreeMap<>();
+    Pattern serviceEntryPattern = Pattern.compile("META-INF/services/(?<serviceName>.+)");
+    try (ModuleReader reader = module.open();
+        Stream<String> entryStream = reader.list()) {
+      List<String> serviceProviderEntryList =
+          entryStream
+              .filter(entry -> serviceEntryPattern.matcher(entry).find())
+              .collect(Collectors.toList());
+
+      for (String entry : serviceProviderEntryList) {
+        List<String> implementations;
+        try (InputStream is = reader.open(entry).get()) {
+          implementations =
+              Arrays.stream(new String(is.readAllBytes(), StandardCharsets.UTF_8).split("\n"))
+                  .map(String::trim)
+                  .filter(line -> !line.isBlank() && !line.startsWith("#"))
+                  .collect(Collectors.toList());
+        }
+
+        Matcher matcher = serviceEntryPattern.matcher(entry);
+        if (!matcher.find()) {
+          throw new AssertionError("Impossible.");
+        }
+        String service = matcher.group("serviceName");
+        services.computeIfAbsent(service, k -> new TreeSet<>()).addAll(implementations);
+      }
+    }
+
+    return services;
+  }
+
+  private static TreeMap<String, TreeSet<String>> getModularServiceProviders(
+      ModuleReference module) {
+    return module.descriptor().provides().stream()
+        .collect(
+            Collectors.toMap(
+                ModuleDescriptor.Provides::service,
+                provides -> new TreeSet<>(provides.providers()),
+                (k, v) -> {
+                  throw new RuntimeException();
+                },
+                TreeMap::new));
+  }
+
+  /**
+   * Ensure all exported packages in the descriptor are in sync with the module's Java classes.
+   *
+   * <p>This test should be progressively tuned so that certain internal packages are hidden in the
+   * module layer.
+   */
+  @Test
+  public void testAllOpenPackagesInSync() throws IOException {
+    for (var module : allCoreModules) {
+      Set<String> jarPackages = getJarPackages(module);
+
+      if (module.descriptor().name().equals("org.apache.lucene.luke")) {
+        jarPackages.removeIf(
+            entry -> {
+              // Luke's packages are not exported.
+              return entry.startsWith("org.apache.lucene.luke");
+            });
+      }
+
+      Set<ModuleDescriptor.Exports> moduleExports = module.descriptor().exports();
+      Assertions.assertThat(moduleExports)
+          .as("Exported packages in module: " + module.descriptor().name())
+          .allSatisfy(
+              export -> {
+                Assertions.assertThat(export.targets())
+                    .as("We only support unqualified exports for now?")
+                    .isEmpty();
+              })
+          .map(ModuleDescriptor.Exports::source)
+          .containsExactlyInAnyOrderElementsOf(jarPackages);
+    }
+  }
+
+  private Set<String> getJarPackages(ModuleReference module) throws IOException {
+    try (ModuleReader reader = module.open()) {
+      return reader
+          .list()
+          .filter(
+              entry ->
+                  !entry.startsWith("META-INF/")
+                      && !entry.equals("module-info.class")
+                      && !entry.endsWith("/"))
+          .map(entry -> entry.replaceAll("/[^/]+$", ""))
+          .map(entry -> entry.replace('/', '.'))
+          .collect(Collectors.toCollection(TreeSet::new));
+    }
+  }
+}
diff --git a/lucene/distribution/binary-release.gradle b/lucene/distribution/binary-release.gradle
index c06bb1a..be2ae60 100644
--- a/lucene/distribution/binary-release.gradle
+++ b/lucene/distribution/binary-release.gradle
@@ -28,6 +28,8 @@ configure(project(":lucene:distribution")) {
     jars
     jarsTestFramework
     jarsThirdParty
+
+    binaryDirForTests
   }
 
   dependencies { DependencyHandler handler ->
@@ -63,7 +65,6 @@ configure(project(":lucene:distribution")) {
     }
   }
 
-
   task assembleBinaryTgz(type: Tar) {
     description "Assemble binary Lucene artifact as a .tgz file."
 
@@ -73,6 +74,26 @@ configure(project(":lucene:distribution")) {
     reproducibleFileOrder = true
     compression = Compression.GZIP
 
+    // Internal archive folder for all files.
+    into "lucene-${rootProject.version}/"
+  }
+
+  task assembleBinaryDirForTests(type: Sync) {
+    description "Assemble a subset of the binary Lucene distribution as an expanded directory for tests."
+
+    destinationDir file("${packageBaseName}-itests")
+  }
+
+  artifacts {
+    binaryDirForTests tasks.assembleBinaryDirForTests.destinationDir, {
+      builtBy tasks.assembleBinaryDirForTests
+    }
+  }
+
+  // Configure distribution content for archives and stand-alone directories.
+  // This is split into binaries and other artifacts to speed up distribution
+  // tests.
+  Closure<Void> distributionBinaryContent = { AbstractCopyTask task ->
     // Manually correct posix permissions (matters when assembling archives on Windows).
     filesMatching(["**/*.sh", "**/*.bat"]) { copy ->
       copy.setMode(0755)
@@ -83,6 +104,21 @@ configure(project(":lucene:distribution")) {
       filteringCharset = 'UTF-8'
     })
 
+    // Binary modules (Lucene).
+    from(configurations.jars, {
+      into 'modules'
+    })
+    from(configurations.jarsTestFramework, {
+      into 'modules-test-framework'
+    })
+
+    // Binary modules (with dependencies). Don't duplicate project artifacts.
+    from((configurations.jarsThirdParty - configurations.jars), {
+      into 'modules-thirdparty'
+    })
+  }
+
+  Closure<Void> distributionOtherContent = { AbstractCopyTask task ->
     // Cherry-pick certain files from the root.
     from(project(':').projectDir, {
       include "LICENSE.txt"
@@ -102,21 +138,14 @@ configure(project(":lucene:distribution")) {
     from(configurations.docs, {
       into 'docs'
     })
+  }
 
-    // Binary modules (Lucene).
-    from(configurations.jars, {
-      into 'modules'
-    })
-    from(configurations.jarsTestFramework, {
-      into 'modules-test-framework'
-    })
-
-    // Binary modules (with dependencies). Don't duplicate project artifacts.
-    from((configurations.jarsThirdParty - configurations.jars), {
-      into 'modules-thirdparty'
-    })
+  [tasks.assembleBinaryTgz].each { Task task ->
+    task.configure distributionBinaryContent
+    task.configure distributionOtherContent
+  }
 
-    // Internal archive folder for all files.
-    into "lucene-${rootProject.version}/"
+  [tasks.assembleBinaryDirForTests].each { Task task ->
+    task.configure distributionBinaryContent
   }
 }
diff --git a/lucene/distribution/src/binary-release/bin/luke.cmd b/lucene/distribution/src/binary-release/bin/luke.cmd
index b4591ae..ee5d06c 100644
--- a/lucene/distribution/src/binary-release/bin/luke.cmd
+++ b/lucene/distribution/src/binary-release/bin/luke.cmd
@@ -17,5 +17,5 @@
 
 SETLOCAL
 SET MODULES=%~dp0..
-start javaw --module-path "%MODULES%\modules;%MODULES%\modules-thirdparty" --add-modules jdk.unsupported --module org.apache.lucene.luke
+start javaw --module-path "%MODULES%\modules;%MODULES%\modules-thirdparty" --module org.apache.lucene.luke
 ENDLOCAL
diff --git a/lucene/distribution/src/binary-release/bin/luke.sh b/lucene/distribution/src/binary-release/bin/luke.sh
index 053edda..b934f4e 100644
--- a/lucene/distribution/src/binary-release/bin/luke.sh
+++ b/lucene/distribution/src/binary-release/bin/luke.sh
@@ -17,4 +17,4 @@
 
 MODULES=`dirname "$0"`/..
 MODULES=`cd "$MODULES" && pwd`
-java --module-path "$MODULES/modules:$MODULES/modules-thirdparty" --add-modules jdk.unsupported --module org.apache.lucene.luke
+java --module-path "$MODULES/modules:$MODULES/modules-thirdparty" --module org.apache.lucene.luke
diff --git a/lucene/expressions/build.gradle b/lucene/expressions/build.gradle
index 7ba76a7..6823ae6 100644
--- a/lucene/expressions/build.gradle
+++ b/lucene/expressions/build.gradle
@@ -20,21 +20,14 @@ apply plugin: 'java-library'
 description = 'Dynamically computed values to sort/facet/search on based on a pluggable grammar'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:codecs')
+  moduleImplementation project(':lucene:codecs')
 
-  implementation 'org.antlr:antlr4-runtime'
+  moduleImplementation 'org.antlr:antlr4-runtime'
 
-  // It is awkward that we force-omit the intermediate dependency here...
-  // The dependency chain is:
-  //   asm-commons -> asm-tree -> asm
-  // Should we really go through these hoops?
-  implementation 'org.ow2.asm:asm'
-  implementation('org.ow2.asm:asm-commons', {
-    exclude group: "org.ow2.asm", module: "asm-tree"
-    exclude group: "org.ow2.asm", module: "asm-analysis"
-  })
+  moduleImplementation 'org.ow2.asm:asm'
+  moduleImplementation 'org.ow2.asm:asm-commons'
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/icu/build.gradle b/lucene/expressions/src/java/module-info.java
similarity index 70%
copy from lucene/analysis/icu/build.gradle
copy to lucene/expressions/src/java/module-info.java
index 2eab963..108f453 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/expressions/src/java/module-info.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.expressions {
+  requires org.objectweb.asm;
+  requires org.objectweb.asm.commons;
+  requires antlr4.runtime;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.codecs;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.expressions;
+  exports org.apache.lucene.expressions.js;
 }
diff --git a/lucene/facet/build.gradle b/lucene/facet/build.gradle
index b4f3ac5..1b16f94 100644
--- a/lucene/facet/build.gradle
+++ b/lucene/facet/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 
 description = 'Faceted indexing and search capabilities'
 
-dependencies { 
-  api project(':lucene:core')
+dependencies {
+  moduleApi project(':lucene:core')
 
-  implementation 'com.carrotsearch:hppc'
+  moduleImplementation 'com.carrotsearch:hppc'
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:queries')
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/facet/src/java/module-info.java
similarity index 62%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/facet/src/java/module-info.java
index 153515f..26859cc 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/facet/src/java/module-info.java
@@ -15,18 +15,17 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Faceted indexing and search capabilities */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.facet {
+  requires java.logging;
+  requires com.carrotsearch.hppc;
+  requires org.apache.lucene.core;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.facet;
+  exports org.apache.lucene.facet.range;
+  exports org.apache.lucene.facet.sortedset;
+  exports org.apache.lucene.facet.taxonomy;
+  exports org.apache.lucene.facet.taxonomy.directory;
+  exports org.apache.lucene.facet.taxonomy.writercache;
 }
diff --git a/lucene/grouping/build.gradle b/lucene/grouping/build.gradle
index 6e71964..7035a11 100644
--- a/lucene/grouping/build.gradle
+++ b/lucene/grouping/build.gradle
@@ -21,9 +21,9 @@ apply plugin: 'java-library'
 description = 'Collectors for grouping search results'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:queries')
+  moduleImplementation project(':lucene:queries')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/backward-codecs/build.gradle b/lucene/grouping/src/java/module-info.java
similarity index 80%
copy from lucene/backward-codecs/build.gradle
copy to lucene/grouping/src/java/module-info.java
index aa47302..0f9665e 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/grouping/src/java/module-info.java
@@ -15,12 +15,10 @@
  * limitations under the License.
  */
 
+/** Collectors for grouping search results */
+module org.apache.lucene.grouping {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.search.grouping;
 }
diff --git a/lucene/highlighter/build.gradle b/lucene/highlighter/build.gradle
index 6bd8426..105f1de 100644
--- a/lucene/highlighter/build.gradle
+++ b/lucene/highlighter/build.gradle
@@ -21,10 +21,10 @@ apply plugin: 'java-library'
 description = 'Highlights search keywords in results'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:queries')
-  implementation project(':lucene:memory')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:memory')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:analysis:common')
diff --git a/lucene/analysis/icu/build.gradle b/lucene/highlighter/src/java/module-info.java
similarity index 67%
copy from lucene/analysis/icu/build.gradle
copy to lucene/highlighter/src/java/module-info.java
index 2eab963..64da8b5 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/highlighter/src/java/module-info.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Highlights search keywords in results */
+module org.apache.lucene.highlighter {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.memory;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.search.highlight;
+  exports org.apache.lucene.search.matchhighlight;
+  exports org.apache.lucene.search.uhighlight;
+  exports org.apache.lucene.search.vectorhighlight;
 }
diff --git a/lucene/join/build.gradle b/lucene/join/build.gradle
index 143daa1..d8c2eab 100644
--- a/lucene/join/build.gradle
+++ b/lucene/join/build.gradle
@@ -20,6 +20,6 @@ apply plugin: 'java-library'
 description = 'Index-time and Query-time joins for normalized content'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 }
\ No newline at end of file
diff --git a/lucene/backward-codecs/build.gradle b/lucene/join/src/java/module-info.java
similarity index 81%
copy from lucene/backward-codecs/build.gradle
copy to lucene/join/src/java/module-info.java
index aa47302..80d2261 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/join/src/java/module-info.java
@@ -15,12 +15,9 @@
  * limitations under the License.
  */
 
+/** Index-time and Query-time joins for normalized content */
+module org.apache.lucene.join {
+  requires org.apache.lucene.core;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.search.join;
 }
diff --git a/lucene/licenses/asm-analysis-7.2.jar.sha1 b/lucene/licenses/asm-analysis-7.2.jar.sha1
new file mode 100644
index 0000000..b0a8b7b
--- /dev/null
+++ b/lucene/licenses/asm-analysis-7.2.jar.sha1
@@ -0,0 +1 @@
+b6e6abe057f23630113f4167c34bda7086691258
diff --git a/lucene/licenses/asm-tree-7.2.jar.sha1 b/lucene/licenses/asm-tree-7.2.jar.sha1
new file mode 100644
index 0000000..077d6f9
--- /dev/null
+++ b/lucene/licenses/asm-tree-7.2.jar.sha1
@@ -0,0 +1 @@
+3a23cc36edaf8fc5a89cb100182758ccb5991487
diff --git a/lucene/licenses/assertj-core-3.21.0.jar.sha1 b/lucene/licenses/assertj-core-3.21.0.jar.sha1
new file mode 100644
index 0000000..9108259
--- /dev/null
+++ b/lucene/licenses/assertj-core-3.21.0.jar.sha1
@@ -0,0 +1 @@
+27a14d6d22c4e3d58f799fb2a5ca8eaf53e6942a
diff --git a/lucene/licenses/assertj-core-LICENSE-ASL.txt b/lucene/licenses/assertj-core-LICENSE-ASL.txt
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/lucene/licenses/assertj-core-LICENSE-ASL.txt
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/lucene/licenses/assertj-core-NOTICE.txt b/lucene/licenses/assertj-core-NOTICE.txt
new file mode 100644
index 0000000..e69de29
diff --git a/lucene/luke/build.gradle b/lucene/luke/build.gradle
index fe556ca..6def3e6 100644
--- a/lucene/luke/build.gradle
+++ b/lucene/luke/build.gradle
@@ -25,25 +25,25 @@ ext {
 }
 
 dependencies {
-  api project(':lucene:core')
-
-  implementation project(':lucene:codecs')
-  implementation project(':lucene:backward-codecs')
-  implementation project(':lucene:analysis:common')
-  implementation project(':lucene:queries')
-  implementation project(':lucene:queryparser')
-  implementation project(':lucene:misc')
-
-  implementation project(":lucene:highlighter")
-  implementation project(':lucene:analysis:icu')
-  implementation project(':lucene:analysis:kuromoji')
-  implementation project(':lucene:analysis:morfologik')
-  implementation project(':lucene:analysis:nori')
-  implementation project(':lucene:analysis:opennlp')
-  implementation project(':lucene:analysis:phonetic')
-  implementation project(':lucene:analysis:smartcn')
-  implementation project(':lucene:analysis:stempel')
-  implementation project(':lucene:suggest')
+  moduleApi project(':lucene:core')
+
+  moduleImplementation project(':lucene:codecs')
+  moduleImplementation project(':lucene:backward-codecs')
+  moduleImplementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:queries')
+  moduleImplementation project(':lucene:queryparser')
+  moduleImplementation project(':lucene:misc')
+
+  moduleImplementation project(":lucene:highlighter")
+  moduleImplementation project(':lucene:analysis:icu')
+  moduleImplementation project(':lucene:analysis:kuromoji')
+  moduleImplementation project(':lucene:analysis:morfologik')
+  moduleImplementation project(':lucene:analysis:nori')
+  moduleImplementation project(':lucene:analysis:opennlp')
+  moduleImplementation project(':lucene:analysis:phonetic')
+  moduleImplementation project(':lucene:analysis:smartcn')
+  moduleImplementation project(':lucene:analysis:stempel')
+  moduleImplementation project(':lucene:suggest')
 
   testImplementation project(':lucene:test-framework')
 }
@@ -55,6 +55,11 @@ tasks.withType(Jar) {
   }
 }
 
+// Configure the main class and version attribute for the module system.
+tasks.compileJava.configure {
+  options.javaModuleMainClass.set("org.apache.lucene.luke.app.desktop.LukeMain")
+}
+
 // Process UTF8 property files to unicode escapes.
 tasks.withType(ProcessResources).configureEach { task ->
   task.filesMatching("**/messages*.properties", {
@@ -105,8 +110,13 @@ task standaloneAssemble(type: Sync) {
   into standaloneDistDir
 
   doLast {
-    logger.lifecycle("Standalone Luke distribution assembled. You can run it with:\n"
-        + "java -jar " + file("${standaloneDistDir}/${standaloneJar.archiveFileName.get()}"))
+    logger.lifecycle(
+        """
+Standalone Luke distribution assembled. You can run it with:
+java -jar "${standaloneDistDir}/${standaloneJar.archiveFileName.get()}"
+java --module-path "${standaloneDistDir}" -m org.apache.lucene.luke
+        """
+    )
   }
 }
 
@@ -141,3 +151,4 @@ task run() {
     }
   }
 }
+
diff --git a/lucene/analysis/icu/build.gradle b/lucene/luke/src/java/module-info.java
similarity index 71%
copy from lucene/analysis/icu/build.gradle
copy to lucene/luke/src/java/module-info.java
index 2eab963..101d4ae 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/luke/src/java/module-info.java
@@ -15,15 +15,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
-
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+/** Luke : Lucene toolbox project. */
+module org.apache.lucene.luke {
+  requires java.desktop;
+  requires java.logging;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.queryparser;
+  requires org.apache.lucene.misc;
 }
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
index f64ec18..cd78468 100644
--- a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/LukeMain.java
@@ -20,8 +20,8 @@ package org.apache.lucene.luke.app.desktop;
 import static org.apache.lucene.luke.app.desktop.util.ExceptionHandler.handle;
 
 import java.awt.GraphicsEnvironment;
-import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.concurrent.SynchronousQueue;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.swing.JFrame;
@@ -48,19 +48,19 @@ public class LukeMain {
     return frame;
   }
 
-  private static void createAndShowGUI() {
+  /** @return Returns {@code true} if GUI startup and initialization was successful. */
+  private static boolean createAndShowGUI() {
     // uncaught error handler
     MessageBroker messageBroker = MessageBroker.getInstance();
-    Thread.setDefaultUncaughtExceptionHandler((thread, cause) -> handle(cause, messageBroker));
-
     try {
+      Thread.setDefaultUncaughtExceptionHandler((thread, cause) -> handle(cause, messageBroker));
+
       frame = new LukeWindowProvider().get();
       frame.setLocation(200, 100);
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.pack();
       frame.setVisible(true);
 
-      // show open index dialog
       OpenIndexDialogFactory openIndexDialogFactory = OpenIndexDialogFactory.getInstance();
       new DialogOpener<>(openIndexDialogFactory)
           .open(
@@ -68,9 +68,12 @@ public class LukeMain {
               600,
               420,
               (factory) -> {});
-    } catch (IOException e) {
+
+      return true;
+    } catch (Throwable e) {
       messageBroker.showUnknownErrorMessage();
       log.log(Level.SEVERE, "Cannot initialize components.", e);
+      return false;
     }
   }
 
@@ -86,6 +89,19 @@ public class LukeMain {
     GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
     genv.registerFont(FontUtils.createElegantIconFont());
 
-    javax.swing.SwingUtilities.invokeLater(LukeMain::createAndShowGUI);
+    var guiThreadResult = new SynchronousQueue<Boolean>();
+    javax.swing.SwingUtilities.invokeLater(
+        () -> {
+          try {
+            guiThreadResult.put(createAndShowGUI());
+          } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+          }
+        });
+
+    if (Boolean.FALSE.equals(guiThreadResult.take())) {
+      Logger.getGlobal().log(Level.SEVERE, "Luke could not start.");
+      Runtime.getRuntime().exit(1);
+    }
   }
 }
diff --git a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java
index 02c201e..90f5c2c 100644
--- a/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java
+++ b/lucene/luke/src/java/org/apache/lucene/luke/app/desktop/util/FontUtils.java
@@ -27,9 +27,7 @@ import javax.swing.JLabel;
 
 /** Font utilities */
 public class FontUtils {
-
-  public static final String TTF_RESOURCE_NAME =
-      "org/apache/lucene/luke/app/desktop/font/ElegantIcons.ttf";
+  private static final String TTF_RESOURCE_NAME = "ElegantIcons.ttf";
 
   @SuppressWarnings("unchecked")
   public static JLabel toLinkText(JLabel label) {
@@ -42,7 +40,7 @@ public class FontUtils {
   }
 
   public static Font createElegantIconFont() throws IOException, FontFormatException {
-    InputStream is = FontUtils.class.getClassLoader().getResourceAsStream(TTF_RESOURCE_NAME);
+    InputStream is = FontUtils.class.getResourceAsStream(TTF_RESOURCE_NAME);
     return Font.createFont(Font.TRUETYPE_FONT, is);
   }
 
diff --git a/lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/font/ElegantIcons.ttf b/lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/util/ElegantIcons.ttf
similarity index 100%
rename from lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/font/ElegantIcons.ttf
rename to lucene/luke/src/resources/org/apache/lucene/luke/app/desktop/util/ElegantIcons.ttf
diff --git a/lucene/memory/build.gradle b/lucene/memory/build.gradle
index 0d3bc1f..eb6626e 100644
--- a/lucene/memory/build.gradle
+++ b/lucene/memory/build.gradle
@@ -21,7 +21,7 @@ apply plugin: 'java-library'
 description = 'Single-document in-memory index implementation'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:queryparser')
diff --git a/lucene/backward-codecs/build.gradle b/lucene/memory/src/java/module-info.java
similarity index 81%
copy from lucene/backward-codecs/build.gradle
copy to lucene/memory/src/java/module-info.java
index aa47302..1d4f84a 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/memory/src/java/module-info.java
@@ -15,12 +15,9 @@
  * limitations under the License.
  */
 
+/** Single-document in-memory index implementation */
+module org.apache.lucene.memory {
+  requires org.apache.lucene.core;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.index.memory;
 }
diff --git a/lucene/misc/build.gradle b/lucene/misc/build.gradle
index efed4f2..b9d7626 100644
--- a/lucene/misc/build.gradle
+++ b/lucene/misc/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Index tools and other miscellaneous code'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
   testImplementation project(':lucene:test-framework')
 
   nativeDeps project(":lucene:misc:native")
diff --git a/lucene/analysis/icu/build.gradle b/lucene/misc/src/java/module-info.java
similarity index 67%
copy from lucene/analysis/icu/build.gradle
copy to lucene/misc/src/java/module-info.java
index 2eab963..269e6b0 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/misc/src/java/module-info.java
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Index tools and other miscellaneous code */
+module org.apache.lucene.misc {
+  requires org.apache.lucene.core;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.misc;
+  exports org.apache.lucene.misc.document;
+  exports org.apache.lucene.misc.index;
+  exports org.apache.lucene.misc.search;
+  exports org.apache.lucene.misc.store;
+  exports org.apache.lucene.misc.util;
+  exports org.apache.lucene.misc.util.fst;
 }
diff --git a/lucene/monitor/build.gradle b/lucene/monitor/build.gradle
index 3dd65a4..ff6677f 100644
--- a/lucene/monitor/build.gradle
+++ b/lucene/monitor/build.gradle
@@ -20,10 +20,10 @@ apply plugin: 'java-library'
 description = 'Reverse-search implementation for monitoring and classification'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:memory')
-  implementation project(':lucene:analysis:common')
+  moduleImplementation project(':lucene:memory')
+  moduleImplementation project(':lucene:analysis:common')
 
   testImplementation project(':lucene:queryparser')
   testImplementation project(':lucene:test-framework')
diff --git a/lucene/analysis/stempel/build.gradle b/lucene/monitor/src/java/module-info.java
similarity index 75%
copy from lucene/analysis/stempel/build.gradle
copy to lucene/monitor/src/java/module-info.java
index 3b80f9f..71b70f0 100644
--- a/lucene/analysis/stempel/build.gradle
+++ b/lucene/monitor/src/java/module-info.java
@@ -15,13 +15,11 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Reverse-search implementation for monitoring and classification */
+module org.apache.lucene.monitor {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+  requires org.apache.lucene.memory;
 
-description = 'Analyzer for indexing Polish'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.monitor;
 }
diff --git a/lucene/queries/build.gradle b/lucene/queries/build.gradle
index bd56a4d..d0cc010 100644
--- a/lucene/queries/build.gradle
+++ b/lucene/queries/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Filters and Queries that add to core Lucene'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
   testImplementation project(':lucene:test-framework')
   testImplementation project(':lucene:expressions')
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/queries/src/java/module-info.java
similarity index 61%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/queries/src/java/module-info.java
index 153515f..ae3ff4d 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/queries/src/java/module-info.java
@@ -15,18 +15,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Filters and Queries that add to core Lucene */
+module org.apache.lucene.queries {
+  requires org.apache.lucene.core;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.queries;
+  exports org.apache.lucene.queries.function;
+  exports org.apache.lucene.queries.function.docvalues;
+  exports org.apache.lucene.queries.function.valuesource;
+  exports org.apache.lucene.queries.intervals;
+  exports org.apache.lucene.queries.mlt;
+  exports org.apache.lucene.queries.payloads;
+  exports org.apache.lucene.queries.spans;
 }
diff --git a/lucene/queryparser/build.gradle b/lucene/queryparser/build.gradle
index 077eb24..b50b565 100644
--- a/lucene/queryparser/build.gradle
+++ b/lucene/queryparser/build.gradle
@@ -20,9 +20,9 @@ apply plugin: 'java-library'
 description = 'Query parsers and parsing framework'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:queries')
-  api project(':lucene:sandbox')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:queries')
+  moduleApi project(':lucene:sandbox')
 
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/queryparser/src/java/module-info.java b/lucene/queryparser/src/java/module-info.java
new file mode 100644
index 0000000..3dab16d
--- /dev/null
+++ b/lucene/queryparser/src/java/module-info.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Query parsers and parsing framework */
+module org.apache.lucene.queryparser {
+  requires java.xml;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
+  requires org.apache.lucene.sandbox;
+
+  exports org.apache.lucene.queryparser.charstream;
+  exports org.apache.lucene.queryparser.classic;
+  exports org.apache.lucene.queryparser.complexPhrase;
+  exports org.apache.lucene.queryparser.ext;
+  exports org.apache.lucene.queryparser.flexible.core;
+  exports org.apache.lucene.queryparser.flexible.core.builders;
+  exports org.apache.lucene.queryparser.flexible.core.config;
+  exports org.apache.lucene.queryparser.flexible.core.messages;
+  exports org.apache.lucene.queryparser.flexible.core.nodes;
+  exports org.apache.lucene.queryparser.flexible.core.parser;
+  exports org.apache.lucene.queryparser.flexible.core.processors;
+  exports org.apache.lucene.queryparser.flexible.core.util;
+  exports org.apache.lucene.queryparser.flexible.messages;
+  exports org.apache.lucene.queryparser.flexible.precedence;
+  exports org.apache.lucene.queryparser.flexible.precedence.processors;
+  exports org.apache.lucene.queryparser.flexible.standard;
+  exports org.apache.lucene.queryparser.flexible.standard.builders;
+  exports org.apache.lucene.queryparser.flexible.standard.config;
+  exports org.apache.lucene.queryparser.flexible.standard.nodes;
+  exports org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn;
+  exports org.apache.lucene.queryparser.flexible.standard.parser;
+  exports org.apache.lucene.queryparser.flexible.standard.processors;
+  exports org.apache.lucene.queryparser.simple;
+  exports org.apache.lucene.queryparser.surround.parser;
+  exports org.apache.lucene.queryparser.surround.query;
+  exports org.apache.lucene.queryparser.xml;
+  exports org.apache.lucene.queryparser.xml.builders;
+}
diff --git a/lucene/replicator/build.gradle b/lucene/replicator/build.gradle
index c51feb0..d3dfc07 100644
--- a/lucene/replicator/build.gradle
+++ b/lucene/replicator/build.gradle
@@ -20,15 +20,15 @@ apply plugin: 'java-library'
 description = 'Lucene index files replication utility'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
-  implementation project(':lucene:facet')
+  moduleImplementation project(':lucene:facet')
 
-  implementation('org.apache.httpcomponents:httpclient', {
+  moduleImplementation('org.apache.httpcomponents:httpclient', {
     exclude group: "commons-codec", module: "commons-codec"
   })
 
-  implementation 'javax.servlet:javax.servlet-api'
+  moduleImplementation 'javax.servlet:javax.servlet-api'
 
   testImplementation 'org.eclipse.jetty:jetty-server'
   testImplementation('org.eclipse.jetty:jetty-servlet', {
diff --git a/lucene/analysis/icu/build.gradle b/lucene/replicator/src/java/module-info.java
similarity index 66%
copy from lucene/analysis/icu/build.gradle
copy to lucene/replicator/src/java/module-info.java
index 2eab963..d44b9ad 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/replicator/src/java/module-info.java
@@ -15,15 +15,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Lucene index files replication utility */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.replicator {
+  requires javax.servlet.api;
+  requires org.apache.httpcomponents.httpclient;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.facet;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.replicator;
+  exports org.apache.lucene.replicator.http;
+  exports org.apache.lucene.replicator.nrt;
 }
diff --git a/lucene/sandbox/build.gradle b/lucene/sandbox/build.gradle
index a1a64d1..b7e8018 100644
--- a/lucene/sandbox/build.gradle
+++ b/lucene/sandbox/build.gradle
@@ -20,7 +20,7 @@ apply plugin: 'java-library'
 description = 'Various third party contributions and new ideas'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:queries')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:queries')
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/analysis/morfologik/build.gradle b/lucene/sandbox/src/java/module-info.java
similarity index 60%
copy from lucene/analysis/morfologik/build.gradle
copy to lucene/sandbox/src/java/module-info.java
index 153515f..6d19544 100644
--- a/lucene/analysis/morfologik/build.gradle
+++ b/lucene/sandbox/src/java/module-info.java
@@ -15,18 +15,17 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Various third party contributions and new ideas */
+module org.apache.lucene.sandbox {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.queries;
 
-description = 'Analyzer for dictionary stemming, built-in Polish dictionary'
+  exports org.apache.lucene.payloads;
+  exports org.apache.lucene.sandbox.codecs.idversion;
+  exports org.apache.lucene.sandbox.document;
+  exports org.apache.lucene.sandbox.queries;
+  exports org.apache.lucene.sandbox.search;
 
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'org.carrot2:morfologik-stemming'
-
-  implementation 'org.carrot2:morfologik-polish'
-  implementation 'ua.net.nlp:morfologik-ukrainian-search'
-
-  testImplementation project(':lucene:test-framework')
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.sandbox.codecs.idversion.IDVersionPostingsFormat;
 }
diff --git a/lucene/spatial-extras/build.gradle b/lucene/spatial-extras/build.gradle
index 252056b..34fa10e 100644
--- a/lucene/spatial-extras/build.gradle
+++ b/lucene/spatial-extras/build.gradle
@@ -20,16 +20,16 @@ apply plugin: 'java-library'
 description = 'Geospatial search'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:spatial3d')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:spatial3d')
 
-  api 'org.locationtech.spatial4j:spatial4j'
-  api 'io.sgr:s2-geometry-library-java'
+  moduleApi 'org.locationtech.spatial4j:spatial4j'
+  moduleApi 'io.sgr:s2-geometry-library-java'
 
   testImplementation project(':lucene:test-framework')
   testImplementation testFixtures(project(':lucene:spatial3d'))
 
-  testImplementation 'org.locationtech.jts:jts-core'
+  moduleTestImplementation 'org.locationtech.jts:jts-core'
   testImplementation 'org.locationtech.spatial4j:spatial4j::tests'
 }
 
diff --git a/lucene/analysis/icu/build.gradle b/lucene/spatial-extras/src/java/module-info.java
similarity index 53%
copy from lucene/analysis/icu/build.gradle
copy to lucene/spatial-extras/src/java/module-info.java
index 2eab963..2464761 100644
--- a/lucene/analysis/icu/build.gradle
+++ b/lucene/spatial-extras/src/java/module-info.java
@@ -15,15 +15,23 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
+/** Geospatial search */
+@SuppressWarnings({"requires-automatic"})
+module org.apache.lucene.spatial_extras {
+  requires java.logging;
+  requires spatial4j;
+  requires s2.geometry.library.java;
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.spatial3d;
 
-description = 'Analysis integration with ICU (International Components for Unicode)'
-
-dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
-
-  api 'com.ibm.icu:icu4j'
-
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.spatial;
+  exports org.apache.lucene.spatial.bbox;
+  exports org.apache.lucene.spatial.composite;
+  exports org.apache.lucene.spatial.prefix;
+  exports org.apache.lucene.spatial.prefix.tree;
+  exports org.apache.lucene.spatial.query;
+  exports org.apache.lucene.spatial.serialized;
+  exports org.apache.lucene.spatial.spatial4j;
+  exports org.apache.lucene.spatial.util;
+  exports org.apache.lucene.spatial.vector;
 }
diff --git a/lucene/spatial3d/build.gradle b/lucene/spatial3d/build.gradle
index db2064c..219d029 100644
--- a/lucene/spatial3d/build.gradle
+++ b/lucene/spatial3d/build.gradle
@@ -21,7 +21,7 @@ apply plugin: 'java-test-fixtures'
 description = '3D spatial planar geometry APIs'
 
 dependencies {
-  api project(':lucene:core')
+  moduleApi project(':lucene:core')
 
   testFixturesApi project(':lucene:test-framework')
   testImplementation project(':lucene:test-framework')
diff --git a/lucene/backward-codecs/build.gradle b/lucene/spatial3d/src/java/module-info.java
similarity index 80%
copy from lucene/backward-codecs/build.gradle
copy to lucene/spatial3d/src/java/module-info.java
index aa47302..9bffb23 100644
--- a/lucene/backward-codecs/build.gradle
+++ b/lucene/spatial3d/src/java/module-info.java
@@ -15,12 +15,10 @@
  * limitations under the License.
  */
 
+/** 3D spatial planar geometry APIs */
+module org.apache.lucene.spatial3d {
+  requires org.apache.lucene.core;
 
-apply plugin: 'java-library'
-
-description = 'Codecs for older versions of Lucene'
-
-dependencies { 
-  api project(':lucene:core')
-  testImplementation project(':lucene:test-framework')
+  exports org.apache.lucene.spatial3d;
+  exports org.apache.lucene.spatial3d.geom;
 }
diff --git a/lucene/suggest/build.gradle b/lucene/suggest/build.gradle
index 54a5596..3afccd8 100644
--- a/lucene/suggest/build.gradle
+++ b/lucene/suggest/build.gradle
@@ -20,8 +20,8 @@ apply plugin: 'java-library'
 description = 'Auto-suggest and Spellchecking support'
 
 dependencies {
-  api project(':lucene:core')
-  api project(':lucene:analysis:common')
+  moduleApi project(':lucene:core')
+  moduleApi project(':lucene:analysis:common')
   
   testImplementation project(':lucene:test-framework')
 }
diff --git a/lucene/suggest/src/java/module-info.java b/lucene/suggest/src/java/module-info.java
new file mode 100644
index 0000000..4444a9dd
--- /dev/null
+++ b/lucene/suggest/src/java/module-info.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/** Auto-suggest and Spellchecking support */
+module org.apache.lucene.suggest {
+  requires org.apache.lucene.core;
+  requires org.apache.lucene.analysis.common;
+
+  exports org.apache.lucene.search.spell;
+  exports org.apache.lucene.search.suggest;
+  exports org.apache.lucene.search.suggest.analyzing;
+  exports org.apache.lucene.search.suggest.document;
+  exports org.apache.lucene.search.suggest.fst;
+  exports org.apache.lucene.search.suggest.tst;
+
+  provides org.apache.lucene.codecs.PostingsFormat with
+      org.apache.lucene.search.suggest.document.Completion50PostingsFormat,
+      org.apache.lucene.search.suggest.document.Completion84PostingsFormat,
+      org.apache.lucene.search.suggest.document.Completion90PostingsFormat;
+  provides org.apache.lucene.analysis.TokenFilterFactory with
+      org.apache.lucene.search.suggest.analyzing.SuggestStopFilterFactory;
+}
diff --git a/settings.gradle b/settings.gradle
index e965095..2d48e41 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -30,6 +30,7 @@ include "lucene:analysis:common"
 include "lucene:analysis:icu"
 include "lucene:analysis:kuromoji"
 include "lucene:analysis:morfologik"
+include "lucene:analysis:morfologik.tests"
 include "lucene:analysis:nori"
 include "lucene:analysis:opennlp"
 include "lucene:analysis:phonetic"
@@ -41,6 +42,9 @@ include "lucene:classification"
 include "lucene:codecs"
 include "lucene:core"
 include "lucene:demo"
+include "lucene:distribution"
+include "lucene:distribution.tests"
+include "lucene:documentation"
 include "lucene:expressions"
 include "lucene:facet"
 include "lucene:grouping"
@@ -55,9 +59,7 @@ include "lucene:queries"
 include "lucene:queryparser"
 include "lucene:replicator"
 include "lucene:sandbox"
-include "lucene:spatial-extras"
 include "lucene:spatial3d"
+include "lucene:spatial-extras"
 include "lucene:suggest"
 include "lucene:test-framework"
-include "lucene:documentation"
-include "lucene:distribution"
diff --git a/versions.lock b/versions.lock
index 87c5b3f..c596e6e 100644
--- a/versions.lock
+++ b/versions.lock
@@ -18,12 +18,15 @@ org.carrot2:morfologik-polish:2.1.8 (1 constraints: 0d050036)
 org.carrot2:morfologik-stemming:2.1.8 (2 constraints: 1112dc0c)
 org.hamcrest:hamcrest:2.2 (1 constraints: a8041f2c)
 org.locationtech.spatial4j:spatial4j:0.8 (1 constraints: ac041f2c)
-org.ow2.asm:asm:7.2 (2 constraints: 900e3e5e)
+org.ow2.asm:asm:7.2 (3 constraints: 2717d96b)
+org.ow2.asm:asm-analysis:7.2 (1 constraints: e409d9a5)
 org.ow2.asm:asm-commons:7.2 (1 constraints: ad042e2c)
+org.ow2.asm:asm-tree:7.2 (2 constraints: 2f14468c)
 ua.net.nlp:morfologik-ukrainian-search:4.9.1 (1 constraints: 10051b36)
 xerces:xercesImpl:2.12.0 (2 constraints: 1f14b675)
 
 [Test dependencies]
+org.assertj:assertj-core:3.21.0 (1 constraints: 38053c3b)
 org.eclipse.jetty:jetty-continuation:9.4.41.v20210516 (1 constraints: 7907fe7c)
 org.eclipse.jetty:jetty-http:9.4.41.v20210516 (1 constraints: f60f2ccd)
 org.eclipse.jetty:jetty-io:9.4.41.v20210516 (2 constraints: 141f4566)
diff --git a/versions.props b/versions.props
index c010392..995a7dc 100644
--- a/versions.props
+++ b/versions.props
@@ -11,6 +11,7 @@ org.antlr:antlr4*=4.5.1-1
 org.apache.commons:commons-compress=1.19
 org.apache.httpcomponents:httpclient=4.5.13
 org.apache.opennlp:opennlp-tools=1.9.1
+org.assertj:*=3.21.0
 org.carrot2:morfologik-*=2.1.8
 org.eclipse.jetty:*=9.4.41.v20210516
 org.hamcrest:*=2.2
@@ -18,4 +19,4 @@ org.locationtech.jts:jts-core=1.17.0
 org.locationtech.spatial4j:*=0.8
 org.ow2.asm:*=7.2
 ua.net.nlp:morfologik-ukrainian-search=4.9.1
-xerces:xercesImpl=2.12.0
+xerces:xercesImpl=2.12.0
\ No newline at end of file