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");
+    }
+}

Reply via email to