Begun work on the data refactoring, long way to go.

This commit is contained in:
Rsl1122 2018-06-01 20:16:43 +03:00
parent 2af36b0ec8
commit 8f474c37f6
20 changed files with 497 additions and 23 deletions

View File

@ -0,0 +1,17 @@
package com.djrapitops.plan.api.exceptions.database;
/**
* Runtime exception for wrapping database errors.
*
* @author Rsl1122
*/
public class DBOpException extends RuntimeException {
public DBOpException(String message) {
super(message);
}
public DBOpException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -4,6 +4,7 @@
*/
package com.djrapitops.plan.data.container;
import com.djrapitops.plan.data.store.objects.DateMap;
import com.djrapitops.plan.utilities.FormatUtils;
import com.djrapitops.plan.utilities.SHA256Hash;
import com.google.common.base.Objects;
@ -35,6 +36,14 @@ public class GeoInfo {
this.ipHash = ipHash;
}
public static DateMap<GeoInfo> intoDateMap(Iterable<GeoInfo> geoInfo) {
DateMap<GeoInfo> map = new DateMap<>();
for (GeoInfo info : geoInfo) {
map.put(info.lastUsed, info);
}
return map;
}
public String getIp() {
return ip;
}

View File

@ -0,0 +1,26 @@
package com.djrapitops.plan.data.store;
import java.util.function.Supplier;
/**
* Caching layer between Supplier and caller.
*
* @author Rsl1122
*/
public class CachingSupplier<T> implements Supplier<T> {
private final Supplier<T> original;
private T cachedValue;
public CachingSupplier(Supplier<T> original) {
this.original = original;
}
@Override
public T get() {
if (cachedValue == null) {
cachedValue = original.get();
}
return cachedValue;
}
}

View File

@ -0,0 +1,68 @@
package com.djrapitops.plan.data.store;
import java.util.Objects;
/**
* Identifier used for storing and fetching data from DataContainers.
*
* @param <T> Type of the object returned by the Value identified by this Key.
* @author Rsl1122
*/
public class Key<T> {
private final Type<T> type;
private final String keyName;
/**
* Create a new key.
* <p>
* Example usage:
* {@code Key<String> key = new Key(String.class, "identifier");}
* <p>
* (In Keys class) {@code public static final Key<String> IDENTIFIER = new Key(String.class, "identifier");}
* {@code Key<String> key = Keys.IDENTIFIER;}
*
* @param type Class with type of the Object returned by the Value identified by this Key.
* @param keyName Name (identifier) of the Key.
*/
public Key(Class<T> type, String keyName) {
this(Type.ofClass(type), keyName);
}
public Key(Type<T> type, String keyName) {
this.type = type;
this.keyName = keyName;
}
/**
* Get the type of the key.
*
* @return specified in constructor.
*/
public Type<T> getType() {
return type;
}
/**
* Get the name (identifier) of the Key.
*
* @return For example "nickname"
*/
public String getKeyName() {
return keyName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key<?> key = (Key<?>) o;
return Objects.equals(type, key.type) &&
Objects.equals(keyName, key.keyName);
}
@Override
public int hashCode() {
return Objects.hash(type, keyName);
}
}

View File

@ -0,0 +1,20 @@
package com.djrapitops.plan.data.store;
/**
* Similar to Google's TypeToken but without requiring whole gson package.
* <p>
* Create new instance with {@code new Type<YourObject>() {}}.
*
* @author Rsl1122
*/
public abstract class Type<T> {
public static <K> Type<K> ofClass(Class<K> of) {
return new Type<K>() {};
}
public static <K> Type<K> of(K object) {
return new Type<K>() {};
}
}

View File

@ -0,0 +1,77 @@
package com.djrapitops.plan.data.store.containers;
import com.djrapitops.plan.data.store.CachingSupplier;
import com.djrapitops.plan.data.store.Key;
import java.util.HashMap;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Abstract representation of an object that holds the Values for different Keys.
* <p>
* The methods in this object are used for placing and fetching the data from the container.
* Methods to use depend on your use case.
*
* @author Rsl1122
*/
public class DataContainer extends HashMap<Key, Supplier> {
/**
* Place your data inside the container.
*
* @param key Key of type T that identifies the data and will be used later when the data needs to be fetched.
* @param obj object to store
* @param <T> Type of the object
*/
public <T> void putRawData(Key<T> key, T obj) {
put(key, () -> obj);
}
public <T> void putSupplier(Key<T> key, Supplier<T> supplier) {
put(key, new CachingSupplier<>(supplier));
}
/**
* Check if a Value with the given Key has been placed into the container.
*
* @param key Key that identifies the data.
* @param <T> Type of the object returned by the Value if it is present.
* @return true if found, false if not.
*/
public <T> boolean supports(Key<T> key) {
return containsKey(key);
}
/**
* Get an Optional of the data identified by the Key.
* <p>
* Since Value is a functional interface, its method may call blocking methods via Value implementations,
* It is therefore recommended to not call this method on the server thread.
* <p>
* It is recommended to check if the Optional is present as null values returned by plugins will be empty.
*
* @param key Key that identifies the Value
* @param <T> Type of the object returned by Value
* @return Optional of the object if the key is registered and key matches the type of the object. Otherwise empty.
*/
public <T> Optional<T> getValue(Key<T> key) {
Supplier supplier = get(key);
if (supplier == null) {
return Optional.empty();
}
try {
return Optional.of((T) supplier.get());
} catch (ClassCastException e) {
return Optional.empty();
}
}
public <T> T getUnsafe(Key<T> key) {
Supplier supplier = get(key);
if (supplier == null) {
throw new IllegalArgumentException("Unsupported Key");
}
return (T) supplier.get();
}
}

View File

@ -0,0 +1,12 @@
package com.djrapitops.plan.data.store.containers;
/**
* DataContainer about a Player.
* <p>
* Use {@code getValue(PlayerKeys.REGISTERED).isPresent()} to determine if Plan has data about the player.
*
* @author Rsl1122
* @see com.djrapitops.plan.data.store.keys.PlayerKeys for supported Key objects.
*/
public class PlayerContainer extends DataContainer {
}

View File

@ -0,0 +1,16 @@
package com.djrapitops.plan.data.store.keys;
import com.djrapitops.plan.data.store.Key;
import java.util.UUID;
/**
* Class holding Key objects that are commonly used across multiple DataContainers.
*
* @author Rsl1122
*/
public class CommonKeys {
public static final Key<UUID> UUID = new Key<>(UUID.class, "uuid");
}

View File

@ -0,0 +1,27 @@
package com.djrapitops.plan.data.store.keys;
import com.djrapitops.plan.data.container.GeoInfo;
import com.djrapitops.plan.data.store.Key;
import com.djrapitops.plan.data.store.Type;
import com.djrapitops.plan.data.store.objects.DateMap;
import com.djrapitops.plan.data.store.objects.Nickname;
import java.util.List;
import java.util.UUID;
/**
* Class that holds Key objects for PlayerContainer.
*
* @author Rsl1122
*/
public class PlayerKeys {
public static final Key<UUID> UUID = CommonKeys.UUID;
public static final Key<String> NAME = new Key<>(String.class, "name");
public static final Key<List<Nickname>> NICKNAMES = new Key<>(new Type<List<Nickname>>() {}, "nicknames");
public static final Key<Long> REGISTERED = new Key<>(Long.class, "registered");
public static final Key<DateMap<UUID>> KICKS = new Key<>(new Type<DateMap<UUID>>() {}, "kicks");
public static final Key<DateMap<GeoInfo>> GEO_INFO = new Key<>(new Type<DateMap<GeoInfo>>() {}, "geo_info");
}

View File

@ -0,0 +1,23 @@
package com.djrapitops.plan.data.store.objects;
import java.util.TreeMap;
/**
* Basic TreeMap that uses Epoch MS as keys.
*
* @author Rsl1122
*/
public class DateMap<T> extends TreeMap<Long, T> {
public DateMap() {
super(Long::compareTo);
}
public boolean hasValuesBetween(long after, long before) {
return countBetween(after, before) > 0;
}
public int countBetween(long after, long before) {
return subMap(after, before).size();
}
}

View File

@ -0,0 +1,20 @@
package com.djrapitops.plan.data.store.objects;
import java.util.TreeSet;
/**
* Basic TreeSet with Epoch ms as values.
*
* @author Rsl1122
*/
public class DateSet extends TreeSet<Long> {
public boolean hasValuesBetween(long after, long before) {
return countBetween(after, before) > 0;
}
public int countBetween(long after, long before) {
return subSet(after, before).size();
}
}

View File

@ -0,0 +1,33 @@
package com.djrapitops.plan.data.store.objects;
import java.util.UUID;
/**
* Object storing nickname information.
*
* @author Rsl1122
*/
public class Nickname {
private final String name;
private final long lastUsed;
private final UUID serverUUID;
public Nickname(String name, long lastUsed, UUID serverUUID) {
this.name = name;
this.lastUsed = lastUsed;
this.serverUUID = serverUUID;
}
public String getName() {
return name;
}
public long getLastUsed() {
return lastUsed;
}
public UUID getServerUUID() {
return serverUUID;
}
}

View File

@ -0,0 +1,13 @@
package com.djrapitops.plan.data.store.objects;
import java.util.HashMap;
import java.util.UUID;
/**
* Map object that has ServerUUID as keys.
*
* @author Rsl1122
*/
public class PerServer<T> extends HashMap<UUID, T> {
}

View File

@ -5,6 +5,7 @@ import com.djrapitops.plan.data.PlayerProfile;
import com.djrapitops.plan.data.ServerProfile;
import com.djrapitops.plan.data.WebUser;
import com.djrapitops.plan.data.container.*;
import com.djrapitops.plan.data.store.containers.PlayerContainer;
import com.djrapitops.plan.system.info.server.Server;
import java.util.*;
@ -21,6 +22,8 @@ public interface FetchOperations {
// UUIDs
PlayerContainer getPlayerContainer(UUID uuid);
Set<UUID> getSavedUUIDs() throws DBException;
Set<UUID> getSavedUUIDs(UUID server) throws DBException;

View File

@ -1,10 +1,13 @@
package com.djrapitops.plan.system.database.databases.sql.operation;
import com.djrapitops.plan.api.exceptions.database.DBException;
import com.djrapitops.plan.api.exceptions.database.DBOpException;
import com.djrapitops.plan.data.PlayerProfile;
import com.djrapitops.plan.data.ServerProfile;
import com.djrapitops.plan.data.WebUser;
import com.djrapitops.plan.data.container.*;
import com.djrapitops.plan.data.store.containers.PlayerContainer;
import com.djrapitops.plan.data.store.keys.PlayerKeys;
import com.djrapitops.plan.system.database.databases.operation.FetchOperations;
import com.djrapitops.plan.system.database.databases.sql.SQLDB;
import com.djrapitops.plan.system.info.server.Server;
@ -116,9 +119,22 @@ public class SQLFetchOps extends SQLOps implements FetchOperations {
return profile;
} catch (SQLException e) {
throw SQLErrorUtil.getExceptionFor(e);
} catch (DBOpException e) {
throw new DBException(e);
}
}
@Override
public PlayerContainer getPlayerContainer(UUID uuid) {
PlayerContainer container = new PlayerContainer();
container.putRawData(PlayerKeys.UUID, uuid);
container.putAll(usersTable.getUserInformation(uuid));
container.put(PlayerKeys.GEO_INFO, () -> GeoInfo.intoDateMap(geoInfoTable.getGeoInfo(uuid)));
return container;
}
private void addUserInfoToProfile(PlayerProfile profile, Map<UUID, UserInfo> userInfo) {
for (Map.Entry<UUID, UserInfo> entry : userInfo.entrySet()) {
UUID serverUUID = entry.getKey();

View File

@ -1,6 +1,7 @@
package com.djrapitops.plan.system.database.databases.sql.tables;
import com.djrapitops.plan.api.exceptions.database.DBInitException;
import com.djrapitops.plan.api.exceptions.database.DBOpException;
import com.djrapitops.plan.data.container.GeoInfo;
import com.djrapitops.plan.system.database.databases.sql.SQLDB;
import com.djrapitops.plan.system.database.databases.sql.processing.ExecStatement;
@ -129,29 +130,33 @@ public class GeoInfoTable extends UserIDTable {
}
public List<GeoInfo> getGeoInfo(UUID uuid) throws SQLException {
public List<GeoInfo> getGeoInfo(UUID uuid) {
String sql = "SELECT DISTINCT * FROM " + tableName +
" WHERE " + Col.USER_ID + "=" + usersTable.statementSelectID;
return query(new QueryStatement<List<GeoInfo>>(sql, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, uuid.toString());
}
@Override
public List<GeoInfo> processResults(ResultSet set) throws SQLException {
List<GeoInfo> geoInfo = new ArrayList<>();
while (set.next()) {
String ip = set.getString(Col.IP.get());
String geolocation = set.getString(Col.GEOLOCATION.get());
String ipHash = set.getString(Col.IP_HASH.get());
long lastUsed = set.getLong(Col.LAST_USED.get());
geoInfo.add(new GeoInfo(ip, geolocation, lastUsed, ipHash));
try {
return query(new QueryStatement<List<GeoInfo>>(sql, 100) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, uuid.toString());
}
return geoInfo;
}
});
@Override
public List<GeoInfo> processResults(ResultSet set) throws SQLException {
List<GeoInfo> geoInfo = new ArrayList<>();
while (set.next()) {
String ip = set.getString(Col.IP.get());
String geolocation = set.getString(Col.GEOLOCATION.get());
String ipHash = set.getString(Col.IP_HASH.get());
long lastUsed = set.getLong(Col.LAST_USED.get());
geoInfo.add(new GeoInfo(ip, geolocation, lastUsed, ipHash));
}
return geoInfo;
}
});
} catch (SQLException e) {
throw new DBOpException("SQL Failed: " + sql, e);
}
}
private void updateGeoInfo(UUID uuid, GeoInfo info) throws SQLException {

View File

@ -27,6 +27,8 @@ import java.util.*;
*/
public class NicknamesTable extends UserIDTable {
// TODO Add last used
public NicknamesTable(SQLDB db) {
super("plan_nicknames", db);
serverTable = db.getServerTable();

View File

@ -1,7 +1,11 @@
package com.djrapitops.plan.system.database.databases.sql.tables;
import com.djrapitops.plan.api.exceptions.database.DBInitException;
import com.djrapitops.plan.api.exceptions.database.DBOpException;
import com.djrapitops.plan.data.container.UserInfo;
import com.djrapitops.plan.data.store.Key;
import com.djrapitops.plan.data.store.containers.DataContainer;
import com.djrapitops.plan.data.store.keys.PlayerKeys;
import com.djrapitops.plan.system.database.databases.sql.SQLDB;
import com.djrapitops.plan.system.database.databases.sql.processing.ExecStatement;
import com.djrapitops.plan.system.database.databases.sql.processing.QueryAllStatement;
@ -13,6 +17,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.function.Supplier;
/**
* Table that is in charge of storing common player data for all servers.
@ -457,6 +462,47 @@ public class UsersTable extends UserIDTable {
});
}
public DataContainer getUserInformation(UUID uuid) {
Key<DataContainer> key = new Key<>(DataContainer.class, "plan_users_data");
DataContainer returnValue = new DataContainer();
Supplier<DataContainer> usersTableResults = () -> {
try {
String sql = "SELECT * FROM " + tableName + " WHERE " + Col.UUID + "=?";
return query(new QueryStatement<DataContainer>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, uuid.toString());
}
@Override
public DataContainer processResults(ResultSet set) throws SQLException {
DataContainer container = new DataContainer();
if (set.next()) {
long registered = set.getLong(Col.REGISTERED.get());
String name = set.getString(Col.USER_NAME.get());
container.putRawData(PlayerKeys.REGISTERED, registered);
container.putRawData(PlayerKeys.NAME, name);
}
return container;
}
});
} catch (SQLException e) {
throw new DBOpException("Failed to fetch user info from plan_users", e);
}
};
returnValue.putSupplier(key, usersTableResults);
returnValue.putRawData(PlayerKeys.UUID, uuid);
returnValue.putSupplier(PlayerKeys.REGISTERED, () -> returnValue.getUnsafe(key).getUnsafe(PlayerKeys.REGISTERED));
returnValue.putSupplier(PlayerKeys.NAME, () -> returnValue.getUnsafe(key).getUnsafe(PlayerKeys.NAME));
return returnValue;
}
public enum Col implements Column {
ID("id"),
UUID("uuid"),

View File

@ -11,6 +11,8 @@ import com.djrapitops.plan.api.exceptions.database.DBInitException;
import com.djrapitops.plan.data.Actions;
import com.djrapitops.plan.data.WebUser;
import com.djrapitops.plan.data.container.*;
import com.djrapitops.plan.data.store.containers.PlayerContainer;
import com.djrapitops.plan.data.store.keys.PlayerKeys;
import com.djrapitops.plan.data.time.GMTimes;
import com.djrapitops.plan.data.time.WorldTimes;
import com.djrapitops.plan.system.database.databases.sql.SQLDB;
@ -23,16 +25,14 @@ import com.djrapitops.plan.utilities.Base64Util;
import com.djrapitops.plan.utilities.SHA256Hash;
import com.djrapitops.plan.utilities.analysis.MathUtils;
import com.djrapitops.plugin.StaticHolder;
import com.djrapitops.plugin.api.TimeAmount;
import com.djrapitops.plugin.api.utility.log.Log;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import utilities.RandomData;
import utilities.Teardown;
import utilities.TestConstants;
import utilities.TestErrorManager;
import utilities.*;
import utilities.mocks.SystemMockUtil;
import java.io.UnsupportedEncodingException;
@ -1032,4 +1032,25 @@ public class SQLiteTest {
assertNotNull(after.get(2));
assertEquals(1, after.get(2).size());
}
@Test
public void testNewContainerForPlayer() throws UnsupportedEncodingException, SQLException, NoSuchAlgorithmException {
saveAllData(db);
long start = System.nanoTime();
PlayerContainer container = db.fetch().getPlayerContainer(playerUUID);
assertTrue(container.supports(PlayerKeys.UUID));
assertTrue(container.supports(PlayerKeys.REGISTERED));
assertTrue(container.supports(PlayerKeys.NAME));
long end = System.nanoTime();
assertFalse("Took too long: " + ((end - start) / 1000000.0) + "ms", end - start > TimeAmount.SECOND.ns());
OptionalAssert.equals(playerUUID, container.getValue(PlayerKeys.UUID));
OptionalAssert.equals(123456789L, container.getValue(PlayerKeys.REGISTERED));
OptionalAssert.equals("Test", container.getValue(PlayerKeys.NAME));
}
}

View File

@ -0,0 +1,20 @@
package utilities;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Utility for asserts containing Optionals.
*
* @author Rsl1122
*/
public class OptionalAssert {
public static <T> void equals(T expected, Optional<T> result) {
assertTrue(result.isPresent());
assertEquals(expected, result.get());
}
}