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 00000000000..728283aec67 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Dialect.java @@ -0,0 +1,46 @@ +/* + * 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 static org.apache.maven.api.ExtensibleEnums.dialect; + +/** + * Dialect. + *

+ * This extensible enum has one defined value, {@link #XML}, + * but can be extended by registering a {@code org.apache.maven.api.spi.DialectProvider}. + *

+ * Implementation must have {@code equals()} and {@code hashCode()} implemented, so implementations of this interface + * can be used as keys. + * + * @since 4.0.0 + */ +@Experimental +@Immutable +@SuppressWarnings("checkstyle:InterfaceIsType") +public interface Dialect extends ExtensibleEnum { + + /** + * The "xml" dialect, the essence of Maven. + */ + Dialect XML = dialect("xml"); +} 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 b23e85da75d..67ce68477fe 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 @@ -26,6 +26,10 @@ abstract class ExtensibleEnums { + static Dialect dialect(String id) { + return new DefaultDialect(id); + } + static Language language(String id) { return new DefaultLanguage(id); } @@ -88,6 +92,13 @@ public Set dependencyScopes() { } } + private static class DefaultDialect extends DefaultExtensibleEnum implements Dialect { + + DefaultDialect(String id) { + super(id); + } + } + private static class DefaultProjectScope extends DefaultExtensibleEnum implements ProjectScope { DefaultProjectScope(String id) { 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 00000000000..b50c33deaa6 --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DialectRegistry.java @@ -0,0 +1,23 @@ +/* + * 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; + +public interface DialectRegistry extends ExtensibleEnumRegistry {} 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 00000000000..3c206efaf9e --- /dev/null +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelDialectManager.java @@ -0,0 +1,68 @@ +/* + * 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.Nonnull; +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.model.Model; + +/** + * Dialect manager, that offers passage between various dialects. + */ +public interface ModelDialectManager extends Service { + /** + * Returns the available dialects, never {@code null}. Result set has at least one element, the {@link org.apache.maven.api.Dialect#XML} + * which is present in core. + */ + @Nonnull + Set getAvailableDialects(); + + /** + * Reads the model from given directory in given dialect. + * + * @param dir the directory from where to read, never {@code null} + * @param dialect the dialect to use to read, never {@code null} + * @param options the options for reading + * @return optional with the model that was read or empty + * @throws IllegalArgumentException if unknown dialect was asked for + */ + @Nonnull + Optional readModel(@Nonnull Path dir, @Nonnull Dialect dialect, @Nullable Map options); + + /** + * Write the model to given directory in given dialect. + * + * @param dir the directory to where to write, never {@code null} + * @param dialect the dialect to use to write, never {@code null} + * @param model the model to write, never {@code null} + * @param options the options for writing + * @return optional with file path where write happened or empty + * @throws IllegalArgumentException if unknown dialect was asked for + */ + @Nonnull + Optional writeModel( + @Nonnull Path dir, @Nonnull Dialect dialect, @Nonnull Model model, @Nullable Map 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 00000000000..31406bfca4d --- /dev/null +++ b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/DialectProvider.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * @since 4.0.0 + */ +@Experimental +@Consumer +@Named +public interface DialectProvider extends ExtensibleEnumProvider {} 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 00000000000..f04401c0692 --- /dev/null +++ b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelDialectProvider.java @@ -0,0 +1,51 @@ +/* + * 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; + +/** + * @since 4.0.0 + */ +@Experimental +@Consumer +@Named +public interface ModelDialectProvider { + /** + * The {@link Dialect} this provider handles. + */ + @Nonnull + Dialect getDialect(); + + /** + * The dialect specific parser. + */ + @Nonnull + ModelParser getModelParser(); + + /** + * The dialect specific writer. + */ + @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 9447e519f57..39c810958ef 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. + *

+ * Note: to provide Maven model "dialect", that can have models translated from-to, implement {@link ModelDialectProvider} + * that compose pairs of parser and writer (make dialect symmetrical). * * @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 00000000000..6eef1481c4e --- /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. + *

+ * Note: to provide Maven model "dialect", that can have models translated from-to, implement {@link ModelDialectProvider} + * that compose pairs of parser and writer (make dialect symmetrical). + * + * @since 4.0.0 + * @see ModelDialectProvider + * @see DialectProvider + */ +@Experimental +@Consumer +@Named +public interface ModelWriter extends SpiService { + + /** + * Targets the pom 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 serializer refuses to target. + */ + @Nonnull + Optional target(@Nonnull Path dir); + + /** + * Write out given model into given directory. The actual file (within directory) is decided by implementation, + * and is returned upon successful write operation. + * + * @param target the file where to write out model, never {@code null}. + * @param model the model to write out. never {@code null} + * @param options possible writing options, may be {@code null} + * @throws ModelWriterException if the model could not be written + */ + @Nonnull + void write(@Nonnull Path target, @Nonnull Model model, @Nullable Map options) + throws ModelWriterException; + + /** + * Target and write out the model in the specified directory. + * This is equivalent to {@code target(dir).map(s -> write(s, model, options))}. + * + * @param dir the directory to target the pom for, never {@code null} + * @param model the model to write out, never {@code null}. + * @param options possible parsing options, may be {@code null} + * @return an optional with pom file path + * @throws ModelWriterException if the target model cannot be written + */ + @Nonnull + default Optional targetAndWrite(@Nonnull Path dir, @Nonnull Model model, @Nullable Map options) + throws ModelWriterException { + return target(dir).map(s -> { + write(s, model, options); + return s; + }); + } +} 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 00000000000..d0f34bc431e --- /dev/null +++ b/api/maven-api-spi/src/main/java/org/apache/maven/api/spi/ModelWriterException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.spi; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.services.MavenException; + +@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/maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java index f8bc69e538b..4078f58313b 100644 --- a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/ExtensibleEnumRegistries.java @@ -25,6 +25,7 @@ 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,10 +34,12 @@ 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.PathScopeProvider; @@ -73,6 +76,17 @@ public DefaultProjectScopeRegistry(List providers) { } } + @Named + @Singleton + public static class DefaultDialectRegistry extends DefaultExtensibleEnumRegistry + implements DialectRegistry { + + @Inject + public DefaultDialectRegistry(List providers) { + super(providers, Dialect.XML); + } + } + @Named @Singleton public static class DefaultLanguageRegistry extends DefaultExtensibleEnumRegistry diff --git a/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelDialectManager.java b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelDialectManager.java new file mode 100644 index 00000000000..238922d9b1f --- /dev/null +++ b/maven-api-impl/src/main/java/org/apache/maven/internal/impl/model/DefaultModelDialectManager.java @@ -0,0 +1,137 @@ +/* + * 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.internal.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.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; + +@Named +@Singleton +public class DefaultModelDialectManager implements ModelDialectManager { + private final Map dialectProviders; + + @Inject + public DefaultModelDialectManager( + ModelXmlFactory modelXmlFactory, List modelDialectProviders) { + this.dialectProviders = new HashMap<>(); + // XML + dialectProviders.put(Dialect.XML, getXmlDialectProvider(modelXmlFactory)); + // SPI TODO: (they can override XML dialect as well?) + for (ModelDialectProvider modelDialectProvider : modelDialectProviders) { + dialectProviders.put(modelDialectProvider.getDialect(), modelDialectProvider); + } + } + + private ModelDialectProvider getXmlDialectProvider(ModelXmlFactory modelXmlFactory) { + ModelParser xmlModelParser = new ModelParser() { + @Override + public Optional locate(Path dir) { + if (Files.isDirectory(dir)) { + Path pom = dir.resolve("pom.xml"); + if (Files.isRegularFile(pom)) { + return Optional.of(Source.fromPath(pom)); + } + } + return Optional.empty(); + } + + @Override + public Model parse(Source source, Map options) throws ModelParserException { + return modelXmlFactory.read(source.getPath(), false); + } + }; + ModelWriter xmlModelWriter = new ModelWriter() { + @Override + public Optional 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 options) throws ModelWriterException { + modelXmlFactory.write(model, target); + } + }; + 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 getAvailableDialects() { + return Set.copyOf(dialectProviders.keySet()); + } + + @Override + public Optional readModel(Path dir, Dialect dialect, Map options) { + return requireModelDialectProvider(dialect).getModelParser().locateAndParse(dir, options); + } + + @Override + public Optional writeModel(Path dir, Dialect dialect, Model model, Map options) { + return requireModelDialectProvider(dialect).getModelWriter().targetAndWrite(dir, model, options); + } + + @Nonnull + private ModelDialectProvider requireModelDialectProvider(Dialect dialect) { + ModelDialectProvider dialectProvider = dialectProviders.get(dialect); + if (dialectProvider == null) { + throw new IllegalArgumentException("Unknown model dialect " + dialect); + } + return dialectProvider; + } +} diff --git a/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultModelDialectManagerTest.java b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultModelDialectManagerTest.java new file mode 100644 index 00000000000..df81a3a3161 --- /dev/null +++ b/maven-api-impl/src/test/java/org/apache/maven/internal/impl/model/DefaultModelDialectManagerTest.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl.model; + +import java.util.Collections; + +import org.apache.maven.api.Dialect; +import org.apache.maven.internal.impl.DefaultModelXmlFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DefaultModelDialectManagerTest { + @Test + void smoke() { + DefaultModelDialectManager modelDialectManager = + new DefaultModelDialectManager(new DefaultModelXmlFactory(), Collections.emptyList()); + assertEquals(1, modelDialectManager.getAvailableDialects().size()); + assertEquals( + Dialect.XML, + modelDialectManager.getAvailableDialects().iterator().next()); + } +}