Skip to content

Improve support for Map attributes for immutables  #3089

@GVladi

Description

@GVladi

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions