Improve support for Map attributes for immutables

Expected behavior

The mapstruct should not produce a "Unmapped target properties" for "put" and "putAll" methods.

Actual behavior

The mapstruct supports org.immutables objects already. In case an object has got a java.util.Map attributes like:
public abstract Map<String, String> getAttributes();

the immutables generates the following methods in the Builder class for this attribute:
public final Builder putAttributes(String key, String value)
public final Builder putAttributes(Map.Entry<String, ? extends String> entry)
public final Builder attributes(Map<String, ? extends String> entries)
public final Builder putAllAttributes(Map<String, ? extends String> entries)

The mapstruct produces a Warning for "put" and "putAll" methods:
WARNING ItemMapper.java:24 Unmapped target properties: "putAttributes putAllAttributes".
as it believes they are fluentSetter.

As a workaround, the warnings can be suppressed by:
@Mapping(target = "putAttributes", ignore = true)

Steps to reproduce the problem

To reproduce the issue, I have add Map<String, String> getAttributes() to the org.mapstruct.ap.test.bugs._1801.domain.Item:

Index: processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java
--- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java	(revision fd27380185fe83feb6d26308c7d358025783ff8f)
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java	(date 1669213502716)
@@ -7,7 +7,10 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 
 /**
  * Immutable implementation of {@link ItemDTO}.
@@ -19,9 +22,11 @@
  */
 public final class ImmutableItemDTO extends ItemDTO {
     private final String id;
+    private final Map<String, String> attributes;
 
-    private ImmutableItemDTO(String id) {
+    private ImmutableItemDTO(String id, Map<String, String> attributes) {
         this.id = id;
+        this.attributes = attributes;
     }
 
     /**
@@ -32,84 +37,103 @@
         return id;
     }
 
+    /**
+     * @return The value of the {@code attributes} attribute
+     */
+    @Override
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
     /**
      * Copy the current immutable object by setting a value for the {@link ItemDTO#getId() id} attribute.
-     * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
-     *
+     * An equals check used to prevent copying of the same value by returning {@code this}.
      * @param value A new value for id
-     *
      * @return A modified copy of the {@code this} object
      */
-    public ImmutableItemDTO withId(String value) {
-        if ( Objects.equals( this.id, value ) ) {
-            return this;
-        }
-        return new ImmutableItemDTO( value );
+    public final ImmutableItemDTO withId(String value) {
+        String newValue = Objects.requireNonNull(value, "id");
+        if (this.id.equals(newValue)) return this;
+        return new ImmutableItemDTO(newValue, this.attributes);
+    }
+
+    /**
+     * Copy the current immutable object by replacing the {@link ItemDTO#getAttributes() attributes} map with the specified map.
+     * Nulls are not permitted as keys or values.
+     * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
+     * @param entries The entries to be added to the attributes map
+     * @return A modified copy of {@code this} object
+     */
+    public final ImmutableItemDTO withAttributes(Map<String, ? extends String> entries) {
+        if (this.attributes == entries) return this;
+        Map<String, String> newValue = createUnmodifiableMap(true, false, entries);
+        return new ImmutableItemDTO(this.id, newValue);
     }
 
     /**
      * This instance is equal to all instances of {@code ImmutableItemDTO} that have equal attribute values.
-     *
      * @return {@code true} if {@code this} is equal to {@code another} instance
      */
     @Override
     public boolean equals(Object another) {
-        if ( this == another ) {
-            return true;
-        }
+        if (this == another) return true;
         return another instanceof ImmutableItemDTO
-            && equalTo( (ImmutableItemDTO) another );
+                && equalTo((ImmutableItemDTO) another);
     }
 
     private boolean equalTo(ImmutableItemDTO another) {
-        return id.equals( another.id );
+        return id.equals(another.id)
+                && attributes.equals(another.attributes);
     }
 
     /**
-     * Computes a hash code from attributes: {@code id}.
-     *
+     * Computes a hash code from attributes: {@code id}, {@code attributes}.
      * @return hashCode value
      */
     @Override
     public int hashCode() {
         int h = 5381;
-        h += ( h << 5 ) + id.hashCode();
+        h += (h << 5) + id.hashCode();
+        h += (h << 5) + attributes.hashCode();
         return h;
     }
 
     /**
-     * Prints the immutable value {@code ItemDTO} with attribute values.
-     *
+     * Prints the immutable value {@code Item} with attribute values.
      * @return A string representation of the value
      */
     @Override
     public String toString() {
-        return "ItemDTO{"
-            + "id=" + id
-            + "}";
+        return "Item{"
+                + "id=" + id
+                + ", attributes=" + attributes
+                + "}";
     }
 
     /**
      * Creates an immutable copy of a {@link ItemDTO} value.
      * Uses accessors to get values to initialize the new immutable instance.
      * If an instance is already immutable, it is returned as is.
-     *
      * @param instance The instance to copy
-     *
-     * @return A copied immutable ItemDTO instance
+     * @return A copied immutable Item instance
      */
     public static ImmutableItemDTO copyOf(ItemDTO instance) {
-        if ( instance instanceof ImmutableItemDTO ) {
+        if (instance instanceof ImmutableItemDTO) {
             return (ImmutableItemDTO) instance;
         }
         return ImmutableItemDTO.builder()
-            .from( instance )
-            .build();
+                .from(instance)
+                .build();
     }
 
     /**
      * Creates a builder for {@link ImmutableItemDTO ImmutableItemDTO}.
-     *
+     * <pre>
+     * ImmutableItemDTO.builder()
+     *    .id(String) // required {@link ItemDTO#getId() id}
+     *    .putAttributes|putAllAttributes(String =&gt; String) // {@link ItemDTO#getAttributes() attributes} mappings
+     *    .build();
+     * </pre>
      * @return A new ImmutableItemDTO builder
      */
     public static ImmutableItemDTO.Builder builder() {
@@ -123,62 +147,149 @@
      * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
      * but instead used immediately to create instances.</em>
      */
-    public static final class Builder {
+    public static class Builder {
         private static final long INIT_BIT_ID = 0x1L;
         private long initBits = 0x1L;
 
         private String id;
+        private Map<String, String> attributes = new LinkedHashMap<String, String>();
 
-        private Builder() {
+        public Builder() {
         }
 
         /**
-         * Fill a builder with attribute values from the provided {@code ItemDTO} instance.
+         * Fill a builder with attribute values from the provided {@code Item} instance.
          * Regular attribute values will be replaced with those from the given instance.
          * Absent optional values will not replace present values.
-         *
+         * Collection elements and entries will be added, not replaced.
          * @param instance The instance from which to copy values
-         *
          * @return {@code this} builder for use in a chained invocation
          */
-        public Builder from(ItemDTO instance) {
-            id( instance.getId() );
+        public final ImmutableItemDTO.Builder from(ItemDTO instance) {
+            Objects.requireNonNull(instance, "instance");
+            id(instance.getId());
+            putAllAttributes(instance.getAttributes());
             return this;
         }
 
         /**
          * Initializes the value for the {@link ItemDTO#getId() id} attribute.
-         *
          * @param id The value for id
-         *
          * @return {@code this} builder for use in a chained invocation
          */
-        public Builder id(String id) {
-            this.id = id;
+        public final ImmutableItemDTO.Builder id(String id) {
+            this.id = Objects.requireNonNull(id, "id");
             initBits &= ~INIT_BIT_ID;
             return this;
         }
 
+        /**
+         * Put one entry to the {@link ItemDTO#getAttributes() attributes} map.
+         * @param key The key in the attributes map
+         * @param value The associated value in the attributes map
+         * @return {@code this} builder for use in a chained invocation
+         */
+        public final ImmutableItemDTO.Builder putAttributes(String key, String value) {
+            this.attributes.put(
+                    Objects.requireNonNull(key, "attributes key"),
+                    Objects.requireNonNull(value, "attributes value"));
+            return this;
+        }
+
+        /**
+         * Put one entry to the {@link ItemDTO#getAttributes() attributes} map. Nulls are not permitted
+         * @param entry The key and value entry
+         * @return {@code this} builder for use in a chained invocation
+         */
+        public final ImmutableItemDTO.Builder putAttributes(Map.Entry<String, ? extends String> entry) {
+            String k = entry.getKey();
+            String v = entry.getValue();
+            this.attributes.put(
+                    Objects.requireNonNull(k, "attributes key"),
+                    Objects.requireNonNull(v, "attributes value"));
+            return this;
+        }
+
+        /**
+         * Sets or replaces all mappings from the specified map as entries for the {@link ItemDTO#getAttributes() attributes} map. Nulls are not permitted
+         * @param entries The entries that will be added to the attributes map
+         * @return {@code this} builder for use in a chained invocation
+         */
+        public final ImmutableItemDTO.Builder attributes(Map<String, ? extends String> entries) {
+            this.attributes.clear();
+            return putAllAttributes(entries);
+        }
+
+        /**
+         * Put all mappings from the specified map as entries to {@link ItemDTO#getAttributes() attributes} map. Nulls are not permitted
+         * @param entries The entries that will be added to the attributes map
+         * @return {@code this} builder for use in a chained invocation
+         */
+        public final ImmutableItemDTO.Builder putAllAttributes(Map<String, ? extends String> entries) {
+            for (Map.Entry<String, ? extends String> e : entries.entrySet()) {
+                String k = e.getKey();
+                String v = e.getValue();
+                this.attributes.put(
+                        Objects.requireNonNull(k, "attributes key"),
+                        Objects.requireNonNull(v, "attributes value"));
+            }
+            return this;
+        }
+
         /**
          * Builds a new {@link ImmutableItemDTO ImmutableItemDTO}.
-         *
-         * @return An immutable instance of ItemDTO
-         *
+         * @return An immutable instance of Item
          * @throws java.lang.IllegalStateException if any required attributes are missing
          */
         public ImmutableItemDTO build() {
-            if ( initBits != 0 ) {
-                throw new IllegalStateException( formatRequiredAttributesMessage() );
+            if (initBits != 0) {
+                throw new IllegalStateException(formatRequiredAttributesMessage());
             }
-            return new ImmutableItemDTO( id );
+            return new ImmutableItemDTO(id, createUnmodifiableMap(false, false, attributes));
         }
 
         private String formatRequiredAttributesMessage() {
             List<String> attributes = new ArrayList<>();
-            if ( ( initBits & INIT_BIT_ID ) != 0 ) {
-                attributes.add( "id" );
-            }
-            return "Cannot build ItemDTO, some of required attributes are not set " + attributes;
+            if ((initBits & INIT_BIT_ID) != 0) attributes.add("id");
+            return "Cannot build Item, some of required attributes are not set " + attributes;
+        }
+    }
+
+    private static <K, V> Map<K, V> createUnmodifiableMap(boolean checkNulls, boolean skipNulls, Map<? extends K, ? extends V> map) {
+        switch (map.size()) {
+            case 0: return Collections.emptyMap();
+            case 1: {
+                Map.Entry<? extends K, ? extends V> e = map.entrySet().iterator().next();
+                K k = e.getKey();
+                V v = e.getValue();
+                if (checkNulls) {
+                    Objects.requireNonNull(k, "key");
+                    Objects.requireNonNull(v, "value");
+                }
+                if (skipNulls && (k == null || v == null)) {
+                    return Collections.emptyMap();
+                }
+                return Collections.singletonMap(k, v);
+            }
+            default: {
+                Map<K, V> linkedMap = new LinkedHashMap<>(map.size());
+                if (skipNulls || checkNulls) {
+                    for (Map.Entry<? extends K, ? extends V> e : map.entrySet()) {
+                        K k = e.getKey();
+                        V v = e.getValue();
+                        if (skipNulls) {
+                            if (k == null || v == null) continue;
+                        } else if (checkNulls) {
+                            Objects.requireNonNull(k, "key");
+                            Objects.requireNonNull(v, "value");
+                        }
+                        linkedMap.put(k, v);
+                    }
+                } else {
+                    linkedMap.putAll(map);
+                }
+                return Collections.unmodifiableMap(linkedMap);
+            }
         }
     }
 }
Index: processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java
--- a/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java	(revision fd27380185fe83feb6d26308c7d358025783ff8f)
+++ b/processor/src/main/java/org/mapstruct/ap/spi/ImmutablesAccessorNamingStrategy.java	(date 1669216681078)
@@ -21,7 +21,19 @@
 
     @Override
     protected boolean isFluentSetter(ExecutableElement method) {
-        return super.isFluentSetter( method ) && !method.getSimpleName().toString().equals( "from" );
+        return super.isFluentSetter( method ) && !method.getSimpleName().toString().equals( "from" )
+                // TODO uncomment the fix
+                // && !isPutterWithUpperCase4thCharacter( method )
+                ;
+    }
+
+    private boolean isPutterWithUpperCase4thCharacter(ExecutableElement method) {
+        return isPutterMethod( method ) && Character.isUpperCase( method.getSimpleName().toString().charAt( 3 ) );
+    }
+
+    public boolean isPutterMethod(ExecutableElement method) {
+        String methodName = method.getSimpleName().toString();
+        return methodName.startsWith( "put" ) && methodName.length() > 3;
     }
 
 }
Index: processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java
--- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java	(revision fd27380185fe83feb6d26308c7d358025783ff8f)
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java	(date 1669212960416)
@@ -5,9 +5,13 @@
  */
 package org.mapstruct.ap.test.bugs._1801.dto;
 
+import java.util.Map;
+
 /**
  * @author Zhizhi Deng
  */
 public abstract class ItemDTO {
     public abstract String getId();
+
+    public abstract Map<String, String> getAttributes();
 }
Index: processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java
--- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java	(revision fd27380185fe83feb6d26308c7d358025783ff8f)
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java	(date 1669212834384)
@@ -5,11 +5,15 @@
  */
 package org.mapstruct.ap.test.bugs._1801.domain;
 
+import java.util.Map;
+
 /**
  * @author Zhizhi Deng
  */
 public abstract class Item {
     public abstract String getId();
 
+    public abstract Map<String, String> getAttributes();
+
     public static class Builder extends ImmutableItem.Builder { }
 }
Index: processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java
--- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java	(revision fd27380185fe83feb6d26308c7d358025783ff8f)
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java	(date 1669213623229)
@@ -11,6 +11,8 @@
 import org.mapstruct.ap.test.bugs._1801.dto.ItemDTO;
 import org.mapstruct.factory.Mappers;
 
+import java.util.Map;
+
 /**
  * @author Zhizhi Deng
  */
@@ -20,4 +22,6 @@
     public static final ItemMapper INSTANCE = Mappers.getMapper( ItemMapper.class );
 
     public abstract Item map(ItemDTO itemDTO);
+
+    public Map<String, String> map(Map<String, String> from) { return from; }
 }
Index: processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java
--- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java	(revision fd27380185fe83feb6d26308c7d358025783ff8f)
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java	(date 1669212872768)
@@ -5,8 +5,12 @@
  */
 package org.mapstruct.ap.test.bugs._1801.domain;
 
-import java.util.ArrayList;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Objects;
 import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
 
 /**
  * Immutable implementation of {@link Item}.
@@ -19,9 +23,11 @@
 @SuppressWarnings({"all"})
 public final class ImmutableItem extends Item {
   private final String id;
+  private final Map<String, String> attributes;
 
-  private ImmutableItem(String id) {
+  private ImmutableItem(String id, Map<String, String> attributes) {
     this.id = id;
+    this.attributes = attributes;
   }
 
   /**
@@ -32,15 +38,37 @@
     return id;
   }
 
+  /**
+   * @return The value of the {@code attributes} attribute
+   */
+  @Override
+  public Map<String, String> getAttributes() {
+    return attributes;
+  }
+
   /**
    * Copy the current immutable object by setting a value for the {@link Item#getId() id} attribute.
-   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
+   * An equals check used to prevent copying of the same value by returning {@code this}.
    * @param value A new value for id
    * @return A modified copy of the {@code this} object
    */
   public final ImmutableItem withId(String value) {
-    if (this.id == value) return this;
-    return new ImmutableItem(value);
+    String newValue = Objects.requireNonNull(value, "id");
+    if (this.id.equals(newValue)) return this;
+    return new ImmutableItem(newValue, this.attributes);
+  }
+
+  /**
+   * Copy the current immutable object by replacing the {@link Item#getAttributes() attributes} map with the specified map.
+   * Nulls are not permitted as keys or values.
+   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
+   * @param entries The entries to be added to the attributes map
+   * @return A modified copy of {@code this} object
+   */
+  public final ImmutableItem withAttributes(Map<String, ? extends String> entries) {
+    if (this.attributes == entries) return this;
+    Map<String, String> newValue = createUnmodifiableMap(true, false, entries);
+    return new ImmutableItem(this.id, newValue);
   }
 
   /**
@@ -51,21 +79,23 @@
   public boolean equals(Object another) {
     if (this == another) return true;
     return another instanceof ImmutableItem
-        && equalTo((ImmutableItem) another);
+            && equalTo((ImmutableItem) another);
   }
 
   private boolean equalTo(ImmutableItem another) {
-    return id.equals(another.id);
+    return id.equals(another.id)
+            && attributes.equals(another.attributes);
   }
 
   /**
-   * Computes a hash code from attributes: {@code id}.
+   * Computes a hash code from attributes: {@code id}, {@code attributes}.
    * @return hashCode value
    */
   @Override
   public int hashCode() {
     int h = 5381;
     h += (h << 5) + id.hashCode();
+    h += (h << 5) + attributes.hashCode();
     return h;
   }
 
@@ -76,8 +106,9 @@
   @Override
   public String toString() {
     return "Item{"
-        + "id=" + id
-        + "}";
+            + "id=" + id
+            + ", attributes=" + attributes
+            + "}";
   }
 
   /**
@@ -91,13 +122,29 @@
     if (instance instanceof ImmutableItem) {
       return (ImmutableItem) instance;
     }
-    return new Builder()
-        .from(instance)
-        .build();
+    return ImmutableItem.builder()
+            .from(instance)
+            .build();
   }
 
+  /**
+   * Creates a builder for {@link ImmutableItem ImmutableItem}.
+   * <pre>
+   * ImmutableItem.builder()
+   *    .id(String) // required {@link Item#getId() id}
+   *    .putAttributes|putAllAttributes(String =&gt; String) // {@link Item#getAttributes() attributes} mappings
+   *    .build();
+   * </pre>
+   * @return A new ImmutableItem builder
+   */
+  public static ImmutableItem.Builder builder() {
+    return new ImmutableItem.Builder();
+  }
+
   /**
    * Builds instances of type {@link ImmutableItem ImmutableItem}.
+   * Initialize attributes and then invoke the {@link #build()} method to create an
+   * immutable instance.
    * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
    * but instead used immediately to create instances.</em>
    */
@@ -106,33 +153,90 @@
     private long initBits = 0x1L;
 
     private String id;
+    private Map<String, String> attributes = new LinkedHashMap<String, String>();
 
-    Builder() {
+    public Builder() {
     }
 
     /**
      * Fill a builder with attribute values from the provided {@code Item} instance.
      * Regular attribute values will be replaced with those from the given instance.
      * Absent optional values will not replace present values.
+     * Collection elements and entries will be added, not replaced.
      * @param instance The instance from which to copy values
      * @return {@code this} builder for use in a chained invocation
      */
     public final Builder from(Item instance) {
+      Objects.requireNonNull(instance, "instance");
       id(instance.getId());
+      putAllAttributes(instance.getAttributes());
       return this;
     }
 
     /**
      * Initializes the value for the {@link Item#getId() id} attribute.
-     * @param id The value for id 
+     * @param id The value for id
      * @return {@code this} builder for use in a chained invocation
      */
     public final Builder id(String id) {
-      this.id = id;
+      this.id = Objects.requireNonNull(id, "id");
       initBits &= ~INIT_BIT_ID;
       return this;
     }
 
+    /**
+     * Put one entry to the {@link Item#getAttributes() attributes} map.
+     * @param key The key in the attributes map
+     * @param value The associated value in the attributes map
+     * @return {@code this} builder for use in a chained invocation
+     */
+    public final Builder putAttributes(String key, String value) {
+      this.attributes.put(
+              Objects.requireNonNull(key, "attributes key"),
+              Objects.requireNonNull(value, "attributes value"));
+      return this;
+    }
+
+    /**
+     * Put one entry to the {@link Item#getAttributes() attributes} map. Nulls are not permitted
+     * @param entry The key and value entry
+     * @return {@code this} builder for use in a chained invocation
+     */
+    public final Builder putAttributes(Map.Entry<String, ? extends String> entry) {
+      String k = entry.getKey();
+      String v = entry.getValue();
+      this.attributes.put(
+              Objects.requireNonNull(k, "attributes key"),
+              Objects.requireNonNull(v, "attributes value"));
+      return this;
+    }
+
+    /**
+     * Sets or replaces all mappings from the specified map as entries for the {@link Item#getAttributes() attributes} map. Nulls are not permitted
+     * @param entries The entries that will be added to the attributes map
+     * @return {@code this} builder for use in a chained invocation
+     */
+    public final Builder attributes(Map<String, ? extends String> entries) {
+      this.attributes.clear();
+      return putAllAttributes(entries);
+    }
+
+    /**
+     * Put all mappings from the specified map as entries to {@link Item#getAttributes() attributes} map. Nulls are not permitted
+     * @param entries The entries that will be added to the attributes map
+     * @return {@code this} builder for use in a chained invocation
+     */
+    public final Builder putAllAttributes(Map<String, ? extends String> entries) {
+      for (Map.Entry<String, ? extends String> e : entries.entrySet()) {
+        String k = e.getKey();
+        String v = e.getValue();
+        this.attributes.put(
+                Objects.requireNonNull(k, "attributes key"),
+                Objects.requireNonNull(v, "attributes value"));
+      }
+      return this;
+    }
+
     /**
      * Builds a new {@link ImmutableItem ImmutableItem}.
      * @return An immutable instance of Item
@@ -142,13 +246,51 @@
       if (initBits != 0) {
         throw new IllegalStateException(formatRequiredAttributesMessage());
       }
-      return new ImmutableItem(id);
+      return new ImmutableItem(id, createUnmodifiableMap(false, false, attributes));
     }
 
     private String formatRequiredAttributesMessage() {
-      List<String> attributes = new ArrayList<String>();
+      List<String> attributes = new ArrayList<>();
       if ((initBits & INIT_BIT_ID) != 0) attributes.add("id");
       return "Cannot build Item, some of required attributes are not set " + attributes;
     }
   }
-}
+
+  private static <K, V> Map<K, V> createUnmodifiableMap(boolean checkNulls, boolean skipNulls, Map<? extends K, ? extends V> map) {
+    switch (map.size()) {
+      case 0: return Collections.emptyMap();
+      case 1: {
+        Map.Entry<? extends K, ? extends V> e = map.entrySet().iterator().next();
+        K k = e.getKey();
+        V v = e.getValue();
+        if (checkNulls) {
+          Objects.requireNonNull(k, "key");
+          Objects.requireNonNull(v, "value");
+        }
+        if (skipNulls && (k == null || v == null)) {
+          return Collections.emptyMap();
+        }
+        return Collections.singletonMap(k, v);
+      }
+      default: {
+        Map<K, V> linkedMap = new LinkedHashMap<>(map.size());
+        if (skipNulls || checkNulls) {
+          for (Map.Entry<? extends K, ? extends V> e : map.entrySet()) {
+            K k = e.getKey();
+            V v = e.getValue();
+            if (skipNulls) {
+              if (k == null || v == null) continue;
+            } else if (checkNulls) {
+              Objects.requireNonNull(k, "key");
+              Objects.requireNonNull(v, "value");
+            }
+            linkedMap.put(k, v);
+          }
+        } else {
+          linkedMap.putAll(map);
+        }
+        return Collections.unmodifiableMap(linkedMap);
+      }
+    }
+  }
+}
\ No newline at end of file

MapStruct Version

Mapstruct 1.5.3