ImmutableValueMap.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2014 wcm.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package io.wcm.sling.commons.resource;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;

/**
 * {@link ValueMap} that does not support changing its content.
 * <p>
 * All methods that may change the content will throw a {@link UnsupportedOperationException}.
 * </p>
 * <p>
 * Static convenience methods provide similar behavior as Guava ImmutableMap variants.
 * </p>
 */
@ProviderType
public final class ImmutableValueMap implements ValueMap {

  private final ValueMap map;

  /**
   * @param map Value map
   */
  ImmutableValueMap(@NotNull ValueMap map) {
    this.map = map;
  }

  /**
   * @param map Map
   */
  ImmutableValueMap(@NotNull Map<String, Object> map) {
    this.map = new ValueMapDecorator(map);
  }

  @Override
  public @Nullable <T> T get(@NotNull String name, @NotNull Class<T> type) {
    return this.map.get(name, type);
  }

  @Override
  public @NotNull <T> T get(@NotNull String name, @NotNull T defaultValue) {
    return this.map.get(name, defaultValue);
  }

  @Override
  public int size() {
    return this.map.size();
  }

  @Override
  public boolean isEmpty() {
    return this.map.isEmpty();
  }

  @Override
  public boolean containsKey(Object key) {
    return this.map.containsKey(key);
  }

  @Override
  public boolean containsValue(Object value) {
    return this.map.containsValue(value);
  }

  @Override
  public Object get(Object key) {
    return this.map.get(key);
  }

  @Override
  public Set<String> keySet() {
    return this.map.keySet();
  }

  @Override
  public Collection<Object> values() {
    return this.map.values();
  }

  @Override
  public Set<Entry<String, Object>> entrySet() {
    return Collections.unmodifiableSet(this.map.entrySet());
  }

