mirror of https://github.com/beemdevelopment/Aegis
Merge pull request #136 from alexbakker/uuid-map
Introduce UUIDMap for storing objects that are keyed by a UUIDpull/177/head
commit
9cb9d47857
@ -1,62 +0,0 @@
|
||||
package com.beemdevelopment.aegis.db;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<DatabaseEntry> iterator() {
|
||||
return _entries.iterator();
|
||||
}
|
||||
|
||||
public void add(DatabaseEntry entry) {
|
||||
if (getByUUID(entry.getUUID()) != null) {
|
||||
throw new AssertionError("entry found with the same uuid");
|
||||
}
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void remove(DatabaseEntry entry) {
|
||||
entry = mustGetByUUID(entry.getUUID());
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replace(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
}
|
||||
|
||||
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getList() {
|
||||
return Collections.unmodifiableList(_entries);
|
||||
}
|
||||
|
||||
public DatabaseEntry getByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseEntry mustGetByUUID(UUID uuid) {
|
||||
DatabaseEntry entry = getByUUID(uuid);
|
||||
if (entry == null) {
|
||||
throw new AssertionError("no entry found with the same uuid");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Cloner {
|
||||
private Cloner() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an exact clone of the given Serializable object.
|
||||
*/
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public static <T extends Serializable> T clone(T obj) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(baos);
|
||||
oos.writeObject(obj);
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||
ObjectInputStream ois = new ObjectInputStream(bais);
|
||||
return (T) ois.readObject();
|
||||
} catch (ClassNotFoundException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A map data structure abstraction for storing values with a UUID as the key. Keys
|
||||
* must be specified by the value itself, instead of separately. It uses a
|
||||
* LinkedHashMap internally (a hash map with a separate linked list that maintains
|
||||
* the order).
|
||||
* @param <T> The type of values in this map
|
||||
*/
|
||||
public class UUIDMap <T extends UUIDMap.Value> implements Iterable<T>, Serializable {
|
||||
private LinkedHashMap<UUID, T> _map = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Adds a value to the internal map.
|
||||
* @throws AssertionError if a map value with the UUID of the given value already exists.
|
||||
*/
|
||||
public void add(T value) {
|
||||
UUID uuid = value.getUUID();
|
||||
if (_map.containsKey(uuid)) {
|
||||
throw new AssertionError(String.format("Existing value found with UUID: %s", uuid));
|
||||
}
|
||||
_map.put(uuid, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a value from the internal map.
|
||||
* @throws AssertionError if no map value exists with the UUID of the given value.
|
||||
* @return The old value that is now no longer present in the internal map.
|
||||
*/
|
||||
public T remove(T value) {
|
||||
T oldValue = getByUUID(value.getUUID());
|
||||
_map.remove(oldValue.getUUID());
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an old value (with the same UUID as the new given value) in the
|
||||
* internal map with the new given value.
|
||||
* @throws AssertionError if no map value exists with the UUID of the given value.
|
||||
* @return The old value that is now no longer present in the internal map.
|
||||
*/
|
||||
public T replace(T newValue) {
|
||||
T oldValue = getByUUID(newValue.getUUID());
|
||||
_map.put(oldValue.getUUID(), newValue);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the position of value1 and value2 in the internal map. This operation is
|
||||
* quite expensive because it has to reallocate the entire underlying LinkedHashMap.
|
||||
* @throws AssertionError if no map value exists with the UUID of the given entries.
|
||||
*/
|
||||
public void swap(T value1, T value2) {
|
||||
boolean found1 = false;
|
||||
boolean found2 = false;
|
||||
List<T> values = new ArrayList<>();
|
||||
|
||||
for (T value : _map.values()) {
|
||||
if (value.getUUID().equals(value1.getUUID())) {
|
||||
values.add(value2);
|
||||
found1 = true;
|
||||
} else if (value.getUUID().equals(value2.getUUID())) {
|
||||
values.add(value1);
|
||||
found2 = true;
|
||||
} else {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found1) {
|
||||
throw new AssertionError(String.format("No value found for value1 with UUID: %s", value1.getUUID()));
|
||||
}
|
||||
if (!found2) {
|
||||
throw new AssertionError(String.format("No value found for value2 with UUID: %s", value2.getUUID()));
|
||||
}
|
||||
|
||||
_map.clear();
|
||||
for (T value : values) {
|
||||
_map.put(value.getUUID(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the internal map contains a value with the UUID of the given value.
|
||||
*/
|
||||
public boolean has(T value) {
|
||||
return _map.containsKey(value.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a read-only view of the values in the internal map.
|
||||
*/
|
||||
public Collection<T> getValues() {
|
||||
return Collections.unmodifiableCollection(_map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an entry from the internal map that has the given UUID.
|
||||
* @throws AssertionError if no map value exists with the given UUID.
|
||||
*/
|
||||
public T getByUUID(UUID uuid) {
|
||||
T value = _map.get(uuid);
|
||||
if (value == null) {
|
||||
throw new AssertionError(String.format("No value found with UUID: %s", uuid));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return _map.values().iterator();
|
||||
}
|
||||
|
||||
public static abstract class Value implements Serializable {
|
||||
private UUID _uuid;
|
||||
|
||||
protected Value(UUID uuid) {
|
||||
_uuid = uuid;
|
||||
}
|
||||
|
||||
protected Value() {
|
||||
this(UUID.randomUUID());
|
||||
}
|
||||
|
||||
public final UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the UUID of this value by generating a new random one.
|
||||
* The caller must ensure that this Value is not in a UUIDMap yet. Otherwise, bad things will happen.
|
||||
*/
|
||||
public final void resetUUID() {
|
||||
_uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof Value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getUUID().equals(((Value) o).getUUID());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package com.beemdevelopment.aegis;
|
||||
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class UUIDMapTest {
|
||||
private UUIDMap<Value> _map;
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
_map = new UUIDMap<>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addValue() {
|
||||
// try adding a new value
|
||||
Value value = addNewValue();
|
||||
|
||||
// try re-adding the value
|
||||
assertThrows(AssertionError.class, () -> _map.add(value));
|
||||
|
||||
// try adding a clone of the value
|
||||
assertThrows(AssertionError.class, () -> _map.add(Cloner.clone(value)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeValue() {
|
||||
// try removing a value
|
||||
final Value value = addNewValue();
|
||||
Value oldValue = _map.remove(value);
|
||||
assertFalse(_map.has(value));
|
||||
|
||||
// ensure we got the original value back
|
||||
assertEquals(value, oldValue);
|
||||
|
||||
// try removing a non-existent value
|
||||
assertThrows(AssertionError.class, () -> _map.remove(value));
|
||||
|
||||
// try removing a value using a clone
|
||||
Value value2 = addNewValue();
|
||||
_map.remove(Cloner.clone(value2));
|
||||
assertFalse(_map.has(value2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replaceValue() {
|
||||
Value value = addNewValue();
|
||||
|
||||
// replace the value with a clone
|
||||
Value valueClone = Cloner.clone(value);
|
||||
Value oldValue = _map.replace(valueClone);
|
||||
|
||||
// ensure we got the original value back
|
||||
assertEquals(value, oldValue);
|
||||
|
||||
// ensure that the clone is now stored in the map
|
||||
assertSame(_map.getByUUID(value.getUUID()), valueClone);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void swapValue() {
|
||||
Collection<Value> values = _map.getValues();
|
||||
|
||||
// set up the map with some values
|
||||
Value value1 = addNewValue();
|
||||
Value value2 = addNewValue();
|
||||
Value value3 = addNewValue();
|
||||
Value value4 = addNewValue();
|
||||
|
||||
// set up a reference list with the reverse order
|
||||
List<Value> ref = new ArrayList<>(values);
|
||||
Collections.reverse(ref);
|
||||
|
||||
// the lists should not be equal at this point
|
||||
assertNotEquals(values, ref);
|
||||
|
||||
// swap the values and see if the lists are equal now
|
||||
_map.swap(value1, value4);
|
||||
_map.swap(value2, value3);
|
||||
assertIterableEquals(values, ref);
|
||||
}
|
||||
|
||||
private Value addNewValue() {
|
||||
Value value = new Value();
|
||||
assertFalse(_map.has(value));
|
||||
_map.add(value);
|
||||
assertTrue(_map.has(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
private static class Value extends UUIDMap.Value {
|
||||
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
#Tue Feb 26 19:31:04 CET 2019
|
||||
#Mon Jun 10 14:39:47 CEST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||
|
Loading…
Reference in New Issue