/*
* Copyright 2017-present Facebook, Inc.
*
* 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.
*/
package com.facebook.buck.util;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Ordering;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* {@link ImmutableMap} uses 16 fewer bytes per entry than {@link java.util.TreeMap}, but does not
* allow null values. This wrapper class lets us have our cake and eat it too -- we use a sentinel
* object in the underlying {@link ImmutableMap} and translate it on any read path.
*/
public final class ImmutableMapWithNullValues<K, V> extends AbstractMap<K, V> {
private static final Object NULL = new Object();
private final Map<K, Object> delegate;
private ImmutableMapWithNullValues(Map<K, Object> delegate) {
this.delegate = delegate;
}
@Override
@Nullable
@SuppressWarnings("unchecked")
public V get(@Nullable Object key) {
Object result = delegate.get(key);
if (result == NULL) {
return null;
}
return (V) result;
}
@Override
@SuppressWarnings("unchecked")
public Collection<V> values() {
return delegate
.values()
.stream()
.map(v -> v == NULL ? null : (V) v)
.collect(Collectors.toList());
}
@Override
@SuppressWarnings("unchecked")
public Set<Entry<K, V>> entrySet() {
return delegate
.entrySet()
.stream()
.map(
e ->
e.getValue() == NULL
? new AbstractMap.SimpleEntry<K, V>(e.getKey(), null)
: (Map.Entry<K, V>) e)
// Use ImmutableSet instead of Set here to preserve iteration order:
.collect(MoreCollectors.toImmutableSet());
}
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Map)) {
return false;
}
return entrySet().equals(((Map<K, V>) other).entrySet());
}
@Override
public int hashCode() {
return entrySet().hashCode();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{");
Joiner.on(", ").useForNull("null").withKeyValueSeparator("=").appendTo(builder, this);
builder.append("}");
return builder.toString();
}
public static class Builder<K, V> {
private final ImmutableMap.Builder<K, Object> builder;
public static <K, V> Builder<K, V> insertionOrder() {
return new Builder<>(new ImmutableMap.Builder<K, Object>());
}
public static <K extends Comparable<?>, V> Builder<K, V> sorted() {
return new Builder<>(new ImmutableSortedMap.Builder<>(Ordering.natural()));
}
private Builder(ImmutableMap.Builder<K, Object> builder) {
this.builder = builder;
}
public Builder<K, V> put(K key, @Nullable V value) {
builder.put(key, value == null ? NULL : value);
return this;
}
public ImmutableMapWithNullValues<K, V> build() {
return new ImmutableMapWithNullValues<K, V>(builder.build());
}
}
}