  @Override
  public int hashCode() {
    return this.map.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof ImmutableValueMap)) {
      return false;
    }
    return this.map.entrySet().equals(((ImmutableValueMap)obj).map.entrySet());
  }

  @Override
  public String toString() {
    return new TreeMap<>(map).toString();
  }

  // mutable operations not supported
  /**
   * @deprecated Unsupported operation
   */
  @Override
  @Deprecated(since = "1.0.0")
  public Object put(String key, Object value) {
    throw new UnsupportedOperationException();
  }

  /**
   * @deprecated Unsupported operation
   */
  @Override
  @Deprecated(since = "1.0.0")
  public Object remove(Object key) {
    throw new UnsupportedOperationException();
  }

  /**
   * @deprecated Unsupported operation
   */
  @Override
  @Deprecated(since = "1.0.0")
  public void putAll(Map<? extends String, ? extends Object> m) {
    throw new UnsupportedOperationException();
  }

  /**
   * @deprecated Unsupported operation
   */
  @Override
  @Deprecated(since = "1.0.0")
  public void clear() {
    throw new UnsupportedOperationException();
  }


  /**
   * Returns the empty map. This map behaves and performs comparably to {@link Collections#emptyMap}, and is preferable
   * mainly for consistency
   * and maintainability of your code.
   * @return ImmutableValueMap
   */
  public static @NotNull ImmutableValueMap of() {
    return new ImmutableValueMap(EMPTY);
  }

  /**
   * Returns an immutable map containing a single entry. This map behaves and
   * performs comparably to {@link Collections#singletonMap} but will not accept
   * a null key or value. It is preferable mainly for consistency and
   * maintainability of your code.
   * @param k1 Key 1
   * @param v1 Value 1
   * @return ImmutableValueMap
   */
  public static @NotNull ImmutableValueMap of(@NotNull String k1, @NotNull Object v1) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put(k1, v1);
    return new ImmutableValueMap(Collections.unmodifiableMap(map));
  }

  /**
   * Returns an immutable map containing the given entries, in order.
   * @param k1 Key 1
   * @param v1 Value 1
   * @param k2 Key 2
   * @param v2 Value 2
   * @return ImmutableValueMap
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  public static @NotNull ImmutableValueMap of(@NotNull String k1, @NotNull Object v1,
      @NotNull String k2, @NotNull Object v2) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put(k1, v1);
    map.put(k2, v2);
    return new ImmutableValueMap(Collections.unmodifiableMap(map));
  }

  /**
   * Returns an immutable map containing the given entries, in order.
   * @param k1 Key 1
   * @param v1 Value 1
   * @param k2 Key 2
   * @param v2 Value 2
   * @param k3 Key 3
   * @param v3 Value 3
   * @return ImmutableValueMap
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  public static @NotNull ImmutableValueMap of(
      @NotNull String k1, @NotNull Object v1,
      @NotNull String k2, @NotNull Object v2,
      @NotNull String k3, @NotNull Object v3) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put(k1, v1);
    map.put(k2, v2);
    map.put(k3, v3);
    return new ImmutableValueMap(Collections.unmodifiableMap(map));
  }

  /**
   * Returns an immutable map containing the given entries, in order.
   * @param k1 Key 1
   * @param v1 Value 1
   * @param k2 Key 2
   * @param v2 Value 2
   * @param k3 Key 3
   * @param v3 Value 3
   * @param k4 Key 4
   * @param v4 Value 4
   * @return ImmutableValueMap
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  @SuppressWarnings({ "java:S107", "PMD.UseObjectForClearerAPI" })
  public static @NotNull ImmutableValueMap of(
      @NotNull String k1, @NotNull Object v1,
      @NotNull String k2, @NotNull Object v2,
      @NotNull String k3, @NotNull Object v3,
      @NotNull String k4, @NotNull Object v4) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put(k1, v1);
    map.put(k2, v2);
    map.put(k3, v3);
    map.put(k4, v4);
    return new ImmutableValueMap(Collections.unmodifiableMap(map));
  }

  /**
   * Returns an immutable map containing the given entries, in order.
   * @param k1 Key 1
   * @param v1 Value 1
   * @param k2 Key 2
   * @param v2 Value 2
   * @param k3 Key 3
   * @param v3 Value 3
   * @param k4 Key 4
   * @param v4 Value 4
   * @param k5 Key 5
   * @param v5 Value 5
   * @return ImmutableValueMap
   * @throws IllegalArgumentException if duplicate keys are provided
   */
  @SuppressWarnings({ "java:S107", "PMD.UseObjectForClearerAPI" })
  public static ImmutableValueMap of(
      @NotNull String k1, @NotNull Object v1,
      @NotNull String k2, @NotNull Object v2,
      @NotNull String k3, @NotNull Object v3,
      @NotNull String k4, @NotNull Object v4,
      @NotNull String k5, @NotNull Object v5) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put(k1, v1);
    map.put(k2, v2);
    map.put(k3, v3);
    map.put(k4, v4);
    map.put(k5, v5);
    return new ImmutableValueMap(Collections.unmodifiableMap(map));
  }

  // looking for of() with > 5 entries? Use the builder instead.

  /**
   * Returns a new builder. The generated builder is equivalent to the builder
   * created by the {@link Builder} constructor.
   * @return Builder
   */
  public static @NotNull Builder builder() {
    return new Builder();
  }

  /**
   * Returns an immutable map containing the same entries as {@code map}. If {@code map} somehow contains entries with
   * duplicate keys (for example, if
   * it is a {@code SortedMap} whose comparator is not <i>consistent with
   * equals</i>), the results of this method are undefined.
   * <p>
   * Despite the method name, this method attempts to avoid actually copying the data when it is safe to do so. The
   * exact circumstances under which a copy will or will not be performed are undocumented and subject to change.
   * </p>
   * @param map Map
   * @return ImmutableValueMap
   * @throws NullPointerException if any key or value in {@code map} is null
   */
  public static @NotNull ImmutableValueMap copyOf(@NotNull Map<String, Object> map) {
    return new ImmutableValueMap(Collections.unmodifiableMap(map));
  }

  /**
   * Builder interface for {@link ImmutableValueMap}.
   */
  public static final class Builder {

    private final @NotNull Map<String, Object> map = new LinkedHashMap<>();

    /**
     * Associates {@code key} with {@code value} in the built map. Duplicate
     * keys are not allowed, and will cause {@link #build} to fail.
     * @param key Key
     * @param value value
     * @return this
     */
    public @NotNull Builder put(@NotNull String key, @NotNull Object value) {
      map.put(key, value);
      return this;
    }

    /**
     * Adds the given {@code entry} to the map, making it immutable if
     * necessary. Duplicate keys are not allowed, and will cause {@link #build} to fail.
     * @param entry Entry
     * @return this
     */
    public @NotNull Builder put(@NotNull Entry<String, Object> entry) {
      return put(entry.getKey(), entry.getValue());
    }

    /**
     * Associates all of the given map's keys and values in the built map.
     * Duplicate keys are not allowed, and will cause {@link #build} to fail.
     * @param value Value
     * @return this
     * @throws NullPointerException if any key or value in {@code map} is null
     */
    public @NotNull Builder putAll(@NotNull Map<String, Object> value) {
      map.putAll(value);
      return this;
    }

    /**
     * Returns a newly-created immutable map.
     * @return ImmutableValueMap
     * @throws IllegalArgumentException if duplicate keys were added
     */
    public @NotNull ImmutableValueMap build() {
      if (map.isEmpty()) {
        return of();
      }
      else {
        return new ImmutableValueMap(map);
      }
    }
  }

}