Make LocalDB Thread Safe and Simplify its API #38
@ -30,59 +30,55 @@ import dev.kske.eventbus.EventListener;
|
||||
*/
|
||||
public final class LocalDB implements EventListener {
|
||||
|
||||
// Data
|
||||
private User user;
|
||||
private Map<String, User> users = new HashMap<>();
|
||||
private List<Chat> chats = new ArrayList<>();
|
||||
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
|
||||
private List<Chat> chats = Collections.synchronizedList(new ArrayList<>());
|
||||
private IDGenerator idGenerator;
|
||||
private CacheMap cacheMap = new CacheMap();
|
||||
private Instant lastSync = Instant.EPOCH;
|
||||
private String authToken;
|
||||
|
||||
// State management
|
||||
private Instant lastSync = Instant.EPOCH;
|
||||
|
||||
// Persistence
|
||||
private File dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile;
|
||||
private FileLock instanceLock;
|
||||
|
||||
/**
|
||||
* Constructs an empty local database. To serialize any user-specific data to
|
||||
* the file system, call {@link LocalDB#initializeUserStorage()} first
|
||||
* and then {@link LocalDB#save(boolean)}.
|
||||
* the file system, call {@link LocalDB#save(boolean)}.
|
||||
*
|
||||
* @param dbDir the directory in which to persist data
|
||||
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
||||
* @throws EnvoyException if {@code dbDir} is in use by another Envoy instance
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public LocalDB(File dbDir) throws IOException {
|
||||
public LocalDB(File dbDir) throws IOException, EnvoyException {
|
||||
this.dbDir = dbDir;
|
||||
EventBus.getInstance().registerListener(this);
|
||||
|
||||
// Ensure that the database directory exists
|
||||
if (!dbDir.exists()) {
|
||||
dbDir.mkdirs();
|
||||
} else if (!dbDir.isDirectory())
|
||||
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||
} else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||
|
||||
// Lock the directory
|
||||
lock();
|
||||
|
||||
// Initialize global files
|
||||
idGeneratorFile = new File(dbDir, "id_gen.db");
|
||||
lastLoginFile = new File(dbDir, "last_login.db");
|
||||
usersFile = new File(dbDir, "users.db");
|
||||
|
||||
// Load global files
|
||||
loadGlobalData();
|
||||
|
||||
// Initialize offline caches
|
||||
cacheMap.put(Message.class, new Cache<>());
|
||||
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database file for a user-specific list of chats.
|
||||
*
|
||||
* @throws IllegalStateException if the client user is not specified
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public void initializeUserStorage() {
|
||||
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||
userFile = new File(dbDir, user.getID() + ".db");
|
||||
}
|
||||
|
||||
FileChannel fc;
|
||||
|
||||
/**
|
||||
* Ensured that only one Envoy instance is using this local database by creating
|
||||
* a lock file.
|
||||
@ -91,9 +87,8 @@ public final class LocalDB implements EventListener {
|
||||
* @throws EnvoyException if the lock cannot by acquired
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void lock() throws EnvoyException {
|
||||
private synchronized void lock() throws EnvoyException {
|
||||
File file = new File(dbDir, "instance.lock");
|
||||
file.deleteOnExit();
|
||||
try {
|
||||
FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
||||
instanceLock = fc.tryLock();
|
||||
@ -104,36 +99,23 @@ public final class LocalDB implements EventListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all users. If the client user is specified, their chats will be stored
|
||||
* as well. The message id generator will also be saved if present.
|
||||
* Loads the local user registry {@code users.db}, the id generator
|
||||
* {@code id_gen.db} and last login file {@code last_login.db}.
|
||||
*
|
||||
* @param isOnline determines which {@code lastSync} time stamp is saved
|
||||
* @throws IOException if the saving process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void save(boolean isOnline) throws IOException {
|
||||
|
||||
// Save users
|
||||
SerializationUtils.write(usersFile, users);
|
||||
|
||||
// Save user data and last sync time stamp
|
||||
if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync);
|
||||
|
||||
// Save last login information
|
||||
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
|
||||
|
||||
// Save id generator
|
||||
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||
private synchronized void loadGlobalData() {
|
||||
try {
|
||||
try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
|
||||
users = (Map<String, User>) in.readObject();
|
||||
}
|
||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
||||
user = (User) in.readObject();
|
||||
authToken = (String) in.readObject();
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all user data.
|
||||
*
|
||||
* @throws ClassNotFoundException if the loading process failed
|
||||
* @throws IOException if the loading process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
|
||||
|
||||
/**
|
||||
* Loads all data of the client user.
|
||||
@ -142,36 +124,15 @@ public final class LocalDB implements EventListener {
|
||||
* @throws IOException if the loading process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadUserData() throws ClassNotFoundException, IOException {
|
||||
public synchronized void loadUserData() throws ClassNotFoundException, IOException {
|
||||
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||
userFile = new File(dbDir, user.getID() + ".db");
|
||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||
chats = (ArrayList<Chat>) in.readObject();
|
||||
chats = (List<Chat>) in.readObject();
|
||||
cacheMap = (CacheMap) in.readObject();
|
||||
lastSync = (Instant) in.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the ID generator. Any exception thrown during this process is ignored.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadIDGenerator() {
|
||||
try {
|
||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||
} catch (ClassNotFoundException | IOException e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the last login information. Any exception thrown during this process is
|
||||
* ignored.
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void loadLastLogin() {
|
||||
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
||||
user = (User) in.readObject();
|
||||
authToken = (String) in.readObject();
|
||||
} catch (ClassNotFoundException | IOException e) {}
|
||||
synchronize();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,7 +141,7 @@ public final class LocalDB implements EventListener {
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void synchronize() {
|
||||
private void synchronize() {
|
||||
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
|
||||
users.put(user.getName(), user);
|
||||
|
||||
@ -197,11 +158,33 @@ public final class LocalDB implements EventListener {
|
||||
.forEach(chats::add);
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onNewAuthToken(NewAuthToken evt) {
|
||||
authToken = evt.get();
|
||||
/**
|
||||
* Stores all users. If the client user is specified, their chats will be stored
|
||||
* as well. The message id generator will also be saved if present.
|
||||
*
|
||||
* @param isOnline determines which {@code lastSync} time stamp is saved
|
||||
* @throws IOException if the saving process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public synchronized void save(boolean isOnline) throws IOException {
|
||||
|
||||
// Save users
|
||||
SerializationUtils.write(usersFile, users);
|
||||
|
||||
// Save user data and last sync time stamp
|
||||
if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync);
|
||||
|
||||
// Save last login information
|
||||
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
|
||||
|
||||
// Save id generator
|
||||
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||
}
|
||||
|
||||
|
||||
@Event
|
||||
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
|
||||
|
||||
/**
|
||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||
* user names as keys
|
||||
|
@ -70,7 +70,6 @@ public final class Startup extends Application {
|
||||
File localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath());
|
||||
logger.info("Initializing LocalDB at " + localDBDir);
|
||||
localDB = new LocalDB(localDBDir);
|
||||
localDB.lock();
|
||||
} catch (IOException | EnvoyException e) {
|
||||
logger.log(Level.SEVERE, "Could not initialize local database: ", e);
|
||||
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
|
||||
@ -79,8 +78,6 @@ public final class Startup extends Application {
|
||||
}
|
||||
|
||||
// Prepare handshake
|
||||
localDB.loadIDGenerator();
|
||||
localDB.loadLastLogin();
|
||||
context.setLocalDB(localDB);
|
||||
|
||||
// Configure stage
|
||||
@ -94,7 +91,6 @@ public final class Startup extends Application {
|
||||
// Authenticate with token if present
|
||||
if (localDB.getAuthToken() != null) {
|
||||
logger.info("Attempting authentication with token...");
|
||||
localDB.initializeUserStorage();
|
||||
localDB.loadUserData();
|
||||
if (!performHandshake(
|
||||
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
|
||||
@ -146,7 +142,6 @@ public final class Startup extends Application {
|
||||
public static boolean attemptOfflineMode(String identifier) {
|
||||
try {
|
||||
// Try entering offline mode
|
||||
localDB.loadUsers();
|
||||
final User clientUser = localDB.getUsers().get(identifier);
|
||||
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
||||
client.setSender(clientUser);
|
||||
@ -169,9 +164,7 @@ public final class Startup extends Application {
|
||||
*/
|
||||
public static Instant loadLastSync(String identifier) {
|
||||
try {
|
||||
localDB.loadUsers();
|
||||
localDB.setUser(localDB.getUsers().get(identifier));
|
||||
localDB.initializeUserStorage();
|
||||
localDB.loadUserData();
|
||||
} catch (final Exception e) {
|
||||
// User storage empty, wrong user name etc. -> default lastSync
|
||||
@ -186,7 +179,6 @@ public final class Startup extends Application {
|
||||
|
||||
// Initialize chats in local database
|
||||
try {
|
||||
localDB.initializeUserStorage();
|
||||
localDB.loadUserData();
|
||||
} catch (final FileNotFoundException e) {
|
||||
// The local database file has not yet been created, probably first login
|
||||
@ -196,7 +188,6 @@ public final class Startup extends Application {
|
||||
}
|
||||
|
||||
context.initWriteProxy();
|
||||
localDB.synchronize();
|
||||
|
||||
if (client.isOnline()) context.getWriteProxy().flushCache();
|
||||
else
|
||||
|
Reference in New Issue
Block a user