Merge pull request 'Make LocalDB Thread Safe and Simplify its API' (#38) from refactor-local-db into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/38 Reviewed-by: delvh <leon@kske.dev>
This commit is contained in:
		@@ -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;
 | 
			
		||||
	private File				dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile;
 | 
			
		||||
	private FileLock			instanceLock;
 | 
			
		||||
 | 
			
		||||
	// 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 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,37 +99,24 @@ 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 {
 | 
			
		||||
			final var 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())))
 | 
			
		||||
@@ -142,7 +138,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);
 | 
			
		||||
@@ -165,9 +160,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
 | 
			
		||||
@@ -182,7 +175,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
 | 
			
		||||
@@ -192,7 +184,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