This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push:
new c5345d0ba0 Model Dialect (fixes #10552) (#13613)
c5345d0ba0 is described below
commit c5345d0ba0f1fab3d69b4339b7e7958bcc7c5939
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Oct 30 21:37:51 2026 +0100
Model Dialect (fixes #10552) (#13613)
Introduces the infrastructure for symmetric dialect translation between
Maven model formats:
- Dialect: new extensible enum (like Language) with built-in XML value
- DialectRegistry: ExtensibleEnumRegistry for Dialect values
- DialectProvider: SPI to extend Dialect enum with custom values
- ModelWriter: SPI symmetric counterpart to ModelParser
- ModelWriterException: checked exception for write failures
- ModelDialectProvider: SPI composing a (ModelParser, ModelWriter) pair for
a Dialect
- ModelDialectManager: service to read/write models across dialects
- DefaultModelDialectManager: implementation wiring XML dialect via
ModelXmlFactory
- DefaultDialectRegistry: DI-wired registry in ExtensibleEnumRegistries
The built-in XML dialect is always available. Extensions may register
additional
dialects by providing a ModelDialectProvider via DI.
This enables tools like maveniverse/mason to implement dialect conversion
(e.g. XML <-> YAML) without throwaway code.
Closes #9976
---
.../main/java/org/apache/maven/api/Dialect.java | 69 +++++++++
.../java/org/apache/maven/api/ExtensibleEnums.java | 20 +++
.../apache/maven/api/services/DialectRegistry.java | 28 ++++
.../maven/api/services/ModelDialectManager.java | 86 ++++++++++++
.../org/apache/maven/api/spi/DialectProvider.java | 34 +++++
.../apache/maven/api/spi/ModelDialectProvider.java | 66 +++++++++
.../java/org/apache/maven/api/spi/ModelParser.java | 5 +
.../java/org/apache/maven/api/spi/ModelWriter.java | 88 ++++++++++++
.../apache/maven/api/spi/ModelWriterException.java | 43 ++++++
.../maven/impl/ExtensibleEnumRegistries.java | 43 ++++++
.../impl/model/DefaultModelDialectManager.java | 156 +++++++++++++++++++++
.../maven/impl/DefaultDialectRegistryTest.java | 156 +++++++++++++++++++++
.../impl/model/DefaultModelDialectManagerTest.java | 96 +++++++++++++
13 files changed, 890 insertions(+)
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Dialect.java
b/api/maven-api-core/src/main/java/org/apache/maven/api/Dialect.java
new file mode 100644
index 0000000000..1578e2ef94
--- /dev/null
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Dialect.java
@@ -0,0 +1,69 @@
+/*
+ * 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.maven.api;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Immutable;
+import org.apache.maven.api.annotations.Nonnull;
+
+import static org.apache.maven.api.ExtensibleEnums.dialect;
+
+/**
+ * Dialect.
+ * <p>
+ * This extensible enum has one defined value, {@link #XML},
+ * but can be extended by registering a {@code
org.apache.maven.api.spi.DialectProvider}.
+ * <p>
+ * Implementation must have {@code equals()} and {@code hashCode()}
implemented, so implementations of this interface
+ * can be used as keys.
+ *
+ * @since 4.1.0
+ */
+@Experimental
+@Immutable
+@SuppressWarnings("checkstyle:InterfaceIsType")
+public interface Dialect extends ExtensibleEnum {
+
+ /**
+ * The "xml" dialect, the essence of Maven.
+ */
+ Dialect XML = dialect("xml");
+
+ /**
+ * Creates a new {@code Dialect} instance with the specified identifier.
+ * <p>
+ * The returned instance properly implements {@link Object#equals(Object)}
and
+ * {@link Object#hashCode()} based on the {@code id}, so it can safely be
used
+ * as a map key or in collections.
+ * <p>
+ * This factory method is intended for use by extension authors who
implement
+ * {@code org.apache.maven.api.spi.DialectProvider} or
+ * {@code org.apache.maven.api.spi.ModelDialectProvider} and need to
declare
+ * a custom {@code Dialect} constant.
+ *
+ * @param id the unique identifier for the dialect, never {@code null}
+ * @return a new {@code Dialect} instance with the given id
+ * @throws NullPointerException if {@code id} is {@code null}
+ * @since 4.1.0
+ */
+ @Nonnull
+ static Dialect of(@Nonnull String id) {
+ return dialect(id);
+ }
+}
diff --git
a/api/maven-api-core/src/main/java/org/apache/maven/api/ExtensibleEnums.java
b/api/maven-api-core/src/main/java/org/apache/maven/api/ExtensibleEnums.java
index 75892c7651..ea094849da 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/ExtensibleEnums.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ExtensibleEnums.java
@@ -33,6 +33,16 @@
*/
abstract class ExtensibleEnums {
+ /**
+ * Creates a new Dialect instance with the specified ID.
+ *
+ * @param id the identifier for the dialect
+ * @return a new Dialect instance
+ */
+ static Dialect dialect(String id) {
+ return new DefaultDialect(id);
+ }
+
/**
* Creates a new Language instance with the specified ID.
*
@@ -150,6 +160,16 @@ public Set<DependencyScope> dependencyScopes() {
}
}
+ /**
+ * Implementation of the Dialect interface.
+ */
+ private static class DefaultDialect extends DefaultExtensibleEnum
implements Dialect {
+
+ DefaultDialect(String id) {
+ super(id);
+ }
+ }
+
/**
* Implementation of the ProjectScope interface.
*/
diff --git
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/DialectRegistry.java
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DialectRegistry.java
new file mode 100644
index 0000000000..4607981e06
--- /dev/null
+++
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DialectRegistry.java
@@ -0,0 +1,28 @@
+/*
+ * 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.maven.api.services;
+
+import org.apache.maven.api.Dialect;
+
+/**
+ * Registry for {@link Dialect} extensible enum values.
+ *
+ * @since 4.1.0
+ */
+public interface DialectRegistry extends ExtensibleEnumRegistry<Dialect> {}
diff --git
a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelDialectManager.java
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelDialectManager.java
new file mode 100644
index 0000000000..6de2fbf245
--- /dev/null
+++
b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelDialectManager.java
@@ -0,0 +1,86 @@
+/*
+ * 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.maven.api.services;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.maven.api.Dialect;
+import org.apache.maven.api.Service;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.model.Model;
+
+/**
+ * The {@code ModelDialectManager} provides a mechanism for managing and
+ * translating between different dialects of Maven models.
+ * It allows reading and writing models in various dialects supported by the
system.
+ *
+ * @since 4.1.0
+ */
+@Experimental
+public interface ModelDialectManager extends Service {
+
+ /**
+ * Retrieves the set of all available dialects.
+ * The result is never {@code null} and always contains at least one
element,
+ * the core {@link Dialect#XML} dialect.
+ *
+ * @return a non-null {@link Set} of available dialects.
+ */
+ @Nonnull
+ Set<Dialect> getAvailableDialects();
+
+ /**
+ * Reads a Maven model from the specified directory using the specified
dialect.
+ * The dialect's parser is asked to {@code locate()} a file in the
directory; if none
+ * is found, an empty {@link Optional} is returned.
+ *
+ * @param dir the directory from which to read the model, must not be
{@code null}.
+ * @param dialect the dialect to be used for reading, must not be {@code
null}.
+ * @param options optional parameters for reading, may be {@code null}.
+ * @return an {@link Optional} containing the read model, or an empty
{@link Optional}
+ * if the dialect's parser finds no file in the given directory.
+ * @throws IllegalArgumentException if an unrecognized dialect is provided.
+ * A {@code ModelParserException} (from {@code maven-api-spi}) is
propagated
+ * if a model file is located but cannot be parsed.
+ */
+ @Nonnull
+ Optional<Model> readModel(@Nonnull Path dir, @Nonnull Dialect dialect,
@Nullable Map<String, ?> options);
+
+ /**
+ * Writes the given Maven model to the specified directory using the
specified dialect.
+ *
+ * @param dir the directory where the model will be written, must not be
{@code null}.
+ * @param dialect the dialect to be used for writing, must not be {@code
null}.
+ * @param model the Maven model to be written, must not be {@code null}.
+ * @param options optional parameters for writing, may be {@code null}.
+ * @return an {@link Optional} containing the path of the written file,
+ * or an empty {@link Optional} if the write operation is refused
by the implementation.
+ * @throws IllegalArgumentException if an unrecognized dialect is provided.
+ * A {@code ModelWriterException} (from {@code maven-api-spi}) is
propagated
+ * if a target path is resolved but the model cannot be written.
+ */
+ @Nonnull
+ Optional<Path> writeModel(
+ @Nonnull Path dir, @Nonnull Dialect dialect, @Nonnull Model model,
@Nullable Map<String, ?> options);
+}
diff --git
a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/DialectProvider.java
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/DialectProvider.java
new file mode 100644
index 0000000000..a4cdf8f198
--- /dev/null
+++
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/DialectProvider.java
@@ -0,0 +1,34 @@
+/*
+ * 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.maven.api.spi;
+
+import org.apache.maven.api.Dialect;
+import org.apache.maven.api.annotations.Consumer;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.di.Named;
+
+/**
+ * SPI to extend the {@link org.apache.maven.api.Dialect} extensible enum with
custom values.
+ *
+ * @since 4.1.0
+ */
+@Experimental
+@Consumer
+@Named
+public interface DialectProvider extends ExtensibleEnumProvider<Dialect> {}
diff --git
a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelDialectProvider.java
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelDialectProvider.java
new file mode 100644
index 0000000000..25a891d7ea
--- /dev/null
+++
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelDialectProvider.java
@@ -0,0 +1,66 @@
+/*
+ * 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.maven.api.spi;
+
+import org.apache.maven.api.Dialect;
+import org.apache.maven.api.annotations.Consumer;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.di.Named;
+
+/**
+ * SPI to provide a symmetric pair of {@link ModelParser} and {@link
ModelWriter} for a given {@link Dialect}.
+ * <p>
+ * Implement this interface to register support for reading and writing Maven
models in an alternative dialect.
+ * The implementation must be registered as a named DI component so that Maven
can discover it.
+ *
+ * @since 4.1.0
+ * @see ModelParser
+ * @see ModelWriter
+ * @see DialectProvider
+ */
+@Experimental
+@Consumer
+@Named
+public interface ModelDialectProvider extends SpiService {
+
+ /**
+ * The {@link Dialect} this provider handles.
+ *
+ * @return the dialect, never {@code null}
+ */
+ @Nonnull
+ Dialect getDialect();
+
+ /**
+ * The dialect-specific parser used to read models.
+ *
+ * @return the model parser, never {@code null}
+ */
+ @Nonnull
+ ModelParser getModelParser();
+
+ /**
+ * The dialect-specific writer used to write models.
+ *
+ * @return the model writer, never {@code null}
+ */
+ @Nonnull
+ ModelWriter getModelWriter();
+}
diff --git
a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java
index 9447e519f5..601cf2b8f4 100644
--- a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java
+++ b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelParser.java
@@ -33,8 +33,13 @@
/**
* The {@code ModelParser} interface is used to locate and read {@link Model}s
from the file system.
* This allows plugging in additional syntaxes for the main model read by
Maven when building a project.
+ * <p>
+ * Note: to provide a Maven model "dialect" that supports symmetric
translation, implement
+ * {@link ModelDialectProvider}, which composes a pair of parser and writer.
*
* @since 4.0.0
+ * @see ModelDialectProvider
+ * @see DialectProvider
*/
@Experimental
@Consumer
diff --git
a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriter.java
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriter.java
new file mode 100644
index 0000000000..ca83214b0f
--- /dev/null
+++ b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriter.java
@@ -0,0 +1,88 @@
+/*
+ * 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.maven.api.spi;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.maven.api.annotations.Consumer;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.model.Model;
+
+/**
+ * The {@code ModelWriter} interface is used to write {@link Model}s to the
file system.
+ * This allows plugging in additional syntaxes for the main model write.
+ * <p>
+ * Note: to provide a Maven model "dialect" that supports symmetric
translation, implement
+ * {@link ModelDialectProvider}, which composes a pair of parser and writer.
+ *
+ * @since 4.1.0
+ * @see ModelDialectProvider
+ * @see DialectProvider
+ */
+@Experimental
+@Consumer
+@Named
+public interface ModelWriter extends SpiService {
+
+ /**
+ * Targets the pom file in the given directory.
+ *
+ * @param dir the directory to target the pom for, never {@code null}
+ * @return a {@code Path} pointing to the targeted pom file,
+ * or an empty {@code Optional} if this writer refuses to target
the directory
+ */
+ @Nonnull
+ Optional<Path> target(@Nonnull Path dir);
+
+ /**
+ * Writes the given model to the specified target path.
+ * The actual file is decided by the implementation via {@link
#target(Path)}.
+ *
+ * @param target the file path to write the model to, never {@code null}
+ * @param model the model to write, never {@code null}
+ * @param options possible writing options, may be {@code null}
+ * @throws ModelWriterException if the model could not be written
+ */
+ void write(@Nonnull Path target, @Nonnull Model model, @Nullable
Map<String, ?> options)
+ throws ModelWriterException;
+
+ /**
+ * Targets and writes the model in the specified directory.
+ * This is equivalent to {@code target(dir).map(path -> { write(path,
model, options); return path; })}.
+ *
+ * @param dir the directory to target the pom for, never {@code null}
+ * @param model the model to write, never {@code null}
+ * @param options possible writing options, may be {@code null}
+ * @return an {@link Optional} containing the written file path, or empty
if this writer refused the directory
+ * @throws ModelWriterException if the model could not be written
+ */
+ @Nonnull
+ default Optional<Path> targetAndWrite(@Nonnull Path dir, @Nonnull Model
model, @Nullable Map<String, ?> options)
+ throws ModelWriterException {
+ return target(dir).map(path -> {
+ write(path, model, options);
+ return path;
+ });
+ }
+}
diff --git
a/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriterException.java
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriterException.java
new file mode 100644
index 0000000000..df951f3d11
--- /dev/null
+++
b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriterException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.maven.api.spi;
+
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.services.MavenException;
+
+/**
+ * Exception thrown when a {@link ModelWriter} fails to write a model.
+ *
+ * @since 4.1.0
+ */
+@Experimental
+public class ModelWriterException extends MavenException {
+
+ public ModelWriterException() {
+ this(null, null);
+ }
+
+ public ModelWriterException(String message) {
+ this(message, null);
+ }
+
+ public ModelWriterException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
index 5716538702..942a088417 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
@@ -18,13 +18,16 @@
*/
package org.apache.maven.impl;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.maven.api.Dialect;
import org.apache.maven.api.ExtensibleEnum;
import org.apache.maven.api.Language;
import org.apache.maven.api.PathScope;
@@ -33,12 +36,15 @@
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.SessionScoped;
import org.apache.maven.api.di.Singleton;
+import org.apache.maven.api.services.DialectRegistry;
import org.apache.maven.api.services.ExtensibleEnumRegistry;
import org.apache.maven.api.services.LanguageRegistry;
import org.apache.maven.api.services.PathScopeRegistry;
import org.apache.maven.api.services.ProjectScopeRegistry;
+import org.apache.maven.api.spi.DialectProvider;
import org.apache.maven.api.spi.ExtensibleEnumProvider;
import org.apache.maven.api.spi.LanguageProvider;
+import org.apache.maven.api.spi.ModelDialectProvider;
import org.apache.maven.api.spi.PathScopeProvider;
import org.apache.maven.api.spi.ProjectScopeProvider;
@@ -84,12 +90,49 @@ public DefaultLanguageRegistry(List<LanguageProvider>
providers) {
}
}
+ @Named
+ @SessionScoped
+ public static class DefaultDialectRegistry extends
DefaultExtensibleEnumRegistry<Dialect, DialectProvider>
+ implements DialectRegistry {
+
+ @Inject
+ public DefaultDialectRegistry(
+ List<DialectProvider> providers, List<ModelDialectProvider>
modelDialectProviders) {
+ // Collect all dialect ids already covered by DialectProvider(s)
and the built-in XML.
+ // Only seed dialects from ModelDialectProvider that are not
already registered,
+ // so that extensions which implement only ModelDialectProvider
(without a separate
+ // DialectProvider) still have their dialect appear in the
DialectRegistry.
+ super(providers, buildBuiltins(providers, modelDialectProviders));
+ }
+
+ private static Dialect[] buildBuiltins(
+ List<DialectProvider> providers, List<ModelDialectProvider>
modelDialectProviders) {
+ // Ids already covered by built-in XML and DialectProvider
contributions
+ Set<String> coveredIds = new HashSet<>();
+ coveredIds.add(Dialect.XML.id().toLowerCase(Locale.ROOT));
+ providers.stream()
+ .flatMap(p -> p.provides().stream())
+ .map(d -> d.id().toLowerCase(Locale.ROOT))
+ .forEach(coveredIds::add);
+
+ // Collect ModelDialectProvider dialects that fill gaps not
covered by DialectProvider
+ Dialect[] extra = modelDialectProviders.stream()
+ .map(ModelDialectProvider::getDialect)
+ .filter(d ->
coveredIds.add(d.id().toLowerCase(Locale.ROOT))) // add returns true if new
+ .toArray(Dialect[]::new);
+
+ // Prepend XML to the builtins; extra entries are new (not yet in
coveredIds before the filter ran)
+ return Stream.concat(Stream.of(Dialect.XML),
Stream.of(extra)).toArray(Dialect[]::new);
+ }
+ }
+
public abstract static class DefaultExtensibleEnumRegistry<
T extends ExtensibleEnum, P extends
ExtensibleEnumProvider<T>>
implements ExtensibleEnumRegistry<T> {
protected final Map<String, T> values;
+ @SafeVarargs
public DefaultExtensibleEnumRegistry(List<P> providers, T...
builtinValues) {
values = Stream.concat(Stream.of(builtinValues),
providers.stream().flatMap(p -> p.provides().stream()))
.collect(Collectors.toUnmodifiableMap(t ->
t.id().toLowerCase(Locale.ROOT), t -> t));
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelDialectManager.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelDialectManager.java
new file mode 100644
index 0000000000..758c914225
--- /dev/null
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelDialectManager.java
@@ -0,0 +1,156 @@
+/*
+ * 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.maven.impl.model;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.maven.api.Dialect;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.di.Inject;
+import org.apache.maven.api.di.Named;
+import org.apache.maven.api.di.Singleton;
+import org.apache.maven.api.model.Model;
+import org.apache.maven.api.services.ModelDialectManager;
+import org.apache.maven.api.services.Source;
+import org.apache.maven.api.services.Sources;
+import org.apache.maven.api.services.xml.ModelXmlFactory;
+import org.apache.maven.api.spi.ModelDialectProvider;
+import org.apache.maven.api.spi.ModelParser;
+import org.apache.maven.api.spi.ModelParserException;
+import org.apache.maven.api.spi.ModelWriter;
+import org.apache.maven.api.spi.ModelWriterException;
+
+/**
+ * Default implementation of {@link ModelDialectManager}.
+ * <p>
+ * Registers the built-in XML dialect and any additional dialects provided via
+ * the {@link ModelDialectProvider} SPI.
+ *
+ * @since 4.1.0
+ */
+@Named
+@Singleton
+public class DefaultModelDialectManager implements ModelDialectManager {
+
+ private final Map<Dialect, ModelDialectProvider> dialectProviders;
+
+ @Inject
+ public DefaultModelDialectManager(
+ ModelXmlFactory modelXmlFactory, List<ModelDialectProvider>
modelDialectProviders) {
+ this.dialectProviders = new HashMap<>();
+ // Built-in XML dialect
+ dialectProviders.put(Dialect.XML,
getXmlDialectProvider(modelXmlFactory));
+ // SPI-contributed dialects (may also override XML if desired)
+ for (ModelDialectProvider provider : modelDialectProviders) {
+ dialectProviders.put(provider.getDialect(), provider);
+ }
+ }
+
+ private static ModelDialectProvider getXmlDialectProvider(ModelXmlFactory
modelXmlFactory) {
+ ModelParser xmlModelParser = new ModelParser() {
+ @Override
+ public Optional<Source> locate(Path dir) {
+ if (Files.isDirectory(dir)) {
+ Path pom = dir.resolve("pom.xml");
+ if (Files.isRegularFile(pom)) {
+ return Optional.of(Sources.fromPath(pom));
+ }
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Model parse(Source source, Map<String, ?> options) throws
ModelParserException {
+ try {
+ return modelXmlFactory.read(source.getPath(), false);
+ } catch (Exception e) {
+ throw new ModelParserException(e.getMessage(), e);
+ }
+ }
+ };
+ ModelWriter xmlModelWriter = new ModelWriter() {
+ @Override
+ public Optional<Path> target(Path dir) {
+ if (Files.isDirectory(dir)) {
+ Path pom = dir.resolve("pom.xml");
+ if (!Files.isDirectory(pom)) {
+ return Optional.of(pom);
+ }
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public void write(Path target, Model model, Map<String, ?>
options) throws ModelWriterException {
+ try {
+ modelXmlFactory.write(model, target);
+ } catch (Exception e) {
+ throw new ModelWriterException(e.getMessage(), e);
+ }
+ }
+ };
+ return new ModelDialectProvider() {
+ @Override
+ public Dialect getDialect() {
+ return Dialect.XML;
+ }
+
+ @Override
+ public ModelParser getModelParser() {
+ return xmlModelParser;
+ }
+
+ @Override
+ public ModelWriter getModelWriter() {
+ return xmlModelWriter;
+ }
+ };
+ }
+
+ @Override
+ public Set<Dialect> getAvailableDialects() {
+ return Set.copyOf(dialectProviders.keySet());
+ }
+
+ @Override
+ public Optional<Model> readModel(@Nonnull Path dir, @Nonnull Dialect
dialect, Map<String, ?> options) {
+ return
requireDialectProvider(dialect).getModelParser().locateAndParse(dir, options);
+ }
+
+ @Override
+ public Optional<Path> writeModel(
+ @Nonnull Path dir, @Nonnull Dialect dialect, @Nonnull Model model,
Map<String, ?> options) {
+ return
requireDialectProvider(dialect).getModelWriter().targetAndWrite(dir, model,
options);
+ }
+
+ @Nonnull
+ private ModelDialectProvider requireDialectProvider(Dialect dialect) {
+ ModelDialectProvider provider = dialectProviders.get(dialect);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown model dialect: " +
dialect);
+ }
+ return provider;
+ }
+}
diff --git
a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultDialectRegistryTest.java
b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultDialectRegistryTest.java
new file mode 100644
index 0000000000..fd1a77ff74
--- /dev/null
+++
b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultDialectRegistryTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.maven.impl;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.maven.api.Dialect;
+import org.apache.maven.api.model.Model;
+import org.apache.maven.api.services.Source;
+import org.apache.maven.api.spi.DialectProvider;
+import org.apache.maven.api.spi.ModelDialectProvider;
+import org.apache.maven.api.spi.ModelParser;
+import org.apache.maven.api.spi.ModelWriter;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DefaultDialectRegistryTest {
+
+ //
-------------------------------------------------------------------------
+ // Helpers
+ //
-------------------------------------------------------------------------
+
+ private static DialectProvider dialectProviderFor(Dialect dialect) {
+ return () -> List.of(dialect);
+ }
+
+ private static ModelDialectProvider modelDialectProviderFor(Dialect
dialect) {
+ return new ModelDialectProvider() {
+ @Override
+ public Dialect getDialect() {
+ return dialect;
+ }
+
+ @Override
+ public ModelParser getModelParser() {
+ return new ModelParser() {
+ @Override
+ public Optional<Source> locate(Path dir) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Model parse(Source source, Map<String, ?> options) {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public ModelWriter getModelWriter() {
+ return new ModelWriter() {
+ @Override
+ public Optional<Path> target(Path dir) {
+ return Optional.empty();
+ }
+
+ @Override
+ public void write(Path target, Model model, Map<String, ?>
options) {}
+ };
+ }
+ };
+ }
+
+ private ExtensibleEnumRegistries.DefaultDialectRegistry registry(
+ List<DialectProvider> dp, List<ModelDialectProvider> mdp) {
+ return new ExtensibleEnumRegistries.DefaultDialectRegistry(dp, mdp);
+ }
+
+ //
-------------------------------------------------------------------------
+ // Tests
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void xmlIsAlwaysPresent() {
+ var reg = registry(List.of(), List.of());
+ assertTrue(reg.lookup("xml").isPresent(), "XML must always be
registered");
+ assertEquals(Dialect.XML, reg.lookup("xml").get());
+ }
+
+ @Test
+ void dialectProviderContributesDialect() {
+ Dialect yaml = Dialect.of("yaml");
+ var reg = registry(List.of(dialectProviderFor(yaml)), List.of());
+ assertTrue(reg.lookup("yaml").isPresent());
+ assertEquals(yaml, reg.lookup("yaml").get());
+ }
+
+ @Test
+ void modelDialectProviderContributesDialectWhenNoDialectProvider() {
+ // Extension implements only ModelDialectProvider, not DialectProvider.
+ // The dialect must still appear in the registry.
+ Dialect toml = Dialect.of("toml");
+ var reg = registry(List.of(), List.of(modelDialectProviderFor(toml)));
+ assertTrue(reg.lookup("toml").isPresent(), "toml dialect must be
registered via ModelDialectProvider");
+ }
+
+ @Test
+ void
modelDialectProviderDoesNotDuplicateWhenDialectProviderAlsoRegisters() {
+ // Extension implements both DialectProvider and ModelDialectProvider
for same dialect.
+ // Registry must not fail and must contain exactly one entry for
"yaml".
+ Dialect yaml = Dialect.of("yaml");
+ var reg = registry(List.of(dialectProviderFor(yaml)),
List.of(modelDialectProviderFor(yaml)));
+ assertTrue(reg.lookup("yaml").isPresent());
+ // lookup returns a single value — no exception, no duplicate
+ }
+
+ @Test
+ void lookupIsCaseInsensitive() {
+ Dialect yaml = Dialect.of("YAML");
+ var reg = registry(List.of(dialectProviderFor(yaml)), List.of());
+ assertTrue(reg.lookup("yaml").isPresent(), "lookup must be
case-insensitive");
+ assertTrue(reg.lookup("YAML").isPresent());
+ }
+
+ @Test
+ void lookupAbsentForUnregistered() {
+ var reg = registry(List.of(), List.of());
+ assertFalse(reg.lookup("nonexistent").isPresent());
+ }
+
+ @Test
+ void multipleDialectsFromModelDialectProviders() {
+ Dialect yaml = Dialect.of("yaml");
+ Dialect toml = Dialect.of("toml");
+ Dialect json = Dialect.of("json");
+ var reg = registry(
+ List.of(),
+ List.of(modelDialectProviderFor(yaml),
modelDialectProviderFor(toml), modelDialectProviderFor(json)));
+ assertTrue(reg.lookup("yaml").isPresent());
+ assertTrue(reg.lookup("toml").isPresent());
+ assertTrue(reg.lookup("json").isPresent());
+ assertTrue(reg.lookup("xml").isPresent()); // XML is always present
+ }
+}
diff --git
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelDialectManagerTest.java
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelDialectManagerTest.java
new file mode 100644
index 0000000000..9b2b409009
--- /dev/null
+++
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelDialectManagerTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.maven.impl.model;
+
+import java.util.Collections;
+
+import org.apache.maven.api.Dialect;
+import org.apache.maven.impl.DefaultModelXmlFactory;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DefaultModelDialectManagerTest {
+
+ @Test
+ void xmlDialectIsAlwaysAvailable() {
+ DefaultModelDialectManager manager =
+ new DefaultModelDialectManager(new DefaultModelXmlFactory(),
Collections.emptyList());
+ assertEquals(1, manager.getAvailableDialects().size());
+ assertTrue(manager.getAvailableDialects().contains(Dialect.XML));
+ }
+
+ @Test
+ void unknownDialectThrowsIllegalArgumentException() {
+ DefaultModelDialectManager manager =
+ new DefaultModelDialectManager(new DefaultModelXmlFactory(),
Collections.emptyList());
+ // Dialect.of() creates a proper Dialect instance with correct
equals/hashCode.
+ // Since "toml" is not registered, the manager must throw.
+ Dialect toml = Dialect.of("toml");
+ assertThrows(IllegalArgumentException.class, () ->
manager.readModel(null, toml, null));
+ }
+
+ //
-------------------------------------------------------------------------
+ // Dialect.of() contract tests
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void dialectOfReturnsNonNullWithCorrectId() {
+ Dialect yaml = Dialect.of("yaml");
+ assertNotNull(yaml);
+ assertEquals("yaml", yaml.id());
+ }
+
+ @Test
+ void dialectOfEqualsWhenSameId() {
+ Dialect a = Dialect.of("yaml");
+ Dialect b = Dialect.of("yaml");
+ assertEquals(a, b, "Two Dialect.of() with same id must be equal");
+ assertEquals(a.hashCode(), b.hashCode(), "Equal dialects must have
equal hashCodes");
+ }
+
+ @Test
+ void dialectOfNotEqualsWhenDifferentId() {
+ Dialect yaml = Dialect.of("yaml");
+ Dialect toml = Dialect.of("toml");
+ assertNotEquals(yaml, toml);
+ }
+
+ @Test
+ void dialectOfXmlEqualsDialectXmlConstant() {
+ // Dialect.of("xml") must equal Dialect.XML
+ Dialect xml = Dialect.of("xml");
+ assertEquals(Dialect.XML, xml, "Dialect.of(\"xml\") must equal
Dialect.XML");
+ assertEquals(Dialect.XML.hashCode(), xml.hashCode());
+ }
+
+ @Test
+ void dialectOfCanBeUsedAsMapKey() {
+ // Verifies that equals+hashCode are correct for map usage
+ java.util.Map<Dialect, String> map = new java.util.HashMap<>();
+ Dialect yaml1 = Dialect.of("yaml");
+ Dialect yaml2 = Dialect.of("yaml");
+ map.put(yaml1, "value");
+ assertEquals("value", map.get(yaml2), "Dialect.of() instances with
same id must map to the same key");
+ }
+}