/**
* This file is part of Obsidian, licensed under the MIT License (MIT).
*
* Copyright (c) 2013-2014 ObsidianBox <http://obsidianbox.org/>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.obsidianbox.obsidian.util.map;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.obsidianbox.magma.util.map.DefaultedKey;
import org.obsidianbox.magma.util.map.SerializableMap;
import org.obsidianbox.obsidian.addon.AddonClassLoader;
/**
* Manages a string keyed, serializable object hashmap that can be serialized easily to an array of bytes and deserialized from an array of bytes, intended for persistence and network transfers.
*
* This should not contain null values.
*/
public class SerializableHashMap implements SerializableMap {
public static final String NILTYPE = "NULL";
// This doesn't need to be persisted across restarts
private static final long serialVersionUID = 1L;
protected final ConcurrentHashMap<String, Serializable> map;
public SerializableHashMap() {
// Let's scale these values down a little
// TODO: maybe tweak these a little
this.map = new ConcurrentHashMap<>(8, .9f, 5);
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
if (key instanceof String) {
return containsKey((String) key);
}
return false;
}
public boolean containsKey(String key) {
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public Serializable get(Object key) {
return get(key, null);
}
@Override
public Serializable put(String key, Serializable value) {
if (value == null || NILTYPE.equals(value)) {
return map.remove(key);
}
return map.put(key, value);
}
@Override
public Serializable remove(Object key) {
if (key instanceof String) {
return remove((String) key);
} else if (key instanceof DefaultedKey) {
return remove(((DefaultedKey<?>) key).getKeyString());
}
return null;
}
public Serializable remove(String key) {
return map.remove(key);
}
@Override
public <T> T get(String key, Class<T> clazz) {
Serializable s = get(key);
if (s != null) {
try {
return clazz.cast(s);
} catch (ClassCastException ignore) {
}
}
return null;
}
@Override
public void putAll(Map<? extends String, ? extends Serializable> m) {
for (Map.Entry<? extends String, ? extends Serializable> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<String> keySet() {
return map.keySet();
}
@Override
public Collection<Serializable> values() {
return new Values();
}
@Override
public Set<java.util.Map.Entry<String, Serializable>> entrySet() {
return new EntrySet();
}
@SuppressWarnings("unchecked")
@Override
public <T extends Serializable> T get(Object key, T defaultValue) {
if (key instanceof DefaultedKey) {
return get((DefaultedKey<T>) key);
}
if (!(key instanceof String)) {
return defaultValue;
}
final String keyString = (String) key;
final T value;
try {
value = (T) map.get(keyString);
} catch (ClassCastException e) {
return defaultValue;
}
if (value == null || NILTYPE.equals(value)) {
if (defaultValue == null) {
return null;
}
Serializable old = putIfAbsent(keyString, defaultValue);
if (old != null) {
return (T) old;
} else {
return defaultValue;
}
}
return value;
}
@Override
public <T extends Serializable> T get(DefaultedKey<T> key) {
T defaultValue = key.getDefaultValue();
String keyString = key.getKeyString();
return get(keyString, defaultValue);
}
@SuppressWarnings("unchecked")
@Override
public <T extends Serializable> T put(DefaultedKey<T> key, T value) {
String keyString = key.getKeyString();
try {
return (T) put(keyString, value);
} catch (ClassCastException e) {
return null;
}
}
@SuppressWarnings("unchecked")
@Override
public <T extends Serializable> T putIfAbsent(DefaultedKey<T> key, T value) {
String keyString = key.getKeyString();
try {
return (T) putIfAbsent(keyString, value);
} catch (ClassCastException e) {
return null;
}
}
@Override
public Serializable putIfAbsent(String key, Serializable value) {
if (value == null || NILTYPE.equals(value)) {
return map.remove(key);
}
return map.putIfAbsent(key, value);
}
@Override
public int hashCode() {
HashCodeBuilder builder = new HashCodeBuilder();
for (Map.Entry<? extends String, ? extends Serializable> e : entrySet()) {
builder.append(e.getKey());
builder.append(e.getValue());
}
return builder.toHashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SerializableHashMap)) {
return false;
}
SerializableHashMap other = (SerializableHashMap) obj;
if (isEmpty() && other.isEmpty()) {
return true;
}
for (Map.Entry<? extends String, ? extends Serializable> e : entrySet()) {
Serializable value = e.getValue();
Serializable otherValue = other.get(e.getKey());
if (value != null) {
if (!value.equals(otherValue)) {
return false;
}
} else if (otherValue != null) {
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuilder toString = new StringBuilder("DataMap {");
for (Map.Entry<? extends String, ? extends Serializable> e : entrySet()) {
toString.append("(");
toString.append(e.getKey());
toString.append(", ");
toString.append(e.getValue());
toString.append("), ");
}
toString.delete(toString.length() - 3, toString.length());
toString.append("}");
return toString.toString();
}
public static class AddonClassResolverObjectInputStream extends ObjectInputStream {
public AddonClassResolverObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
try {
return super.resolveClass(desc);
} catch (ClassNotFoundException e) {
return AddonClassLoader.findAddonClass(desc.getName());
}
}
}
private final class EntrySet extends AbstractSet<Map.Entry<String, Serializable>> {
int size = map.size();
@Override
public Iterator<java.util.Map.Entry<String, Serializable>> iterator() {
return new EntryIterator();
}
@Override
public int size() {
return size;
}
}
private final class Values extends AbstractCollection<Serializable> {
@Override
public Iterator<Serializable> iterator() {
return new ValueIterator();
}
@Override
public int size() {
return map.size();
}
@Override
public boolean contains(Object o) {
return containsValue(o);
}
@Override
public void clear() {
map.clear();
}
}
private final class EntryIterator implements Iterator<Map.Entry<String, Serializable>> {
Serializable next, current;
int index = 0;
int expectedAmount = map.size();
ArrayList<Serializable> values = new ArrayList<>();
ArrayList<String> keys = new ArrayList<>();
EntryIterator() {
for (String s : map.keySet()) {
keys.add(s);
values.add(map.get(s));
}
current = null;
if (expectedAmount == 0) {
next = null;
} else {
next = values.get(index);
}
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public Map.Entry<String, Serializable> next() {
if (map.size() != expectedAmount) {
throw new ConcurrentModificationException();
}
index++;
current = next;
if (index < expectedAmount) {
next = values.get(index);
} else {
next = null;
}
return new Entry(keys.get(index - 1), current);
}
@Override
public void remove() {
if (current == null) {
throw new IllegalStateException();
}
if (map.size() != expectedAmount) {
throw new ConcurrentModificationException();
}
current = null;
SerializableHashMap.this.remove(keys.get(index));
}
}
private final class Entry implements Map.Entry<String, Serializable> {
final String key;
Serializable value;
Entry(String key, Serializable value) {
this.key = key;
this.value = value;
}
@Override
public String getKey() {
return key;
}
@Override
public Serializable getValue() {
return value;
}
@Override
public Serializable setValue(Serializable value) {
this.value = value;
return SerializableHashMap.this.put(key, value);
}
}
private final class ValueIterator implements Iterator<Serializable> {
Serializable next, current;
int index = 0;
int expectedAmount = map.size();
ArrayList<Serializable> values = new ArrayList<>();
ArrayList<String> keys = new ArrayList<>();
ValueIterator() {
for (String s : map.keySet()) {
keys.add(s);
values.add(map.get(s));
}
if (expectedAmount > 1) {
current = values.get(index);
next = values.get(index + 1);
} else if (expectedAmount > 0) {
current = values.get(index);
next = null;
} else {
current = next = null;
}
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public Serializable next() {
if (map.size() != expectedAmount) {
throw new ConcurrentModificationException();
}
index++;
current = next;
if (index < expectedAmount) {
next = values.get(index);
} else {
next = null;
}
return current;
}
@Override
public void remove() {
if (current == null) {
throw new IllegalStateException();
}
if (map.size() != expectedAmount) {
throw new ConcurrentModificationException();
}
current = null;
SerializableHashMap.this.remove(keys.get(index));
}
}
/**
* This serializes only the data, as opposed to the whole object.
*/
@Override
public byte[] serialize() {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(map);
return out.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("Unable to compress SerializableMap");
}
}
/**
* This deserializes only the data, as opposed to the whole object.
*/
@Override
@SuppressWarnings("unchecked")
public void deserialize(byte[] serializedData, boolean wipe) throws IOException {
if (wipe) {
map.clear();
}
InputStream in = new ByteArrayInputStream(serializedData);
ObjectInputStream ois = new AddonClassResolverObjectInputStream(in);
try {
// Because it may be a map of maps, we want to UPDATE inner maps, not overwrite
for (Map.Entry<String, ? extends Serializable> e : ((Map<String, ? extends Serializable>) ois.readObject()).entrySet()) {
if (e.getValue() instanceof Map && map.get(e.getKey()) instanceof Map) {
((Map) map.get(e.getKey())).putAll((Map) e.getValue());
} else {
put(e.getKey(), e.getValue());
}
}
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("Unable to decompress SerializableHashMap", ex);
}
}
@Override
public void deserialize(byte[] compressedData) throws IOException {
deserialize(compressedData, true);
}
@Override
public SerializableMap deepCopy() {
SerializableMap map = new SerializableHashMap();
try {
map.deserialize(serialize(), true);
} catch (IOException e) {
throw new RuntimeException("Unable to create a deep copy!", e);
}
return map;
}
}