diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..e5e44d8
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "maven" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
+ ignore:
+ - dependency-name: "*"
+ update-types: [ "version-update:semver-major" ]
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..f985483
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,44 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Maven Central Deployment
+
+on:
+ workflow_dispatch
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Set up Maven Central
+ uses: actions/setup-java@v3
+ with: # running setup-java again overwrites the settings.xml
+ distribution: 'temurin'
+ java-version: '17'
+ server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml
+ server-username: OSSRH_USERNAME # env variable for username in deploy
+ server-password: OSSRH_TOKEN # env variable for token in deploy
+ gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
+ gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
+
+ - name: Build and Publish to Maven Central
+ run: mvn -B clean deploy -Prelease --file pom.xml
+ env:
+ OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
+ OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..113af43
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,32 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Java CI with Maven
+
+on:
+ push:
+ branches: [ "**" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Build with Maven
+ run: mvn -B clean install --file pom.xml
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..db3d2a0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+*target*
+*.jar
+*.war
+*.ear
+*.class
+.DS_Store
+
+# eclipse specific git ignore
+.project
+.metadata
+.factorypath
+bin/**
+tmp/**
+tmp/**/*
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# IntelliJ specific git ignore
+.idea/
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..e1fd273
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d264116
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+# Mapstruct SPI implementation for protocol buffers mapping
+
+This project provides a SPI implementation for [Mapstruct](http://mapstruct.org/) to generate mapping code from protocol
+buffers to the following targets:
+
+- Plain Old Java Objects (POJOs)
+- [Immutables](https://immutables.github.io/) value objects
+- Java records
+
+Unit tests exist to validate all of these mappings. The SPI implementation generally requires Mapstruct 1.5.5 and Java
+1.8+ (of course if you want to map to records, Java 14+ is required).
+
+The enum mapping strategy assumes that Google's enum value naming scheme is used, as described
+here: https://developers.google.com/protocol-buffers/docs/style#enum
+
+This SPI implementation also includes a [pull request](https://github.com/mapstruct/mapstruct/pull/2219) from the
+Mapstruct repository that was not merged yet, but fixes a
+deficiency with Mapstructs' own org.immutables support when using inner classes and @Value.Enclosing.
+
+## Usage
+
+Your protobuf mapping interfaces must be annotated with `@Mapper`
+and `collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED`
+because the protobuf classes use a builder pattern.
+
+```java
+
+@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
+public interface XXX {
+
+```
+
+Include the mapstruct dependency and the annotation processor in your Maven project:
+
+```xml
+
+
+
+ org.mapstruct
+ mapstruct
+ ${org.mapstruct.version}
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ de.firehead
+ mapstruct-spi-protobuf
+ 1.0.0
+
+
+
+
+
+
+
+```
+
+Or for Gradle:
+
+```java
+
+implementation"org.mapstruct:mapstruct:${mapstructVersion}"
+annotationProcessor"org.mapstruct:mapstruct-processor:${mapstructVersion}"
+annotationProcessor"de.firehead:mapstruct-spi-protobuf:1.0.0"
+
+```
+
diff --git a/mapstruct-spi-protobuf-test-immutables/pom.xml b/mapstruct-spi-protobuf-test-immutables/pom.xml
new file mode 100644
index 0000000..442a57e
--- /dev/null
+++ b/mapstruct-spi-protobuf-test-immutables/pom.xml
@@ -0,0 +1,106 @@
+
+
+ 4.0.0
+
+
+ de.firehead
+ mapstruct-spi-protobuf-parent
+ 1.0.0-SNAPSHOT
+
+
+ mapstruct-spi-protobuf-test-immutables
+
+
+ 1.8
+ 1.8
+ true
+ true
+
+
+
+
+ de.firehead
+ mapstruct-spi-protobuf-test-protos
+
+
+
+ org.mapstruct
+ mapstruct
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+ org.immutables
+ value
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+
+
+ dev.cookiecode
+ another-protobuf-maven-plugin
+ 2.1.0
+
+
+
+ compile
+ compile-custom
+
+ generate-sources
+
+
+ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 1.8
+
+
+ org.immutables
+ value-processor
+ ${immutables.version}
+
+
+ de.firehead
+ mapstruct-spi-protobuf
+ ${project.version}
+
+
+
+
+
+
+
+
diff --git a/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/DeepTestImmutableObject.java b/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/DeepTestImmutableObject.java
new file mode 100644
index 0000000..6aade6e
--- /dev/null
+++ b/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/DeepTestImmutableObject.java
@@ -0,0 +1,31 @@
+/* mapstruct-spi-protobuf
+ *
+ * Copyright (C) 2024
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+package de.firehead.mapstruct.spi.protobuf.test.immutables;
+
+import org.immutables.value.Value;
+
+/**
+ * Test immutable object for deep testing purposes.
+ * This corresponds to the following test proto message:
+ *
+ * message DeepTestProtoMessage {
+ * TestProtoMessage testProtoMessage = 1;
+ * repeated TestProtoMessage testProtoMessageList = 2;
+ * map testProtoMessageMap = 3;
+ * }
+ */
+@Value.Immutable
+public interface DeepTestImmutableObject {
+
+ TestImmutableObject getTestProtoMessagePlain();
+
+ java.util.List getTestProtoMessageList();
+
+ java.util.Map getTestProtoMessageMap();
+
+}
diff --git a/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/TestEnum.java b/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/TestEnum.java
new file mode 100644
index 0000000..f760693
--- /dev/null
+++ b/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/TestEnum.java
@@ -0,0 +1,13 @@
+/* mapstruct-spi-protobuf
+ *
+ * Copyright (C) 2024
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+package de.firehead.mapstruct.spi.protobuf.test.immutables;
+
+public enum TestEnum {
+
+ VALUE;
+}
diff --git a/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/TestImmutableObject.java b/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/TestImmutableObject.java
new file mode 100644
index 0000000..d938eef
--- /dev/null
+++ b/mapstruct-spi-protobuf-test-immutables/src/main/java/de/firehead/mapstruct/spi/protobuf/test/immutables/TestImmutableObject.java
@@ -0,0 +1,70 @@
+/* mapstruct-spi-protobuf
+ *
+ * Copyright (C) 2024
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+package de.firehead.mapstruct.spi.protobuf.test.immutables;
+
+import org.immutables.value.Value;
+
+/**
+ * Test immutable object for testing purposes.
+ * This corresponds to the following test proto message:
+ *
+ * message DeepTestProtoMessage {
+ * TestProtoMessage testProtoMessage = 1;
+ * repeated TestProtoMessage testProtoMessageList = 2;
+ * map testProtoMessageMap = 3;
+ * }
+ */
+public record DeepTestRecord(TestRecord testProtoMessagePlain,
+ List testProtoMessageList,
+ Map testProtoMessageMap) {
+}
diff --git a/mapstruct-spi-protobuf-test-records/src/main/java/de/firehead/mapstruct/spi/protobuf/test/records/TestEnum.java b/mapstruct-spi-protobuf-test-records/src/main/java/de/firehead/mapstruct/spi/protobuf/test/records/TestEnum.java
new file mode 100644
index 0000000..c0a9ffe
--- /dev/null
+++ b/mapstruct-spi-protobuf-test-records/src/main/java/de/firehead/mapstruct/spi/protobuf/test/records/TestEnum.java
@@ -0,0 +1,13 @@
+/* mapstruct-spi-protobuf
+ *
+ * Copyright (C) 2024
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+package de.firehead.mapstruct.spi.protobuf.test.records;
+
+public enum TestEnum {
+
+ VALUE;
+}
diff --git a/mapstruct-spi-protobuf-test-records/src/main/java/de/firehead/mapstruct/spi/protobuf/test/records/TestRecord.java b/mapstruct-spi-protobuf-test-records/src/main/java/de/firehead/mapstruct/spi/protobuf/test/records/TestRecord.java
new file mode 100644
index 0000000..f71b0d4
--- /dev/null
+++ b/mapstruct-spi-protobuf-test-records/src/main/java/de/firehead/mapstruct/spi/protobuf/test/records/TestRecord.java
@@ -0,0 +1,43 @@
+/* mapstruct-spi-protobuf
+ *
+ * Copyright (C) 2024
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+package de.firehead.mapstruct.spi.protobuf.test.records;
+
+import java.util.Map;
+
+/**
+ * Test immutable record for testing purposes.
+ * This corresponds to the following test proto message:
+ *
+ * Optionally supports Immutables as counterpart, much like
+ * {@link org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy}, but doesn't require them.
+ */
+public class ProtobufAccessorNamingStrategy extends DefaultAccessorNamingStrategy {
+
+ /**
+ * An expected interface used to find out if a type is a protobuf message type (or a builder of one).
+ */
+ protected TypeMirror protobufMarkerInterface;
+
+ /**
+ * Whether Immutables is found on the classpath. If true, we assume that the counterpart for protobuf mapping are
+ * Immutables-based data structures.
+ */
+ protected boolean isImmutables;
+
+ @Override
+ public void init(MapStructProcessingEnvironment aProcessingEnvironment) {
+ super.init(aProcessingEnvironment);
+
+ final TypeElement typeElement = elementUtils.getTypeElement("com.google.protobuf.MessageLiteOrBuilder");
+ if (typeElement != null) {
+ protobufMarkerInterface = typeElement.asType();
+ }
+
+ isImmutables = elementUtils.getTypeElement(ImmutablesConstants.IMMUTABLE_FQN) != null;
+ }
+
+ /**
+ * Recursively checks whether the given {@link TypeElement} is a protobuf message type (or a builder of one).
+ *
+ * @param aType the type to check
+ * @return true if it's a protobuf message type, false otherwise
+ */
+ private boolean isProtobufGeneratedMessage(TypeElement aType) {
+ // Check the interfaces first
+ for (final TypeMirror implementedInterface : aType.getInterfaces()) {
+ if (implementedInterface.toString().startsWith("com.google.protobuf.MessageLiteOrBuilder")) {
+ return true;
+ } else if (implementedInterface instanceof DeclaredType) {
+ if (isProtobufGeneratedMessage((TypeElement) ((DeclaredType) implementedInterface).asElement())) {
+ return true;
+ }
+ }
+ }
+
+ // If no match was found, check the superclasses' interfaces recursively
+ final TypeMirror superType = aType.getSuperclass();
+ if (superType instanceof DeclaredType) {
+ return isProtobufGeneratedMessage((TypeElement) ((DeclaredType) superType).asElement());
+ }
+
+ return false;
+ }
+
+ /**
+ * These are purely internal methods generated into protobuf classes. They can be entirely ignored when mapping.
+ */
+ private static final Set INTERNAL_PROTOBUF_METHODS = new HashSet<>(Arrays.asList(
+ "clear",
+ "clearField",
+ "clearOneof",
+ "getAllFields",
+ "getAllFieldsMutable",
+ "getAllFieldsRaw",
+ "getDefaultInstance",
+ "getDefaultInstanceForType",
+ "getDescriptor",
+ "getDescriptorForType",
+ "getField",
+ "getFieldRaw",
+ "getInitializationErrorString",
+ "getMemoizedSerializedSize",
+ "getOneofFieldDescriptor",
+ "getParserForType",
+ "getRepeatedField",
+ "getRepeatedFieldCount",
+ "getSerializedSize",
+ "getSerializingExceptionMessage",
+ "getUnknownFields",
+ "isInitialized",
+ "mergeFrom",
+ "mergeUnknownFields",
+ "newBuilder",
+ "newBuilderForType",
+ "parseDelimitedFrom",
+ "parseFrom",
+ "setRepeatedField",
+ "setUnknownFields"));
+
+ /**
+ * Checks whether the given method is an internal protobuf method.
+ *
+ * @param aMethod the method to check
+ * @return true if it is an internal protobuf method, false otherwise
+ */
+ private boolean isInternalProtobufMethod(ExecutableElement aMethod) {
+ return INTERNAL_PROTOBUF_METHODS.contains(aMethod.getSimpleName().toString());
+ }
+
+ /**
+ * Checks whether the given {@link ExecutableElement} is a method from a generated protobuf message class.
+ *
+ * @param aMethod the method to check
+ * @return true if it's from a protobuf class, false otherwise
+ */
+ private boolean isProtobufMethod(ExecutableElement aMethod) {
+ return aMethod.getKind() == ElementKind.METHOD
+ && aMethod.getEnclosingElement() != null
+ && protobufMarkerInterface != null
+ && typeUtils.isAssignable(aMethod.getEnclosingElement().asType(), protobufMarkerInterface);
+ }
+
+ /**
+ * Checks whether the given {@link TypeElement} has a method of the provided name.
+ *
+ * @param aType the type to check
+ * @param aMethodName the method to check for
+ * @return true if a matching method is found, false otherwise
+ */
+ private boolean doesHaveMethod(TypeElement aType, String aMethodName) {
+ return aType.getEnclosedElements()
+ .stream()
+ .anyMatch(e -> e.getSimpleName().toString().equals(aMethodName));
+ }
+
+ /**
+ * Protobuf message fields often have several "auxiliary accessor methods" generated for them, which relate to
+ * a particular field (and thus have its name as part of their name). Which methods exist depends on the type
+ * of the field, but for the purpose of mapping we must ignore all of these as they all relate to the same field.
+ *
+ * @param aMethod the method to check
+ * @return true if it is an auxiliary accessor for a field of a protobuf message, false otherwise
+ */
+ private boolean isAuxiliaryProtobufPropertyAccessor(ExecutableElement aMethod) {
+ if (!isProtobufMethod(aMethod)) {
+ return false;
+ }
+ final String methodName = aMethod.getSimpleName().toString();
+
+ if (methodName.startsWith("getOneOf") && methodName.endsWith("Case")) {
+ return true;
+ }
+
+ for (String prefixCandidate : Arrays.asList("clear", "merge", "mutable", "putAll", "remove")) {
+ if (methodName.startsWith(prefixCandidate)) {
+ String expectedAccessor = "get" + methodName.substring(prefixCandidate.length());
+ if (doesHaveMethod((TypeElement) aMethod.getEnclosingElement(), expectedAccessor)) {
+ return true;
+ }
+ }
+ }
+
+ for (String suffixCandidate : Arrays.asList("Bytes", "Count", "Map", "Value", "ValueList")) {
+ if (methodName.endsWith(suffixCandidate)) {
+ final String expectedAccessor = methodName.substring(0, methodName.length() - suffixCandidate.length());
+ if (doesHaveMethod((TypeElement) aMethod.getEnclosingElement(), expectedAccessor)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks if a given type is a list (Java or protobuf ProtocolStringList) type.
+ *
+ * @param aType the type to check
+ * @return true if it is a list, false otherwise
+ */
+ private boolean isListType(TypeMirror aType) {
+ final String typeString = aType.toString();
+ return typeString.startsWith(List.class.getCanonicalName())
+ || typeString.startsWith("com.google.protobuf.ProtocolStringList");
+ }
+
+ /**
+ * Checks whether a given type is a {@link Map}.
+ *
+ * @param aType the type to check
+ * @return true if it is a map, false otherwise
+ */
+ private boolean isMapType(TypeMirror aType) {
+ return aType.toString().startsWith(Map.class.getCanonicalName());
+ }
+
+ /**
+ * Checks if a given type is a {@link java.util.Map.Entry}.
+ *
+ * @param aType the type to check
+ * @return true if it is an entry, false otherwise
+ */
+ private boolean isMapEntryType(TypeMirror aType) {
+ return aType.toString().startsWith(Map.Entry.class.getCanonicalName());
+ }
+
+ /**
+ * Checks whether a given method is an accessor to a mutable map.
+ *
+ * @param aMethod the method to check
+ * @return true if it is a mutable map accessor, false otherwise
+ */
+ private boolean isMutableMapAccessor(ExecutableElement aMethod) {
+ return aMethod.getSimpleName().toString().startsWith("getMutable") && isMapType(aMethod.getReturnType());
+ }
+
+ /**
+ * Checks whether a given method is an accessor to an immutable map.
+ *
+ * @param aMethod the method to check
+ * @return true if it is an immutable map accessor, false otherwise
+ */
+ private boolean isImmutableMapAccessor(ExecutableElement aMethod) {
+ final String methodName = aMethod.getSimpleName().toString();
+ return methodName.startsWith("get")
+ && !methodName.startsWith("getMutable")
+ && isMapType(aMethod.getReturnType())
+ && (!methodName.endsWith("Map") || doesHaveMethod((TypeElement) aMethod.getEnclosingElement(), methodName + "Map"));
+ }
+
+ /**
+ * Checks whether a given method is a getter for a list.
+ *
+ * @param aMethod the method to check
+ * @return true if it is a list getter, false otherwise
+ */
+ private boolean isListGetter(ExecutableElement aMethod) {
+ return aMethod.getSimpleName().toString().startsWith("get") && isListType(aMethod.getReturnType());
+ }
+
+ /**
+ * Checks whether a given method is a setter for a list.
+ *
+ * @param aMethod the method to check
+ * @return true if it is a list setter, false otherwise
+ */
+ private boolean isListSetter(ExecutableElement aMethod) {
+ return aMethod.getSimpleName().toString().startsWith("set")
+ && aMethod.getParameters().size() == 1
+ && isListType(aMethod.getParameters().get(0).asType());
+ }
+
+ /**
+ * Checks whether a given method is a putter for a map value.
+ *
+ * @param aMethod the method to check
+ * @return true if it is a map value putter, false otherwise
+ */
+ private boolean isMapValuePutter(ExecutableElement aMethod) {
+ return aMethod.getSimpleName().toString().startsWith("put") && aMethod.getParameters().size() == 2;
+ }
+
+ /**
+ * Checks whether a given method is a putter for a map entry.
+ *
+ * @param aMethod the method to check
+ * @return true if it is a map entry putter, false otherwise
+ */
+ private boolean isMapEntryPutter(ExecutableElement aMethod) {
+ return aMethod.getSimpleName().toString().startsWith("put")
+ && aMethod.getParameters().size() == 1
+ && isMapEntryType(aMethod.getParameters().get(0).asType());
+ }
+
+ /**
+ * Checks whether a given method is a putAll for a map entry.
+ *
+ * @param aMethod the method to check
+ * @return true if it is a map entry putAll, false otherwise
+ */
+ private boolean isMapEntryPutAll(ExecutableElement aMethod) {
+ return aMethod.getSimpleName().toString().startsWith("putAll")
+ && aMethod.getParameters().size() == 1
+ && isMapType(aMethod.getParameters().get(0).asType());
+ }
+
+ @Override
+ public boolean isGetterMethod(ExecutableElement aMethod) {
+ if (isInternalProtobufMethod(aMethod) || isAuxiliaryProtobufPropertyAccessor(aMethod)) {
+ return false;
+ }
+
+ final String methodName = aMethod.getSimpleName().toString();
+ if (methodName.endsWith("OrBuilder") || methodName.endsWith("BuilderList")) {
+ return false;
+ }
+
+ if (isMutableMapAccessor(aMethod)) {
+ return true;
+ } else if (isImmutableMapAccessor(aMethod)) {
+ // For protobuf builder types we want to use the mutable map accessor
+ final TypeElement type = (TypeElement) aMethod.getEnclosingElement();
+ return !type.getSuperclass().toString().startsWith("com.google.protobuf.GeneratedMessageV3.Builder");
+ }
+
+ return super.isGetterMethod(aMethod);
+ }
+
+ @Override
+ public boolean isSetterMethod(ExecutableElement aMethod) {
+ if (isInternalProtobufMethod(aMethod) || isAuxiliaryProtobufPropertyAccessor(aMethod)) {
+ return false;
+ }
+ if (isMapEntryPutAll(aMethod) || isMapEntryPutter(aMethod) || isMapValuePutter(aMethod)) {
+ return false;
+ }
+
+ return super.isSetterMethod(aMethod);
+ }
+
+ @Override
+ protected boolean isFluentSetter(ExecutableElement aMethod) {
+ // Equivalent from ImmutablesAccessorNamingStrategy
+ if (isImmutables && aMethod.getSimpleName().toString().equals("from")) {
+ return false;
+ }
+
+ if (isInternalProtobufMethod(aMethod) || isAuxiliaryProtobufPropertyAccessor(aMethod)) {
+ return false;
+ }
+
+ final String methodName = aMethod.getSimpleName().toString();
+ if (methodName.startsWith("get")) {
+ return false;
+ }
+
+ return super.isFluentSetter(aMethod);
+ }
+
+ @Override
+ public boolean isAdderMethod(ExecutableElement aMethod) {
+ if (isInternalProtobufMethod(aMethod) || isAuxiliaryProtobufPropertyAccessor(aMethod)) {
+ return false;
+ }
+
+ return super.isAdderMethod(aMethod);
+ }
+
+ @Override
+ public boolean isPresenceCheckMethod(ExecutableElement aMethod) {
+ if (isInternalProtobufMethod(aMethod) || isAuxiliaryProtobufPropertyAccessor(aMethod)) {
+ return false;
+ }
+
+ return super.isPresenceCheckMethod(aMethod);
+ }
+
+ @Override
+ public String getElementName(ExecutableElement aMethod) {
+ final String methodName = super.getElementName(aMethod);
+ if (isProtobufMethod(aMethod)) {
+ return Nouns.singularize(methodName);
+ } else {
+ return methodName;
+ }
+ }
+
+ @Override
+ public String getPropertyName(ExecutableElement aMethod) {
+ // Protobuf list/map accessors have special naming conventions that we must "reverse" here in order to get the
+ // actual property name
+
+ final Element receiver = aMethod.getEnclosingElement();
+ if (receiver != null && (receiver.getKind() == ElementKind.CLASS || receiver.getKind() == ElementKind.INTERFACE)) {
+ final TypeElement type = (TypeElement) receiver;
+ if (isProtobufGeneratedMessage(type)) {
+ final String methodName = aMethod.getSimpleName().toString();
+
+ if (isListGetter(aMethod) || isListSetter(aMethod)) {
+ return IntrospectorUtils.decapitalize(
+ methodName.substring("get" .length(), methodName.length() - "List" .length()));
+ } else if (isMutableMapAccessor(aMethod)) {
+ return IntrospectorUtils.decapitalize(methodName.substring("getMutable" .length()));
+ } else if (isImmutableMapAccessor(aMethod)) {
+ return IntrospectorUtils.decapitalize(methodName.substring("get" .length()));
+ }
+ }
+ }
+
+ return super.getPropertyName(aMethod);
+ }
+
+}
diff --git a/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/builderprovider/DelegatingBuilderProvider.java b/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/builderprovider/DelegatingBuilderProvider.java
new file mode 100644
index 0000000..947b49e
--- /dev/null
+++ b/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/builderprovider/DelegatingBuilderProvider.java
@@ -0,0 +1,33 @@
+package de.firehead.mapstruct.spi.protobuf.builderprovider;
+
+import org.mapstruct.ap.internal.util.ImmutablesConstants;
+import org.mapstruct.ap.spi.BuilderInfo;
+import org.mapstruct.ap.spi.BuilderProvider;
+import org.mapstruct.ap.spi.DefaultBuilderProvider;
+import org.mapstruct.ap.spi.MapStructProcessingEnvironment;
+
+import javax.lang.model.type.TypeMirror;
+
+public class DelegatingBuilderProvider implements BuilderProvider {
+
+ protected BuilderProvider delegate;
+
+ @Override
+ public void init(MapStructProcessingEnvironment aProcessingEnvironment) {
+ if (delegate == null) {
+ if (aProcessingEnvironment.getElementUtils().getTypeElement(ImmutablesConstants.IMMUTABLE_FQN) != null) {
+ delegate = new ImmutablesBuilderProvider();
+ } else {
+ delegate = new DefaultBuilderProvider();
+ }
+ }
+
+ delegate.init(aProcessingEnvironment);
+ }
+
+ @Override
+ public BuilderInfo findBuilderInfo(TypeMirror aType) {
+ return delegate.findBuilderInfo(aType);
+ }
+
+}
diff --git a/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/builderprovider/ImmutablesBuilderProvider.java b/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/builderprovider/ImmutablesBuilderProvider.java
new file mode 100644
index 0000000..a6ccb24
--- /dev/null
+++ b/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/builderprovider/ImmutablesBuilderProvider.java
@@ -0,0 +1,326 @@
+/*
+ * Original Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package de.firehead.mapstruct.spi.protobuf.builderprovider;
+
+import org.mapstruct.ap.spi.BuilderInfo;
+import org.mapstruct.ap.spi.DefaultBuilderProvider;
+import org.mapstruct.ap.spi.TypeHierarchyErroneousException;
+import org.mapstruct.util.Experimental;
+
+import javax.lang.model.element.*;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.SimpleAnnotationValueVisitor8;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Builder provider for Immutables. A custom provider is needed because Immutables creates an implementation of an
+ * interface and that implementation has the builder. This implementation would try to find the type created by
+ * Immutables and would look for the builder in it. Only types annotated with the
+ * {@code org.immutables.value.Value.Immutable} are considered for this discovery.
+ *
+ * @author Filip Hrisafov
+ */
+// This ImmutablesBuilderProvider was taken from the following Pull Request:
+// https://github.com/mapstruct/mapstruct/pull/2219
+// It fixes incompatibilities of the Mapstruct default ImmutablesBuilderProvider with inner classes and @Value.Enclosing
+// The PR unfortunately has not been merged to official Mapstruct due to the maintainer not having the time to resolve
+// some obscure problems with executing the associated tests within the PR in an ECJ JDK8 environment. I don't care
+// about running those tests under ECJ, I want the fix.
+//
+@Experimental("The Immutables builder provider might change in a subsequent release")
+public class ImmutablesBuilderProvider extends DefaultBuilderProvider {
+
+ private static final Pattern JAVA_JAVAX_PACKAGE = Pattern.compile("^javax?\\..*");
+
+ private static final String IMMUTABLE_FQN = "org.immutables.value.Value.Immutable";
+
+ private static final String VALUE_ENCLOSING_FQN = "org.immutables.value.Value.Enclosing";
+
+ private static final String VALUE_STYLE_FQN = "org.immutables.value.Value.Style";
+
+ @Override
+ protected BuilderInfo findBuilderInfo(TypeElement typeElement) {
+ Name name = typeElement.getQualifiedName();
+ if (name.length() == 0 || JAVA_JAVAX_PACKAGE.matcher(name).matches()) {
+ return null;
+ }
+ TypeElement immutableAnnotation = elementUtils.getTypeElement(IMMUTABLE_FQN);
+ if (immutableAnnotation != null) {
+ BuilderInfo info = findBuilderInfoForImmutables(typeElement, immutableAnnotation);
+ if (info != null) {
+ return info;
+ }
+ }
+
+ return super.findBuilderInfo(typeElement);
+ }
+
+ /**
+ * Finds the builder info for the given type or returns null if not found.
+ *
+ * @param targetTypeElement a type which may require a builder
+ * @param immutableAnnotation type of the immutables annotation we're looking for
+ * @return BuilderInfo or null if none found
+ * @throws TypeHierarchyErroneousException if unable to process in this round
+ */
+ protected BuilderInfo findBuilderInfoForImmutables(TypeElement targetTypeElement, TypeElement immutableAnnotation) {
+
+ // we can avoid any reflection/type mirror inspection of the annotations
+ // if the type we're dealing with now has a builder method on it.
+ BuilderInfo fromType = super.findBuilderInfo(targetTypeElement);
+ if (fromType != null) {
+ return fromType;
+ }
+
+ // if there's no build method on the type, then look for the immutable annotation
+ // since it may be accompanied by a Value.Style which provides info on the
+ // name of the generated builder
+ return findTypeWithImmutableAnnotation(targetTypeElement, immutableAnnotation.asType()).map(typeElement -> {
+ TypeElement immutableElement = asImmutableElement(typeElement);
+ if (immutableElement != null) {
+ return super.findBuilderInfo(immutableElement);
+ } else {
+ // Immutables processor has not run yet. Trigger a postpone to the next round for MapStruct
+ throw new TypeHierarchyErroneousException(typeElement);
+ }
+
+ }).orElse(null);
+ }
+
+ /**
+ * This method looks for the Value.Immutable on the targetTypeElement in the following order:
+ *
+ * 1) directly on the element itself 2) on an interface in the same package that the element implements 3) on the
+ * superclass for the element
+ *
+ * We're looking for the immutable annotation since there could be additional annotations there which affect the
+ * name of the generated immutable builder.
+ *
+ * @param targetTypeElement element to analyze for the immutables annotation
+ * @param immutableAnnotationTypeMirror type of the annotation we're looking for
+ * @return first found element with the type or empty
+ */
+ protected Optional findTypeWithImmutableAnnotation(TypeElement targetTypeElement,
+ TypeMirror immutableAnnotationTypeMirror) {
+ Predicate hasImmutableAnnotation = element -> elementUtils.getAllAnnotationMirrors(element)
+ .stream()
+ .anyMatch(am -> typeUtils.isSameType(am.getAnnotationType(), immutableAnnotationTypeMirror));
+
+ // 1. If the TypeElement has the immutable annotation
+ // then use the targetTypeElement to find the builder
+ //
+ if (hasImmutableAnnotation.test(targetTypeElement)) {
+ return Optional.of(targetTypeElement);
+ }
+
+ String targetPackage = findPackage(targetTypeElement);
+ return Stream.concat(
+ // 2. we'll check interfaces second
+ targetTypeElement.getInterfaces().stream(),
+ // 3. if not found on an interface, check the super class
+ Stream.of(targetTypeElement.getSuperclass()))
+ .filter(intf -> intf.getKind() == TypeKind.DECLARED)
+ .map(DeclaredType.class::cast)
+ .map(DeclaredType::asElement)
+ .map(TypeElement.class::cast)
+ .filter(intf -> targetPackage.equals(findPackage(intf)))
+ .filter(hasImmutableAnnotation)
+ .findFirst();
+ }
+
+ /**
+ * @param typeElement element that has the Value.Immutable annotation
+ * @return type that should have the builder or null if none found
+ */
+ protected TypeElement asImmutableElement(TypeElement typeElement) {
+ // the java package that the generated builder is in
+ String packageQualifier;
+ // optional enclosing qualifier if the generated builder is an inner class
+ // the value.enclosing annotation customizes this qualifier
+ String enclosingQualifier = "";
+ // name of the builder, defaults to Immutable + non-abstract simple type name
+ // the style annotation customizes the builder
+ String builderName;
+
+ AnnotationMirror style = null;
+
+ Element enclosingElement = typeElement.getEnclosingElement();
+ while (enclosingElement.getKind() != ElementKind.PACKAGE) {
+ // look for the first enclosing element with Value.Enclosing
+ if (hasValueEnclosingAnnotation(enclosingElement) && enclosingQualifier.isEmpty()) {
+ style = findStyle(enclosingElement);
+ if (style != null) {
+ enclosingQualifier = enclosingQualifierFromStyle(style, enclosingElement);
+ } else {
+ enclosingQualifier = "Immutable" + enclosingElement.getSimpleName();
+ }
+ }
+ enclosingElement = enclosingElement.getEnclosingElement();
+ }
+ packageQualifier = ((PackageElement) enclosingElement).getQualifiedName().toString();
+
+ builderName = builderFromStyle(style, typeElement, !enclosingQualifier.isEmpty());
+
+ // check for @Value.Enclosing
+ // ::=
+ String bqn = Stream.of(packageQualifier, enclosingQualifier, builderName)
+ .filter(segment -> !segment.isEmpty())
+ .collect(Collectors.joining("."));
+
+ return elementUtils.getTypeElement(bqn);
+ }
+
+ protected String enclosingQualifierFromStyle(AnnotationMirror style, Element element) {
+ // Value.Style influences the qualifier name through the typeAbstract, typeImmutable, and typeImmutableEnclosing
+ return immutableNameFromStylePattern(nameWithoutAbstractPrefix(style, element),
+ getSingleAnnotationValue("typeImmutable", style).orElseGet(
+ () -> getSingleAnnotationValue("typeImmutableEnclosing", style).orElse("Immutable*")));
+ }
+
+ protected String builderFromStyle(AnnotationMirror style, TypeElement element, boolean valueEnclosingFound) {
+ assert element != null;
+
+ // if we're given a style, then use it. If not, then
+ // keep walking up until we find one or run out of enclosing elements
+ // If we don't find a style, then the naming behavior is driven
+ // by defaults as documented by the immutables annotations
+ AnnotationMirror resolvedStyle = Optional.ofNullable(style).orElseGet(() -> {
+ Element currentElement = element;
+ AnnotationMirror found = null;
+ while (currentElement != null && found == null) {
+ found = findStyle(currentElement);
+ currentElement = currentElement.getEnclosingElement();
+ }
+ return found;
+ });
+
+ if (resolvedStyle == null && !valueEnclosingFound) {
+ // no @Value.Style found, use the default behavior from immutables
+ // no @Value.Enclosing
+ return "Immutable" + element.getSimpleName();
+ }
+
+ if (resolvedStyle == null) {
+ // no @Value.Style found, but there was a @Value.Enclosing, use the default behavior
+ return element.getSimpleName().toString();
+ }
+
+ // style is present, see what it has to say about the names
+ return immutableNameFromStylePattern(
+ // trim the abstract portion from the name (defaults to "Abstract*")
+ nameWithoutAbstractPrefix(resolvedStyle, element),
+ // use the value from typeImmutable
+ getSingleAnnotationValue("typeImmutable", resolvedStyle)
+ // Note: typeImmutable is defined as having a default value so we shouldn't
+ // hit this orElse. Leaving this instead of throwing since
+ // it's a reasonable default (and currently matches their docs)
+ .orElse("Immutable*"));
+ }
+
+ protected String nameWithoutAbstractPrefix(AnnotationMirror style, Element element) {
+ final String simpleNameOfElement = element.getSimpleName().toString();
+ return getTypeAbstractValues(style).stream()
+ .filter(p -> simpleNameOfElement.startsWith(p.substring(0, p.length() - 1)))
+ .map(p -> simpleNameOfElement.substring(p.length() - 1))
+ .findFirst()
+ .orElseGet(() -> element.getSimpleName().toString());
+ }
+
+ protected Optional getSingleAnnotationValue(String annotationKey, AnnotationMirror style) {
+ return elementUtils.getElementValuesWithDefaults(style)
+ .entrySet()
+ .stream()
+ .filter(entry -> annotationKey.equals(entry.getKey().getSimpleName().toString()))
+ .map(Map.Entry::getValue)
+ .map(value -> value.accept(new SimpleAnnotationValueVisitor8() {
+
+ @Override
+ public String visitString(String s, Void unused) {
+ return s;
+ }
+ }, null))
+ .findFirst();
+ }
+
+ protected List getTypeAbstractValues(AnnotationMirror styleOrNull) {
+
+ // this is the pattern if there is no style or if typeAbstract value not found
+ Supplier> noStyleOrMissingDefault = () -> Collections.singletonList("Abstract*");
+
+ return Optional.ofNullable(styleOrNull)
+ .map(style -> elementUtils.getElementValuesWithDefaults(style)
+ .entrySet()
+ .stream()
+ .filter(entry -> "typeAbstract".equals(entry.getKey().getSimpleName().toString()))
+ .map(Map.Entry::getValue)
+ .map(value -> value.accept(new SimpleAnnotationValueVisitor8, Void>() {
+
+ @Override
+ public List visitArray(List extends AnnotationValue> values, Void unused) {
+ return values.stream()
+ .map(val -> val.accept(new SimpleAnnotationValueVisitor8() {
+
+ @Override
+ public String visitString(String s, Void param) {
+ return s;
+ }
+ }, null))
+ .collect(Collectors.toList());
+ }
+ }, null))
+ .findFirst()
+ .orElseGet(noStyleOrMissingDefault))
+ .orElseGet(noStyleOrMissingDefault);
+ }
+
+ protected boolean hasValueEnclosingAnnotation(Element enclosingElement) {
+ TypeElement typeElement = elementUtils.getTypeElement(VALUE_ENCLOSING_FQN);
+
+ return Optional.ofNullable(typeElement)
+ .map(Element::asType)
+ .map(mirror -> elementUtils.getAllAnnotationMirrors(enclosingElement)
+ .stream()
+ .anyMatch(am -> typeUtils.isSameType(am.getAnnotationType(), mirror)))
+ .orElse(Boolean.FALSE);
+ }
+
+ protected AnnotationMirror findStyle(Element element) {
+ TypeElement styleTypeElement = elementUtils.getTypeElement(VALUE_STYLE_FQN);
+ if (styleTypeElement == null) {
+ return null;
+ }
+ TypeMirror styleAnnotation = styleTypeElement.asType();
+ return elementUtils.getAllAnnotationMirrors(element)
+ .stream()
+ .filter(am -> typeUtils.isSameType(am.getAnnotationType(), styleAnnotation))
+ .findFirst()
+ .orElse(null);
+ }
+
+ protected String immutableNameFromStylePattern(String simpleNameOfElement, String typeImmutablePattern) {
+ String fixedPattern = typeImmutablePattern.substring(0, typeImmutablePattern.length() - 1);
+ return fixedPattern + simpleNameOfElement;
+ }
+
+ protected String findPackage(Element element) {
+ Element current = element;
+ while (current.getKind() != ElementKind.PACKAGE) {
+ current = current.getEnclosingElement();
+ }
+ return current.getSimpleName().toString();
+ }
+
+}
diff --git a/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/enums/ProtobufEnumMappingStrategy.java b/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/enums/ProtobufEnumMappingStrategy.java
new file mode 100644
index 0000000..30f2db9
--- /dev/null
+++ b/mapstruct-spi-protobuf/src/main/java/de/firehead/mapstruct/spi/protobuf/enums/ProtobufEnumMappingStrategy.java
@@ -0,0 +1,118 @@
+/* mapstruct-spi-protobuf
+ *
+ * Copyright (C) 2024
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+package de.firehead.mapstruct.spi.protobuf.enums;
+
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ap.spi.DefaultEnumMappingStrategy;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * Enum mapping strategy implementing the common enum value name mapping suggestion from Google.
+ *
+ * @author Rene Schneider
+ */
+public class ProtobufEnumMappingStrategy extends DefaultEnumMappingStrategy {
+
+ /**
+ * The postfix used for the default value (if enum is unset).
+ */
+ private static final String DEFAULT_ENUM_POSTFIX = "UNSPECIFIED";
+
+ @Override
+ public String getDefaultNullEnumConstant(TypeElement anEnumType) {
+ if (isProtobufEnum(anEnumType)) {
+ final String prefix = upperCamelToUpperUnderscore(anEnumType.getSimpleName().toString());
+ return prefix + "_" + DEFAULT_ENUM_POSTFIX;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * The enum value if an unrecognizable input is read from a serialized proto blob.
+ */
+ private static final String UNPARSEABLE_ENUM_CONSTANT = "UNRECOGNIZED";
+
+ @Override
+ public String getEnumConstant(TypeElement anEnumType, String aSourceEnumValue) {
+ if (isProtobufEnum(anEnumType)) {
+ if (aSourceEnumValue == null) {
+ return getDefaultNullEnumConstant(anEnumType);
+ }
+ final String enumWithoutNamePrefix = removeEnumNamePrefixFromValueIfPossible(anEnumType, aSourceEnumValue);
+ if (UNPARSEABLE_ENUM_CONSTANT.equals(aSourceEnumValue)
+ || DEFAULT_ENUM_POSTFIX.equals(enumWithoutNamePrefix)) {
+ return MappingConstants.NULL;
+ }
+ return enumWithoutNamePrefix;
+ }
+
+ return aSourceEnumValue;
+ }
+
+ /**
+ * Removes the prefix expected according to Google's enum value prefixing rules from the provided enum value, if it
+ * is found to be present. If it's not present, this method will return the original value.
+ *
+ * @param anEnumType the enum type from which to generate the corresponding prefix
+ * @param anEnumValue the enum value to modify
+ * @return the modified value or the original value if the expected prefix was not found
+ */
+ private String removeEnumNamePrefixFromValueIfPossible(TypeElement anEnumType, String anEnumValue) {
+ final String prefix = upperCamelToUpperUnderscore(anEnumType.getSimpleName().toString());
+ return anEnumValue.replace(prefix + "_", "");
+ }
+
+ /**
+ * The interface signaling protobuf enums.
+ */
+ private static final String PROTOBUF_ENUM_INTERFACE_NAME = "com.google.protobuf.ProtocolMessageEnum";
+
+ /**
+ * The interface signaling protobuf lite enums.
+ */
+ private static final String PROTOBUF_LITE_ENUM_INTERFACE_NAME = "com.google.protobuf.Internal.EnumLite";
+
+ /**
+ * Checks whether the given {@link TypeElement} is a protobuf enum.
+ *
+ * @param anEnumType the enum type to check
+ * @return true if it's a protobuf enum, false otherwise
+ */
+ private boolean isProtobufEnum(TypeElement anEnumType) {
+ for (final TypeMirror implementedInterface : anEnumType.getInterfaces()) {
+ final String implementedInterfaceName = implementedInterface.toString();
+ if (PROTOBUF_ENUM_INTERFACE_NAME.equals(implementedInterfaceName)
+ || PROTOBUF_LITE_ENUM_INTERFACE_NAME.equals(implementedInterfaceName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Converts upper camel case to upper underscore case.
+ *
+ * @param anInput the upper camel case input
+ * @return the upper underscore output
+ */
+ private static String upperCamelToUpperUnderscore(String anInput) {
+ // Regular expression to find uppercase letters
+ final String regex = "([a-z])([A-Z]+)";
+ // Replacement pattern to add underscore before uppercase letters
+ final String replacement = "$1_$2";
+
+ final String result = anInput.replaceAll(regex, replacement);
+
+ return result.toUpperCase();
+ }
+
+}
diff --git a/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy b/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy
new file mode 100644
index 0000000..887037b
--- /dev/null
+++ b/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy
@@ -0,0 +1 @@
+de.firehead.mapstruct.spi.protobuf.accessors.ProtobufAccessorNamingStrategy
\ No newline at end of file
diff --git a/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.BuilderProvider b/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.BuilderProvider
new file mode 100644
index 0000000..d70a9f0
--- /dev/null
+++ b/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.BuilderProvider
@@ -0,0 +1 @@
+de.firehead.mapstruct.spi.protobuf.builderprovider.DelegatingBuilderProvider
\ No newline at end of file
diff --git a/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumMappingStrategy b/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumMappingStrategy
new file mode 100644
index 0000000..93d7e07
--- /dev/null
+++ b/mapstruct-spi-protobuf/src/main/resources/META-INF/services/org.mapstruct.ap.spi.EnumMappingStrategy
@@ -0,0 +1 @@
+de.firehead.mapstruct.spi.protobuf.enums.ProtobufEnumMappingStrategy
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..007ec43
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,248 @@
+
+
+ 4.0.0
+
+ de.firehead
+ mapstruct-spi-protobuf-parent
+ 1.0.0-SNAPSHOT
+ pom
+
+ Mapstruct SPI extension for mapping between protobuf and org.immutables
+ This Mapstruct SPI extension allows to perform seamless Mapstruct mapper generation to map between
+ protobuf and an org.immutables based data structures.
+
+ https://github.com/S1artie/mapstruct-spi-protobuf
+
+
+
+ MIT License
+ http://opensource.org/licenses/MIT
+
+
+
+
+
+ Rene Schneider
+ rene@firehead.de
+ firehead.de
+ https://github.com/S1artie
+
+
+
+
+ https://github.com/S1artie/mapstruct-spi-protobuf
+ scm:git:git@github.com:S1artie/mapstruct-spi-protobuf.git
+ scm:git:ssh://github.com:S1artie/mapstruct-spi-protobuf.git
+
+
+
+
+
+ ossrh
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+ mapstruct-spi-protobuf
+ mapstruct-spi-protobuf-test-immutables
+ mapstruct-spi-protobuf-test-pojo
+ mapstruct-spi-protobuf-test-protos
+ mapstruct-spi-protobuf-test-records
+
+
+
+ UTF-8
+ 17
+ 17
+
+ 1.5.5.Final
+ 2.0.13
+ 3.25.4
+ 1.65.0
+ 2.10.1
+ 5.11.0-M2
+
+
+
+
+
+
+ de.firehead
+ mapstruct-spi-protobuf-test-protos
+ ${project.version}
+
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf.version}
+
+
+ org.immutables
+ value
+ ${immutables.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+
+
+
+ dev.cookiecode
+ another-protobuf-maven-plugin
+ 2.1.0
+
+
+
+ test-compile
+ test-compile-custom
+
+ generate-sources
+
+
+ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+
+ grpc-java
+
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.7.0
+ true
+
+ ossrh
+ https://s01.oss.sonatype.org/
+ false
+
+
+
+
+
+
+
+
+ release
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.6.0
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+ flatten.clean
+ clean
+
+ clean
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ false
+
+
+
+
+
+
+