Search code examples
javamultithreadingdictionarythread-safetyfinal

How to make sure my map is never modified once set in my Builder pattern?


Below is my builder pattern which I am using in one of my projects and I wanted to make it thread safe in multithreading environment. Once the ClientKey is set, I don't want anyone to modify it again.

public final class ClientKey {

    private final long userId;
    private final int clientId;
    private final long timeout;
    private final boolean dataFlag;
    // how can I make sure that my parameterMap is never modified once set
    private final Map<String, String> parameterMap;

    private ClientKey(Builder builder) {
        this.userId = builder.userId;
        this.clientId = builder.clientId;
        this.remoteFlag = builder.remoteFlag;
        this.dataFlag = builder.dataFlag;
        this.parameterMap = builder.parameterMap;
        this.timeout = builder.timeout;
    }

    public static class Builder {
        protected final long userId;
        protected final int clientId;
        protected long timeout = 200L;
        protected boolean remoteFlag = false;
        protected boolean dataFlag = true;
        protected Map<String, String> parameterMap;

        public Builder(long userId, int clientId) {
            this.userId = userId;
            this.clientId = clientId;
        }

        public Builder parameterMap(Map<String, String> parameterMap) {
            this.parameterMap = parameterMap;
            return this;
        }

        public Builder remoteFlag(boolean remoteFlag) {
            this.remoteFlag = remoteFlag;
            return this;
        }

        public Builder dataFlag(boolean dataFlag) {
            this.dataFlag = dataFlag;
            return this;
        }

        public Builder addTimeout(long timeout) {
            this.timeout = timeout;
            return this;
        }

        public ClientKey build() {
            return new ClientKey(this);
        }
    }

    public long getUserId() {
        return userId;
    }

    public int getClientId() {
        return clientId;
    }

    public long getTimeout() {
        return timeout;
    }

    public Map<String, String> getParameterMap() {
        return parameterMap;
    }

    public boolean isDataFlag() {
        return dataFlag;
    }
}

In my above ClientKey, sometimes I will pass parameterMap while making ClientKey object but sometimes, it will be null. It is not always that we will set parameterMap. And below is the way I am iterating parameterMap in my application code once ClientKey object is set and passed around in my application code:

final Map<String, String> parameterMap = clientKey.getParameterMap();
if (!MapUtils.isEmpty(parameterMap)) {
    Set<Entry<String, String>> params = parameterMap.entrySet();

    for (Entry<String, String> e : params) {
        url.append("&").append(e.getKey());
        url.append("=").append(e.getValue());
    }
}

Question is - How can I make sure that my parameterMap is never modified in between once it has been set. As you can see once I have build the ClientKey object, the parameterMap is only ever read and never modified. However it relies on the fact that no malicious or buggy client isn't trying to modify in the meantime so what's the best way to protect against that?


Solution

  • In such cases I recommend using Guava's ImmutableMap. I also suggest avoiding null value for parameterMap field and use an empty ImmutableMap instead. For that you could also use its Builder.

    So your code fragment would become:

    public class ClientKey {
       ...
       private final ImmutableMap<String, String> parameterMap;
    
       public ClientKey(Builder builder) {
           ...
           this.parameterMap = builder.parameterMap.build();
           ...
       }
    
       public static class Builder {
            ...
            protected ImmutableMap.Builder<String, String> parameterMap = ImmutableMap.builder();
    
            public Builder parameterMap(Map<String, String> parameterMap) {
                this.parameterMap.putAll(parameterMap);
                return this;
            }
    
            public Builder addParameter(String key, String value) {
                this.parameterMap.put(key, value);
                return this;
            }
         }
     }
    

    Using immutable map will make it clear to the ClientKey users that it cannot be modified.