You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/08/15 09:32:37 UTC
[sis] 07/08: Add Gradle build scripts. Port Maven MOJO to Gradle plugin.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 70ccdd51e5398ed5727795ae03522b7c98a284fe
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat May 20 19:11:46 2023 +0200
Add Gradle build scripts.
Port Maven MOJO to Gradle plugin.
---
.gitignore | 7 +-
buildSrc/build.gradle.kts | 87 +++++
.../{internal => buildtools}/book/Assembler.java | 6 +-
.../book/BookException.java | 4 +-
.../{internal => buildtools}/book/Characters.java | 4 +-
.../book/CodeColorizer.java | 4 +-
.../sis/{internal => buildtools}/book/GEOAPI.lst | 0
.../sis/{internal => buildtools}/book/OGC.lst | 0
.../sis/{internal => buildtools}/book/SIS.lst | 0
.../{internal => buildtools}/book/XML_PREFIX.lst | 0
.../book/package-info.java | 8 +-
.../{internal => buildtools}/doclet/Doclet.java | 72 +---
.../{internal => buildtools}/doclet/Include.java | 4 +-
.../{internal => buildtools}/doclet/Rewriter.java | 11 +-
.../{internal => buildtools}/doclet/Taglet.java | 4 +-
.../doclet/package-info.java | 4 +-
.../apache/sis/buildtools/gradle/Assembler.java | 131 +++++++
.../apache/sis/buildtools/gradle/BuildHelper.java | 180 ++++++++++
.../apache/sis/buildtools/gradle/Conventions.java | 192 ++++++++++
.../apache/sis/buildtools/gradle/Dependency.java | 271 ++++++++++++++
.../apache/sis/buildtools/gradle/JavaMaker.java | 118 +++++++
.../sis/buildtools/gradle/ModularCompilation.java | 283 +++++++++++++++
.../apache/sis/buildtools/gradle/ModularJAR.java | 176 +++++++++
.../sis/buildtools/gradle/ModularJavadoc.java | 200 +++++++++++
.../sis/buildtools/gradle/ModularPublishing.java | 79 +++++
.../apache/sis/buildtools/gradle/ModularTest.java | 260 ++++++++++++++
.../gradle/SISBuildException.java} | 27 +-
.../org/apache/sis/buildtools/gradle/UnoPkg.java | 215 +++++++++++
.../apache/sis/buildtools/gradle/ZipWriter.java | 286 +++++++++++++++
.../maven => buildtools/gradle}/package-info.java | 17 +-
.../org/apache/sis/buildtools/maven/Element.java | 73 ++++
.../org/apache/sis/buildtools/maven/Generator.java | 239 +++++++++++++
.../doclet => buildtools/maven}/package-info.java | 13 +-
.../sis/buildtools/maven/workaround/Argument.java | 100 ++++++
.../maven/workaround/ProxyGenerator.java | 279 +++++++++++++++
.../buildtools/maven/workaround/package-info.java | 38 ++
.../resources/IndexedResourceCompiler.java | 321 ++++++++++++-----
.../resources}/package-info.java | 11 +-
.../org/apache/sis/internal/maven/Assembler.java | 270 --------------
.../org/apache/sis/internal/maven/Filenames.java | 111 ------
.../apache/sis/internal/maven/JarCollector.java | 282 ---------------
.../org/apache/sis/internal/unopkg/JavaMaker.java | 148 --------
.../org/apache/sis/internal/unopkg/UnoPkg.java | 295 ----------------
.../apache/sis/internal/unopkg/package-info.java | 87 -----
.../sis/util/resources/ResourceCompilerMojo.java | 283 ---------------
.../org/apache/sis/util/resources/package-info.txt | 2 -
.../resources/IndexedResourceCompilerTest.java | 65 ++++
.../util/resources/ResourceCompilerMojoTest.java | 51 ---
endorsed/build.gradle.kts | 392 +++++++++++++++++++++
.../bundle/build-instruction.html | 4 +-
.../org/apache/sis/openoffice/XReferencing.java | 2 +-
.../org/apache/sis/openoffice/package-info.java | 2 +-
.../src/org.apache.sis.test/test/module-info.java | 14 +-
incubator/build.gradle.kts | 144 ++++++++
.../main/antlr/org/apache/sis/internal/cql/CQL.g4 | 3 +
.../main/org/apache/sis/internal/cql/.gitignore | 7 +
.../src/org.apache.sis.test/test/module-info.java | 12 +-
optional/build.gradle.kts | 168 +++++++++
.../src/org.apache.sis.test/test/module-info.java | 12 +-
settings.gradle.kts | 92 +++++
60 files changed, 4387 insertions(+), 1783 deletions(-)
diff --git a/.gitignore b/.gitignore
index b233e1efca..6840c52153 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,11 @@
-target/
+# Gradle files and build results
+.gradle/
+build/
*.tmp
*.log
+
+# Legacy systems (should not be used anymore)
+target/
.svn/
# Generated by NetBeans IDE
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000000..c32e77e311
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,87 @@
+/*
+ * 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 {
+ `java-gradle-plugin`
+}
+
+gradlePlugin {
+ plugins {
+ create("buildTools") {
+ id = "org.apache.sis.buildtools"
+ implementationClass = "org.apache.sis.buildtools.gradle.BuildHelper"
+ }
+ }
+}
+
+/*
+ * Source files are organized following the same convention than other Apache SIS sub-projects.
+ * However since there is currently only one module in `buildSRC`, we do not need to configure
+ * for Module Source Hierarchy. We let Gradle uses Package Hierarchy (the default Gradle mode)
+ * and simulate the module source hierarchy with `setSrcDirs(…)`.
+ */
+sourceSets {
+ main {
+ java {
+ setSrcDirs(listOf("src/org.apache.sis.buildtools/main"))
+ }
+ resources {
+ setSrcDirs(listOf("src/org.apache.sis.buildtools/main"))
+ include("**/*.lst")
+ }
+ }
+ test {
+ java {
+ setSrcDirs(listOf("src/org.apache.sis.buildtools/test"))
+ }
+ }
+}
+
+/*
+ * All dependencies used by the plugin.
+ */
+repositories {
+ mavenCentral()
+}
+dependencies {
+ implementation (group = "org.apache.commons", name = "commons-compress", version = "1.23.0")
+ testCompileOnly(group = "junit", name = "junit", version = "4.13.2")
+ testRuntimeOnly(group = "org.junit.vintage", name = "junit-vintage-engine", version = "5.9.3")
+ testRuntimeOnly(group = "org.junit.platform", name = "junit-platform-launcher", version = "1.9.3")
+}
+
+/*
+ * Compile main classes from Package Hierarchy.
+ */
+tasks.compileJava {
+ options.encoding = "UTF-8" // The character encoding to be used when reading source files.
+ options.setDeprecation(true) // Whether to log details of usage of deprecated members or classes.
+}
+tasks.compileTestJava {
+ options.encoding = "UTF-8"
+ options.setDeprecation(true)
+}
+
+/*
+ * Discover and execute JUnit-based tests.
+ */
+tasks.test {
+ useJUnitPlatform()
+ testLogging {
+ events("PASSED", "SKIPPED", "FAILED", "STANDARD_OUT", "STANDARD_ERROR")
+ setExceptionFormat("FULL")
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/Assembler.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/Assembler.java
similarity index 99%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/Assembler.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/Assembler.java
index d1c21313b9..700eb469a9 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/Assembler.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/Assembler.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.book;
+package org.apache.sis.buildtools.book;
import java.util.Arrays;
import java.util.HashMap;
@@ -39,7 +39,7 @@ import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
// Share a convenience method.
-import static org.apache.sis.internal.book.CodeColorizer.toArray;
+import static org.apache.sis.buildtools.book.CodeColorizer.toArray;
/**
@@ -60,7 +60,7 @@ import static org.apache.sis.internal.book.CodeColorizer.toArray;
* See package javadoc for usage example.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 0.7
*/
public final class Assembler {
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/BookException.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/BookException.java
similarity index 95%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/BookException.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/BookException.java
index 7ea2f517c4..71bc5b692c 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/BookException.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/BookException.java
@@ -14,14 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.book;
+package org.apache.sis.buildtools.book;
/**
* Thrown when an error is found in the content of the HTML files used for building the developer guide.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
* @since 0.7
*/
@SuppressWarnings("serial")
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/Characters.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/Characters.java
similarity index 96%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/Characters.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/Characters.java
index 4ed6a15677..0b7028f873 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/Characters.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/Characters.java
@@ -14,14 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.book;
+package org.apache.sis.buildtools.book;
/**
* Utilities related to the handling of characters.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 1.3
*/
public final class Characters {
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/CodeColorizer.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/CodeColorizer.java
similarity index 99%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/CodeColorizer.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/CodeColorizer.java
index 7c50438599..bec70d3930 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/CodeColorizer.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/CodeColorizer.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.book;
+package org.apache.sis.buildtools.book;
import java.util.HashMap;
import java.util.Map;
@@ -36,7 +36,7 @@ import org.w3c.dom.NodeList;
* standard, in GeoAPI or in Apache SIS.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 0.7
*/
public final class CodeColorizer {
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/GEOAPI.lst b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/GEOAPI.lst
similarity index 100%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/GEOAPI.lst
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/GEOAPI.lst
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/OGC.lst b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/OGC.lst
similarity index 100%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/OGC.lst
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/OGC.lst
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/SIS.lst b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/SIS.lst
similarity index 100%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/SIS.lst
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/SIS.lst
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/XML_PREFIX.lst b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/XML_PREFIX.lst
similarity index 100%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/XML_PREFIX.lst
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/XML_PREFIX.lst
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/package-info.java
similarity index 90%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/package-info.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/package-info.java
index 304e62cdc1..a4b5372b94 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/book/package-info.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/book/package-info.java
@@ -19,7 +19,7 @@
* Takes the source HTML files in the {@code sis-site/main/content/developer-guide/} directory
* and assembles them in a single file to be published in the {@code asf-staging/book/} directory.
*
- * <p>The main class in this package is {@link org.apache.sis.internal.book.Assembler}.
+ * <p>The main class in this package is {@link org.apache.sis.buildtools.book.Assembler}.
* Other classes are helper classes that should be ignored. Assuming the following directory layout:</p>
*
* <pre><current directory>
@@ -35,7 +35,7 @@
*
* Then the command can be used as below on Unix systems:
*
- * <pre>java -classpath master/core/sis-build-helper/target/classes org.apache.sis.internal.book.Assembler site</pre>
+ * <pre>java -classpath master/core/sis-build-helper/target/classes org.apache.sis.buildtools.book.Assembler site</pre>
*
* <h2>Future evolution</h2>
* We may replace (at least partially) this tools by some more advanced open-source alternatives.
@@ -49,7 +49,7 @@
* See <a href="http://www.xmlmind.com/tutorials/HTML5Books/HTML5Books.html">HTML5 as an alternative to DITA and DocBook</a>.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 0.7
*/
-package org.apache.sis.internal.book;
+package org.apache.sis.buildtools.book;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Doclet.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Doclet.java
similarity index 59%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Doclet.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Doclet.java
index e7c77981e7..a4e8d52549 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Doclet.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Doclet.java
@@ -14,19 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.doclet;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.List;
import java.nio.file.Path;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.nio.file.Files;
import javax.tools.Diagnostic;
import jdk.javadoc.doclet.Reporter;
import jdk.javadoc.doclet.Doclet.Option;
@@ -39,19 +37,14 @@ import jdk.javadoc.doclet.StandardDoclet;
* The post-javadoc actions are:
*
* <ul>
- * <li>Copy additional resources.</li>
+ * <li>Insert hyphenation.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 0.5
*/
public final class Doclet extends StandardDoclet {
- /**
- * The name of the SIS-specific stylesheet file.
- */
- private static final String STYLESHEET = "sis.css";
-
/**
* The directory where HTML pages will be written.
*/
@@ -112,8 +105,6 @@ public final class Doclet extends StandardDoclet {
final boolean success = super.run(environment);
if (success && outputDirectory != null) try {
final Path output = Path.of(outputDirectory);
- final Path resources = resources(output);
- copyResources(resources, output);
final Rewriter r = new Rewriter();
for (final File file : output.toFile().listFiles()) {
if (file.isDirectory()) { // Do not process files in the root directory, only in sub-directories.
@@ -130,63 +121,6 @@ public final class Doclet extends StandardDoclet {
return success;
}
- /**
- * Returns the {@code src/main/javadoc/} directory relative to the root of the Maven project.
- * This method scans parents of the given directory until we find the root of the Maven project.
- */
- private static Path resources(Path directory) throws FileNotFoundException {
- boolean isModuleDirectory = false;
- while ((directory = directory.getParent()) != null && Files.isDirectory(directory)) {
- if (Files.isRegularFile(directory.resolve("pom.xml"))) {
- isModuleDirectory = true;
- final Path candidate = directory.resolve("src").resolve("main").resolve("javadoc");
- if (Files.isRegularFile(candidate.resolve(STYLESHEET))) {
- return candidate;
- }
- } else if (isModuleDirectory) {
- // If we were in a Maven module and the parent directory does not
- // have a pom.xml file, then we are no longer in the Maven project.
- break;
- }
- }
- throw new FileNotFoundException("Cannot locate \"src/main/javadoc\" from the root of this Maven project.");
- }
-
- /**
- * Creates links to Javadoc resources in the top-level directory (not from "{@code doc-files}" subdirectories).
- * While the Maven documentation said that the "{@code src/main/javadoc}" directory is copied by default, or a
- * directory can be specified with {@code <javadocResourcesDirectory>}, I have been unable to make it work even
- * with absolute paths.
- *
- * @param inputFile the directory containing resources.
- * @param outputDirectory the directory where to copy the resource files.
- * @throws IOException if an error occurred while reading or writing.
- */
- private static void copyResources(final Path inputDirectory, final Path outputDirectory) throws IOException {
- Files.walk(inputDirectory).forEach((input) -> {
- if (Files.isRegularFile(input)) {
- final Path file = input.getFileName();
- switch (file.toString()) {
- case "overview.html":
- case STYLESHEET: break;
- default: {
- final Path output = outputDirectory.resolve(input.getFileName());
- if (!Files.exists(output)) try {
- try {
- Files.createLink(output, input);
- } catch (UnsupportedOperationException e) {
- Files.copy(input, output);
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- break;
- }
- }
- }
- });
- }
-
/**
* Reports an I/O errors.
*/
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Include.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Include.java
similarity index 98%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Include.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Include.java
index 8d635b4535..6d88e28e6f 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Include.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Include.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.doclet;
import java.nio.file.Path;
import java.nio.file.Files;
@@ -36,7 +36,7 @@ import com.sun.source.doctree.DocTree;
* just before the parts to copy in the javadoc.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.4
* @since 0.5
*/
public final class Include extends Taglet {
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Rewriter.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Rewriter.java
similarity index 96%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Rewriter.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Rewriter.java
index a97c4921c4..34eaeeb7f7 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Rewriter.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Rewriter.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.doclet;
import java.io.File;
import java.io.IOException;
@@ -24,7 +24,7 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
-import org.apache.sis.internal.book.Characters;
+import org.apache.sis.buildtools.book.Characters;
/**
@@ -38,13 +38,10 @@ import org.apache.sis.internal.book.Characters;
* insert a zero-width space before the dot or underscore.</li>
* </ul>
*
- * The intent is to avoid large amount of white spaces in Javadoc when a line content long code.
- *
- * <p>Current version does not try to extends the standard doclet because the latter is under revision in JDK 9.
- * We may revisit when JDK 9 become available, if their new standard doclet API is public.</p>
+ * The intent is to avoid large amount of white spaces in Javadoc when a line contains long code.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.4
* @since 0.7
*/
final class Rewriter {
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Taglet.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Taglet.java
similarity index 99%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Taglet.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Taglet.java
index acb188f712..e8f9e87a97 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/Taglet.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/Taglet.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.doclet;
import java.util.Set;
import java.util.EnumSet;
@@ -38,7 +38,7 @@ import jdk.javadoc.doclet.StandardDoclet;
* Taglets are assumed inline by default.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 0.3
*/
abstract class Taglet implements jdk.javadoc.doclet.Taglet {
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/package-info.java
similarity index 94%
copy from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
copy to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/package-info.java
index 5817ad614a..f799e882ab 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/doclet/package-info.java
@@ -21,7 +21,7 @@
* together with doclet performing post-processing after Javadoc generation.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 0.5
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.doclet;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Assembler.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Assembler.java
new file mode 100644
index 0000000000..35f49935fb
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Assembler.java
@@ -0,0 +1,131 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.FileNotFoundException;
+import java.io.UncheckedIOException;
+import java.io.IOException;
+import org.gradle.api.Task;
+import org.gradle.api.Project;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+
+
+/**
+ * Creates a ZIP file containing the Apache SIS binary distribution.
+ * The created file contains:
+ *
+ * <ul>
+ * <li>the content of the <code>optional/src/org.apache.sis.gui/bundle</code> directory;</li>
+ * <li>the content of the <code>endorsed/build/libs</code> and <code>optional/build/libs</code> directories.</li>
+ * </ul>
+ *
+ * Assemblies are created only for module having an {@code bundle} sub-directory.
+ * The module is hard-coded for now (a future version could perform a search if desired).
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @author Quentin Bialota (Geomatys)
+ * @version 1.4
+ * @since 0.4
+ */
+final class Assembler extends ZipWriter.Apache implements FilenameFilter {
+ /**
+ * The module on which to apply this task.
+ */
+ private static final String MODULE = "org.apache.sis.gui";
+
+ /**
+ * The name of the sub-directory inside the ZIP file where the JAR files will be located.
+ * Note that we will not write in the real directory, but only in the directory structure
+ * which is reproduced in the ZIP file.
+ */
+ private static final String LIB_DIRECTORY = "lib/";
+
+ /**
+ * Creates an helper object for creating the assembly.
+ *
+ * @param project the project for which to create an assembly.
+ * @param out output stream of the ZIP file to create.
+ */
+ private Assembler(final Project project, final ZipArchiveOutputStream out) {
+ super(project, out);
+ }
+
+ /**
+ * Creates the distribution file.
+ *
+ * @param task the assembler task (in Gradle 8.2.1, it appears to be an opaque decorator that we cannot cast).
+ */
+ static void create(final Task task) {
+ final Project project = task.getProject();
+ final File sourceDirectory = getBundleSourceDirectory(project, MODULE);
+ if (sourceDirectory.isDirectory()) try {
+ File target = new File(project.getBuildDir(), BUNDLE_DIRECTORY);
+ mkdir(target);
+ String filename = FINALNAME_PREFIX + project.getVersion();
+ target = new File(target, filename + ".zip");
+ filename += '/';
+ try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(target)) {
+ final var c = new Assembler(project, out);
+ c.writeDirectory(sourceDirectory, null, filename);
+ c.copyCompiledJARs(filename + LIB_DIRECTORY);
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * Copies all endorsed and optional JAR files compiled by the Apache SIS project.
+ *
+ * @param libDir name of the "lib" directory inside the ZIP file.
+ * @throws IOException if an error occurred while copying a file.
+ */
+ private void copyCompiledJARs(final String libDir) throws IOException {
+ writeDirectory(fileRelativeToBuild(LIBS_DIRECTORY), null, libDir);
+ final File endorsed = new File(new File(new File(project.getRootDir(), ENDORSED_SUBPROJECT), BUILD_DIRECTORY), LIBS_DIRECTORY);
+ final File[] deps = endorsed.listFiles(this);
+ if (deps == null) {
+ throw new FileNotFoundException("Missing directory: " + endorsed);
+ }
+ for (final File dep : deps) {
+ writeFile(dep, libDir);
+ }
+ }
+
+ /**
+ * Whether to include the given file in the ZIP file.
+ *
+ * @param dir directory of the file.
+ * @param name name of the file.
+ * @return whether to include the file.
+ */
+ @Override
+ public boolean accept(final File dir, final String name) {
+ if (!name.endsWith(".jar")) {
+ return false;
+ }
+ if (name.startsWith("org.apache.sis.cloud.aws")) {
+ return false; // Excluded for avoiding AWS dependencies.
+ }
+ if (name.startsWith("org.apache.sis.profile.japan")) {
+ return false; // Excluded for avoiding UCAR dependencies.
+ }
+ return name.startsWith("org.apache.sis.");
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/BuildHelper.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/BuildHelper.java
new file mode 100644
index 0000000000..c17df41d9e
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/BuildHelper.java
@@ -0,0 +1,180 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.stream.Stream;
+import org.gradle.api.Project;
+import org.gradle.api.Plugin;
+import org.gradle.api.Task;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.tasks.SourceSet;
+import org.gradle.api.tasks.SourceSetContainer;
+import org.gradle.api.tasks.TaskContainer;
+import org.gradle.api.tasks.compile.JavaCompile;
+import org.gradle.api.tasks.javadoc.Javadoc;
+import org.gradle.api.tasks.bundling.Jar;
+import org.gradle.api.tasks.testing.Test;
+
+
+/**
+ * Registration of Gradle tasks for building Apache SIS.
+ *
+ * @author Quentin Bialota (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+public final class BuildHelper implements Plugin<Project> {
+ /**
+ * Roots of package hierarchy inside each Java module. Keys are module names and values are directories.
+ * Populated by {@link #setSourceModulePaths(Project)} and should not be modified after that method call.
+ */
+ private final Map<String,File> mainDirs, testDirs;
+
+ /**
+ * Creates the task.
+ */
+ public BuildHelper() {
+ mainDirs = new LinkedHashMap<>(32);
+ testDirs = new LinkedHashMap<>(32);
+ }
+
+ /**
+ * Sets the collection of sources as one directory per module.
+ * The expected hierarchy is:
+ *
+ * <pre>endorsed
+ * ├─ build
+ * └─ src
+ * ├─ org.apache.sis.metadata
+ * │ ├─ main
+ * │ │ ├─ module-info.java
+ * │ │ └─ org/apache/sis/metadata/…
+ * │ └─ test
+ * │ └─ org/apache/sis/metadata/…
+ * ├─ org.apache.sis.referencing
+ * │ ├─ main
+ * │ │ ├─ module-info.java
+ * │ │ └─ org/apache/sis/referencing/…
+ * │ └─ test
+ * │ └─ org/apache/sis/referencing/…
+ * └─ etc.</pre>
+ *
+ * @param project the project for which to set the sources.
+ */
+ private void setSourceModulePaths(final Project project) {
+ final File[] modules = project.file(Conventions.SOURCE_DIRECTORY).listFiles(File::isDirectory);
+ if (modules == null) {
+ throw new IllegalArgumentException("Not a directory: " + Conventions.SOURCE_DIRECTORY);
+ }
+ for (final File module : modules) {
+ File p;
+ final String name = module.getName();
+ if ((p = new File(module, Conventions.MAIN_DIRECTORY)).isDirectory()) mainDirs.put(name, p);
+ if ((p = new File(module, Conventions.TEST_DIRECTORY)).isDirectory()) testDirs.put(name, p);
+ }
+ final SourceSetContainer sources = project.getExtensions().getByType(SourceSetContainer.class);
+ sources.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getJava().setSrcDirs(mainDirs.values());
+ sources.getByName(SourceSet.TEST_SOURCE_SET_NAME).getJava().setSrcDirs(testDirs.values());
+ }
+
+ /**
+ * Returns the name of all main modules.
+ *
+ * @return module names. This collection is not cloned, <strong>do not modify</strong>.
+ */
+ final Set<String> getModuleNames() {
+ return mainDirs.keySet();
+ }
+
+ /**
+ * Returns the roots of package hierarchy inside each Java module.
+ * There is one directory per module.
+ *
+ * @param tests {@code true} for returning test directories, or {@code false} for main directories.
+ * @return the directories. This collection is not cloned, <strong>do not modify</strong>.
+ */
+ final Collection<File> getSourceDirectories(final boolean tests) {
+ return (tests ? testDirs : mainDirs).values();
+ }
+
+ /**
+ * {@return the main modules to patch with test classes}.
+ */
+ final Stream<Map.Entry<String,File>> getPatchedDirectories() {
+ return testDirs.entrySet().stream().filter((e) -> mainDirs.containsKey(e.getKey()));
+ }
+
+ /**
+ * Registers tasks potentially used for Apache SIS build.
+ * This method is invoked during the configuration phase of Gradle.
+ *
+ * @param project the project where to register the task.
+ */
+ @Override
+ public void apply(final Project project) {
+ project.getPluginManager().apply(JavaPlugin.class); // This plugin depends on the Java plugin.
+ setSourceModulePaths(project);
+ /*
+ * Configurations to be applied before a task is executed.
+ * Tasks are compilation, tests and javadoc.
+ */
+ final TaskContainer tasks = project.getTasks();
+ tasks.withType(JavaCompile.class).forEach((task) -> { // We usually have only one instance.
+ final var ext = new ModularCompilation(task.getProject());
+ task.doFirst((t) -> {
+ ModularCompilation.modularize(BuildHelper.this, (JavaCompile) t);
+ ext.compileOrcopyResources(BuildHelper.this, t);
+ });
+ task.doLast((t) -> {
+ ext.flushResources();
+ JavaMaker.execute(project);
+ });
+ });
+ tasks.withType(Test.class).forEach((task) -> {
+ task.doFirst((t) -> {
+ ModularTest.modularizeAndExportToJunit((Test) t);
+ });
+ });
+ tasks.withType(Javadoc.class).forEach((task) -> {
+ task.doFirst((t) -> {
+ new ModularJavadoc((Javadoc) t).modularize(BuildHelper.this);
+ });
+ });
+ tasks.withType(Jar.class).forEach((task) -> {
+ task.setActions(List.of((t) -> { // Replace the default action by our own.
+ ModularJAR.execute(BuildHelper.this, (Jar) t);
+ }));
+ });
+ final Task assemble = tasks.getByPath("assemble");
+ assemble.dependsOn("jar");
+ assemble.doLast((t) -> { // tasks.withType(Assembler.class) does not work.
+ Assembler.create(t);
+ UnoPkg.create(t);
+ });
+ /*
+ * Configuration of the publication to Maven repository.
+ */
+ new ModularPublishing(project).configure();
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Conventions.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Conventions.java
new file mode 100644
index 0000000000..bbfd9c1090
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Conventions.java
@@ -0,0 +1,192 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import org.gradle.api.Project;
+
+
+/**
+ * An helper class working on hard-coded file and directory names.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 0.4
+ */
+abstract class Conventions {
+ /**
+ * The directory where source code is located, relative to sub-project directory.
+ * This is a Gradle convention (inherited from Maven).
+ */
+ static final String SOURCE_DIRECTORY = "src";
+
+ /**
+ * The directory where compilation result are saved, relative to sub-project directory.
+ * This is a Gradle convention.
+ */
+ static final String BUILD_DIRECTORY = "build";
+
+ /**
+ * The root directory of the build tools (custom Gradle plugin).
+ * This is a Gradle convention.
+ */
+ static final String BUILD_TOOLS_DIRECTORY = "buildSrc";
+
+ /**
+ * The sub-directory inside "{@value #SOURCE_DIRECTORY}" for main Java source code.
+ * This is a Gradle convention (inherited from Maven), except that the module name
+ * is inserted between "{@value #SOURCE_DIRECTORY}" and "{@value}".
+ */
+ static final String MAIN_DIRECTORY = "main";
+
+ /**
+ * The sub-directory inside "{@value #SOURCE_DIRECTORY}" for test Java source code.
+ * This is a Gradle convention (inherited from Maven), except that the module name
+ * is inserted between "{@value #SOURCE_DIRECTORY}" and "{@value}".
+ */
+ static final String TEST_DIRECTORY = "test";
+
+ /**
+ * The sub-directory inside "{@value #BUILD_DIRECTORY}" for compiled main class files.
+ * This is a Gradle convention.
+ */
+ static final String MAIN_CLASSES_DIRECTORY = "classes/java/" + MAIN_DIRECTORY;
+
+ /**
+ * The sub-directory inside "{@value #BUILD_DIRECTORY}" for compiled test class files.
+ * This is a Gradle convention.
+ */
+ static final String TEST_CLASSES_DIRECTORY = "classes/java/" + TEST_DIRECTORY;
+
+ /**
+ * The sub-directory inside "{@value #BUILD_DIRECTORY}" for the JAR files.
+ * This is a Gradle convention.
+ */
+ static final String LIBS_DIRECTORY = "libs";
+
+ /**
+ * The sub-directory inside "{@value #BUILD_DIRECTORY}" for the ZIP or OXT files (assemblies).
+ * Also the sibling directory of "{@value #MAIN_DIRECTORY}" and "{@value #TEST_DIRECTORY}"
+ * for the sources to copy in the ZIP file. This is an Apache SIS convention.
+ */
+ static final String BUNDLE_DIRECTORY = "bundle";
+
+ /**
+ * Name of the sub-project containing the JAR files that are included in Apache SIS releases.
+ */
+ static final String ENDORSED_SUBPROJECT = "endorsed";
+
+ /**
+ * The prefix of the final filename of bundles created by assembly tasks.
+ */
+ static final String FINALNAME_PREFIX = "apache-sis-";
+
+ /**
+ * The encoding to use everywhere (source code, generated HTML pages, …).
+ */
+ static final String ENCODING = "UTF-8";
+
+ /**
+ * The sub-project being compiled.
+ */
+ protected final Project project;
+
+ /**
+ * Creates the helper class.
+ *
+ * @param project the sub-project being compiled.
+ */
+ Conventions(final Project project) {
+ this.project = project;
+ }
+
+ /**
+ * Returns the directory of the source files for the bundle in the given module.
+ *
+ * @param project the sub-project being compiled.
+ * @param module the module for which to get the bundle source directory.
+ * @return the bundle source directory of the specified module.
+ */
+ static File getBundleSourceDirectory(final Project project, final String module) {
+ return new File(new File(new File(project.getProjectDir(), SOURCE_DIRECTORY), module), BUNDLE_DIRECTORY);
+ }
+
+ /**
+ * Returns a sub-directory of the directory where the compilation result is saved.
+ *
+ * @param path name of sub-directories, in order.
+ * @return path of the given name relative to build directory.
+ */
+ final File fileRelativeToBuild(final String... path) {
+ File file = project.getBuildDir();
+ for (final String p : path) {
+ file = new File(file, p);
+ }
+ return file;
+ }
+
+ /**
+ * Returns the absolute path of a file relative to the root of the whole project.
+ *
+ * @param filename name of the filename to return.
+ * @return path of the given name relative to project root.
+ */
+ final File fileRelativeToRoot(final String filename) {
+ return new File(project.getRootDir(), filename);
+ }
+
+ /**
+ * Creates a subdirectory if it does not already exists.
+ * The parent directory must exist, this method intentionally does not create it.
+ *
+ * @param dir the subdirectory to create.
+ * @throws IOException if the subdirectory cannot be created.
+ */
+ static void mkdir(final File dir) throws IOException {
+ if (!(dir.exists() || dir.mkdir())) {
+ throw new IOException("Failed to create \"" + dir.getName() + "\" directory.");
+ }
+ }
+
+ /**
+ * Potentially creates a pseudo-copy (using hard-link) of the given file.
+ * If the file system does not support hard links, then a real copy is done.
+ * If the target file already exists and is not older than the source, then this method does nothing.
+ *
+ * @param source the file to copy.
+ * @param target the destination file.
+ * @throws IOException if an error occurred while creating the hard-link or copying the file.
+ */
+ static void linkOrCopy(final Path source, final Path target) throws IOException {
+ if (Files.isRegularFile(target)) {
+ if (Files.getLastModifiedTime(source).compareTo(Files.getLastModifiedTime(target)) <= 0) {
+ return; // File is assumed up-to-date.
+ }
+ Files.delete(target);
+ }
+ Files.createDirectories(target.getParent());
+ try {
+ Files.createLink(target, source);
+ } catch (UnsupportedOperationException e) {
+ Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Dependency.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Dependency.java
new file mode 100644
index 0000000000..f28b60752f
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/Dependency.java
@@ -0,0 +1,271 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes;
+import java.util.zip.ZipEntry;
+import java.io.File;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.module.ModuleDescriptor;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ConfigurationContainer;
+import org.gradle.api.artifacts.ModuleVersionIdentifier;
+import org.gradle.api.artifacts.ResolvedArtifact;
+import org.gradle.api.publish.maven.MavenPublication;
+
+
+/**
+ * Information about a dependency.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+public final class Dependency {
+ /**
+ * Workaround for what seems to be a Gradle bug. {@link MavenPublication#getArtifactId()}
+ * does not contain the value specified in the {@code build.gradle.kts} file.
+ */
+ private static final Map<String,String> MODULE_NAMES = Map.ofEntries(
+ Map.entry("util", "core:sis-utility"),
+ Map.entry("metadata", "core:sis-metadata"),
+ Map.entry("referencing", "core:sis-referencing"),
+ Map.entry("referencing.gazetteer", "core:sis-referencing-by-identifiers"),
+ Map.entry("feature", "core:sis-feature"),
+ Map.entry("portrayal", "core:sis-portrayal"),
+ Map.entry("storage", "storage:sis-storage"),
+ Map.entry("storage.sql", "storage:sis-sqlstore"),
+ Map.entry("storage.xml", "storage:sis-xmlstore"),
+ Map.entry("storage.netcdf", "storage:sis-netcdf"),
+ Map.entry("storage.geotiff", "storage:sis-geotiff"),
+ Map.entry("storage.earthobservation", "storage:sis-earth-observation"),
+ Map.entry("cloud.aws", "cloud:sis-cloud-aws"),
+ Map.entry("profile.france", "profiles:sis-french-profile"),
+ Map.entry("profile.japan", "profiles:sis-japan-profile"),
+ Map.entry("console", "application:sis-console"),
+ Map.entry("openoffice", "application:sis-openoffice"),
+ Map.entry("gui", "application:sis-javafx"), // Optional.
+ Map.entry("cql", "core:sis-cql"), // Incubator.
+ Map.entry("storage.shapefile", "core:sis-shapefile"),
+ Map.entry("webapp", "application:sis-webapp")
+ );
+
+ /**
+ * The attribute for automatic module name in {@code META-INF/MANIFEST.MF} files.
+ */
+ private static final Attributes.Name AUTO_MODULE_NAME = new Attributes.Name("Automatic-Module-Name");
+
+ /**
+ * Path to the dependency JAR file.
+ */
+ public final File file;
+
+ /**
+ * Maven coordinates of the dependency.
+ *
+ * @see #writeAsMaven(StringBuilder, String)
+ */
+ private final String group, name, version;
+
+ /**
+ * Name of the JPMS module of the dependency, or {@code null} if unspecified.
+ */
+ public final String module;
+
+ /**
+ * Creates information about a publication to be handled as a dependency for other publication.
+ *
+ * @param pub the publication.
+ */
+ Dependency(final MavenPublication pub) {
+ final String patch = MODULE_NAMES.get(pub.getName());
+ if (patch == null) {
+ throw new SISBuildException("Unknown publication: " + pub.getName());
+ }
+ final int s = patch.indexOf(':');
+ group = "org.apache.sis." + patch.substring(0, s);
+ name = patch.substring(s+1);
+ version = pub.getVersion();
+ module = "org.apache.sis." + pub.getName();
+ file = null;
+ }
+
+ /**
+ * Creates a new dependency.
+ *
+ * @param artifact Gradle artifact to be converted to Maven one.
+ * @throws IOException if an I/O error occurred while reading the module description.
+ */
+ private Dependency(final ResolvedArtifact artifact) throws IOException {
+ final ModuleVersionIdentifier id = artifact.getModuleVersion().getId();
+ group = id.getGroup();
+ name = id.getName();
+ version = id.getVersion();
+ file = artifact.getFile();
+ module = getModuleName();
+ }
+
+ /**
+ * Returns the name of the JPMS module of the dependency.
+ *
+ * @return JPMS module name, or {@code null} if unspecified.
+ * @throws IOException if an I/O error occurred while reading the module description.
+ */
+ private String getModuleName() throws IOException {
+ try (JarFile zip = new JarFile(file, false, JarFile.OPEN_READ, JarFile.runtimeVersion())) {
+ final ZipEntry entry = zip.getEntry("module-info.class");
+ if (entry != null) {
+ try (InputStream in = zip.getInputStream(entry)) {
+ final ModuleDescriptor desc = ModuleDescriptor.read(in);
+ return desc.name();
+ }
+ }
+ final Manifest mf = zip.getManifest();
+ if (mf != null) {
+ final Object value = mf.getMainAttributes().get(AUTO_MODULE_NAME);
+ if (value instanceof String) {
+ return (String) value;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the dependencies of the given project.
+ *
+ * @param project the project for which to find dependencies.
+ * @return dependencies of the specified project.
+ * @throws IOException if an I/O error occurred while reading the module description.
+ */
+ public static List<Dependency> find(final Project project) throws IOException {
+ final var dependencies = new ArrayList<Dependency>();
+ final ConfigurationContainer configurations = project.getConfigurations();
+ for (final String type : new String[] {"implementation","compileOnly","runtimeOnly"}) {
+ final Configuration config = configurations.getByName(type).copyRecursive();
+ config.setCanBeResolved(true);
+ for (final ResolvedArtifact artifact : config.getResolvedConfiguration().getResolvedArtifacts()) {
+ dependencies.add(new Dependency(artifact));
+ }
+ }
+ return dependencies;
+ }
+
+ /**
+ * Returns the JPMS dependencies of the given project.
+ * Keys are JPMS module names are values are dependency descriptions.
+ *
+ * @param project the project for which to find dependencies.
+ * @param subProjects the sub-projects, which also need to be included as dependencies across JPMS modules.
+ * @return dependencies of the specified project.
+ * @throws IOException if an I/O error occurred while reading the module description.
+ */
+ public static Map<String,Dependency> jpms(final Project project, final Collection<Dependency> subProjects) throws IOException {
+ final var dependencies = new HashMap<String,Dependency>();
+ final var warnings = new HashSet<String>();
+ final var sources = find(project);
+ sources.addAll(subProjects);
+ sources.forEach((dep) -> {
+ if (dep.module != null) {
+ final Dependency old = dependencies.putIfAbsent(dep.module, dep);
+ if (old != null && !old.equals(dep)) {
+ final StringBuilder message = new StringBuilder(600).append("Inconsistent dependencies:\n");
+ old.writeAsMaven(message, 1, false);
+ dep.writeAsMaven(message, 1, false);
+ final String s = message.toString();
+ if (warnings.add(s)) {
+ project.getLogger().warn(s);
+ }
+ }
+ }
+ });
+ return dependencies;
+ }
+
+ /**
+ * Writes the Maven {@code <dependency>} element in the given buffer.
+ *
+ * @param pom where to write.
+ * @param level the initial indentation level.
+ * @param optional whether the dependency is optional.
+ */
+ public final void writeAsMaven(final StringBuilder pom, final int level, final boolean optional) {
+ final String indent = " ";
+ final String margin = indent.repeat(level);
+ pom.append(margin).append("<dependency>\n");
+ if (module != null) {
+ pom.append(margin).append(indent).append("<!-- module ").append(module).append(" -->\n");
+ }
+ pom.append(margin).append(indent).append("<groupId>") .append(group) .append("</groupId>\n");
+ pom.append(margin).append(indent).append("<artifactId>").append(name) .append("</artifactId>\n");
+ pom.append(margin).append(indent).append("<version>") .append(version).append("</version>\n");
+ if (optional) {
+ pom.append(margin).append(indent).append("<optional>true</optional>\n");
+ }
+ pom.append(margin).append("</dependency>\n");
+ }
+
+ /**
+ * Returns a string representation for debugging purposes.
+ *
+ * @return string representation of the {@code <dependency>} element.
+ */
+ @Override
+ public String toString() {
+ final var pom = new StringBuilder(100);
+ writeAsMaven(pom, 0, false);
+ return pom.toString();
+ }
+
+ /**
+ * Compares this dependency with the given object for equality.
+ *
+ * @param obj the object to compare with this dependency.
+ * @return whether the two object are equal.
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Dependency)) {
+ return false;
+ }
+ final var other = (Dependency) obj;
+ return Objects.equals(file, other.file) &&
+ Objects.equals(group, other.group) &&
+ Objects.equals(name, other.name) &&
+ Objects.equals(version, other.version) &&
+ Objects.equals(module, other.module);
+ }
+
+ /**
+ * {@return a hash code for this dependency}.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(file, group, name, version, module);
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/JavaMaker.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/JavaMaker.java
new file mode 100644
index 0000000000..0935c1c1c8
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/JavaMaker.java
@@ -0,0 +1,118 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.FileVisitResult;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import org.gradle.api.Project;
+
+
+/**
+ * Compiles Java interfaces from OpenOffice IDL files.
+ *
+ * <p>In an ideal world, this task would execute {@code idlc} on the {@code *.idl} files,
+ * then {@code regmerge} on the generated {@code *.urd} files,
+ * then {@code javamaker} on the generated {@code *.rdb} files.
+ * However, since the above mentioned tools are native and would require a manual installation
+ * on every developer machine, current version just copies a pre-compiled class file.
+ * This copy must occurs after the compilation phase in order to overwrite the files
+ * generated by {@code javac}.
+ *
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @author Quentin Bialota (Geomatys)
+ * @version 1.4
+ * @since 0.8
+ */
+final class JavaMaker extends SimpleFileVisitor<Path> {
+ /**
+ * The root of source and target directories. Files below {@code source} will be copied
+ * with identical path (relative to {@code source}) under {@code target} directory.
+ */
+ private final Path source, target;
+
+ /**
+ * Number of files copied.
+ */
+ private int count;
+
+ /**
+ * Creates a new copier.
+ *
+ * @param unopkgDirectory base directory of the class to "compile".
+ * @param outputDirectory directory where the output class files are located.
+ */
+ private JavaMaker(final File unopkgDirectory, final File outputDirectory) {
+ source = unopkgDirectory.toPath();
+ target = outputDirectory.toPath();
+ }
+
+ /**
+ * Copies {@code *.class} files from source directory to output directory.
+ * The output directory shall already exist. It should be the case if all
+ * sources files have been compiled before this method is invoked.
+ *
+ * @param project the sub-project.
+ */
+ static void execute(final Project project) {
+ final File moduleDirectory = Conventions.getBundleSourceDirectory(project, UnoPkg.MODULE);
+ if (moduleDirectory.isDirectory()) try {
+ File outputDirectory; // Path to "./build/classes/java/main/org.apache.sis.openoffice/"
+ outputDirectory = project.getBuildDir();
+ outputDirectory = new File(outputDirectory, Conventions.MAIN_CLASSES_DIRECTORY);
+ outputDirectory = new File(outputDirectory, UnoPkg.MODULE);
+ final var c = new JavaMaker(moduleDirectory, outputDirectory);
+ Files.walkFileTree(c.source, c);
+ project.getLogger().debug("[sis-unopkg] Copied " + c.count + " pre-compiled class files.");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * Determines whether the given directory should be visited.
+ * This method skips hidden directories.
+ */
+ @Override
+ public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) {
+ return dir.getFileName().toString().startsWith(".") ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
+ }
+
+ /**
+ * Invoked for a file in a directory. The destination shall already exists.
+ * It should be the case if this task is invoked after Java compilation.
+ */
+ @Override
+ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+ final String filename = file.getFileName().toString();
+ if (filename.endsWith(".class") || filename.endsWith(".CLASS")) {
+ final Path dst = target.resolve(source.relativize(file)).normalize();
+ if (!dst.startsWith(target)) {
+ throw new SISBuildException("Unexpected target path: " + dst);
+ }
+ Files.copy(file, dst, StandardCopyOption.REPLACE_EXISTING);
+ count++;
+ }
+ return FileVisitResult.CONTINUE;
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularCompilation.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularCompilation.java
new file mode 100644
index 0000000000..2042d8ab2d
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularCompilation.java
@@ -0,0 +1,283 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Map;
+import java.util.Set;
+import java.util.List;
+import java.util.Queue;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.tasks.compile.JavaCompile;
+import org.gradle.api.tasks.compile.CompileOptions;
+import org.apache.sis.buildtools.resources.IndexedResourceCompiler;
+
+
+/**
+ * Extension to Gradle {@link JavaCompile} task.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class ModularCompilation extends Conventions {
+ /**
+ * Resources to copy after the compilation is finished.
+ * We copy resources after compilation because otherwise, they seem to be deleted randomly.
+ */
+ private final Queue<Map<File,Object>> pendingResources;
+
+ /**
+ * Creates a new helper instance for Apache SIS compilation.
+ *
+ * @param project the sub-project being compiled.
+ */
+ ModularCompilation(final Project project) {
+ super(project);
+ pendingResources = new ConcurrentLinkedQueue<>();
+ }
+
+ /**
+ * Returns {@code true} if the specified compilation task is for compiling the tests.
+ *
+ * @param task the task to check.
+ * @return whether the given task is compiling tests.
+ */
+ private static boolean isCompileTest(final Task task) {
+ return task.getName().contains("Test");
+ }
+
+ /**
+ * Returns the value of the {@code --module-source-path} option.
+ * That option is critical for Module Source Hierarchy.
+ * The directory structure that we expect for Apache SIS project is:
+ *
+ * <pre><gradle sub-project>
+ * ├─ build
+ * └─ src
+ * ├─ org.apache.sis.metadata
+ * │ ├─ main
+ * │ │ ├─ module-info.java
+ * │ │ └─ org/apache/sis/metadata/…
+ * │ └─ test
+ * │ └─ org/apache/sis/metadata/…
+ * ├─ org.apache.sis.referencing
+ * │ ├─ main
+ * │ │ ├─ module-info.java
+ * │ │ └─ org/apache/sis/referencing/…
+ * │ └─ test
+ * │ └─ org/apache/sis/referencing/…
+ * └─ etc.</pre>
+ *
+ * @param project the Gradle project being compiled.
+ * @param isCompileTest whether to specify the main sources or the test sources.
+ * @return value of the {@code --module-source-path} option.
+ */
+ static String getModuleSourcePath(final Project project, final boolean isCompileTest) {
+ File path;
+ path = project.file(SOURCE_DIRECTORY);
+ path = new File(path, "*"); // Handled in a special way by `--module-source-path`.
+ path = new File(path, isCompileTest ? TEST_DIRECTORY : MAIN_DIRECTORY);
+ return path.getPath();
+ }
+
+ /**
+ * Invoked when the {@code compileJava} task is executed.
+ *
+ * @param context the extension which is invoking this task.
+ * @param task the {@link JavaCompile} task to configure.
+ */
+ static void modularize(final BuildHelper context, final JavaCompile task) {
+ final boolean isCompileTest = isCompileTest(task);
+ final Project project = task.getProject();
+ final CompileOptions options = task.getOptions();
+ options.setEncoding(ENCODING); // The character encoding to be used when reading source files.
+ options.setDeprecation(true); // Whether to log details of usage of deprecated members or classes.
+
+ final List<String> args = options.getCompilerArgs();
+ args.add("--module-source-path");
+ args.add(getModuleSourcePath(project, isCompileTest));
+ /*
+ * WORKAROUND FOR GRADLE 8.1 PROBLEM:
+ * The class-path and the module-path should be mutually exclusive set of path elements.
+ * In theory Gradle should decide itself which dependencies to put on the module path,
+ * since this property is true by default:
+ *
+ * modularity.inferModulePath
+ *
+ * However it doesn't seem to work with Gradle 8.1.1 because Gradle does not recognize
+ * the Module Source Hierarchy. Instead of separating ourselves the path elements, an
+ * easier workaround is to put everything on the module path. Javac seems to manage.
+ *
+ * This code replace the following code in `build.gradle.kts` file:
+ *
+ * options.compilerArgs.add("--module-path=${classpath.asPath}")
+ * setClasspath(files()) // For avoiding duplication with the --module-path option.
+ */
+ args.add("--module-path");
+ args.add(task.getClasspath().getAsPath());
+ task.setClasspath(project.files()); // For avoiding duplication with the --module-path option.
+ /*
+ * Declare explicitly all modules to take in account.
+ * It avoid problems when recompiling only a few classes.
+ */
+ args.add("--add-modules");
+ args.add(String.join(",", context.getModuleNames()));
+ /*
+ * If compiling tests, declare that we are patching modules.
+ */
+ if (isCompileTest) {
+ context.getPatchedDirectories().forEach((entry) -> {
+ args.add("--patch-module");
+ args.add(entry.getKey() + '=' + entry.getValue());
+ });
+ }
+ }
+
+ /*
+ * ┌───────────────────────────────────────────────────────────────────┐
+ * │ Code below this point are for handling international resources. │
+ * │ This is a non-standard feature specific to Apache SIS project. │
+ * │ For modularisation of arbitrary projects, ignore the remaining. │
+ * └───────────────────────────────────────────────────────────────────┘
+ */
+
+ /**
+ * Modules to exclude. This is a temporary workaround for a SIS module
+ * not yet well integrated with other SIS modules.
+ */
+ private static final Set<String> EXCLUDE_MODULES = Set.of("org.apache.sis.storage.shapefile");
+
+ /**
+ * File extensions of resources to not copy.
+ *
+ * @todo Reduce this list to IDL and Markdown by converting HTML and text files.
+ */
+ private static final Set<String> EXCLUDE_RESOURCES = Set.of(
+ "tmp", "bak", "log", // Same extensions than in `.gitignore`.
+ "idl", // Source code for OpenOffice add-in.
+ "md", // Notes in Markdown format.
+ "html", // Richer alternative to Markdown.
+ "txt"); // Simpler alternative to Markdown.
+
+ /**
+ * Compiles the international resources that are found in all modules.
+ * If the {@code *.properties} file is not a resource to compile,
+ * then the file will be linked or copied instead of being compiled.
+ *
+ * @param context the extension which is invoking this task.
+ * @param task the compilation task which will be executed after this method.
+ */
+ final void compileOrcopyResources(final BuildHelper context, final Task task) {
+ final boolean isCompileTest = isCompileTest(task);
+ pendingResources.clear();
+ /*
+ * There is probably a cleaner way to get the source directories than above hack.
+ * For now we use hard-coded paths for main and test classes.
+ */
+ int errors = 0;
+ @SuppressWarnings("LocalVariableHidesMemberVariable") // Should be the same value.
+ final Project project = task.getProject();
+ final File outputDir = new File(project.getBuildDir(),
+ isCompileTest ? TEST_CLASSES_DIRECTORY
+ : MAIN_CLASSES_DIRECTORY);
+ final var results = new HashMap<File,Object>();
+ for (final File sourceDirectory : context.getSourceDirectories(isCompileTest)) {
+ final var buildDirectory = new File(outputDir, sourceDirectory.getParentFile().getName());
+ final var compiler = new IndexedResourceCompiler(sourceDirectory, buildDirectory, results) {
+ @Override protected void info (String message) {project.getLogger().info(message);}
+ @Override protected void warning(String message) {project.getLogger().warn(message);}
+ @Override protected void otherResource(File source, File target) throws IOException {
+ if (include(source)) {
+ results.put(target, source);
+ }
+ }
+ @Override protected boolean acceptCompilableResource(File source) {
+ while (source != null) {
+ if (EXCLUDE_MODULES.contains(source.getName())) return false;
+ source = source.getParentFile();
+ }
+ return true;
+ }
+ };
+ try {
+ errors += compiler.onJavaSourceDirectory();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ if (errors != 0) {
+ throw new SISBuildException(String.valueOf(errors) + " errors in resources bundles.");
+ }
+ pendingResources.add(results);
+ }
+
+ /**
+ * Writes now the resources which were found by {@code #compileOrcopyResources(…)}.
+ * This operation should be done last, after compilation, because otherwise Gradle
+ * may delete the resources as part of a clean operation.
+ */
+ final void flushResources() {
+ Map<File,Object> resources;
+ while ((resources = pendingResources.poll()) != null) {
+ for (final Map.Entry<File,Object> entry : resources.entrySet()) try {
+ final File target = entry.getKey();
+ final Object data = entry.getValue();
+ if (data instanceof byte[]) {
+ Files.write(target.toPath(), (byte[]) data);
+ } else {
+ final File source = (File) data;
+ linkOrCopy(source.toPath(), target.toPath());
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ }
+
+ /**
+ * Whether to copy the specified resource.
+ *
+ * @param resource the resource file to potentially copy.
+ * @return {@code true} for copying or {@code false} for ignoring.
+ */
+ private static boolean include(File resource) {
+ String ext = resource.getName();
+ ext = ext.substring(ext.lastIndexOf('.') + 1);
+ if (EXCLUDE_RESOURCES.contains(ext)) {
+ if (ext.equals("txt")) {
+ /*
+ * Special case: include text file for tests.
+ * TODO: remove that special case. See EXCLUDE_RESOURCES comment.
+ */
+ while ((resource = resource.getParentFile()) != null) {
+ if (resource.getName().equals(TEST_DIRECTORY)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularJAR.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularJAR.java
new file mode 100644
index 0000000000..99802d3caa
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularJAR.java
@@ -0,0 +1,176 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import org.gradle.api.Project;
+import org.gradle.api.tasks.bundling.Jar;
+
+
+/**
+ * Extension to Gradle {@link Jar} task. This extension generates one JAR file per module.
+ * If the {@code build.gradle} file specifies a {@code Main-Class} attribute, the latter
+ * can apply to only one module, which is identified in this class as the "main module".
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class ModularJAR extends ZipWriter.JDK {
+ /**
+ * Version of the GeoAPI dependency.
+ * This value depends on the Apache SIS branch being compiled.
+ *
+ * @todo should be specified in a property.
+ */
+ private static final String GEOAPI_VERSION = "4.0-SNAPSHOT";
+
+ /**
+ * Attributes where values are class names. Those attributes can be assigned
+ * to only one module, because the same class cannot appear in two modules.
+ */
+ private static final String[] CLASSNAME_ATTRIBUTES = {
+ Attributes.Name.MAIN_CLASS.toString(),
+ "RegistrationClassName"
+ };
+
+ /**
+ * Creates a helper instance.
+ *
+ * @param project the sub-project being compiled.
+ * @param out output stream of the JAR file to create.
+ */
+ private ModularJAR(final Project project, final JarOutputStream out) {
+ super(project, out);
+ }
+
+ /**
+ * Lists all compiled modules in the current Gradle sub-project.
+ *
+ * @return all modules in current Gradle sub-project.
+ */
+ private static File[] listModules(final Project project) {
+ return new File(project.getBuildDir(), MAIN_CLASSES_DIRECTORY).listFiles(File::isDirectory);
+ }
+
+ /**
+ * Invoked when the {@code jar} task is executed.
+ *
+ * @param context the extension which is invoking this task.
+ * @param task the {@link Javadoc} task to configure.
+ */
+ static void execute(final BuildHelper context, final Jar task) {
+ final Project project = task.getProject();
+ final Map<String,?> attributes = task.getManifest().getAttributes();
+ final var filteredAttributes = new HashMap<String,String>();
+ final var classnameAttributes = new HashMap<String,String>();
+ for (final String classname : CLASSNAME_ATTRIBUTES) {
+ final Object value = attributes.get(classname);
+ if (value instanceof String) {
+ final String path = ((String) value).replace('.', File.separatorChar) + ".class";
+ classnameAttributes.put(classname, path);
+ }
+ }
+ final File target = new File(project.getBuildDir(), LIBS_DIRECTORY);
+ for (final File module : listModules(project)) {
+ for (final Iterator<Map.Entry<String,String>> it = classnameAttributes.entrySet().iterator(); it.hasNext();) {
+ final Map.Entry<String,String> entry = it.next();
+ if (new File(module, entry.getValue()).isFile()) {
+ final String key = entry.getKey();
+ filteredAttributes.put(key, (String) attributes.get(key));
+ it.remove();
+ }
+ }
+ try {
+ write(project, module, new File(target, module.getName() + ".jar"), filteredAttributes);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ filteredAttributes.clear();
+ }
+ for (final Map.Entry<String,String> entry : classnameAttributes.entrySet()) {
+ task.getLogger().warn(entry.getKey() + " not found: " + entry.getValue());
+ }
+ try {
+ linkDependencies(project, target);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (UnsupportedOperationException e) {
+ project.getLogger().warn("Links to dependencies not provided because hard-links are not supported on this platform.", e);
+ }
+ }
+
+ /**
+ * Writes a JAR file for a single module.
+ *
+ * @param project the project for which to write a JAR file.
+ * @param source root directory of compiled class files for the module to package.
+ * @param target the file to write.
+ * @param specific attributes specific to this module.
+ * @throws IOException if an error occurred while reading the source or writing the JAR file.
+ */
+ private static void write(final Project project, final File source, final File target,
+ final Map<String,String> specific) throws IOException
+ {
+ final var mf = new Manifest();
+ final Attributes attributes = mf.getMainAttributes();
+ attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ attributes.put(Attributes.Name.SPECIFICATION_TITLE, "GeoAPI");
+ attributes.put(Attributes.Name.SPECIFICATION_VENDOR, "Open Geospatial Consortium");
+ attributes.put(Attributes.Name.SPECIFICATION_VERSION, GEOAPI_VERSION);
+ attributes.put(Attributes.Name.IMPLEMENTATION_TITLE, "Apache Spatial Information System (SIS)");
+ attributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, "The Apache Software Foundation");
+ attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, project.getVersion().toString());
+ for (final Map.Entry<String,String> entry : specific.entrySet()) {
+ attributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
+ }
+ try (JarOutputStream out = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(target)), mf)) {
+ new ModularJAR(project, out).writeDirectory(source, null, "");
+ }
+ }
+
+ /**
+ * Adds hard-links to all dependencies.
+ *
+ * @param project the project for which to add dependencies.
+ * @param target where to write dependencies.
+ * @throws IOException if an I/O error occurred while reading the module description.
+ * @throws UnsupportedOperationException if links are not supported on this platform.
+ */
+ private static void linkDependencies(final Project project, final File target) throws IOException {
+ for (final Dependency dep : Dependency.find(project)) {
+ if (dep.module != null) {
+ final Path f = new File(target, dep.module + ".jar").toPath();
+ if (Files.notExists(f)) {
+ Files.createLink(f, dep.file.toPath());
+ }
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularJavadoc.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularJavadoc.java
new file mode 100644
index 0000000000..c9371d71a8
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularJavadoc.java
@@ -0,0 +1,200 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.util.List;
+import java.util.Arrays;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import org.gradle.api.tasks.javadoc.Javadoc;
+import org.gradle.external.javadoc.StandardJavadocDocletOptions;
+
+
+/**
+ * Extension to Gradle {@link Javadoc} task.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class ModularJavadoc extends Conventions {
+ /**
+ * The javadoc task.
+ */
+ private final Javadoc task;
+
+ /**
+ * Creates a helper instance.
+ *
+ * @param task the {@link Javadoc} task to configure.
+ */
+ ModularJavadoc(final Javadoc task) {
+ super(task.getProject());
+ this.task = task;
+ }
+
+ /**
+ * Invoked when the {@code javadoc} task is executed.
+ *
+ * @param context the extension which is invoking this task.
+ */
+ final void modularize(final BuildHelper context) {
+ final var options = (StandardJavadocDocletOptions) task.getOptions();
+ options.encoding (ENCODING); // Encoding of Java source file.
+ options.docEncoding (ENCODING); // Encoding of the generated HTML files.
+ options.charSet (ENCODING); // Encoding to declare in the HTML META tag.
+ options.locale ("en"); // Locale for navigation bar, help file contents, etc.
+ options.breakIterator(true); // Better boundary detection when determining the end of the first sentence.
+ options.noQualifiers ("all"); // Omit qualifying package name before class names in output.
+ options.author (false); // Excludes the authors text in the generated docs.
+ options.version (false); // Excludes the version text in the generated docs.
+ options.keyWords (true); // Adds HTML meta keyword tags to the generated files.
+ options.noTimestamp.setValue(false); // Allow timestamp in generated HTML pages.
+ options.tags("category:X:\"Category:\"",
+ "todo:a:\"TODO:\"");
+ options.links(
+ "https://docs.oracle.com/en/java/javase/11/docs/api",
+ "http://www.geoapi.org/snapshot/javadoc",
+ "http://unitsofmeasurement.github.io/unit-api/site/apidocs");
+ /*
+ * Taglet defined in this `buildSrc` sub-project.
+ */
+ final File buildSrc = fileRelativeToRoot(BUILD_TOOLS_DIRECTORY);
+ final File bClasses = new File(new File(buildSrc, BUILD_DIRECTORY), MAIN_CLASSES_DIRECTORY);
+ options.docletpath(bClasses);
+ options.tagletPath(bClasses);
+ options.setDoclet("org.apache.sis.buildtools.doclet.Doclet");
+ options.taglets("org.apache.sis.buildtools.doclet.Include");
+ /*
+ * Put only one '-' in given options because Gradles add a '-' itself.
+ */
+ options.addFileOption ("-add-stylesheet", new File(new File(project.getRootDir(), "parent"), "sis.css"));
+ options.addStringOption("-module-source-path", ModularCompilation.getModuleSourcePath(project, false));
+ options.addStringOption("-add-modules", String.join(",", context.getModuleNames()));
+ /*
+ * Same workaround than for the compiler task.
+ */
+ options.modulePath(options.getClasspath());
+ options.setClasspath(List.of());
+ /*
+ * In current version, Javadoc generation fails because some dependencies are incorrectly
+ * put in class-path option instead of module-path option. The above workaround does not work
+ * because some class-path entries are added by Gradle only after this configuration phase.
+ * The reason for this behavior is unknown and we found no way to resolve this problem yet.
+ * For now, we create a patched `javadoc.options` file that user can execute from the command line.
+ * The new file is created using the file from the previous run (not this run).
+ */
+ try {
+ patchOptionsFile(task.getOptionsFile().toPath());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ project.getLogger().info(String.format(
+ "The `javadoc` task is known to not work in this configuration (tested with Gradle 8.2.1).%n" +
+ "Resolving this issue may require changes in the way that Gradle handles the module path.%n" +
+ "In the meantime, Javadoc can be generated with the following command line:%n" +
+ "%n" +
+ " javadoc @%s%sbuild%<stmp%<sjavadoc%<sjavadoc.options.patched%n" +
+ "%n" +
+ "It may be necessary to run the `javadoc` task again for generating the patched file.%n" +
+ "%n", project.getProjectDir().getName(), File.separator));
+ }
+
+ /**
+ * Creates a copy of the given file with patched {@code --module-path} option.
+ * User can execute with the following command line:
+ *
+ * {@snippet lang="shell" :
+ * javadoc @build/tmp/javadoc/javadoc.options.patched
+ * }
+ *
+ * @param file the file to patch.
+ * @throws IOException if an error occurred while reading or writing the file.
+ */
+ private static void patchOptionsFile(final Path file) throws IOException {
+ if (!Files.isRegularFile(file)) {
+ return;
+ }
+ final String searchFor = "-classpath "; // The option to search.
+ final String moveTo = "--module-path "; // The option where to move the classpath.
+
+ final List<String> lines = Files.readAllLines(file);
+ final String[] rewrite = new String[lines.size()];
+ final StringBuilder modulePath = new StringBuilder(16*1024).append(moveTo);
+ int lineOfModulePath = -1;
+ int currentLineIndex = 0;
+ boolean foundClassPath = false;
+ boolean foundSomePath = false;
+ for (final String line : lines) {
+ if (line.startsWith(searchFor)) {
+ appendWithoutQuotes(line, searchFor.length(), foundSomePath, modulePath);
+ foundClassPath = true;
+ foundSomePath = true;
+ } else {
+ if (line.startsWith(moveTo)) {
+ appendWithoutQuotes(line, moveTo.length(), foundSomePath, modulePath);
+ foundSomePath = true;
+ if (lineOfModulePath >= 0) continue;
+ lineOfModulePath = currentLineIndex;
+ }
+ rewrite[currentLineIndex++] = line;
+ }
+ }
+ if (foundClassPath && lineOfModulePath >= 0) {
+ rewrite[lineOfModulePath] = modulePath.toString();
+ Files.write(file.getParent().resolve(file.getFileName().toString() + ".patched"),
+ Arrays.asList(rewrite).subList(0, currentLineIndex));
+ }
+ }
+
+ /**
+ * Appends the given value to the string builder without the quotes around the values.
+ * This is used for merging many paths (which may be quoted) in a single unquoted path.
+ *
+ * @param value the value to unquote and append to the string builder.
+ * @param start first character to examine in the given value.
+ * @param separator whether to prepend a path separator.
+ * @param appendTo where to append the unquoted value.
+ */
+ private static void appendWithoutQuotes(final String value, int start, final boolean separator, final StringBuilder appendTo) {
+ int end = value.length();
+ while (end > 0) {
+ final int c = value.codePointBefore(end);
+ if (!Character.isWhitespace(c)) break;
+ end -= Character.charCount(c);
+ }
+ while (start < end) {
+ final int c = value.codePointAt(start);
+ if (!Character.isWhitespace(c)) break;
+ start += Character.charCount(c);
+ }
+ final int last = end - 1;
+ if (start < end && value.charAt(start) == '\'' && value.charAt(last) == '\'') {
+ start++;
+ end = last;
+ }
+ if (start < end) {
+ if (separator) {
+ appendTo.append(File.pathSeparatorChar);
+ }
+ appendTo.append(value, start, end);
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularPublishing.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularPublishing.java
new file mode 100644
index 0000000000..6be4bdcdd6
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularPublishing.java
@@ -0,0 +1,79 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.util.Map;
+import java.util.HashMap;
+import org.gradle.api.Project;
+import org.gradle.api.publish.maven.MavenPom;
+import org.gradle.api.publish.maven.MavenPublication;
+import org.gradle.api.publish.PublishingExtension;
+import org.apache.sis.buildtools.maven.Generator;
+
+
+/**
+ * Extension to Gradle Maven publishing tasks.
+ * The publication name must be the JPMS module name without the {@code org.apache.sis} prefix.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class ModularPublishing {
+ /**
+ * The project to publish.
+ */
+ private final Project project;
+
+ /**
+ * The publications to handle as dependencies for other publications.
+ * Keys are JPMS module names.
+ *
+ * @todo It should not be a static field. But I didn't found another way
+ * to get the compilation results of other sub-projects.
+ */
+ private static final Map<String,Dependency> publications = new HashMap<>();
+
+ /**
+ * Creates a helper instance.
+ *
+ * @param project the project to publish.
+ */
+ ModularPublishing(final Project project) {
+ this.project = project;
+ }
+
+ /**
+ * Gets the list of publications and register code for rewriting the {@code pom.xml} files.
+ */
+ final void configure() {
+ final PublishingExtension ext = project.getExtensions().findByType(PublishingExtension.class);
+ if (ext != null) {
+ ext.getPublications().configureEach((pub) -> {
+ if (pub instanceof MavenPublication) {
+ final var mp = (MavenPublication) pub;
+ final var dep = new Dependency(mp);
+ final MavenPom pom = mp.getPom();
+ synchronized (publications) {
+ publications.put(dep.module, dep);
+ pom.withXml((xml) -> Generator.rewrite(project, publications.values(), mp, xml));
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularTest.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularTest.java
new file mode 100644
index 0000000000..18f646c9f8
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ModularTest.java
@@ -0,0 +1,260 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.module.ModuleDescriptor;
+import org.gradle.api.tasks.testing.Test;
+import org.gradle.api.Project;
+
+
+/**
+ * Extension to Gradle {@link Test} task.
+ * Contrarily to other {@code Modular*} classes in this package,
+ * this extension replaces completely the Gradle's default task.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class ModularTest extends Conventions {
+ /**
+ * Sentinel value for packages that are exported to all.
+ */
+ private static final String EXPORTED = "#exported";
+
+ /**
+ * Root directories of compiled test class for each module. Keys are module names and values are directories.
+ * The name of the directory, as returned by {@link File#getName()}, should be the module name (i.e. the key).
+ */
+ private final Map<String,File> moduleDirectories;
+
+ /**
+ * All packages together with whether the package is visible to all.
+ * If the package is exported, then the associated value is {@link #EXPORTED}.
+ * Otherwise the associated value is the name of the module that contains the package.
+ * This information is needed for building an {@code --add-exports} JVM option.
+ */
+ private final Map<String,String> packageVisibility;
+
+ /**
+ * Packages containing at least one test classes.
+ */
+ private final List<String> testedPackages;
+
+ /**
+ * A buffer for building the package name as we iterate over the directory structure.
+ */
+ private final StringBuilder packageName;
+
+ /**
+ * The main and test directory with leading and trailing platform-specific name separator.
+ */
+ private final String mainDirectory, testDirectory;
+
+ /**
+ * Creates an object for holding temporary data.
+ *
+ * @param project the sub-project being tested.
+ */
+ private ModularTest(final Project project) {
+ super(project);
+ moduleDirectories = new LinkedHashMap<>(32);
+ packageVisibility = new LinkedHashMap<>(128);
+ testedPackages = new ArrayList<>(64);
+ packageName = new StringBuilder(64);
+ mainDirectory = File.separator + MAIN_DIRECTORY + File.separator;
+ testDirectory = File.separator + TEST_DIRECTORY + File.separator;
+ }
+
+ /**
+ * Replaces a path from test directory to a path to main directory.
+ * This method assumes that the "test" name to replace is the last path element.
+ * It should not be used if the path can contain arbitrary directories after "test".
+ *
+ * @param file a path to a file in the test directory.
+ * @return path to the same file, but in the main directory.
+ */
+ private File testToMain(File file) {
+ final StringBuilder path = new StringBuilder(file.getPath());
+ final int s = path.lastIndexOf(testDirectory);
+ if (s >= 0) {
+ file = new File(path.replace(s, s + testDirectory.length(), mainDirectory).toString());
+ }
+ return file;
+ }
+
+ /**
+ * Given a directory of modules, builds the list of {@code --add-exports} JVM arguments.
+ *
+ * @param directory output directory of test class. Sub-directories shall be module names.
+ * @return directories of test classes.
+ */
+ private void scanAllModules(final File directory) {
+ final File[] moduleCandidates = directory.listFiles(File::isDirectory);
+ if (moduleCandidates != null) try {
+ for (final File moduleRoot : moduleCandidates) {
+ final String moduleName = readModuleInfo(moduleRoot);
+ if (moduleName != null) {
+ final File existing = moduleDirectories.put(moduleName, moduleRoot);
+ if (existing != null) {
+ throw new SISBuildException("Duplicated module name: " + moduleName +
+ "\n Found in: " + existing +
+ "\n Also in: " + moduleRoot);
+ }
+ for (final File headPackage : moduleRoot.listFiles(File::isDirectory)) {
+ collectTestedPackages(moduleName, headPackage);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * Reads the export declarations of the given {@code module-info.class} file.
+ * Packages that <em>may</em> need to be specified in an {@code --add-exports}
+ * JVM argument are added to the {@link #packageVisibility} map.
+ *
+ * @param moduleRoot the module directory or compiled test class.
+ * @return module name, or {@code null} if {@code "module-info.class"} has not been found.
+ * @throws IOException if an error occurred while reading the file.
+ */
+ private String readModuleInfo(final File moduleRoot) throws IOException {
+ final File file = testToMain(new File(moduleRoot, "module-info.class"));
+ if (!file.isFile()) {
+ return null;
+ }
+ try (FileInputStream in = new FileInputStream(file)) {
+ final ModuleDescriptor module = ModuleDescriptor.read(in);
+ final String name = module.name();
+ module.packages().forEach((p) -> packageVisibility.put(p, name));
+ module.exports().forEach((descriptor) -> {
+ if (!descriptor.isQualified()) {
+ packageVisibility.put(descriptor.source(), EXPORTED);
+ }
+ });
+ return name;
+ }
+ }
+
+ /**
+ * Collects a list of tested packages, regardless is exported or not.
+ * The package names are added to the {@link #testedPackages} list.
+ *
+ * @param moduleName name of the module being scanned.
+ * @param file directory or file to inspect.
+ * @return whether the given file was presumably a test class.
+ */
+ private boolean collectTestedPackages(final String moduleName, final File file) {
+ final File[] content = file.listFiles();
+ if (content == null) {
+ return file.getName().endsWith(".class");
+ }
+ final int oldLength = packageName.length();
+ packageName.append(file.getName()).append('.');
+ boolean hasTests = false;
+ for (final File c : content) {
+ hasTests |= collectTestedPackages(moduleName, c);
+ }
+ if (hasTests) {
+ final String name = packageName.substring(0, packageName.length() - 1);
+ packageVisibility.putIfAbsent(name, moduleName);
+ testedPackages.add(name);
+ }
+ packageName.setLength(oldLength);
+ return false;
+ }
+
+ /**
+ * Returns the {@code --add-module}, {@code --add-reads} and {@code --add-exports} statements
+ * to specify as JVM arguments. The export statements may be numerous, so this method tries to
+ * add only those that are necessary.
+ *
+ * @return the list of arguments to add to the JVM.
+ */
+ private List<String> moduleOptions() {
+ final var args = new ArrayList<String>(2 + 4*moduleDirectories.size() + 2*testedPackages.size());
+ args.add("--add-modules");
+ args.add(String.join(",", moduleDirectories.keySet()));
+ /*
+ * --patch-module ${module}=${buildDir}/classes/java/test/${it}
+ * --add-reads ${module}=junit,ALL-UNNAMED
+ */
+ moduleDirectories.entrySet().forEach((entry) -> {
+ final String module = entry.getKey();
+ args.add("--patch-module"); args.add(module + '=' + entry.getValue());
+ args.add("--add-reads"); args.add(module + "=junit,ALL-UNNAMED");
+ });
+ /*
+ * --add-exports ${module}/${package}=junit
+ */
+ for (final String name : testedPackages) {
+ String module = packageVisibility.remove(name);
+ if (module != null && !EXPORTED.equals(module)) {
+ args.add("--add-exports");
+ args.add(module + '/' + name + "=junit");
+ }
+ }
+ return args;
+ }
+
+ /**
+ * Adds modularization arguments and exports to JUnit all non-exported packages.
+ *
+ * @param tasks the task where to add JVM arguments.
+ */
+ static void modularizeAndExportToJunit(final Test task) {
+ /*
+ * Create a module path which excludes the compiled test classes, because those
+ * classes will be specified using another JVM option (namely `--patch-module`).
+ */
+ var mainpath = task.getClasspath().filter((path) -> !path.getName().equals("test"));
+ task.jvmArgs(List.of("--module-path", mainpath.getAsPath()));
+ /*
+ * JPMS options.
+ */
+ final var t = new ModularTest(task.getProject());
+ task.getTestClassesDirs().forEach((dir) -> {
+ t.scanAllModules(dir);
+ });
+ task.jvmArgs(t.moduleOptions());
+ task.setTestClassesDirs(task.getProject().files(t.moduleDirectories.values().toArray()));
+ /*
+ * Custom options specific to Apache SIS.
+ * By default, the working directory is the sub-project root.
+ * We move the working directory to a sub-directory of `build`.
+ * It will be the location of the `derby.log` file for instance.
+ */
+ task.useJUnitPlatform();
+ task.include("**/*Test.class");
+ task.setScanForTestClasses(false);
+
+ final File workingDir = t.fileRelativeToBuild("test-output");
+ workingDir.mkdirs();
+ task.setWorkingDir(workingDir);
+ task.getEnvironment().putIfAbsent("SIS_DATA", workingDir.getPath());
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/ResourceCompilerException.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/SISBuildException.java
similarity index 58%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/ResourceCompilerException.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/SISBuildException.java
index 68c99b1045..26aa89b37c 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/ResourceCompilerException.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/SISBuildException.java
@@ -14,35 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.util.resources;
-
-import org.apache.maven.plugin.MojoExecutionException;
+package org.apache.sis.buildtools.gradle;
/**
- * Thrown when the {@link ResourceCompilerMojo} exit abnormally.
+ * Thrown when an error occurred with the Apache SIS build helper
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.3
- * @since 0.3
+ * @version 1.4
+ * @since 1.4
*/
@SuppressWarnings("serial")
-final class ResourceCompilerException extends MojoExecutionException {
+public class SISBuildException extends RuntimeException {
/**
- * Creates an exception with the given detail message.
+ * Creates a new exception with the given message.
*
- * @param message the detail message.
+ * @param message description of the error.
*/
- ResourceCompilerException(final String message) {
+ public SISBuildException(final String message) {
super(message);
}
-
- /**
- * Creates an exception with the given cause.
- *
- * @param cause the cause of this exception.
- */
- ResourceCompilerException(final Throwable cause) {
- super(cause.getLocalizedMessage(), cause);
- }
}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/UnoPkg.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/UnoPkg.java
new file mode 100644
index 0000000000..dafe0e3115
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/UnoPkg.java
@@ -0,0 +1,215 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.OutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.gradle.api.Task;
+import org.gradle.api.Project;
+
+
+/**
+ * Creates an {@code .oxt} package for <a href="http://www.openoffice.org">OpenOffice.org</a> addins.
+ * Those files are basically ZIP files with {@code META-INF/manifest.xml} and {@code .rdb} entries.
+ * The principle is similar to Java JAR files.
+ *
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @author Quentin Bialota (Geomatys)
+ * @version 1.4
+ * @since 0.8
+ */
+final class UnoPkg extends ZipWriter.JDK {
+ /**
+ * The module on which to apply this task.
+ */
+ static final String MODULE = "org.apache.sis.openoffice";
+
+ /**
+ * Module to decompress. We inflate the {@value} module because Derby is much slower
+ * when using an embedded database in a compressed ZIP file compared to flat storage.
+ * Since the JAR files are distributed in a ZIP file anyway, inflating that file has
+ * little impact on the final ZIP file size.
+ */
+ private static final String TO_INFLATE = "org.apache.sis.referencing.database";
+
+ /**
+ * Hard-coded set of libraries to include.
+ *
+ * @see #accept(File, String)
+ */
+ private static final Set<String> LIBRARIES = Set.of(
+ "java.measure",
+ "org.opengis.geoapi",
+ "org.opengis.geoapi.pending",
+ "org.apache.sis.util",
+ "org.apache.sis.metadata",
+ "org.apache.sis.referencing",
+ "org.apache.sis.feature",
+ "org.apache.sis.storage",
+ "org.apache.sis.openoffice",
+ "jakarta.activation",
+ "jakarta.xml.bind",
+ TO_INFLATE);
+
+ /**
+ * The string to replace by the final name.
+ */
+ private static final String FILTERED_NAME = "${project.build.finalName}";
+
+ /**
+ * The string to replace by the version number.
+ */
+ private static final String FILTERED_VERSION = "${project.version}";
+
+ /**
+ * Creates an helper object for creating the assembly.
+ *
+ * @param project the project for which to create an assembly.
+ * @param out output stream of the ZIP file to create.
+ */
+ private UnoPkg(final Project project, final ZipOutputStream out) {
+ super(project, out);
+ }
+
+ /**
+ * Generates the {@code .oxt} file from a selected amount if {@code .jar} files.
+ *
+ * @param task the assembler task (in Gradle 8.2.1, it appears to be an opaque decorator that we cannot cast).
+ */
+ static void create(final Task task) {
+ final Project project = task.getProject();
+ final File sourceDirectory = getBundleSourceDirectory(project, MODULE);
+ final File libsDirectory = new File(project.getBuildDir(), LIBS_DIRECTORY);
+ if (sourceDirectory.isDirectory() && libsDirectory.isDirectory()) try {
+ File target = new File(project.getBuildDir(), BUNDLE_DIRECTORY);
+ mkdir(target);
+ target = new File(target, FINALNAME_PREFIX + project.getVersion() + ".oxt");
+ try (ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(target)))) {
+ final var c = new UnoPkg(project, out);
+ c.writeDirectory(sourceDirectory, UnoPkg::filterSource, "");
+ c.writeDirectory(libsDirectory, UnoPkg::filterDependency, "");
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
+ * Tests if a specified file from the source directory should be included in the bundle.
+ *
+ * @param file the file to test.
+ * @return whether the file should be included.
+ */
+ private static boolean filterSource(final File file) {
+ return !file.getName().equals("org");
+ }
+
+ /**
+ * Tests if a specified file from the libraries directory should be included in the bundle.
+ *
+ * @param file the file to test.
+ * @return whether the file should be included.
+ */
+ private static boolean filterDependency(final File file) {
+ final String name = file.getName();
+ return name.endsWith(".jar") && LIBRARIES.contains(name.substring(0, name.length() - 4));
+ }
+
+ /**
+ * Writes the content of the specified file to the output stream of a ZIP entry.
+ */
+ @Override
+ protected void writeEntryContent(final File source, final OutputStream out) throws IOException {
+ final String filename = source.getName();
+ if (filename.endsWith(".xml")) {
+ copyFiltered(source, out);
+ } else if (filename.equals(TO_INFLATE + ".jar")) {
+ copyInflated(source, out);
+ } else {
+ super.writeEntryContent(source, out);
+ }
+ }
+
+ /**
+ * Copies the content of the specified ASCII file to the output stream.
+ * Occurrences of {@value #FILTERED_NAME} and {@value #FILTERED_VERSION}
+ * are replaced by actual values.
+ *
+ * @param file the regular file to copy inside the ZIP file.
+ * @param name the ZIP entry name.
+ */
+ private void copyFiltered(final File source, final OutputStream out) throws IOException {
+ final String version = project.getVersion().toString();
+ final String finalName = FINALNAME_PREFIX + version;
+ final var writer = new OutputStreamWriter(out, ENCODING);
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(source), ENCODING))) {
+ String line;
+ while ((line = in.readLine()) != null) {
+ line = line.replace(FILTERED_NAME, finalName);
+ line = line.replace(FILTERED_VERSION, version);
+ writer.write(line);
+ writer.write('\n');
+ }
+ }
+ writer.flush();
+ }
+
+ /**
+ * Copies a JAR file in the ZIP file, but without compression.
+ * This is recommended for files in the {@code SIS_DATA} directory.
+ *
+ * @param file the JAR file to copy.
+ */
+ private void copyInflated(final File file, final OutputStream out) throws IOException {
+ final ZipOutputStream jar = new ZipOutputStream(out);
+ try (ZipFile in = new ZipFile(file)) {
+ final Enumeration<? extends ZipEntry> entries = in.entries();
+ while (entries.hasMoreElements()) {
+ final ZipEntry entry = entries.nextElement();
+ final String name = entry.getName();
+ final ZipEntry outEntry = new ZipEntry(name);
+ if (name.startsWith("SIS_DATA")) {
+ final long size = entry.getSize();
+ outEntry.setMethod(ZipOutputStream.STORED);
+ outEntry.setSize(size);
+ outEntry.setCompressedSize(size);
+ outEntry.setCrc(entry.getCrc());
+ }
+ try (InputStream inStream = in.getInputStream(entry)) {
+ jar.putNextEntry(outEntry);
+ inStream.transferTo(jar);
+ jar.closeEntry();
+ }
+ }
+ }
+ jar.finish();
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ZipWriter.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ZipWriter.java
new file mode 100644
index 0000000000..082ff25623
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/ZipWriter.java
@@ -0,0 +1,286 @@
+/*
+ * 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.sis.buildtools.gradle;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.gradle.api.Project;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+
+
+/**
+ * Base class of tasks producing a ZIP or JAR file.
+ * One of {@link JDK} or {@link Apache} inner classed should be used.
+ * The method to invoke is {@link #writeDirectory(File, FileFilter, String)}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+abstract class ZipWriter extends Conventions {
+ /**
+ * Name of the entry in process of being written in the output stream.
+ * Path separator is standardized to {@code '/'} (not platform-specific).
+ */
+ private final StringBuilder path;
+
+ /**
+ * Buffer for transferring file contents to ZIP or JAR entries.
+ * Allocated only once and reused for all entries.
+ */
+ private final byte[] buffer;
+
+ /**
+ * Creates a helper instance.
+ *
+ * @param project the sub-project being compiled.
+ * @param out output stream of the ZIP or JAR file to create.
+ */
+ private ZipWriter(final Project project) {
+ super(project);
+ this.path = new StringBuilder();
+ this.buffer = new byte[8192];
+ }
+
+ /**
+ * {@return the ZIP output stream}. This is either the JDK {@link ZipArchiveEntry}
+ * or the Apache Common Compress {@link ZipArchiveOutputStream} implementation.
+ */
+ abstract OutputStream out();
+
+ /**
+ * Begins writing a new ZIP file entry.
+ *
+ * @param source file which will be copied in the entry.
+ * @param name name of the entry to add.
+ * @throws IOException if an error occurred.
+ */
+ abstract void putNextEntry(File source, String name) throws IOException;
+
+ /**
+ * Closes the current entry.
+ *
+ * @throws IOException if an error occurred.
+ */
+ abstract void closeEntry() throws IOException;
+
+ /**
+ * An output stream backed by the JDK implementation.
+ */
+ static class JDK extends ZipWriter {
+ /**
+ * Output stream of the ZIP or JAR file to create.
+ */
+ protected final ZipOutputStream out;
+
+ /**
+ * Creates a new ZIP output stream.
+ *
+ * @param project the sub-project being compiled.
+ * @param out the stream where to write ZIP entries.
+ */
+ JDK(final Project project, final ZipOutputStream out) {
+ super(project);
+ this.out = out;
+ out.setLevel(Deflater.BEST_COMPRESSION);
+ }
+
+ /** {@return the ZIP output stream}. */
+ @Override final OutputStream out() {
+ return out;
+ }
+
+ /** Begins writing a new ZIP file entry. */
+ @Override final void putNextEntry(final File source, final String name) throws IOException {
+ final var entry = new ZipEntry(name);
+ completeEntry(source, entry);
+ out.putNextEntry(entry);
+ }
+
+ /** Closes the current entry. */
+ @Override final void closeEntry() throws IOException {
+ out.closeEntry();
+ }
+ }
+
+ /**
+ * An output stream backed by the Apache implementation.
+ * This implementation is used when we need the capability to set Unix execution flag on some files.
+ */
+ static class Apache extends ZipWriter {
+ /**
+ * Output stream of the ZIP or JAR file to create.
+ */
+ protected final ZipArchiveOutputStream out;
+
+ /**
+ * Creates a new ZIP output stream.
+ *
+ * @param project the sub-project being compiled.
+ * @param out the stream where to write ZIP entries.
+ */
+ Apache(final Project project, final ZipArchiveOutputStream out) {
+ super(project);
+ this.out = out;
+ out.setLevel(Deflater.BEST_COMPRESSION);
+ }
+
+ /** {@return the ZIP output stream}. */
+ @Override final OutputStream out() {
+ return out;
+ }
+
+ /** Begins writing a new ZIP file entry. */
+ @Override final void putNextEntry(final File source, final String name) throws IOException {
+ final var entry = new ZipArchiveEntry(name);
+ completeEntry(source, entry);
+ if (source.canExecute()) {
+ entry.setUnixMode(0744);
+ }
+ out.putArchiveEntry(entry);
+ }
+
+ /** Closes the current entry. */
+ @Override final void closeEntry() throws IOException {
+ out.closeArchiveEntry();
+ }
+ }
+
+ /**
+ * Configures the entry for the file to be copied.
+ *
+ * @param source the file which will be written in the entry.
+ * @param entry the entry to configure.
+ */
+ private static void completeEntry(final File source, final ZipEntry entry) {
+ if (entry.isDirectory()) {
+ entry.setMethod(ZipEntry.STORED);
+ entry.setCompressedSize(0);
+ entry.setSize(0);
+ entry.setCrc(0);
+ } else {
+ entry.setMethod(ZipEntry.DEFLATED);
+ entry.setSize(source.length());
+ }
+ entry.setTime(source.lastModified());
+ }
+
+ /**
+ * List all files in the given directory excluding hidden files.
+ * This method sorts the elements with regular files before directories,
+ * then case-sensitive alphabetical order with upper-cases before lower cases.
+ *
+ * @param directory the directory from which to get the files.
+ * @return files in the directory, or {@code null} if the argument is not a directory.
+ */
+ private static File[] listIgnoreHidden(final File directory) {
+ final File[] files = directory.listFiles((f) -> !f.isHidden());
+ if (files != null) {
+ Arrays.sort(files, (f1, f2) -> {
+ int c = f1.isDirectory() ? 1 : 0;
+ if (f2.isDirectory()) c--;
+ if (c == 0) {
+ c = f1.getName().compareTo(f2.getName());
+ }
+ return c;
+ });
+ }
+ return files;
+ }
+
+ /**
+ * Copies to the ZIP file the content of the given directory.
+ *
+ * @param source the directory to zip.
+ * @param filter filter to apply on the files, or {@code null} if none.
+ * @param target where to write the files in the ZIP file. Empty for writing in the root.
+ * @throws IOException if an error occurred while reading the source or writing the ZIP file.
+ */
+ protected final void writeDirectory(final File source, final FileFilter filter, final String target) throws IOException {
+ path.setLength(0);
+ path.append(target);
+ for (final File file : listIgnoreHidden(source)) {
+ if (filter == null || filter.accept(file)) {
+ appendRecursively(file);
+ }
+ }
+ }
+
+ /**
+ * Writes a single file or directory.
+ *
+ * @param source the file or directory to add.
+ * @param target name of the entry in the ZIP file.
+ * @throws IOException if an error occurred while reading the source or writing the ZIP file.
+ */
+ protected final void writeFile(final File source, final String target) throws IOException {
+ path.setLength(0);
+ path.append(target);
+ appendRecursively(source);
+ }
+
+ /**
+ * Appends the content of the given file. If the file is a directory,
+ * then the content of that directory will be appended recursively.
+ *
+ * @param source the file to append.
+ * @throws IOException if an error occurred while reading the source or writing the ZIP file.
+ */
+ private void appendRecursively(final File source) throws IOException {
+ final File[] files = listIgnoreHidden(source);
+ final int parentNameLength = path.length();
+ path.append(source.getName());
+ if (files != null) path.append('/'); // Fixed by ZIP standard, not platform-dependent.
+ putNextEntry(source, path.toString());
+ if (files == null) {
+ writeEntryContent(source, out());
+ }
+ closeEntry();
+ if (files != null) {
+ for (final File sub : files) {
+ appendRecursively(sub);
+ }
+ }
+ path.setLength(parentNameLength);
+ }
+
+ /**
+ * Writes the content of the specified file to the output stream of a ZIP entry.
+ * Subclasses can override for filtering the content.
+ *
+ * @param source the file to open for providing an entry content.
+ * @param out where to write the (potentially filtered) file content.
+ * @throws IOException if an error occurred while reading the source or writing the ZIP file.
+ */
+ protected void writeEntryContent(final File source, final OutputStream out) throws IOException {
+ try (final InputStream in = new FileInputStream(source)) {
+ int n;
+ while ((n = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, n);
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/package-info.java
similarity index 63%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/package-info.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/package-info.java
index 964f4cf62f..c03c0080af 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/package-info.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/gradle/package-info.java
@@ -17,17 +17,14 @@
/**
- * Maven plugins (others than {@link org.apache.sis.util.resources.ResourceCompilerMojo}) used
- * for building Apache SIS.
- *
- * <ul>
- * <li>{@link org.apache.sis.internal.maven.JarCollector} collects all JAR files and their dependencies
- * in a single {@code target/binaries} directory, using hard links instead of copying the files.</li>
- * <li>{@link org.apache.sis.internal.maven.Assembler} builds the Apache SIS distribution file.</li>
- * </ul>
+ * Gradle plugin for building Apache SIS.
+ * This plugin assumes that the code is organized according Module Source Hierarchy.
+ * Some conventions on directory names and a few module names are hard-coded in the
+ * {@link org.apache.sis.buildtools.gradle.Conventions} class.
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @author Quentin Bialota (Geomatys)
+ * @version 1.4
* @since 0.3
*/
-package org.apache.sis.internal.maven;
+package org.apache.sis.buildtools.gradle;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/Element.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/Element.java
new file mode 100644
index 0000000000..92cf9c5214
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/Element.java
@@ -0,0 +1,73 @@
+/*
+ * 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.sis.buildtools.maven;
+
+
+/**
+ * An XML element to read from the original {@code pom.xml} file content.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+enum Element {
+ /**
+ * The group of the artifact to publish.
+ */
+ GROUB_ID("<groupId>", "${sis.groupId}"),
+
+ /**
+ * The identifier of the artifact to publish.
+ */
+ ARTIFACT_ID("<artifactId>", "${sis.artifactId}"),
+
+ /**
+ * The version of the artifact to publish.
+ */
+ VERSION("<version>", "${sis.version}"),
+
+ /**
+ * Human-readable name of the artifact to publish.
+ */
+ NAME("<name>", "${sis.name}"),
+
+ /**
+ * Human-readable description of the artifact to publish.
+ */
+ DESCRIPTION("<description>", "${sis.description}");
+
+ /**
+ * The XML tag where to read or write the element value.
+ */
+ final String tag;
+
+ /**
+ * The placeholder for the value to write.
+ */
+ final String property;
+
+ /**
+ * Creates a new enumeration value.
+ *
+ * @param tag XML tag where to read or write the element value.
+ * @param property placeholder for the value to write.
+ */
+ private Element(final String tag, final String property) {
+ this.tag = tag;
+ this.property = property;
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/Generator.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/Generator.java
new file mode 100644
index 0000000000..a7a328f92b
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/Generator.java
@@ -0,0 +1,239 @@
+/*
+ * 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.sis.buildtools.maven;
+
+import java.util.Map;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.Collection;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import java.lang.module.ModuleDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import org.gradle.api.Project;
+import org.gradle.api.XmlProvider;
+import org.gradle.api.publish.maven.MavenArtifact;
+import org.gradle.api.publish.maven.MavenArtifactSet;
+import org.gradle.api.publish.maven.MavenPublication;
+import org.apache.sis.buildtools.gradle.Dependency;
+import org.apache.sis.buildtools.gradle.SISBuildException;
+
+
+/**
+ * Generates the {@code pom.xml} file.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+public final class Generator {
+ /**
+ * The template to use for generating the {@code pom.xml} file.
+ * Properties such as {@code ${sis.foo}} are enumerated in {@link Element}.
+ */
+ private static final String TEMPLATE = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <!--
+ 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.
+ -->
+
+ <project xmlns = "http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.sis</groupId>
+ <artifactId>parent</artifactId>
+ <version>${sis.version}</version>
+ </parent>
+
+ <groupId>${sis.groupId}</groupId>
+ <artifactId>${sis.artifactId}</artifactId>
+ <name>${sis.name}</name>
+ <description>
+ ${sis.description}
+ </description>
+
+ <dependencies>
+ </dependencies>
+ </project>
+ """;
+
+ /**
+ * The {@code pom.xml} file content.
+ */
+ private final StringBuilder pom;
+
+ /**
+ * Information (such as Maven coordinates) of the artifact to publish.
+ * Those information are parsed from the original {@code pom.xml} file.
+ */
+ private final EnumMap<Element,String> info;
+
+ /**
+ * The required dependencies as JPML module name.
+ * Values are whether the dependency is optional.
+ */
+ private final Map<String,Boolean> requires;
+
+ /**
+ * Creates a new generator which will read and rewrite in the given buffer.
+ *
+ * @param pom the original {@code pom.xml} content to read, and the destination where the rewrite it.
+ */
+ private Generator(final StringBuilder pom) {
+ this.pom = pom;
+ info = new EnumMap<>(Element.class);
+ requires = new LinkedHashMap<>();
+ }
+
+ /**
+ * Parses the original {@code pom.xml} content.
+ * This content is minimalist, with no copyright header, no parent
+ * and no dependencies (in the way that we specified the artifact).
+ * The main things that we need to collect are Maven coordinates.
+ */
+ private void parseOriginalPOM() {
+ for (final Element element : Element.values()) {
+ int start = pom.indexOf(element.tag);
+ if (start >= 0) {
+ start += element.tag.length();
+ if (pom.indexOf(element.tag, start) >= 0) {
+ throw new SISBuildException("Duplicated tag: " + element.tag);
+ }
+ final int end = pom.indexOf("<", start);
+ if (end < 0) {
+ throw new SISBuildException("Unclosed tag: " + element.tag);
+ }
+ info.put(element, pom.substring(start, end).trim());
+ }
+ }
+ }
+
+ /**
+ * Overwrites the {@code pom.xml} with a new content derived from the template.
+ * The Maven coordinates will be written using the information found by {@link #parseOriginalPOM()}.
+ * Dependencies are not included, they must be added by {@link #addDependencies(Dependency[])}.
+ */
+ private void rewritePOM() {
+ pom.setLength(0);
+ pom.append(TEMPLATE);
+ for (final Element element : Element.values()) {
+ int start = pom.length();
+ while ((start = pom.lastIndexOf(element.property, start)) >= 0) {
+ final int end = start + element.property.length();
+ final String value = info.get(element);
+ if (value == null) {
+ throw new SISBuildException("Tag not found: " + element.tag);
+ }
+ pom.replace(start, end, value);
+ }
+ }
+ final int start = pom.indexOf("${");
+ if (start >= 0) {
+ int end = pom.indexOf("}", start) + 1;
+ if (end <= 0) end = pom.length();
+ throw new SISBuildException("Unknown property: " + pom.substring(start, end));
+ }
+ }
+
+ /**
+ * Adds the given dependencies at the end of the {@code pom.xml}.
+ * An initial POM must have been written by {@link #rewritePOM()}.
+ * Dependencies must have been collected by {@link #findUsedDependencies(MavenArtifactSet)} first.
+ *
+ * @param dependencies the project dependencies to add.
+ */
+ private void addDependencies(final Map<String,Dependency> dependencies) {
+ final int start = pom.lastIndexOf("\n", pom.lastIndexOf("</dependencies>")) + 1;
+ if (start <= 0) {
+ throw new SISBuildException("Bad pom.xml template.");
+ }
+ final String suffix = pom.substring(start);
+ pom.setLength(start);
+ for (final Map.Entry<String,Boolean> entry : requires.entrySet()) {
+ final Dependency dep = dependencies.get(entry.getKey());
+ if (dep != null) {
+ dep.writeAsMaven(pom, 2, entry.getValue());
+ }
+ }
+ pom.append(suffix);
+ }
+
+ /**
+ * Collects the dependencies declared in {@code module-info.class} for the given artifacts.
+ * Artifact without {@code module-info.class} are ignored. Duplicated values are ignored.
+ * Dependencies are added in the {@link #requires} map.
+ *
+ * @param artifacts the artifacts for which to collect dependencies.
+ * @throws IOException if an error occurred while reading {@code module-info.class}.
+ */
+ private void findUsedDependencies(final MavenArtifactSet artifacts) throws IOException {
+ for (final MavenArtifact artifact : artifacts) {
+ try (JarFile zip = new JarFile(artifact.getFile(), false, JarFile.OPEN_READ, JarFile.runtimeVersion())) {
+ final ZipEntry entry = zip.getEntry("module-info.class");
+ if (entry != null) {
+ try (InputStream in = zip.getInputStream(entry)) {
+ final ModuleDescriptor module = ModuleDescriptor.read(in);
+ for (final ModuleDescriptor.Requires r : module.requires()) {
+ requires.put(r.name(), r.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads and rewrites the {@code pom.xml} file content.
+ *
+ * @param project the project to be published.
+ * @param subProjects the sub-projects, which also need to be included as dependencies across JPMS modules.
+ * @param pub configuration of the artifact to publish in Maven format.
+ * @param xml provider of the original content, and where to write the result.
+ */
+ public static void rewrite(final Project project, final Collection<Dependency> subProjects, final MavenPublication pub, final XmlProvider xml) {
+ final var g = new Generator(xml.asString());
+ g.parseOriginalPOM();
+ g.rewritePOM();
+ try {
+ g.findUsedDependencies(pub.getArtifacts());
+ g.addDependencies(Dependency.jpms(project, subProjects));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/package-info.java
similarity index 65%
copy from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
copy to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/package-info.java
index 5817ad614a..d9afdfff4e 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/package-info.java
@@ -17,11 +17,14 @@
/**
- * Javadoc taglets for processing custom tags in SIS source code comments,
- * together with doclet performing post-processing after Javadoc generation.
+ * Generates Maven POM from a template file. We do not use Gradle's API for this task
+ * (e.g. the setter methods in {@link org.gradle.api.publish.maven.MavenPom}) because
+ * as of Gradle 8.2.1, they do not provide sufficient control over the file content.
+ * In particular we didn't found the way to specify the parent POM,
+ * neither how to control the list of dependencies.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since 0.5
+ * @version 1.4
+ * @since 1.4
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.maven;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/Argument.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/Argument.java
new file mode 100644
index 0000000000..4906946d22
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/Argument.java
@@ -0,0 +1,100 @@
+/*
+ * 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.sis.buildtools.maven.workaround;
+
+import java.util.Arrays;
+import java.util.EnumMap;
+
+
+/**
+ * Command-line argument for {@link ProxyGenerator#main(String[])}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+enum Argument {
+ /**
+ * Directory where to write the generated files.
+ */
+ OUT("Directory where to write the generated files."),
+
+ /**
+ * Java package where to put generated Java code (if needed).
+ */
+ PACKAGE("Java package where to put generated Java code (if needed).");
+
+ /**
+ * Explanation shown in help screen for this option.
+ */
+ private final String description;
+
+ /**
+ * Creates a new option.
+ *
+ * @param description explanation shown in help screen for this option.
+ */
+ private Argument(final String description) {
+ this.description = description;
+ }
+
+ /**
+ * Parses the options. For each parsed option, the corresponding element in the given array
+ * is set to {@code null}. Non-null elements that are left after this method completion are
+ * JAR files to process.
+ *
+ * @param args command line options.
+ * @return parsed options, or an empty map in case of parsing error.
+ */
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ static EnumMap<Argument,String> parse(final String[] args) {
+ String message = "Illegal option: %s%n";
+ final EnumMap<Argument,String> map = new EnumMap<>(Argument.class);
+ for (int i=0; i<args.length; i++) {
+ String arg = args[i];
+ if (arg.startsWith("--")) {
+ args[i] = null;
+ final int s = arg.indexOf('=');
+ if (s >= 0) try {
+ final var option = Argument.valueOf(arg.substring(2,s).trim().toUpperCase());
+ if (map.put(option, arg.substring(s+1).trim()) == null) {
+ continue;
+ }
+ message = "--%s cannot be repeated.%n";
+ arg = option.name().toLowerCase();
+ } catch (IllegalArgumentException e) {
+ arg = arg.substring(0, s).trim();
+ }
+ System.err.printf(message, arg);
+ Arrays.fill(args, null);
+ map.clear();
+ break;
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Prints all available options.
+ */
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ static void help() {
+ System.out.println("Syntax: [option=value] --out=<dir> jar...");
+ System.out.println("Options:");
+ for (final Argument option : values()) {
+ System.out.printf(" --%-8s %s%n", option.name().toLowerCase(), option.description);
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/ProxyGenerator.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/ProxyGenerator.java
new file mode 100644
index 0000000000..6938795b54
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/ProxyGenerator.java
@@ -0,0 +1,279 @@
+/*
+ * 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.sis.buildtools.maven.workaround;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.BufferedWriter;
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.Map;
+import java.util.List;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import java.lang.module.ModuleDescriptor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+
+/**
+ * Generates {@code META-INF/services} declarations and, if needed, proxy classes.
+ * The input is one or more JAR files containing {@code module-info.class} files.
+ * The output is {@code META-INF/services} and (optionally) Java wrapper classes
+ * written in the specified target directory. See MNG-7855 for more information.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @see <a href="https://issues.apache.org/jira/browse/MNG-7855">MNG-7855 on JIRA issue tracker</a>
+ */
+@SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace"})
+public class ProxyGenerator {
+ /**
+ * Executes from the command-line. The arguments shall contain {@code --out=dir}
+ * where {@code dir} is the target directory where to write generated files.
+ * Other arguments that are not options are JAR files.
+ *
+ * @param args command-line arguments as {@code <option>=<value>} pairs followed by JAR files.
+ * @throws IOException if an error occurred while reading a JAR file or writing the generated files.
+ */
+ public static void main(final String[] args) throws IOException {
+ final EnumMap<Argument,String> options = Argument.parse(args);
+ final String target = options.get(Argument.OUT);
+ ProxyGenerator generator = null;
+ if (target != null) {
+ for (final String arg : args) {
+ if (arg != null) {
+ if (generator == null) {
+ generator = new ProxyGenerator(new File(target), options.get(Argument.PACKAGE));
+ }
+ generator.collectProviders(new File(arg));
+ }
+ }
+ }
+ if (generator != null) {
+ generator.checkStaticProviderMethods();
+ generator.generate();
+ } else {
+ Argument.help();
+ }
+ }
+
+ /**
+ * Class names of all service providers found so far.
+ * Keys are names of the classes or interfaces defining the services.
+ * Values are names of the implementation classes.
+ */
+ private final Map<String, List<String>> providers;
+
+ /**
+ * {@code true} if we found at least one provider which is not on the classpath.
+ * This is used for avoiding to flood the console with too many warning messages.
+ */
+ private boolean providerNotOnClasspath;
+
+ /**
+ * {@code true} if at least one public static {@code provider()} method has been found.
+ * This is used for avoiding to flood the console with too many warning messages.
+ */
+ private boolean providerStaticMethodFound;
+
+ /**
+ * Name of the Java package to use if proxy Java classed need to be generated.
+ * Null if the {@link Argument#PACKAGE} option has not been specified.
+ */
+ private final String packageName;
+
+ /**
+ * Number of the proxy generated, used in class name.
+ */
+ private int proxyNumber;
+
+ /**
+ * Destination directory where to write the generated files.
+ */
+ private final File target;
+
+ /**
+ * Creates a new generator of {@code META-INF/services} declarations.
+ *
+ * @param target destination directory where to write the generated files.
+ * @param packageName name of the Java package to use if proxy Java classed need to be generated.
+ */
+ private ProxyGenerator(final File target, final String packageName) {
+ this.target = target;
+ this.packageName = packageName;
+ this.providers = new HashMap<>();
+ }
+
+ /**
+ * Collects all providers declared in the {@code module-info.class} entry of the specified JAR file.
+ *
+ * @param file the file for which to collect service providers.
+ * @throws IOException if an error occurred while reading the JAR file.
+ */
+ private void collectProviders(final File file) throws IOException {
+ try (JarFile zip = new JarFile(file, false, JarFile.OPEN_READ, JarFile.runtimeVersion())) {
+ final ZipEntry entry = zip.getEntry("module-info.class");
+ if (entry != null) {
+ try (InputStream in = zip.getInputStream(entry)) {
+ final ModuleDescriptor desc = ModuleDescriptor.read(in);
+ desc.provides().forEach((provides) -> {
+ providers.computeIfAbsent(provides.service(), (service) -> new ArrayList<>())
+ .addAll(provides.providers());
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Verifies is any service provider declares a public static {@code provider()} method.
+ * Those methods are incompatible with {@code META-INF/services} and need to be replaced
+ * by a proxy class.
+ *
+ * @throws IOException if an error occurred while generating a proxy class.
+ */
+ private void checkStaticProviderMethods() throws IOException {
+ for (final Map.Entry<String, List<String>> entry : providers.entrySet()) {
+ final List<String> providers = entry.getValue();
+ for (int i=0; i < providers.size(); i++) {
+ final Class<?> service, provider;
+ try {
+ provider = Class.forName(providers.get(i));
+ service = Class.forName(entry.getKey()); // Should not fail if above line didn't fail.
+ if (!Modifier.isStatic(provider.getMethod("provider", (Class[]) null).getModifiers())) {
+ continue;
+ }
+ } catch (ClassNotFoundException e) {
+ if (!providerNotOnClasspath) {
+ providerNotOnClasspath = true;
+ System.err.printf("The implementation class of at least one service provider could not be found.%n"
+ + "ProxyGenerator cannot verify if a public static provider() method exists.%n"
+ + "If this check is desired, add the provider implementations on the classpath.%n"
+ + "The error is: %s%n", e);
+ }
+ continue;
+ } catch (NoSuchMethodException e) {
+ // No public `provider()` method. This is normal.
+ continue;
+ }
+ if (packageName != null) {
+ final String replacement = writeJavaProxyClass(service, provider);
+ providers.set(i, replacement);
+ } else if (!providerStaticMethodFound) {
+ providerStaticMethodFound = true;
+ System.out.printf("A public static provider() method was found%n"
+ + "but the --package option was not specified.%n"
+ + "Provider is: %s%n", provider.getCanonicalName());
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates Java code for the proxy class.
+ *
+ * @param service the service to provide.
+ * @param provider the provider to wrap.
+ * @return fully qualified name of the proxy class.
+ * @throws IOException if an error occurred while writing the proxy class.
+ */
+ private String writeJavaProxyClass(final Class<?> service, final Class<?> provider) throws IOException {
+ final String proxyName = String.format("Proxy%03d", ++proxyNumber);
+ File file = new File(new File(target, "java"), packageName.replace('.', File.separatorChar));
+ file.mkdirs();
+ file = new File(file, proxyName);
+ try (final var out = new BufferedWriter(new FileWriter(file))) {
+ final String ls = System.lineSeparator();
+ out.append("package ").append(packageName).append(';').append(ls)
+ .append(ls)
+ .append("public final class ").append(proxyName)
+ .append(" implements ").append(service.getCanonicalName()).append(" {").append(ls)
+ .append(" private final ").append(service.getCanonicalName()).append(" provider;").append(ls)
+ .append(" public ").append(proxyName).append("() {").append(ls)
+ .append(" provider = ").append(provider.getCanonicalName()).append(".provider();").append(ls)
+ .append(" }").append(ls);
+ for (final Method m : service.getMethods()) {
+ out.append(" public ").append(m.getReturnType().getCanonicalName())
+ .append(' ').append(m.getName()).append('(');
+ final Class<?>[] args = m.getParameterTypes();
+ for (int i=0; i<args.length; i++) {
+ if (i != 0) out.append(", ");
+ out.append(args[i].getCanonicalName()).append(" p").append(Integer.toString(i));
+ }
+ out.append(')');
+ String separator = " throws ";
+ for (final Class<?> c : m.getExceptionTypes()) {
+ out.append(separator).append(c.getCanonicalName());
+ separator = ", ";
+ }
+ out.append(" {").append(ls).append(" ");
+ if (m.getReturnType() != Void.TYPE) {
+ out.append("return ");
+ }
+ out.append("provider.").append(m.getName()).append('(');
+ for (int i=0; i<args.length; i++) {
+ if (i != 0) out.append(", ");
+ out.append('p').append(Integer.toString(i));
+ }
+ out.append(");").append(ls)
+ .append(" }").append(ls);
+ }
+ out.append('}').append(ls);
+ }
+ return packageName + '.' + proxyName;
+ }
+
+ /**
+ * Generates the {@code META-INF/services} files in the target directory.
+ *
+ * @throws IOException if an error occurred while writing a file.
+ */
+ private void generate() throws IOException {
+ final File services = new File(target, "resources/META-INF/services");
+ services.mkdirs();
+ for (final Map.Entry<String, List<String>> entry : providers.entrySet()) {
+ try (BufferedWriter out = new BufferedWriter(new FileWriter(new File(services, entry.getKey())))) {
+ out.write("# Workaround for Maven bug https://issues.apache.org/jira/browse/MNG-7855"); out.newLine();
+ out.write("# The content of this file is automatically derived from module-info.class file."); out.newLine();
+ out.write("# Should be used only if the JAR file was on class-path rather than module-path."); out.newLine();
+ for (final String provider : entry.getValue()) {
+ out.write(provider);
+ out.write(System.lineSeparator());
+ }
+ }
+ }
+ }
+
+ /**
+ * {@returns the class names of all service providers found so far}.
+ * This information is provided for debugging purposes only.
+ */
+ @Override
+ public String toString() {
+ final var sb = new StringBuilder(1000);
+ providers.entrySet().forEach((entry) -> {
+ sb.append(entry.getKey()).append(':').append(System.lineSeparator());
+ entry.getValue().forEach((provider) -> {
+ sb.append(" ").append(provider).append(System.lineSeparator());
+ });
+ });
+ return sb.toString();
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/package-info.java
new file mode 100644
index 0000000000..8976baf299
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/maven/workaround/package-info.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+/**
+ * Generates {@code META-INF/services} files with the content of {@code module-info.class} files.
+ * This is used for compatibility when a modularized dependency is declared on the class-path
+ * rather than the module-path. This need occurs when dependencies are automatically dispatched
+ * between {@code --class-path} and {@code --module-path} options by a tool on which we have no
+ * control, such as Maven 3.8.6 or Gradle 8.2.1.
+ *
+ * <p>Note that this workaround does not fix the real issue,
+ * which is that dependencies are loaded as unnamed modules when they should not.
+ * The workaround allows libraries and applications to find some service providers despite this problem,
+ * sometime not in the way that the providers should be (because of wrappers).
+ * But any other features that depend on named modules are still broken.</p>
+ *
+ * <p>The classes in this package are not used during Apache SIS builds.
+ * They are used only if the content of some {@code module-info.java} files changes
+ * and the {@code META-INF/services/*} files need to be regenerated.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @see <a href="https://issues.apache.org/jira/browse/MNG-7855">MNG-7855 on JIRA issue tracker</a>
+ */
+package org.apache.sis.buildtools.maven.workaround;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/IndexedResourceCompiler.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/resources/IndexedResourceCompiler.java
similarity index 68%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/IndexedResourceCompiler.java
rename to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/resources/IndexedResourceCompiler.java
index 879d261cce..872158b366 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/IndexedResourceCompiler.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/resources/IndexedResourceCompiler.java
@@ -14,37 +14,54 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.sis.util.resources;
-
-import java.io.*;
-import java.util.*;
-import java.text.MessageFormat;
+package org.apache.sis.buildtools.resources;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.TreeMap;
+import java.util.SortedMap;
+import java.util.Iterator;
+import java.util.Properties;
import java.util.regex.Pattern;
+import java.text.MessageFormat;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Writer;
/**
* Reads a given list of {@code .properties} files and copies their content to {@code .utf} files using UTF-8 encoding.
- * This class also checks for key validity and checks values for {@link MessageFormat} compatibility.
+ * This class also checks for key validity and checks the property values for {@link MessageFormat} compatibility.
* Finally, it writes the key values in the Java source files.
*
- * <p>This class is independent of any Mojo and could be executed from the command-line.
- * For now we keep it package-private, but we could consider to enable execution from
- * the command-line in a future version if this happen to be useful.</p>
+ * <p>Instances of this class are not thread safe.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.4
* @since 0.3
*/
-class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
+public class IndexedResourceCompiler {
/**
* Extension for java source files.
*/
- static final String JAVA_EXT = ".java";
+ private static final String JAVA_EXT = ".java";
/**
* Extension for properties source files.
*/
- static final String PROPERTIES_EXT = ".properties";
+ private static final String PROPERTIES_EXT = ".properties";
/**
* Extension for resource target files.
@@ -83,7 +100,12 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
private static final String KEY_MARGIN = " "; // 8 spaces
/**
- * The source directory of the java and properties files.
+ * Directory to exclude when parsing sub-directories recursively.
+ */
+ private static final String EXCLUDE_DIRECTORY = "doc-files";
+
+ /**
+ * The source directory of the Java and properties files.
*/
private final File sourceDirectory;
@@ -105,57 +127,170 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
private final Map<Integer,String> allocatedIDs = new HashMap<>();
/**
- * Resource keys and their localized values. This map will be cleared for each language
- * in a resource bundle.
+ * Resource keys and their localized values.
+ * This map will be cleared for each language in a resource bundle.
*/
private final Map<Object,Object> resources = new HashMap<>();
/**
- * The resources bundle base classes.
+ * Buffer to use for writing UTF data in an array of bytes.
+ *
+ * @see #writeUTF()
*/
- private final File[] resourcesToProcess;
+ private final ByteArrayOutputStream bufferUTF = new ByteArrayOutputStream(1024 * 8);
/**
- * Number of errors found.
+ * The result of compiling UTF files. Keys are the files where to write the results,
+ * and values are the bytes to write for the associated file.
+ */
+ private final Map<File, ? super byte[]> destination;
+
+ /**
+ * Number of logical errors found.
*/
private int errors;
/**
* Constructs a new {@code IndexedResourceCompiler}.
+ * The compilation results are not written immediately to the build directory.
+ * Instead they are saved in the given map and written later by the caller.
*
- * @param sourceDirectory the source directory.
- * @param buildDirectory the target directory where to write UTF files.
- * @param resourcesToProcess the resource bundle base classes
- * (e.g. {@code org/apache/sis/util/resources/Vocabulary.java}).
- */
- IndexedResourceCompiler(final File sourceDirectory, final File buildDirectory, final File[] resourcesToProcess) {
- this.sourceDirectory = sourceDirectory;
- this.buildDirectory = buildDirectory;
- this.resourcesToProcess = resourcesToProcess;
+ * @param sourceDirectory the source directory.
+ * @param buildDirectory the target directory where to write UTF files.
+ * @param destination where to write the compilation results.
+ */
+ public IndexedResourceCompiler(final File sourceDirectory, final File buildDirectory,
+ final Map<File, ? super byte[]> destination)
+ {
+ this.sourceDirectory = sourceDirectory;
+ this.buildDirectory = buildDirectory;
+ this.destination = destination;
}
/**
- * Runs the resource compiler.
+ * Flags the properties files which seem to be about internationalized resources.
+ * For example if the given map contains the following files:
+ * <ul>
+ * <li>{@code "Errors.properties"}</li>
+ * <li>{@code "Errors_en.properties"}</li>
+ * <li>{@code "Errors_fr.properties"}</li>
+ * <li>{@code "Messages.properties"}</li>
+ * <li>{@code "Messages_en.properties"}</li>
+ * <li>{@code "Messages_fr.properties"}</li>
+ * <li>{@code "NotAnInternationalResource.properties"}</li>
+ * </ul>
+ *
+ * Then this method will set to {@code Boolean.TRUE} the values associated to the following files
+ * and remove the entries for their language variants:
+ * <ul>
+ * <li>{@code "Errors.properties"}</li>
+ * <li>{@code "Messages.properties"}</li>
+ * </ul>
*
- * @throws ResourceCompilerException if an error occurred.
- * @return the number of errors found.
+ * The entries that do not seem to be about internationalized resources will be left unchanged.
+ * Their associated values should be {@code Boolean.FALSE}.
+ *
+ * @param resourcesToProcess the files to filter. This map will be modified in-place.
+ * @param checkSourceExists whether to check that the Java source file exists.
*/
- @SuppressWarnings("ForLoopReplaceableByForEach")
- public int run() throws ResourceCompilerException {
- if (!sourceDirectory.isDirectory()) {
- throw new ResourceCompilerException(sourceDirectory + " not found or is not a directory.");
+ static void filterLanguages(final SortedMap<File,Boolean> resourcesToProcess, final boolean checkSourceExists) {
+ final Iterator<Map.Entry<File,Boolean>> it = resourcesToProcess.entrySet().iterator();
+ Map.Entry<File,Boolean> baseEntry = null;
+ String baseName = null;
+ while (it.hasNext()) {
+ final Map.Entry<File,Boolean> entry = it.next();
+ final File file = entry.getKey();
+ if (baseName != null && file.getName().startsWith(baseName)) {
+ if (baseEntry != null) {
+ baseEntry.setValue(Boolean.TRUE);
+ it.remove();
+ }
+ continue;
+ }
+ baseEntry = entry;
+ baseName = getBaseName(file);
+ var source = new File(file.getParentFile(), baseName + JAVA_EXT);
+ baseName += '_';
+ if (checkSourceExists && !source.isFile()) {
+ baseEntry = null;
+ }
}
- for (int i=0; i<resourcesToProcess.length; i++) {
- bundleClass = resourcesToProcess[i];
- allocatedIDs.clear();
- resources.clear();
- try {
- loadKeyValues();
- scanForResources();
- } catch (IOException exception) {
- throw new ResourceCompilerException(exception);
+ }
+
+ /**
+ * Returns the file name without the {@value #PROPERTIES_EXT} extension.
+ * For multilingual resources, this is the base resource name.
+ */
+ private static String getBaseName(final File file) {
+ final String name = file.getName();
+ return name.substring(0, name.length() - PROPERTIES_EXT.length());
+ }
+
+ /**
+ * Recursively scans the source directory and finds all Java classes having a property files of the same name.
+ * Then invokes the resource compiler for those files by calls to {@link #onJavaSource(File)}.
+ * Property files that can not be compiled are linked or copied verbatim to the destination directory.
+ *
+ * @return the number of logical errors found in properties.
+ * @throws IOException if an error occurred while reading or writing a file.
+ */
+ public int onJavaSourceDirectory() throws IOException {
+ onJavaSourceDirectory(sourceDirectory);
+ return errors;
+ }
+
+ /**
+ * Implementation of {@link #onJavaSourceDirectory()} invoked recursively for scanning down the tree.
+ *
+ * @param directory root directory of Java source files for a single module.
+ * @throws IOException if an error occurred while reading or writing a file.
+ */
+ private void onJavaSourceDirectory(final File directory) throws IOException {
+ final File[] files = directory.listFiles((dir, name) -> !name.endsWith(JAVA_EXT));
+ if (files == null) {
+ throw new FileNotFoundException(directory + " not found or is not a directory.");
+ }
+ final var resourcesToProcess = new TreeMap<File,Boolean>();
+ for (final File file : files) {
+ final String name = file.getName();
+ if (!name.isEmpty() && name.charAt(0) != '.') {
+ if (file.isDirectory()) {
+ if (!EXCLUDE_DIRECTORY.equalsIgnoreCase(name)) {
+ onJavaSourceDirectory(file);
+ }
+ } else if (name.endsWith(PROPERTIES_EXT)) {
+ resourcesToProcess.put(file, Boolean.FALSE);
+ } else {
+ otherResource(file, sourceToTarget(file));
+ }
+ }
+ }
+ filterLanguages(resourcesToProcess, true);
+ for (final Map.Entry<File,Boolean> entry : resourcesToProcess.entrySet()) {
+ final File source = entry.getKey();
+ if (entry.getValue() && acceptCompilableResource(source)) {
+ onJavaSource(new File(source.getParentFile(), getBaseName(source) + JAVA_EXT));
+ } else {
+ otherResource(source, sourceToTarget(source));
}
}
+ }
+
+ /**
+ * Runs the resource compiler on the given Java source file.
+ * The source file may be modified.
+ *
+ * @param resourceToProcess the resource bundle base classes
+ * (e.g. {@code org/apache/sis/util/resources/Vocabulary.java}).
+ * @return the number of logical errors found in properties.
+ * @throws IOException if an error occurred while reading or writing a file.
+ */
+ public int onJavaSource(final File resourceToProcess) throws IOException {
+ bundleClass = resourceToProcess;
+ allocatedIDs.clear();
+ resources.clear();
+ loadKeyValues();
+ scanForResources();
return errors;
}
@@ -197,11 +332,14 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
}
/**
- * Returns the path of the given file relative to the given directory.
+ * Converts a path relative to the source directory into a path relative to the target directory.
+ *
+ * @param file an absolute path to a source file.
+ * @return absolute path to the destination file in the build directory.
*/
- private static String relative(final File directory, final File file) throws IOException {
+ private File sourceToTarget(final File file) throws IOException {
String path = file.getPath();
- final String expected = directory.getPath();
+ final String expected = sourceDirectory.getPath();
if (!path.startsWith(expected)) {
throw new IOException(path + " is not relative to " + expected);
}
@@ -209,16 +347,16 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
if (!path.startsWith(File.separator)) {
path = path.substring(File.separator.length());
}
- return path;
+ return new File(buildDirectory, path);
}
/**
- * Scans the package of {@link #bundleClass} for its {@code .properties} files.
+ * Searches {@code .properties} files in the package which contains {@link #bundleClass}.
* The following methods are invoked by this method:
*
* <ul>
* <li>{@link #loadProperties(File)}</li>
- * <li>{@link #writeUTF(File)}</li>
+ * <li>{@link #writeUTF()}</li>
* <li>{@link #writeJavaSource()}</li>
* </ul>
*
@@ -228,7 +366,7 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
String classname = bundleClass.getName();
classname = classname.substring(0, classname.lastIndexOf('.'));
final File srcDir = bundleClass.getParentFile();
- final File utfDir = new File(buildDirectory, relative(sourceDirectory, srcDir));
+ final File utfDir = sourceToTarget(srcDir);
if (!srcDir.isDirectory()) {
throw new FileNotFoundException("\"" + srcDir + "\" is not a directory.");
}
@@ -236,16 +374,15 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
throw new FileNotFoundException("\"" + utfDir + "\" is not a directory.");
}
File defaultLanguage = null;
- for (final File file : srcDir.listFiles(this)) {
+ final String prefix = classname;
+ for (final File file : srcDir.listFiles((directory, name) -> name.endsWith(PROPERTIES_EXT) && name.startsWith(prefix))) {
+ loadProperties(file);
final String filename = file.getName();
- if (filename.startsWith(classname)) {
- loadProperties(file);
- final String noExt = filename.substring(0, filename.length() - PROPERTIES_EXT.length());
- final File utfFile = new File(utfDir, noExt + RESOURCES_EXT);
- writeUTF(utfFile);
- if (noExt.equals(classname)) {
- defaultLanguage = file;
- }
+ final String noExt = filename.substring(0, filename.length() - PROPERTIES_EXT.length());
+ final File utfFile = new File(utfDir, noExt + RESOURCES_EXT);
+ destination.put(utfFile, writeUTF());
+ if (noExt.equals(classname)) {
+ defaultLanguage = file;
}
}
if (defaultLanguage != null) {
@@ -255,18 +392,6 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
writeJavaSource();
}
- /**
- * Returns {@code true} if the given file is a property file.
- *
- * @param directory the directory (ignored).
- * @param name the file name.
- * @return {@code true} if the given file is a property file.
- */
- @Override
- public final boolean accept(final File directory, final String name) {
- return name.endsWith(PROPERTIES_EXT);
- }
-
/**
* Loads the specified property file. No processing are performed on them.
*
@@ -353,7 +478,7 @@ class IndexedResourceCompiler implements FilenameFilter, Comparator<Object> {
* because some classes use key value 0 for meaning "no message".
*/
final String[] keys = resources.keySet().toArray(new String[resources.size()]);
- Arrays.sort(keys, this);
+ Arrays.sort(keys);
int freeID = 0;
for (final String key : keys) {
if (!allocatedIDs.containsValue(key)) {
@@ -423,29 +548,27 @@ search: for (int i=0; i<buffer.length(); i++) { // Length of `bu
}
/**
- * Writes UTF file. The following methods must be invoked before this one:
+ * Writes UTF data in an array.
+ * The following methods must be invoked before this one:
*
* <ul>
* <li>{@link #loadKeyValues()}</li>
* <li>{@link #loadProperties(File)}</li>
* </ul>
*
- * @param file the destination file.
* @throws IOException if an input/output operation failed.
*/
- private void writeUTF(final File file) throws IOException {
- final File directory = file.getParentFile();
- if (!directory.isDirectory() && !directory.mkdirs()) {
- throw new IOException("Cannot create the " + directory + " directory.");
- }
+ private byte[] writeUTF() throws IOException {
+ bufferUTF.reset();
final int count = allocatedIDs.isEmpty() ? 0 : Collections.max(allocatedIDs.keySet());
- try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
+ try (DataOutputStream out = new DataOutputStream(bufferUTF)) {
out.writeInt(count);
for (int i=1; i<=count; i++) {
final String value = (String) resources.get(allocatedIDs.get(i));
out.writeUTF((value != null) ? value : "");
}
}
+ return bufferUTF.toByteArray();
}
/**
@@ -505,7 +628,7 @@ search: for (int i=0; i<buffer.length(); i++) { // Length of `bu
*/
int startLineToCompare = buffer.length();
final Map.Entry<?,?>[] entries = allocatedIDs.entrySet().toArray(new Map.Entry<?,?>[allocatedIDs.size()]);
- Arrays.sort(entries, this);
+ Arrays.sort(entries, (o1, o2) -> ((String) o1.getValue()).compareTo((String) o2.getValue()));
for (final Map.Entry<?,?> entry : entries) {
buffer.append(lineSeparator);
final String key = (String) entry.getValue();
@@ -589,21 +712,27 @@ search: for (int i=0; i<buffer.length(); i++) { // Length of `bu
}
/**
- * Compares two resource keys. Object {@code o1} and {@code o2} are usually {@link String}
- * objects representing resource keys (for example, "{@code MISMATCHED_DIMENSION}"), but
- * may also be {@link java.util.Map.Entry}.
+ * Invoked for each resource file that {@code IndexedResourceCompiler} thinks is compilable.
+ * Subclasses can override this method for excluding some false positives.
+ * The default implementation always returns {@code true}.
+ *
+ * @param source the {@code .properties} file that may be compiled.
+ * @return whether to compile the specified file.
+ */
+ protected boolean acceptCompilableResource(final File source) {
+ return true;
+ }
+
+ /**
+ * Invoked when a file is not a Java source file, a properties file being compiled or a directory.
+ * The default implementation does nothing.
+ * Subclasses can override for copying of filtering the resource if desired.
*
- * @param o1 the resource key to compare.
- * @param o2 the second resource key to compare.
- * @return -1, 0 or +1 based on the alphabetic order of resource keys.
- */
- @Override
- public final int compare(Object o1, Object o2) {
- if (o1 instanceof Map.Entry<?,?>) o1 = ((Map.Entry<?,?>) o1).getValue();
- if (o2 instanceof Map.Entry<?,?>) o2 = ((Map.Entry<?,?>) o2).getValue();
- final String key1 = (String) o1;
- final String key2 = (String) o2;
- return key1.compareTo(key2);
+ * @param source the file which is not a Java source of properties file being compiled.
+ * @param target destination file in the build directory if the implementation wants to copy.
+ * @throws IOException if an error occurred while processing the resource.
+ */
+ protected void otherResource(File source, File target) throws IOException {
}
/**
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/resources/package-info.java
similarity index 75%
copy from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
copy to buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/resources/package-info.java
index 5817ad614a..17e61860dc 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
+++ b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/buildtools/resources/package-info.java
@@ -17,11 +17,10 @@
/**
- * Javadoc taglets for processing custom tags in SIS source code comments,
- * together with doclet performing post-processing after Javadoc generation.
+ * Build resources for internationalization in Apache SIS.
*
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since 0.5
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.4
+ * @since 1.4
*/
-package org.apache.sis.internal.doclet;
+package org.apache.sis.buildtools.resources;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/Assembler.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/Assembler.java
deleted file mode 100644
index 1478df82a0..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/Assembler.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * 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.sis.internal.maven;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.FilenameFilter;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.util.Map;
-import java.util.Set;
-import java.util.Enumeration;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-import org.apache.maven.artifact.Artifact;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-import org.apache.maven.project.MavenProject;
-import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
-import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
-
-import static org.apache.sis.internal.maven.Filenames.*;
-
-
-/**
- * Creates a ZIP file containing the Apache SIS binary distribution.
- * The created file contains:
- *
- * <ul>
- * <li>the content of the <code>application/sis-javafx/src/main/artifact</code> directory;</li>
- * <li>the JAR files of all modules and their dependencies, without their native resources;</li>
- * <li>the native resources in a separated {@code lib/} directory.</li>
- * </ul>
- *
- * This MOJO can be invoked from the command line in the {@code sis-javafx} module as below:
- *
- * <blockquote><code>mvn package org.apache.sis.core:sis-build-helper:dist</code></blockquote>
- *
- * <h2>Limitation</h2>
- * The current implementation uses some hard-coded paths and filenames.
- * See the <cite>Distribution file</cite> section in
- * <a href="http://sis.apache.org/build.html">Build from source</a> page
- * for more information.</p>
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since 0.4
- */
-@Mojo(name = "dist", defaultPhase = LifecyclePhase.INSTALL, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
-public final class Assembler extends AbstractMojo implements FilenameFilter {
- /**
- * Project information (name, version, URL).
- */
- @Parameter(property="project", required=true, readonly=true)
- private MavenProject project;
-
- /**
- * Base directory of the module to compile.
- * Artifact content is expected in the {@code "src/main/artifact"} subdirectory.
- */
- @Parameter(property="basedir", required=true, readonly=true)
- private String baseDirectory;
-
- /**
- * The root directory (without the "<code>target/binaries</code>" sub-directory) where JARs
- * are to be copied. It should be the directory of the root <code>pom.xml</code>.
- */
- @Parameter(property="session.executionRootDirectory", required=true)
- private String rootDirectory;
-
- /**
- * Invoked by reflection for creating the MOJO.
- */
- public Assembler() {
- }
-
- /**
- * Creates the distribution file.
- *
- * @throws MojoExecutionException if the plugin execution failed.
- */
- @Override
- public void execute() throws MojoExecutionException {
- final File sourceDirectory = new File(baseDirectory, ARTIFACT_PATH);
- if (!sourceDirectory.isDirectory()) {
- throw new MojoExecutionException("Directory not found: " + sourceDirectory);
- }
- final String artifactBase = FINALNAME_PREFIX + project.getVersion();
- final File targetFile = distributionFile(rootDirectory, artifactBase + ".zip");
- try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(targetFile)) {
- zip.setLevel(9);
- appendRecursively(sourceDirectory, artifactBase, zip);
- /*
- * At this point, all the "application/sis-javafx/src/main/artifact" and sub-directories
- * have been zipped. Now append the JAR files for each module and their dependencies.
- */
- final Map<String,byte[]> nativeFiles = new LinkedHashMap<>();
- for (final File file : files(project)) {
- ZipArchiveEntry entry = new ZipArchiveEntry(artifactBase + '/' + LIB_DIRECTORY + '/' + file.getName());
- zip.putArchiveEntry(entry);
- appendJAR(file, zip, nativeFiles);
- zip.closeArchiveEntry();
- }
- /*
- * At this point we finished creating all entries in the ZIP file, except native resources.
- * Copy them now.
- */
- for (final Map.Entry<String,byte[]> nf : nativeFiles.entrySet()) {
- ZipArchiveEntry entry = new ZipArchiveEntry(artifactBase + '/' + LIB_DIRECTORY + '/' + nf.getKey());
- entry.setUnixMode(0555); // Readable and executable for all, but not writable.
- zip.putArchiveEntry(entry);
- zip.write(nf.getValue());
- zip.closeArchiveEntry();
- nf.setValue(null);
- }
- } catch (IOException e) {
- throw new MojoExecutionException(e.getLocalizedMessage(), e);
- }
- }
-
- /**
- * Returns all files to include for the given Maven project.
- */
- private static Set<File> files(final MavenProject project) throws MojoExecutionException {
- final Set<File> files = new LinkedHashSet<>();
- files.add(project.getArtifact().getFile());
- for (final Artifact dep : project.getArtifacts()) {
- final String scope = dep.getScope();
- if (Artifact.SCOPE_COMPILE.equalsIgnoreCase(scope) ||
- Artifact.SCOPE_RUNTIME.equalsIgnoreCase(scope))
- {
- files.add(dep.getFile());
- }
- }
- if (files.remove(null)) {
- throw new MojoExecutionException("Invocation of this MOJO shall be done together with a \"package\" Maven phase.");
- }
- return files;
- }
-
- /**
- * Adds the given file in the ZIP file. If the given file is a directory, then this method
- * recursively adds all files contained in this directory. This method is invoked for zipping
- * the "application/sis-javafx/src/main/artifact" directory and sub-directories before to zip.
- */
- private void appendRecursively(final File file, String relativeFile, final ZipArchiveOutputStream out) throws IOException {
- if (file.isDirectory()) {
- relativeFile += '/';
- }
- final ZipArchiveEntry entry = new ZipArchiveEntry(file, relativeFile);
- if (file.canExecute()) {
- entry.setUnixMode(0744);
- }
- out.putArchiveEntry(entry);
- if (!entry.isDirectory()) {
- try (FileInputStream in = new FileInputStream(file)) {
- in.transferTo(out);
- }
- }
- out.closeArchiveEntry();
- if (entry.isDirectory()) {
- for (final String filename : file.list(this)) {
- appendRecursively(new File(file, filename), relativeFile.concat(filename), out);
- }
- }
- }
-
- /**
- * The filter to use for selecting the files to be included in the ZIP file.
- *
- * @param directory the directory.
- * @param filename the filename.
- * @return {@code true} if the given file should be included in the ZIP file.
- */
- @Override
- public boolean accept(final File directory, final String filename) {
- return !filename.isEmpty() && filename.charAt(0) != '.' && !filename.equals(CONTENT_FILE);
- }
-
- /**
- * Returns {@code true} if the given JAR file contains at least one resource in the
- * {@value Filenames#NATIVE_DIRECTORY} directory. Those files will need to be rewritten
- * in order to exclude those resources.
- */
- private static boolean hasNativeResources(final ZipFile in) {
- final Enumeration<? extends ZipEntry> entries = in.entries();
- while (entries.hasMoreElements()) {
- if (entries.nextElement().getName().startsWith(NATIVE_DIRECTORY)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Copies a JAR file in the given ZIP file, except the native resources which are stored in the given map.
- *
- * @param file the JAR file to copy.
- * @param bundle destination where to copy the JAR file.
- * @param nativeFiles where to store the native resources.
- */
- private void appendJAR(final File file, final ZipArchiveOutputStream bundle, final Map<String,byte[]> nativeFiles)
- throws IOException
- {
- try (ZipFile in = new ZipFile(file)) {
- if (hasNativeResources(in)) {
- final ZipOutputStream out = new ZipOutputStream(bundle);
- out.setLevel(9);
- final Enumeration<? extends ZipEntry> entries = in.entries();
- while (entries.hasMoreElements()) {
- final ZipEntry entry = entries.nextElement();
- final String name = entry.getName();
- try (InputStream eis = in.getInputStream(entry)) {
- if (!name.startsWith(NATIVE_DIRECTORY)) {
- out.putNextEntry(new ZipEntry(name));
- eis.transferTo(out); // Copy the entry verbatim.
- out.closeEntry();
- } else if (!entry.isDirectory()) {
- final long size = entry.getSize(); // For memorizing the entry without copying it now.
- if (size <= 0 || size > Integer.MAX_VALUE) {
- throw new IOException(String.format("Errors while copying %s:%n"
- + "Unsupported size for \"%s\" entry: %d", file, name, size));
- }
- final byte[] content = new byte[(int) size];
- final int actual = eis.read(content);
- if (actual != size) {
- throw new IOException(String.format("Errors while copying %s:%n"
- + "Expected %d bytes in \"%s\" but found %d", file, size, name, actual));
- }
- if (nativeFiles.put(name.substring(NATIVE_DIRECTORY.length()), content) != null) {
- throw new IOException(String.format("Errors while copying %s:%n"
- + "Duplicated entry: %s", file, name));
- }
- }
- }
- }
- out.finish();
- return;
- }
- }
- /*
- * If the JAR file has no native resources to exclude, we can copy the JAR file verbatim.
- * This is faster than inflating and deflating again the JAR file.
- */
- try (FileInputStream in = new FileInputStream(file)) {
- in.transferTo(bundle);
- }
- }
-}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/Filenames.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/Filenames.java
deleted file mode 100644
index 828dfcecf5..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/Filenames.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.sis.internal.maven;
-
-import java.io.File;
-import org.apache.maven.plugin.MojoExecutionException;
-
-
-/**
- * Hard-coded file and directory names used by this package.
- *
- * <p><b>Reminder:</b>
- * if the above constants are modified, please remind to edit the <cite>Distribution file</cite>
- * section in the <code>site/content/build.mdtext</code> file.</p>
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since 0.4
- */
-final class Filenames {
- /**
- * The target directory. This directory name is hard-coded instead of using a property annotated
- * by {@code @Parameter(defaultValue="${project.build.directory}")} - or alternatively by invoking
- * {@code MavenProject.getModel().getBuild().getDirectory()}, because we need the target directory
- * of the project root rather than the directory of the module being built.
- */
- static final String TARGET_DIRECTORY = "target";
-
- /**
- * The sub-directory inside {@value #TARGET_DIRECTORY} for binaries.
- */
- static final String BINARIES_DIRECTORY = "binaries";
-
- /**
- * The name of the file inside {@value #BINARIES_DIRECTORY} where to list SIS JAR files and their
- * dependencies on platforms that do not support hard links. Also the file to ignore when copying
- * entries in a ZIP file.
- */
- static final String OTHER_DEPENDENCIES_FILE = "other_dependencies.txt";
-
- /**
- * The file to ignore when copying entries in a ZIP file.
- * Those files appear in the {@value #ARTIFACT_PATH} directory.
- */
- static final String CONTENT_FILE = "content.txt";
-
- /**
- * The sub-directory inside {@value #TARGET_DIRECTORY} containing pack files.
- * This directory will be automatically created if it does not already exist.
- */
- private static final String DISTRIBUTION_DIRECTORY = "distribution";
-
- /**
- * The path to the directory (relative to the project directory) to zip for creating the distribution ZIP file.
- */
- static final String ARTIFACT_PATH = "src/main/artifact";
-
- /**
- * The name of the sub-directory inside {@value #ARTIFACT_PATH} where the JAR files will be located.
- * Note that we will not write in the real directory, but only in the directory structure which is
- * reproduced in the ZIP file.
- */
- static final String LIB_DIRECTORY = "lib";
-
- /**
- * The prefix of native resources in JAR files. All those resources will be excluded from
- * the JAR copied in the zip file and stored in a {@code nativeFiles} map instead.
- */
- static final String NATIVE_DIRECTORY = "native/";
-
- /**
- * The prefix of the final filename. This is hard coded for now.
- */
- static final String FINALNAME_PREFIX = "apache-sis-";
-
- /**
- * Do not allow instantiation of this class.
- */
- private Filenames() {
- }
-
- /**
- * Returns the distribution file, creating its directory if needed.
- *
- * @param rootDirectory the project root directory.
- * @param filename name of the file to create.
- */
- static File distributionFile(final String rootDirectory, final String filename) throws MojoExecutionException {
- final File outDirectory = new File(new File(rootDirectory, TARGET_DIRECTORY), DISTRIBUTION_DIRECTORY);
- if (!outDirectory.isDirectory()) {
- if (!outDirectory.mkdir()) {
- throw new MojoExecutionException("Cannot create the \"" + DISTRIBUTION_DIRECTORY + "\" directory.");
- }
- }
- return new File(outDirectory, filename);
- }
-}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/JarCollector.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/JarCollector.java
deleted file mode 100644
index 367f86a204..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/maven/JarCollector.java
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * 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.sis.internal.maven;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Files;
-import java.nio.file.FileSystemException;
-import java.util.Set;
-import java.util.LinkedHashSet;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-import org.apache.maven.project.MavenProject;
-import org.apache.maven.artifact.Artifact;
-
-import static org.apache.sis.internal.maven.Filenames.*;
-
-
-/**
- * Collects <code>.jar</code> files in a single "{@code target/binaries}" directory.
- * Dependencies are collected as well, except if already presents. This mojo uses hard links
- * on platforms that support them. If hard links are not supported, then this mojo will instead
- * creates a "{@code target/binaries/other_dependencies.txt}" file listing the dependencies.
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since 0.3
- */
-@Mojo(name = "collect-jars",
- defaultPhase = LifecyclePhase.PACKAGE,
- requiresDependencyResolution = ResolutionScope.RUNTIME)
-public final class JarCollector extends AbstractMojo implements FileFilter {
- /**
- * The Maven project running this plugin.
- */
- @Parameter(property="project", required=true, readonly=true)
- private MavenProject project;
-
- /**
- * The root directory (without the "<code>target/binaries</code>" sub-directory) where JARs
- * are to be copied. It should be the directory of the root <code>pom.xml</code>.
- */
- @Parameter(property="session.executionRootDirectory", required=true)
- private String rootDirectory;
-
- /**
- * Invoked by reflection for creating the MOJO.
- */
- public JarCollector() {
- }
-
- /**
- * Copies the {@code *.jar} files to the collect directory.
- *
- * @throws MojoExecutionException if the plugin execution failed.
- */
- @Override
- public void execute() throws MojoExecutionException {
- if (rootDirectory == null || rootDirectory.startsWith("${")) {
- getLog().warn("Unresolved directory: " + rootDirectory);
- return;
- }
- /*
- * Now collects the JARs.
- */
- try {
- collect();
- } catch (IOException e) {
- throw new MojoExecutionException("Error collecting the JAR files.", e);
- }
- }
-
- /**
- * Implementation of the {@link #execute()} method.
- */
- private void collect() throws MojoExecutionException, IOException {
- /*
- * Make sure that we are collecting the JAR file from a module which produced
- * such file. Some modules use pom packaging, which do not produce any JAR file.
- */
- final File jarFile = getProjectFile();
- if (jarFile == null) {
- return;
- }
- /*
- * Get the "target" directory of the parent pom.xml and make sure it exists.
- */
- File collect = new File(rootDirectory, TARGET_DIRECTORY);
- if (!collect.exists()) {
- if (!collect.mkdir()) {
- throw new MojoExecutionException("Failed to create \"" + TARGET_DIRECTORY + "\" directory.");
- }
- }
- if (collect.getCanonicalFile().equals(jarFile.getParentFile().getCanonicalFile())) {
- /*
- * The parent's directory is the same one than this module's directory.
- * In other words, this plugin is not executed from the parent POM. Do
- * not copy anything, since this is not the place where we want to
- * collect the JAR files.
- */
- return;
- }
- /*
- * Creates a "binaries" subdirectory inside the "target" directory, then copy the
- * JAR file compiled by Maven. If an JAR file already existed, it will be deleted.
- */
- collect = new File(collect, BINARIES_DIRECTORY);
- if (!collect.exists()) {
- if (!collect.mkdir()) {
- throw new MojoExecutionException("Failed to create \"" + BINARIES_DIRECTORY + "\" directory.");
- }
- }
- File copy = new File(collect, jarFile.getName());
- copy.delete();
- linkFileToDirectory(jarFile, copy);
- /*
- * Copies the dependencies.
- */
- final Set<Artifact> dependencies = project.getArtifacts();
- if (dependencies != null) {
- for (final Artifact artifact : dependencies) {
- final String scope = artifact.getScope();
- if (scope != null && // Maven 2.0.6 bug?
- (scope.equalsIgnoreCase(Artifact.SCOPE_COMPILE) ||
- scope.equalsIgnoreCase(Artifact.SCOPE_RUNTIME)))
- {
- final File file = artifact.getFile();
- if (file != null) { // I'm not sure why the file is sometimes null.
- copy = new File(collect, getFinalName(file, artifact));
- if (!copy.exists()) {
- /*
- * Copies the dependency only if it was not already copied. Note that
- * the module's JAR was copied unconditionally above (because it may
- * be the result of a new compilation).
- */
- linkFileToDirectory(file, copy);
- }
- }
- }
- }
- }
- }
-
- /**
- * Filters the content of the "target" directory in order to keep only the project
- * build result. We scan the directory because the final name may be different than
- * the actual file name, because a classifier may have been added to the name.
- *
- * <p>The {@code .jar} extension is not quite appropriate for source and Javadoc files;
- * a better extension would be {@code .zip}. Unfortunately, the {@code .jar} extension
- * for those content is a very common practice, so we have to filter them.</p>
- */
- @Override
- public boolean accept(final File pathname) {
- final String name = pathname.getName();
- return name.endsWith(".jar") &&
- !name.endsWith("-tests.jar") &&
- !name.endsWith("-sources.jar") &&
- !name.endsWith("-javadoc.jar") &&
- name.startsWith(project.getBuild().getFinalName()) &&
- pathname.isFile();
- }
-
- /**
- * Returns the JAR file, or {@code null} if none.
- * In case of doubt, conservatively returns {@code null}.
- */
- private File getProjectFile() {
- final File[] files = new File(project.getBuild().getDirectory()).listFiles(this);
- if (files != null && files.length == 1) {
- return files[0];
- }
- return null;
- }
-
- /**
- * Returns the name of the given file. If the given file is a snapshot, then the
- * {@code "SNAPSHOT"} will be replaced by the timestamp if possible.
- *
- * @param file the file from which to get the filename.
- * @param artifact the artifact that produced the given file.
- * @return the filename to use.
- */
- private static String getFinalName(final File file, final Artifact artifact) {
- String filename = file.getName();
- final String baseVersion = artifact.getBaseVersion();
- if (baseVersion != null) {
- final int pos = filename.lastIndexOf(baseVersion);
- if (pos >= 0) {
- final String version = artifact.getVersion();
- if (version != null && !baseVersion.equals(version)) {
- filename = filename.substring(0, pos) + version + filename.substring(pos + baseVersion.length());
- }
- }
- }
- return filename;
- }
-
- /**
- * Creates a link from the given source file to the given target file.
- * On platforms that do not support links, this method rather updates
- * the {@value Filenames#OTHER_DEPENDENCIES_FILE} file.
- *
- * @param file the source file to read.
- * @param copy the destination file to create.
- */
- private static void linkFileToDirectory(final File file, final File copy) throws IOException {
- final Path source = file.toPath();
- final Path target = copy.toPath();
- try {
- Files.createLink(target, source);
- return;
- } catch (UnsupportedOperationException | FileSystemException e) {
- /*
- * If hard links are not supported, edit the "other_dependencies.txt" file instead.
- * Note that a hard link may be unsupported because the source and target
- * are on different Windows drives or mount points, in which case we get
- * a FileSystemException instead of UnsupportedOperationException.
- */
- }
- /*
- * If we cannot use hard links, creates or updates a "target/other_dependencies.txt" file instead.
- * This file will contains the list of all dependencies, without duplicated values.
- */
- final File dependenciesFile = new File(copy.getParentFile(), OTHER_DEPENDENCIES_FILE);
- final Set<String> dependencies = loadDependencyList(dependenciesFile);
- if (dependencies.add(file.getPath())) {
- // Save the dependencies list only if it has been modified.
- try (BufferedWriter out = new BufferedWriter(new FileWriter(dependenciesFile))) {
- for (final String dependency : dependencies) {
- out.write(dependency);
- out.newLine();
- }
- }
- }
- }
-
- /**
- * Loads the {@value #OTHER_DEPENDENCIES_FILE} from the given directory, if it exists.
- * Otherwise returns an empty but modifiable set. This method is invoked on platforms
- * that do not support hard links.
- */
- private static Set<String> loadDependencyList(final File dependenciesFile) throws IOException {
- final Set<String> dependencies = new LinkedHashSet<>();
- if (dependenciesFile.exists()) {
- try (BufferedReader in = new BufferedReader(new FileReader(dependenciesFile))) {
- String line;
- while ((line = in.readLine()) != null) {
- line = line.trim();
- if (!line.isEmpty()) {
- dependencies.add(line);
- }
- }
- }
- }
- return dependencies;
- }
-}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/JavaMaker.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/JavaMaker.java
deleted file mode 100644
index 7fd86e08a8..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/JavaMaker.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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.sis.internal.unopkg;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Files;
-import java.nio.file.FileVisitResult;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.attribute.BasicFileAttributes;
-
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-
-
-/**
- * Compiles Java interfaces from OpenOffice IDL files.
- *
- * <p>In an ideal world, this plugin would execute {@code idlc} on the {@code *.idl} files,
- * then {@code regmerge} on the generated {@code *.urd} files,
- * then {@code javamaker} on the generated {@code *.rdb} files.
- * However, since the above mentioned tools are native and would require a manual installation
- * on every developer machine, current version just copies a pre-compiled class file.
- * This copy must occurs after the compilation phase (in order to overwrite the files generated
- * by {@code javac}), which is why the usual Maven resources mechanism doesn't fit.
- *
- * @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
- * @since 0.8
- */
-@Mojo(name = "javamaker", defaultPhase = LifecyclePhase.PROCESS_CLASSES)
-public final class JavaMaker extends AbstractMojo {
- /**
- * Base directory of the module to compile.
- */
- @Parameter(property="basedir", required=true, readonly=true)
- private String baseDirectory;
-
- /**
- * Directory where the output Java files will be located.
- */
- @Parameter(property="project.build.outputDirectory", required=true, readonly=true)
- private String outputDirectory;
-
- /**
- * Invoked by reflection for creating the MOJO.
- */
- public JavaMaker() {
- }
-
- /**
- * Copies the {@code .class} files generated by OpenOffice.
- *
- * @throws MojoExecutionException if the plugin execution failed.
- */
- @Override
- public void execute() throws MojoExecutionException {
- final Copier c = new Copier(baseDirectory, outputDirectory);
- try {
- c.run();
- } catch (IOException e) {
- throw new MojoExecutionException("Failed to copy *.class files.", e);
- }
- getLog().info("[geotk-unopkg] Copied " + c.count + " pre-compiled class files.");
- }
-
- /**
- * Copies {@code *.class} files from source directory to output directory.
- * The output directory should already exist. It should be the case if all
- * sources files have been compiled before this method is invoked.
- */
- private static final class Copier extends SimpleFileVisitor<Path> {
- /**
- * The root of source and target directories. Files below {@code source} will be copied
- * with identical path (relative to {@code source}) under {@code target} directory.
- */
- private final Path source, target;
-
- /**
- * Number of files copied.
- */
- int count;
-
- /**
- * Creates a new copier.
- *
- * @param baseDirectory base directory of the module to compile.
- * @param outputDirectory directory where the output Java files will be located.
- */
- Copier(final String baseDirectory, final String outputDirectory) {
- source = Path.of(baseDirectory).resolve(UnoPkg.SOURCE_DIRECTORY);
- target = Path.of(outputDirectory);
- }
-
- /**
- * Executes the copy operation.
- */
- void run() throws IOException {
- Files.walkFileTree(source, this);
- }
-
- /**
- * Determines whether the given directory should be visited.
- * This method skips hidden directories.
- */
- @Override
- public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) {
- return dir.getFileName().toString().startsWith(".") ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
- }
-
- /**
- * Invoked for a file in a directory. This method creates the directory if it does not exist
- * and performs the actual copy operation.
- */
- @Override
- public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
- final String filename = file.getFileName().toString();
- if (filename.endsWith(".class") || filename.endsWith(".CLASS")) {
- final Path dst = target.resolve(source.relativize(file)).normalize();
- if (!dst.startsWith(target)) {
- throw new IOException("Unexpected target path: " + dst);
- }
- Files.createDirectories(dst.getParent());
- Files.copy(file, dst, StandardCopyOption.REPLACE_EXISTING);
- count++;
- }
- return FileVisitResult.CONTINUE;
- }
- }
-}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/UnoPkg.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/UnoPkg.java
deleted file mode 100644
index 7ff07b06af..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/UnoPkg.java
+++ /dev/null
@@ -1,295 +0,0 @@
-/*
- * 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.sis.internal.unopkg;
-
-import java.io.*;
-import java.util.Enumeration;
-import java.util.zip.CRC32;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
-
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.project.MavenProject;
-import org.apache.maven.artifact.Artifact;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.ResolutionScope;
-
-
-/**
- * Creates an {@code .oxt} package for <a href="http://www.openoffice.org">OpenOffice.org</a> addins.
- *
- * @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
- * @since 0.8
- */
-@Mojo(name = "unopkg", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
-public final class UnoPkg extends AbstractMojo implements FilenameFilter {
- /**
- * The subdirectory (relative to {@link #baseDirectory}) where the UNO files are expected.
- */
- static final String SOURCE_DIRECTORY = "src/main/unopkg";
-
- /**
- * Module to decompress. We inflate the {@value} module because Derby is much slower
- * when using an embedded database in a compressed ZIP file compared to flat storage.
- * Since the JAR files are distributed in a ZIP file anyway, inflating that file has
- * little impact on the final ZIP file size.
- */
- private static final String TO_INFLATE = "sis-embedded-data";
-
- /**
- * The encoding for text files to read and write.
- */
- private static final String ENCODING = "UTF-8";
-
- /**
- * The string to replace by the final name.
- */
- private static final String FILTERED_NAME = "${project.build.finalName}";
-
- /**
- * The string to replace by the version number.
- */
- private static final String FILTERED_VERSION = "${project.version}";
-
- /**
- * Base directory of the module to compile.
- * The UNO files are expected in the {@code "src/main/unopkg"} subdirectory.
- * The plugin will look for the {@code META-INF/manifest.xml} and {@code *.rdb} files in that directory.
- */
- @Parameter(property="basedir", required=true, readonly=true)
- private String baseDirectory;
-
- /**
- * Directory where the output {@code .oxt} file will be located.
- */
- @Parameter(property="project.build.directory", required=true, readonly=true)
- private String outputDirectory;
-
- /**
- * In {@code META-INF/manifest.xml}, all occurrences of {@code ${project.build.finalName}}
- * will be replaced by this value.
- */
- @Parameter(property="project.build.finalName", required=true, readonly=true)
- private String finalName;
-
- /**
- * In {@code description.xml}, all occurrences of {@code ${project.version}}
- * will be replaced by this value.
- */
- @Parameter(property="project.version", required=true, readonly=true)
- private String version;
-
- /**
- * The name for the {@code .oxt} file to create, without the {@code ".oxt"} filename extension.
- */
- @Parameter(property="project.build.finalName", required=true, readonly=true)
- private String oxtName;
-
- /**
- * The Maven project running this plugin.
- */
- @Parameter(property="project", required=true, readonly=true)
- private MavenProject project;
-
- /**
- * Invoked by reflection for creating the MOJO.
- */
- public UnoPkg() {
- }
-
- /**
- * Tests if a specified file should be included in a file list.
- *
- * @param directory the directory in which the file was found.
- * @param name the name of the file.
- */
- @Override
- public boolean accept(final File directory, final String name) {
- if (name.endsWith("-sources.jar") || name.endsWith("-tests.jar") || name.endsWith("-javadoc.jar")) {
- return false;
- }
- return name.endsWith(".jar") || name.endsWith(".JAR") ||
- name.endsWith(".rdb") || name.endsWith(".RDB") ||
- name.endsWith(".xml") || name.endsWith(".XML") ||
- name.endsWith(".xcu") || name.endsWith(".XCU") ||
- name.endsWith(".txt") || name.endsWith(".TXT") ||
- name.endsWith(".png") || name.endsWith(".PNG");
- }
-
- /**
- * Generates the {@code .oxt} file from all {@code .jar} files found in the target directory.
- *
- * @throws MojoExecutionException if the plugin execution failed.
- */
- @Override
- public void execute() throws MojoExecutionException {
- final String manifestName = "META-INF/manifest.xml";
- final File sourceDirectory = new File(baseDirectory, SOURCE_DIRECTORY);
- final File outputDirectory = new File(this.outputDirectory);
- final File zipFile = new File(outputDirectory, oxtName + ".oxt");
- final File manifestFile = new File(sourceDirectory, manifestName);
- final File[] jars = outputDirectory.listFiles(this);
- final File[] rdbs = sourceDirectory.listFiles(this);
- try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile))) {
- out.setLevel(9);
- if (manifestFile.isFile()) {
- copyFiltered(manifestFile, out, manifestName);
- }
- /*
- * Copies files from "unopkg" source directory. This include binary files like RDB,
- * but also XML file. We apply filtering on the "description.xml" file in order to
- * set the version number automatically.
- */
- for (final File rdb : rdbs) {
- final String name = rdb.getName();
- if (name.endsWith(".xml")) {
- copyFiltered(rdb, out, name);
- } else {
- copy(rdb, out);
- }
- }
- /*
- * Copies the JAR for this module and any additional JARs provided in the output directory.
- */
- for (final File jar : jars) {
- copy(jar, out);
- }
- /*
- * Copies the dependencies.
- */
- for (final Artifact artifact : project.getArtifacts()) {
- final String scope = artifact.getScope();
- if (Artifact.SCOPE_COMPILE.equalsIgnoreCase(scope) ||
- Artifact.SCOPE_RUNTIME.equalsIgnoreCase(scope))
- {
- final File file = artifact.getFile();
- final String name = file.getName();
- if (name.startsWith(TO_INFLATE)) {
- copyInflated(file, out);
- } else {
- copy(file, out);
- }
- }
- }
- } catch (IOException e) {
- throw new MojoExecutionException("Error creating the oxt file.", e);
- }
- }
-
- /**
- * Copies the content of the specified binary file to the specified output stream.
- *
- * @param file the regular file to copy inside the ZIP file.
- * @param bundle the ZIP file where to copy the given regular file.
- */
- private static void copy(final File file, final ZipOutputStream bundle) throws IOException {
- final String name = file.getName();
- final ZipEntry entry = new ZipEntry(name);
- if (name.endsWith(".png")) {
- final long size = file.length();
- entry.setMethod(ZipOutputStream.STORED);
- entry.setSize(size);
- entry.setCompressedSize(size);
- entry.setCrc(getCRC32(file));
- }
- bundle.putNextEntry(entry);
- try (InputStream in = new FileInputStream(file)) {
- in.transferTo(bundle);
- }
- bundle.closeEntry();
- }
-
- /**
- * Copies the content of the specified ASCII file to the specified output stream.
- *
- * @param file the regular file to copy inside the ZIP file.
- * @param bundle the ZIP file where to copy the given regular file.
- * @param name the ZIP entry name.
- */
- private void copyFiltered(final File file, final ZipOutputStream bundle, final String name) throws IOException {
- final ZipEntry entry = new ZipEntry(name);
- bundle.putNextEntry(entry);
- final Writer writer = new OutputStreamWriter(bundle, ENCODING);
- try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), ENCODING))) {
- String line;
- while ((line = in.readLine()) != null) {
- line = line.replace(FILTERED_NAME, finalName);
- line = line.replace(FILTERED_VERSION, version);
- writer.write(line);
- writer.write('\n');
- }
- }
- writer.flush();
- bundle.closeEntry();
- }
-
- /**
- * Copies a JAR file in the given ZIP file, but without compression for the files
- * in {@code SIS_DATA} directory.
- *
- * @param file the JAR file to copy.
- * @param bundle destination where to copy the JAR file.
- */
- private static void copyInflated(final File file, final ZipOutputStream bundle) throws IOException {
- final ZipEntry entry = new ZipEntry(file.getName());
- bundle.putNextEntry(entry);
- final ZipOutputStream out = new ZipOutputStream(bundle);
- out.setLevel(9);
- try (ZipFile in = new ZipFile(file)) {
- final Enumeration<? extends ZipEntry> entries = in.entries();
- while (entries.hasMoreElements()) {
- final ZipEntry inEntry = entries.nextElement();
- final String name = inEntry.getName();
- final ZipEntry outEntry = new ZipEntry(name);
- if (name.startsWith("SIS_DATA")) {
- final long size = inEntry.getSize();
- outEntry.setMethod(ZipOutputStream.STORED);
- outEntry.setSize(size);
- outEntry.setCompressedSize(size);
- outEntry.setCrc(inEntry.getCrc());
- }
- try (InputStream inStream = in.getInputStream(inEntry)) {
- out.putNextEntry(outEntry);
- inStream.transferTo(out);
- out.closeEntry();
- }
- }
- }
- out.finish();
- bundle.closeEntry();
- }
-
- /**
- * Computes CRC32 for the given file.
- */
- private static long getCRC32(final File file) throws IOException {
- final CRC32 crc = new CRC32();
- try (InputStream in = new FileInputStream(file)) {
- final byte[] buffer = new byte[4*1024];
- int length;
- while ((length = in.read(buffer)) >= 0) {
- crc.update(buffer, 0, length);
- }
- }
- return crc.getValue();
- }
-}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/package-info.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/package-info.java
deleted file mode 100644
index ae5e6e1317..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/unopkg/package-info.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.
- */
-
-
-/**
- * Creates a {@code .oxt} file for <a href="http://www.openoffice.org">OpenOffice.org</a> add-in.
- * Those files are basically ZIP files with {@code META-INF/manifest.xml} and {@code .rdb} entries.
- * The principle is similar to Java JAR files.
- *
- * <p>This Maven plugin has be written for building the Apache SIS add-in for OpenOffice.org,
- * but can also be used for other projects. See the Apache SIS add-in module for an example
- * about how a module can be configured in order to use this Maven plugin.</p>
- *
- * <h2>Usage</h2>
- * Below is an example of {@code pom.xml} build configuration.
- * The following strings need to be replaced:
- *
- * <ul>
- * <li>{@code com.myproject.Registration} —
- * The fully qualified class name of your class which contain the {@code __getServiceFactory(…)}
- * and {@code __writeRegistryServiceInfo(…)} public static methods.</li>
- * <li>{@code myAddinFilename} —
- * The final filename, without the {@code .oxt} extension.</li>
- * </ul>
- *
- * <h2>Maven project file</h2>
- * {@snippet lang="xml" :
- * <dependencies>
- * <!-- Put all your project dependencies here, including transitive dependencies. -->
- * </dependencies>
- *
- * <build>
- * <plugins>
- * <!-- Add a manifest entry for add-ins registration in OpenOffice -->
- * <plugin>
- * <groupId>org.apache.maven.plugins</groupId>
- * <artifactId>maven-jar-plugin</artifactId>
- * <configuration>
- * <archive>
- * <manifestEntries>
- * <RegistrationClassName>
- * com.myproject.Registration
- * </RegistrationClassName>
- * </manifestEntries>
- * </archive>
- * </configuration>
- * </plugin>
- *
- * <!-- Create the oxt file. -->
- * <plugin>
- * <groupId>org.apache.sis.core</groupId>
- * <artifactId>sis-build-helper</artifactId>
- * <configuration>
- * <oxtName>myAddinFilename</oxtName>
- * </configuration>
- * <executions>
- * <execution>
- * <goals>
- * <goal>javamaker</goal>
- * <goal>unopkg</goal>
- * </goals>
- * </execution>
- * </executions>
- * </plugin>
- * </plugins>
- * </build>
- * }
- *
- * @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
- * @since 0.8
- */
-package org.apache.sis.internal.unopkg;
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/ResourceCompilerMojo.java b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/ResourceCompilerMojo.java
deleted file mode 100644
index 826374573d..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/ResourceCompilerMojo.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * 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.sis.util.resources;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.util.Arrays;
-import java.util.List;
-
-import org.apache.maven.model.Resource;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Component;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.plugins.annotations.LifecyclePhase;
-import org.apache.maven.project.MavenProject;
-import org.codehaus.plexus.util.Scanner;
-import org.sonatype.plexus.build.incremental.BuildContext;
-
-import static org.apache.sis.util.resources.IndexedResourceCompiler.JAVA_EXT;
-import static org.apache.sis.util.resources.IndexedResourceCompiler.PROPERTIES_EXT;
-
-
-/**
- * Compiles the international resources that are found in the module from which this mojo is invoked.
- * See the <code>sis-build-helper</code> module for more information.
- *
- * @author Martin Desruisseaux (Geomatys)
- * @author Olivier Nouguier (Geomatys)
- * @version 0.7
- * @since 0.3
- */
-@Mojo(name = "compile-resources", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
-public class ResourceCompilerMojo extends AbstractMojo implements FilenameFilter {
- /**
- * Pattern to filter properties files that were modified.
- */
- private static final String[] PROPERTIES_PATTERN = new String[] {"**/*.properties"};
-
- /**
- * Project information (name, version, URL).
- */
- @Parameter(property="project", required=true, readonly=true)
- private MavenProject project;
-
- /**
- * A handler for the Eclipse workspace, used for declaring new resources.
- * When Maven is run from the command line, this object does nothing.
- *
- * @see <a href="http://wiki.eclipse.org/M2E_compatible_maven_plugins">M2E compatible maven plugins</a>
- */
- @Component
- private BuildContext buildContext;
-
- /**
- * The source directories containing the sources to be compiled.
- */
- @Parameter(property="project.compileSourceRoots", required=true, readonly=true)
- private List<String> compileSourceRoots;
-
- /**
- * Directory containing the generated class files.
- *
- * <p><b>Note:</b> at the time of writing, we found no well-established convention for generated resources.
- * The conventions that we found were rather for generated sources. In the latter case, the conventions use
- * a different directory for each Maven plugin, e.g. <code>"generated-sources/xxx"</code>. But in our case
- * (for resources), such separation seems of limited use since the resources are copied verbatim in the JAR
- * file, so preventing clash in the <code>generated-resources</code> directory would not prevent clash in
- * the JAR file anyway.</p>
- */
- @Parameter(defaultValue="${project.build.directory}/generated-resources", required=true)
- private File outputDirectory;
-
- /**
- * The <code>compileSourceRoots</code> named "java" as a <code>File</code>.
- */
- private File javaDirectoryFile;
-
- /**
- * Constructs a new resource compiler MOJO.
- */
- public ResourceCompilerMojo() {
- }
-
- /**
- * Executes the mojo.
- *
- * @throws MojoExecutionException if the plugin execution failed.
- */
- @Override
- public void execute() throws MojoExecutionException {
- final boolean isIncremental = buildContext.isIncremental();
- declareOutputDirectory();
-
- int errors = 0;
- for (final String sourceDirectory : compileSourceRoots) {
- final File directory = new File(sourceDirectory);
- if (directory.getName().equals("java")) {
- /*
- * Check if we can skip the resources compilation (Eclipse environment only).
- *
- * Scanner.getIncludedFiles() returns an array of modified files. For now we ignore the array
- * content and unconditionally re-compile all resource files as soon as at least one file has
- * been modified. This is okay for now since changes in resource files are rare and compiling
- * them is very fast.
- */
- if (isIncremental) {
- Scanner scanner = buildContext.newScanner(directory);
- scanner.setIncludes(PROPERTIES_PATTERN);
- scanner.scan();
- final String[] includedFiles = scanner.getIncludedFiles();
- if (includedFiles == null || includedFiles.length == 0) {
- continue;
- }
- }
- javaDirectoryFile = directory;
- errors += processAllResourceDirectories(directory);
- buildContext.refresh(outputDirectory);
- }
- }
- if (errors != 0) {
- throw new ResourceCompilerException(String.valueOf(errors) + " errors in resources bundles.");
- }
- }
-
- /**
- * Declares {@link #outputDirectory} as resource, for inclusion by Maven in the JAR file.
- */
- private void declareOutputDirectory() {
- final Resource resource = new Resource();
- resource.setDirectory(outputDirectory.getPath());
- project.addResource(resource);
- }
-
- /**
- * Recursively scans the directories and find all Java classes having a property files of the same name.
- * Then invokes the resource compiler for those files.
- */
- private int processAllResourceDirectories(final File directory) throws ResourceCompilerException {
- int errors = 0;
- final File[] subdirs = directory.listFiles();
- if (subdirs != null) { // Appears to be sometimes null with auto-generated sub-directories.
- for (final File subdir : subdirs) {
- if (subdir.isDirectory()) {
- File[] resourcesToProcess = subdir.listFiles(this);
- int count = filterLanguages(resourcesToProcess);
- if (count != 0) {
- count = toJavaSourceFiles(resourcesToProcess, count);
- if (count != 0) {
- resourcesToProcess = Arrays.copyOf(resourcesToProcess, count);
- errors += new Compiler(resourcesToProcess).run();
- }
- }
- errors += processAllResourceDirectories(subdir);
- }
- }
- }
- return errors;
- }
-
- /**
- * Accepts all {@code "*.properties"} files.
- *
- * @param directory the directory.
- * @param name the file name.
- * @return {@code true} if the given file is a property file.
- */
- @Override
- public final boolean accept(final File directory, final String name) {
- return name.endsWith(PROPERTIES_EXT);
- }
-
- /**
- * Retains only the properties files which seems to be about internationalized resources.
- * For example if the given array contains the following files:
- * <ul>
- * <li>{@code "Errors.properties"}</li>
- * <li>{@code "Errors_en.properties"}</li>
- * <li>{@code "Errors_fr.properties"}</li>
- * <li>{@code "Messages.properties"}</li>
- * <li>{@code "Messages_en.properties"}</li>
- * <li>{@code "Messages_fr.properties"}</li>
- * <li>{@code "NotAnInternationalResource.properties"}</li>
- * </ul>
- *
- * Then this method will retain the following files:
- * <ul>
- * <li>{@code "Errors.properties"}</li>
- * <li>{@code "Messages.properties"}</li>
- * </ul>
- *
- * @param resourcesToProcess the files to filter. This array will be overwritten in-place.
- * @return number of valid elements in the {@code resourcesToProcess} after this method completion.
- */
- static int filterLanguages(final File[] resourcesToProcess) {
- int count = 0;
- if (resourcesToProcess != null) {
- Arrays.sort(resourcesToProcess);
- for (int i=0; i<resourcesToProcess.length;) {
- final File file = resourcesToProcess[i];
- String name = file.getName();
- name = name.substring(0, name.length() - PROPERTIES_EXT.length()) + '_';
- final int fileIndex = i;
- while (++i < resourcesToProcess.length) {
- if (!resourcesToProcess[i].getName().startsWith(name)) {
- break;
- }
- }
- /*
- * Accepts the property file only if we found at least one language.
- * Example: "Messages.properties" and "Messages_en.properties".
- */
- if (i - fileIndex >= 2) {
- resourcesToProcess[count++] = file;
- }
- }
- }
- return count;
- }
-
- /**
- * Converts the given property files into Java source file, provided that the latter exists.
- * The given array is overwritten in place.
- *
- * @param resourcesToProcess the filtered resource files, as returned by {@link #filterLanguages(File[])}.
- * @param count number of valid elements in {@code resourcesToProcess}.
- * @return number of valid elements after this method completion.
- */
- private static int toJavaSourceFiles(final File[] resourcesToProcess, final int count) {
- int n = 0;
- for (int i=0; i<count; i++) {
- File file = resourcesToProcess[i];
- String name = file.getName();
- name = name.substring(0, name.length() - PROPERTIES_EXT.length());
- name += JAVA_EXT;
- file = new File(file.getParentFile(), name);
- if (file.isFile()) {
- resourcesToProcess[n++] = file;
- }
- }
- return n;
- }
-
- /**
- * A resource compiler that delegates the messages to the Mojo logger.
- */
- private final class Compiler extends IndexedResourceCompiler {
- Compiler(File[] resourcesToProcess) {
- super(javaDirectoryFile, outputDirectory, resourcesToProcess);
- }
-
- /**
- * Logs the given message at the {@code INFO} level.
- */
- @Override
- protected void info(final String message) {
- getLog().info(message);
- }
-
- /**
- * Logs the given message at the {@code WARNING} level.
- */
- @Override
- protected void warning(final String message) {
- getLog().warn(message);
- }
- }
-}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/package-info.txt b/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/package-info.txt
deleted file mode 100644
index a0130859a7..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/util/resources/package-info.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-This directory has no "package-info.java" file
-because it is defined in the sis-utility module.
diff --git a/buildSrc/src/org.apache.sis.buildtools/test/org/apache/sis/buildtools/resources/IndexedResourceCompilerTest.java b/buildSrc/src/org.apache.sis.buildtools/test/org/apache/sis/buildtools/resources/IndexedResourceCompilerTest.java
new file mode 100644
index 0000000000..b845e934df
--- /dev/null
+++ b/buildSrc/src/org.apache.sis.buildtools/test/org/apache/sis/buildtools/resources/IndexedResourceCompilerTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.sis.buildtools.resources;
+
+import java.io.File;
+import java.util.Map;
+import java.util.TreeMap;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * Tests {@link IndexedResourceCompiler}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @author Quentin Bialota (Geomatys)
+ * @version 1.4
+ * @since 0.5
+ */
+public class IndexedResourceCompilerTest {
+ /**
+ * Tests {@link ResourceCompiler#filterLanguages(File[])}.
+ */
+ @Test
+ public void testFilterLanguages() {
+ final var resourcesToProcess = new TreeMap<File,Boolean>();
+ for (final String file : new String[] {
+ "Errors_en.properties",
+ "Messages_fr.properties",
+ "Errors.properties",
+ "Errors_fr.properties",
+ "ShallIgnore.properties",
+ "Messages_en.properties",
+ "Messages.properties"})
+ {
+ assertNull(resourcesToProcess.put(new File(file), Boolean.FALSE));
+ }
+ IndexedResourceCompiler.filterLanguages(resourcesToProcess, false);
+ final var it = resourcesToProcess.entrySet().iterator();
+ Map.Entry<File,Boolean> entry;
+
+ entry = it.next(); assertEquals("Errors.properties", entry.getKey().getName()); assertTrue (entry.getValue());
+ entry = it.next(); assertEquals("Messages.properties", entry.getKey().getName()); assertTrue (entry.getValue());
+ entry = it.next(); assertEquals("ShallIgnore.properties", entry.getKey().getName()); assertFalse(entry.getValue());
+ assertFalse(it.hasNext());
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/test/org/apache/sis/util/resources/ResourceCompilerMojoTest.java b/buildSrc/src/org.apache.sis.buildtools/test/org/apache/sis/util/resources/ResourceCompilerMojoTest.java
deleted file mode 100644
index 0eb7ab9dee..0000000000
--- a/buildSrc/src/org.apache.sis.buildtools/test/org/apache/sis/util/resources/ResourceCompilerMojoTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.sis.util.resources;
-
-import java.io.File;
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-
-/**
- * Tests {@link ResourceCompilerMojo}.
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 0.5
- * @since 0.5
- */
-public class ResourceCompilerMojoTest {
- /**
- * Tests {@link ResourceCompilerMojo#filterLanguages(File[])}.
- */
- @Test
- public void testFilterLanguages() {
- final File[] files = {
- new File("Errors_en.properties"),
- new File("Messages_fr.properties"),
- new File("Errors.properties"),
- new File("Errors_fr.properties"),
- new File("ShallIgnore.properties"),
- new File("Messages_en.properties"),
- new File("Messages.properties")
- };
- assertEquals(2, ResourceCompilerMojo.filterLanguages(files));
- assertEquals("Errors.properties", files[0].getName());
- assertEquals("Messages.properties", files[1].getName());
- }
-}
diff --git a/endorsed/build.gradle.kts b/endorsed/build.gradle.kts
new file mode 100644
index 0000000000..28a3b4e04f
--- /dev/null
+++ b/endorsed/build.gradle.kts
@@ -0,0 +1,392 @@
+/*
+ * 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.
+ */
+group = "org.apache.sis"
+version = "2.0-SNAPSHOT"
+
+/*
+ * "org.apache.sis.buildtools" is a custom Gradle plugin for building a project with Module Source Hierarchy
+ * as specified in https://docs.oracle.com/en/java/javase/20/docs/specs/man/javac.html#directory-hierarchies
+ * The expected hierarchy is:
+ *
+ * endorsed
+ * ├─ build
+ * └─ src
+ * ├─ org.apache.sis.metadata
+ * │ ├─ main
+ * │ │ ├─ module-info.java
+ * │ │ └─ org/apache/sis/metadata/…
+ * │ └─ test
+ * │ └─ org/apache/sis/metadata/…
+ * ├─ org.apache.sis.referencing
+ * │ ├─ main
+ * │ │ ├─ module-info.java
+ * │ │ └─ org/apache/sis/referencing/…
+ * │ └─ test
+ * │ └─ org/apache/sis/referencing/…
+ * └─ etc.
+ */
+plugins {
+ `java-library`
+ `maven-publish`
+ id("org.apache.sis.buildtools")
+}
+
+/*
+ * All JAR files that may potentially be used by a module.
+ * For each Apache SIS module, the dependencies actually
+ * used are declared in the `module-info.java` file.
+ */
+dependencies {
+ // Mandatory dependencies
+ api(libs.units)
+ api(libs.geoapi)
+ api(libs.jaxb.api)
+
+ // Optional dependencies
+ runtimeOnly(libs.jaxb.impl)
+ compileOnly(libs.jts.core)
+ compileOnly(libs.esri.geometry)
+ compileOnly(libs.libreoffice)
+ compileOnly(libs.ucar)
+ compileOnly(libs.aws.s3)
+ compileOnly(drivers.postgres)
+
+ // Test dependencies
+ testImplementation(tests.junit4)
+ testImplementation(tests.geoapi)
+ testImplementation(tests.jama)
+ testImplementation(tests.geographiclib)
+ testImplementation(libs.jts.core) // We need to repeat optional dependencies.
+ testImplementation(libs.esri.geometry) // Idem.
+ testImplementation(libs.libreoffice)
+ testImplementation(libs.ucar)
+ testImplementation(libs.aws.s3)
+ testImplementation(drivers.derby.tools)
+ testImplementation(drivers.derby.core)
+ testImplementation(drivers.postgres)
+ testImplementation(drivers.hsql)
+ testImplementation(drivers.h2)
+
+ // For test execution
+ testRuntimeOnly(tests.junit)
+ testRuntimeOnly(tests.junitLauncher)
+ testRuntimeOnly(tests.slf4j)
+}
+
+/*
+ * Compile main and tests classes from Module Source Hierarchy.
+ * The test classes require some additional dependencies declared in the `org.apache.sis.test` module.
+ */
+var srcDir = file("src") // Must be the same as the hard-coded value in `BuildHelper.java`.
+tasks.compileJava {
+ options.release.set(11) // The version of both Java source code and compiled byte code.
+}
+tasks.compileTestJava {
+ srcDir.list().forEach {
+ addRead(options.compilerArgs, it, "org.apache.sis.test,junit")
+ }
+ addExportForTests(options.compilerArgs)
+}
+
+/*
+ * Adds a JVM argument for adding dependencies to a module.
+ * This is for dependencies not declared in `module-info`
+ * but needed for test compilation or test execution.
+ */
+fun addRead(args : MutableList<String>, module : String, dependencies : String) {
+ args.add("--add-reads")
+ args.add(module + '=' + dependencies)
+}
+
+/*
+ * Adds a JVM argument for making an internal package accessible to another module.
+ * This is for making internal packages accessible to JUnit or to some test classes
+ * defined in other modules.
+ */
+fun addExport(args : MutableList<String>, module : String, pkg : String, consumers : String) {
+ args.add("--add-exports")
+ args.add(module + '/' + pkg + '=' + consumers)
+}
+
+/*
+ * Add compiler and runtime options for patching the Apache SIS main modules with the test classes.
+ * The same options are required for both compiling and executing the tests.
+ */
+fun addExportForTests(args : MutableList<String>) {
+ var allModules = srcDir.list().joinToString(separator=",")
+
+ // ――――――――――――― Module name ――――――――――――――――――――――― Package to export ―――――――――――――――
+ addExport(args, "org.apache.sis.util", "org.apache.sis.test",
+ allModules)
+
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.test.mock",
+ "org.apache.sis.referencing")
+
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.test.sql",
+ "org.apache.sis.referencing," +
+ "org.apache.sis.storage.sql")
+
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.test.xml",
+ "org.apache.sis.referencing," +
+ "org.apache.sis.profile.france")
+
+ addExport(args, "org.apache.sis.storage", "org.apache.sis.test.storage",
+ "org.apache.sis.storage.geotiff," +
+ "org.apache.sis.storage.netcdf")
+ /*
+ * Some test classes need access to more internal packages than requested by the main classes.
+ * The following lines may need to be edited when export statements are added or removed in a
+ * module-info.java file of main source code, or when a test class starts using or stop using
+ * an internal API.
+ */
+ // ――――――――――――― Module name ――――――――――――――――――――――― Package to export ―――――――――――――――
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.internal.metadata",
+ "org.apache.sis.referencing.gazetteer")
+
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.metadata.xml",
+ "org.apache.sis.storage," +
+ "org.apache.sis.console")
+
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.internal.xml",
+ "org.apache.sis.storage.geotiff")
+
+ addExport(args, "org.apache.sis.metadata", "org.apache.sis.internal.jaxb.gcx",
+ "org.apache.sis.referencing")
+
+ addExport(args, "org.apache.sis.feature", "org.apache.sis.internal.feature.jts",
+ "org.apache.sis.storage.sql," +
+ "org.apache.sis.portrayal")
+
+ addExport(args, "org.apache.sis.storage", "org.apache.sis.internal.storage",
+ "org.apache.sis.portrayal")
+}
+
+/*
+ * Discover and execute JUnit-based tests.
+ */
+tasks.test {
+ val args = mutableListOf("-enableassertions")
+ addExportForTests(args)
+ /*
+ * Dependencies needed only for executing the tests (not for compilation).
+ */
+ addRead(args, "org.apache.sis.metadata", "org.apache.derby.tools,com.h2database,org.hsqldb")
+ addRead(args, "org.apache.sis.referencing", "jama,GeographicLib.Java")
+ addRead(args, "org.apache.sis.storage", "esri.geometry.api")
+ addRead(args, "org.apache.sis.storage.xml", "esri.geometry.api")
+ /*
+ * JAXB implementation needs permissions to do reflection.
+ */
+ args.add("--add-opens")
+ args.add("org.apache.sis.metadata/org.apache.sis.test.mock=jakarta.xml.bind")
+ setAllJvmArgs(args)
+ /*
+ * Request full stack trace in case of failure. The short stack trace is not enough
+ * for identifying the failure cause.
+ */
+ testLogging {
+ events("FAILED", "STANDARD_OUT", "STANDARD_ERROR")
+ setExceptionFormat("FULL")
+ }
+}
+
+/*
+ * Javadoc generation. Most of the configuration is specified in the `../buildSrc` plugin.
+ * This file contains only the subproject-dependent parts.
+ */
+tasks.javadoc {
+ setTitle("Apache SIS ${version} API")
+}
+
+/*
+ * Configuration of some META-INF/MANIFEST.MF attributes.
+ * Other attributes are hard-coded in `../buildSrc`.
+ */
+tasks.jar {
+ manifest {
+ attributes["Main-Class"] = "org.apache.sis.console.Command"
+ attributes["RegistrationClassName"] = "org.apache.sis.openoffice.Registration"
+ }
+}
+
+/*
+ * Configuration of the modules to deploy as Maven artifacts.
+ */
+publishing {
+ publications {
+ create<MavenPublication>("util") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-utility"
+ artifact(file("${buildDir}/libs/org.apache.sis.util.jar"))
+ pom {
+ name = "Apache SIS utilities"
+ description = "Units of measurement and miscellaneous utility methods required by Apache SIS."
+ }
+ }
+ create<MavenPublication>("metadata") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-metadata"
+ artifact(file("${buildDir}/libs/org.apache.sis.metadata.jar"))
+ pom {
+ name = "Apache SIS metadata"
+ description = "Implementations of metadata derived from ISO 19115. " +
+ "This module provides both an implementation of the metadata interfaces defined in GeoAPI, " +
+ "and a framework for handling those metadata through Java reflection."
+ }
+ }
+ create<MavenPublication>("referencing") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-referencing"
+ artifact(file("${buildDir}/libs/org.apache.sis.referencing.jar"))
+ pom {
+ name = "Apache SIS referencing"
+ description = "Implementations of Coordinate Reference Systems (CRS), " +
+ "conversion and transformation services derived from ISO 19111."
+ }
+ }
+ create<MavenPublication>("referencing.gazetteer") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-referencing-by-identifiers"
+ artifact(file("${buildDir}/libs/org.apache.sis.referencing.gazetteer.jar"))
+ pom {
+ name = "Apache SIS referencing by geographic identifiers"
+ description = "Implementations of Spatial Reference Systems using Geographic Identifiers " +
+ "gazetteer services derived from ISO 19112."
+ }
+ }
+ create<MavenPublication>("feature") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-feature"
+ artifact(file("${buildDir}/libs/org.apache.sis.feature.jar"))
+ pom {
+ name = "Apache SIS features"
+ description = "Representations of geographic features. " +
+ "Includes access to both vector and raster data."
+ }
+ }
+ create<MavenPublication>("portrayal") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-portrayal"
+ artifact(file("${buildDir}/libs/org.apache.sis.portrayal.jar"))
+ pom {
+ name = "Apache SIS portrayal"
+ description = "Symbology and map representations, together with a rendering engine for display."
+ }
+ }
+ create<MavenPublication>("storage") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-storage"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.jar"))
+ pom {
+ name = "Apache SIS common storage"
+ description = "Provides the interfaces and base classes to be implemented by various storage formats."
+ }
+ }
+ create<MavenPublication>("storage.xml") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-xmlstore"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.xml.jar"))
+ pom {
+ name = "Apache SIS XML storage"
+ description = "Read and write files in the GPX format."
+ }
+ }
+ create<MavenPublication>("storage.netcdf") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-netcdf"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.netcdf.jar"))
+ pom {
+ name = "Apache SIS netCDF storage"
+ description = "Bridge between netCDF Climate and Forecast (CF) convention and ISO 19115 metadata."
+ }
+ }
+ create<MavenPublication>("storage.geotiff") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-geotiff"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.geotiff.jar"))
+ pom {
+ name = "Apache SIS GeoTIFF storage"
+ description = "Cloud Optimized GeoTIFF reader and bridge to ISO 19115 metadata."
+ }
+ }
+ create<MavenPublication>("storage.earthobservation") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-earth-observation"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.earthobservation.jar"))
+ pom {
+ name = "Apache SIS Earth Observation storage"
+ description = "Read a directory of Landsat files as a single resource."
+ }
+ }
+ create<MavenPublication>("storage.sql") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-sqlstore"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.sql.jar"))
+ pom {
+ name = "Apache SIS SQL storage"
+ description = "Read and write features from SQL databases."
+ }
+ }
+ create<MavenPublication>("cloud.aws") {
+ groupId = "org.apache.sis.cloud"
+ artifactId = "sis-cloud-aws"
+ artifact(file("${buildDir}/libs/org.apache.sis.cloud.aws.jar"))
+ pom {
+ name = "Apache SIS storage from Amazon AWS S3"
+ description = "Provides access to Amazon AWS S3 storage from Apache SIS data stores."
+ }
+ }
+ create<MavenPublication>("profile.france") {
+ groupId = "org.apache.sis.profiles"
+ artifactId = "sis-french-profile"
+ artifact(file("${buildDir}/libs/org.apache.sis.profile.france.jar"))
+ pom {
+ name = "Apache SIS French profiles"
+ description = "Extensions to ISO-19115 metadata mandated by the French government."
+ }
+ }
+ create<MavenPublication>("profile.japan") {
+ groupId = "org.apache.sis.profiles"
+ artifactId = "sis-japan-profile"
+ artifact(file("${buildDir}/libs/org.apache.sis.profile.japan.jar"))
+ pom {
+ name = "Apache SIS Japanese profiles"
+ description = "Extensions to netCDF reader for file formats published by Japanese Aerospace Exploration Agency (JAXA)."
+ }
+ }
+ create<MavenPublication>("console") {
+ groupId = "org.apache.sis.application"
+ artifactId = "sis-console"
+ artifact(file("${buildDir}/libs/org.apache.sis.console.jar"))
+ pom {
+ name = "Apache SIS console"
+ description = "Console application."
+ }
+ }
+ create<MavenPublication>("openoffice") {
+ groupId = "org.apache.sis.application"
+ artifactId = "sis-openoffice"
+ artifact(file("${buildDir}/libs/org.apache.sis.openoffice.jar"))
+ pom {
+ name = "Bridges to Apache OpenOffice or LibreOffice"
+ description = "Provides some Apache SIS functionalities as Apache OpenOffice addins. " +
+ "For example, addins provide coordinate operation services as formulas " +
+ "inside the Calc spreadsheet."
+ }
+ }
+ }
+}
diff --git a/endorsed/src/org.apache.sis.openoffice/bundle/build-instruction.html b/endorsed/src/org.apache.sis.openoffice/bundle/build-instruction.html
index 2ebc99958a..82684a9f30 100644
--- a/endorsed/src/org.apache.sis.openoffice/bundle/build-instruction.html
+++ b/endorsed/src/org.apache.sis.openoffice/bundle/build-instruction.html
@@ -77,9 +77,9 @@ idlc -C -cid -w -I $OO_SDK_HOME/idl org/apache/sis/openoffice/*.idl</pre></block
most users will not have the necessary installation for producing those files themselves
(including the <code>*.class</code> files, which is not the usual <code>javac</code> output).
</p>
-<blockquote><pre>regmerge ../unopkg/sis.rdb /UCR org/apache/sis/openoffice/*.urd
+<blockquote><pre>regmerge ../bundle/sis.rdb /UCR org/apache/sis/openoffice/*.urd
rm org/apache/sis/openoffice/*.urd
-cd ../unopkg
+cd ../bundle
javamaker -BUCR sis.rdb $OFFICE_BASE_HOME/program/types.rdb -Torg.apache.sis.openoffice.XReferencing</pre></blockquote>
diff --git a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/XReferencing.java b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/XReferencing.java
index 19a5d171d4..c1957b5175 100644
--- a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/XReferencing.java
+++ b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/XReferencing.java
@@ -29,7 +29,7 @@ import com.sun.star.lang.IllegalArgumentException;
* This source file exists only for convenience: for avoiding that IDEs report errors, and opportunistically for
* Javadoc purpose. But the {@code XReferencing.class} file compiled from this source file <strong>must</strong>
* be overwritten by the {@code XReferencing.class} file generated by {@code javamaker}.
- * See {@code application/sis-openoffice/src/main/unopkg/build-instruction.html} for more information.</p>
+ * See {@code endorsed/src/org.apache.sis.openoffice/bundle/build-instruction.html} for more information.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.8
diff --git a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/package-info.java b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/package-info.java
index f1aed8371d..8e0786e80c 100644
--- a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/package-info.java
+++ b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/package-info.java
@@ -22,7 +22,7 @@
*
* <p>Note that the {@code X*.class} files to be included in the final JAR file
* shall be compiled from the {@code X*.idl} files, not from the {@code X*.java} files.
- * See {@code application/sis-openoffice/src/main/unopkg/build-instruction.html} for more information.</p>
+ * See {@code endorsed/src/org.apache.sis.openoffice/bundle/build-instruction.html} for more information.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.8
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java b/endorsed/src/org.apache.sis.test/test/module-info.java
similarity index 76%
copy from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
copy to endorsed/src/org.apache.sis.test/test/module-info.java
index 5817ad614a..565c714bb9 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
+++ b/endorsed/src/org.apache.sis.test/test/module-info.java
@@ -15,13 +15,15 @@
* limitations under the License.
*/
-
/**
- * Javadoc taglets for processing custom tags in SIS source code comments,
- * together with doclet performing post-processing after Javadoc generation.
+ * Additional dependencies needed for Apache SIS tests.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since 0.5
+ * @version 1.4
+ * @since 1.4
*/
-package org.apache.sis.internal.doclet;
+module org.apache.sis.test {
+ requires transitive org.opengis.geoapi.conformance;
+ requires transitive org.apache.derby.tools;
+ requires transitive org.hsqldb;
+}
diff --git a/incubator/build.gradle.kts b/incubator/build.gradle.kts
new file mode 100644
index 0000000000..be719b4b30
--- /dev/null
+++ b/incubator/build.gradle.kts
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+group = "org.apache.sis"
+version = "2.0-SNAPSHOT"
+
+/*
+ * "org.apache.sis.buildtools" is a custom Gradle plugin for building a project with Module Source Hierarchy.
+ * See the Gradle build script in the `endorsed` directory for more information.
+ */
+plugins {
+ `java-library`
+ `maven-publish` // For local deployment only. Not to be published on Maven Central.
+ id("org.apache.sis.buildtools")
+ antlr
+}
+
+/*
+ * All JAR files that may potentially be used by a module.
+ * For each Apache SIS module, the dependencies actually
+ * used are declared in the `module-info.java` file.
+ */
+dependencies {
+ // Mandatory dependencies
+ api(libs.units)
+ api(libs.geoapi)
+ api(libs.jaxb.api) // Transitive dependency.
+ api(libs.jts.core) // Should be an optional dependency.
+ api(libs.esri.geometry) // Idem.
+ api(files("../endorsed/build/classes/java/main/org.apache.sis.util"))
+ api(files("../endorsed/build/classes/java/main/org.apache.sis.metadata"))
+ api(files("../endorsed/build/classes/java/main/org.apache.sis.referencing"))
+ api(files("../endorsed/build/classes/java/main/org.apache.sis.feature"))
+ api(files("../endorsed/build/classes/java/main/org.apache.sis.storage"))
+
+ // Test dependencies
+ testImplementation(tests.geoapi)
+ testImplementation(tests.junit4)
+ testRuntimeOnly (tests.junit)
+ testRuntimeOnly (tests.junitLauncher)
+ /*
+ * Dependencies used only by incubated modules. The dependencies are not declared
+ * in the global `settings.gradle.kts` because incubated modules are not released.
+ */
+ implementation(group = "org.antlr", name = "antlr4-maven-plugin", version = "4.11.1")
+ compileOnly (group = "jakarta.servlet", name = "jakarta.servlet-api", version = "6.0.0")
+ compileOnly (group = "org.osgi", name = "osgi.core", version = "8.0.0")
+ antlr (group = "org.antlr", name = "antlr4", version = "4.11.1")
+}
+
+/*
+ * Generate ANTLR4 source files, then compile main and tests classes from Module Source Hierarchy.
+ * The test classes require some additional dependencies declared in the `org.apache.sis.test` module.
+ *
+ * Note: as of Gradle 8.2, the ANTLR task does not work well with arbitrary source directories.
+ * We have to keep default convention even if it does not fit well in Module Source Hierarchy.
+ * Another problem is that we didn't found the right compiler options for combining a directory
+ * of generated sources with the main sources in a Module Source Hierarchy, so we have to write
+ * the output directly in main source directory. This is hopefully temporary, since we hope to
+ * replace ANTLR generated code by hand-written code in a future version.
+ */
+var srcDir = file("src") // Must be the same as the hard-coded value in `BuildHelper.java`.
+tasks.generateGrammarSource {
+ setOutputDirectory(file("${srcDir}/org.apache.sis.cql/main"))
+}
+tasks.compileJava {
+ dependsOn(":endorsed:compileJava")
+ options.release.set(11) // The version of both Java source code and compiled byte code.
+}
+tasks.compileTestJava {
+ srcDir.list().forEach {
+ addRead(options.compilerArgs, it, "org.apache.sis.test,junit")
+ }
+}
+
+/*
+ * Adds a JVM argument for adding dependencies to a module.
+ * This is for dependencies not declared in `module-info`
+ * but needed for test compilation or test execution.
+ */
+fun addRead(args : MutableList<String>, module : String, dependencies : String) {
+ args.add("--add-reads")
+ args.add(module + '=' + dependencies)
+}
+
+/*
+ * Discover and execute JUnit-based tests.
+ */
+tasks.test {
+ val args = mutableListOf("-enableassertions")
+ setAllJvmArgs(args)
+ testLogging {
+ events("FAILED", "STANDARD_OUT", "STANDARD_ERROR")
+ setExceptionFormat("FULL")
+ }
+}
+
+/*
+ * Configuration of the modules to deploy as Maven artifacts.
+ */
+publishing {
+ publications {
+ create<MavenPublication>("cql") {
+ groupId = "org.apache.sis.core"
+ artifactId = "sis-cql"
+ artifact(file("${buildDir}/libs/org.apache.sis.cql.jar"))
+ pom {
+ name = "Apache SIS CQL"
+ description = "CQL parser."
+ }
+ }
+ create<MavenPublication>("storage.shapefile") {
+ groupId = "org.apache.sis.storage"
+ artifactId = "sis-shapefile"
+ artifact(file("${buildDir}/libs/org.apache.sis.storage.shapefile.jar"))
+ pom {
+ name = "Apache SIS Shapefile storage"
+ description = "Read and write files in the Shapefile format."
+ }
+ }
+ create<MavenPublication>("webapp") {
+ groupId = "org.apache.sis.application"
+ artifactId = "sis-webapp"
+ artifact(file("${buildDir}/libs/org.apache.sis.webapp.jar"))
+ pom {
+ name = "Apache SIS web services layer"
+ description = "Placeholder for future developments."
+ }
+ }
+ }
+}
diff --git a/incubator/src/main/antlr/org/apache/sis/internal/cql/CQL.g4 b/incubator/src/main/antlr/org/apache/sis/internal/cql/CQL.g4
index c2e3fcf3b5..6860141030 100644
--- a/incubator/src/main/antlr/org/apache/sis/internal/cql/CQL.g4
+++ b/incubator/src/main/antlr/org/apache/sis/internal/cql/CQL.g4
@@ -19,6 +19,9 @@ grammar CQL;
options {
language = Java;
}
+@header {
+ package org.apache.sis.internal.cql;
+}
//-----------------------------------------------------------------//
// LEXER
diff --git a/incubator/src/org.apache.sis.cql/main/org/apache/sis/internal/cql/.gitignore b/incubator/src/org.apache.sis.cql/main/org/apache/sis/internal/cql/.gitignore
new file mode 100644
index 0000000000..4948df6ac7
--- /dev/null
+++ b/incubator/src/org.apache.sis.cql/main/org/apache/sis/internal/cql/.gitignore
@@ -0,0 +1,7 @@
+# Generated by ANTLR
+*.interp
+*.tokens
+CQLLexer.java
+CQLParser.java
+CQLListener.java
+CQLBaseListener.java
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java b/incubator/src/org.apache.sis.test/test/module-info.java
similarity index 79%
copy from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
copy to incubator/src/org.apache.sis.test/test/module-info.java
index 5817ad614a..a4dea60674 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
+++ b/incubator/src/org.apache.sis.test/test/module-info.java
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-
/**
- * Javadoc taglets for processing custom tags in SIS source code comments,
- * together with doclet performing post-processing after Javadoc generation.
+ * Additional dependencies needed for Apache SIS tests.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since 0.5
+ * @version 1.4
+ * @since 1.4
*/
-package org.apache.sis.internal.doclet;
+module org.apache.sis.test {
+ requires transitive org.opengis.geoapi.conformance;
+}
diff --git a/optional/build.gradle.kts b/optional/build.gradle.kts
new file mode 100644
index 0000000000..997b286b37
--- /dev/null
+++ b/optional/build.gradle.kts
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+group = "org.apache.sis"
+version = "2.0-SNAPSHOT"
+
+val pathToFX = System.getenv("PATH_TO_FX")
+if (pathToFX == null) {
+ throw GradleException("For compiling the Apache SIS optional sub-project, the PATH_TO_FX environment variable must be set.")
+}
+if (!File(pathToFX, "javafx.base.jar").isFile()) {
+ throw GradleException("The directory specified by the PATH_TO_FX environment variable shall contain \"javafx.*.jar\" files.")
+}
+
+/*
+ * "org.apache.sis.buildtools" is a custom Gradle plugin for building a project with Module Source Hierarchy.
+ * See the Gradle build script in the `endorsed` directory for more information.
+ */
+plugins {
+ `java-library`
+ `maven-publish`
+ id("org.apache.sis.buildtools")
+}
+
+/*
+ * All JAR files that may potentially be used by a module.
+ * For each Apache SIS module, the dependencies actually
+ * used are declared in the `module-info.java` file.
+ */
+var mainDepPath = file("../endorsed/build/classes/java/main")
+var testDepPath = file("../endorsed/build/classes/java/test")
+dependencies {
+ // Mandatory dependencies
+ api (libs.units)
+ api (libs.geoapi)
+ implementation(libs.jaxb.api) // Transitive dependency.
+ runtimeOnly (libs.jaxb.impl)
+ api (files(File(pathToFX, "javafx.base.jar")))
+ api (files(File(pathToFX, "javafx.graphics.jar")))
+ api (files(File(pathToFX, "javafx.controls.jar")))
+ api (files(File(pathToFX, "javafx.web.jar")))
+ runtimeOnly (files(File(pathToFX, "javafx.media.jar")))
+ api (files("${mainDepPath}/org.apache.sis.util"))
+ api (files("${mainDepPath}/org.apache.sis.metadata"))
+ api (files("${mainDepPath}/org.apache.sis.referencing"))
+ implementation(files("${mainDepPath}/org.apache.sis.referencing.gazetteer"))
+ api (files("${mainDepPath}/org.apache.sis.feature"))
+ api (files("${mainDepPath}/org.apache.sis.storage"))
+ implementation(files("${mainDepPath}/org.apache.sis.storage.xml"))
+ api (files("${mainDepPath}/org.apache.sis.portrayal"))
+
+ // Test dependencies
+ testImplementation(tests.geoapi)
+ testImplementation(tests.junit4)
+ testRuntimeOnly (tests.junit)
+ testRuntimeOnly (tests.junitLauncher)
+}
+
+/*
+ * Compile main and tests classes from Module Source Hierarchy.
+ * The test classes require some additional dependencies declared in the `org.apache.sis.test` module.
+ */
+var srcDir = file("src") // Must be the same as the hard-coded value in `BuildHelper.java`.
+tasks.compileJava {
+ dependsOn(":endorsed:compileJava")
+ options.release.set(16) // The version of both Java source code and compiled byte code.
+}
+tasks.compileTestJava {
+ patchForTests(options.compilerArgs);
+ srcDir.list().forEach {
+ addRead(options.compilerArgs, it, "org.apache.sis.test,junit")
+ }
+}
+
+/*
+ * Adds a JVM argument for adding dependencies to a module.
+ * This is for dependencies not declared in `module-info`
+ * but needed for test compilation or test execution.
+ */
+fun addRead(args : MutableList<String>, module : String, dependencies : String) {
+ args.add("--add-reads")
+ args.add(module + '=' + dependencies)
+}
+
+/*
+ * Adds a JVM argument for making an internal package accessible to another module.
+ * This is for making internal packages accessible to JUnit or to some test classes.
+ */
+fun addExport(args : MutableList<String>, module : String, pkg : String, consumers : String) {
+ args.add("--add-exports")
+ args.add(module + '/' + pkg + '=' + consumers)
+}
+
+/*
+ * Adds a JVM argument for patching a module with test classes.
+ */
+fun patchModuleWithTests(args : MutableList<String>, module : String) {
+ args.add("--patch-module")
+ args.add("${module}=${testDepPath}/${module}")
+}
+
+/*
+ * Add compiler and runtime options for patching the Apache SIS main modules with the test classes.
+ * The same options are required for both compiling and executing the tests.
+ */
+fun patchForTests(args : MutableList<String>) {
+ patchModuleWithTests(args, "org.apache.sis.util")
+ patchModuleWithTests(args, "org.apache.sis.feature")
+ addExport(args, "org.apache.sis.util", "org.apache.sis.test", "org.apache.sis.gui")
+}
+
+/*
+ * Discover and execute JUnit-based tests.
+ */
+tasks.test {
+ val args = mutableListOf("-enableassertions")
+ patchForTests(args);
+ addRead (args, "org.apache.sis.util", "ALL-UNNAMED")
+ addExport(args, "org.apache.sis.util", "org.apache.sis.test", "ALL-UNNAMED")
+ addExport(args, "org.apache.sis.gui", "org.apache.sis.internal.gui", "ALL-UNNAMED")
+ setAllJvmArgs(args)
+ testLogging {
+ events("FAILED", "STANDARD_OUT", "STANDARD_ERROR")
+ setExceptionFormat("FULL")
+ }
+}
+
+/*
+ * Configuration of some META-INF/MANIFEST.MF attributes.
+ * Other attributes are hard-coded in `../buildSrc`.
+ */
+tasks.jar {
+ manifest {
+ attributes["Main-Class"] = "org.apache.sis.gui.DataViewer"
+ }
+}
+
+/*
+ * Configuration of the modules to deploy as Maven artifacts.
+ */
+publishing {
+ publications {
+ create<MavenPublication>("gui") {
+ groupId = "org.apache.sis.application"
+ artifactId = "sis-javafx"
+ artifact(file("${buildDir}/libs/org.apache.sis.gui.jar"))
+ pom {
+ name = "Apache SIS application for JavaFX (optional)"
+ description = "Client application for JavaFX. " +
+ "This module requires the JavaFX environment to be pre-installed. " +
+ "See https://openjfx.io/openjfx-docs/#install-javafx for details."
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java b/optional/src/org.apache.sis.test/test/module-info.java
similarity index 79%
rename from buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
rename to optional/src/org.apache.sis.test/test/module-info.java
index 5817ad614a..a4dea60674 100644
--- a/buildSrc/src/org.apache.sis.buildtools/main/org/apache/sis/internal/doclet/package-info.java
+++ b/optional/src/org.apache.sis.test/test/module-info.java
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-
/**
- * Javadoc taglets for processing custom tags in SIS source code comments,
- * together with doclet performing post-processing after Javadoc generation.
+ * Additional dependencies needed for Apache SIS tests.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
- * @since 0.5
+ * @version 1.4
+ * @since 1.4
*/
-package org.apache.sis.internal.doclet;
+module org.apache.sis.test {
+ requires transitive org.opengis.geoapi.conformance;
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000000..120ea09901
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+rootProject.name = "Apache SIS on GeoAPI 4.0"
+val geoapiVersion = "4.0-SNAPSHOT"
+
+/*
+ * The sub-projects to include in the build.
+ * They are directory names relative to this file.
+ */
+include("endorsed")
+include("incubator")
+if (System.getenv("PATH_TO_FX") != null) {
+ include("optional")
+}
+
+/*
+ * Central place where to declare the repositories inherited by all sub-projects.
+ * Sub-projects can override by declaring their own `repositories` block.
+ */
+dependencyResolutionManagement {
+ /*
+ * The repositories from where to download the JAR files declared in the `dependencies` section.
+ * The Maven local repository should be avoided for security reasons (Gradle has its own cache).
+ */
+ repositories {
+ mavenCentral()
+ mavenLocal() // For GeoAPI SNAPSHOT only, which are built locally.
+ maven {
+ url = uri("https://artifacts.unidata.ucar.edu/repository/unidata-releases")
+ content {
+ includeGroup("edu.ucar") // Restrict usage to those dependencies.
+ }
+ mavenContent {
+ releasesOnly()
+ }
+ }
+ }
+ /*
+ * All dependencies used by sub-projects, together with their versions.
+ * For most dependencies (except tests), we assume semantic versioning.
+ * The preferred versions are the ones that we tested.
+ */
+ versionCatalogs {
+ create("libs") {
+ library("geoapi", "org.opengis", "geoapi-pending") .version {strictly(geoapiVersion)}
+ library("units", "javax.measure", "unit-api") .version {strictly("[2.1, 3.0["); prefer("2.1.3")}
+ library("jaxb.api", "jakarta.xml.bind", "jakarta.xml.bind-api").version {strictly("[4.0, 5.0["); prefer("4.0.0")}
+ library("jaxb.impl", "org.glassfish.jaxb", "jaxb-runtime") .version {strictly("[4.0, 5.0["); prefer("4.0.2")}
+ library("jts.core", "org.locationtech.jts", "jts-core") .version {strictly("[1.15, 2.0["); prefer("1.19.0")}
+ library("esri.geometry", "com.esri.geometry", "esri-geometry-api") .version {strictly("[2.0, 3.0["); prefer("2.2.4")}
+ library("libreoffice", "org.libreoffice", "libreoffice") .version {strictly("[7.0, 8.0["); prefer("7.3.6")}
+ library("ucar", "edu.ucar", "cdm-core") .version {strictly("[5.0, 6.0["); prefer("5.5.3")}
+ library("aws.s3", "software.amazon.awssdk", "s3") .version {strictly("[2.0, 3.0["); prefer("2.18.40")}
+ }
+ create("tests") {
+ library("geoapi", "org.opengis", "geoapi-conformance") .version {strictly(geoapiVersion)}
+ library("junit4", "junit", "junit") .version {strictly("4.13.2")}
+ library("junit", "org.junit.vintage", "junit-vintage-engine") .version {strictly("5.9.3")}
+ library("junitLauncher", "org.junit.platform", "junit-platform-launcher").version {strictly("1.9.3")}
+ library("jama", "gov.nist.math", "jama") .version {strictly("1.0.3")}
+ library("geographiclib", "net.sf.geographiclib", "GeographicLib-Java") .version {strictly("2.0")}
+ library("slf4j", "org.slf4j", "slf4j-jdk14").version {
+ prefer("1.7.28") // Should match the version used by UCAR.
+ }
+ }
+ create("drivers") {
+ version("derby") {
+ strictly("[10.0, 11.0[")
+ prefer("10.15.2.0") // 10.15 is the last series compatible with JDK 11.
+ }
+ library("derby.core", "org.apache.derby", "derby") .versionRef("derby")
+ library("derby.tools", "org.apache.derby", "derbytools") .versionRef("derby")
+ library("postgres", "org.postgresql", "postgresql") .version {prefer("42.6.0")}
+ library("hsql", "org.hsqldb", "hsqldb") .version {strictly("[2.0, 3.0["); prefer("2.7.2")}
+ library("h2", "com.h2database", "h2") .version {strictly("[2.0, 3.0["); prefer("2.2.220")}
+ }
+ }
+}