Reformat all source files with new formatter

This commit is contained in:
Leon Hofmeister 2020-10-19 18:17:51 +02:00
parent 77a75fc37c
commit b2c3cf62c8
Signed by: delvh
GPG Key ID: 3DECE05F6D9A647C
142 changed files with 2242 additions and 1591 deletions

View File

@ -7,8 +7,8 @@ import envoy.client.ui.Startup;
/** /**
* Triggers application startup. * Triggers application startup.
* <p> * <p>
* To allow Maven shading, the main method has to be separated from the * To allow Maven shading, the main method has to be separated from the {@link Startup} class which
* {@link Startup} class which extends {@link Application}. * extends {@link Application}.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -25,8 +25,7 @@ public final class Main {
/** /**
* Starts the application. * Starts the application.
* *
* @param args the command line arguments are processed by the * @param args the command line arguments are processed by the client configuration
* client configuration
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -35,7 +35,9 @@ public final class Cache<T> implements Consumer<T>, Serializable {
} }
@Override @Override
public String toString() { return String.format("Cache[elements=" + elements + "]"); } public String toString() {
return String.format("Cache[elements=" + elements + "]");
}
/** /**
* Sets the processor to which cached elements are relayed. * Sets the processor to which cached elements are relayed.
@ -52,7 +54,8 @@ public final class Cache<T> implements Consumer<T>, Serializable {
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void relay() { public void relay() {
if (processor == null) throw new IllegalStateException("Processor is not defined"); if (processor == null)
throw new IllegalStateException("Processor is not defined");
elements.forEach(processor::accept); elements.forEach(processor::accept);
elements.clear(); elements.clear();
} }
@ -62,5 +65,7 @@ public final class Cache<T> implements Consumer<T>, Serializable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void clear() { elements.clear(); } public void clear() {
elements.clear();
}
} }

View File

@ -4,8 +4,7 @@ import java.io.Serializable;
import java.util.*; import java.util.*;
/** /**
* Stores a heterogeneous map of {@link Cache} objects with different type * Stores a heterogeneous map of {@link Cache} objects with different type parameters.
* parameters.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -24,7 +23,9 @@ public final class CacheMap implements Serializable {
* @param cache the cache to store * @param cache the cache to store
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); } public <T> void put(Class<T> key, Cache<T> cache) {
map.put(key, cache);
}
/** /**
* Returns a cache mapped by a class. * Returns a cache mapped by a class.
@ -34,7 +35,9 @@ public final class CacheMap implements Serializable {
* @return the cache * @return the cache
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T> Cache<T> get(Class<T> key) { return (Cache<T>) map.get(key); } public <T> Cache<T> get(Class<T> key) {
return (Cache<T>) map.get(key);
}
/** /**
* Returns a cache mapped by a class or any of its subclasses. * Returns a cache mapped by a class or any of its subclasses.
@ -64,5 +67,7 @@ public final class CacheMap implements Serializable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void clear() { map.values().forEach(Cache::clear); } public void clear() {
map.values().forEach(Cache::clear);
}
} }

View File

@ -5,14 +5,14 @@ import java.util.*;
import javafx.collections.*; import javafx.collections.*;
import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
import envoy.client.net.WriteProxy;
/** /**
* Represents a chat between two {@link User}s * Represents a chat between two {@link User}s as a list of {@link Message} objects.
* as a list of {@link Message} objects.
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @author Leon Hofmeister * @author Leon Hofmeister
@ -43,7 +43,9 @@ public class Chat implements Serializable {
* @param recipient the user who receives the messages * @param recipient the user who receives the messages
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
*/ */
public Chat(Contact recipient) { this.recipient = recipient; } public Chat(Contact recipient) {
this.recipient = recipient;
}
private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException { private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException {
stream.defaultReadObject(); stream.defaultReadObject();
@ -62,8 +64,7 @@ public class Chat implements Serializable {
getClass().getSimpleName(), getClass().getSimpleName(),
recipient, recipient,
messages.size(), messages.size(),
disabled disabled);
);
} }
/** /**
@ -72,7 +73,9 @@ public class Chat implements Serializable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@Override @Override
public int hashCode() { return Objects.hash(recipient); } public int hashCode() {
return Objects.hash(recipient);
}
/** /**
* Tests equality to another object based on the recipient. * Tests equality to another object based on the recipient.
@ -81,19 +84,20 @@ public class Chat implements Serializable {
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (!(obj instanceof Chat)) return false; return true;
if (!(obj instanceof Chat))
return false;
final var other = (Chat) obj; final var other = (Chat) obj;
return Objects.equals(recipient, other.recipient); return Objects.equals(recipient, other.recipient);
} }
/** /**
* Sets the status of all chat messages received from the recipient to * Sets the status of all chat messages received from the recipient to {@code READ} starting
* {@code READ} starting from the bottom and stopping once a read message is * from the bottom and stopping once a read message is found.
* found.
* *
* @param writeProxy the write proxy instance used to notify the server about * @param writeProxy the write proxy instance used to notify the server about the message status
* the message status changes * changes
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void read(WriteProxy writeProxy) { public void read(WriteProxy writeProxy) {
@ -111,11 +115,14 @@ public class Chat implements Serializable {
} }
/** /**
* @return {@code true} if the newest message received in the chat doesn't have * @return {@code true} if the newest message received in the chat doesn't have the status
* the status {@code READ} * {@code READ}
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; } public boolean isUnread() {
return !messages.isEmpty()
&& messages.get(messages.size() - 1).getStatus() != MessageStatus.READ;
}
/** /**
* Inserts a message at the correct place according to its creation date. * Inserts a message at the correct place according to its creation date.
@ -139,14 +146,18 @@ public class Chat implements Serializable {
* @return whether the message has been found and removed * @return whether the message has been found and removed
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public boolean remove(long messageID) { return messages.removeIf(m -> m.getID() == messageID); } public boolean remove(long messageID) {
return messages.removeIf(m -> m.getID() == messageID);
}
/** /**
* Increments the amount of unread messages. * Increments the amount of unread messages.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void incrementUnreadAmount() { ++unreadAmount; } public void incrementUnreadAmount() {
++unreadAmount;
}
/** /**
* @return the amount of unread messages in this chat * @return the amount of unread messages in this chat
@ -167,8 +178,7 @@ public class Chat implements Serializable {
public Contact getRecipient() { return recipient; } public Contact getRecipient() { return recipient; }
/** /**
* @return the last known time a {@link envoy.event.IsTyping} event has been * @return the last known time a {@link envoy.event.IsTyping} event has been sent
* sent
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public long getLastWritingEvent() { return lastWritingEvent; } public long getLastWritingEvent() { return lastWritingEvent; }
@ -178,11 +188,13 @@ public class Chat implements Serializable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); } public void lastWritingEventWasNow() {
lastWritingEvent = System.currentTimeMillis();
}
/** /**
* Determines whether messages can be sent in this chat. Should be {@code true} * Determines whether messages can be sent in this chat. Should be {@code true} i.e. for chats
* i.e. for chats whose recipient deleted this client as a contact. * whose recipient deleted this client as a contact.
* *
* @return whether this chat has been disabled * @return whether this chat has been disabled
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
@ -190,8 +202,8 @@ public class Chat implements Serializable {
public boolean isDisabled() { return disabled; } public boolean isDisabled() { return disabled; }
/** /**
* Determines whether messages can be sent in this chat. Should be true i.e. for * Determines whether messages can be sent in this chat. Should be true i.e. for chats whose
* chats whose recipient deleted this client as a contact. * recipient deleted this client as a contact.
* *
* @param disabled whether this chat should be disabled * @param disabled whether this chat should be disabled
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta

View File

@ -5,8 +5,8 @@ import static java.util.function.Function.identity;
import envoy.data.Config; import envoy.data.Config;
/** /**
* Implements a configuration specific to the Envoy Client with default values * Implements a configuration specific to the Envoy Client with default values and convenience
* and convenience methods. * methods.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -20,7 +20,8 @@ public final class ClientConfig extends Config {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static ClientConfig getInstance() { public static ClientConfig getInstance() {
if (config == null) config = new ClientConfig(); if (config == null)
config = new ClientConfig();
return config; return config;
} }
@ -47,5 +48,7 @@ public final class ClientConfig extends Config {
* @return the amount of minutes after which the local database should be saved * @return the amount of minutes after which the local database should be saved
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Integer getLocalDBSaveInterval() { return (Integer) items.get("localDBSaveInterval").get(); } public Integer getLocalDBSaveInterval() {
return (Integer) items.get("localDBSaveInterval").get();
}
} }

View File

@ -36,7 +36,8 @@ public class Context {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void initWriteProxy() { public void initWriteProxy() {
if (localDB == null) throw new IllegalStateException("The LocalDB has to be initialized!"); if (localDB == null)
throw new IllegalStateException("The LocalDB has to be initialized!");
writeProxy = new WriteProxy(client, localDB); writeProxy = new WriteProxy(client, localDB);
} }

View File

@ -2,14 +2,14 @@ package envoy.client.data;
import java.time.Instant; import java.time.Instant;
import envoy.client.net.WriteProxy;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.GroupMessageStatusChange; import envoy.event.GroupMessageStatusChange;
import envoy.client.net.WriteProxy;
/** /**
* Represents a chat between a user and a group * Represents a chat between a user and a group as a list of messages.
* as a list of messages.
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -34,11 +34,14 @@ public final class GroupChat extends Chat {
public void read(WriteProxy writeProxy) { public void read(WriteProxy writeProxy) {
for (int i = messages.size() - 1; i >= 0; --i) { for (int i = messages.size() - 1; i >= 0; --i) {
final GroupMessage gmsg = (GroupMessage) messages.get(i); final GroupMessage gmsg = (GroupMessage) messages.get(i);
if (gmsg.getSenderID() != sender.getID()) if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break; if (gmsg.getSenderID() != sender.getID())
else { if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ)
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ); break;
writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, Instant.now(), sender.getID())); else {
} gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
writeProxy.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(),
MessageStatus.READ, Instant.now(), sender.getID()));
}
} }
unreadAmount = 0; unreadAmount = 0;
} }

View File

@ -13,7 +13,10 @@ import java.util.stream.Stream;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.*; import javafx.collections.*;
import envoy.client.event.*; import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
@ -21,13 +24,11 @@ import envoy.event.contact.*;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.*; import envoy.util.*;
import dev.kske.eventbus.Event; import envoy.client.event.*;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
/** /**
* Stores information about the current {@link User} and their {@link Chat}s. * Stores information about the current {@link User} and their {@link Chat}s. For message ID
* For message ID generation a {@link IDGenerator} is stored as well. * generation a {@link IDGenerator} is stored as well.
* <p> * <p>
* The managed objects are stored inside a folder in the local file system. * The managed objects are stored inside a folder in the local file system.
* *
@ -73,8 +74,11 @@ public final class LocalDB implements EventListener {
EventBus.getInstance().registerListener(this); EventBus.getInstance().registerListener(this);
// Ensure that the database directory exists // Ensure that the database directory exists
if (!dbDir.exists()) dbDir.mkdirs(); if (!dbDir.exists())
else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath())); dbDir.mkdirs();
else if (!dbDir.isDirectory())
throw new IOException(
String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
// Lock the directory // Lock the directory
lock(); lock();
@ -93,8 +97,7 @@ public final class LocalDB implements EventListener {
} }
/** /**
* Ensured that only one Envoy instance is using this local database by creating * Ensured that only one Envoy instance is using this local database by creating a lock file.
* a lock file.
* The lock file is deleted on application exit. * The lock file is deleted on application exit.
* *
* @throws EnvoyException if the lock cannot by acquired * @throws EnvoyException if the lock cannot by acquired
@ -103,17 +106,19 @@ public final class LocalDB implements EventListener {
private synchronized void lock() throws EnvoyException { private synchronized void lock() throws EnvoyException {
final var file = new File(dbDir, "instance.lock"); final var file = new File(dbDir, "instance.lock");
try { try {
final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE); final var fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE,
StandardOpenOption.WRITE);
instanceLock = fc.tryLock(); instanceLock = fc.tryLock();
if (instanceLock == null) throw new EnvoyException("Another Envoy instance is using this local database!"); if (instanceLock == null)
throw new EnvoyException("Another Envoy instance is using this local database!");
} catch (final IOException e) { } catch (final IOException e) {
throw new EnvoyException("Could not create lock file!", e); throw new EnvoyException("Could not create lock file!", e);
} }
} }
/** /**
* Loads the local user registry {@code users.db}, the id generator * Loads the local user registry {@code users.db}, the id generator {@code id_gen.db} and last
* {@code id_gen.db} and last login file {@code last_login.db}. * login file {@code last_login.db}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@ -138,7 +143,8 @@ public final class LocalDB implements EventListener {
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public synchronized 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"); if (user == null)
throw new IllegalStateException("Client user is null, cannot initialize user storage");
userFile = new File(dbDir, user.getID() + ".db"); userFile = new File(dbDir, user.getID() + ".db");
try (var in = new ObjectInputStream(new FileInputStream(userFile))) { try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
chats = FXCollections.observableList((List<Chat>) in.readObject()); chats = FXCollections.observableList((List<Chat>) in.readObject());
@ -150,19 +156,28 @@ public final class LocalDB implements EventListener {
// Mark chats as disabled if a contact is no longer in this users contact list // Mark chats as disabled if a contact is no longer in this users contact list
final var changedUserChats = chats.stream() final var changedUserChats = chats.stream()
.filter(not(chat -> contacts.contains(chat.getRecipient()))) .filter(not(chat -> contacts.contains(chat.getRecipient())))
.peek(chat -> { chat.setDisabled(true); logger.log(Level.INFO, String.format("Deleted chat with %s.", chat.getRecipient())); }); .peek(chat -> {
chat.setDisabled(true);
logger.log(Level.INFO,
String.format("Deleted chat with %s.", chat.getRecipient()));
});
// Also update groups with a different member count // Also update groups with a different member count
final var changedGroupChats = contacts.stream().filter(Group.class::isInstance).flatMap(group -> { final var changedGroupChats =
final var potentialChat = getChat(group.getID()); contacts.stream().filter(Group.class::isInstance).flatMap(group -> {
if (potentialChat.isEmpty()) return Stream.empty(); final var potentialChat = getChat(group.getID());
final var chat = potentialChat.get(); if (potentialChat.isEmpty())
if (group.getContacts().size() != chat.getRecipient().getContacts().size()) { return Stream.empty();
logger.log(Level.INFO, "Removed one (or more) members from " + group); final var chat = potentialChat.get();
return Stream.of(chat); if (group.getContacts().size() != chat.getRecipient().getContacts()
} else return Stream.empty(); .size()) {
}); logger.log(Level.INFO, "Removed one (or more) members from " + group);
Stream.concat(changedUserChats, changedGroupChats).forEach(chat -> chats.set(chats.indexOf(chat), chat)); return Stream.of(chat);
} else
return Stream.empty();
});
Stream.concat(changedUserChats, changedGroupChats)
.forEach(chat -> chats.set(chats.indexOf(chat), chat));
// loadUserData can get called two (or more?) times during application lifecycle // loadUserData can get called two (or more?) times during application lifecycle
contactsChanged = false; contactsChanged = false;
@ -175,19 +190,22 @@ public final class LocalDB implements EventListener {
} }
/** /**
* Synchronizes the contact list of the client user with the chat and user * Synchronizes the contact list of the client user with the chat and user storage.
* storage.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private 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)); 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); users.put(user.getName(), user);
// Synchronize user status data // Synchronize user status data
for (final var contact : user.getContacts()) for (final var contact : user.getContacts())
if (contact instanceof User) if (contact instanceof User)
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); }); getChat(contact.getID()).ifPresent(chat -> {
((User) chat.getRecipient()).setStatus(((User) contact).getStatus());
});
// Create missing chats // Create missing chats
user.getContacts() user.getContacts()
@ -198,8 +216,8 @@ public final class LocalDB implements EventListener {
} }
/** /**
* Initializes a timer that automatically saves this local database after a * Initializes a timer that automatically saves this local database after a period of time
* period of time specified in the settings. * specified in the settings.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@ -214,13 +232,15 @@ public final class LocalDB implements EventListener {
autoSaver.schedule(new TimerTask() { autoSaver.schedule(new TimerTask() {
@Override @Override
public void run() { save(); } public void run() {
save();
}
}, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000); }, 2000, ClientConfig.getInstance().getLocalDBSaveInterval() * 60000);
} }
/** /**
* Stores all users. If the client user is specified, their chats will be stored * Stores all users. If the client user is specified, their chats will be stored as well. The
* as well. The message id generator will also be saved if present. * message id generator will also be saved if present.
* *
* @throws IOException if the saving process failed * @throws IOException if the saving process failed
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -234,21 +254,29 @@ public final class LocalDB implements EventListener {
SerializationUtils.write(usersFile, users); SerializationUtils.write(usersFile, users);
// Save user data and last sync time stamp // Save user data and last sync time stamp
if (user != null) SerializationUtils if (user != null)
.write(userFile, new ArrayList<>(chats), cacheMap, Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync); SerializationUtils
.write(userFile, new ArrayList<>(chats), cacheMap,
Context.getInstance().getClient().isOnline() ? Instant.now() : lastSync);
// Save last login information // Save last login information
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken); if (authToken != null)
SerializationUtils.write(lastLoginFile, user, authToken);
// Save ID generator // Save ID generator
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator); if (hasIDGenerator())
SerializationUtils.write(idGeneratorFile, idGenerator);
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ", e); EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ",
e);
} }
} }
@Event(priority = 500) @Event(priority = 500)
private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) msg.nextStatus(); } private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT)
msg.nextStatus();
}
@Event(priority = 500) @Event(priority = 500)
private void onGroupMessage(GroupMessage msg) { private void onGroupMessage(GroupMessage msg) {
@ -258,16 +286,20 @@ public final class LocalDB implements EventListener {
} }
@Event(priority = 500) @Event(priority = 500)
private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); } private void onMessageStatusChange(MessageStatusChange evt) {
getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get()));
}
@Event(priority = 500) @Event(priority = 500)
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) { private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
this.<GroupMessage>getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get())); this.<GroupMessage>getMessage(evt.getID())
.ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
} }
@Event(priority = 500) @Event(priority = 500)
private void onUserStatusChange(UserStatusChange evt) { private void onUserStatusChange(UserStatusChange evt) {
getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get())); getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast)
.ifPresent(u -> u.setStatus(evt.get()));
} }
@Event(priority = 500) @Event(priority = 500)
@ -288,18 +320,24 @@ public final class LocalDB implements EventListener {
final var newGroup = evt.get(); final var newGroup = evt.get();
// The group creation was not successful // The group creation was not successful
if (newGroup == null) return; if (newGroup == null)
return;
// The group was successfully created // The group was successfully created
else Platform.runLater(() -> chats.add(new GroupChat(user, newGroup))); else
Platform.runLater(() -> chats.add(new GroupChat(user, newGroup)));
} }
@Event(priority = 500) @Event(priority = 500)
private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); } private void onGroupResize(GroupResize evt) {
getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast)
.ifPresent(evt::apply);
}
@Event(priority = 500) @Event(priority = 500)
private void onNameChange(NameChange evt) { private void onNameChange(NameChange evt) {
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny().ifPresent(c -> c.setName(evt.get())); chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny()
.ifPresent(c -> c.setName(evt.get()));
} }
/** /**
@ -309,7 +347,9 @@ public final class LocalDB implements EventListener {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@Event @Event
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); } private void onNewAuthToken(NewAuthToken evt) {
authToken = evt.get();
}
/** /**
* Deletes all associations to the current user. * Deletes all associations to the current user.
@ -343,22 +383,28 @@ public final class LocalDB implements EventListener {
// once a message was removed // once a message was removed
final var messageID = message.get(); final var messageID = message.get();
for (final var chat : chats) for (final var chat : chats)
if (chat.remove(messageID)) break; if (chat.remove(messageID))
break;
}); });
} }
@Event(priority = 500) @Event(priority = 500)
private void onOwnStatusChange(OwnStatusChange statusChange) { user.setStatus(statusChange.get()); } private void onOwnStatusChange(OwnStatusChange statusChange) {
user.setStatus(statusChange.get());
}
@Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500) @Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500)
private void onContactsChangedSinceLastLogin() { contactsChanged = true; } private void onContactsChangedSinceLastLogin() {
contactsChanged = true;
}
@Event(priority = 500) @Event(priority = 500)
private void onContactDisabled(ContactDisabled event) { getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); } private void onContactDisabled(ContactDisabled event) {
getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true));
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their * @return a {@code Map<String, User>} of all users stored locally with their user names as keys
* user names as keys
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public Map<String, User> getUsers() { return users; } public Map<String, User> getUsers() { return users; }
@ -371,7 +417,8 @@ public final class LocalDB implements EventListener {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T extends Message> Optional<T> getMessage(long id) { public <T extends Message> Optional<T> getMessage(long id) {
return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny(); return (Optional<T>) chats.stream().map(Chat::getMessages).flatMap(List::stream)
.filter(m -> m.getID() == id).findAny();
} }
/** /**
@ -381,11 +428,12 @@ public final class LocalDB implements EventListener {
* @return an optional containing the chat * @return an optional containing the chat
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); } public Optional<Chat> getChat(long recipientID) {
return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny();
}
/** /**
* @return all saved {@link Chat} objects that list the client user as the * @return all saved {@link Chat} objects that list the client user as the sender
* sender
* @since Envoy Client v0.1-alpha * @since Envoy Client v0.1-alpha
**/ **/
public ObservableList<Chat> getChats() { return chats; } public ObservableList<Chat> getChats() { return chats; }
@ -419,7 +467,9 @@ public final class LocalDB implements EventListener {
* @return {@code true} if an {@link IDGenerator} is present * @return {@code true} if an {@link IDGenerator} is present
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public boolean hasIDGenerator() { return idGenerator != null; } public boolean hasIDGenerator() {
return idGenerator != null;
}
/** /**
* @return the cache map for messages and message status changes * @return the cache map for messages and message status changes

View File

@ -5,16 +5,16 @@ import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import envoy.client.event.EnvoyCloseEvent;
import envoy.util.*;
import dev.kske.eventbus.*; import dev.kske.eventbus.*;
import dev.kske.eventbus.EventListener; import dev.kske.eventbus.EventListener;
import envoy.util.*;
import envoy.client.event.EnvoyCloseEvent;
/** /**
* Manages all application settings, which are different objects that can be * Manages all application settings, which are different objects that can be changed during runtime
* changed during runtime and serialized them by using either the file system or * and serialized them by using either the file system or the {@link Preferences} API.
* the {@link Preferences} API.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -29,7 +29,8 @@ public final class Settings implements EventListener {
/** /**
* Settings are stored in this file. * Settings are stored in this file.
*/ */
private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser"); private static final File settingsFile =
new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
/** /**
* Singleton instance of this class. * Singleton instance of this class.
@ -37,8 +38,8 @@ public final class Settings implements EventListener {
private static Settings settings = new Settings(); private static Settings settings = new Settings();
/** /**
* The way to instantiate the settings. Is set to private to deny other * The way to instantiate the settings. Is set to private to deny other instances of that
* instances of that object. * object.
* *
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
@ -76,20 +77,27 @@ public final class Settings implements EventListener {
try { try {
SerializationUtils.write(settingsFile, items); SerializationUtils.write(settingsFile, items);
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ", e); EnvoyLog.getLogger(Settings.class).log(Level.SEVERE, "Unable to save settings file: ",
e);
} }
} }
private void supplementDefaults() { private void supplementDefaults() {
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key.")); items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send",
items.putIfAbsent("hideOnClose", new SettingsItem<>(false, "Hide on close", "Hides the chat window when it is closed.")); "Sends a message by pressing the enter key."));
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme.")); items.putIfAbsent("hideOnClose",
new SettingsItem<>(false, "Hide on close", "Hides the chat window when it is closed."));
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name",
"The name of the currently selected theme."));
items.putIfAbsent("downloadLocation", items.putIfAbsent("downloadLocation",
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location", new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"),
"The location where files will be saved to")); "Download location",
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?", "Should downloads be saved without asking?")); "The location where files will be saved to"));
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?",
"Should downloads be saved without asking?"));
items.putIfAbsent("askForConfirmation", items.putIfAbsent("askForConfirmation",
new SettingsItem<>(true, "Ask for confirmation", "Will ask for confirmation before doing certain things")); new SettingsItem<>(true, "Ask for confirmation",
"Will ask for confirmation before doing certain things"));
} }
/** /**
@ -104,7 +112,9 @@ public final class Settings implements EventListener {
* @param themeName the name to set * @param themeName the name to set
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); } public void setCurrentTheme(String themeName) {
((SettingsItem<String>) items.get("currentTheme")).set(themeName);
}
/** /**
* @return true if the currently used theme is one of the default themes * @return true if the currently used theme is one of the default themes
@ -116,9 +126,8 @@ public final class Settings implements EventListener {
} }
/** /**
* @return {@code true}, if pressing the {@code Enter} key suffices to send a * @return {@code true}, if pressing the {@code Enter} key suffices to send a message. Otherwise
* message. Otherwise it has to be pressed in conjunction with the * it has to be pressed in conjunction with the {@code Control} key.
* {@code Control} key.
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); } public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
@ -126,26 +135,27 @@ public final class Settings implements EventListener {
/** /**
* Changes the keystrokes performed by the user to send a message. * Changes the keystrokes performed by the user to send a message.
* *
* @param enterToSend If set to {@code true} a message can be sent by pressing * @param enterToSend If set to {@code true} a message can be sent by pressing the {@code Enter}
* the {@code Enter} key. Otherwise it has to be pressed in * key. Otherwise it has to be pressed in conjunction with the
* conjunction with the {@code Control} key. * {@code Control} key.
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); } public void setEnterToSend(boolean enterToSend) {
((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend);
}
/** /**
* @return whether Envoy will prompt a dialogue before saving an * @return whether Envoy will prompt a dialogue before saving an {@link envoy.data.Attachment}
* {@link envoy.data.Attachment}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Boolean isDownloadSavedWithoutAsking() { return (Boolean) items.get("autoSaveDownloads").get(); } public Boolean isDownloadSavedWithoutAsking() {
return (Boolean) items.get("autoSaveDownloads").get();
}
/** /**
* Sets whether Envoy will prompt a dialogue before saving an * Sets whether Envoy will prompt a dialogue before saving an {@link envoy.data.Attachment}.
* {@link envoy.data.Attachment}.
* *
* @param autosaveDownload whether a download should be saved without asking * @param autosaveDownload whether a download should be saved without asking before
* before
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void setDownloadSavedWithoutAsking(boolean autosaveDownload) { public void setDownloadSavedWithoutAsking(boolean autosaveDownload) {
@ -164,7 +174,9 @@ public final class Settings implements EventListener {
* @param downloadLocation the path to set * @param downloadLocation the path to set
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void setDownloadLocation(File downloadLocation) { ((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation); } public void setDownloadLocation(File downloadLocation) {
((SettingsItem<File>) items.get("downloadLocation")).set(downloadLocation);
}
/** /**
* @return the current on close mode. * @return the current on close mode.
@ -178,21 +190,24 @@ public final class Settings implements EventListener {
* @param hideOnClose whether the application should be minimized on close * @param hideOnClose whether the application should be minimized on close
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); } public void setHideOnClose(boolean hideOnClose) {
((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose);
}
/** /**
* @return whether a confirmation dialog should be displayed before certain * @return whether a confirmation dialog should be displayed before certain actions
* actions
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public Boolean isAskForConfirmation() { return (Boolean) items.get("askForConfirmation").get(); } public Boolean isAskForConfirmation() {
return (Boolean) items.get("askForConfirmation").get();
}
/** /**
* Changes the behavior of calling certain functionality by displaying a * Changes the behavior of calling certain functionality by displaying a confirmation dialog
* confirmation dialog before executing it. * before executing it.
* *
* @param askForConfirmation whether confirmation dialogs should be displayed * @param askForConfirmation whether confirmation dialogs should be displayed before certain
* before certain actions * actions
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void setAskForConfirmation(boolean askForConfirmation) { public void setAskForConfirmation(boolean askForConfirmation) {

View File

@ -6,8 +6,7 @@ import java.util.function.Consumer;
import javax.swing.JComponent; import javax.swing.JComponent;
/** /**
* Encapsulates a persistent value that is directly or indirectly mutable by the * Encapsulates a persistent value that is directly or indirectly mutable by the user.
* user.
* *
* @param <T> the type of this {@link SettingsItem}'s value * @param <T> the type of this {@link SettingsItem}'s value
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -23,9 +22,8 @@ public final class SettingsItem<T> implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Initializes a {@link SettingsItem}. The default value's class will be mapped * Initializes a {@link SettingsItem}. The default value's class will be mapped to a
* to a {@link JComponent} that can be used to display this {@link SettingsItem} * {@link JComponent} that can be used to display this {@link SettingsItem} to the user.
* to the user.
* *
* @param value the default value * @param value the default value
* @param userFriendlyName the user friendly name (short) * @param userFriendlyName the user friendly name (short)
@ -42,17 +40,20 @@ public final class SettingsItem<T> implements Serializable {
* @return the value * @return the value
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public T get() { return value; } public T get() {
return value;
}
/** /**
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if * Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if defined, it
* defined, it will be invoked with this value. * will be invoked with this value.
* *
* @param value the value to set * @param value the value to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void set(T value) { public void set(T value) {
if (changeHandler != null && value != this.value) changeHandler.accept(value); if (changeHandler != null && value != this.value)
changeHandler.accept(value);
this.value = value; this.value = value;
} }
@ -66,7 +67,9 @@ public final class SettingsItem<T> implements Serializable {
* @param userFriendlyName the userFriendlyName to set * @param userFriendlyName the userFriendlyName to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; } public void setUserFriendlyName(String userFriendlyName) {
this.userFriendlyName = userFriendlyName;
}
/** /**
* @return the description * @return the description
@ -81,9 +84,8 @@ public final class SettingsItem<T> implements Serializable {
public void setDescription(String description) { this.description = description; } public void setDescription(String description) { this.description = description; }
/** /**
* Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be * Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be invoked with the
* invoked with the current value once during the registration and every time * current value once during the registration and every time when the value changes.
* when the value changes.
* *
* @param changeHandler the changeHandler to set * @param changeHandler the changeHandler to set
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha

View File

@ -22,7 +22,9 @@ public final class AudioPlayer {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); } public AudioPlayer() {
this(AudioRecorder.DEFAULT_AUDIO_FORMAT);
}
/** /**
* Initializes the player with a given audio format. * Initializes the player with a given audio format.

View File

@ -20,7 +20,8 @@ public final class AudioRecorder {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false); public static final AudioFormat DEFAULT_AUDIO_FORMAT =
new AudioFormat(16000, 16, 1, true, false);
/** /**
* The format in which audio files will be saved. * The format in which audio files will be saved.
@ -38,7 +39,9 @@ public final class AudioRecorder {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); } public AudioRecorder() {
this(DEFAULT_AUDIO_FORMAT);
}
/** /**
* Initializes the recorder with a given audio format. * Initializes the recorder with a given audio format.

View File

@ -1,6 +1,6 @@
/** /**
* Contains classes related to recording and playing back audio clips. * Contains classes related to recording and playing back audio clips.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */

View File

@ -3,8 +3,7 @@ package envoy.client.data.commands;
import java.util.List; import java.util.List;
/** /**
* This interface defines an action that should be performed when a system * This interface defines an action that should be performed when a system command gets called.
* command gets called.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -12,11 +11,9 @@ import java.util.List;
public interface Callable { public interface Callable {
/** /**
* Performs the instance specific action when a {@link SystemCommand} has been * Performs the instance specific action when a {@link SystemCommand} has been called.
* called.
* *
* @param arguments the arguments that should be passed to the * @param arguments the arguments that should be passed to the {@link SystemCommand}
* {@link SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
void call(List<String> arguments); void call(List<String> arguments);

View File

@ -4,15 +4,12 @@ import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
* This class is the base class of all {@code SystemCommands} and contains an * This class is the base class of all {@code SystemCommands} and contains an action and a number of
* action and a number of arguments that should be used as input for this * arguments that should be used as input for this function. No {@code SystemCommand} can return
* function. * anything. Every {@code SystemCommand} must have as argument type {@code List<String>} so that the
* No {@code SystemCommand} can return anything. * words following the indicator String can be used as input of the function. This approach has one
* Every {@code SystemCommand} must have as argument type {@code List<String>} * limitation:<br>
* so that the words following the indicator String can be used as input of the * <b>Order matters!</b> Changing the order of arguments will likely result in unexpected behavior.
* function. This approach has one limitation:<br>
* <b>Order matters!</b> Changing the order of arguments will likely result in
* unexpected behavior.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -28,8 +25,8 @@ public final class SystemCommand implements Callable {
/** /**
* This function takes a {@code List<String>} as argument because automatically * This function takes a {@code List<String>} as argument because automatically
* {@code SystemCommand#numberOfArguments} words following the necessary command * {@code SystemCommand#numberOfArguments} words following the necessary command will be put
* will be put into this list. * into this list.
* *
* @see String#split(String) * @see String#split(String)
*/ */
@ -48,7 +45,8 @@ public final class SystemCommand implements Callable {
* @param description the description of this {@code SystemCommand} * @param description the description of this {@code SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand(Consumer<List<String>> action, int numberOfArguments, List<String> defaults, String description) { public SystemCommand(Consumer<List<String>> action, int numberOfArguments,
List<String> defaults, String description) {
this.numberOfArguments = numberOfArguments; this.numberOfArguments = numberOfArguments;
this.action = action; this.action = action;
this.defaults = defaults == null ? new ArrayList<>() : defaults; this.defaults = defaults == null ? new ArrayList<>() : defaults;
@ -92,20 +90,27 @@ public final class SystemCommand implements Callable {
public List<String> getDefaults() { return defaults; } public List<String> getDefaults() { return defaults; }
@Override @Override
public int hashCode() { return Objects.hash(action); } public int hashCode() {
return Objects.hash(action);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final var other = (SystemCommand) obj; final var other = (SystemCommand) obj;
return Objects.equals(action, other.action); return Objects.equals(action, other.action);
} }
@Override @Override
public String toString() { public String toString() {
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", " return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments
+ (description != null ? "description=" + description + ", " : "") + (defaults != null ? "defaults=" + defaults : "") + "]"; + ", "
+ (description != null ? "description=" + description + ", " : "")
+ (defaults != null ? "defaults=" + defaults : "") + "]";
} }
} }

View File

@ -20,18 +20,21 @@ public final class SystemCommandBuilder {
private final SystemCommandMap commandsMap; private final SystemCommandMap commandsMap;
/** /**
* Creates a new {@code SystemCommandsBuilder} without underlying * Creates a new {@code SystemCommandsBuilder} without underlying {@link SystemCommandMap}.
* {@link SystemCommandMap}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommandBuilder() { this(null); } public SystemCommandBuilder() {
this(null);
}
/** /**
* @param commandsMap the map to use when calling build (optional) * @param commandsMap the map to use when calling build (optional)
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommandBuilder(SystemCommandMap commandsMap) { this.commandsMap = commandsMap; } public SystemCommandBuilder(SystemCommandMap commandsMap) {
this.commandsMap = commandsMap;
}
/** /**
* @param numberOfArguments the numberOfArguments to set * @param numberOfArguments the numberOfArguments to set
@ -104,12 +107,14 @@ public final class SystemCommandBuilder {
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand build() { return build(true); } public SystemCommand build() {
return build(true);
}
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the * {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the previous
* previous value.<br> * value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
@ -122,8 +127,8 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the * {@code SystemCommand#numberOfArguments} will be set to use the rest of the string as
* string as argument, regardless of the previous value.<br> * argument, regardless of the previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
@ -136,27 +141,25 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. * Automatically adds the built object to the given map. At the end, this
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must * {@code SystemCommandBuilder} <b>can</b> be reset but must not be.
* not be.
* *
* @param reset whether this {@code SystemCommandBuilder} should be reset * @param reset whether this {@code SystemCommandBuilder} should be reset afterwards.<br>
* afterwards.<br> * This can be useful if another command wants to execute something similar
* This can be useful if another command wants to execute something
* similar
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand build(boolean reset) { public SystemCommand build(boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description); final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance); sc.setRelevance(relevance);
if (reset) reset(); if (reset)
reset();
return sc; return sc;
} }
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data. * Builds a {@code SystemCommand} based upon the previously entered data. Automatically adds the
* Automatically adds the built object to the given map. * built object to the given map.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap} * {@link SystemCommandMap}
@ -164,13 +167,14 @@ public final class SystemCommandBuilder {
* @throws NullPointerException if no map has been assigned to this builder * @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public SystemCommand build(String command) { return build(command, true); } public SystemCommand build(String command) {
return build(command, true);
}
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. * Automatically adds the built object to the given map. {@code SystemCommand#numberOfArguments}
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the * will be set to 0, regardless of the previous value.<br>
* previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
@ -186,9 +190,8 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. * Automatically adds the built object to the given map. {@code SystemCommand#numberOfArguments}
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the * will be set to use the rest of the string as argument, regardless of the previous value.<br>
* string as argument, regardless of the previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset. * At the end, this {@code SystemCommandBuilder} will be reset.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
@ -204,17 +207,13 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map. * Automatically adds the built object to the given map. At the end, this
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must * {@code SystemCommandBuilder} <b>can</b> be reset but must not be.
* not be.
* *
* @param command the command under which to store the SystemCommand in the * @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap} * {@link SystemCommandMap}
* @param reset whether this {@code SystemCommandBuilder} should be reset * @param reset whether this {@code SystemCommandBuilder} should be reset afterwards.<br>
* afterwards.<br> * This can be useful if another command wants to execute something similar
* This can be useful if another command wants to execute
* something
* similar
* @return the built {@code SystemCommand} * @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder * @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -222,9 +221,12 @@ public final class SystemCommandBuilder {
public SystemCommand build(String command, boolean reset) { public SystemCommand build(String command, boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description); final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance); sc.setRelevance(relevance);
if (commandsMap != null) commandsMap.add(command, sc); if (commandsMap != null)
else throw new NullPointerException("No map in SystemCommandsBuilder present"); commandsMap.add(command, sc);
if (reset) reset(); else
throw new NullPointerException("No map in SystemCommandsBuilder present");
if (reset)
reset();
return sc; return sc;
} }
} }

View File

@ -14,11 +14,9 @@ import javafx.scene.control.Alert.AlertType;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* Stores all {@link SystemCommand}s used. * Stores all {@link SystemCommand}s used. SystemCommands can be called using an activator char and
* SystemCommands can be called using an activator char and the text that needs * the text that needs to be present behind the activator. Additionally offers the option to request
* to be present behind the activator. * recommendations for a partial input String.
* Additionally offers the option to request recommendations for a partial input
* String.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -27,96 +25,100 @@ public final class SystemCommandMap {
private final Character activator; private final Character activator;
private final Map<String, SystemCommand> systemCommands = new HashMap<>(); private final Map<String, SystemCommand> systemCommands = new HashMap<>();
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!/\\(\\)\\?\\.\\,\\;\\-]+$"); private final Pattern commandPattern =
Pattern.compile("^[a-zA-Z0-9_:!/\\(\\)\\?\\.\\,\\;\\-]+$");
private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class); private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
/** /**
* Creates a new {@code SystemCommandMap} with the given char as activator. * Creates a new {@code SystemCommandMap} with the given char as activator. If this Character is
* If this Character is null, any text used as input will be treated as a system * null, any text used as input will be treated as a system command.
* command.
* *
* @param activator the char to use as activator for commands * @param activator the char to use as activator for commands
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public SystemCommandMap(Character activator) { this.activator = activator; } public SystemCommandMap(Character activator) {
this.activator = activator;
}
/** /**
* Creates a new {@code SystemCommandMap} with '/' as activator. * Creates a new {@code SystemCommandMap} with '/' as activator.
* *
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public SystemCommandMap() { activator = '/'; } public SystemCommandMap() {
activator = '/';
}
/** /**
* Adds a new command to the map if the command name is valid. * Adds a new command to the map if the command name is valid.
* *
* @param command the input string to execute the * @param command the input string to execute the given action
* given action * @param systemCommand the command to add - can be built using {@link SystemCommandBuilder}
* @param systemCommand the command to add - can be built using
* {@link SystemCommandBuilder}
* @see SystemCommandMap#isValidKey(String) * @see SystemCommandMap#isValidKey(String)
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void add(String command, SystemCommand systemCommand) { public void add(String command, SystemCommand systemCommand) {
if (isValidKey(command)) systemCommands.put(command.toLowerCase(), systemCommand); if (isValidKey(command))
systemCommands.put(command.toLowerCase(), systemCommand);
} }
/** /**
* This method checks if the input String is a key in the map and returns the * This method checks if the input String is a key in the map and returns the wrapped System
* wrapped System command if present. * command if present.
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
* {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, * {@code systemCommands.add("example", new SystemCommand(text -> {}, 1, null, ""));}<br>
* ""));}<br>
* {@code ....}<br> * {@code ....}<br>
* user input: {@code "*example xyz ..."}<br> * user input: {@code "*example xyz ..."}<br>
* {@code systemCommands.get("example xyz ...")} or * {@code systemCommands.get("example xyz ...")} or
* {@code systemCommands.get("*example xyz ...")} * {@code systemCommands.get("*example xyz ...")} result:
* result: {@code Optional<SystemCommand>.get() != null} * {@code Optional<SystemCommand>.get() != null}
* *
* @param input the input string given by the user * @param input the input string given by the user
* @return the wrapped system command, if present * @return the wrapped system command, if present
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Optional<SystemCommand> get(String input) { return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase()))); } public Optional<SystemCommand> get(String input) {
return Optional.ofNullable(systemCommands.get(getCommand(input.toLowerCase())));
}
/** /**
* This method ensures that the activator of a {@link SystemCommand} is * This method ensures that the activator of a {@link SystemCommand} is stripped.<br>
* stripped.<br> * It only checks the word beginning from the first non-blank position in the input. It returns
* It only checks the word beginning from the first non-blank position in the * the command as (most likely) entered as key in the map for the first word of the text.<br>
* input.
* It returns the command as (most likely) entered as key in the map for the
* first word of the text.<br>
* Activators in the middle of the word will be disregarded. * Activators in the middle of the word will be disregarded.
* *
* @param raw the input * @param raw the input
* @return the command as entered in the map * @return the command as entered in the map
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote this method will (most likely) not return anything useful if * @apiNote this method will (most likely) not return anything useful if whatever is entered
* whatever is entered after the activator is not a system command. * after the activator is not a system command. Only exception: for recommendation
* Only exception: for recommendation purposes. * purposes.
*/ */
public String getCommand(String raw) { public String getCommand(String raw) {
final var trimmed = raw.stripLeading(); final var trimmed = raw.stripLeading();
// Entering only the activator should not throw an error // Entering only the activator should not throw an error
if (trimmed.length() == 1 && activator != null && activator.equals(trimmed.charAt(0))) return ""; if (trimmed.length() == 1 && activator != null && activator.equals(trimmed.charAt(0)))
return "";
else { else {
final var index = trimmed.indexOf(' '); final var index = trimmed.indexOf(' ');
return trimmed.substring(activator != null && activator.equals(trimmed.charAt(0)) ? 1 : 0, index < 1 ? trimmed.length() : index); return trimmed.substring(
activator != null && activator.equals(trimmed.charAt(0)) ? 1 : 0,
index < 1 ? trimmed.length() : index);
} }
} }
/** /**
* Examines whether a key can be put in the map and logs it with * Examines whether a key can be put in the map and logs it with {@code Level.WARNING} if that
* {@code Level.WARNING} if that key violates API constrictions.<br> * key violates API constrictions.<br>
* (allowed chars are <b>a-zA-Z0-9_:!/()?.,;-</b>) * (allowed chars are <b>a-zA-Z0-9_:!/()?.,;-</b>)
* <p> * <p>
* The approach to not throw an exception was taken so that an ugly try-catch * The approach to not throw an exception was taken so that an ugly try-catch block for every
* block for every addition to the system commands map could be avoided, an * addition to the system commands map could be avoided, an error that should only occur during
* error that should only occur during implementation and not in production. * implementation and not in production.
* *
* @param command the key to examine * @param command the key to examine
* @return whether this key can be used in the map * @return whether this key can be used in the map
@ -124,17 +126,18 @@ public final class SystemCommandMap {
*/ */
public boolean isValidKey(String command) { public boolean isValidKey(String command) {
final var valid = commandPattern.matcher(command).matches(); final var valid = commandPattern.matcher(command).matches();
if (!valid) logger.log(Level.WARNING, if (!valid)
logger.log(Level.WARNING,
"The command \"" + command "The command \"" + command
+ "\" is not valid. As it might cause problems when executed, it will not be entered into the map. Only the characters " + "\" is not valid. As it might cause problems when executed, it will not be entered into the map. Only the characters "
+ commandPattern + "are allowed"); + commandPattern + "are allowed");
return valid; return valid;
} }
/** /**
* Takes a 'raw' string (the whole input) and checks if the activator is the * Takes a 'raw' string (the whole input) and checks if the activator is the first visible
* first visible character and then checks if a command is present after that * character and then checks if a command is present after that activator. If that is the case,
* activator. If that is the case, it will be executed. * it will be executed.
* *
* @param raw the raw input string * @param raw the raw input string
* @return whether a command could be found and successfully executed * @return whether a command could be found and successfully executed
@ -144,24 +147,26 @@ public final class SystemCommandMap {
// possibly a command was detected and could be executed // possibly a command was detected and could be executed
final var raw2 = raw.stripLeading(); final var raw2 = raw.stripLeading();
final var commandFound = activator == null || raw2.startsWith(activator.toString()) ? executeAvailableCommand(raw2) : false; final var commandFound = activator == null || raw2.startsWith(activator.toString())
? executeAvailableCommand(raw2)
: false;
// the command was executed successfully - no further checking needed // the command was executed successfully - no further checking needed
if (commandFound) logger.log(Level.FINE, "executed system command " + getCommand(raw2)); if (commandFound)
logger.log(Level.FINE, "executed system command " + getCommand(raw2));
return commandFound; return commandFound;
} }
/** /**
* Retrieves the recommendations based on the current input entered.<br> * Retrieves the recommendations based on the current input entered.<br>
* The first word is used for the recommendations and * The first word is used for the recommendations and it does not matter if the activator is at
* it does not matter if the activator is at its beginning or not.<br> * its beginning or not.<br>
* If recommendations are present, the given function will be executed on the * If recommendations are present, the given function will be executed on the
* recommendations.<br> * recommendations.<br>
* Otherwise nothing will be done.<br> * Otherwise nothing will be done.<br>
* *
* @param input the input string * @param input the input string
* @param action the action that should be taken for the recommendations, if any * @param action the action that should be taken for the recommendations, if any are present
* are present
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void requestRecommendations(String input, Consumer<Set<String>> action) { public void requestRecommendations(String input, Consumer<Set<String>> action) {
@ -169,27 +174,28 @@ public final class SystemCommandMap {
// Get the expected commands // Get the expected commands
final var recommendations = recommendCommands(partialCommand); final var recommendations = recommendCommands(partialCommand);
if (recommendations.isEmpty()) return; if (recommendations.isEmpty())
return;
// Execute the given action // Execute the given action
else action.accept(recommendations); else
action.accept(recommendations);
} }
/** /**
* This method checks if the input String is a key in the map and executes the * This method checks if the input String is a key in the map and executes the wrapped System
* wrapped System command if present. * command if present.
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap('*');}<br>
* {@code Button button = new Button();}<br> * {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", new SystemCommand(text -> * {@code systemCommands.add("example", new SystemCommand(text -> {button.setText(text.get(0))},
* {button.setText(text.get(0))}, 1, null, * 1, null, ""));}<br>
* ""));}<br>
* {@code ....}<br> * {@code ....}<br>
* user input: {@code "*example xyz ..."}<br> * user input: {@code "*example xyz ..."}<br>
* {@code systemCommands.executeIfPresent("example xyz ...")} or * {@code systemCommands.executeIfPresent("example xyz ...")} or
* {@code systemCommands.executeIfPresent("*example xyz ...")} * {@code systemCommands.executeIfPresent("*example xyz ...")} result:
* result: {@code button.getText()=="xyz"} * {@code button.getText()=="xyz"}
* *
* @param input the input string given by the user * @param input the input string given by the user
* @return whether a command could be found and successfully executed * @return whether a command could be found and successfully executed
@ -211,9 +217,9 @@ public final class SystemCommandMap {
systemCommand.call(arguments); systemCommand.call(arguments);
} catch (final NumberFormatException e) { } catch (final NumberFormatException e) {
logger.log(Level.INFO, logger.log(Level.INFO,
String.format( String.format(
"System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.", "System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.",
command)); command));
Platform.runLater(() -> { Platform.runLater(() -> {
final var alert = new Alert(AlertType.ERROR); final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Please enter a readable number as argument."); alert.setContentText("Please enter a readable number as argument.");
@ -224,7 +230,8 @@ public final class SystemCommandMap {
logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e); logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e);
Platform.runLater(() -> { Platform.runLater(() -> {
final var alert = new Alert(AlertType.ERROR); final var alert = new Alert(AlertType.ERROR);
alert.setContentText("Could not execute system command: Internal error. Please insult the responsible programmer."); alert.setContentText(
"Could not execute system command: Internal error. Please insult the responsible programmer.");
alert.showAndWait(); alert.showAndWait();
}); });
commandExecuted.set(false); commandExecuted.set(false);
@ -245,7 +252,8 @@ public final class SystemCommandMap {
// no more arguments follow after the command (e.g. text = "/DABR") // no more arguments follow after the command (e.g. text = "/DABR")
final var indexOfSpace = input.indexOf(" "); final var indexOfSpace = input.indexOf(" ");
if (indexOfSpace < 0) return supplementDefaults(new String[] {}, systemCommand); if (indexOfSpace < 0)
return supplementDefaults(new String[] {}, systemCommand);
// the arguments behind a system command // the arguments behind a system command
final var remainingString = input.substring(indexOfSpace + 1); final var remainingString = input.substring(indexOfSpace + 1);
@ -253,15 +261,17 @@ public final class SystemCommandMap {
// splitting those arguments and supplying default values // splitting those arguments and supplying default values
final var textArguments = remainingString.split(" ", -1); final var textArguments = remainingString.split(" ", -1);
final var originalArguments = numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments) : textArguments; final var originalArguments =
numberOfArguments >= 0 ? Arrays.copyOfRange(textArguments, 0, numberOfArguments)
: textArguments;
final var arguments = supplementDefaults(originalArguments, systemCommand); final var arguments = supplementDefaults(originalArguments, systemCommand);
return arguments; return arguments;
} }
/** /**
* Recommends commands based upon the currently entered input.<br> * Recommends commands based upon the currently entered input.<br>
* In the current implementation, all that gets checked is whether a key * In the current implementation, all that gets checked is whether a key contains this input.
* contains this input. This might be updated later on. * This might be updated later on.
* *
* @param partialCommand the partially entered command * @param partialCommand the partially entered command
* @return a set of all commands that match this input * @return a set of all commands that match this input
@ -274,36 +284,41 @@ public final class SystemCommandMap {
return systemCommands.keySet() return systemCommands.keySet()
.stream() .stream()
.filter(command -> command.contains(partialCommand)) .filter(command -> command.contains(partialCommand))
.sorted((command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(), systemCommands.get(command2).getRelevance())) .sorted(
(command1, command2) -> Integer.compare(systemCommands.get(command1).getRelevance(),
systemCommands.get(command2).getRelevance()))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
/** /**
* Supplies the default values for arguments if none are present in the text for * Supplies the default values for arguments if none are present in the text for any argument.
* any argument. <br> * <br>
* *
* @param textArguments the arguments that were parsed from the text * @param textArguments the arguments that were parsed from the text
* @param toEvaluate the system command whose default values should be used * @param toEvaluate the system command whose default values should be used
* @return the final argument list * @return the final argument list
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote this method will insert an empty String if the size of the list * @apiNote this method will insert an empty String if the size of the list given to the
* given to the {@code SystemCommand} is smaller than its argument * {@code SystemCommand} is smaller than its argument counter and no more text
* counter and no more text arguments could be found. * arguments could be found.
*/ */
private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) { private List<String> supplementDefaults(String[] textArguments, SystemCommand toEvaluate) {
final var defaults = toEvaluate.getDefaults(); final var defaults = toEvaluate.getDefaults();
final var numberOfArguments = toEvaluate.getNumberOfArguments(); final var numberOfArguments = toEvaluate.getNumberOfArguments();
final List<String> result = new ArrayList<>(); final List<String> result = new ArrayList<>();
if (toEvaluate.getNumberOfArguments() > 0) for (var index = 0; index < numberOfArguments; index++) { if (toEvaluate.getNumberOfArguments() > 0)
String textArg = null; for (var index = 0; index < numberOfArguments; index++) {
if (index < textArguments.length) textArg = textArguments[index]; String textArg = null;
if (index < textArguments.length)
textArg = textArguments[index];
// Set the argument at position index to the current argument of the text, if it // Set the argument at position index to the current argument of the text, if it
// is present. Otherwise the default for that argument will be taken if present. // is present. Otherwise the default for that argument will be taken if present.
// In the worst case, an empty String will be used. // In the worst case, an empty String will be used.
result.add(!(textArg == null) && !textArg.isBlank() ? textArg : index < defaults.size() ? defaults.get(index) : ""); result.add(!(textArg == null) && !textArg.isBlank() ? textArg
} : index < defaults.size() ? defaults.get(index) : "");
}
return result; return result;
} }

View File

@ -2,11 +2,12 @@ package envoy.client.data.shortcuts;
import javafx.scene.input.*; import javafx.scene.input.*;
import envoy.data.User.UserStatus;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.util.UserUtil; import envoy.client.util.UserUtil;
import envoy.data.User.UserStatus;
/** /**
* Envoy-specific implementation of the keyboard-shortcut interaction offered by * Envoy-specific implementation of the keyboard-shortcut interaction offered by
@ -29,37 +30,48 @@ public class EnvoyShortcutConfig {
// Add the option to exit with "Control" + "Q" or "Alt" + "F4" as offered by // Add the option to exit with "Control" + "Q" or "Alt" + "F4" as offered by
// some desktop environments // some desktop environments
instance.add(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit); instance.add(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN),
ShutdownHelper::exit);
// Add the option to logout using "Control"+"Shift"+"L" if not in login scene // Add the option to logout using "Control"+"Shift"+"L" if not in login scene
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), instance.addForNotExcluded(
UserUtil::logout, new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN,
SceneInfo.LOGIN_SCENE); KeyCombination.SHIFT_DOWN),
UserUtil::logout,
SceneInfo.LOGIN_SCENE);
// Add option to open settings scene with "Control"+"S", if not in login scene // Add option to open settings scene with "Control"+"S", if not in login scene
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), instance.addForNotExcluded(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN),
() -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE), () -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE),
SceneInfo.SETTINGS_SCENE, SceneInfo.SETTINGS_SCENE,
SceneInfo.LOGIN_SCENE); SceneInfo.LOGIN_SCENE);
// Add option to change to status away // Add option to change to status away
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), instance.addForNotExcluded(
() -> UserUtil.changeStatus(UserStatus.AWAY), new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN,
SceneInfo.LOGIN_SCENE); KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.AWAY),
SceneInfo.LOGIN_SCENE);
// Add option to change to status busy // Add option to change to status busy
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), instance.addForNotExcluded(
() -> UserUtil.changeStatus(UserStatus.BUSY), new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN,
SceneInfo.LOGIN_SCENE); KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.BUSY),
SceneInfo.LOGIN_SCENE);
// Add option to change to status offline // Add option to change to status offline
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), instance.addForNotExcluded(
() -> UserUtil.changeStatus(UserStatus.OFFLINE), new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN,
SceneInfo.LOGIN_SCENE); KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.OFFLINE),
SceneInfo.LOGIN_SCENE);
// Add option to change to status online // Add option to change to status online
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), instance.addForNotExcluded(
() -> UserUtil.changeStatus(UserStatus.ONLINE), new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN,
SceneInfo.LOGIN_SCENE); KeyCombination.SHIFT_DOWN),
() -> UserUtil.changeStatus(UserStatus.ONLINE),
SceneInfo.LOGIN_SCENE);
} }
} }

View File

@ -14,7 +14,8 @@ import envoy.client.ui.SceneContext.SceneInfo;
*/ */
public final class GlobalKeyShortcuts { public final class GlobalKeyShortcuts {
private final EnumMap<SceneInfo, Map<KeyCombination, Runnable>> shortcuts = new EnumMap<>(SceneInfo.class); private final EnumMap<SceneInfo, Map<KeyCombination, Runnable>> shortcuts =
new EnumMap<>(SceneInfo.class);
private static GlobalKeyShortcuts instance = new GlobalKeyShortcuts(); private static GlobalKeyShortcuts instance = new GlobalKeyShortcuts();
@ -36,16 +37,16 @@ public final class GlobalKeyShortcuts {
* @param action the action to perform * @param action the action to perform
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public void add(KeyCombination keys, Runnable action) { shortcuts.values().forEach(collection -> collection.put(keys, action)); } public void add(KeyCombination keys, Runnable action) {
shortcuts.values().forEach(collection -> collection.put(keys, action));
}
/** /**
* Adds the given keyboard shortcut and its action to all scenes that are not * Adds the given keyboard shortcut and its action to all scenes that are not part of exclude.
* part of exclude.
* *
* @param keys the keys to press to perform the given action * @param keys the keys to press to perform the given action
* @param action the action to perform * @param action the action to perform
* @param exclude the scenes that should be excluded from receiving this * @param exclude the scenes that should be excluded from receiving this keyboard shortcut
* keyboard shortcut
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public void addForNotExcluded(KeyCombination keys, Runnable action, SceneInfo... exclude) { public void addForNotExcluded(KeyCombination keys, Runnable action, SceneInfo... exclude) {
@ -53,10 +54,10 @@ public final class GlobalKeyShortcuts {
// Computing the remaining sceneInfos // Computing the remaining sceneInfos
final var include = new SceneInfo[SceneInfo.values().length - exclude.length]; final var include = new SceneInfo[SceneInfo.values().length - exclude.length];
int index = 0; int index = 0;
outer: outer: for (final var sceneInfo : SceneInfo.values()) {
for (final var sceneInfo : SceneInfo.values()) {
for (final var excluded : exclude) for (final var excluded : exclude)
if (sceneInfo.equals(excluded)) continue outer; if (sceneInfo.equals(excluded))
continue outer;
include[index++] = sceneInfo; include[index++] = sceneInfo;
} }
@ -72,5 +73,7 @@ public final class GlobalKeyShortcuts {
* @return all stored keyboard shortcuts for this scene * @return all stored keyboard shortcuts for this scene
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public Map<KeyCombination, Runnable> getKeyboardShortcuts(SceneInfo sceneInfo) { return shortcuts.get(sceneInfo); } public Map<KeyCombination, Runnable> getKeyboardShortcuts(SceneInfo sceneInfo) {
return shortcuts.get(sceneInfo);
}
} }

View File

@ -7,10 +7,9 @@ import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext; import envoy.client.ui.SceneContext;
/** /**
* Provides methods to set the keyboard shortcuts for a specific scene. * Provides methods to set the keyboard shortcuts for a specific scene. Should only be implemented
* Should only be implemented by controllers of scenes so that these methods can * by controllers of scenes so that these methods can automatically be called inside
* automatically be called inside {@link SceneContext} as soon * {@link SceneContext} as soon as the underlying FXML file has been loaded.
* as the underlying FXML file has been loaded.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta

View File

@ -17,5 +17,7 @@ public class ContactDisabled extends Event<Contact> {
* @param contact the contact that should be disabled * @param contact the contact that should be disabled
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public ContactDisabled(Contact contact) { super(contact); } public ContactDisabled(Contact contact) {
super(contact);
}
} }

View File

@ -3,9 +3,8 @@ package envoy.client.event;
import envoy.event.Event.Valueless; import envoy.event.Event.Valueless;
/** /**
* This event notifies various Envoy components of the application being about * This event notifies various Envoy components of the application being about to shut down. This
* to shut down. This allows the graceful closing of connections, persisting * allows the graceful closing of connections, persisting local data etc.
* local data etc.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta

View File

@ -16,5 +16,7 @@ public class MessageDeletion extends Event<Long> {
* @param messageID the ID of the deleted message * @param messageID the ID of the deleted message
* @since Envoy Common v0.3-beta * @since Envoy Common v0.3-beta
*/ */
public MessageDeletion(long messageID) { super(messageID); } public MessageDeletion(long messageID) {
super(messageID);
}
} }

View File

@ -17,5 +17,7 @@ public class OwnStatusChange extends Event<UserStatus> {
* @param value the new user status of the client user * @param value the new user status of the client user
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public OwnStatusChange(UserStatus value) { super(value); } public OwnStatusChange(UserStatus value) {
super(value);
}
} }

View File

@ -15,20 +15,19 @@ public final class AlertHelper {
private AlertHelper() {} private AlertHelper() {}
/** /**
* Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()} * Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()} returns
* returns {@code true}. * {@code true}. Immediately executes the action if no dialog was requested or the dialog was
* Immediately executes the action if no dialog was requested or the dialog was * exited with a confirmation. Does nothing if the dialog was closed without clicking on OK.
* exited with a confirmation.
* Does nothing if the dialog was closed without clicking on OK.
* *
* @param alert the (customized) alert to show. <strong>Should not be shown * @param alert the (customized) alert to show. <strong>Should not be shown already</strong>
* already</strong>
* @param action the action to perform in case of success * @param action the action to perform in case of success
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void confirmAction(Alert alert, Runnable action) { public static void confirmAction(Alert alert, Runnable action) {
alert.setHeaderText(""); alert.setHeaderText("");
if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run()); if (Settings.getInstance().isAskForConfirmation())
else action.run(); alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
else
action.run();
} }
} }

View File

@ -1,11 +1,11 @@
package envoy.client.helper; package envoy.client.helper;
import dev.kske.eventbus.EventBus;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent; import envoy.client.event.EnvoyCloseEvent;
import envoy.client.ui.StatusTrayIcon; import envoy.client.ui.StatusTrayIcon;
import dev.kske.eventbus.EventBus;
/** /**
* Simplifies shutdown actions. * Simplifies shutdown actions.
* *
@ -22,18 +22,21 @@ public final class ShutdownHelper {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void exit() { exit(false); } public static void exit() {
exit(false);
}
/** /**
* Exits Envoy immediately if {@code force = true}, * Exits Envoy immediately if {@code force = true}, else it can exit or minimize Envoy,
* else it can exit or minimize Envoy, depending on the current state of * depending on the current state of {@link Settings#isHideOnClose()} and
* {@link Settings#isHideOnClose()} and {@link StatusTrayIcon#isSupported()}. * {@link StatusTrayIcon#isSupported()}.
* *
* @param force whether to close in any case. * @param force whether to close in any case.
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static void exit(boolean force) { public static void exit(boolean force) {
if (!force && Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) Context.getInstance().getStage().setIconified(true); if (!force && Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
Context.getInstance().getStage().setIconified(true);
else { else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent()); EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0); System.exit(0);

View File

@ -5,18 +5,19 @@ import java.net.Socket;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.logging.*; import java.util.logging.*;
import envoy.client.data.*; import dev.kske.eventbus.*;
import envoy.client.event.EnvoyCloseEvent; import dev.kske.eventbus.Event;
import envoy.data.*; import envoy.data.*;
import envoy.event.*; import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import dev.kske.eventbus.*; import envoy.client.data.*;
import dev.kske.eventbus.Event; import envoy.client.event.EnvoyCloseEvent;
/** /**
* Establishes a connection to the server, performs a handshake and delivers * Establishes a connection to the server, performs a handshake and delivers certain objects to the
* certain objects to the server. * server.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -44,27 +45,31 @@ public final class Client implements EventListener, Closeable {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Client() { eventBus.registerListener(this); } public Client() {
eventBus.registerListener(this);
}
/** /**
* Enters the online mode by acquiring a user ID from the server. As a * Enters the online mode by acquiring a user ID from the server. As a connection has to be
* connection has to be established and a handshake has to be made, this method * established and a handshake has to be made, this method will block for up to 5 seconds. If
* will block for up to 5 seconds. If the handshake does exceed this time limit, * the handshake does exceed this time limit, an exception is thrown.
* an exception is thrown.
* *
* @param credentials the login credentials of the user * @param credentials the login credentials of the user
* @param cacheMap the map of all caches needed * @param cacheMap the map of all caches needed
* @throws TimeoutException if the server could not be reached * @throws TimeoutException if the server could not be reached
* @throws IOException if the login credentials could not be written * @throws IOException if the login credentials could not be written
* @throws InterruptedException if the current thread is interrupted while * @throws InterruptedException if the current thread is interrupted while waiting for the
* waiting for the handshake response * handshake response
*/ */
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException { public void performHandshake(LoginCredentials credentials, CacheMap cacheMap)
if (online) throw new IllegalStateException("Handshake has already been performed successfully"); throws TimeoutException, IOException, InterruptedException {
if (online)
throw new IllegalStateException("Handshake has already been performed successfully");
rejected = false; rejected = false;
// Establish TCP connection // Establish TCP connection
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...",
config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort()); socket = new Socket(config.getServer(), config.getPort());
logger.log(Level.FINE, "Successfully established TCP connection to server"); logger.log(Level.FINE, "Successfully established TCP connection to server");
@ -106,14 +111,12 @@ public final class Client implements EventListener, Closeable {
} }
/** /**
* Initializes the {@link Receiver} used to process data sent from the server to * Initializes the {@link Receiver} used to process data sent from the server to this client.
* this client.
* *
* @param localDB the local database used to persist the current * @param localDB the local database used to persist the current {@link IDGenerator}
* {@link IDGenerator}
* @param cacheMap the map of all caches needed * @param cacheMap the map of all caches needed
* @throws IOException if no {@link IDGenerator} is present and none could be * @throws IOException if no {@link IDGenerator} is present and none could be requested from the
* requested from the server * server
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha
*/ */
public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException { public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
@ -129,7 +132,8 @@ public final class Client implements EventListener, Closeable {
cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch); cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch);
// Request a generator if none is present or the existing one is consumed // Request a generator if none is present or the existing one is consumed
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIDGenerator(); if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext())
requestIDGenerator();
// Relay caches // Relay caches
cacheMap.getMap().values().forEach(Cache::relay); cacheMap.getMap().values().forEach(Cache::relay);
@ -154,8 +158,8 @@ public final class Client implements EventListener, Closeable {
} }
/** /**
* Sends a message to the server. The message's status will be incremented once * Sends a message to the server. The message's status will be incremented once it was delivered
* it was delivered successfully. * successfully.
* *
* @param message the message to send * @param message the message to send
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -176,7 +180,9 @@ public final class Client implements EventListener, Closeable {
} }
@Event(eventType = HandshakeRejection.class, priority = 1000) @Event(eventType = HandshakeRejection.class, priority = 1000)
private void onHandshakeRejection() { rejected = true; } private void onHandshakeRejection() {
rejected = true;
}
@Override @Override
@Event(eventType = EnvoyCloseEvent.class, priority = 50) @Event(eventType = EnvoyCloseEvent.class, priority = 50)
@ -201,7 +207,10 @@ public final class Client implements EventListener, Closeable {
* @throws IllegalStateException if the client is not online * @throws IllegalStateException if the client is not online
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
private void checkOnline() throws IllegalStateException { if (!online) throw new IllegalStateException("Client is not online"); } private void checkOnline() throws IllegalStateException {
if (!online)
throw new IllegalStateException("Client is not online");
}
/** /**
* @return the {@link User} as which this client is logged in * @return the {@link User} as which this client is logged in

View File

@ -6,13 +6,12 @@ import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.*; import java.util.logging.*;
import envoy.util.*;
import dev.kske.eventbus.*; import dev.kske.eventbus.*;
import envoy.util.*;
/** /**
* Receives objects from the server and passes them to processor objects based * Receives objects from the server and passes them to processor objects based on their class.
* on their class.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -40,8 +39,7 @@ public final class Receiver extends Thread {
} }
/** /**
* Starts the receiver loop. When an object is read, it is passed to the * Starts the receiver loop. When an object is read, it is passed to the appropriate processor.
* appropriate processor.
* *
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
@ -66,15 +64,19 @@ public final class Receiver extends Thread {
// Server has stopped sending, i.e. because he went offline // Server has stopped sending, i.e. because he went offline
if (bytesRead == -1) { if (bytesRead == -1) {
isAlive = false; isAlive = false;
logger.log(Level.INFO, "Lost connection to the server. Exiting receiver..."); logger.log(Level.INFO,
"Lost connection to the server. Exiting receiver...");
continue; continue;
} }
logger.log(Level.WARNING, logger.log(Level.WARNING,
String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead)); String.format(
"LV encoding violated: expected %d bytes, received %d bytes. Discarding object...",
len, bytesRead));
continue; continue;
} }
try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) { try (ObjectInputStream oin =
new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
final Object obj = oin.readObject(); final Object obj = oin.readObject();
logger.log(Level.FINE, "Received " + obj); logger.log(Level.FINE, "Received " + obj);
@ -83,12 +85,17 @@ public final class Receiver extends Thread {
final Consumer processor = processors.get(obj.getClass()); final Consumer processor = processors.get(obj.getClass());
// Dispatch to the processor if present // Dispatch to the processor if present
if (processor != null) processor.accept(obj); if (processor != null)
processor.accept(obj);
// Dispatch to the event bus if the object is an event without a processor // Dispatch to the event bus if the object is an event without a processor
else if (obj instanceof IEvent) eventBus.dispatch((IEvent) obj); else if (obj instanceof IEvent)
eventBus.dispatch((IEvent) obj);
// Notify if no processor could be located // Notify if no processor could be located
else logger.log(Level.WARNING, else
String.format("The received object has the %s for which no processor is defined.", obj.getClass())); logger.log(Level.WARNING,
String.format(
"The received object has the %s for which no processor is defined.",
obj.getClass()));
} }
} catch (final SocketException | EOFException e) { } catch (final SocketException | EOFException e) {
// Connection probably closed by client. // Connection probably closed by client.
@ -100,14 +107,16 @@ public final class Receiver extends Thread {
} }
/** /**
* Adds an object processor to this {@link Receiver}. It will be called once an * Adds an object processor to this {@link Receiver}. It will be called once an object of the
* object of the accepted class has been received. * accepted class has been received.
* *
* @param processorClass the object class accepted by the processor * @param processorClass the object class accepted by the processor
* @param processor the object processor * @param processor the object processor
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { processors.put(processorClass, processor); } public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) {
processors.put(processorClass, processor);
}
/** /**
* Adds a map of object processors to this {@link Receiver}. * Adds a map of object processors to this {@link Receiver}.
@ -115,12 +124,16 @@ public final class Receiver extends Thread {
* @param processors the processors to add the processors to add * @param processors the processors to add the processors to add
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { this.processors.putAll(processors); } public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) {
this.processors.putAll(processors);
}
/** /**
* Removes all object processors registered at this {@link Receiver}. * Removes all object processors registered at this {@link Receiver}.
* *
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void removeAllProcessors() { processors.clear(); } public void removeAllProcessors() {
processors.clear();
}
} }

View File

@ -2,15 +2,15 @@ package envoy.client.net;
import java.util.logging.*; import java.util.logging.*;
import envoy.client.data.*;
import envoy.data.Message; import envoy.data.Message;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.*;
/** /**
* Implements methods to send {@link Message}s and * Implements methods to send {@link Message}s and {@link MessageStatusChange}s to the server or
* {@link MessageStatusChange}s to the server or cache them inside a * cache them inside a {@link LocalDB} depending on the online status.
* {@link LocalDB} depending on the online status.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
@ -23,12 +23,11 @@ public final class WriteProxy {
private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class); private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
/** /**
* Initializes a write proxy using a client and a local database. The * Initializes a write proxy using a client and a local database. The corresponding cache
* corresponding cache processors are injected into the caches. * processors are injected into the caches.
* *
* @param client the client instance used to send messages and events if online * @param client the client instance used to send messages and events if online
* @param localDB the local database used to cache messages and events if * @param localDB the local database used to cache messages and events if offline
* offline
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public WriteProxy(Client client, LocalDB localDB) { public WriteProxy(Client client, LocalDB localDB) {
@ -47,34 +46,39 @@ public final class WriteProxy {
} }
/** /**
* Sends cached {@link Message}s and {@link MessageStatusChange}s to the * Sends cached {@link Message}s and {@link MessageStatusChange}s to the server.
* server.
* *
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void flushCache() { localDB.getCacheMap().getMap().values().forEach(Cache::relay); } public void flushCache() {
localDB.getCacheMap().getMap().values().forEach(Cache::relay);
}
/** /**
* Delivers a message to the server if online. Otherwise the message is cached * Delivers a message to the server if online. Otherwise the message is cached inside the local
* inside the local database. * database.
* *
* @param message the message to send * @param message the message to send
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void writeMessage(Message message) { public void writeMessage(Message message) {
if (client.isOnline()) client.sendMessage(message); if (client.isOnline())
else localDB.getCacheMap().getApplicable(Message.class).accept(message); client.sendMessage(message);
else
localDB.getCacheMap().getApplicable(Message.class).accept(message);
} }
/** /**
* Delivers a message status change event to the server if online. Otherwise the * Delivers a message status change event to the server if online. Otherwise the event is cached
* event is cached inside the local database. * inside the local database.
* *
* @param evt the event to send * @param evt the event to send
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public void writeMessageStatusChange(MessageStatusChange evt) { public void writeMessageStatusChange(MessageStatusChange evt) {
if (client.isOnline()) client.send(evt); if (client.isOnline())
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt); client.send(evt);
else
localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
} }
} }

View File

@ -1,8 +1,8 @@
package envoy.client.ui; package envoy.client.ui;
/** /**
* This interface defines an action that should be performed when a scene gets * This interface defines an action that should be performed when a scene gets restored from the
* restored from the scene stack in {@link SceneContext}. * scene stack in {@link SceneContext}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -12,8 +12,7 @@ public interface Restorable {
/** /**
* This method is getting called when a scene gets restored.<br> * This method is getting called when a scene gets restored.<br>
* Hence, it can contain anything that should be done when the underlying scene * Hence, it can contain anything that should be done when the underlying scene gets restored.
* gets restored.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */

View File

@ -9,20 +9,19 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.*; import javafx.scene.*;
import javafx.stage.Stage; import javafx.stage.Stage;
import dev.kske.eventbus.*;
import envoy.util.EnvoyLog;
import envoy.client.data.Settings; import envoy.client.data.Settings;
import envoy.client.data.shortcuts.*; import envoy.client.data.shortcuts.*;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
/** /**
* Manages a stack of scenes. The most recently added scene is displayed inside * Manages a stack of scenes. The most recently added scene is displayed inside a stage. When a
* a stage. When a scene is removed from the stack, its predecessor is * scene is removed from the stack, its predecessor is displayed.
* displayed.
* <p> * <p>
* When a scene is loaded, the style sheet for the current theme is applied to * When a scene is loaded, the style sheet for the current theme is applied to it.
* it.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -63,7 +62,9 @@ public final class SceneContext implements EventListener {
*/ */
public final String path; public final String path;
SceneInfo(String path) { this.path = path; } SceneInfo(String path) {
this.path = path;
}
} }
private final Stage stage; private final Stage stage;
@ -97,7 +98,8 @@ public final class SceneContext implements EventListener {
loader.setController(null); loader.setController(null);
try { try {
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path)); final var rootNode =
(Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode); final var scene = new Scene(rootNode);
final var controller = loader.getController(); final var controller = loader.getController();
controllerStack.push(controller); controllerStack.push(controller);
@ -106,10 +108,13 @@ public final class SceneContext implements EventListener {
stage.setScene(scene); stage.setScene(scene);
// Supply the global custom keyboard shortcuts for that scene // Supply the global custom keyboard shortcuts for that scene
scene.getAccelerators().putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo)); scene.getAccelerators()
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo));
// Supply the scene specific keyboard shortcuts // Supply the scene specific keyboard shortcuts
if (controller instanceof KeyboardMapping) scene.getAccelerators().putAll(((KeyboardMapping) controller).getKeyboardShortcuts()); if (controller instanceof KeyboardMapping)
scene.getAccelerators()
.putAll(((KeyboardMapping) controller).getKeyboardShortcuts());
// The LoginScene is the only scene not intended to be resized // The LoginScene is the only scene not intended to be resized
// As strange as it seems, this is needed as otherwise the LoginScene won't be // As strange as it seems, this is needed as otherwise the LoginScene won't be
@ -119,7 +124,8 @@ public final class SceneContext implements EventListener {
applyCSS(); applyCSS();
stage.show(); stage.show();
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e); EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE,
String.format("Could not load scene for %s: ", sceneInfo), e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@ -144,7 +150,8 @@ public final class SceneContext implements EventListener {
// If the controller implements the Restorable interface, // If the controller implements the Restorable interface,
// the actions to perform on restoration will be executed here // the actions to perform on restoration will be executed here
final var controller = controllerStack.peek(); final var controller = controllerStack.peek();
if (controller instanceof Restorable) ((Restorable) controller).onRestore(); if (controller instanceof Restorable)
((Restorable) controller).onRestore();
} }
stage.show(); stage.show();
} }
@ -154,7 +161,8 @@ public final class SceneContext implements EventListener {
final var styleSheets = stage.getScene().getStylesheets(); final var styleSheets = stage.getScene().getStylesheets();
final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css"; final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css";
styleSheets.clear(); styleSheets.clear();
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm()); styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(),
getClass().getResource(themeCSS).toExternalForm());
} }
} }
@ -165,7 +173,9 @@ public final class SceneContext implements EventListener {
} }
@Event(priority = 150, eventType = ThemeChangeEvent.class) @Event(priority = 150, eventType = ThemeChangeEvent.class)
private void onThemeChange() { applyCSS(); } private void onThemeChange() {
applyCSS();
}
/** /**
* @param <T> the type of the controller * @param <T> the type of the controller

View File

@ -10,6 +10,12 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.*;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.shortcuts.EnvoyShortcutConfig; import envoy.client.data.shortcuts.EnvoyShortcutConfig;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
@ -17,11 +23,6 @@ import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene; import envoy.client.ui.controller.LoginScene;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.*;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/** /**
* Handles application startup. * Handles application startup.
@ -47,8 +48,8 @@ public final class Startup extends Application {
private static final Logger logger = EnvoyLog.getLogger(Startup.class); private static final Logger logger = EnvoyLog.getLogger(Startup.class);
/** /**
* Loads the configuration, initializes the client and the local database and * Loads the configuration, initializes the client and the local database and delegates the rest
* delegates the rest of the startup process to {@link LoginScene}. * of the startup process to {@link LoginScene}.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -57,7 +58,8 @@ public final class Startup extends Application {
// Initialize config and logger // Initialize config and logger
try { try {
config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0])); config.loadAll(Startup.class, "client.properties",
getParameters().getRaw().toArray(new String[0]));
EnvoyLog.initialize(config); EnvoyLog.initialize(config);
} catch (final IllegalStateException e) { } catch (final IllegalStateException e) {
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e); new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
@ -97,7 +99,8 @@ public final class Startup extends Application {
logger.info("Attempting authentication with token..."); logger.info("Attempting authentication with token...");
localDB.loadUserData(); localDB.loadUserData();
if (!performHandshake( if (!performHandshake(
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()))) LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(),
VERSION, localDB.getLastSync())))
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
} else } else
// Load login scene // Load login scene
@ -117,7 +120,8 @@ public final class Startup extends Application {
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>()); cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>()); cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>()); cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
final var originalStatus = localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus(); final var originalStatus =
localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus();
try { try {
client.performHandshake(credentials, cacheMap); client.performHandshake(credentials, cacheMap);
if (client.isOnline()) { if (client.isOnline()) {
@ -127,7 +131,8 @@ public final class Startup extends Application {
loadChatScene(); loadChatScene();
client.initReceiver(localDB, cacheMap); client.initReceiver(localDB, cacheMap);
return true; return true;
} else return false; } else
return false;
} catch (IOException | InterruptedException | TimeoutException e) { } catch (IOException | InterruptedException | TimeoutException e) {
logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
return attemptOfflineMode(credentials.getIdentifier()); return attemptOfflineMode(credentials.getIdentifier());
@ -135,8 +140,8 @@ public final class Startup extends Application {
} }
/** /**
* Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode * Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode for a given
* for a given user. * user.
* *
* @param identifier the identifier of the user - currently his username * @param identifier the identifier of the user - currently his username
* @return whether the offline mode could be entered * @return whether the offline mode could be entered
@ -146,7 +151,8 @@ public final class Startup extends Application {
try { try {
// Try entering offline mode // Try entering offline mode
final User clientUser = localDB.getUsers().get(identifier); final User clientUser = localDB.getUsers().get(identifier);
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown"); if (clientUser == null)
throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser); client.setSender(clientUser);
loadChatScene(); loadChatScene();
return true; return true;
@ -187,7 +193,9 @@ public final class Startup extends Application {
} catch (final FileNotFoundException e) { } catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login // The local database file has not yet been created, probably first login
} catch (final Exception e) { } catch (final Exception e) {
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait(); new Alert(AlertType.ERROR,
"Error while loading local database: " + e + "\nChats will not be stored locally.")
.showAndWait();
logger.log(Level.WARNING, "Could not load local database: ", e); logger.log(Level.WARNING, "Could not load local database: ", e);
} }
@ -197,7 +205,8 @@ public final class Startup extends Application {
context.getWriteProxy().flushCache(); context.getWriteProxy().flushCache();
// Inform the server that this user has a different user status than expected // Inform the server that this user has a different user status than expected
if (!user.getStatus().equals(UserStatus.ONLINE)) client.send(new UserStatusChange(user)); if (!user.getStatus().equals(UserStatus.ONLINE))
client.send(new UserStatusChange(user));
} else } else
// Set all contacts to offline mode // Set all contacts to offline mode
@ -211,7 +220,8 @@ public final class Startup extends Application {
final var stage = context.getStage(); final var stage = context.getStage();
// Pop LoginScene if present // Pop LoginScene if present
if (!context.getSceneContext().isEmpty()) context.getSceneContext().pop(); if (!context.getSceneContext().isEmpty())
context.getSceneContext().pop();
// Load ChatScene // Load ChatScene
stage.setMinHeight(400); stage.setMinHeight(400);
@ -221,15 +231,21 @@ public final class Startup extends Application {
// Exit or minimize the stage when a close request occurs // Exit or minimize the stage when a close request occurs
stage.setOnCloseRequest( stage.setOnCloseRequest(
e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) e.consume(); }); e -> {
ShutdownHelper.exit();
if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported())
e.consume();
});
if (StatusTrayIcon.isSupported()) { if (StatusTrayIcon.isSupported()) {
// Initialize status tray icon // Initialize status tray icon
final var trayIcon = new StatusTrayIcon(stage); final var trayIcon = new StatusTrayIcon(stage);
Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> { Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> {
if ((Boolean) c) trayIcon.show(); if ((Boolean) c)
else trayIcon.hide(); trayIcon.show();
else
trayIcon.hide();
}); });
} }

View File

@ -6,14 +6,15 @@ import java.awt.TrayIcon.MessageType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.event.OwnStatusChange; import dev.kske.eventbus.*;
import envoy.client.helper.ShutdownHelper; import dev.kske.eventbus.Event;
import envoy.client.util.*;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import dev.kske.eventbus.*; import envoy.client.event.OwnStatusChange;
import dev.kske.eventbus.Event; import envoy.client.helper.ShutdownHelper;
import envoy.client.util.*;
/** /**
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -22,15 +23,14 @@ import dev.kske.eventbus.Event;
public final class StatusTrayIcon implements EventListener { public final class StatusTrayIcon implements EventListener {
/** /**
* The {@link TrayIcon} provided by the System Tray API for controlling the * The {@link TrayIcon} provided by the System Tray API for controlling the system tray. This
* system tray. This includes displaying the icon, but also creating * includes displaying the icon, but also creating notifications when new messages are received.
* notifications when new messages are received.
*/ */
private final TrayIcon trayIcon; private final TrayIcon trayIcon;
/** /**
* A received {@link Message} is only displayed as a system tray notification if * A received {@link Message} is only displayed as a system tray notification if this variable
* this variable is set to {@code true}. * is set to {@code true}.
*/ */
private boolean displayMessages; private boolean displayMessages;
@ -41,11 +41,9 @@ public final class StatusTrayIcon implements EventListener {
public static boolean isSupported() { return SystemTray.isSupported(); } public static boolean isSupported() { return SystemTray.isSupported(); }
/** /**
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up * Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up menu.
* menu.
* *
* @param stage the stage whose focus determines if message * @param stage the stage whose focus determines if message notifications are displayed
* notifications are displayed
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public StatusTrayIcon(Stage stage) { public StatusTrayIcon(Stage stage) {
@ -62,14 +60,18 @@ public final class StatusTrayIcon implements EventListener {
// Adding the logout menu item // Adding the logout menu item
final var logoutMenuItem = new MenuItem("Logout"); final var logoutMenuItem = new MenuItem("Logout");
logoutMenuItem.addActionListener(evt -> { hide(); Platform.runLater(UserUtil::logout); }); logoutMenuItem.addActionListener(evt -> {
hide();
Platform.runLater(UserUtil::logout);
});
popup.add(logoutMenuItem); popup.add(logoutMenuItem);
// Adding the status change items // Adding the status change items
final var statusSubMenu = new Menu("Change status"); final var statusSubMenu = new Menu("Change status");
for (final var status : UserStatus.values()) { for (final var status : UserStatus.values()) {
final var statusMenuItem = new MenuItem(status.toString().toLowerCase()); final var statusMenuItem = new MenuItem(status.toString().toLowerCase());
statusMenuItem.addActionListener(evt -> Platform.runLater(() -> UserUtil.changeStatus(status))); statusMenuItem
.addActionListener(evt -> Platform.runLater(() -> UserUtil.changeStatus(status)));
statusSubMenu.add(statusMenuItem); statusSubMenu.add(statusMenuItem);
} }
popup.add(statusSubMenu); popup.add(statusSubMenu);
@ -78,10 +80,15 @@ public final class StatusTrayIcon implements EventListener {
// Only display messages if the stage is not focused and the current user status // Only display messages if the stage is not focused and the current user status
// is not BUSY (if BUSY, displayMessages will be false) // is not BUSY (if BUSY, displayMessages will be false)
stage.focusedProperty().addListener((ov, wasFocused, isFocused) -> displayMessages = !displayMessages && wasFocused ? false : !isFocused); stage.focusedProperty().addListener((ov, wasFocused, isFocused) -> displayMessages =
!displayMessages && wasFocused ? false : !isFocused);
// Show the window if the user clicks on the icon // Show the window if the user clicks on the icon
trayIcon.addActionListener(evt -> Platform.runLater(() -> { stage.setIconified(false); stage.toFront(); stage.requestFocus(); })); trayIcon.addActionListener(evt -> Platform.runLater(() -> {
stage.setIconified(false);
stage.toFront();
stage.requestFocus();
}));
// Start processing message events // Start processing message events
EventBus.getInstance().registerListener(this); EventBus.getInstance().registerListener(this);
@ -103,15 +110,22 @@ public final class StatusTrayIcon implements EventListener {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void hide() { SystemTray.getSystemTray().remove(trayIcon); } public void hide() {
SystemTray.getSystemTray().remove(trayIcon);
}
@Event @Event
private void onOwnStatusChange(OwnStatusChange statusChange) { displayMessages = !statusChange.get().equals(UserStatus.BUSY); } private void onOwnStatusChange(OwnStatusChange statusChange) {
displayMessages = !statusChange.get().equals(UserStatus.BUSY);
}
@Event @Event
private void onMessage(Message message) { private void onMessage(Message message) {
if (displayMessages) trayIcon if (displayMessages)
.displayMessage(message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" trayIcon
.displayMessage(message.hasAttachment()
? "New " + message.getAttachment().getType().toString().toLowerCase()
+ " message received"
: "New message received", message.getText(), MessageType.INFO); : "New message received", message.getText(), MessageType.INFO);
} }
} }

View File

@ -8,19 +8,19 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.skin.VirtualFlow; import javafx.scene.control.skin.VirtualFlow;
import envoy.data.Message;
import envoy.data.User.UserStatus;
import envoy.util.EnvoyLog;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.data.commands.*; import envoy.client.data.commands.*;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.ChatScene; import envoy.client.ui.controller.ChatScene;
import envoy.client.util.*; import envoy.client.util.*;
import envoy.data.Message;
import envoy.data.User.UserStatus;
import envoy.util.EnvoyLog;
/** /**
* Contains all {@link SystemCommand}s used for * Contains all {@link SystemCommand}s used for {@link envoy.client.ui.controller.ChatScene}.
* {@link envoy.client.ui.controller.ChatScene}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
@ -29,12 +29,13 @@ public final class ChatSceneCommands {
private final ListView<Message> messageList; private final ListView<Message> messageList;
private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap(); private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
private final SystemCommandBuilder builder = new SystemCommandBuilder(messageTextAreaCommands); private final SystemCommandBuilder builder =
new SystemCommandBuilder(messageTextAreaCommands);
private static final String messageDependantCommandDescription = " the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message."; private static final String messageDependantCommandDescription =
" the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message.";
/** /**
*
* @param messageList the message list to use for some commands * @param messageList the message list to use for some commands
* @param chatScene the instance of {@code ChatScene} that uses this object * @param chatScene the instance of {@code ChatScene} that uses this object
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
@ -43,25 +44,33 @@ public final class ChatSceneCommands {
this.messageList = messageList; this.messageList = messageList;
// Error message initialization // Error message initialization
builder.setAction(text -> { throw new RuntimeException(); }).setDescription("Shows an error message.").buildNoArg("error"); builder.setAction(text -> { throw new RuntimeException(); })
.setDescription("Shows an error message.").buildNoArg("error");
// Do A Barrel roll initialization // Do A Barrel roll initialization
final var random = new Random(); final var random = new Random();
builder.setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1)))) builder
.setDefaults(Integer.toString(random.nextInt(3) + 1), Double.toString(random.nextDouble() * 3 + 1)) .setAction(text -> chatScene.doABarrelRoll(Integer.parseInt(text.get(0)),
Double.parseDouble(text.get(1))))
.setDefaults(Integer.toString(random.nextInt(3) + 1),
Double.toString(random.nextDouble() * 3 + 1))
.setDescription("See for yourself :)") .setDescription("See for yourself :)")
.setNumberOfArguments(2) .setNumberOfArguments(2)
.build("dabr"); .build("dabr");
// Logout initialization // Logout initialization
builder.setAction(text -> UserUtil.logout()).setDescription("Logs you out.").buildNoArg("logout"); builder.setAction(text -> UserUtil.logout()).setDescription("Logs you out.")
.buildNoArg("logout");
// Exit initialization // Exit initialization
builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0).setDescription("Exits the program.").build("exit", false); builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0)
.setDescription("Exits the program.").build("exit", false);
builder.build("q"); builder.build("q");
// Open settings scene initialization // Open settings scene initialization
builder.setAction(text -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE)) builder
.setAction(
text -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE))
.setDescription("Opens the settings screen") .setDescription("Opens the settings screen")
.buildNoArg("settings"); .buildNoArg("settings");
@ -74,81 +83,106 @@ public final class ChatSceneCommands {
alert.setContentText("Please provide an existing status"); alert.setContentText("Please provide an existing status");
alert.showAndWait(); alert.showAndWait();
} }
}).setDescription("Changes your status to the given status.").setNumberOfArguments(1).setDefaults("").build("status"); }).setDescription("Changes your status to the given status.").setNumberOfArguments(1)
.setDefaults("").build("status");
// Selection of a new message initialization // Selection of a new message initialization
messageDependantAction("s", messageDependantAction("s",
m -> { messageList.getSelectionModel().clearSelection(); messageList.getSelectionModel().select(m); }, m -> {
m -> true, messageList.getSelectionModel().clearSelection();
"Selects"); messageList.getSelectionModel().select(m);
},
m -> true,
"Selects");
// Copy text of selection initialization // Copy text of selection initialization
messageDependantAction("cp", MessageUtil::copyMessageText, m -> !m.getText().isEmpty(), "Copies the text of"); messageDependantAction("cp", MessageUtil::copyMessageText, m -> !m.getText().isEmpty(),
"Copies the text of");
// Delete selection initialization // Delete selection initialization
messageDependantAction("del", MessageUtil::deleteMessage, m -> true, "Deletes"); messageDependantAction("del", MessageUtil::deleteMessage, m -> true, "Deletes");
// Save attachment of selection initialization // Save attachment of selection initialization
messageDependantAction("save-att", MessageUtil::saveAttachment, Message::hasAttachment, "Saves the attachment of"); messageDependantAction("save-att", MessageUtil::saveAttachment, Message::hasAttachment,
"Saves the attachment of");
} }
private void messageDependantAction(String command, Consumer<Message> action, Predicate<Message> additionalCheck, String description) { private void messageDependantAction(String command, Consumer<Message> action,
Predicate<Message> additionalCheck, String description) {
builder.setAction(text -> { builder.setAction(text -> {
final var positionalArgument = text.get(0).toLowerCase(); final var positionalArgument = text.get(0).toLowerCase();
// the currently selected message was requested // the currently selected message was requested
if (positionalArgument.startsWith("s")) { if (positionalArgument.startsWith("s")) {
final var relativeString = positionalArgument.length() == 1 ? "" : positionalArgument.substring(1); final var relativeString =
positionalArgument.length() == 1 ? "" : positionalArgument.substring(1);
// Only s has been used as input // Only s has been used as input
if (positionalArgument.length() == 1) { if (positionalArgument.length() == 1) {
final var selectedMessage = messageList.getSelectionModel().getSelectedItem(); final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
if (selectedMessage != null && additionalCheck.test(selectedMessage)) action.accept(selectedMessage); if (selectedMessage != null && additionalCheck.test(selectedMessage))
action.accept(selectedMessage);
return; return;
// Either s++ or s-- has been requested // Either s++ or s-- has been requested
} else if (relativeString.equals("++") || relativeString.equals("--")) selectionNeighbor(action, additionalCheck, positionalArgument); } else if (relativeString.equals("++") || relativeString.equals("--"))
selectionNeighbor(action, additionalCheck, positionalArgument);
// A message relative to the currently selected message should be used (i.e. // A message relative to the currently selected message should be used (i.e.
// s+4) // s+4)
else useRelativeMessage(command, action, additionalCheck, relativeString, true); else
useRelativeMessage(command, action, additionalCheck, relativeString, true);
// Either ++s or --s has been requested // Either ++s or --s has been requested
} else if (positionalArgument.equals("--s") || positionalArgument.equals("++s")) } else if (positionalArgument.equals("--s") || positionalArgument.equals("++s"))
selectionNeighbor(action, additionalCheck, positionalArgument); selectionNeighbor(action, additionalCheck, positionalArgument);
// Just a number is expected: ((+)4) // Just a number is expected: ((+)4)
else useRelativeMessage(command, action, additionalCheck, positionalArgument, false); else
}).setDefaults("s").setNumberOfArguments(1).setDescription(description.concat(messageDependantCommandDescription)).build(command); useRelativeMessage(command, action, additionalCheck, positionalArgument, false);
}).setDefaults("s").setNumberOfArguments(1)
.setDescription(description.concat(messageDependantCommandDescription)).build(command);
} }
private void selectionNeighbor(Consumer<Message> action, Predicate<Message> additionalCheck, final String positionalArgument) { private void selectionNeighbor(Consumer<Message> action, Predicate<Message> additionalCheck,
final var wantedIndex = messageList.getSelectionModel().getSelectedIndex() + (positionalArgument.contains("+") ? 1 : -1); final String positionalArgument) {
final var wantedIndex = messageList.getSelectionModel().getSelectedIndex()
+ (positionalArgument.contains("+") ? 1 : -1);
messageList.getSelectionModel().clearAndSelect(wantedIndex); messageList.getSelectionModel().clearAndSelect(wantedIndex);
final var selectedMessage = messageList.getItems().get(wantedIndex); final var selectedMessage = messageList.getItems().get(wantedIndex);
if (selectedMessage != null && additionalCheck.test(selectedMessage)) action.accept(selectedMessage); if (selectedMessage != null && additionalCheck.test(selectedMessage))
action.accept(selectedMessage);
} }
private void useRelativeMessage(String command, Consumer<Message> action, Predicate<Message> additionalCheck, final String positionalArgument, private void useRelativeMessage(String command, Consumer<Message> action,
boolean useSelectedMessage) throws NumberFormatException { Predicate<Message> additionalCheck, final String positionalArgument,
final var stripPlus = positionalArgument.startsWith("+") ? positionalArgument.substring(1) : positionalArgument; boolean useSelectedMessage) throws NumberFormatException {
final var stripPlus =
positionalArgument.startsWith("+") ? positionalArgument.substring(1)
: positionalArgument;
final var incDec = Integer.valueOf(stripPlus); final var incDec = Integer.valueOf(stripPlus);
try { try {
// The currently selected message is the base message // The currently selected message is the base message
if (useSelectedMessage) { if (useSelectedMessage) {
final var messageToUse = messageList.getItems().get(messageList.getSelectionModel().getSelectedIndex() + incDec); final var messageToUse = messageList.getItems()
if (messageToUse != null && additionalCheck.test(messageToUse)) action.accept(messageToUse); .get(messageList.getSelectionModel().getSelectedIndex() + incDec);
if (messageToUse != null && additionalCheck.test(messageToUse))
action.accept(messageToUse);
// The currently upmost completely visible message is the base message // The currently upmost completely visible message is the base message
} else { } else {
final var messageToUse = messageList.getItems() final var messageToUse = messageList.getItems()
.get(((VirtualFlow<?>) messageList.lookup(".virtual-flow")).getFirstVisibleCell().getIndex() + 1 + incDec); .get(((VirtualFlow<?>) messageList.lookup(".virtual-flow"))
if (messageToUse != null && additionalCheck.test(messageToUse)) action.accept(messageToUse); .getFirstVisibleCell().getIndex() + 1 + incDec);
if (messageToUse != null && additionalCheck.test(messageToUse))
action.accept(messageToUse);
} }
} catch (final IndexOutOfBoundsException e) { } catch (final IndexOutOfBoundsException e) {
EnvoyLog.getLogger(ChatSceneCommands.class) EnvoyLog.getLogger(ChatSceneCommands.class)
.log(Level.INFO, " A non-existing message was requested by the user for System command " + command); .log(Level.INFO,
" A non-existing message was requested by the user for System command "
+ command);
} }
} }

View File

@ -7,8 +7,8 @@ import javafx.scene.control.*;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
/** /**
* Displays a context menu that offers an additional option when one of * Displays a context menu that offers an additional option when one of its menu items has been
* its menu items has been clicked. * clicked.
* <p> * <p>
* Current options are: * Current options are:
* <ul> * <ul>
@ -24,9 +24,8 @@ import javafx.scene.input.Clipboard;
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote please refrain from using * @apiNote please refrain from using {@link ContextMenu#setOnShowing(EventHandler)} as this is
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already * already used by this component
* used by this component
*/ */
public class TextInputContextMenu extends ContextMenu { public class TextInputContextMenu extends ContextMenu {
@ -40,8 +39,8 @@ public class TextInputContextMenu extends ContextMenu {
private final MenuItem selectAllMI = new MenuItem("Select all"); private final MenuItem selectAllMI = new MenuItem("Select all");
/** /**
* Creates a new {@code TextInputContextMenu} with an optional action when * Creates a new {@code TextInputContextMenu} with an optional action when this menu was
* this menu was clicked. Currently shows: * clicked. Currently shows:
* <ul> * <ul>
* <li>undo</li> * <li>undo</li>
* <li>redo</li> * <li>redo</li>
@ -53,14 +52,12 @@ public class TextInputContextMenu extends ContextMenu {
* <li>Select all</li> * <li>Select all</li>
* </ul> * </ul>
* *
* @param control the text input component to display this * @param control the text input component to display this {@code ContextMenu}
* {@code ContextMenu} * @param menuItemClicked the second action to perform when a menu item of this context menu has
* @param menuItemClicked the second action to perform when a menu item of this * been clicked
* context menu has been clicked
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
* @apiNote please refrain from using * @apiNote please refrain from using {@link ContextMenu#setOnShowing(EventHandler)} as this is
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already * already used by this component
* used by this component
*/ */
public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) { public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) {
@ -100,7 +97,11 @@ public class TextInputContextMenu extends ContextMenu {
getItems().add(selectAllMI); getItems().add(selectAllMI);
} }
private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction, Consumer<ActionEvent> additionalAction) { private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction,
return e -> { originalAction.accept(e); additionalAction.accept(e); }; Consumer<ActionEvent> additionalAction) {
return e -> {
originalAction.accept(e);
additionalAction.accept(e);
};
} }
} }

View File

@ -1,6 +1,6 @@
/** /**
* Contains classes that influence the appearance and behavior of ChatScene. * Contains classes that influence the appearance and behavior of ChatScene.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */

View File

@ -6,10 +6,11 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import envoy.client.data.audio.AudioPlayer;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.audio.AudioPlayer;
/** /**
* Enables the play back of audio clips through a button. * Enables the play back of audio clips through a button.
* *
@ -18,7 +19,7 @@ import envoy.util.EnvoyLog;
*/ */
public final class AudioControl extends HBox { public final class AudioControl extends HBox {
private AudioPlayer player = new AudioPlayer(); private final AudioPlayer player = new AudioPlayer();
private static final Logger logger = EnvoyLog.getLogger(AudioControl.class); private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);

View File

@ -9,8 +9,8 @@ import envoy.client.data.*;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
/** /**
* Displays a chat using a contact control for the recipient and a label for the * Displays a chat using a contact control for the recipient and a label for the unread message
* unread message count. * count.
* *
* @see ContactControl * @see ContactControl
* @author Leon Hofmeister * @author Leon Hofmeister
@ -19,7 +19,7 @@ import envoy.client.util.IconUtil;
public final class ChatControl extends HBox { public final class ChatControl extends HBox {
private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32), private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32),
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32); groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
/** /**
* Creates a new {@code ChatControl}. * Creates a new {@code ChatControl}.
@ -32,7 +32,8 @@ public final class ChatControl extends HBox {
setPadding(new Insets(0, 0, 3, 0)); setPadding(new Insets(0, 0, 3, 0));
// Profile picture // Profile picture
final var contactProfilePic = new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32); final var contactProfilePic =
new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32);
getChildren().add(contactProfilePic); getChildren().add(contactProfilePic);
// Spacing // Spacing

View File

@ -6,9 +6,8 @@ import javafx.scene.layout.VBox;
import envoy.data.*; import envoy.data.*;
/** /**
* Displays information about a contact in two rows. The first row contains the * Displays information about a contact in two rows. The first row contains the name. The second row
* name. The second row contains the online status (user) or the member count * contains the online status (user) or the member count (group).
* (group).
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -29,23 +28,24 @@ public final class ContactControl extends VBox {
getChildren().add(nameLabel); getChildren().add(nameLabel);
// Online status (user) or member count (group) // Online status (user) or member count (group)
getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact) : new GroupSizeLabel((Group) contact)); getChildren().add(contact instanceof User ? new UserStatusLabel((User) contact)
: new GroupSizeLabel((Group) contact));
getStyleClass().add("list-element"); getStyleClass().add("list-element");
} }
/** /**
* Replaces the info label of this {@code ContactControl} with an updated * Replaces the info label of this {@code ContactControl} with an updated version.
* version.
* <p> * <p>
* This method should be called when the status of the underlying user or the * This method should be called when the status of the underlying user or the size of the
* size of the underlying group has changed. * underlying group has changed.
* *
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
* @apiNote will produce buggy results if contact control gets updated so that * @apiNote will produce buggy results if contact control gets updated so that the info label is
* the info label is no longer on index 1. * no longer on index 1.
*/ */
public void replaceInfoLabel() { public void replaceInfoLabel() {
getChildren().set(1, contact instanceof User ? new UserStatusLabel((User) contact) : new GroupSizeLabel((Group) contact)); getChildren().set(1, contact instanceof User ? new UserStatusLabel((User) contact)
: new GroupSizeLabel((Group) contact));
} }
} }

View File

@ -17,6 +17,7 @@ public final class GroupSizeLabel extends Label {
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public GroupSizeLabel(Group recipient) { public GroupSizeLabel(Group recipient) {
super(recipient.getContacts().size() + " member" + (recipient.getContacts().size() != 1 ? "s" : "")); super(recipient.getContacts().size() + " member"
+ (recipient.getContacts().size() != 1 ? "s" : ""));
} }
} }

View File

@ -11,13 +11,14 @@ import javafx.scene.control.*;
import javafx.scene.image.*; import javafx.scene.image.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.util.*;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.util.*;
/** /**
* This class transforms a single {@link Message} into a UI component. * This class transforms a single {@link Message} into a UI component.
* *
@ -33,13 +34,15 @@ public final class MessageControl extends Label {
private final Client client = context.getClient(); private final Client client = context.getClient();
private static final Context context = Context.getInstance(); private static final Context context = Context.getInstance();
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") private static final DateTimeFormatter dateFormat =
.withZone(ZoneId.systemDefault()); DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16); .withZone(ZoneId.systemDefault());
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class); private static final Map<MessageStatus, Image> statusImages =
IconUtil.loadByEnum(MessageStatus.class, 16);
private static final Logger logger =
EnvoyLog.getLogger(MessageControl.class);
/** /**
*
* @param message the message that should be formatted * @param message the message that should be formatted
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -107,7 +110,9 @@ public final class MessageControl extends Label {
switch (message.getAttachment().getType()) { switch (message.getAttachment().getType()) {
case PICTURE: case PICTURE:
vbox.getChildren() vbox.getChildren()
.add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true))); .add(new ImageView(
new Image(new ByteArrayInputStream(message.getAttachment().getData()),
256, 256, true, true)));
break; break;
case VIDEO: case VIDEO:
break; break;
@ -138,7 +143,8 @@ public final class MessageControl extends Label {
hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT); hBoxBottom.setAlignment(Pos.BOTTOM_RIGHT);
getStyleClass().add("own-message"); getStyleClass().add("own-message");
hbox.setAlignment(Pos.CENTER_RIGHT); hbox.setAlignment(Pos.CENTER_RIGHT);
} else getStyleClass().add("received-message"); } else
getStyleClass().add("received-message");
vbox.getChildren().add(hBoxBottom); vbox.getChildren().add(hBoxBottom);
// Adjusting height and weight of the cell to the corresponding ListView // Adjusting height and weight of the cell to the corresponding ListView
paddingProperty().setValue(new Insets(5, 20, 5, 20)); paddingProperty().setValue(new Insets(5, 20, 5, 20));
@ -146,11 +152,13 @@ public final class MessageControl extends Label {
setGraphic(vbox); setGraphic(vbox);
} }
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); } private void loadMessageInfoScene(Message message) {
logger.log(Level.FINEST, "message info scene was requested for " + message);
}
/** /**
* @return whether the message stored by this {@code MessageControl} has been * @return whether the message stored by this {@code MessageControl} has been sent by this user
* sent by this user of Envoy * of Envoy
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public boolean isOwnMessage() { return ownMessage; } public boolean isOwnMessage() { return ownMessage; }

View File

@ -16,7 +16,9 @@ public final class ProfilePicImageView extends ImageView {
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ProfilePicImageView() { this(null); } public ProfilePicImageView() {
this(null);
}
/** /**
* Creates a new {@code ProfilePicImageView}. * Creates a new {@code ProfilePicImageView}.
@ -24,17 +26,20 @@ public final class ProfilePicImageView extends ImageView {
* @param image the image to display * @param image the image to display
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ProfilePicImageView(Image image) { this(image, 40); } public ProfilePicImageView(Image image) {
this(image, 40);
}
/** /**
* Creates a new {@code ProfilePicImageView}. * Creates a new {@code ProfilePicImageView}.
* *
* @param image the image to display * @param image the image to display
* @param sizeAndRounding the size and rounding for a circular * @param sizeAndRounding the size and rounding for a circular {@code ProfilePicImageView}
* {@code ProfilePicImageView}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public ProfilePicImageView(Image image, double sizeAndRounding) { this(image, sizeAndRounding, sizeAndRounding); } public ProfilePicImageView(Image image, double sizeAndRounding) {
this(image, sizeAndRounding, sizeAndRounding);
}
/** /**
* Creates a new {@code ProfilePicImageView}. * Creates a new {@code ProfilePicImageView}.

View File

@ -8,25 +8,26 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import envoy.data.User;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
import envoy.data.*;
/** /**
* Displays an {@link User} as a quick select control which is used in the * Displays an {@link User} as a quick select control which is used in the quick select list.
* quick select list. *
*
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public class QuickSelectControl extends VBox { public class QuickSelectControl extends VBox {
private User user; private final User user;
/** /**
* Creates an instance of the {@code QuickSelectControl}. * Creates an instance of the {@code QuickSelectControl}.
* *
* @param user the contact whose data is used to create this instance. * @param user the contact whose data is used to create this instance.
* @param action the action to perform when a contact is removed with this control as a parameter * @param action the action to perform when a contact is removed with this control as a
* parameter
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public QuickSelectControl(User user, Consumer<QuickSelectControl> action) { public QuickSelectControl(User user, Consumer<QuickSelectControl> action) {
@ -44,7 +45,8 @@ public class QuickSelectControl extends VBox {
picHold.setPrefHeight(35); picHold.setPrefHeight(35);
picHold.setMaxHeight(35); picHold.setMaxHeight(35);
picHold.setMinHeight(35); picHold.setMinHeight(35);
var contactProfilePic = new ImageView(IconUtil.loadIconThemeSensitive("user_icon", 32)); var contactProfilePic =
new ImageView(IconUtil.loadIconThemeSensitive("user_icon", 32));
final var clip = new Rectangle(); final var clip = new Rectangle();
clip.setWidth(32); clip.setWidth(32);
clip.setHeight(32); clip.setHeight(32);

View File

@ -24,6 +24,17 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration; import javafx.util.Duration;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
import envoy.data.*;
import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus;
import envoy.event.*;
import envoy.event.contact.UserOperation;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
import envoy.client.event.*; import envoy.client.event.*;
@ -33,16 +44,6 @@ import envoy.client.ui.chatscene.*;
import envoy.client.ui.control.*; import envoy.client.ui.control.*;
import envoy.client.ui.listcell.*; import envoy.client.ui.listcell.*;
import envoy.client.util.*; import envoy.client.util.*;
import envoy.data.*;
import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus;
import envoy.event.*;
import envoy.event.contact.UserOperation;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
/** /**
* Controller for the chat scene. * Controller for the chat scene.
@ -139,9 +140,11 @@ public final class ChatScene implements EventListener, Restorable {
private final WriteProxy writeProxy = context.getWriteProxy(); private final WriteProxy writeProxy = context.getWriteProxy();
private final SceneContext sceneContext = context.getSceneContext(); private final SceneContext sceneContext = context.getSceneContext();
private final AudioRecorder recorder = new AudioRecorder(); private final AudioRecorder recorder = new AudioRecorder();
private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this"); private final Tooltip onlyIfOnlineTooltip =
new Tooltip("You need to be online to do this");
private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20); private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE =
IconUtil.loadIconThemeSensitive("attachment_present", 20);
private static final Settings settings = Settings.getInstance(); private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
@ -167,14 +170,19 @@ public final class ChatScene implements EventListener, Restorable {
// JavaFX provides an internal way of populating the context menu of a text // JavaFX provides an internal way of populating the context menu of a text
// area. // area.
// We, however, need additional functionality. // We, however, need additional functionality.
messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null))); messageTextArea.setContextMenu(
new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
// Set the icons of buttons and image views // Set the icons of buttons and image views
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); settingsButton.setGraphic(
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE); attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE))); messageSearchButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
onlyIfOnlineTooltip.setShowDelay(Duration.millis(250)); onlyIfOnlineTooltip.setShowDelay(Duration.millis(250));
final var clip = new Rectangle(); final var clip = new Rectangle();
@ -195,15 +203,19 @@ public final class ChatScene implements EventListener, Restorable {
// no check will be performed in case it has already been disabled - a negative // no check will be performed in case it has already been disabled - a negative
// GroupCreationResult might have been returned // GroupCreationResult might have been returned
if (!newGroupButton.isDisabled()) newGroupButton.setDisable(!online); if (!newGroupButton.isDisabled())
newGroupButton.setDisable(!online);
newContactButton.setDisable(!online); newContactButton.setDisable(!online);
if (online) try { if (online)
Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip); try {
contactSearchTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml"))); Tooltip.uninstall(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
groupCreationTab.setContent(new FXMLLoader().load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml"))); contactSearchTab.setContent(new FXMLLoader()
} catch (final IOException e) { .load(getClass().getResourceAsStream("/fxml/ContactSearchTab.fxml")));
logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e); groupCreationTab.setContent(new FXMLLoader()
} .load(getClass().getResourceAsStream("/fxml/GroupCreationTab.fxml")));
} catch (final IOException e) {
logger.log(Level.SEVERE, "An error occurred when attempting to load tabs: ", e);
}
else { else {
Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip); Tooltip.install(contactSpecificOnlineOperations, onlyIfOnlineTooltip);
updateInfoLabel("You are offline", "info-label-warning"); updateInfoLabel("You are offline", "info-label-warning");
@ -212,7 +224,9 @@ public final class ChatScene implements EventListener, Restorable {
} }
@Event(eventType = BackEvent.class) @Event(eventType = BackEvent.class)
private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); } private void onBackEvent() {
tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal());
}
@Event(includeSubtypes = true) @Event(includeSubtypes = true)
private void onMessage(Message message) { private void onMessage(Message message) {
@ -221,7 +235,9 @@ public final class ChatScene implements EventListener, Restorable {
// Exceptions: this user is the sender (sync) or group message (group is // Exceptions: this user is the sender (sync) or group message (group is
// recipient) // recipient)
final var ownMessage = message.getSenderID() == localDB.getUser().getID(); final var ownMessage = message.getSenderID() == localDB.getUser().getID();
final var recipientID = message instanceof GroupMessage || ownMessage ? message.getRecipientID() : message.getSenderID(); final var recipientID =
message instanceof GroupMessage || ownMessage ? message.getRecipientID()
: message.getSenderID();
localDB.getChat(recipientID).ifPresent(chat -> { localDB.getChat(recipientID).ifPresent(chat -> {
Platform.runLater(() -> { Platform.runLater(() -> {
@ -231,13 +247,15 @@ public final class ChatScene implements EventListener, Restorable {
if (chat.equals(currentChat)) { if (chat.equals(currentChat)) {
currentChat.read(writeProxy); currentChat.read(writeProxy);
scrollToMessageListEnd(); scrollToMessageListEnd();
} else if (!ownMessage && message.getStatus() != MessageStatus.READ) chat.incrementUnreadAmount(); } else if (!ownMessage && message.getStatus() != MessageStatus.READ)
chat.incrementUnreadAmount();
// Move chat with most recent unread messages to the top // Move chat with most recent unread messages to the top
chats.getSource().remove(chat); chats.getSource().remove(chat);
((ObservableList<Chat>) chats.getSource()).add(0, chat); ((ObservableList<Chat>) chats.getSource()).add(0, chat);
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0); if (chat.equals(currentChat))
chatList.getSelectionModel().select(0);
}); });
}); });
} }
@ -247,9 +265,10 @@ public final class ChatScene implements EventListener, Restorable {
// Update UI if in current chat and the current user was the sender of the // Update UI if in current chat and the current user was the sender of the
// message // message
if (currentChat != null) localDB.getMessage(evt.getID()) if (currentChat != null)
.filter(msg -> msg.getSenderID() == client.getSender().getID()) localDB.getMessage(evt.getID())
.ifPresent(msg -> Platform.runLater(messageList::refresh)); .filter(msg -> msg.getSenderID() == client.getSender().getID())
.ifPresent(msg -> Platform.runLater(messageList::refresh));
} }
@Event @Event
@ -270,7 +289,8 @@ public final class ChatScene implements EventListener, Restorable {
private void onUserOperation(UserOperation operation) { private void onUserOperation(UserOperation operation) {
// All ADD dependent logic resides in LocalDB // All ADD dependent logic resides in LocalDB
if (operation.getOperationType().equals(ElementOperation.REMOVE)) Platform.runLater(() -> disableChat(new ContactDisabled(operation.get()))); if (operation.getOperationType().equals(ElementOperation.REMOVE))
Platform.runLater(() -> disableChat(new ContactDisabled(operation.get())));
} }
@Event @Event
@ -280,8 +300,10 @@ public final class ChatScene implements EventListener, Restorable {
chatList.refresh(); chatList.refresh();
// Update the top-bar status label if all conditions apply // Update the top-bar status label if all conditions apply
if (currentChat != null && currentChat.getRecipient().equals(chat.getRecipient())) topBarStatusLabel if (currentChat != null && currentChat.getRecipient().equals(chat.getRecipient()))
.setText(chat.getRecipient().getContacts().size() + " member" + (currentChat.getRecipient().getContacts().size() != 1 ? "s" : "")); topBarStatusLabel
.setText(chat.getRecipient().getContacts().size() + " member"
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
})); }));
} }
@ -299,29 +321,42 @@ public final class ChatScene implements EventListener, Restorable {
} }
@Event(priority = 150) @Event(priority = 150)
private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(result.get() == null)); } private void onGroupCreationResult(GroupCreationResult result) {
Platform.runLater(() -> newGroupButton.setDisable(result.get() == null));
}
@Event(eventType = ThemeChangeEvent.class) @Event(eventType = ThemeChangeEvent.class)
private void onThemeChange() { private void onThemeChange() {
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); settingsButton.setGraphic(
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20); DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
attachmentView.setImage(isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE); attachmentView.setImage(
messageSearchButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE))); isCustomAttachmentImage ? attachmentView.getImage() : DEFAULT_ATTACHMENT_VIEW_IMAGE);
messageSearchButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("search", DEFAULT_ICON_SIZE)));
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
if (currentChat != null) if (currentChat != null)
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); if (currentChat.getRecipient() instanceof User)
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
else
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
@Event(eventType = Logout.class, priority = 200) @Event(eventType = Logout.class, priority = 200)
private void onLogout() { eventBus.removeListener(this); } private void onLogout() {
eventBus.removeListener(this);
}
@Override @Override
public void onRestore() { updateRemainingCharsLabel(); } public void onRestore() {
updateRemainingCharsLabel();
}
/** /**
* Actions to perform when the list of contacts has been clicked. * Actions to perform when the list of contacts has been clicked.
@ -330,9 +365,11 @@ public final class ChatScene implements EventListener, Restorable {
*/ */
@FXML @FXML
private void chatListClicked() { private void chatListClicked() {
if (chatList.getSelectionModel().isEmpty()) return; if (chatList.getSelectionModel().isEmpty())
return;
final var chat = chatList.getSelectionModel().getSelectedItem(); final var chat = chatList.getSelectionModel().getSelectedItem();
if (chat == null) return; if (chat == null)
return;
final var user = chat.getRecipient(); final var user = chat.getRecipient();
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) { if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
@ -353,7 +390,8 @@ public final class ChatScene implements EventListener, Restorable {
// Discard the pending attachment // Discard the pending attachment
if (recorder.isRecording()) { if (recorder.isRecording()) {
recorder.cancel(); recorder.cancel();
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(
IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null); voiceButton.setText(null);
} }
pendingAttachment = null; pendingAttachment = null;
@ -361,7 +399,8 @@ public final class ChatScene implements EventListener, Restorable {
remainingChars.setVisible(true); remainingChars.setVisible(true);
remainingChars remainingChars
.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH)); .setText(String.format("remaining chars: %d/%d",
MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
} }
// Enable or disable the necessary UI controls // Enable or disable the necessary UI controls
@ -383,7 +422,8 @@ public final class ChatScene implements EventListener, Restorable {
topBarStatusLabel.getStyleClass().add(status.toLowerCase()); topBarStatusLabel.getStyleClass().add(status.toLowerCase());
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
} else { } else {
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " member" topBarStatusLabel
.setText(currentChat.getRecipient().getContacts().size() + " member"
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : "")); + (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
topBarStatusLabel.getStyleClass().clear(); topBarStatusLabel.getStyleClass().clear();
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
@ -404,7 +444,9 @@ public final class ChatScene implements EventListener, Restorable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void settingsButtonClicked() { sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); } private void settingsButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
}
/** /**
* Actions to perform when the "Add Contact" - Button has been clicked. * Actions to perform when the "Add Contact" - Button has been clicked.
@ -412,10 +454,14 @@ public final class ChatScene implements EventListener, Restorable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void addContactButtonClicked() { tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal()); } private void addContactButtonClicked() {
tabPane.getSelectionModel().select(Tabs.CONTACT_SEARCH.ordinal());
}
@FXML @FXML
private void groupCreationButtonClicked() { tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal()); } private void groupCreationButtonClicked() {
tabPane.getSelectionModel().select(Tabs.GROUP_CREATION.ordinal());
}
@FXML @FXML
private void voiceButtonClicked() { private void voiceButtonClicked() {
@ -424,15 +470,19 @@ public final class ChatScene implements EventListener, Restorable {
if (!recorder.isRecording()) { if (!recorder.isRecording()) {
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setText("Recording"); voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(
IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
}); });
recorder.start(); recorder.start();
} else { } else {
pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_" pendingAttachment = new Attachment(recorder.finish(), "Voice_recording_"
+ DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss").format(LocalDateTime.now()) + "." + AudioRecorder.FILE_FORMAT, + DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss")
AttachmentType.VOICE); .format(LocalDateTime.now())
+ "." + AudioRecorder.FILE_FORMAT,
AttachmentType.VOICE);
Platform.runLater(() -> { Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(
IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null); voiceButton.setText(null);
checkPostConditions(false); checkPostConditions(false);
updateAttachmentView(true); updateAttachmentView(true);
@ -440,7 +490,8 @@ public final class ChatScene implements EventListener, Restorable {
} }
} catch (final EnvoyException e) { } catch (final EnvoyException e) {
logger.log(Level.SEVERE, "Could not record audio: ", e); logger.log(Level.SEVERE, "Could not record audio: ", e);
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait); Platform
.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
} }
}).start(); }).start();
} }
@ -454,15 +505,16 @@ public final class ChatScene implements EventListener, Restorable {
fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.getExtensionFilters() fileChooser.getExtensionFilters()
.addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"), .addAll(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"),
new FileChooser.ExtensionFilter("Videos", "*.mp4"), new FileChooser.ExtensionFilter("Videos", "*.mp4"),
new FileChooser.ExtensionFilter("All Files", "*.*")); new FileChooser.ExtensionFilter("All Files", "*.*"));
final var file = fileChooser.showOpenDialog(sceneContext.getStage()); final var file = fileChooser.showOpenDialog(sceneContext.getStage());
if (file != null) { if (file != null) {
// Check max file size // Check max file size
if (file.length() > 16E6) { if (file.length() > 16E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!").showAndWait(); new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 16MB!")
.showAndWait();
return; return;
} }
@ -484,7 +536,8 @@ public final class ChatScene implements EventListener, Restorable {
checkPostConditions(false); checkPostConditions(false);
// Setting the preview image as image of the attachmentView // Setting the preview image as image of the attachmentView
if (type == AttachmentType.PICTURE) { if (type == AttachmentType.PICTURE) {
attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes), DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true)); attachmentView.setImage(new Image(new ByteArrayInputStream(fileBytes),
DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE, true, true));
isCustomAttachmentImage = true; isCustomAttachmentImage = true;
} }
attachmentView.setVisible(true); attachmentView.setVisible(true);
@ -495,8 +548,7 @@ public final class ChatScene implements EventListener, Restorable {
} }
/** /**
* Rotates every element in our application by {@code rotations}*360° in * Rotates every element in our application by {@code rotations}*360° in {@code an}.
* {@code an}.
* *
* @param rotations the amount of times the scene is rotated by 360° * @param rotations the amount of times the scene is rotated by 360°
* @param animationTime the time in seconds that this animation lasts * @param animationTime the time in seconds that this animation lasts
@ -513,7 +565,8 @@ public final class ChatScene implements EventListener, Restorable {
final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this); final var rotatableNodes = ReflectionUtil.getAllDeclaredNodeVariables(this);
for (final var node : rotatableNodes) { for (final var node : rotatableNodes) {
// Sets the animation duration to {animationTime} // Sets the animation duration to {animationTime}
final var rotateTransition = new RotateTransition(Duration.seconds(animationTime), node); final var rotateTransition =
new RotateTransition(Duration.seconds(animationTime), node);
// rotates every element {rotations} times // rotates every element {rotations} times
rotateTransition.setByAngle(rotations * 360); rotateTransition.setByAngle(rotations * 360);
rotateTransition.play(); rotateTransition.play();
@ -524,9 +577,8 @@ public final class ChatScene implements EventListener, Restorable {
} }
/** /**
* Checks the text length of the {@code messageTextArea}, adjusts the * Checks the text length of the {@code messageTextArea}, adjusts the {@code remainingChars}
* {@code remainingChars} label and checks whether to send the message * label and checks whether to send the message automatically.
* automatically.
* *
* @param e the key event that will be analyzed for a post request * @param e the key event that will be analyzed for a post request
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -539,29 +591,33 @@ public final class ChatScene implements EventListener, Restorable {
// Sending an IsTyping event if none has been sent for // Sending an IsTyping event if none has been sent for
// IsTyping#millisecondsActive // IsTyping#millisecondsActive
if (client.isOnline() && currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) { if (client.isOnline() && currentChat.getLastWritingEvent()
+ IsTyping.millisecondsActive <= System.currentTimeMillis()) {
client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID())); client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID()));
currentChat.lastWritingEventWasNow(); currentChat.lastWritingEventWasNow();
} }
// KeyPressed will be called before the char has been added to the text, hence // KeyPressed will be called before the char has been added to the text, hence
// this is needed for the first char // this is needed for the first char
if (messageTextArea.getText().length() == 1 && e != null) checkPostConditions(e); if (messageTextArea.getText().length() == 1 && e != null)
checkPostConditions(e);
// This is needed for the messageTA context menu // This is needed for the messageTA context menu
else if (e == null) checkPostConditions(false); else if (e == null)
checkPostConditions(false);
} }
/** /**
* Returns the id that should be used to send things to the server: the id of * Returns the id that should be used to send things to the server: the id of 'our' {@link User}
* 'our' {@link User} if the recipient of that object is another User, else the * if the recipient of that object is another User, else the id of the {@link Group} 'our' user
* id of the {@link Group} 'our' user is sending to. * is sending to.
* *
* @return an id that can be sent to the server * @return an id that can be sent to the server
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private long getChatID() { private long getChatID() {
return currentChat.getRecipient() instanceof User ? client.getSender().getID() : currentChat.getRecipient().getID(); return currentChat.getRecipient() instanceof User ? client.getSender().getID()
: currentChat.getRecipient().getID();
} }
/** /**
@ -571,21 +627,25 @@ public final class ChatScene implements EventListener, Restorable {
@FXML @FXML
private void checkPostConditions(KeyEvent e) { private void checkPostConditions(KeyEvent e) {
final var enterPressed = e.getCode() == KeyCode.ENTER; final var enterPressed = e.getCode() == KeyCode.ENTER;
final var messagePosted = enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown() : false; final var messagePosted =
enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown()
: false;
if (messagePosted) { if (messagePosted) {
// Removing an inserted line break if added by pressing enter // Removing an inserted line break if added by pressing enter
final var text = messageTextArea.getText(); final var text = messageTextArea.getText();
final var textPosition = messageTextArea.getCaretPosition() - 1; final var textPosition = messageTextArea.getCaretPosition() - 1;
if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n') if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n')
messageTextArea.setText(new StringBuilder(text).deleteCharAt(textPosition).toString()); messageTextArea
.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
} }
// if control is pressed, the enter press is originally invalidated. Here it'll // if control is pressed, the enter press is originally invalidated. Here it'll
// be inserted again // be inserted again
else if (enterPressed && e.isControlDown()) { else if (enterPressed && e.isControlDown()) {
var caretPosition = messageTextArea.getCaretPosition(); var caretPosition = messageTextArea.getCaretPosition();
messageTextArea.setText(new StringBuilder(messageTextArea.getText()).insert(caretPosition, '\n').toString()); messageTextArea.setText(new StringBuilder(messageTextArea.getText())
.insert(caretPosition, '\n').toString());
messageTextArea.positionCaret(++caretPosition); messageTextArea.positionCaret(++caretPosition);
} }
checkPostConditions(messagePosted); checkPostConditions(messagePosted);
@ -593,8 +653,10 @@ public final class ChatScene implements EventListener, Restorable {
private void checkPostConditions(boolean postMessage) { private void checkPostConditions(boolean postMessage) {
if (!postingPermanentlyDisabled) { if (!postingPermanentlyDisabled) {
if (!postButton.isDisabled() && postMessage) postMessage(); if (!postButton.isDisabled() && postMessage)
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null || currentChat == null); postMessage();
postButton.setDisable(messageTextArea.getText().isBlank() && pendingAttachment == null
|| currentChat == null);
} else { } else {
final var noMoreMessaging = "Go online to send messages"; final var noMoreMessaging = "Go online to send messages";
if (!infoLabel.getText().equals(noMoreMessaging)) if (!infoLabel.getText().equals(noMoreMessaging))
@ -628,13 +690,14 @@ public final class ChatScene implements EventListener, Restorable {
private void updateRemainingCharsLabel() { private void updateRemainingCharsLabel() {
final var currentLength = messageTextArea.getText().length(); final var currentLength = messageTextArea.getText().length();
final var remainingLength = MAX_MESSAGE_LENGTH - currentLength; final var remainingLength = MAX_MESSAGE_LENGTH - currentLength;
remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH)); remainingChars
.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1)); remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
} }
/** /**
* Sends a new {@link Message} or {@link GroupMessage} to the server based on * Sends a new {@link Message} or {@link GroupMessage} to the server based on the text entered
* the text entered in the {@code messageTextArea} and the given attachment. * in the {@code messageTextArea} and the given attachment.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -651,8 +714,9 @@ public final class ChatScene implements EventListener, Restorable {
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
if (!commands.getChatSceneCommands().executeIfPresent(text)) { if (!commands.getChatSceneCommands().executeIfPresent(text)) {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) final var builder = new MessageBuilder(localDB.getUser().getID(),
.setText(text); currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text);
// Setting an attachment, if present // Setting an attachment, if present
if (pendingAttachment != null) { if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment); builder.setAttachment(pendingAttachment);
@ -660,8 +724,9 @@ public final class ChatScene implements EventListener, Restorable {
updateAttachmentView(false); updateAttachmentView(false);
} }
// Building the final message // Building the final message
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient()) final var message = currentChat.getRecipient() instanceof Group
: builder.build(); ? builder.buildGroupMessage((Group) currentChat.getRecipient())
: builder.build();
// Send message // Send message
writeProxy.writeMessage(message); writeProxy.writeMessage(message);
@ -679,7 +744,8 @@ public final class ChatScene implements EventListener, Restorable {
scrollToMessageListEnd(); scrollToMessageListEnd();
// Request a new ID generator if all IDs were used // Request a new ID generator if all IDs were used
if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIDGenerator(); if (!localDB.getIDGenerator().hasNext() && client.isOnline())
client.requestIDGenerator();
} }
// Clear text field and disable post button // Clear text field and disable post button
@ -694,14 +760,16 @@ public final class ChatScene implements EventListener, Restorable {
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void scrollToMessageListEnd() { messageList.scrollTo(messageList.getItems().size() - 1); } private void scrollToMessageListEnd() {
messageList.scrollTo(messageList.getItems().size() - 1);
}
/** /**
* Updates the {@code infoLabel}. * Updates the {@code infoLabel}.
* *
* @param text the text to use * @param text the text to use
* @param infoLabelID the id the the {@code infoLabel} should have so that it * @param infoLabelID the id the the {@code infoLabel} should have so that it can be styled
* can be styled accordingly in CSS * accordingly in CSS
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void updateInfoLabel(String text, String infoLabelID) { private void updateInfoLabel(String text, String infoLabelID) {
@ -712,14 +780,15 @@ public final class ChatScene implements EventListener, Restorable {
/** /**
* Updates the {@code attachmentView} in terms of visibility.<br> * Updates the {@code attachmentView} in terms of visibility.<br>
* Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} * Additionally resets the shown image to {@code DEFAULT_ATTACHMENT_VIEW_IMAGE} if another image
* if another image is currently present. * is currently present.
* *
* @param visible whether the {@code attachmentView} should be displayed * @param visible whether the {@code attachmentView} should be displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private void updateAttachmentView(boolean visible) { private void updateAttachmentView(boolean visible) {
if (!(attachmentView.getImage() == null || attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE))) if (!(attachmentView.getImage() == null
|| attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)))
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE); attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
attachmentView.setVisible(visible); attachmentView.setVisible(visible);
} }
@ -741,8 +810,7 @@ public final class ChatScene implements EventListener, Restorable {
} }
/** /**
* Redesigns the UI when the {@link Chat} of the given contact has been marked * Redesigns the UI when the {@link Chat} of the given contact has been marked as disabled.
* as disabled.
* *
* @param event the contact whose chat got disabled * @param event the contact whose chat got disabled
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
@ -754,7 +822,8 @@ public final class ChatScene implements EventListener, Restorable {
// Decrement member count for groups // Decrement member count for groups
if (recipient instanceof Group) if (recipient instanceof Group)
topBarStatusLabel.setText(recipient.getContacts().size() + " member" + (recipient.getContacts().size() != 1 ? "s" : "")); topBarStatusLabel.setText(recipient.getContacts().size() + " member"
+ (recipient.getContacts().size() != 1 ? "s" : ""));
if (currentChat != null && currentChat.getRecipient().equals(recipient)) { if (currentChat != null && currentChat.getRecipient().equals(recipient)) {
messageTextArea.setDisable(true); messageTextArea.setDisable(true);
voiceButton.setDisable(true); voiceButton.setDisable(true);
@ -786,13 +855,15 @@ public final class ChatScene implements EventListener, Restorable {
remainingChars.setVisible(false); remainingChars.setVisible(false);
pendingAttachment = null; pendingAttachment = null;
recipientProfilePic.setImage(null); recipientProfilePic.setImage(null);
if (recorder.isRecording()) recorder.cancel(); if (recorder.isRecording())
recorder.cancel();
} }
@FXML @FXML
private void copyAndPostMessage() { private void copyAndPostMessage() {
final var messageText = messageTextArea.getText(); final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null); Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new StringSelection(messageText), null);
final var image = attachmentView.getImage(); final var image = attachmentView.getImage();
final var messageAttachment = pendingAttachment; final var messageAttachment = pendingAttachment;
postMessage(); postMessage();
@ -800,7 +871,8 @@ public final class ChatScene implements EventListener, Restorable {
updateRemainingCharsLabel(); updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank()); postButton.setDisable(messageText.isBlank());
attachmentView.setImage(image); attachmentView.setImage(image);
if (attachmentView.getImage() != null) attachmentView.setVisible(true); if (attachmentView.getImage() != null)
attachmentView.setVisible(true);
pendingAttachment = messageAttachment; pendingAttachment = messageAttachment;
} }
@ -809,11 +881,14 @@ public final class ChatScene implements EventListener, Restorable {
* *
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public void clearMessageSelection() { messageList.getSelectionModel().clearSelection(); } public void clearMessageSelection() {
messageList.getSelectionModel().clearSelection();
}
@FXML @FXML
private void searchContacts() { private void searchContacts() {
chats.setPredicate(contactSearch.getText().isBlank() ? c -> true chats.setPredicate(contactSearch.getText().isBlank() ? c -> true
: c -> c.getRecipient().getName().toLowerCase().contains(contactSearch.getText().toLowerCase())); : c -> c.getRecipient().getName().toLowerCase()
.contains(contactSearch.getText().toLowerCase()));
} }
} }

View File

@ -7,28 +7,28 @@ import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.*;
import envoy.data.User;
import envoy.event.ElementOperation;
import envoy.event.contact.*;
import envoy.util.EnvoyLog;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.event.BackEvent; import envoy.client.event.BackEvent;
import envoy.client.helper.AlertHelper; import envoy.client.helper.AlertHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.control.ContactControl; import envoy.client.ui.control.ContactControl;
import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.ListCellFactory;
import envoy.data.User;
import envoy.event.ElementOperation;
import envoy.event.contact.*;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
/** /**
* Provides a search bar in which a user name (substring) can be entered. The * Provides a search bar in which a user name (substring) can be entered. The users with a matching
* users with a matching name are then displayed inside a list view. A * name are then displayed inside a list view. A {@link UserSearchRequest} is sent on every
* {@link UserSearchRequest} is sent on every keystroke. * keystroke.
* <p> * <p>
* <i>The actual search algorithm is implemented on the server. * <i>The actual search algorithm is implemented on the server.
* <p> * <p>
* To create a group, a button is available that loads the * To create a group, a button is available that loads the {@link GroupCreationTab}.
* {@link GroupCreationTab}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -59,16 +59,22 @@ public class ContactSearchTab implements EventListener {
@Event @Event
private void onUserSearchResult(UserSearchResult result) { private void onUserSearchResult(UserSearchResult result) {
Platform.runLater(() -> { userList.getItems().clear(); userList.getItems().addAll(result.get()); }); Platform.runLater(() -> {
userList.getItems().clear();
userList.getItems().addAll(result.get());
});
} }
@Event @Event
private void onUserOperation(UserOperation operation) { private void onUserOperation(UserOperation operation) {
final var contact = operation.get(); final var contact = operation.get();
if (operation.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> { if (operation.getOperationType() == ElementOperation.ADD)
userList.getItems().remove(contact); Platform.runLater(() -> {
if (currentlySelectedUser != null && currentlySelectedUser.equals(contact) && alert.isShowing()) alert.close(); userList.getItems().remove(contact);
}); if (currentlySelectedUser != null && currentlySelectedUser.equals(contact)
&& alert.isShowing())
alert.close();
});
} }
/** /**
@ -79,13 +85,15 @@ public class ContactSearchTab implements EventListener {
@FXML @FXML
private void sendRequest() { private void sendRequest() {
final var text = searchBar.getText().strip(); final var text = searchBar.getText().strip();
if (!text.isBlank()) client.send(new UserSearchRequest(text)); if (!text.isBlank())
else userList.getItems().clear(); client.send(new UserSearchRequest(text));
else
userList.getItems().clear();
} }
/** /**
* Clears the text in the search bar and the items shown in the list. * Clears the text in the search bar and the items shown in the list. Additionally disables both
* Additionally disables both clear and search button. * clear and search button.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -96,8 +104,7 @@ public class ContactSearchTab implements EventListener {
} }
/** /**
* Sends an {@link UserOperation} for the selected user to the * Sends an {@link UserOperation} for the selected user to the server.
* server.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -106,7 +113,8 @@ public class ContactSearchTab implements EventListener {
final var user = userList.getSelectionModel().getSelectedItem(); final var user = userList.getSelectionModel().getSelectedItem();
if (user != null) { if (user != null) {
currentlySelectedUser = user; currentlySelectedUser = user;
alert.setContentText("Add user " + currentlySelectedUser.getName() + " to your contacts?"); alert.setContentText(
"Add user " + currentlySelectedUser.getName() + " to your contacts?");
AlertHelper.confirmAction(alert, this::addAsContact); AlertHelper.confirmAction(alert, this::addAsContact);
} }
} }

View File

@ -10,25 +10,25 @@ import javafx.scene.control.*;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import envoy.client.data.*; import dev.kske.eventbus.*;
import envoy.client.event.BackEvent;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.ListCellFactory;
import envoy.data.*; import envoy.data.*;
import envoy.event.GroupCreation; import envoy.event.GroupCreation;
import envoy.event.contact.UserOperation; import envoy.event.contact.UserOperation;
import envoy.util.Bounds; import envoy.util.Bounds;
import dev.kske.eventbus.*; import envoy.client.data.*;
import envoy.client.event.BackEvent;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.ListCellFactory;
/** /**
* Provides a group creation interface. A group name can be entered in the text * Provides a group creation interface. A group name can be entered in the text field at the top.
* field at the top. Available users (local chat recipients) are displayed * Available users (local chat recipients) are displayed inside a list and can be selected (multiple
* inside a list and can be selected (multiple selection available). * selection available).
* <p> * <p>
* When the group creation button is pressed, a {@link GroupCreation} is sent to * When the group creation button is pressed, a {@link GroupCreation} is sent to the server. This
* the server. This controller enforces a valid group name and a non-empty * controller enforces a valid group name and a non-empty member list (excluding the client user).
* member list (excluding the client user).
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -93,8 +93,10 @@ public class GroupCreationTab implements EventListener {
@FXML @FXML
private void userListClicked() { private void userListClicked() {
if (userList.getSelectionModel().getSelectedItem() != null) { if (userList.getSelectionModel().getSelectedItem() != null) {
quickSelectList.getItems().add(new QuickSelectControl(userList.getSelectionModel().getSelectedItem(), this::removeFromQuickSelection)); quickSelectList.getItems().add(new QuickSelectControl(
createButton.setDisable(quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank()); userList.getSelectionModel().getSelectedItem(), this::removeFromQuickSelection));
createButton.setDisable(
quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank());
resizeQuickSelectSpace(60); resizeQuickSelectSpace(60);
userList.getItems().remove(userList.getSelectionModel().getSelectedItem()); userList.getItems().remove(userList.getSelectionModel().getSelectedItem());
userList.getSelectionModel().clearSelection(); userList.getSelectionModel().clearSelection();
@ -102,13 +104,16 @@ public class GroupCreationTab implements EventListener {
} }
/** /**
* Checks, whether the {@code createButton} can be enabled because text is * Checks, whether the {@code createButton} can be enabled because text is present in the text
* present in the text field. * field.
* *
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@FXML @FXML
private void textUpdated() { createButton.setDisable(quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank()); } private void textUpdated() {
createButton
.setDisable(quickSelectList.getItems().isEmpty() || groupNameField.getText().isBlank());
}
/** /**
* Sends a {@link GroupCreation} to the server and closes this scene. * Sends a {@link GroupCreation} to the server and closes this scene.
@ -152,19 +157,20 @@ public class GroupCreationTab implements EventListener {
private void createGroup(String name) { private void createGroup(String name) {
Context.getInstance() Context.getInstance()
.getClient() .getClient()
.send(new GroupCreation(name, quickSelectList.getItems().stream().map(q -> q.getUser().getID()).collect(Collectors.toSet()))); .send(new GroupCreation(name, quickSelectList.getItems().stream()
.map(q -> q.getUser().getID()).collect(Collectors.toSet())));
} }
/** /**
* Returns true if the proposed group name is already present in the users * Returns true if the proposed group name is already present in the users {@code LocalDB}.
* {@code LocalDB}.
* *
* @param newName the chosen group name * @param newName the chosen group name
* @return true if this name is already present * @return true if this name is already present
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public boolean groupNameAlreadyPresent(String newName) { public boolean groupNameAlreadyPresent(String newName) {
return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance).map(Contact::getName).anyMatch(newName::equals); return localDB.getChats().stream().map(Chat::getRecipient).filter(Group.class::isInstance)
.map(Contact::getName).anyMatch(newName::equals);
} }
/** /**

View File

@ -10,14 +10,15 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import envoy.client.data.ClientConfig; import dev.kske.eventbus.*;
import envoy.client.ui.*;
import envoy.client.util.IconUtil;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.event.HandshakeRejection; import envoy.event.HandshakeRejection;
import envoy.util.*; import envoy.util.*;
import dev.kske.eventbus.*; import envoy.client.data.ClientConfig;
import envoy.client.ui.Startup;
import envoy.client.util.IconUtil;
/** /**
* Controller for the login scene. * Controller for the login scene.
@ -78,25 +79,32 @@ public final class LoginScene implements EventListener {
@FXML @FXML
private void loginButtonPressed() { private void loginButtonPressed() {
final String user = userTextField.getText(), pass = passwordField.getText(), repeatPass = repeatPasswordField.getText(); final String user = userTextField.getText(), pass = passwordField.getText(),
repeatPass = repeatPasswordField.getText();
final boolean requestToken = cbStaySignedIn.isSelected(); final boolean requestToken = cbStaySignedIn.isSelected();
// Prevent registration with unequal passwords // Prevent registration with unequal passwords
if (registration && !pass.equals(repeatPass)) { if (registration && !pass.equals(repeatPass)) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait(); new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one")
.showAndWait();
repeatPasswordField.clear(); repeatPasswordField.clear();
} else if (!Bounds.isValidContactName(user)) { } else if (!Bounds.isValidContactName(user)) {
new Alert(AlertType.ERROR, "The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait(); new Alert(AlertType.ERROR,
"The entered user name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")")
.showAndWait();
userTextField.clear(); userTextField.clear();
} else { } else {
Instant lastSync = Startup.loadLastSync(userTextField.getText()); Instant lastSync = Startup.loadLastSync(userTextField.getText());
Startup.performHandshake(registration ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync) Startup.performHandshake(registration
: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync)); ? LoginCredentials.registration(user, pass, requestToken, Startup.VERSION, lastSync)
: LoginCredentials.login(user, pass, requestToken, Startup.VERSION, lastSync));
} }
} }
@FXML @FXML
private void offlineModeButtonPressed() { Startup.attemptOfflineMode(userTextField.getText()); } private void offlineModeButtonPressed() {
Startup.attemptOfflineMode(userTextField.getText());
}
@FXML @FXML
private void registerSwitchPressed() { private void registerSwitchPressed() {
@ -127,5 +135,7 @@ public final class LoginScene implements EventListener {
} }
@Event @Event
private void onHandshakeRejection(HandshakeRejection evt) { Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait()); } private void onHandshakeRejection(HandshakeRejection evt) {
Platform.runLater(() -> new Alert(AlertType.ERROR, evt.get()).showAndWait());
}
} }

View File

@ -28,7 +28,8 @@ public final class SettingsScene implements KeyboardMapping {
@FXML @FXML
private void initialize() { private void initialize() {
settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle()))); settingsList.setCellFactory(new ListCellFactory<>(pane -> new Label(pane.getTitle())));
settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(), new DownloadSettingsPane(), new BugReportPane()); settingsList.getItems().addAll(new GeneralSettingsPane(), new UserSettingsPane(),
new DownloadSettingsPane(), new BugReportPane());
} }
@FXML @FXML
@ -41,10 +42,13 @@ public final class SettingsScene implements KeyboardMapping {
} }
@FXML @FXML
private void backButtonClicked() { Context.getInstance().getSceneContext().pop(); } private void backButtonClicked() {
Context.getInstance().getSceneContext().pop();
}
@Override @Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() { public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.of(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN), this::backButtonClicked); return Map.of(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN),
this::backButtonClicked);
} }
} }

View File

@ -2,11 +2,12 @@ package envoy.client.ui.listcell;
import javafx.scene.control.*; import javafx.scene.control.*;
import envoy.data.User;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.control.ChatControl; import envoy.client.ui.control.ChatControl;
import envoy.client.util.UserUtil; import envoy.client.util.UserUtil;
import envoy.data.User;
/** /**
* A list cell containing chats represented as chat controls. * A list cell containing chats represented as chat controls.
@ -22,7 +23,9 @@ public class ChatListCell extends AbstractListCell<Chat, ChatControl> {
* @param listView the list view inside of which the cell will be displayed * @param listView the list view inside of which the cell will be displayed
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public ChatListCell(ListView<? extends Chat> listView) { super(listView); } public ChatListCell(ListView<? extends Chat> listView) {
super(listView);
}
@Override @Override
protected ChatControl renderItem(Chat chat) { protected ChatControl renderItem(Chat chat) {
@ -30,17 +33,23 @@ public class ChatListCell extends AbstractListCell<Chat, ChatControl> {
final var menu = new ContextMenu(); final var menu = new ContextMenu();
final var removeMI = new MenuItem(); final var removeMI = new MenuItem();
removeMI.setText( removeMI.setText(
chat.isDisabled() ? "Delete " : chat.getRecipient() instanceof User ? "Block " : "Leave group " + chat.getRecipient().getName()); chat.isDisabled() ? "Delete "
: chat.getRecipient() instanceof User ? "Block "
: "Leave group " + chat.getRecipient().getName());
removeMI.setOnAction( removeMI.setOnAction(
chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient()) : e -> UserUtil.disableContact(chat.getRecipient())); chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient())
: e -> UserUtil.disableContact(chat.getRecipient()));
menu.getItems().add(removeMI); menu.getItems().add(removeMI);
setContextMenu(menu); setContextMenu(menu);
} else setContextMenu(null); } else
setContextMenu(null);
// TODO: replace with icon in ChatControl // TODO: replace with icon in ChatControl
final var chatControl = new ChatControl(chat); final var chatControl = new ChatControl(chat);
if (chat.isDisabled()) chatControl.getStyleClass().add("disabled-chat"); if (chat.isDisabled())
else chatControl.getStyleClass().remove("disabled-chat"); chatControl.getStyleClass().add("disabled-chat");
else
chatControl.getStyleClass().remove("disabled-chat");
return chatControl; return chatControl;
} }
} }

View File

@ -28,5 +28,7 @@ public final class GenericListCell<T, U extends Node> extends AbstractListCell<T
} }
@Override @Override
protected U renderItem(T item) { return renderer.apply(item); } protected U renderItem(T item) {
return renderer.apply(item);
}
} }

View File

@ -7,15 +7,15 @@ import javafx.scene.control.*;
import javafx.util.Callback; import javafx.util.Callback;
/** /**
* Provides a creation mechanism for generic list cells given a list view and a * Provides a creation mechanism for generic list cells given a list view and a conversion function.
* conversion function.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @param <T> the type of object to display * @param <T> the type of object to display
* @param <U> the type of node displayed * @param <U> the type of node displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class ListCellFactory<T, U extends Node> implements Callback<ListView<T>, ListCell<T>> { public final class ListCellFactory<T, U extends Node>
implements Callback<ListView<T>, ListCell<T>> {
private final Function<? super T, U> renderer; private final Function<? super T, U> renderer;
@ -23,8 +23,12 @@ public final class ListCellFactory<T, U extends Node> implements Callback<ListVi
* @param renderer a function converting the type to display into a node * @param renderer a function converting the type to display into a node
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public ListCellFactory(Function<? super T, U> renderer) { this.renderer = renderer; } public ListCellFactory(Function<? super T, U> renderer) {
this.renderer = renderer;
}
@Override @Override
public ListCell<T> call(ListView<T> listView) { return new GenericListCell<>(listView, renderer); } public ListCell<T> call(ListView<T> listView) {
return new GenericListCell<>(listView, renderer);
}
} }

View File

@ -3,9 +3,10 @@ package envoy.client.ui.listcell;
import javafx.geometry.*; import javafx.geometry.*;
import javafx.scene.control.ListView; import javafx.scene.control.ListView;
import envoy.client.ui.control.MessageControl;
import envoy.data.Message; import envoy.data.Message;
import envoy.client.ui.control.MessageControl;
/** /**
* A list cell containing messages represented as message controls. * A list cell containing messages represented as message controls.
* *
@ -18,15 +19,20 @@ public final class MessageListCell extends AbstractListCell<Message, MessageCont
* @param listView the list view inside of which the cell will be displayed * @param listView the list view inside of which the cell will be displayed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public MessageListCell(ListView<? extends Message> listView) { super(listView); } public MessageListCell(ListView<? extends Message> listView) {
super(listView);
}
@Override @Override
protected MessageControl renderItem(Message message) { protected MessageControl renderItem(Message message) {
final var control = new MessageControl(message); final var control = new MessageControl(message);
listView.widthProperty().addListener((observable, oldValue, newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage())); listView.widthProperty().addListener((observable, oldValue,
newValue) -> adjustPadding(newValue.intValue(), control.isOwnMessage()));
adjustPadding((int) listView.getWidth(), control.isOwnMessage()); adjustPadding((int) listView.getWidth(), control.isOwnMessage());
if (control.isOwnMessage()) setAlignment(Pos.CENTER_RIGHT); if (control.isOwnMessage())
else setAlignment(Pos.CENTER_LEFT); setAlignment(Pos.CENTER_RIGHT);
else
setAlignment(Pos.CENTER_LEFT);
return control; return control;
} }

View File

@ -1,6 +1,5 @@
/** /**
* This package contains custom list cells that are used to display certain * This package contains custom list cells that are used to display certain things.
* things.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart

View File

@ -7,8 +7,8 @@ import javafx.scene.input.InputEvent;
import envoy.event.IssueProposal; import envoy.event.IssueProposal;
/** /**
* This class offers the option for users to submit a bug report. Only the title * This class offers the option for users to submit a bug report. Only the title of a bug is needed
* of a bug is needed to be sent. * to be sent.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
@ -17,12 +17,15 @@ public final class BugReportPane extends OnlineOnlySettingsPane {
private final Label titleLabel = new Label("Suggest a title for the bug:"); private final Label titleLabel = new Label("Suggest a title for the bug:");
private final TextField titleTextField = new TextField(); private final TextField titleTextField = new TextField();
private final Label pleaseExplainLabel = new Label("Paste here the log of what went wrong and/ or explain what went wrong:"); private final Label pleaseExplainLabel =
new Label("Paste here the log of what went wrong and/ or explain what went wrong:");
private final TextArea errorDetailArea = new TextArea(); private final TextArea errorDetailArea = new TextArea();
private final CheckBox showUsernameInBugReport = new CheckBox("Show your username in the bug report?"); private final CheckBox showUsernameInBugReport =
new CheckBox("Show your username in the bug report?");
private final Button submitReportButton = new Button("Submit report"); private final Button submitReportButton = new Button("Submit report");
private final EventHandler<? super InputEvent> inputEventHandler = e -> submitReportButton.setDisable(titleTextField.getText().isBlank()); private final EventHandler<? super InputEvent> inputEventHandler =
e -> submitReportButton.setDisable(titleTextField.getText().isBlank());
/** /**
* Creates a new {@code BugReportPane}. * Creates a new {@code BugReportPane}.
@ -59,7 +62,9 @@ public final class BugReportPane extends OnlineOnlySettingsPane {
submitReportButton.setDisable(true); submitReportButton.setDisable(true);
submitReportButton.setOnAction(e -> { submitReportButton.setOnAction(e -> {
String title = titleTextField.getText(), description = errorDetailArea.getText(); String title = titleTextField.getText(), description = errorDetailArea.getText();
client.send(showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true) : new IssueProposal(title, description, client.getSender().getName(), true)); client.send(
showUsernameInBugReport.isSelected() ? new IssueProposal(title, description, true)
: new IssueProposal(title, description, client.getSender().getName(), true));
}); });
getChildren().add(submitReportButton); getChildren().add(submitReportButton);
} }

View File

@ -24,18 +24,22 @@ public final class DownloadSettingsPane extends SettingsPane {
setPadding(new Insets(15)); setPadding(new Insets(15));
// Checkbox to disable asking // Checkbox to disable asking
final var checkBox = new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName()); final var checkBox =
new CheckBox(settings.getItems().get("autoSaveDownloads").getUserFriendlyName());
checkBox.setSelected(settings.isDownloadSavedWithoutAsking()); checkBox.setSelected(settings.isDownloadSavedWithoutAsking());
checkBox.setTooltip(new Tooltip("Determines whether a \"Select save location\" - dialogue will be shown when saving attachments.")); checkBox.setTooltip(new Tooltip(
"Determines whether a \"Select save location\" - dialogue will be shown when saving attachments."));
checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected())); checkBox.setOnAction(e -> settings.setDownloadSavedWithoutAsking(checkBox.isSelected()));
getChildren().add(checkBox); getChildren().add(checkBox);
// Displaying the default path to save to // Displaying the default path to save to
final var pathLabel = new Label(settings.getItems().get("downloadLocation").getDescription() + ":"); final var pathLabel =
new Label(settings.getItems().get("downloadLocation").getDescription() + ":");
pathLabel.setWrapText(true); pathLabel.setWrapText(true);
getChildren().add(pathLabel); getChildren().add(pathLabel);
final var hbox = new HBox(20); final var hbox = new HBox(20);
Tooltip.install(hbox, new Tooltip("Determines the location where attachments will be saved to.")); Tooltip.install(hbox,
new Tooltip("Determines the location where attachments will be saved to."));
final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath()); final var currentPath = new Label(settings.getDownloadLocation().getAbsolutePath());
hbox.getChildren().add(currentPath); hbox.getChildren().add(currentPath);
@ -45,7 +49,8 @@ public final class DownloadSettingsPane extends SettingsPane {
final var directoryChooser = new DirectoryChooser(); final var directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Select the directory where attachments should be saved to"); directoryChooser.setTitle("Select the directory where attachments should be saved to");
directoryChooser.setInitialDirectory(settings.getDownloadLocation()); directoryChooser.setInitialDirectory(settings.getDownloadLocation());
final var selectedDirectory = directoryChooser.showDialog(context.getSceneContext().getStage()); final var selectedDirectory =
directoryChooser.showDialog(context.getSceneContext().getStage());
if (selectedDirectory != null) { if (selectedDirectory != null) {
currentPath.setText(selectedDirectory.getAbsolutePath()); currentPath.setText(selectedDirectory.getAbsolutePath());

View File

@ -2,13 +2,14 @@ package envoy.client.ui.settings;
import javafx.scene.control.*; import javafx.scene.control.*;
import dev.kske.eventbus.EventBus;
import envoy.data.User.UserStatus;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.ui.StatusTrayIcon; import envoy.client.ui.StatusTrayIcon;
import envoy.client.util.UserUtil; import envoy.client.util.UserUtil;
import envoy.data.User.UserStatus;
import dev.kske.eventbus.EventBus;
/** /**
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -27,22 +28,27 @@ public final class GeneralSettingsPane extends SettingsPane {
// Add hide on close if supported // Add hide on close if supported
if (StatusTrayIcon.isSupported()) { if (StatusTrayIcon.isSupported()) {
final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose")); final var hideOnCloseCheckbox =
final var hideOnCloseTooltip = new Tooltip("If selected, Envoy will still be present in the task bar when closed."); new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
final var hideOnCloseTooltip = new Tooltip(
"If selected, Envoy will still be present in the task bar when closed.");
hideOnCloseTooltip.setWrapText(true); hideOnCloseTooltip.setWrapText(true);
hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip); hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
getChildren().add(hideOnCloseCheckbox); getChildren().add(hideOnCloseCheckbox);
} }
final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend")); final var enterToSendCheckbox =
new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
final var enterToSendTooltip = new Tooltip( final var enterToSendTooltip = new Tooltip(
"When selected, messages can be sent pressing \"Enter\". A line break can be inserted by pressing \"Ctrl\" + \"Enter\". Else it will be the other way around."); "When selected, messages can be sent pressing \"Enter\". A line break can be inserted by pressing \"Ctrl\" + \"Enter\". Else it will be the other way around.");
enterToSendTooltip.setWrapText(true); enterToSendTooltip.setWrapText(true);
enterToSendCheckbox.setTooltip(enterToSendTooltip); enterToSendCheckbox.setTooltip(enterToSendTooltip);
getChildren().add(enterToSendCheckbox); getChildren().add(enterToSendCheckbox);
final var askForConfirmationCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation")); final var askForConfirmationCheckbox =
final var askForConfirmationTooltip = new Tooltip("When selected, nothing will prompt a confirmation dialog"); new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation"));
final var askForConfirmationTooltip =
new Tooltip("When selected, nothing will prompt a confirmation dialog");
askForConfirmationTooltip.setWrapText(true); askForConfirmationTooltip.setWrapText(true);
askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip); askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip);
getChildren().add(askForConfirmationCheckbox); getChildren().add(askForConfirmationCheckbox);
@ -50,9 +56,13 @@ public final class GeneralSettingsPane extends SettingsPane {
final var combobox = new ComboBox<String>(); final var combobox = new ComboBox<String>();
combobox.getItems().add("dark"); combobox.getItems().add("dark");
combobox.getItems().add("light"); combobox.getItems().add("light");
combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in.")); combobox
.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
combobox.setValue(settings.getCurrentTheme()); combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction(e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); }); combobox.setOnAction(e -> {
settings.setCurrentTheme(combobox.getValue());
EventBus.getInstance().dispatch(new ThemeChangeEvent());
});
getChildren().add(combobox); getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>(); final var statusComboBox = new ComboBox<UserStatus>();
@ -64,7 +74,8 @@ public final class GeneralSettingsPane extends SettingsPane {
final var logoutButton = new Button("Logout"); final var logoutButton = new Button("Logout");
logoutButton.setOnAction(e -> UserUtil.logout()); logoutButton.setOnAction(e -> UserUtil.logout());
final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account"); final var logoutTooltip = new Tooltip(
"Brings you back to the login screen and removes \"remember me\" status from this account");
logoutTooltip.setWrapText(true); logoutTooltip.setWrapText(true);
logoutButton.setTooltip(logoutTooltip); logoutButton.setTooltip(logoutTooltip);
getChildren().add(logoutButton); getChildren().add(logoutButton);

View File

@ -8,10 +8,10 @@ import javafx.scene.paint.Color;
import envoy.client.net.Client; import envoy.client.net.Client;
/** /**
* Inheriting from this class signifies that options should only be available if * Inheriting from this class signifies that options should only be available if the
* the {@link envoy.data.User} is currently online. If the user is currently * {@link envoy.data.User} is currently online. If the user is currently offline, all
* offline, all {@link javafx.scene.Node} variables will be disabled and a * {@link javafx.scene.Node} variables will be disabled and a {@link Tooltip} will be displayed for
* {@link Tooltip} will be displayed for the whole node. * the whole node.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -21,7 +21,8 @@ public abstract class OnlineOnlySettingsPane extends SettingsPane {
protected final Client client = context.getClient(); protected final Client client = context.getClient();
private final Tooltip beOnlineReminder = new Tooltip("You need to be online to modify your account."); private final Tooltip beOnlineReminder =
new Tooltip("You need to be online to modify your account.");
/** /**
* @param title the title of this pane * @param title the title of this pane
@ -33,14 +34,17 @@ public abstract class OnlineOnlySettingsPane extends SettingsPane {
setDisable(!client.isOnline()); setDisable(!client.isOnline());
if (!client.isOnline()) { if (!client.isOnline()) {
final var infoLabel = new Label("You shall not pass!\n(... Unless you would happen to be online)"); final var infoLabel =
new Label("You shall not pass!\n(... Unless you would happen to be online)");
infoLabel.setId("info-label-warning"); infoLabel.setId("info-label-warning");
infoLabel.setWrapText(true); infoLabel.setWrapText(true);
getChildren().add(infoLabel); getChildren().add(infoLabel);
setBackground(new Background(new BackgroundFill(Color.grayRgb(100, 0.3), CornerRadii.EMPTY, Insets.EMPTY))); setBackground(new Background(
new BackgroundFill(Color.grayRgb(100, 0.3), CornerRadii.EMPTY, Insets.EMPTY)));
Tooltip.install(this, beOnlineReminder); Tooltip.install(this, beOnlineReminder);
} else Tooltip.uninstall(this, beOnlineReminder); } else
Tooltip.uninstall(this, beOnlineReminder);
} }
/** /**
@ -49,5 +53,7 @@ public abstract class OnlineOnlySettingsPane extends SettingsPane {
* @param text the text to display * @param text the text to display
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
protected void setToolTipText(String text) { beOnlineReminder.setText(text); } protected void setToolTipText(String text) {
beOnlineReminder.setText(text);
}
} }

View File

@ -20,9 +20,9 @@ public final class SettingsCheckbox extends CheckBox {
public SettingsCheckbox(SettingsItem<Boolean> settingsItem) { public SettingsCheckbox(SettingsItem<Boolean> settingsItem) {
super(settingsItem.getUserFriendlyName()); super(settingsItem.getUserFriendlyName());
setSelected(settingsItem.get()); setSelected(settingsItem.get());
// "Schau, es hat sich behindert" - Kai, 2020 // "Schau, es hat sich behindert" - Kai, 2020
addEventHandler(ActionEvent.ACTION, e -> settingsItem.set(isSelected())); addEventHandler(ActionEvent.ACTION, e -> settingsItem.set(isSelected()));
} }
} }

View File

@ -15,7 +15,9 @@ public abstract class SettingsPane extends VBox {
protected static final Settings settings = Settings.getInstance(); protected static final Settings settings = Settings.getInstance();
protected static final Context context = Context.getInstance(); protected static final Context context = Context.getInstance();
protected SettingsPane(String title) { this.title = title; } protected SettingsPane(String title) {
this.title = title;
}
/** /**
* @return the title of this settings pane * @return the title of this settings pane

View File

@ -14,12 +14,13 @@ import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import envoy.client.ui.control.ProfilePicImageView; import dev.kske.eventbus.EventBus;
import envoy.client.util.IconUtil;
import envoy.event.*; import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import dev.kske.eventbus.EventBus; import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -58,12 +59,14 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
profilePic.setFitWidth(60); profilePic.setFitWidth(60);
profilePic.setFitHeight(60); profilePic.setFitHeight(60);
profilePic.setOnMouseClicked(e -> { profilePic.setOnMouseClicked(e -> {
if (!client.isOnline()) return; if (!client.isOnline())
return;
final var pictureChooser = new FileChooser(); final var pictureChooser = new FileChooser();
pictureChooser.setTitle("Select a new profile pic"); pictureChooser.setTitle("Select a new profile pic");
pictureChooser.setInitialDirectory(new File(System.getProperty("user.home"))); pictureChooser.setInitialDirectory(new File(System.getProperty("user.home")));
pictureChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif")); pictureChooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Pictures", "*.png", "*.jpg", "*.bmp", "*.gif"));
final var file = pictureChooser.showOpenDialog(context.getSceneContext().getStage()); final var file = pictureChooser.showOpenDialog(context.getSceneContext().getStage());
@ -72,7 +75,8 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// Check max file size // Check max file size
// TODO: Move to config // TODO: Move to config
if (file.length() > 5E6) { if (file.length() > 5E6) {
new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!").showAndWait(); new Alert(AlertType.WARNING, "The selected file exceeds the size limit of 5MB!")
.showAndWait();
return; return;
} }
@ -92,8 +96,8 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
newUsername = username; newUsername = username;
usernameTextField.setText(username); usernameTextField.setText(username);
final EventHandler<? super InputEvent> textChanged = e -> { final EventHandler<? super InputEvent> textChanged = e -> {
newUsername = usernameTextField.getText(); newUsername = usernameTextField.getText();
usernameChanged = newUsername != username; usernameChanged = newUsername != username;
}; };
usernameTextField.setOnInputMethodTextChanged(textChanged); usernameTextField.setOnInputMethodTextChanged(textChanged);
usernameTextField.setOnKeyTyped(textChanged); usernameTextField.setOnKeyTyped(textChanged);
@ -102,14 +106,21 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// "Displaying" the password change mechanism // "Displaying" the password change mechanism
final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() }; final HBox[] passwordHBoxes = { new HBox(), new HBox(), new HBox() };
final Label[] passwordLabels = { new Label("Enter current password:"), new Label("Enter new password:"), final Label[] passwordLabels =
{ new Label("Enter current password:"), new Label("Enter new password:"),
new Label("Repeat new password:") }; new Label("Repeat new password:") };
final PasswordField[] passwordFields = { currentPasswordField, newPasswordField, repeatNewPasswordField }; final PasswordField[] passwordFields =
{ currentPasswordField, newPasswordField, repeatNewPasswordField };
final EventHandler<? super InputEvent> passwordEntered = e -> { final EventHandler<? super InputEvent> passwordEntered = e -> {
newPassword = newPasswordField.getText(); newPassword =
validPassword = newPassword.equals(repeatNewPasswordField.getText()) newPasswordField.getText();
&& !newPasswordField.getText().isBlank(); validPassword = newPassword
.equals(
repeatNewPasswordField
.getText())
&& !newPasswordField
.getText().isBlank();
}; };
newPasswordField.setOnInputMethodTextChanged(passwordEntered); newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered); newPasswordField.setOnKeyTyped(passwordEntered);
@ -125,7 +136,8 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
} }
// Displaying the save button // Displaying the save button
saveButton.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText())); saveButton
.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT); saveButton.setAlignment(Pos.BOTTOM_RIGHT);
getChildren().add(saveButton); getChildren().add(saveButton);
} }
@ -156,7 +168,9 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
} else if (!validContactName) { } else if (!validContactName) {
final var alert = new Alert(AlertType.ERROR); final var alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid username"); alert.setTitle("Invalid username");
alert.setContentText("The entered username does not conform with the naming limitations: " + Bounds.CONTACT_NAME_PATTERN); alert.setContentText(
"The entered username does not conform with the naming limitations: "
+ Bounds.CONTACT_NAME_PATTERN);
alert.showAndWait(); alert.showAndWait();
logger.log(Level.INFO, "An invalid username was requested."); logger.log(Level.INFO, "An invalid username was requested.");
return; return;

View File

@ -1,6 +1,5 @@
/** /**
* This package contains classes used for representing the settings * This package contains classes used for representing the settings visually.
* visually.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart

View File

@ -9,12 +9,12 @@ import javax.imageio.ImageIO;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import envoy.client.data.Settings;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.client.data.Settings;
/** /**
* Provides static utility methods for loading icons from the resource * Provides static utility methods for loading icons from the resource folder.
* folder.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -34,7 +34,10 @@ public final class IconUtil {
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path) { return cache.computeIfAbsent(path, p -> new Image(IconUtil.class.getResource(p).toExternalForm())); } public static Image load(String path) {
return cache.computeIfAbsent(path,
p -> new Image(IconUtil.class.getResource(p).toExternalForm()));
}
/** /**
* Loads an image from the resource folder and scales it to the given size. * Loads an image from the resource folder and scales it to the given size.
@ -45,12 +48,13 @@ public final class IconUtil {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static Image load(String path, int size) { public static Image load(String path, int size) {
return scaledCache.computeIfAbsent(path + size, p -> new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true)); return scaledCache.computeIfAbsent(path + size,
p -> new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true,
true));
} }
/** /**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the * Loads a {@code .png} image from the sub-folder {@code /icons/} of the resource folder.
* resource folder.
* <p> * <p>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
@ -60,11 +64,13 @@ public final class IconUtil {
* @apiNote let's load a sample image {@code /icons/abc.png}.<br> * @apiNote let's load a sample image {@code /icons/abc.png}.<br>
* To do that, we only have to call {@code IconUtil.loadIcon("abc")} * To do that, we only have to call {@code IconUtil.loadIcon("abc")}
*/ */
public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); } public static Image loadIcon(String name) {
return load("/icons/" + name + ".png");
}
/** /**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the * Loads a {@code .png} image from the sub-folder {@code /icons/} of the resource folder and
* resource folder and scales it to the given size.<br> * scales it to the given size.<br>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
* @param name the image name without the .png suffix * @param name the image name without the .png suffix
@ -72,20 +78,19 @@ public final class IconUtil {
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
* @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br> * @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br>
* To do that, we only have to call * To do that, we only have to call {@code IconUtil.loadIcon("abc", 16)}
* {@code IconUtil.loadIcon("abc", 16)}
*/ */
public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); } public static Image loadIcon(String name, int size) {
return load("/icons/" + name + ".png", size);
}
/** /**
* Loads a {@code .png} image whose design depends on the currently active theme * Loads a {@code .png} image whose design depends on the currently active theme from the
* from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the * sub-folder {@code /icons/dark/} or {@code /icons/light/} of the resource folder.
* resource folder.
* <p> * <p>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
* @param name the image name without the "black" or "white" suffix and without * @param name the image name without the "black" or "white" suffix and without the .png suffix
* the .png suffix
* @return the loaded image * @return the loaded image
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
* @apiNote let's take two sample images {@code /icons/dark/abc.png} and * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
@ -93,12 +98,14 @@ public final class IconUtil {
* To do that theme sensitive, we only have to call * To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc")} * {@code IconUtil.loadIconThemeSensitive("abc")}
*/ */
public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); } public static Image loadIconThemeSensitive(String name) {
return loadIcon(themeSpecificSubFolder() + name);
}
/** /**
* Loads a {@code .png} image whose design depends on the currently active theme * Loads a {@code .png} image whose design depends on the currently active theme from the
* from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the * sub-folder {@code /icons/dark/} or {@code /icons/light/} of the resource folder and scales it
* resource folder and scales it to the given size. * to the given size.
* <p> * <p>
* The suffix {@code .png} is automatically appended. * The suffix {@code .png} is automatically appended.
* *
@ -111,20 +118,19 @@ public final class IconUtil {
* To do that theme sensitive, we only have to call * To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc", 16)} * {@code IconUtil.loadIconThemeSensitive("abc", 16)}
*/ */
public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); } public static Image loadIconThemeSensitive(String name, int size) {
return loadIcon(themeSpecificSubFolder() + name, size);
}
/** /**
* * Loads images specified by an enum. The images have to be named like the lowercase enum
* Loads images specified by an enum. The images have to be named like the * constants with {@code .png} extension and be located inside a folder with the lowercase name
* lowercase enum constants with {@code .png} extension and be located inside a * of the enum, which must be contained inside the {@code /icons/} folder.
* folder with the lowercase name of the enum, which must be contained inside
* the {@code /icons/} folder.
* *
* @param <T> the enum that specifies the images to load * @param <T> the enum that specifies the images to load
* @param enumClass the class of the enum * @param enumClass the class of the enum
* @param size the size to scale the images to * @param size the size to scale the images to
* @return a map containing the loaded images with the corresponding enum * @return a map containing the loaded images with the corresponding enum constants as keys
* constants as keys
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static <T extends Enum<T>> EnumMap<T, Image> loadByEnum(Class<T> enumClass, int size) { public static <T extends Enum<T>> EnumMap<T, Image> loadByEnum(Class<T> enumClass, int size) {
@ -147,22 +153,25 @@ public final class IconUtil {
try { try {
return ImageIO.read(IconUtil.class.getResource(path)); return ImageIO.read(IconUtil.class.getResource(path));
} catch (IOException e) { } catch (IOException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e); EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING,
String.format("Could not load image at path %s: ", path), e);
return null; return null;
} }
}); });
} }
/** /**
* This method should be called if the display of an image depends upon the * This method should be called if the display of an image depends upon the currently active
* currently active theme.<br> * theme.<br>
* In case of a default theme, the string returned will be * In case of a default theme, the string returned will be ({@code dark/} or {@code light/}),
* ({@code dark/} or {@code light/}), otherwise it will be empty. * otherwise it will be empty.
* *
* @return the theme specific folder * @return the theme specific folder
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
private static String themeSpecificSubFolder() { private static String themeSpecificSubFolder() {
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : ""; return Settings.getInstance().isUsingDefaultTheme()
? Settings.getInstance().getCurrentTheme() + "/"
: "";
} }
} }

View File

@ -7,13 +7,14 @@ import java.util.logging.*;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import envoy.client.data.*; import dev.kske.eventbus.EventBus;
import envoy.client.event.MessageDeletion;
import envoy.client.ui.controller.ChatScene;
import envoy.data.Message; import envoy.data.Message;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus; import envoy.client.data.*;
import envoy.client.event.MessageDeletion;
import envoy.client.ui.controller.ChatScene;
/** /**
* Contains methods that are commonly used for {@link Message}s. * Contains methods that are commonly used for {@link Message}s.
@ -34,8 +35,10 @@ public class MessageUtil {
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public static void copyMessageText(Message message) { public static void copyMessageText(Message message) {
logger.log(Level.FINEST, "A copy of message text \"" + message.getText() + "\" was requested"); logger.log(Level.FINEST,
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null); "A copy of message text \"" + message.getText() + "\" was requested");
Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new StringSelection(message.getText()), null);
} }
/** /**
@ -46,8 +49,10 @@ public class MessageUtil {
*/ */
public static void deleteMessage(Message message) { public static void deleteMessage(Message message) {
final var messageDeletionEvent = new MessageDeletion(message.getID()); final var messageDeletionEvent = new MessageDeletion(message.getID());
final var controller = Context.getInstance().getSceneContext().getController(); final var controller =
if (controller instanceof ChatScene) ((ChatScene) controller).clearMessageSelection(); Context.getInstance().getSceneContext().getController();
if (controller instanceof ChatScene)
((ChatScene) controller).clearMessageSelection();
// Removing the message locally // Removing the message locally
EventBus.getInstance().dispatch(messageDeletionEvent); EventBus.getInstance().dispatch(messageDeletionEvent);
@ -56,22 +61,24 @@ public class MessageUtil {
} }
/** /**
* Forwards the given message. * Forwards the given message. Currently not implemented.
* Currently not implemented.
* *
* @param message the message to forward * @param message the message to forward
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public static void forwardMessage(Message message) { logger.log(Level.FINEST, "Message forwarding was requested for " + message); } public static void forwardMessage(Message message) {
logger.log(Level.FINEST, "Message forwarding was requested for " + message);
}
/** /**
* Quotes the given message. * Quotes the given message. Currently not implemented.
* Currently not implemented.
* *
* @param message the message to quote * @param message the message to quote
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public static void quoteMessage(Message message) { logger.log(Level.FINEST, "Message quotation was requested for " + message); } public static void quoteMessage(Message message) {
logger.log(Level.FINEST, "Message quotation was requested for " + message);
}
/** /**
* Saves the attachment of a message, if present. * Saves the attachment of a message, if present.
@ -81,7 +88,8 @@ public class MessageUtil {
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public static void saveAttachment(Message message) { public static void saveAttachment(Message message) {
if (!message.hasAttachment()) throw new IllegalArgumentException("Cannot save a non-existing attachment"); if (!message.hasAttachment())
throw new IllegalArgumentException("Cannot save a non-existing attachment");
File file; File file;
final var fileName = message.getAttachment().getName(); final var fileName = message.getAttachment().getName();
final var downloadLocation = Settings.getInstance().getDownloadLocation(); final var downloadLocation = Settings.getInstance().getDownloadLocation();
@ -92,14 +100,17 @@ public class MessageUtil {
fileChooser.setInitialFileName(fileName); fileChooser.setInitialFileName(fileName);
fileChooser.setInitialDirectory(downloadLocation); fileChooser.setInitialDirectory(downloadLocation);
file = fileChooser.showSaveDialog(Context.getInstance().getSceneContext().getStage()); file = fileChooser.showSaveDialog(Context.getInstance().getSceneContext().getStage());
} else file = new File(downloadLocation, fileName); } else
file = new File(downloadLocation, fileName);
// A file was selected // A file was selected
if (file != null) try (var fos = new FileOutputStream(file)) { if (file != null)
fos.write(message.getAttachment().getData()); try (var fos = new FileOutputStream(file)) {
logger.log(Level.FINE, "Attachment of message was saved at " + file.getAbsolutePath()); fos.write(message.getAttachment().getData());
} catch (final IOException e) { logger.log(Level.FINE,
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e); "Attachment of message was saved at " + file.getAbsolutePath());
} } catch (final IOException e) {
logger.log(Level.WARNING, "Could not save attachment of " + message + ": ", e);
}
} }
} }

View File

@ -14,47 +14,43 @@ public final class ReflectionUtil {
private ReflectionUtil() {} private ReflectionUtil() {}
/** /**
* Gets all declared variable values of the given instance that have the * Gets all declared variable values of the given instance that have the specified class.
* specified class.
* <p> * <p>
* (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a * (i.e. can get all {@code JComponents} (Swing) or {@code Nodes} (JavaFX) in a GUI class).
* GUI class).
* <p> * <p>
* <b>Important: If you are using a module, you first need to declare <br> * <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>. * "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
* *
* @param <T> the type of the object * @param <T> the type of the object
* @param <R> the type to return * @param <R> the type to return
* @param instance the instance of a given class whose values are to be * @param instance the instance of a given class whose values are to be evaluated
* evaluated
* @param typeToReturn the type of variable to return * @param typeToReturn the type of variable to return
* @return all variables in the given instance that have the requested type * @return all variables in the given instance that have the requested type
* @throws RuntimeException if an exception occurs * @throws RuntimeException if an exception occurs
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static <T, R> Stream<R> getAllDeclaredVariablesOfTypeAsStream(T instance, Class<R> typeToReturn) { public static <T, R> Stream<R> getAllDeclaredVariablesOfTypeAsStream(T instance,
return Arrays.stream(instance.getClass().getDeclaredFields()).filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> { Class<R> typeToReturn) {
try { return Arrays.stream(instance.getClass().getDeclaredFields())
field.setAccessible(true); .filter(field -> typeToReturn.isAssignableFrom(field.getType())).map(field -> {
return typeToReturn.cast(field.get(instance)); try {
} catch (IllegalArgumentException | IllegalAccessException e) { field.setAccessible(true);
throw new RuntimeException(e); return typeToReturn.cast(field.get(instance));
} } catch (IllegalArgumentException | IllegalAccessException e) {
}); throw new RuntimeException(e);
}
});
} }
/** /**
* Gets all declared variables of the given instance that are children of * Gets all declared variables of the given instance that are children of {@code Node}.
* {@code Node}.
* <p> * <p>
* <b>Important: If you are using a module, you first need to declare <br> * <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>. * "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
* *
* @param <T> the type of the instance * @param <T> the type of the instance
* @param instance the instance of a given class whose values are to be * @param instance the instance of a given class whose values are to be evaluated
* evaluated * @return all variables of the given object that have the requested type as {@code Stream}
* @return all variables of the given object that have the requested type as
* {@code Stream}
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public static <T> Stream<Node> getAllDeclaredNodeVariablesAsStream(T instance) { public static <T> Stream<Node> getAllDeclaredNodeVariablesAsStream(T instance) {
@ -62,15 +58,13 @@ public final class ReflectionUtil {
} }
/** /**
* Gets all declared variables of the given instance that are children of * Gets all declared variables of the given instance that are children of {@code Node}<br>
* {@code Node}<br>
* <p> * <p>
* <b>Important: If you are using a module, you first need to declare <br> * <b>Important: If you are using a module, you first need to declare <br>
* "opens {your_package} to envoy.client.util;" in your module-info.java</b>. * "opens {your_package} to envoy.client.util;" in your module-info.java</b>.
* *
* @param <T> the type of the instance * @param <T> the type of the instance
* @param instance the instance of a given class whose values are to be * @param instance the instance of a given class whose values are to be evaluated
* evaluated
* @return all variables of the given object that have the requested type * @return all variables of the given object that have the requested type
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */

View File

@ -5,18 +5,19 @@ import java.util.logging.*;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import envoy.client.data.Context; import dev.kske.eventbus.EventBus;
import envoy.client.event.*;
import envoy.client.helper.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.ChatScene;
import envoy.data.*; import envoy.data.*;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.UserOperation; import envoy.event.contact.UserOperation;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus; import envoy.client.data.Context;
import envoy.client.event.*;
import envoy.client.helper.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.ChatScene;
/** /**
* Contains methods that change something about the currently logged in user. * Contains methods that change something about the currently logged in user.
@ -32,8 +33,7 @@ public final class UserUtil {
private UserUtil() {} private UserUtil() {}
/** /**
* Logs the current user out and reopens * Logs the current user out and reopens {@link envoy.client.ui.controller.LoginScene}.
* {@link envoy.client.ui.controller.LoginScene}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
@ -52,8 +52,7 @@ public final class UserUtil {
} }
/** /**
* Notifies the application that the status of the currently logged in user has * Notifies the application that the status of the currently logged in user has changed.
* changed.
* *
* @param newStatus the new status * @param newStatus the new status
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
@ -61,10 +60,13 @@ public final class UserUtil {
public static void changeStatus(UserStatus newStatus) { public static void changeStatus(UserStatus newStatus) {
// Sending the already active status is a valid action // Sending the already active status is a valid action
if (newStatus.equals(context.getLocalDB().getUser().getStatus())) return; if (newStatus.equals(context.getLocalDB().getUser().getStatus()))
return;
else { else {
EventBus.getInstance().dispatch(new OwnStatusChange(newStatus)); EventBus.getInstance().dispatch(new OwnStatusChange(newStatus));
if (context.getClient().isOnline()) context.getClient().send(new UserStatusChange(context.getLocalDB().getUser().getID(), newStatus)); if (context.getClient().isOnline())
context.getClient()
.send(new UserStatusChange(context.getLocalDB().getUser().getID(), newStatus));
logger.log(Level.INFO, "A manual status change occurred."); logger.log(Level.INFO, "A manual status change occurred.");
} }
} }
@ -76,16 +78,20 @@ public final class UserUtil {
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public static void disableContact(Contact block) { public static void disableContact(Contact block) {
if (!context.getClient().isOnline() || block == null) return; if (!context.getClient().isOnline() || block == null)
return;
else { else {
final var alert = new Alert(AlertType.CONFIRMATION); final var alert = new Alert(AlertType.CONFIRMATION);
alert.setContentText("Are you sure you want to " + (block instanceof User ? "block " : "leave group ") + block.getName() + "?"); alert.setContentText("Are you sure you want to "
+ (block instanceof User ? "block " : "leave group ") + block.getName() + "?");
AlertHelper.confirmAction(alert, () -> { AlertHelper.confirmAction(alert, () -> {
final var isUser = block instanceof User; final var isUser = block instanceof User;
context.getClient() context.getClient()
.send(isUser ? new UserOperation((User) block, ElementOperation.REMOVE) .send(isUser ? new UserOperation((User) block, ElementOperation.REMOVE)
: new GroupResize(context.getLocalDB().getUser(), (Group) block, ElementOperation.REMOVE)); : new GroupResize(context.getLocalDB().getUser(), (Group) block,
if (!isUser) block.getContacts().remove(context.getLocalDB().getUser()); ElementOperation.REMOVE));
if (!isUser)
block.getContacts().remove(context.getLocalDB().getUser());
EventBus.getInstance().dispatch(new ContactDisabled(block)); EventBus.getInstance().dispatch(new ContactDisabled(block));
logger.log(Level.INFO, isUser ? "A user was blocked." : "The user left a group."); logger.log(Level.INFO, isUser ? "A user was blocked." : "The user left a group.");
}); });
@ -99,14 +105,16 @@ public final class UserUtil {
* @since Envoy Client v0.3-beta * @since Envoy Client v0.3-beta
*/ */
public static void deleteContact(Contact delete) { public static void deleteContact(Contact delete) {
if (delete == null) return; if (delete == null)
return;
else { else {
final var alert = new Alert(AlertType.CONFIRMATION); final var alert = new Alert(AlertType.CONFIRMATION);
alert.setContentText("Are you sure you want to delete " + delete.getName() alert.setContentText("Are you sure you want to delete " + delete.getName()
+ " entirely? All messages with this contact will be deleted. This action cannot be undone."); + " entirely? All messages with this contact will be deleted. This action cannot be undone.");
AlertHelper.confirmAction(alert, () -> { AlertHelper.confirmAction(alert, () -> {
context.getLocalDB().getUsers().remove(delete.getName()); context.getLocalDB().getUsers().remove(delete.getName());
context.getLocalDB().getChats().removeIf(chat -> chat.getRecipient().equals(delete)); context.getLocalDB().getChats()
.removeIf(chat -> chat.getRecipient().equals(delete));
if (context.getSceneContext().getController() instanceof ChatScene) if (context.getSceneContext().getController() instanceof ChatScene)
((ChatScene) context.getSceneContext().getController()).resetState(); ((ChatScene) context.getSceneContext().getController()).resetState();
logger.log(Level.INFO, "A contact with all his messages was deleted."); logger.log(Level.INFO, "A contact with all his messages was deleted.");

View File

@ -1,6 +1,5 @@
/** /**
* This module contains all classes defining the client application of the Envoy * This module contains all classes defining the client application of the Envoy project.
* project.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister

View File

@ -3,8 +3,8 @@ package envoy.data;
import java.io.Serializable; import java.io.Serializable;
/** /**
* This interface should be used for any type supposed to be a {@link Message} * This interface should be used for any type supposed to be a {@link Message} attachment (i.e.
* attachment (i.e. images or sound). * images or sound).
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart

View File

@ -9,15 +9,13 @@ import java.util.stream.Collectors;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
/** /**
* Manages all application settings that are set during application startup by * Manages all application settings that are set during application startup by either loading them
* either loading them from the {@link Properties} file (default values) * from the {@link Properties} file (default values) {@code client.properties} or parsing them from
* {@code client.properties} or parsing them from the command line arguments of * the command line arguments of the application.
* the application.
* <p> * <p>
* All items inside the {@code Config} are supposed to either be supplied over * All items inside the {@code Config} are supposed to either be supplied over default value or over
* default value or over command line argument. Developers that fail to provide * command line argument. Developers that fail to provide default values will be greeted with an
* default values will be greeted with an error message the next time they try * error message the next time they try to start Envoy...
* to start Envoy...
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
@ -44,15 +42,14 @@ public class Config {
*/ */
private void load(Properties properties) { private void load(Properties properties) {
items.entrySet().stream().filter(e -> properties.containsKey(e.getKey())) items.entrySet().stream().filter(e -> properties.containsKey(e.getKey()))
.forEach(e -> e.getValue().parse(properties.getProperty(e.getKey()))); .forEach(e -> e.getValue().parse(properties.getProperty(e.getKey())));
} }
/** /**
* Parses config items from an array of command line arguments. * Parses config items from an array of command line arguments.
* *
* @param args the command line arguments to parse * @param args the command line arguments to parse
* @throws IllegalStateException if a malformed command line argument has been * @throws IllegalStateException if a malformed command line argument has been supplied
* supplied
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
private void load(String[] args) { private void load(String[] args) {
@ -61,7 +58,7 @@ public class Config {
if (args[i].startsWith("--")) { if (args[i].startsWith("--")) {
if (args[i].length() == 2) if (args[i].length() == 2)
throw new IllegalStateException( throw new IllegalStateException(
"Malformed command line argument at position " + i + ": " + args[i]); "Malformed command line argument at position " + i + ": " + args[i]);
final String commandLong = args[i].substring(2); final String commandLong = args[i].substring(2);
if (item.getCommandLong().equals(commandLong)) { if (item.getCommandLong().equals(commandLong)) {
item.parse(args[++i]); item.parse(args[++i]);
@ -70,7 +67,7 @@ public class Config {
} else if (args[i].startsWith("-")) { } else if (args[i].startsWith("-")) {
if (args[i].length() == 1) if (args[i].length() == 1)
throw new IllegalStateException( throw new IllegalStateException(
"Malformed command line argument at position " + i + ": " + args[i]); "Malformed command line argument at position " + i + ": " + args[i]);
final String commandShort = args[i].substring(1); final String commandShort = args[i].substring(1);
if (item.getCommandShort().equals(commandShort)) { if (item.getCommandShort().equals(commandShort)) {
item.parse(args[++i]); item.parse(args[++i]);
@ -78,35 +75,36 @@ public class Config {
} }
} else } else
throw new IllegalStateException( throw new IllegalStateException(
"Malformed command line argument at position " + i + ": " + args[i]); "Malformed command line argument at position " + i + ": " + args[i]);
} }
/** /**
* Supplies default values from the given .properties file and parses the * Supplies default values from the given .properties file and parses the configuration from an
* configuration from an array of command line arguments. * array of command line arguments.
* *
* @param declaringClass the class calling this method * @param declaringClass the class calling this method
* @param propertiesFilePath the path to where the .properties file can be found * @param propertiesFilePath the path to where the .properties file can be found - will be only
* - will be only the file name if it is located * the file name if it is located directly inside the
* directly inside the {@code src/main/resources} * {@code src/main/resources} folder
* folder
* @param args the command line arguments to parse * @param args the command line arguments to parse
* @throws IllegalStateException if this method is getting called again or if a * @throws IllegalStateException if this method is getting called again or if a malformed
* malformed command line argument has been * command line argument has been supplied
* supplied
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public void loadAll(Class<?> declaringClass, String propertiesFilePath, String[] args) { public void loadAll(Class<?> declaringClass, String propertiesFilePath, String[] args) {
if (modificationDisabled) if (modificationDisabled)
throw new IllegalStateException("Cannot change config after isInitialized has been called"); throw new IllegalStateException(
"Cannot change config after isInitialized has been called");
// Load the defaults from the given .properties file first // Load the defaults from the given .properties file first
final var properties = new Properties(); final var properties = new Properties();
try { try {
properties.load(declaringClass.getClassLoader().getResourceAsStream(propertiesFilePath)); properties
.load(declaringClass.getClassLoader().getResourceAsStream(propertiesFilePath));
} catch (final IOException e) { } catch (final IOException e) {
EnvoyLog.getLogger(Config.class).log(Level.SEVERE, "An error occurred when reading in the configuration: ", EnvoyLog.getLogger(Config.class).log(Level.SEVERE,
e); "An error occurred when reading in the configuration: ",
e);
} }
load(properties); load(properties);
@ -122,13 +120,13 @@ public class Config {
} }
/** /**
* @throws IllegalStateException if a {@link ConfigItem} has not been * @throws IllegalStateException if a {@link ConfigItem} has not been initialized
* initialized
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
private void isInitialized() { private void isInitialized() {
String uninitialized = items.values().stream().filter(c -> c.get() == null).map(ConfigItem::getCommandLong).collect(Collectors.joining(", ")); String uninitialized = items.values().stream().filter(c -> c.get() == null)
if(!uninitialized.isEmpty()) .map(ConfigItem::getCommandLong).collect(Collectors.joining(", "));
if (!uninitialized.isEmpty())
throw new IllegalStateException("Config items uninitialized: " + uninitialized); throw new IllegalStateException("Config items uninitialized: " + uninitialized);
} }
@ -148,11 +146,11 @@ public class Config {
* @param <T> the type of the {@link ConfigItem} * @param <T> the type of the {@link ConfigItem}
* @param commandName the key for this config item as well as its long name * @param commandName the key for this config item as well as its long name
* @param commandShort the abbreviation of this config item * @param commandShort the abbreviation of this config item
* @param parseFunction the {@code Function<String, T>} that parses the value * @param parseFunction the {@code Function<String, T>} that parses the value from a string
* from a string
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
protected <T> void put(String commandName, String commandShort, Function<String, T> parseFunction) { protected <T> void put(String commandName, String commandShort,
Function<String, T> parseFunction) {
items.put(commandName, new ConfigItem<>(commandName, commandShort, parseFunction)); items.put(commandName, new ConfigItem<>(commandName, commandShort, parseFunction));
} }
@ -160,17 +158,13 @@ public class Config {
* @return the directory in which all local files are saves * @return the directory in which all local files are saves
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public File getHomeDirectory() { public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
return (File) items.get("homeDirectory").get();
}
/** /**
* @return the minimal {@link Level} to log inside the log file * @return the minimal {@link Level} to log inside the log file
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public Level getFileLevelBarrier() { public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
return (Level) items.get("fileLevelBarrier").get();
}
/** /**
* @return the minimal {@link Level} to log inside the console * @return the minimal {@link Level} to log inside the console

View File

@ -3,8 +3,8 @@ package envoy.data;
import java.util.function.Function; import java.util.function.Function;
/** /**
* Contains a single {@link Config} value as well as the corresponding command * Contains a single {@link Config} value as well as the corresponding command line arguments and
* line arguments and its default value. * its default value.
* <p> * <p>
* All {@code ConfigItem}s are automatically mandatory. * All {@code ConfigItem}s are automatically mandatory.
* *
@ -24,8 +24,7 @@ public final class ConfigItem<T> {
* *
* @param commandLong the long command line argument to set this value * @param commandLong the long command line argument to set this value
* @param commandShort the short command line argument to set this value * @param commandShort the short command line argument to set this value
* @param parseFunction the {@code Function<String, T>} that parses the value * @param parseFunction the {@code Function<String, T>} that parses the value from a string
* from a string
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction) { public ConfigItem(String commandLong, String commandShort, Function<String, T> parseFunction) {
@ -40,18 +39,18 @@ public final class ConfigItem<T> {
* @param input the string to parse from * @param input the string to parse from
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public void parse(String input) { value = parseFunction.apply(input); } public void parse(String input) {
value = parseFunction.apply(input);
}
/** /**
* @return The long command line argument to set the value of this * @return The long command line argument to set the value of this {@link ConfigItem}
* {@link ConfigItem}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public String getCommandLong() { return commandLong; } public String getCommandLong() { return commandLong; }
/** /**
* @return The short command line argument to set the value of this * @return The short command line argument to set the value of this {@link ConfigItem}
* {@link ConfigItem}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public String getCommandShort() { return commandShort; } public String getCommandShort() { return commandShort; }
@ -60,7 +59,9 @@ public final class ConfigItem<T> {
* @return the value of this {@link ConfigItem} * @return the value of this {@link ConfigItem}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public T get() { return value; } public T get() {
return value;
}
/** /**
* @param value the value to set * @param value the value to set

View File

@ -53,23 +53,27 @@ public abstract class Contact implements Serializable {
/** /**
* Provides a hash code based on the ID of this contact. * Provides a hash code based on the ID of this contact.
* *
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
@Override @Override
public final int hashCode() { return Objects.hash(id); } public final int hashCode() {
return Objects.hash(id);
}
/** /**
* Tests equality to another object. If that object is a contact as well, * Tests equality to another object. If that object is a contact as well, equality is determined
* equality is determined by the ID. * by the ID.
* *
* @param obj the object to test for equality to this contact * @param obj the object to test for equality to this contact
* @return {code true} if both objects are contacts and have identical IDs * @return {code true} if both objects are contacts and have identical IDs
*/ */
@Override @Override
public final boolean equals(Object obj) { public final boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (!(obj instanceof Contact)) return false; return true;
if (!(obj instanceof Contact))
return false;
return id == ((Contact) obj).id; return id == ((Contact) obj).id;
} }

View File

@ -18,7 +18,9 @@ public final class Group extends Contact {
* @param name the name of this group * @param name the name of this group
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Group(long id, String name) { this(id, name, new HashSet<User>()); } public Group(long id, String name) {
this(id, name, new HashSet<User>());
}
/** /**
* Creates an instance of a {@link Group}. * Creates an instance of a {@link Group}.
@ -28,10 +30,14 @@ public final class Group extends Contact {
* @param members all members that should be preinitialized * @param members all members that should be preinitialized
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Group(long id, String name, Set<User> members) { super(id, name, members); } public Group(long id, String name, Set<User> members) {
super(id, name, members);
}
@Override @Override
public String toString() { return String.format("Group[id=%d,name=%s,%d member(s)]", id, name, contacts.size()); } public String toString() {
return String.format("Group[id=%d,name=%s,%d member(s)]", id, name, contacts.size());
}
private void readObject(ObjectInputStream inputStream) throws Exception { private void readObject(ObjectInputStream inputStream) throws Exception {
inputStream.defaultReadObject(); inputStream.defaultReadObject();

View File

@ -14,11 +14,9 @@ public final class GroupMessage extends Message {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Initializes a {@link GroupMessage} with values for all of its properties. The * Initializes a {@link GroupMessage} with values for all of its properties. The use of this
* use * constructor is only intended for the {@link MessageBuilder} class, as this class provides
* of this constructor is only intended for the {@link MessageBuilder} class, as * {@code null} checks and default values for all properties.
* this class provides {@code null} checks and default values for all
* properties.
* *
* @param id unique ID * @param id unique ID
* @param senderID the ID of the user who sends the message * @param senderID the ID of the user who sends the message
@ -28,16 +26,18 @@ public final class GroupMessage extends Message {
* @param readDate the read date of the message * @param readDate the read date of the message
* @param text the text content of the message * @param text the text content of the message
* @param attachment the attachment of the message, if present * @param attachment the attachment of the message, if present
* @param status the current {@link Message.MessageStatus} of the * @param status the current {@link Message.MessageStatus} of the message
* message
* @param forwarded whether this message was forwarded * @param forwarded whether this message was forwarded
* @param memberStatuses a map of all members and their status according to this * @param memberStatuses a map of all members and their status according to this
* {@link GroupMessage} * {@link GroupMessage}
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
GroupMessage(long id, long senderID, long groupID, Instant creationDate, Instant receivedDate, Instant readDate, String text, GroupMessage(long id, long senderID, long groupID, Instant creationDate, Instant receivedDate,
Attachment attachment, MessageStatus status, boolean forwarded, Map<Long, MessageStatus> memberStatuses) { Instant readDate, String text,
super(id, senderID, groupID, creationDate, receivedDate, readDate, text, attachment, status, forwarded); Attachment attachment, MessageStatus status, boolean forwarded,
Map<Long, MessageStatus> memberStatuses) {
super(id, senderID, groupID, creationDate, receivedDate, readDate, text, attachment, status,
forwarded);
this.memberStatuses = memberStatuses; this.memberStatuses = memberStatuses;
} }

View File

@ -30,20 +30,25 @@ public final class IDGenerator implements IEvent, Serializable {
} }
@Override @Override
public String toString() { return String.format("IDGenerator[current=%d,end=%d]", current, end); } public String toString() {
return String.format("IDGenerator[current=%d,end=%d]", current, end);
}
/** /**
* @return {@code true} if there are unused IDs remaining * @return {@code true} if there are unused IDs remaining
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public boolean hasNext() { return current < end; } public boolean hasNext() {
return current < end;
}
/** /**
* @return the next ID * @return the next ID
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public long next() { public long next() {
if (!hasNext()) throw new IllegalStateException("All IDs have been used"); if (!hasNext())
throw new IllegalStateException("All IDs have been used");
return current++; return current++;
} }
} }

View File

@ -4,11 +4,9 @@ import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
/** /**
* Contains a {@link User}'s login / registration information as well as the * Contains a {@link User}'s login / registration information as well as the client version.
* client version.
* <p> * <p>
* If the authentication is performed with a token, the token is stored instead * If the authentication is performed with a token, the token is stored instead of the password.
* of the password.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
@ -21,8 +19,9 @@ public final class LoginCredentials implements Serializable {
private static final long serialVersionUID = 4; private static final long serialVersionUID = 4;
private LoginCredentials(String identifier, String password, boolean registration, boolean token, boolean requestToken, String clientVersion, private LoginCredentials(String identifier, String password, boolean registration,
Instant lastSync) { boolean token, boolean requestToken, String clientVersion,
Instant lastSync) {
this.identifier = identifier; this.identifier = identifier;
this.password = password; this.password = password;
this.registration = registration; this.registration = registration;
@ -43,8 +42,10 @@ public final class LoginCredentials implements Serializable {
* @return the created login credentials * @return the created login credentials
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials login(String identifier, String password, boolean requestToken, String clientVersion, Instant lastSync) { public static LoginCredentials login(String identifier, String password, boolean requestToken,
return new LoginCredentials(identifier, password, false, false, requestToken, clientVersion, lastSync); String clientVersion, Instant lastSync) {
return new LoginCredentials(identifier, password, false, false, requestToken, clientVersion,
lastSync);
} }
/** /**
@ -57,7 +58,8 @@ public final class LoginCredentials implements Serializable {
* @return the created login credentials * @return the created login credentials
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials loginWithToken(String identifier, String token, String clientVersion, Instant lastSync) { public static LoginCredentials loginWithToken(String identifier, String token,
String clientVersion, Instant lastSync) {
return new LoginCredentials(identifier, token, false, true, false, clientVersion, lastSync); return new LoginCredentials(identifier, token, false, true, false, clientVersion, lastSync);
} }
@ -72,19 +74,22 @@ public final class LoginCredentials implements Serializable {
* @return the created login credentials * @return the created login credentials
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials registration(String identifier, String password, boolean requestToken, String clientVersion, Instant lastSync) { public static LoginCredentials registration(String identifier, String password,
return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion, lastSync); boolean requestToken, String clientVersion, Instant lastSync) {
return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion,
lastSync);
} }
@Override @Override
public String toString() { public String toString() {
return String.format("LoginCredentials[identifier=%s,registration=%b,token=%b,requestToken=%b,clientVersion=%s,lastSync=%s]", return String.format(
identifier, "LoginCredentials[identifier=%s,registration=%b,token=%b,requestToken=%b,clientVersion=%s,lastSync=%s]",
registration, identifier,
token, registration,
requestToken, token,
clientVersion, requestToken,
lastSync); clientVersion,
lastSync);
} }
/** /**
@ -100,24 +105,27 @@ public final class LoginCredentials implements Serializable {
public String getPassword() { return password; } public String getPassword() { return password; }
/** /**
* @return {@code true} if these credentials are used for user registration * @return {@code true} if these credentials are used for user registration instead of user
* instead of user login * login
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public boolean isRegistration() { return registration; } public boolean isRegistration() { return registration; }
/** /**
* @return {@code true} if these credentials use an authentication token instead * @return {@code true} if these credentials use an authentication token instead of a password
* of a password
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public boolean usesToken() { return token; } public boolean usesToken() {
return token;
}
/** /**
* @return {@code true} if the server should generate a new authentication token * @return {@code true} if the server should generate a new authentication token
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public boolean requestToken() { return requestToken; } public boolean requestToken() {
return requestToken;
}
/** /**
* @return the version of the client sending these credentials * @return the version of the client sending these credentials

View File

@ -6,9 +6,8 @@ import java.time.Instant;
import dev.kske.eventbus.IEvent; import dev.kske.eventbus.IEvent;
/** /**
* Represents a unique message with a unique, numeric ID. Further metadata * Represents a unique message with a unique, numeric ID. Further metadata includes the sender and
* includes the sender and recipient {@link User}s, as well as the creation * recipient {@link User}s, as well as the creation date and the current {@link MessageStatus}.<br>
* date and the current {@link MessageStatus}.<br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister
@ -56,10 +55,9 @@ public class Message implements Serializable, IEvent {
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
/** /**
* Initializes a {@link Message} with values for all of its properties. The use * Initializes a {@link Message} with values for all of its properties. The use of this
* of this constructor is only intended for the {@link MessageBuilder} class, as * constructor is only intended for the {@link MessageBuilder} class, as this class provides
* this class provides {@code null} checks and default values for all * {@code null} checks and default values for all properties.
* properties.
* *
* @param id unique ID * @param id unique ID
* @param senderID the ID of the user who sends the message * @param senderID the ID of the user who sends the message
@ -73,8 +71,9 @@ public class Message implements Serializable, IEvent {
* @param forwarded whether this message was forwarded * @param forwarded whether this message was forwarded
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
Message(long id, long senderID, long recipientID, Instant creationDate, Instant receivedDate, Instant readDate, String text, Message(long id, long senderID, long recipientID, Instant creationDate, Instant receivedDate,
Attachment attachment, MessageStatus status, boolean forwarded) { Instant readDate, String text,
Attachment attachment, MessageStatus status, boolean forwarded) {
this.id = id; this.id = id;
this.senderID = senderID; this.senderID = senderID;
this.recipientID = recipientID; this.recipientID = recipientID;
@ -101,21 +100,23 @@ public class Message implements Serializable, IEvent {
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public void nextStatus() { public void nextStatus() {
if (status == MessageStatus.READ) throw new IllegalStateException("Message status READ is already reached"); if (status == MessageStatus.READ)
throw new IllegalStateException("Message status READ is already reached");
status = MessageStatus.values()[status.ordinal() + 1]; status = MessageStatus.values()[status.ordinal() + 1];
} }
@Override @Override
public String toString() { public String toString() {
return String.format("Message[id=%d,sender=%s,recipient=%s,date=%s,status=%s,text=%s,forwarded=%b,hasAttachment=%b]", return String.format(
id, "Message[id=%d,sender=%s,recipient=%s,date=%s,status=%s,text=%s,forwarded=%b,hasAttachment=%b]",
senderID, id,
recipientID, senderID,
creationDate, recipientID,
status, creationDate,
text, status,
forwarded, text,
attachment != null); forwarded,
attachment != null);
} }
/** /**
@ -149,8 +150,7 @@ public class Message implements Serializable, IEvent {
public Instant getReceivedDate() { return receivedDate; } public Instant getReceivedDate() { return receivedDate; }
/** /**
* @param receivedDate the date at which the message has been received by the * @param receivedDate the date at which the message has been received by the sender
* sender
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public void setReceivedDate(Instant receivedDate) { this.receivedDate = receivedDate; } public void setReceivedDate(Instant receivedDate) { this.receivedDate = receivedDate; }
@ -183,7 +183,9 @@ public class Message implements Serializable, IEvent {
* @return {@code true} if an attachment is present * @return {@code true} if an attachment is present
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public boolean hasAttachment() { return attachment != null; } public boolean hasAttachment() {
return attachment != null;
}
/** /**
* @return the current status of this message * @return the current status of this message
@ -196,7 +198,8 @@ public class Message implements Serializable, IEvent {
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public void setStatus(MessageStatus status) { public void setStatus(MessageStatus status) {
if (status.ordinal() < this.status.ordinal()) throw new IllegalStateException("This message is moving backwards in time"); if (status.ordinal() < this.status.ordinal())
throw new IllegalStateException("This message is moving backwards in time");
this.status = status; this.status = status;
} }

View File

@ -25,20 +25,21 @@ public final class MessageBuilder {
private boolean forwarded; private boolean forwarded;
/** /**
* Creates an instance of {@link MessageBuilder} with all mandatory values * Creates an instance of {@link MessageBuilder} with all mandatory values without defaults for
* without defaults for the {@link Message} class. * the {@link Message} class.
* *
* @param senderID the ID of the user who sends the {@link Message} * @param senderID the ID of the user who sends the {@link Message}
* @param recipientID the ID of the user who receives the {@link Message} * @param recipientID the ID of the user who receives the {@link Message}
* @param idGenerator the ID generator used to generate a unique {@link Message} * @param idGenerator the ID generator used to generate a unique {@link Message} id
* id
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public MessageBuilder(long senderID, long recipientID, IDGenerator idGenerator) { this(senderID, recipientID, idGenerator.next()); } public MessageBuilder(long senderID, long recipientID, IDGenerator idGenerator) {
this(senderID, recipientID, idGenerator.next());
}
/** /**
* Creates an instance of {@link MessageBuilder} with all mandatory values * Creates an instance of {@link MessageBuilder} with all mandatory values without defaults for
* without defaults for the {@link Message} class. * the {@link Message} class.
* *
* @param senderID the ID of the user who sends the {@link Message} * @param senderID the ID of the user who sends the {@link Message}
* @param recipientID the ID of the user who receives the {@link Message} * @param recipientID the ID of the user who receives the {@link Message}
@ -52,14 +53,12 @@ public final class MessageBuilder {
} }
/** /**
* This constructor transforms a given {@link Message} into a new message for a * This constructor transforms a given {@link Message} into a new message for a new receiver.
* new receiver.
* This makes it especially useful in the case of forwarding messages. * This makes it especially useful in the case of forwarding messages.
* *
* @param msg the message to copy * @param msg the message to copy
* @param recipientID the ID of the user who receives the {@link Message} * @param recipientID the ID of the user who receives the {@link Message}
* @param iDGenerator the ID generator used to generate a unique {@link Message} * @param iDGenerator the ID generator used to generate a unique {@link Message} id
* id
* @since Envoy v0.1-beta * @since Envoy v0.1-beta
*/ */
public MessageBuilder(Message msg, long recipientID, IDGenerator iDGenerator) { public MessageBuilder(Message msg, long recipientID, IDGenerator iDGenerator) {
@ -72,79 +71,69 @@ public final class MessageBuilder {
} }
/** /**
* Creates an instance of {@link Message} with the previously supplied values. * Creates an instance of {@link Message} with the previously supplied values. If a mandatory
* If a mandatory value is not set, a default value will be used instead:<br> * value is not set, a default value will be used instead:<br>
* <br> * <br>
* {@code date} * {@code date} {@code Instant.now()} and {@code null} for {@code receivedDate} and
* {@code Instant.now()} and {@code null} for {@code receivedDate} and * {@code readDate} <br>
* {@code readDate} * {@code text} {@code ""} <br>
* <br> * {@code status} {@code MessageStatus.WAITING}
* {@code text}
* {@code ""}
* <br>
* {@code status}
* {@code MessageStatus.WAITING}
* *
* @return a new instance of {@link Message} * @return a new instance of {@link Message}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public Message build() { public Message build() {
supplyDefaults(); supplyDefaults();
return new Message(id, senderID, recipientID, creationDate, receivedDate, readDate, text, attachment, status, forwarded); return new Message(id, senderID, recipientID, creationDate, receivedDate, readDate, text,
attachment, status, forwarded);
} }
/** /**
* Creates an instance of {@link GroupMessage} with the previously supplied * Creates an instance of {@link GroupMessage} with the previously supplied values. <br>
* values. <br>
* <b> Sets all member statuses to {@link MessageStatus#WAITING}.</b><br> * <b> Sets all member statuses to {@link MessageStatus#WAITING}.</b><br>
* If a mandatory value is not set, a default value will be used * If a mandatory value is not set, a default value will be used instead:<br>
* instead:<br>
* <br>
* {@code time stamp}
* {@code Instant.now()}
* <br>
* {@code text}
* {@code ""}
* <br> * <br>
* {@code time stamp} {@code Instant.now()} <br>
* {@code text} {@code ""} <br>
* *
* @param group the {@link Group} that is used to fill the map of member * @param group the {@link Group} that is used to fill the map of member statuses
* statuses
* @return a new instance of {@link GroupMessage} * @return a new instance of {@link GroupMessage}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public GroupMessage buildGroupMessage(Group group) { public GroupMessage buildGroupMessage(Group group) {
final var memberStatuses = new HashMap<Long, Message.MessageStatus>(); final var memberStatuses = new HashMap<Long, Message.MessageStatus>();
group.getContacts().forEach(user -> memberStatuses.put(user.getID(), MessageStatus.WAITING)); group.getContacts()
.forEach(user -> memberStatuses.put(user.getID(), MessageStatus.WAITING));
return buildGroupMessage(group, memberStatuses); return buildGroupMessage(group, memberStatuses);
} }
/** /**
* Creates an instance of {@link GroupMessage} with the previously supplied * Creates an instance of {@link GroupMessage} with the previously supplied values. If a
* values. If a mandatory value is not set, a default value will be used * mandatory value is not set, a default value will be used instead:<br>
* instead:<br>
* <br> * <br>
* {@code time stamp} * {@code time stamp} {@code Instant.now()} <br>
* {@code Instant.now()} * {@code text} {@code ""}
* <br>
* {@code text}
* {@code ""}
* *
* @param group the {@link Group} that is used to fill the map of * @param group the {@link Group} that is used to fill the map of member statuses
* member statuses
* @param memberStatuses the map of all current statuses * @param memberStatuses the map of all current statuses
* @return a new instance of {@link GroupMessage} * @return a new instance of {@link GroupMessage}
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public GroupMessage buildGroupMessage(Group group, Map<Long, MessageStatus> memberStatuses) { public GroupMessage buildGroupMessage(Group group, Map<Long, MessageStatus> memberStatuses) {
if (group == null || memberStatuses == null) throw new NullPointerException(); if (group == null || memberStatuses == null)
throw new NullPointerException();
supplyDefaults(); supplyDefaults();
return new GroupMessage(id, senderID, recipientID, creationDate, receivedDate, readDate, text, attachment, status, forwarded, memberStatuses); return new GroupMessage(id, senderID, recipientID, creationDate, receivedDate, readDate,
text, attachment, status, forwarded, memberStatuses);
} }
private void supplyDefaults() { private void supplyDefaults() {
if (creationDate == null) creationDate = Instant.now(); if (creationDate == null)
if (text == null) text = ""; creationDate = Instant.now();
if (status == null) status = MessageStatus.WAITING; if (text == null)
text = "";
if (status == null)
status = MessageStatus.WAITING;
} }
/** /**
@ -188,8 +177,7 @@ public final class MessageBuilder {
} }
/** /**
* @param attachment the {@link Attachment} of the {@link Message} to * @param attachment the {@link Attachment} of the {@link Message} to create
* create
* @return this {@link MessageBuilder} * @return this {@link MessageBuilder}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */

View File

@ -4,8 +4,7 @@ import java.io.*;
import java.util.*; import java.util.*;
/** /**
* Represents a unique user with a unique, numeric ID, a name and a current * Represents a unique user with a unique, numeric ID, a name and a current {@link UserStatus}.<br>
* {@link UserStatus}.<br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
@ -34,8 +33,7 @@ public final class User extends Contact {
ONLINE, ONLINE,
/** /**
* select this, if a user is online but unavailable at the moment (sudden * select this, if a user is online but unavailable at the moment (sudden interruption)
* interruption)
*/ */
AWAY, AWAY,
@ -52,8 +50,7 @@ public final class User extends Contact {
/** /**
* Initializes a {@link User}. <br> * Initializes a {@link User}. <br>
* The {@link UserStatus} is set to {@link UserStatus#ONLINE}. * The {@link UserStatus} is set to {@link UserStatus#ONLINE}. No contacts are initialized.
* No contacts are initialized.
* *
* @param id unique ID * @param id unique ID
* @param name user name * @param name user name
@ -94,7 +91,8 @@ public final class User extends Contact {
@Override @Override
public String toString() { public String toString() {
return String.format("User[id=%d,name=%s,status=%s", id, name, status) + (contacts.isEmpty() ? "]" : "," + contacts.size() + " contact(s)]"); return String.format("User[id=%d,name=%s,status=%s", id, name, status)
+ (contacts.isEmpty() ? "]" : "," + contacts.size() + " contact(s)]");
} }
/** /**
@ -119,15 +117,18 @@ public final class User extends Contact {
private void writeObject(ObjectOutputStream outputStream) throws Exception { private void writeObject(ObjectOutputStream outputStream) throws Exception {
outputStream.defaultWriteObject(); outputStream.defaultWriteObject();
if (serializeContacts) { if (serializeContacts) {
getContacts().stream().filter(User.class::isInstance).map(User.class::cast).forEach(user -> user.serializeContacts = false); getContacts().stream().filter(User.class::isInstance).map(User.class::cast)
.forEach(user -> user.serializeContacts = false);
outputStream.writeObject(getContacts()); outputStream.writeObject(getContacts());
} else outputStream.writeObject(new HashSet<>()); } else
outputStream.writeObject(new HashSet<>());
} }
/** /**
* @param serializeContacts whether the contacts of this {@link User} should be * @param serializeContacts whether the contacts of this {@link User} should be serialized
* serialized
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public void serializeContacts(boolean serializeContacts) { this.serializeContacts = serializeContacts; } public void serializeContacts(boolean serializeContacts) {
this.serializeContacts = serializeContacts;
}
} }

View File

@ -1,6 +1,5 @@
/** /**
* This package contains all data objects that are used both by Envoy Client and * This package contains all data objects that are used both by Envoy Client and by Envoy Server.
* by Envoy Server.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer

View File

@ -3,8 +3,7 @@ package envoy.event;
/** /**
* This enum declares all modification possibilities for a given container. * This enum declares all modification possibilities for a given container.
* <p> * <p>
* These can be: {@link ElementOperation#ADD} or * These can be: {@link ElementOperation#ADD} or {@link ElementOperation#REMOVE}.
* {@link ElementOperation#REMOVE}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
@ -12,14 +11,12 @@ package envoy.event;
public enum ElementOperation { public enum ElementOperation {
/** /**
* Select this element, if the given element should be added to the given * Select this element, if the given element should be added to the given container.
* container.
*/ */
ADD, ADD,
/** /**
* Select this element, if the given element should be removed from the given * Select this element, if the given element should be removed from the given container.
* container.
*/ */
REMOVE REMOVE
} }

View File

@ -5,9 +5,9 @@ import java.io.Serializable;
import dev.kske.eventbus.IEvent; import dev.kske.eventbus.IEvent;
/** /**
* This class serves as a convenience base class for all events. It implements * This class serves as a convenience base class for all events. It implements the {@link IEvent}
* the {@link IEvent} interface and provides a generic value. For events without * interface and provides a generic value. For events without a value there also is
* a value there also is {@link envoy.event.Event.Valueless}. * {@link envoy.event.Event.Valueless}.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @param <T> the type of the Event * @param <T> the type of the Event
@ -19,15 +19,21 @@ public abstract class Event<T> implements IEvent, Serializable {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
protected Event(T value) { this.value = value; } protected Event(T value) {
this.value = value;
}
/** /**
* @return the data associated with this event * @return the data associated with this event
*/ */
public T get() { return value; } public T get() {
return value;
}
@Override @Override
public String toString() { return String.format("%s[value=%s]", this.getClass().getSimpleName(), value); } public String toString() {
return String.format("%s[value=%s]", this.getClass().getSimpleName(), value);
}
/** /**
* Serves as a super class for events that do not carry a value. * Serves as a super class for events that do not carry a value.
@ -39,9 +45,13 @@ public abstract class Event<T> implements IEvent, Serializable {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
protected Valueless() { super(null); } protected Valueless() {
super(null);
}
@Override @Override
public String toString() { return this.getClass().getSimpleName(); } public String toString() {
return this.getClass().getSimpleName();
}
} }
} }

View File

@ -18,9 +18,8 @@ public final class GroupCreation extends Event<String> {
/** /**
* @param name the name of this group at creation time * @param name the name of this group at creation time
* @param initialMemberIDs the IDs of all {@link User}s that should be group * @param initialMemberIDs the IDs of all {@link User}s that should be group members from the
* members from the beginning on (excluding the creator * beginning on (excluding the creator of this group)
* of this group)
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public GroupCreation(String name, Set<Long> initialMemberIDs) { public GroupCreation(String name, Set<Long> initialMemberIDs) {
@ -29,8 +28,8 @@ public final class GroupCreation extends Event<String> {
} }
/** /**
* @return the IDs of all {@link User}s that are members from the beginning * @return the IDs of all {@link User}s that are members from the beginning (excluding the
* (excluding the creator of this group) * creator of this group)
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Set<Long> getInitialMemberIDs() { return initialMemberIDs; } public Set<Long> getInitialMemberIDs() { return initialMemberIDs; }

View File

@ -3,8 +3,8 @@ package envoy.event;
import envoy.data.Group; import envoy.data.Group;
/** /**
* Used to communicate with a client that his request to create a group might * Used to communicate with a client that his request to create a group might have been rejected as
* have been rejected as it might be disabled on his current server. * it might be disabled on his current server.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
@ -19,7 +19,9 @@ public class GroupCreationResult extends Event<Group> {
* *
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public GroupCreationResult() { super(null); } public GroupCreationResult() {
super(null);
}
/** /**
* Creates a new {@code GroupCreationResult}. * Creates a new {@code GroupCreationResult}.
@ -27,5 +29,7 @@ public class GroupCreationResult extends Event<Group> {
* @param resultGroup the group the server created * @param resultGroup the group the server created
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public GroupCreationResult(Group resultGroup) { super(resultGroup); } public GroupCreationResult(Group resultGroup) {
super(resultGroup);
}
} }

View File

@ -17,11 +17,10 @@ public final class GroupMessageStatusChange extends MessageStatusChange {
/** /**
* Initializes a {@link GroupMessageStatusChange}. * Initializes a {@link GroupMessageStatusChange}.
* *
* @param id the ID of the {@link GroupMessage} this event is related to * @param id the ID of the {@link GroupMessage} this event is related to
* @param status the status of this specific members {@link GroupMessage} * @param status the status of this specific members {@link GroupMessage}
* @param date the date at which the MessageStatus change occurred for * @param date the date at which the MessageStatus change occurred for this specific member
* this specific member
* @param memberID the ID of the group member that caused the status change * @param memberID the ID of the group member that caused the status change
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
@ -37,5 +36,8 @@ public final class GroupMessageStatusChange extends MessageStatusChange {
public long getMemberID() { return memberID; } public long getMemberID() { return memberID; }
@Override @Override
public String toString() { return String.format("GroupMessageStatusChange[meta=%s,memberID=%d]", super.toString(), memberID); } public String toString() {
return String.format("GroupMessageStatusChange[meta=%s,memberID=%d]", super.toString(),
memberID);
}
} }

View File

@ -5,11 +5,9 @@ import static envoy.event.ElementOperation.*;
import envoy.data.*; import envoy.data.*;
/** /**
* This event is used to communicate changes in the group size between client * This event is used to communicate changes in the group size between client and server.
* and server.
* <p> * <p>
* Possible actions are adding or removing certain {@link User}s to or from a * Possible actions are adding or removing certain {@link User}s to or from a certain {@link Group}.
* certain {@link Group}.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
@ -22,8 +20,7 @@ public final class GroupResize extends Event<User> {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
/** /**
* Initializes a {@link GroupResize} through a Contact where the name has * Initializes a {@link GroupResize} through a Contact where the name has already been set.
* already been set.
* *
* @param user the {@link User} who wants to join or leave a group * @param user the {@link User} who wants to join or leave a group
* @param group the {@link Group} he wants to join or leave * @param group the {@link Group} he wants to join or leave
@ -37,7 +34,9 @@ public final class GroupResize extends Event<User> {
final var contained = group.getContacts().contains(user); final var contained = group.getContacts().contains(user);
if (contained && operation.equals(ADD)) if (contained && operation.equals(ADD))
throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group)); throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group));
else if (operation.equals(REMOVE) && !contained) throw new IllegalArgumentException(String.format("Cannot remove %s from %s!", user, group)); else if (operation.equals(REMOVE) && !contained)
throw new IllegalArgumentException(
String.format("Cannot remove %s from %s!", user, group));
groupID = group.getID(); groupID = group.getID();
} }
@ -71,5 +70,8 @@ public final class GroupResize extends Event<User> {
} }
@Override @Override
public String toString() { return String.format("GroupResize[user=%s,groupid=%d,operation=%s]", get(), groupID, operation); } public String toString() {
return String.format("GroupResize[user=%s,groupid=%d,operation=%s]", get(), groupID,
operation);
}
} }

View File

@ -1,8 +1,7 @@
package envoy.event; package envoy.event;
/** /**
* Signifies to the client that the handshake failed for the attached * Signifies to the client that the handshake failed for the attached reason.
* reason.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.3-alpha * @since Envoy Common v0.3-alpha
@ -24,8 +23,7 @@ public final class HandshakeRejection extends Event<String> {
public static final String USERNAME_TAKEN = "Incorrect user name or password."; public static final String USERNAME_TAKEN = "Incorrect user name or password.";
/** /**
* Select this value if the version of the client is incompatible with the * Select this value if the version of the client is incompatible with the server.
* server.
* *
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
@ -39,8 +37,7 @@ public final class HandshakeRejection extends Event<String> {
public static final String INVALID_TOKEN = "Invalid authentication token"; public static final String INVALID_TOKEN = "Invalid authentication token";
/** /**
* Select this value if the handshake could not be completed for some different * Select this value if the handshake could not be completed for some different reason.
* reason.
* *
* @since Envoy Common v0.3-alpha * @since Envoy Common v0.3-alpha
*/ */
@ -54,7 +51,9 @@ public final class HandshakeRejection extends Event<String> {
* *
* @since Envoy Common v0.3-alpha * @since Envoy Common v0.3-alpha
*/ */
public HandshakeRejection() { super(INTERNAL_ERROR); } public HandshakeRejection() {
super(INTERNAL_ERROR);
}
/** /**
* Creates an instance of {@link HandshakeRejection}. * Creates an instance of {@link HandshakeRejection}.
@ -62,5 +61,7 @@ public final class HandshakeRejection extends Event<String> {
* @param reason the reason why the handshake was rejected * @param reason the reason why the handshake was rejected
* @since Envoy Common v0.3-alpha * @since Envoy Common v0.3-alpha
*/ */
public HandshakeRejection(String reason) { super(reason); } public HandshakeRejection(String reason) {
super(reason);
}
} }

View File

@ -1,8 +1,7 @@
package envoy.event; package envoy.event;
/** /**
* Signifies to the server that the client needs a new * Signifies to the server that the client needs a new {@link envoy.data.IDGenerator} instance.
* {@link envoy.data.IDGenerator} instance.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Common v0.3-alpha * @since Envoy Common v0.3-alpha

View File

@ -1,8 +1,7 @@
package envoy.event; package envoy.event;
/** /**
* This event should be sent when a user is currently typing something in a * This event should be sent when a user is currently typing something in a chat.
* chat.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta

View File

@ -1,8 +1,8 @@
package envoy.event; package envoy.event;
/** /**
* This class allows envoy users to send an issue proposal to the server who, if * This class allows envoy users to send an issue proposal to the server who, if not disabled by its
* not disabled by its administrator, will forward it directly to Gitea. * administrator, will forward it directly to Gitea.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
@ -17,9 +17,8 @@ public final class IssueProposal extends Event<String> {
/** /**
* @param title the title of the reported bug * @param title the title of the reported bug
* @param description the description of this bug * @param description the description of this bug
* @param isBug determines whether this {@code IssueProposal} is * @param isBug determines whether this {@code IssueProposal} is supposed to be a feature
* supposed to be a * or a bug (true = bug, false = feature)
* feature or a bug (true = bug, false = feature)
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public IssueProposal(String title, String description, boolean isBug) { public IssueProposal(String title, String description, boolean isBug) {
@ -32,14 +31,14 @@ public final class IssueProposal extends Event<String> {
* @param title the title of the reported bug * @param title the title of the reported bug
* @param description the description of this bug * @param description the description of this bug
* @param user the name of the user creating the issue * @param user the name of the user creating the issue
* @param isBug determines whether this {@code IssueProposal} is * @param isBug determines whether this {@code IssueProposal} is supposed to be a feature
* supposed to be a * or a bug (true = bug, false = feature)
* feature or a bug (true = bug, false = feature)
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public IssueProposal(String title, String description, String user, boolean isBug) { public IssueProposal(String title, String description, String user, boolean isBug) {
super(escape(title)); super(escape(title));
this.description = sanitizeDescription(description) + String.format("<br>Submitted by user %s.", user); this.description =
sanitizeDescription(description) + String.format("<br>Submitted by user %s.", user);
bug = isBug; bug = isBug;
} }
@ -61,7 +60,9 @@ public final class IssueProposal extends Event<String> {
* @return the escaped string * @return the escaped string
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private static String escape(String raw) { return raw.replace("\\", "\\\\").replace("\"", "\\\""); } private static String escape(String raw) {
return raw.replace("\\", "\\\\").replace("\"", "\\\"");
}
/** /**
* @return the description * @return the description
@ -70,8 +71,7 @@ public final class IssueProposal extends Event<String> {
public String getDescription() { return description; } public String getDescription() { return description; }
/** /**
* @return whether this issue is supposed to be a bug - otherwise it is intended * @return whether this issue is supposed to be a bug - otherwise it is intended as a feature
* as a feature
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public boolean isBug() { return bug; } public boolean isBug() { return bug; }

View File

@ -19,8 +19,7 @@ public class MessageStatusChange extends Event<Message.MessageStatus> {
* Initializes a {@link MessageStatusChange}. * Initializes a {@link MessageStatusChange}.
* *
* @param id the ID of the {@link Message} this event is related to * @param id the ID of the {@link Message} this event is related to
* @param status the status of the {@link Message} this event is related * @param status the status of the {@link Message} this event is related to
* to
* @param date the date at which the MessageStatus change occurred * @param date the date at which the MessageStatus change occurred
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
@ -36,7 +35,9 @@ public class MessageStatusChange extends Event<Message.MessageStatus> {
* @param message the message from which to build the event * @param message the message from which to build the event
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public MessageStatusChange(Message message) { this(message.getID(), message.getStatus(), Instant.now()); } public MessageStatusChange(Message message) {
this(message.getID(), message.getStatus(), Instant.now());
}
/** /**
* @return the ID of the {@link Message} this event is related to * @return the ID of the {@link Message} this event is related to
@ -51,5 +52,7 @@ public class MessageStatusChange extends Event<Message.MessageStatus> {
public Instant getDate() { return date; } public Instant getDate() { return date; }
@Override @Override
public String toString() { return String.format("MessageStatusChange[id=%d,status=%s,date=%s]", id, value, date); } public String toString() {
return String.format("MessageStatusChange[id=%d,status=%s,date=%s]", id, value, date);
}
} }

View File

@ -5,8 +5,7 @@ import envoy.data.Contact;
/** /**
* This event informs * This event informs
* <p> * <p>
* a) the server of the name change of a user or a group. * a) the server of the name change of a user or a group. b) another user of this users name change.
* b) another user of this users name change.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
@ -15,7 +14,7 @@ public final class NameChange extends Event<String> {
private final long id; private final long id;
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
/** /**
* Creates a new {@link NameChange} for a user or a group. * Creates a new {@link NameChange} for a user or a group.
@ -30,13 +29,14 @@ public final class NameChange extends Event<String> {
} }
/** /**
* Initializes a {@link NameChange} through a Contact where the name has * Initializes a {@link NameChange} through a Contact where the name has already been set.
* already been set.
* *
* @param contact the contact whose name was updated * @param contact the contact whose name was updated
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public NameChange(Contact contact) { this(contact.getID(), contact.getName()); } public NameChange(Contact contact) {
this(contact.getID(), contact.getName());
}
/** /**
* @return the ID of the {@link Contact} this event is related to * @return the ID of the {@link Contact} this event is related to
@ -45,5 +45,7 @@ public final class NameChange extends Event<String> {
public long getID() { return id; } public long getID() { return id; }
@Override @Override
public String toString() { return String.format("NameChange[id=%d,name=%s]", id, value); } public String toString() {
return String.format("NameChange[id=%d,name=%s]", id, value);
}
} }

View File

@ -21,5 +21,7 @@ public class NewAuthToken extends Event<String> {
} }
@Override @Override
public String toString() { return "NewAuthToken"; } public String toString() {
return "NewAuthToken";
}
} }

View File

@ -1,8 +1,7 @@
package envoy.event; package envoy.event;
/** /**
* This event is used so that the server can tell the client that attachments * This event is used so that the server can tell the client that attachments will be filtered out.
* will be filtered out.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta

View File

@ -38,5 +38,7 @@ public final class PasswordChangeRequest extends Event<String> {
public String getOldPassword() { return oldPassword; } public String getOldPassword() { return oldPassword; }
@Override @Override
public String toString() { return "PasswordChangeRequest[id=" + id + "]"; } public String toString() {
return "PasswordChangeRequest[id=" + id + "]";
}
} }

View File

@ -1,8 +1,8 @@
package envoy.event; package envoy.event;
/** /**
* This class acts as a notice to the user whether his * This class acts as a notice to the user whether his {@link envoy.event.PasswordChangeRequest} was
* {@link envoy.event.PasswordChangeRequest} was successful. * successful.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
@ -14,9 +14,10 @@ public final class PasswordChangeResult extends Event<Boolean> {
/** /**
* Creates an instance of {@code PasswordChangeResult}. * Creates an instance of {@code PasswordChangeResult}.
* *
* @param value whether the preceding {@link envoy.event.PasswordChangeRequest} * @param value whether the preceding {@link envoy.event.PasswordChangeRequest} was successful.
* was successful.
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public PasswordChangeResult(boolean value) { super(value); } public PasswordChangeResult(boolean value) {
super(value);
}
} }

View File

@ -17,8 +17,7 @@ public final class UserStatusChange extends Event<UserStatus> {
* Initializes a {@link UserStatusChange}. * Initializes a {@link UserStatusChange}.
* *
* @param id the ID of the {@link User} this event is related to * @param id the ID of the {@link User} this event is related to
* @param status the status of the {@link User} this event is related * @param status the status of the {@link User} this event is related to
* to
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public UserStatusChange(long id, User.UserStatus status) { public UserStatusChange(long id, User.UserStatus status) {
@ -32,7 +31,9 @@ public final class UserStatusChange extends Event<UserStatus> {
* @param user the User from which to build the event * @param user the User from which to build the event
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public UserStatusChange(User user) { this(user.getID(), user.getStatus()); } public UserStatusChange(User user) {
this(user.getID(), user.getStatus());
}
/** /**
* @return the ID of the {@link User} this event is related to * @return the ID of the {@link User} this event is related to
@ -41,5 +42,7 @@ public final class UserStatusChange extends Event<UserStatus> {
public long getID() { return id; } public long getID() { return id; }
@Override @Override
public String toString() { return String.format("UserStatusChange[id=%d,status=%s]", id, value); } public String toString() {
return String.format("UserStatusChange[id=%d,status=%s]", id, value);
}
} }

View File

@ -3,8 +3,8 @@ package envoy.event.contact;
import envoy.event.Event.Valueless; import envoy.event.Event.Valueless;
/** /**
* Conveys that either a direct contact or a group member has been deleted while * Conveys that either a direct contact or a group member has been deleted while the user has been
* the user has been offline. * offline.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Common v0.3-beta * @since Envoy Common v0.3-beta

View File

@ -34,5 +34,8 @@ public final class UserOperation extends Event<User> {
public ElementOperation getOperationType() { return operationType; } public ElementOperation getOperationType() { return operationType; }
@Override @Override
public String toString() { return String.format("%s[contact=%s, operation=%s]", UserOperation.class.getSimpleName(), value, operationType); } public String toString() {
return String.format("%s[contact=%s, operation=%s]", UserOperation.class.getSimpleName(),
value, operationType);
}
} }

View File

@ -18,5 +18,7 @@ public final class UserSearchRequest extends Event<String> {
* @param searchPhrase the search phrase to use in the user search * @param searchPhrase the search phrase to use in the user search
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public UserSearchRequest(String searchPhrase) { super(searchPhrase); } public UserSearchRequest(String searchPhrase) {
super(searchPhrase);
}
} }

View File

@ -21,5 +21,7 @@ public final class UserSearchResult extends Event<List<User>> {
* @param users the users found during the search * @param users the users found during the search
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public UserSearchResult(List<User> users) { super(users); } public UserSearchResult(List<User> users) {
super(users);
}
} }

View File

@ -1,6 +1,6 @@
/** /**
* This package contains all events that can be sent or received by Envoy Client * This package contains all events that can be sent or received by Envoy Client or Envoy Server
* or Envoy Server Standalone. * Standalone.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer

View File

@ -12,20 +12,24 @@ public final class EnvoyException extends Exception {
* @param message the message to display once this Exception is thrown * @param message the message to display once this Exception is thrown
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public EnvoyException(String message) { super(message); } public EnvoyException(String message) {
super(message);
}
/** /**
* @param message the message to display once this Exception is thrown * @param message the message to display once this Exception is thrown
* @param cause the {@link Throwable} which resulted in the throw of an * @param cause the {@link Throwable} which resulted in the throw of an EnvoyException
* EnvoyException
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public EnvoyException(String message, Throwable cause) { super(message, cause); } public EnvoyException(String message, Throwable cause) {
super(message, cause);
}
/** /**
* @param cause the {@link Throwable} which resulted in the throw of an * @param cause the {@link Throwable} which resulted in the throw of an EnvoyException
* EnvoyException
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public EnvoyException(Throwable cause) { super(cause); } public EnvoyException(Throwable cause) {
super(cause);
}
} }

View File

@ -26,13 +26,16 @@ public final class Bounds {
* @return {@code true} if the given contact name is valid * @return {@code true} if the given contact name is valid
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public static boolean isValidContactName(String contactName) { return CONTACT_NAME_PATTERN.matcher(contactName).matches(); } public static boolean isValidContactName(String contactName) {
return CONTACT_NAME_PATTERN.matcher(contactName).matches();
}
/** /**
* @return the maximum size allowed for a user/ group name. * @return the maximum size allowed for a user/ group name.
* @apiNote has to be updated manually if {@link Bounds#CONTACT_NAME_PATTERN} * @apiNote has to be updated manually if {@link Bounds#CONTACT_NAME_PATTERN} gets updated.
* gets updated.
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public static int maximumUsernameSize() { return 16; } public static int maximumUsernameSize() {
return 16;
}
} }

View File

@ -8,8 +8,7 @@ import java.util.logging.*;
import envoy.data.Config; import envoy.data.Config;
/** /**
* Configures the {@link java.util.logging} API to output the log into the * Configures the {@link java.util.logging} API to output the log into the console and a log file.
* console and a log file.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -30,20 +29,23 @@ public final class EnvoyLog {
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public static void initialize(Config config) { public static void initialize(Config config) {
if (initialized) throw new IllegalStateException("EnvoyLog is already initialized"); if (initialized)
throw new IllegalStateException("EnvoyLog is already initialized");
// Remove default console handler // Remove default console handler
LogManager.getLogManager().reset(); LogManager.getLogManager().reset();
// Configure log file // Configure log file
final File logFile = new File(config.getHomeDirectory(), final File logFile = new File(config.getHomeDirectory(),
"log/" + DateTimeFormatter.ofPattern("yyyy-MM-dd--hh-mm").format(LocalDateTime.now()) + ".log"); "log/" + DateTimeFormatter.ofPattern("yyyy-MM-dd--hh-mm").format(LocalDateTime.now())
+ ".log");
logFile.getParentFile().mkdirs(); logFile.getParentFile().mkdirs();
// Configure formatting // Configure formatting
// Sample log entry: [2020-06-13 16:50:26] [INFORMATION] // Sample log entry: [2020-06-13 16:50:26] [INFORMATION]
// [envoy.client.ui.Startup] Closing connection... // [envoy.client.ui.Startup] Closing connection...
System.setProperty("java.util.logging.SimpleFormatter.format", "[%1$tF %1$tT] [%4$-7s] [%3$s] %5$s %6$s%n"); System.setProperty("java.util.logging.SimpleFormatter.format",
"[%1$tF %1$tT] [%4$-7s] [%3$s] %5$s %6$s%n");
final SimpleFormatter formatter = new SimpleFormatter(); final SimpleFormatter formatter = new SimpleFormatter();
try { try {
@ -69,20 +71,22 @@ public final class EnvoyLog {
} }
/** /**
* Configures all loggers that are contained within the hierarchy of a specific * Configures all loggers that are contained within the hierarchy of a specific path to use the
* path to use the console and file handlers. * console and file handlers.
* *
* @param path the path to the loggers to configure * @param path the path to the loggers to configure
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
private static void attach(String path) { private static void attach(String path) {
if (!initialized) throw new IllegalStateException("EnvoyLog is not initialized"); if (!initialized)
throw new IllegalStateException("EnvoyLog is not initialized");
// Get root logger // Get root logger
final Logger logger = Logger.getLogger(path); final Logger logger = Logger.getLogger(path);
// Add handlers // Add handlers
if (fileHandler != null) logger.addHandler(fileHandler); if (fileHandler != null)
logger.addHandler(fileHandler);
logger.addHandler(consoleHandler); logger.addHandler(consoleHandler);
// Delegate logger level filtering to the handlers // Delegate logger level filtering to the handlers
@ -90,12 +94,14 @@ public final class EnvoyLog {
} }
/** /**
* Creates a logger for a specified class, which output will be displayed inside * Creates a logger for a specified class, which output will be displayed inside the console and
* the console and written to the log file. * written to the log file.
* *
* @param logClass the class in which the logger is used * @param logClass the class in which the logger is used
* @return the created logger * @return the created logger
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public static Logger getLogger(Class<?> logClass) { return Logger.getLogger(logClass.getCanonicalName()); } public static Logger getLogger(Class<?> logClass) {
return Logger.getLogger(logClass.getCanonicalName());
}
} }

View File

@ -19,7 +19,9 @@ public final class SerializationUtils {
* @return a byte array of length 4 * @return a byte array of length 4
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public static byte[] intToBytes(int n) { return new byte[] { (byte) (n >>> 24), (byte) (n >>> 16), (byte) (n >>> 8), (byte) n }; } public static byte[] intToBytes(int n) {
return new byte[] { (byte) (n >>> 24), (byte) (n >>> 16), (byte) (n >>> 8), (byte) n };
}
/** /**
* Converts four bytes in byte array to an integer * Converts four bytes in byte array to an integer
@ -30,8 +32,9 @@ public final class SerializationUtils {
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public static int bytesToInt(byte[] bytes, int offset) { public static int bytesToInt(byte[] bytes, int offset) {
return (bytes[offset] & 0xFF) << 24 | (bytes[offset + 1] & 0xFF) << 16 | (bytes[offset + 2] & 0xFF) << 8 return (bytes[offset] & 0xFF) << 24 | (bytes[offset + 1] & 0xFF) << 16
| (bytes[offset + 3] & 0xFF) << 0; | (bytes[offset + 2] & 0xFF) << 8
| (bytes[offset + 3] & 0xFF) << 0;
} }
/** /**
@ -41,14 +44,14 @@ public final class SerializationUtils {
* @param file the file to deserialize from * @param file the file to deserialize from
* @param serializedClass the class of the object to deserialize * @param serializedClass the class of the object to deserialize
* @return the deserialized object * @return the deserialized object
* @throws IOException if something failed while deserializing the * @throws IOException if something failed while deserializing the object
* object * @throws ClassNotFoundException if the deserialized object can not be linked to a class
* @throws ClassNotFoundException if the deserialized object can not be linked
* to a class
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public static <T extends Serializable> T read(File file, Class<T> serializedClass) throws IOException, ClassNotFoundException { public static <T extends Serializable> T read(File file, Class<T> serializedClass)
if (file == null) throw new NullPointerException("File is null"); throws IOException, ClassNotFoundException {
if (file == null)
throw new NullPointerException("File is null");
return read(new FileInputStream(file), serializedClass); return read(new FileInputStream(file), serializedClass);
} }
@ -59,13 +62,12 @@ public final class SerializationUtils {
* @param bytes the array in which the serialized object is stored * @param bytes the array in which the serialized object is stored
* @param serializedClass the class of the serialized object * @param serializedClass the class of the serialized object
* @return the deserialized object * @return the deserialized object
* @throws IOException if something failed while deserializing the * @throws IOException if something failed while deserializing the object
* object * @throws ClassNotFoundException if the deserialized object can not be linked to a class
* @throws ClassNotFoundException if the deserialized object can not be linked
* to a class
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public static <T extends Serializable> T read(byte[] bytes, Class<T> serializedClass) throws IOException, ClassNotFoundException { public static <T extends Serializable> T read(byte[] bytes, Class<T> serializedClass)
throws IOException, ClassNotFoundException {
return read(new ByteArrayInputStream(bytes), serializedClass); return read(new ByteArrayInputStream(bytes), serializedClass);
} }
@ -74,16 +76,14 @@ public final class SerializationUtils {
* *
* @param <T> the type of the serialized object * @param <T> the type of the serialized object
* @param in the {@link InputStream} of a serialized Object * @param in the {@link InputStream} of a serialized Object
* @param serializedClass the object type to convert the deserialized object * @param serializedClass the object type to convert the deserialized object into
* into
* @return the deserialized object * @return the deserialized object
* @throws IOException if something failed while deserializing the * @throws IOException if something failed while deserializing the object
* object * @throws ClassNotFoundException if the deserialized object can not be linked to a class
* @throws ClassNotFoundException if the deserialized object can not be linked
* to a class
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public static <T extends Serializable> T read(InputStream in, Class<T> serializedClass) throws IOException, ClassNotFoundException { public static <T extends Serializable> T read(InputStream in, Class<T> serializedClass)
throws IOException, ClassNotFoundException {
try (ObjectInputStream oin = new ObjectInputStream(in)) { try (ObjectInputStream oin = new ObjectInputStream(in)) {
return serializedClass.cast(oin.readObject()); return serializedClass.cast(oin.readObject());
} }
@ -98,8 +98,10 @@ public final class SerializationUtils {
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
*/ */
public static void write(File file, Object... objs) throws IOException { public static void write(File file, Object... objs) throws IOException {
if (file == null) throw new NullPointerException("File is null"); if (file == null)
if (objs == null) throw new NullPointerException("Null array passed to serialize"); throw new NullPointerException("File is null");
if (objs == null)
throw new NullPointerException("Null array passed to serialize");
if (!file.exists()) { if (!file.exists()) {
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
@ -127,8 +129,8 @@ public final class SerializationUtils {
} }
/** /**
* Serializes an object and writes it into an output stream preceded by 4 bytes * Serializes an object and writes it into an output stream preceded by 4 bytes containing the
* containing the number of serialized bytes. * number of serialized bytes.
* *
* @param obj the object to serialize * @param obj the object to serialize
* @param out the output stream to serialize to * @param out the output stream to serialize to

View File

@ -1,7 +1,6 @@
/** /**
* This package contains general useful classes that can be used by both Envoy * This package contains general useful classes that can be used by both Envoy Client and Envoy
* Client and Envoy Server Standalone and that could not be assigned to any * Server Standalone and that could not be assigned to any other package.
* other package.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer

View File

@ -1,6 +1,6 @@
/** /**
* This module contains all packages that are used by Envoy Client and Envoy * This module contains all packages that are used by Envoy Client and Envoy Server Standalone at
* Server Standalone at the same time. * the same time.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer

View File

@ -26,7 +26,8 @@ class UserTest {
User user3 = new User(3, "ai"); User user3 = new User(3, "ai");
User user4 = new User(4, "ki", Set.of(user2, user3)); User user4 = new User(4, "ki", Set.of(user2, user3));
User user5 = new User(5, "ka", Set.of(user2, user3, user4)); User user5 = new User(5, "ka", Set.of(user2, user3, user4));
User user = new User(1, "maxi", UserStatus.AWAY, Set.of(user2, user3, user4, user5)); User user =
new User(1, "maxi", UserStatus.AWAY, Set.of(user2, user3, user4, user5));
var serializedUser = SerializationUtils.writeToByteArray(user); var serializedUser = SerializationUtils.writeToByteArray(user);
var deserializedUser = SerializationUtils.read(serializedUser, User.class); var deserializedUser = SerializationUtils.read(serializedUser, User.class);
assertEquals(user.getContacts(), deserializedUser.getContacts()); assertEquals(user.getContacts(), deserializedUser.getContacts());

View File

@ -6,10 +6,11 @@ import java.util.logging.Level;
import com.jenkov.nioserver.Server; import com.jenkov.nioserver.Server;
import envoy.util.EnvoyLog;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.server.processors.*; import envoy.server.processors.*;
import envoy.util.EnvoyLog;
/** /**
* Starts the server. * Starts the server.
@ -27,8 +28,8 @@ public final class Startup {
/** /**
* Starts the server. * Starts the server.
* *
* @param args the run configuration. If it is "no-enter-to-stop" at position 0, * @param args the run configuration. If it is "no-enter-to-stop" at position 0, no command to
* no command to read in an enter press will be generated * read in an enter press will be generated
* @throws IOException if the server crashes * @throws IOException if the server crashes
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
@ -37,32 +38,34 @@ public final class Startup {
config.loadAll(Startup.class, "server.properties", args); config.loadAll(Startup.class, "server.properties", args);
EnvoyLog.initialize(config); EnvoyLog.initialize(config);
} catch (final IllegalStateException e) { } catch (final IllegalStateException e) {
EnvoyLog.getLogger(Startup.class).log(Level.SEVERE, "Error loading configuration values: ", e); EnvoyLog.getLogger(Startup.class).log(Level.SEVERE,
"Error loading configuration values: ", e);
System.exit(1); System.exit(1);
} }
final var server = new Server(8080, ObjectMessageReader::new, final var server = new Server(8080, ObjectMessageReader::new,
new ObjectMessageProcessor(Set.of(new LoginCredentialProcessor(), new ObjectMessageProcessor(Set.of(new LoginCredentialProcessor(),
new MessageProcessor(), new MessageProcessor(),
new GroupMessageProcessor(), new GroupMessageProcessor(),
new GroupCreationProcessor(), new GroupCreationProcessor(),
new MessageStatusChangeProcessor(), new MessageStatusChangeProcessor(),
new GroupMessageStatusChangeProcessor(), new GroupMessageStatusChangeProcessor(),
new UserStatusChangeProcessor(), new UserStatusChangeProcessor(),
new GroupResizeProcessor(), new GroupResizeProcessor(),
new IDGeneratorRequestProcessor(), new IDGeneratorRequestProcessor(),
new UserSearchProcessor(), new UserSearchProcessor(),
new UserOperationProcessor(), new UserOperationProcessor(),
new IsTypingProcessor(), new IsTypingProcessor(),
new NameChangeProcessor(), new NameChangeProcessor(),
new ProfilePicChangeProcessor(), new ProfilePicChangeProcessor(),
new PasswordChangeRequestProcessor(), new PasswordChangeRequestProcessor(),
new IssueProposalProcessor()))); new IssueProposalProcessor())));
// Initialize the current message ID // Initialize the current message ID
final var persistenceManager = PersistenceManager.getInstance(); final var persistenceManager = PersistenceManager.getInstance();
if (persistenceManager.getConfigItemByID("currentMessageId") == null) if (persistenceManager.getConfigItemByID("currentMessageId") == null)
persistenceManager.addConfigItem(new envoy.server.data.ConfigItem("currentMessageId", "0")); persistenceManager
.addConfigItem(new envoy.server.data.ConfigItem("currentMessageId", "0"));
server.start(); server.start();
server.getSocketProcessor().registerSocketIdListener(ConnectionManager.getInstance()); server.getSocketProcessor().registerSocketIdListener(ConnectionManager.getInstance());

View File

@ -6,8 +6,7 @@ import java.util.Set;
import javax.persistence.*; import javax.persistence.*;
/** /**
* This class acts as a superclass for all contacts, being {@link User}s and * This class acts as a superclass for all contacts, being {@link User}s and {@link Group}s.
* {@link Group}s.
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
@ -30,18 +29,16 @@ public abstract class Contact {
protected Set<Contact> contacts; protected Set<Contact> contacts;
/** /**
* @return a {@link envoy.data.Contact} object of this envoy.server.data.Contact * @return a {@link envoy.data.Contact} object of this envoy.server.data.Contact object.
* object.
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public abstract envoy.data.Contact toCommon(); public abstract envoy.data.Contact toCommon();
/** /**
* Transforms this contact into a {@link envoy.data.Contact} where the contacts * Transforms this contact into a {@link envoy.data.Contact} where the contacts set of contacts
* set of contacts is empty. * is empty.
* *
* @return a {@link envoy.data.Contact} object of this contact * @return a {@link envoy.data.Contact} object of this contact object.
* object.
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
protected abstract envoy.data.Contact toFlatCommon(); protected abstract envoy.data.Contact toFlatCommon();
@ -99,5 +96,8 @@ public abstract class Contact {
public void setCreationDate(Instant creationDate) { this.creationDate = creationDate; } public void setCreationDate(Instant creationDate) { this.creationDate = creationDate; }
@Override @Override
public String toString() { return String.format("%s[id=%d,name=%s,%d contact(s)]", getClass().getSimpleName(), id, name, contacts.size()); } public String toString() {
return String.format("%s[id=%d,name=%s,%d contact(s)]", getClass().getSimpleName(), id,
name, contacts.size());
}
} }

View File

@ -5,22 +5,16 @@ import java.util.stream.Collectors;
import javax.persistence.*; import javax.persistence.*;
/** /**
* Represents a group inside the database. Referred to as "server group" as * Represents a group inside the database. Referred to as "server group" as opposed to "group" from
* opposed to "group" from Envoy Common. * Envoy Common.
* *
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
@Entity @Entity
@NamedQueries({ @NamedQueries({
@NamedQuery( @NamedQuery(name = Group.findByName, query = "SELECT g FROM Group g WHERE g.name = :name"),
name = Group.findByName, @NamedQuery(name = Group.findPendingGroups, query = "SELECT g FROM Group g WHERE g.creationDate > :lastSeen AND :user MEMBER OF g.contacts")
query = "SELECT g FROM Group g WHERE g.name = :name"
),
@NamedQuery(
name = Group.findPendingGroups,
query = "SELECT g FROM Group g WHERE g.creationDate > :lastSeen AND :user MEMBER OF g.contacts"
)
}) })
public final class Group extends Contact { public final class Group extends Contact {
@ -32,7 +26,8 @@ public final class Group extends Contact {
public static final String findByName = "Group.findByName"; public static final String findByName = "Group.findByName";
/** /**
* Named query retrieving all pending groups for a specific user (parameter {@code :user}, {@code :lastSeen}). * Named query retrieving all pending groups for a specific user (parameter {@code :user},
* {@code :lastSeen}).
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -40,9 +35,12 @@ public final class Group extends Contact {
@Override @Override
public envoy.data.Group toCommon() { public envoy.data.Group toCommon() {
return new envoy.data.Group(id, name, contacts.parallelStream().map(User.class::cast).map(User::toFlatCommon).collect(Collectors.toSet())); return new envoy.data.Group(id, name, contacts.parallelStream().map(User.class::cast)
.map(User::toFlatCommon).collect(Collectors.toSet()));
} }
@Override @Override
protected envoy.data.Group toFlatCommon() { return toCommon(); } protected envoy.data.Group toFlatCommon() {
return toCommon();
}
} }

View File

@ -12,20 +12,17 @@ import envoy.data.Group;
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@Entity @Entity
@NamedQuery( @NamedQuery(name = GroupMessage.getPendingGroupMsg, query = "SELECT m FROM GroupMessage m JOIN m.memberMessageStatus s WHERE KEY(s) = :userId AND (m.creationDate > :lastSeen "
name = GroupMessage.getPendingGroupMsg, + "OR m.status = envoy.data.Message$MessageStatus.RECEIVED AND m.receivedDate > :lastSeen "
query = "SELECT m FROM GroupMessage m JOIN m.memberMessageStatus s WHERE KEY(s) = :userId AND (m.creationDate > :lastSeen " + "OR m.status = envoy.data.Message$MessageStatus.READ AND m.readDate > :lastSeen "
+ "OR m.status = envoy.data.Message$MessageStatus.RECEIVED AND m.receivedDate > :lastSeen " + "OR m.lastStatusChangeDate > :lastSeen)")
+ "OR m.status = envoy.data.Message$MessageStatus.READ AND m.readDate > :lastSeen "
+ "OR m.lastStatusChangeDate > :lastSeen)"
)
public final class GroupMessage extends Message { public final class GroupMessage extends Message {
/** /**
* Named query retrieving pending group messages sent to a group containing a * Named query retrieving pending group messages sent to a group containing a specific user
* specific user (parameter {@code userId}) that were sent after a certain time * (parameter {@code userId}) that were sent after a certain time stamp (parameter
* stamp (parameter {@code :lastSeen}). * {@code :lastSeen}).
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public static final String getPendingGroupMsg = "GroupMessage.getPendingGroupMsg"; public static final String getPendingGroupMsg = "GroupMessage.getPendingGroupMsg";
@ -46,9 +43,8 @@ public final class GroupMessage extends Message {
/** /**
* Constructs a database groupMessage from a common groupMessage. * Constructs a database groupMessage from a common groupMessage.
* *
* @param groupMessage the {@link envoy.data.GroupMessage} to convert * @param groupMessage the {@link envoy.data.GroupMessage} to convert into a database
* into a * {@link GroupMessage}
* database {@link GroupMessage}
* @param lastStatusChangeDate the time stamp to set * @param lastStatusChangeDate the time stamp to set
* @since Envoy Server Standalone v0.2-beta * @since Envoy Server Standalone v0.2-beta
*/ */
@ -59,29 +55,31 @@ public final class GroupMessage extends Message {
} }
/** /**
* Converts this groupMessage into an instance of * Converts this groupMessage into an instance of {@link envoy.data.GroupMessage}.
* {@link envoy.data.GroupMessage}.
* *
* @return a {@link envoy.data.GroupMessage} containing the same values as this * @return a {@link envoy.data.GroupMessage} containing the same values as this groupMessage
* groupMessage
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@Override @Override
public envoy.data.GroupMessage toCommon() { public envoy.data.GroupMessage toCommon() {
return prepareBuilder().buildGroupMessage((Group) recipient.toCommon(), new HashMap<>(memberMessageStatus)); return prepareBuilder().buildGroupMessage((Group) recipient.toCommon(),
new HashMap<>(memberMessageStatus));
} }
/** /**
* @return the memberMessageStatus * @return the memberMessageStatus
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public Map<Long, envoy.data.Message.MessageStatus> getMemberMessageStatus() { return memberMessageStatus; } public Map<Long, envoy.data.Message.MessageStatus> getMemberMessageStatus() {
return memberMessageStatus;
}
/** /**
* @param memberMessageStatus the memberMessageStatus to set * @param memberMessageStatus the memberMessageStatus to set
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public void setMemberMessageStatus(Map<Long, envoy.data.Message.MessageStatus> memberMessageStatus) { public void setMemberMessageStatus(
Map<Long, envoy.data.Message.MessageStatus> memberMessageStatus) {
this.memberMessageStatus = memberMessageStatus; this.memberMessageStatus = memberMessageStatus;
} }

View File

@ -12,14 +12,14 @@ import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
/** /**
* This JPA entity, which will be referred to as database message, stores the * This JPA entity, which will be referred to as database message, stores the information contained
* information contained inside a {@link envoy.data.Message} inside the * inside a {@link envoy.data.Message} inside the database, while having a slightly different data
* database, while having a slightly different data layout. * layout.
* <p> * <p>
* A message can be converted to a database message by using the * A message can be converted to a database message by using the
* {@link Message#Message(envoy.data.Message)} constructor. A database message * {@link Message#Message(envoy.data.Message)} constructor. A database message can be converted to a
* can be converted to a regular message using the {@link Message#toCommon()} * regular message using the {@link Message#toCommon()} method. In both cases, the objects will not
* method. In both cases, the objects will not contain references to each other. * contain references to each other.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
@ -27,22 +27,19 @@ import envoy.data.Message.MessageStatus;
@Entity @Entity
@Table(name = "messages") @Table(name = "messages")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@NamedQuery( @NamedQuery(name = Message.getPending, query = "SELECT m FROM Message m WHERE "
name = Message.getPending, // Send to or by the user before last seen
query = "SELECT m FROM Message m WHERE " + "(m.sender = :user OR m.recipient = :user) AND m.creationDate > :lastSeen "
// Send to or by the user before last seen // SENT to the user
+ "(m.sender = :user OR m.recipient = :user) AND m.creationDate > :lastSeen " + "OR m.recipient = :user AND m.status = envoy.data.Message$MessageStatus.SENT "
// SENT to the user // Sent by the user and RECEIVED / READ after last seen
+ "OR m.recipient = :user AND m.status = envoy.data.Message$MessageStatus.SENT " + "OR m.sender = :user AND (m.status = envoy.data.Message$MessageStatus.RECEIVED AND m.receivedDate > :lastSeen "
// Sent by the user and RECEIVED / READ after last seen + "OR m.status = envoy.data.Message$MessageStatus.READ AND m.readDate > :lastSeen)")
+ "OR m.sender = :user AND (m.status = envoy.data.Message$MessageStatus.RECEIVED AND m.receivedDate > :lastSeen "
+ "OR m.status = envoy.data.Message$MessageStatus.READ AND m.readDate > :lastSeen)"
)
public class Message { public class Message {
/** /**
* Named query retrieving pending messages for a user (parameter {@code :user}) * Named query retrieving pending messages for a user (parameter {@code :user}) which was last
* which was last seen after a specific date (parameter {@code :lastSeen}). * seen after a specific date (parameter {@code :lastSeen}).
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -85,8 +82,7 @@ public class Message {
/** /**
* Constructs a database message from a common message. * Constructs a database message from a common message.
* *
* @param message the {@link envoy.data.Message} to convert into a database * @param message the {@link envoy.data.Message} to convert into a database {@link Message}
* {@link Message}
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public Message(envoy.data.Message message) { public Message(envoy.data.Message message) {
@ -111,11 +107,12 @@ public class Message {
/** /**
* Converts this message into an instance of {@link envoy.data.Message}. * Converts this message into an instance of {@link envoy.data.Message}.
* *
* @return a {@link envoy.data.Message} containing the same values as this * @return a {@link envoy.data.Message} containing the same values as this message
* message
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public envoy.data.Message toCommon() { return prepareBuilder().build(); } public envoy.data.Message toCommon() {
return prepareBuilder().build();
}
/** /**
* @return a message builder containing the state of this message * @return a message builder containing the state of this message
@ -128,13 +125,14 @@ public class Message {
.setReadDate(readDate) .setReadDate(readDate)
.setStatus(status) .setStatus(status)
.setForwarded(forwarded); .setForwarded(forwarded);
if (attachment != null) builder.setAttachment(new Attachment(attachment, attachmentName, attachmentType)); if (attachment != null)
builder.setAttachment(new Attachment(attachment, attachmentName, attachmentType));
return builder; return builder;
} }
/** /**
* Sets the message status to {@link MessageStatus#RECEIVED} and sets the * Sets the message status to {@link MessageStatus#RECEIVED} and sets the current time stamp as
* current time stamp as the received date. * the received date.
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -144,8 +142,8 @@ public class Message {
} }
/** /**
* Sets the message status to {@link MessageStatus#READ} and sets the * Sets the message status to {@link MessageStatus#READ} and sets the current time stamp as the
* current time stamp as the read date. * read date.
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -207,8 +205,7 @@ public class Message {
public void setCreationDate(Instant creationDate) { this.creationDate = creationDate; } public void setCreationDate(Instant creationDate) { this.creationDate = creationDate; }
/** /**
* @return the date at which a {link envoy.data.Message} has been received by * @return the date at which a {link envoy.data.Message} has been received by the server
* the server
* @since Envoy Server Standalone v0.2-beta * @since Envoy Server Standalone v0.2-beta
*/ */
public Instant getReceivedDate() { return receivedDate; } public Instant getReceivedDate() { return receivedDate; }
@ -279,7 +276,9 @@ public class Message {
* @param attachmentType the attachmentType to set * @param attachmentType the attachmentType to set
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public void setAttachmentType(AttachmentType attachmentType) { this.attachmentType = attachmentType; } public void setAttachmentType(AttachmentType attachmentType) {
this.attachmentType = attachmentType;
}
/** /**
* @return the attachmentName * @return the attachmentName
@ -291,7 +290,9 @@ public class Message {
* @param attachmentName the attachmentName to set * @param attachmentName the attachmentName to set
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
*/ */
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; } public void setAttachmentName(String attachmentName) {
this.attachmentName = attachmentName;
}
/** /**
* @return whether this message is a forwarded message * @return whether this message is a forwarded message

View File

@ -7,9 +7,10 @@ import java.util.logging.Level;
import javax.persistence.*; import javax.persistence.*;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import envoy.server.net.ConnectionManager;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.server.net.ConnectionManager;
/** /**
* Contains operations used for persistence. * Contains operations used for persistence.
* *
@ -19,7 +20,8 @@ import envoy.util.EnvoyLog;
*/ */
public final class PersistenceManager { public final class PersistenceManager {
private final EntityManager entityManager = Persistence.createEntityManagerFactory("envoy").createEntityManager(); private final EntityManager entityManager =
Persistence.createEntityManagerFactory("envoy").createEntityManager();
private final EntityTransaction transaction = entityManager.getTransaction(); private final EntityTransaction transaction = entityManager.getTransaction();
private static final PersistenceManager persistenceManager = new PersistenceManager(); private static final PersistenceManager persistenceManager = new PersistenceManager();
@ -35,7 +37,11 @@ public final class PersistenceManager {
.getOnlineUsers() .getOnlineUsers()
.stream() .stream()
.map(this::getUserByID) .map(this::getUserByID)
.forEach(user -> { user.setStatus(UserStatus.OFFLINE); user.setLastSeen(Instant.now()); entityManager.merge(user); }); .forEach(user -> {
user.setStatus(UserStatus.OFFLINE);
user.setLastSeen(Instant.now());
entityManager.merge(user);
});
}))); })));
} }
@ -51,7 +57,9 @@ public final class PersistenceManager {
* @param contact the {@link Contact} to add to the database * @param contact the {@link Contact} to add to the database
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void addContact(Contact contact) { persist(contact); } public void addContact(Contact contact) {
persist(contact);
}
/** /**
* Adds a {@link Message} to the database. * Adds a {@link Message} to the database.
@ -59,7 +67,9 @@ public final class PersistenceManager {
* @param message the {@link Message} to add to the database * @param message the {@link Message} to add to the database
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void addMessage(Message message) { persist(message); } public void addMessage(Message message) {
persist(message);
}
/** /**
* Adds a {@link ConfigItem} to the database. * Adds a {@link ConfigItem} to the database.
@ -67,7 +77,9 @@ public final class PersistenceManager {
* @param configItem the {@link ConfigItem} to add to the database * @param configItem the {@link ConfigItem} to add to the database
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void addConfigItem(ConfigItem configItem) { persist(configItem); } public void addConfigItem(ConfigItem configItem) {
persist(configItem);
}
/** /**
* Updates a {@link Contact} in the database * Updates a {@link Contact} in the database
@ -75,7 +87,9 @@ public final class PersistenceManager {
* @param contact the {@link Contact} to add to the database * @param contact the {@link Contact} to add to the database
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void updateContact(Contact contact) { merge(contact); } public void updateContact(Contact contact) {
merge(contact);
}
/** /**
* Updates a {@link Message} in the database. * Updates a {@link Message} in the database.
@ -83,7 +97,9 @@ public final class PersistenceManager {
* @param message the message to update * @param message the message to update
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void updateMessage(Message message) { merge(message); } public void updateMessage(Message message) {
merge(message);
}
/** /**
* Updates a {@link ConfigItem} in the database. * Updates a {@link ConfigItem} in the database.
@ -91,7 +107,9 @@ public final class PersistenceManager {
* @param configItem the configItem to update * @param configItem the configItem to update
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void updateConfigItem(ConfigItem configItem) { merge(configItem); } public void updateConfigItem(ConfigItem configItem) {
merge(configItem);
}
/** /**
* Deletes a {@link Contact} in the database. * Deletes a {@link Contact} in the database.
@ -115,7 +133,9 @@ public final class PersistenceManager {
* @param message the {@link Message} to delete * @param message the {@link Message} to delete
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void deleteMessage(Message message) { remove(message); } public void deleteMessage(Message message) {
remove(message);
}
/** /**
* Searches for a {@link User} with a specific ID. * Searches for a {@link User} with a specific ID.
@ -124,7 +144,9 @@ public final class PersistenceManager {
* @return the user with the specified ID or {@code null} if none was found * @return the user with the specified ID or {@code null} if none was found
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public User getUserByID(long id) { return entityManager.find(User.class, id); } public User getUserByID(long id) {
return entityManager.find(User.class, id);
}
/** /**
* Searches for a {@link Group} with a specific ID. * Searches for a {@link Group} with a specific ID.
@ -133,7 +155,9 @@ public final class PersistenceManager {
* @return the group with the specified ID or {@code null} if none was found * @return the group with the specified ID or {@code null} if none was found
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public Group getGroupByID(long id) { return entityManager.find(Group.class, id); } public Group getGroupByID(long id) {
return entityManager.find(Group.class, id);
}
/** /**
* Searches for a {@link Contact} with a specific ID. * Searches for a {@link Contact} with a specific ID.
@ -142,7 +166,9 @@ public final class PersistenceManager {
* @return the contact with the specified ID or {@code null} if none was found * @return the contact with the specified ID or {@code null} if none was found
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public Contact getContactByID(long id) { return entityManager.find(Contact.class, id); } public Contact getContactByID(long id) {
return entityManager.find(Contact.class, id);
}
/** /**
* Searched for a {@link User} with a specific name. * Searched for a {@link User} with a specific name.
@ -152,7 +178,8 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public User getUserByName(String name) { public User getUserByName(String name) {
return (User) entityManager.createNamedQuery(User.findByName).setParameter("name", name).getSingleResult(); return (User) entityManager.createNamedQuery(User.findByName).setParameter("name", name)
.getSingleResult();
} }
/** /**
@ -163,7 +190,8 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public Group getGroupByName(String name) { public Group getGroupByName(String name) {
return (Group) entityManager.createNamedQuery(Group.findByName).setParameter("name", name).getSingleResult(); return (Group) entityManager.createNamedQuery(Group.findByName).setParameter("name", name)
.getSingleResult();
} }
/** /**
@ -173,18 +201,21 @@ public final class PersistenceManager {
* @return the message with the specified ID or {@code null} if none is found * @return the message with the specified ID or {@code null} if none is found
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public Message getMessageByID(long id) { return entityManager.find(Message.class, id); } public Message getMessageByID(long id) {
return entityManager.find(Message.class, id);
}
/** /**
* @param key the name of this {@link ConfigItem} * @param key the name of this {@link ConfigItem}
* @return the {@link ConfigItem} with the given name * @return the {@link ConfigItem} with the given name
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public ConfigItem getConfigItemByID(String key) { return entityManager.find(ConfigItem.class, key); } public ConfigItem getConfigItemByID(String key) {
return entityManager.find(ConfigItem.class, key);
}
/** /**
* Returns all messages received while being offline or the ones that have * Returns all messages received while being offline or the ones that have changed.
* changed.
* *
* @param user the user who wants to receive his unread messages * @param user the user who wants to receive his unread messages
* @param lastSync the time stamp of the last synchronization * @param lastSync the time stamp of the last synchronization
@ -192,17 +223,16 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.2-beta * @since Envoy Server Standalone v0.2-beta
*/ */
public List<Message> getPendingMessages(User user, Instant lastSync) { public List<Message> getPendingMessages(User user, Instant lastSync) {
return entityManager.createNamedQuery(Message.getPending).setParameter("user", user).setParameter("lastSeen", lastSync).getResultList(); return entityManager.createNamedQuery(Message.getPending).setParameter("user", user)
.setParameter("lastSeen", lastSync).getResultList();
} }
/** /**
* Returns all groupMessages received while being offline or the ones that have * Returns all groupMessages received while being offline or the ones that have changed.
* changed.
* *
* @param user the user who wants to receive his unread groupMessages * @param user the user who wants to receive his unread groupMessages
* @param lastSync the time stamp of the last synchronization * @param lastSync the time stamp of the last synchronization
* @return all groupMessages that the client does not yet have (unread * @return all groupMessages that the client does not yet have (unread groupMessages)
* groupMessages)
* @since Envoy Server Standalone v0.2-beta * @since Envoy Server Standalone v0.2-beta
*/ */
public List<GroupMessage> getPendingGroupMessages(User user, Instant lastSync) { public List<GroupMessage> getPendingGroupMessages(User user, Instant lastSync) {
@ -213,12 +243,11 @@ public final class PersistenceManager {
} }
/** /**
* Searches for users matching a search phrase. Contacts of the attached user * Searches for users matching a search phrase. Contacts of the attached user and the attached
* and the attached user is ignored. * user is ignored.
* *
* @param searchPhrase the search phrase * @param searchPhrase the search phrase
* @param userId the ID of the user in whose context the search is * @param userId the ID of the user in whose context the search is performed
* performed
* @return a list of all users who matched the criteria * @return a list of all users who matched the criteria
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
@ -254,7 +283,10 @@ public final class PersistenceManager {
contact2.getContacts().add(contact1); contact2.getContacts().add(contact1);
// Synchronize changes with the database // Synchronize changes with the database
transaction(() -> { entityManager.merge(contact1); entityManager.merge(contact2); }); transaction(() -> {
entityManager.merge(contact1);
entityManager.merge(contact2);
});
} }
/** /**
@ -282,7 +314,10 @@ public final class PersistenceManager {
contact2.getContacts().remove(contact1); contact2.getContacts().remove(contact1);
// Synchronize changes with the database // Synchronize changes with the database
transaction(() -> { entityManager.merge(contact1); entityManager.merge(contact2); }); transaction(() -> {
entityManager.merge(contact1);
entityManager.merge(contact2);
});
} }
/** /**
@ -291,21 +326,27 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public List<User> getContacts(User user) { public List<User> getContacts(User user) {
return entityManager.createNamedQuery(User.findContacts).setParameter("user", user).getResultList(); return entityManager.createNamedQuery(User.findContacts).setParameter("user", user)
.getResultList();
} }
private void persist(Object obj) { transaction(() -> entityManager.persist(obj)); } private void persist(Object obj) {
transaction(() -> entityManager.persist(obj));
}
private void merge(Object obj) { transaction(() -> entityManager.merge(obj)); } private void merge(Object obj) {
transaction(() -> entityManager.merge(obj));
}
private void remove(Object obj) { transaction(() -> entityManager.remove(obj)); } private void remove(Object obj) {
transaction(() -> entityManager.remove(obj));
}
/** /**
* Performs a transaction with the given Runnable, that should somewhere call * Performs a transaction with the given Runnable, that should somewhere call
* {@link EntityManager}. * {@link EntityManager}.
* *
* @param entityManagerRelatedAction the action that changes something in the * @param entityManagerRelatedAction the action that changes something in the database
* database
* @since Envoy Server v0.3-beta * @since Envoy Server v0.3-beta
*/ */
private void transaction(Runnable entityManagerRelatedAction) { private void transaction(Runnable entityManagerRelatedAction) {
@ -328,8 +369,10 @@ public final class PersistenceManager {
if (transaction.isActive()) { if (transaction.isActive()) {
transaction.rollback(); transaction.rollback();
EnvoyLog.getLogger(PersistenceManager.class) EnvoyLog.getLogger(PersistenceManager.class)
.log(Level.SEVERE, "Could not perform transaction, hence discarding it. It's likely that a serious issue exists."); .log(Level.SEVERE,
} else throw e2; "Could not perform transaction, hence discarding it. It's likely that a serious issue exists.");
} else
throw e2;
} }
} }
} }

View File

@ -16,7 +16,9 @@ public final class ServerConfig extends Config {
* @return the singleton instance of the server config * @return the singleton instance of the server config
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public static ServerConfig getInstance() { return config == null ? config = new ServerConfig() : config; } public static ServerConfig getInstance() {
return config == null ? config = new ServerConfig() : config;
}
private ServerConfig() { private ServerConfig() {
super(".envoy-server"); super(".envoy-server");
@ -45,13 +47,17 @@ public final class ServerConfig extends Config {
* @return {@code true} if issue reporting is enabled * @return {@code true} if issue reporting is enabled
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public Boolean isIssueReportingEnabled() { return (Boolean) items.get("enableIssueReporting").get(); } public Boolean isIssueReportingEnabled() {
return (Boolean) items.get("enableIssueReporting").get();
}
/** /**
* @return {@code true} if attachment support has been enabled * @return {@code true} if attachment support has been enabled
* @since Envoy Client v0.3-alpha * @since Envoy Client v0.3-alpha
*/ */
public Boolean isAttachmentSupportEnabled() { return (Boolean) items.get("enableAttachments").get(); } public Boolean isAttachmentSupportEnabled() {
return (Boolean) items.get("enableAttachments").get();
}
/** /**
* @return {@code true} if group support has been enabled * @return {@code true} if group support has been enabled
@ -85,9 +91,8 @@ public final class ServerConfig extends Config {
/** /**
* @return the authorization token used to authenticate to * @return the authorization token used to authenticate to
* {@link ServerConfig#getIssueReportingURL()}. If null, * {@link ServerConfig#getIssueReportingURL()}. If null, authorization is expected to
* authorization is expected to occur via a query_param or via a basic * occur via a query_param or via a basic user-password-combination
* user-password-combination
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
*/ */
public String getIssueAuthToken() { return (String) items.get("issueAuthToken").get(); } public String getIssueAuthToken() { return (String) items.get("issueAuthToken").get(); }
@ -96,5 +101,7 @@ public final class ServerConfig extends Config {
* @return the amount of days after which user authentication tokens expire * @return the amount of days after which user authentication tokens expire
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
*/ */
public Integer getAuthTokenExpiration() { return (Integer) items.get("authTokenExpiration").get(); } public Integer getAuthTokenExpiration() {
return (Integer) items.get("authTokenExpiration").get();
}
} }

View File

@ -9,9 +9,9 @@ import javax.persistence.*;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
/** /**
* This class enables the storage of user specific data inside a database using * This class enables the storage of user specific data inside a database using Hibernate. Its
* Hibernate. Its objects will be referred to as database users as opposed to * objects will be referred to as database users as opposed to the common user objects present on
* the common user objects present on both the client and the server. * both the client and the server.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -19,18 +19,9 @@ import envoy.data.User.UserStatus;
*/ */
@Entity @Entity
@NamedQueries({ @NamedQueries({
@NamedQuery( @NamedQuery(name = User.findByName, query = "SELECT u FROM User u WHERE u.name = :name"),
name = User.findByName, @NamedQuery(name = User.findContacts, query = "SELECT u.contacts FROM User u WHERE u = :user"),
query = "SELECT u FROM User u WHERE u.name = :name" @NamedQuery(name = User.searchByName, query = "SELECT u FROM User u WHERE (lower(u.name) LIKE lower(:searchPhrase) AND u <> :context AND :context NOT MEMBER OF u.contacts)")
),
@NamedQuery(
name = User.findContacts,
query = "SELECT u.contacts FROM User u WHERE u = :user"
),
@NamedQuery(
name = User.searchByName,
query = "SELECT u FROM User u WHERE (lower(u.name) LIKE lower(:searchPhrase) AND u <> :context AND :context NOT MEMBER OF u.contacts)"
)
}) })
public final class User extends Contact { public final class User extends Contact {
@ -42,8 +33,7 @@ public final class User extends Contact {
public static final String findByName = "User.findByName"; public static final String findByName = "User.findByName";
/** /**
* Named query retrieving the contacts of a given user (parameter * Named query retrieving the contacts of a given user (parameter {@code :user}).
* {@code :user}).
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -51,8 +41,8 @@ public final class User extends Contact {
/** /**
* Named query searching for users with a name like a search phrase (parameter * Named query searching for users with a name like a search phrase (parameter
* {@code :searchPhrase}) that are not in the contact list of a given user * {@code :searchPhrase}) that are not in the contact list of a given user (parameter
* (parameter {@code :context}). * {@code :context}).
* *
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -77,11 +67,14 @@ public final class User extends Contact {
@Override @Override
public envoy.data.User toCommon() { public envoy.data.User toCommon() {
return new envoy.data.User(id, name, status, contacts.parallelStream().map(Contact::toFlatCommon).collect(Collectors.toSet())); return new envoy.data.User(id, name, status,
contacts.parallelStream().map(Contact::toFlatCommon).collect(Collectors.toSet()));
} }
@Override @Override
protected envoy.data.User toFlatCommon() { return new envoy.data.User(id, name, status, Set.of()); } protected envoy.data.User toFlatCommon() {
return new envoy.data.User(id, name, status, Set.of());
}
/** /**
* @return the password hash * @return the password hash
@ -114,11 +107,12 @@ public final class User extends Contact {
public Instant getAuthTokenExpiration() { return authTokenExpiration; } public Instant getAuthTokenExpiration() { return authTokenExpiration; }
/** /**
* @param authTokenExpiration the authentication token expiration timestamp to * @param authTokenExpiration the authentication token expiration timestamp to set
* set
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
*/ */
public void setAuthTokenExpiration(Instant authTokenExpiration) { this.authTokenExpiration = authTokenExpiration; } public void setAuthTokenExpiration(Instant authTokenExpiration) {
this.authTokenExpiration = authTokenExpiration;
}
/** /**
* @return the last date the user has been online * @return the last date the user has been online
@ -154,5 +148,7 @@ public final class User extends Contact {
* @param latestContactDeletion the latestContactDeletion to set * @param latestContactDeletion the latestContactDeletion to set
* @since Envoy Server v0.3-beta * @since Envoy Server v0.3-beta
*/ */
public void setLatestContactDeletion(Instant latestContactDeletion) { this.latestContactDeletion = latestContactDeletion; } public void setLatestContactDeletion(Instant latestContactDeletion) {
this.latestContactDeletion = latestContactDeletion;
}
} }

View File

@ -7,6 +7,7 @@ import java.util.stream.Collectors;
import com.jenkov.nioserver.ISocketIdListener; import com.jenkov.nioserver.ISocketIdListener;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.processors.UserStatusChangeProcessor; import envoy.server.processors.UserStatusChangeProcessor;
@ -17,8 +18,8 @@ import envoy.server.processors.UserStatusChangeProcessor;
public final class ConnectionManager implements ISocketIdListener { public final class ConnectionManager implements ISocketIdListener {
/** /**
* Contains all socket IDs that have not yet performed a handshake / acquired * Contains all socket IDs that have not yet performed a handshake / acquired their
* their corresponding user ID. * corresponding user ID.
* *
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
@ -46,7 +47,8 @@ public final class ConnectionManager implements ISocketIdListener {
if (!pendingSockets.remove(socketID)) { if (!pendingSockets.remove(socketID)) {
// Notify contacts of this users offline-going // Notify contacts of this users offline-going
final envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID)); final envoy.server.data.User user =
PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
user.setLastSeen(Instant.now()); user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE); UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
@ -56,7 +58,9 @@ public final class ConnectionManager implements ISocketIdListener {
} }
@Override @Override
public void socketRegistered(long socketID) { pendingSockets.add(socketID); } public void socketRegistered(long socketID) {
pendingSockets.add(socketID);
}
/** /**
* Associates a socket ID with a user ID. * Associates a socket ID with a user ID.
@ -75,7 +79,9 @@ public final class ConnectionManager implements ISocketIdListener {
* @return the ID of the socket * @return the ID of the socket
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public long getSocketID(long userID) { return sockets.get(userID); } public long getSocketID(long userID) {
return sockets.get(userID);
}
/** /**
* @param socketID the id of the socket whose User is needed * @param socketID the id of the socket whose User is needed
@ -83,7 +89,8 @@ public final class ConnectionManager implements ISocketIdListener {
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public long getUserIDBySocketID(long socketID) { public long getUserIDBySocketID(long socketID) {
return sockets.entrySet().stream().filter(entry -> entry.getValue().equals(socketID)).findFirst().get().getKey(); return sockets.entrySet().stream().filter(entry -> entry.getValue().equals(socketID))
.findFirst().get().getKey();
} }
/** /**
@ -91,7 +98,9 @@ public final class ConnectionManager implements ISocketIdListener {
* @return {@code true} if the user is online * @return {@code true} if the user is online
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public boolean isOnline(long userID) { return sockets.containsKey(userID); } public boolean isOnline(long userID) {
return sockets.containsKey(userID);
}
/** /**
* @return the userIDs of all users who are currently online * @return the userIDs of all users who are currently online
@ -105,6 +114,7 @@ public final class ConnectionManager implements ISocketIdListener {
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public Set<Long> getOnlineUsersOfGroup(Group group) { public Set<Long> getOnlineUsersOfGroup(Group group) {
return group.getContacts().stream().map(Contact::getID).filter(this::isOnline).collect(Collectors.toSet()); return group.getContacts().stream().map(Contact::getID).filter(this::isOnline)
.collect(Collectors.toSet());
} }
} }

View File

@ -7,9 +7,10 @@ import java.util.logging.*;
import com.jenkov.nioserver.*; import com.jenkov.nioserver.*;
import envoy.server.processors.ObjectProcessor;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.server.processors.ObjectProcessor;
/** /**
* Handles incoming objects. * Handles incoming objects.
* *
@ -28,12 +29,16 @@ public final class ObjectMessageProcessor implements IMessageProcessor {
* @param processors the {@link ObjectProcessor} to set * @param processors the {@link ObjectProcessor} to set
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public ObjectMessageProcessor(Set<ObjectProcessor<?>> processors) { this.processors = processors; } public ObjectMessageProcessor(Set<ObjectProcessor<?>> processors) {
this.processors = processors;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public void process(Message message, WriteProxy writeProxy) { public void process(Message message, WriteProxy writeProxy) {
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(message.sharedArray, message.offset + 4, message.length - 4))) { try (ObjectInputStream in =
new ObjectInputStream(new ByteArrayInputStream(message.sharedArray, message.offset + 4,
message.length - 4))) {
Object obj = in.readObject(); Object obj = in.readObject();
if (obj == null) { if (obj == null) {
logger.warning("Received a null object"); logger.warning("Received a null object");
@ -45,7 +50,8 @@ public final class ObjectMessageProcessor implements IMessageProcessor {
// Get processor and input class and process object // Get processor and input class and process object
for (@SuppressWarnings("rawtypes") for (@SuppressWarnings("rawtypes")
ObjectProcessor p : processors) { ObjectProcessor p : processors) {
Class<?> c = (Class<?>) ((ParameterizedType) p.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]; Class<?> c = (Class<?>) ((ParameterizedType) p.getClass().getGenericInterfaces()[0])
.getActualTypeArguments()[0];
if (c.equals(obj.getClass())) if (c.equals(obj.getClass()))
try { try {
p.process(c.cast(obj), message.socketId, new ObjectWriteProxy(writeProxy)); p.process(c.cast(obj), message.socketId, new ObjectWriteProxy(writeProxy));

View File

@ -16,9 +16,9 @@ import envoy.util.SerializationUtils;
*/ */
public final class ObjectMessageReader implements IMessageReader { public final class ObjectMessageReader implements IMessageReader {
private List<Message> completeMessages = new ArrayList<>(); private final List<Message> completeMessages = new ArrayList<>();
private Message nextMessage; private Message nextMessage;
private MessageBuffer messageBuffer; private MessageBuffer messageBuffer;
@Override @Override
public List<Message> getMessages() { return completeMessages; } public List<Message> getMessages() { return completeMessages; }
@ -43,7 +43,8 @@ public final class ObjectMessageReader implements IMessageReader {
buffer.clear(); buffer.clear();
// Get message length // Get message length
if (nextMessage.length < 4) return; if (nextMessage.length < 4)
return;
int length = SerializationUtils.bytesToInt(nextMessage.sharedArray, nextMessage.offset) + 4; int length = SerializationUtils.bytesToInt(nextMessage.sharedArray, nextMessage.offset) + 4;
do { do {
@ -58,7 +59,8 @@ public final class ObjectMessageReader implements IMessageReader {
} }
// Get message length // Get message length
if (nextMessage.length < 4) return; if (nextMessage.length < 4)
return;
length = SerializationUtils.bytesToInt(nextMessage.sharedArray, nextMessage.offset) + 4; length = SerializationUtils.bytesToInt(nextMessage.sharedArray, nextMessage.offset) + 4;
} while (nextMessage.length >= length); } while (nextMessage.length >= length);

View File

@ -7,9 +7,10 @@ import java.util.stream.Stream;
import com.jenkov.nioserver.*; import com.jenkov.nioserver.*;
import envoy.server.data.Contact;
import envoy.util.*; import envoy.util.*;
import envoy.server.data.Contact;
/** /**
* This class defines methods to send an object to a client. * This class defines methods to send an object to a client.
* *
@ -21,7 +22,8 @@ public final class ObjectWriteProxy {
private final WriteProxy writeProxy; private final WriteProxy writeProxy;
private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ObjectWriteProxy.class); private static final Logger logger =
EnvoyLog.getLogger(ObjectWriteProxy.class);
/** /**
* Creates an instance of {@link ObjectWriteProxy}. * Creates an instance of {@link ObjectWriteProxy}.
@ -29,13 +31,14 @@ public final class ObjectWriteProxy {
* @param writeProxy the {@link WriteProxy} to write objects to another client * @param writeProxy the {@link WriteProxy} to write objects to another client
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public ObjectWriteProxy(WriteProxy writeProxy) { this.writeProxy = writeProxy; } public ObjectWriteProxy(WriteProxy writeProxy) {
this.writeProxy = writeProxy;
}
/** /**
* @param recipientSocketID the socket id of the recipient * @param recipientSocketID the socket id of the recipient
* @param obj the object to return to the client * @param obj the object to return to the client
* @throws RuntimeException if the serialization of the object failed (this is * @throws RuntimeException if the serialization of the object failed (this is highly unlikely)
* highly unlikely)
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public void write(long recipientSocketID, Object obj) { public void write(long recipientSocketID, Object obj) {
@ -67,7 +70,9 @@ public final class ObjectWriteProxy {
* @param message the object to send * @param message the object to send
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public void writeToOnlineContacts(Set<? extends Contact> contacts, Object message) { writeToOnlineContacts(contacts.stream(), message); } public void writeToOnlineContacts(Set<? extends Contact> contacts, Object message) {
writeToOnlineContacts(contacts.stream(), message);
}
/** /**
* Sends an object to all contacts in a set that are online. * Sends an object to all contacts in a set that are online.
@ -77,6 +82,7 @@ public final class ObjectWriteProxy {
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public void writeToOnlineContacts(Stream<? extends Contact> contacts, Object message) { public void writeToOnlineContacts(Stream<? extends Contact> contacts, Object message) {
contacts.map(Contact::getID).filter(connectionManager::isOnline).map(connectionManager::getSocketID).forEach(id -> write(id, message)); contacts.map(Contact::getID).filter(connectionManager::isOnline)
.map(connectionManager::getSocketID).forEach(id -> write(id, message));
} }
} }

View File

@ -5,6 +5,7 @@ import static envoy.server.Startup.config;
import java.util.HashSet; import java.util.HashSet;
import envoy.event.*; import envoy.event.*;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
@ -32,7 +33,9 @@ public final class GroupCreationProcessor implements ObjectProcessor<GroupCreati
.stream() .stream()
.map(persistenceManager::getUserByID) .map(persistenceManager::getUserByID)
.forEach(member -> persistenceManager.addContactBidirectional(member, group)); .forEach(member -> persistenceManager.addContactBidirectional(member, group));
persistenceManager.addContactBidirectional(persistenceManager.getUserByID(connectionManager.getUserIDBySocketID(socketID)), group); persistenceManager.addContactBidirectional(
writeProxy.writeToOnlineContacts(group.getContacts(), new GroupCreationResult(group.toCommon())); persistenceManager.getUserByID(connectionManager.getUserIDBySocketID(socketID)), group);
writeProxy.writeToOnlineContacts(group.getContacts(),
new GroupCreationResult(group.toCommon()));
} }
} }

View File

@ -11,9 +11,10 @@ import javax.persistence.EntityExistsException;
import envoy.data.GroupMessage; import envoy.data.GroupMessage;
import envoy.event.*; import envoy.event.*;
import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.util.EnvoyLog;
/** /**
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -23,14 +24,16 @@ public final class GroupMessageProcessor implements ObjectProcessor<GroupMessage
private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(GroupCreationProcessor.class); private static final Logger logger =
EnvoyLog.getLogger(GroupCreationProcessor.class);
@Override @Override
public void process(GroupMessage groupMessage, long socketID, ObjectWriteProxy writeProxy) { public void process(GroupMessage groupMessage, long socketID, ObjectWriteProxy writeProxy) {
groupMessage.nextStatus(); groupMessage.nextStatus();
// Update statuses to SENT / RECEIVED depending on online status // Update statuses to SENT / RECEIVED depending on online status
groupMessage.getMemberStatuses().replaceAll((memberID, status) -> connectionManager.isOnline(memberID) ? RECEIVED : SENT); groupMessage.getMemberStatuses().replaceAll(
(memberID, status) -> connectionManager.isOnline(memberID) ? RECEIVED : SENT);
// Set status for sender to READ // Set status for sender to READ
groupMessage.getMemberStatuses().replace(groupMessage.getSenderID(), READ); groupMessage.getMemberStatuses().replace(groupMessage.getSenderID(), READ);
@ -44,7 +47,8 @@ public final class GroupMessageProcessor implements ObjectProcessor<GroupMessage
} }
// message attachment will be automatically removed if disabled in config // message attachment will be automatically removed if disabled in config
final var groupMessageServer = new envoy.server.data.GroupMessage(groupMessage, Instant.now()); final var groupMessageServer =
new envoy.server.data.GroupMessage(groupMessage, Instant.now());
// Telling the server to reload the message without the attachment and telling // Telling the server to reload the message without the attachment and telling
// the client not to send anymore attachments // the client not to send anymore attachments
if (!config.isAttachmentSupportEnabled() && groupMessage.hasAttachment()) { if (!config.isAttachmentSupportEnabled() && groupMessage.hasAttachment()) {
@ -57,11 +61,11 @@ public final class GroupMessageProcessor implements ObjectProcessor<GroupMessage
final var groupMessageCopy = groupMessage; final var groupMessageCopy = groupMessage;
// Deliver the message to the recipients that are online // Deliver the message to the recipients that are online
writeProxy.writeToOnlineContacts( writeProxy.writeToOnlineContacts(
persistenceManager.getGroupByID(groupMessageCopy.getRecipientID()) persistenceManager.getGroupByID(groupMessageCopy.getRecipientID())
.getContacts() .getContacts()
.stream() .stream()
.filter(c -> c.getID() != groupMessageCopy.getSenderID()), .filter(c -> c.getID() != groupMessageCopy.getSenderID()),
groupMessageCopy); groupMessageCopy);
try { try {
PersistenceManager.getInstance().addMessage(groupMessageServer); PersistenceManager.getInstance().addMessage(groupMessageServer);

View File

@ -8,22 +8,26 @@ import java.util.logging.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
import envoy.util.EnvoyLog;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.util.EnvoyLog;
/** /**
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public final class GroupMessageStatusChangeProcessor implements ObjectProcessor<GroupMessageStatusChange> { public final class GroupMessageStatusChangeProcessor
implements ObjectProcessor<GroupMessageStatusChange> {
private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class); private static final Logger logger =
EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
@Override @Override
public void process(GroupMessageStatusChange statusChange, long socketID, ObjectWriteProxy writeProxy) { public void process(GroupMessageStatusChange statusChange, long socketID,
ObjectWriteProxy writeProxy) {
GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID()); GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID());
// Any other status than READ is not supposed to be sent to the server // Any other status than READ is not supposed to be sent to the server
@ -51,7 +55,7 @@ public final class GroupMessageStatusChangeProcessor implements ObjectProcessor<
// Notify online members about the status change // Notify online members about the status change
writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(), writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(),
new MessageStatusChange(gmsg.getID(), gmsg.getStatus(), Instant.now())); new MessageStatusChange(gmsg.getID(), gmsg.getStatus(), Instant.now()));
} }
persistenceManager.updateMessage(gmsg); persistenceManager.updateMessage(gmsg);
} }

View File

@ -4,9 +4,10 @@ import java.time.Instant;
import java.util.logging.Level; import java.util.logging.Level;
import envoy.event.GroupResize; import envoy.event.GroupResize;
import envoy.util.EnvoyLog;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/** /**
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -35,16 +36,19 @@ public final class GroupResizeProcessor implements ObjectProcessor<GroupResize>
// The group has no more members and hence will be deleted // The group has no more members and hence will be deleted
if (group.getContacts().isEmpty()) { if (group.getContacts().isEmpty()) {
EnvoyLog.getLogger(GroupResizeProcessor.class).log(Level.INFO, "Deleting now empty group " + group.getName()); EnvoyLog.getLogger(GroupResizeProcessor.class).log(Level.INFO,
"Deleting now empty group " + group.getName());
persistenceManager.deleteContact(group); persistenceManager.deleteContact(group);
} else { } else {
// Informing the other members // Informing the other members
writeProxy.writeToOnlineContacts(group.getContacts(), groupResize); writeProxy.writeToOnlineContacts(group.getContacts(), groupResize);
group.getContacts().forEach(c -> ((User) c).setLatestContactDeletion(Instant.now())); group.getContacts()
.forEach(c -> ((User) c).setLatestContactDeletion(Instant.now()));
} }
group.getContacts().remove(persistenceManager.getUserByID(groupResize.get().getID())); group.getContacts()
.remove(persistenceManager.getUserByID(groupResize.get().getID()));
return; return;
} }
} }

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import envoy.data.IDGenerator; import envoy.data.IDGenerator;
import envoy.event.IDGeneratorRequest; import envoy.event.IDGeneratorRequest;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
@ -17,7 +18,8 @@ public final class IDGeneratorRequestProcessor implements ObjectProcessor<IDGene
private static final long ID_RANGE = 200; private static final long ID_RANGE = 200;
@Override @Override
public void process(IDGeneratorRequest input, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(IDGeneratorRequest input, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
writeProxy.write(socketID, createIDGenerator()); writeProxy.write(socketID, createIDGenerator());
} }
@ -25,7 +27,9 @@ public final class IDGeneratorRequestProcessor implements ObjectProcessor<IDGene
* @return a new IDGenerator * @return a new IDGenerator
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public static IDGenerator createIDGenerator() { return createIDGenerator(ID_RANGE); } public static IDGenerator createIDGenerator() {
return createIDGenerator(ID_RANGE);
}
/** /**
* @param range of IDs used by the new IDGenerator * @param range of IDs used by the new IDGenerator
@ -33,7 +37,8 @@ public final class IDGeneratorRequestProcessor implements ObjectProcessor<IDGene
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
public static IDGenerator createIDGenerator(long range) { public static IDGenerator createIDGenerator(long range) {
ConfigItem currentID = PersistenceManager.getInstance().getConfigItemByID("currentMessageId"); ConfigItem currentID =
PersistenceManager.getInstance().getConfigItemByID("currentMessageId");
IDGenerator generator = new IDGenerator(Integer.parseInt(currentID.getValue()), range); IDGenerator generator = new IDGenerator(Integer.parseInt(currentID.getValue()), range);
currentID.setValue(String.valueOf(Integer.parseInt(currentID.getValue()) + range)); currentID.setValue(String.valueOf(Integer.parseInt(currentID.getValue()) + range));
PersistenceManager.getInstance().updateConfigItem(currentID); PersistenceManager.getInstance().updateConfigItem(currentID);

View File

@ -3,6 +3,7 @@ package envoy.server.processors;
import java.io.IOException; import java.io.IOException;
import envoy.event.IsTyping; import envoy.event.IsTyping;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.*; import envoy.server.net.*;
@ -18,11 +19,14 @@ public final class IsTypingProcessor implements ObjectProcessor<IsTyping> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override @Override
public void process(IsTyping event, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(IsTyping event, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
final var contact = persistenceManager.getContactByID(event.get()); final var contact = persistenceManager.getContactByID(event.get());
if (contact instanceof User) { if (contact instanceof User) {
final var destinationID = event.getDestinationID(); final var destinationID = event.getDestinationID();
if (connectionManager.isOnline(destinationID)) writeProxy.write(connectionManager.getSocketID(destinationID), event); if (connectionManager.isOnline(destinationID))
} else writeProxy.writeToOnlineContacts(contact.getContacts(), event); writeProxy.write(connectionManager.getSocketID(destinationID), event);
} else
writeProxy.writeToOnlineContacts(contact.getContacts(), event);
} }
} }

View File

@ -7,12 +7,13 @@ import java.net.*;
import java.util.logging.*; import java.util.logging.*;
import envoy.event.IssueProposal; import envoy.event.IssueProposal;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.server.net.ObjectWriteProxy;
/** /**
* This processor handles incoming {@link IssueProposal}s and automatically * This processor handles incoming {@link IssueProposal}s and automatically creates a new issue on
* creates a new issue on the gitea site, if not disabled by its administrator. * the gitea site, if not disabled by its administrator.
* *
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
@ -22,9 +23,11 @@ public final class IssueProposalProcessor implements ObjectProcessor<IssuePropos
private static final Logger logger = EnvoyLog.getLogger(IssueProposalProcessor.class); private static final Logger logger = EnvoyLog.getLogger(IssueProposalProcessor.class);
@Override @Override
public void process(IssueProposal issueProposal, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(IssueProposal issueProposal, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
// Do nothing if manually disabled // Do nothing if manually disabled
if (!config.isIssueReportingEnabled()) return; if (!config.isIssueReportingEnabled())
return;
try { try {
final var url = new URL(config.getIssueReportingURL()); final var url = new URL(config.getIssueReportingURL());
final var connection = (HttpURLConnection) url.openConnection(); final var connection = (HttpURLConnection) url.openConnection();
@ -41,18 +44,22 @@ public final class IssueProposalProcessor implements ObjectProcessor<IssuePropos
connection.setDoOutput(true); connection.setDoOutput(true);
final var json = String.format("{\"title\":\"%s\",\"body\":\"%s\",\"labels\":[%s, %s]}", final var json = String.format("{\"title\":\"%s\",\"body\":\"%s\",\"labels\":[%s, %s]}",
issueProposal.get(), issueProposal.get(),
issueProposal.getDescription(), issueProposal.getDescription(),
config.getUserMadeLabel(), config.getUserMadeLabel(),
issueProposal.isBug() ? config.getBugLabel() : config.getFeatureLabel()); issueProposal.isBug() ? config.getBugLabel() : config.getFeatureLabel());
try (final var os = connection.getOutputStream()) { try (final var os = connection.getOutputStream()) {
final byte[] input = json.getBytes("utf-8"); final byte[] input = json.getBytes("utf-8");
os.write(input, 0, input.length); os.write(input, 0, input.length);
} }
final var status = connection.getResponseCode(); final var status = connection.getResponseCode();
if (status == 201) logger.log(Level.INFO, "Successfully created an issue"); if (status == 201)
else logger.log(Level.WARNING, logger.log(Level.INFO, "Successfully created an issue");
String.format("Tried creating an issue for %s but received status code %d - Request params:%s", url, status, json)); else
logger.log(Level.WARNING,
String.format(
"Tried creating an issue for %s but received status code %d - Request params:%s",
url, status, json));
} catch (final IOException e) { } catch (final IOException e) {
logger.log(Level.WARNING, "An error occurred while creating an issue: ", e); logger.log(Level.WARNING, "An error occurred while creating an issue: ", e);
} }

View File

@ -14,10 +14,11 @@ import javax.persistence.NoResultException;
import envoy.data.LoginCredentials; import envoy.data.LoginCredentials;
import envoy.event.*; import envoy.event.*;
import envoy.event.contact.ContactsChangedSinceLastLogin; import envoy.event.contact.ContactsChangedSinceLastLogin;
import envoy.util.*;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.server.util.*; import envoy.server.util.*;
import envoy.util.*;
/** /**
* This {@link ObjectProcessor} handles {@link LoginCredentials}. * This {@link ObjectProcessor} handles {@link LoginCredentials}.
@ -47,27 +48,29 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Acquire a user object (or reject the handshake if that's impossible) // Acquire a user object (or reject the handshake if that's impossible)
User user = null; User user = null;
if (!credentials.isRegistration()) try { if (!credentials.isRegistration())
user = persistenceManager.getUserByName(credentials.getIdentifier()); try {
user = persistenceManager.getUserByName(credentials.getIdentifier());
// Check if the user is already online // Check if the user is already online
if (connectionManager.isOnline(user.getID())) { if (connectionManager.isOnline(user.getID())) {
logger.warning(user + " is already online!"); logger.warning(user + " is already online!");
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR)); writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
return;
}
// Authenticate with password or token
if (credentials.usesToken()) {
// Check the token
if (user.getAuthToken() == null || user.getAuthTokenExpiration().isBefore(Instant.now())
|| !user.getAuthToken().equals(credentials.getPassword())) {
logger.info(user + " tried to use an invalid token.");
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
return; return;
} }
} else
// Authenticate with password or token
if (credentials.usesToken()) {
// Check the token
if (user.getAuthToken() == null
|| user.getAuthTokenExpiration().isBefore(Instant.now())
|| !user.getAuthToken().equals(credentials.getPassword())) {
logger.info(user + " tried to use an invalid token.");
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
return;
}
} else
// Check the password hash // Check the password hash
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) { if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
@ -75,11 +78,11 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return; return;
} }
} catch (final NoResultException e) { } catch (final NoResultException e) {
logger.info("The requested user does not exist."); logger.info("The requested user does not exist.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return; return;
} }
else { else {
// Validate user name // Validate user name
@ -131,12 +134,14 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
token = AuthTokenGenerator.nextToken(); token = AuthTokenGenerator.nextToken();
user.setAuthToken(token); user.setAuthToken(token);
} }
user.setAuthTokenExpiration(Instant.now().plus(ServerConfig.getInstance().getAuthTokenExpiration().longValue(), ChronoUnit.DAYS)); user.setAuthTokenExpiration(Instant.now().plus(
ServerConfig.getInstance().getAuthTokenExpiration().longValue(), ChronoUnit.DAYS));
persistenceManager.updateContact(user); persistenceManager.updateContact(user);
writeProxy.write(socketID, new NewAuthToken(token)); writeProxy.write(socketID, new NewAuthToken(token));
} }
final var pendingMessages = PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync()); final var pendingMessages =
PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync());
pendingMessages.removeIf(GroupMessage.class::isInstance); pendingMessages.removeIf(GroupMessage.class::isInstance);
logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "..."); logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "...");
@ -156,19 +161,24 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Notify the sender about the delivery // Notify the sender about the delivery
if (connectionManager.isOnline(msg.getSender().getID())) { if (connectionManager.isOnline(msg.getSender().getID())) {
msgCommon.nextStatus(); msgCommon.nextStatus();
writeProxy.write(connectionManager.getSocketID(msg.getSender().getID()), new MessageStatusChange(msgCommon)); writeProxy.write(connectionManager.getSocketID(msg.getSender().getID()),
new MessageStatusChange(msgCommon));
} }
} else writeProxy.write(socketID, new MessageStatusChange(msgCommon)); } else
writeProxy.write(socketID, new MessageStatusChange(msgCommon));
} }
final List<GroupMessage> pendingGroupMessages = PersistenceManager.getInstance().getPendingGroupMessages(user, credentials.getLastSync()); final List<GroupMessage> pendingGroupMessages = PersistenceManager.getInstance()
logger.fine("Sending " + pendingGroupMessages.size() + " pending group messages to " + user + "..."); .getPendingGroupMessages(user, credentials.getLastSync());
logger.fine("Sending " + pendingGroupMessages.size() + " pending group messages to " + user
+ "...");
for (final var gmsg : pendingGroupMessages) { for (final var gmsg : pendingGroupMessages) {
final var gmsgCommon = gmsg.toCommon(); final var gmsgCommon = gmsg.toCommon();
// Deliver the message to the user if he hasn't received it yet // Deliver the message to the user if he hasn't received it yet
if (gmsg.getCreationDate().isAfter(credentials.getLastSync()) || gmsg.getMemberMessageStatus().get(user.getID()) == SENT) { if (gmsg.getCreationDate().isAfter(credentials.getLastSync())
|| gmsg.getMemberMessageStatus().get(user.getID()) == SENT) {
if (gmsg.getMemberMessageStatus().replace(user.getID(), RECEIVED) != RECEIVED) { if (gmsg.getMemberMessageStatus().replace(user.getID(), RECEIVED) != RECEIVED) {
gmsg.setLastStatusChangeDate(Instant.now()); gmsg.setLastStatusChangeDate(Instant.now());
@ -177,14 +187,15 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Notify all online group members about the status change // Notify all online group members about the status change
writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(), writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(),
new GroupMessageStatusChange(gmsg.getID(), RECEIVED, Instant.now(), connectionManager.getUserIDBySocketID(socketID))); new GroupMessageStatusChange(gmsg.getID(), RECEIVED, Instant.now(),
connectionManager.getUserIDBySocketID(socketID)));
if (Collections.min(gmsg.getMemberMessageStatus().values()) == RECEIVED) { if (Collections.min(gmsg.getMemberMessageStatus().values()) == RECEIVED) {
gmsg.received(); gmsg.received();
// Notify online members about the status change // Notify online members about the status change
writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(), writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(),
new MessageStatusChange(gmsg.getID(), gmsg.getStatus(), Instant.now())); new MessageStatusChange(gmsg.getID(), gmsg.getStatus(), Instant.now()));
} }
PersistenceManager.getInstance().updateMessage(gmsg); PersistenceManager.getInstance().updateMessage(gmsg);
@ -195,20 +206,25 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
} else { } else {
// Sending group message status changes // Sending group message status changes
if (gmsg.getStatus() == SENT && gmsg.getLastStatusChangeDate().isAfter(gmsg.getCreationDate()) if (gmsg.getStatus() == SENT
|| gmsg.getStatus() == RECEIVED && gmsg.getLastStatusChangeDate().isAfter(gmsg.getReceivedDate())) && gmsg.getLastStatusChangeDate().isAfter(gmsg.getCreationDate())
|| gmsg.getStatus() == RECEIVED
&& gmsg.getLastStatusChangeDate().isAfter(gmsg.getReceivedDate()))
gmsg.getMemberMessageStatus() gmsg.getMemberMessageStatus()
.forEach((memberID, memberStatus) -> writeProxy.write(socketID, .forEach((memberID, memberStatus) -> writeProxy.write(socketID,
new GroupMessageStatusChange(gmsg.getID(), memberStatus, gmsg.getLastStatusChangeDate(), memberID))); new GroupMessageStatusChange(gmsg.getID(), memberStatus,
gmsg.getLastStatusChangeDate(), memberID)));
// Deliver just a status change instead of the whole message // Deliver just a status change instead of the whole message
if (gmsg.getStatus() == RECEIVED && user.getLastSeen().isBefore(gmsg.getReceivedDate()) if (gmsg.getStatus() == RECEIVED
|| gmsg.getStatus() == READ && user.getLastSeen().isBefore(gmsg.getReadDate())) && user.getLastSeen().isBefore(gmsg.getReceivedDate())
|| gmsg.getStatus() == READ && user.getLastSeen().isBefore(gmsg.getReadDate()))
writeProxy.write(socketID, new MessageStatusChange(gmsgCommon)); writeProxy.write(socketID, new MessageStatusChange(gmsgCommon));
} }
} }
// Notify the user if a contact deletion has happened since he last logged in // Notify the user if a contact deletion has happened since he last logged in
if (user.getLatestContactDeletion().isAfter(user.getLastSeen())) writeProxy.write(socketID, new ContactsChangedSinceLastLogin()); if (user.getLatestContactDeletion().isAfter(user.getLastSeen()))
writeProxy.write(socketID, new ContactsChangedSinceLastLogin());
// Complete the handshake // Complete the handshake
writeProxy.write(socketID, user.toCommon()); writeProxy.write(socketID, user.toCommon());

View File

@ -8,9 +8,10 @@ import javax.persistence.EntityExistsException;
import envoy.data.Message; import envoy.data.Message;
import envoy.event.*; import envoy.event.*;
import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.util.EnvoyLog;
/** /**
* This {@link ObjectProcessor} handles incoming {@link Message}s. * This {@link ObjectProcessor} handles incoming {@link Message}s.
@ -23,7 +24,8 @@ public final class MessageProcessor implements ObjectProcessor<Message> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(MessageProcessor.class); private static final Logger logger =
EnvoyLog.getLogger(MessageProcessor.class);
@Override @Override
public void process(Message message, long socketID, ObjectWriteProxy writeProxy) { public void process(Message message, long socketID, ObjectWriteProxy writeProxy) {

View File

@ -5,9 +5,10 @@ import java.util.logging.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange; import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.util.EnvoyLog;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -17,10 +18,12 @@ public final class MessageStatusChangeProcessor implements ObjectProcessor<Messa
private final ConnectionManager connectionManager = ConnectionManager.getInstance(); private final ConnectionManager connectionManager = ConnectionManager.getInstance();
private final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class); private final Logger logger =
EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
@Override @Override
public void process(MessageStatusChange statusChange, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(MessageStatusChange statusChange, long socketID,
ObjectWriteProxy writeProxy) throws IOException {
// Any other status than READ is not supposed to be sent to the server // Any other status than READ is not supposed to be sent to the server
if (statusChange.get() != MessageStatus.READ) { if (statusChange.get() != MessageStatus.READ) {
@ -34,6 +37,7 @@ public final class MessageStatusChangeProcessor implements ObjectProcessor<Messa
// Notifies the sender of the message about the status-update to READ // Notifies the sender of the message about the status-update to READ
final long senderID = msg.getSender().getID(); final long senderID = msg.getSender().getID();
if (connectionManager.isOnline(senderID)) writeProxy.write(connectionManager.getSocketID(senderID), statusChange); if (connectionManager.isOnline(senderID))
writeProxy.write(connectionManager.getSocketID(senderID), statusChange);
} }
} }

View File

@ -3,6 +3,7 @@ package envoy.server.processors;
import java.io.IOException; import java.io.IOException;
import envoy.event.NameChange; import envoy.event.NameChange;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
@ -15,7 +16,8 @@ public final class NameChangeProcessor implements ObjectProcessor<NameChange> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override @Override
public void process(NameChange nameChange, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(NameChange nameChange, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
Contact toUpdate = persistenceManager.getContactByID(nameChange.getID()); Contact toUpdate = persistenceManager.getContactByID(nameChange.getID());
toUpdate.setName(nameChange.get()); toUpdate.setName(nameChange.get());
persistenceManager.updateContact(toUpdate); persistenceManager.updateContact(toUpdate);

View File

@ -5,8 +5,7 @@ import java.io.IOException;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
/** /**
* This interface defines methods for processing objects of a specific * This interface defines methods for processing objects of a specific type incoming from a client.
* type incoming from a client.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @param <T> type of the request object * @param <T> type of the request object

View File

@ -4,27 +4,34 @@ import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import envoy.event.*; import envoy.event.*;
import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
import envoy.server.util.PasswordUtil; import envoy.server.util.PasswordUtil;
import envoy.util.EnvoyLog;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Server v0.2-beta * @since Envoy Server v0.2-beta
*/ */
public final class PasswordChangeRequestProcessor implements ObjectProcessor<PasswordChangeRequest> { public final class PasswordChangeRequestProcessor
implements ObjectProcessor<PasswordChangeRequest> {
@Override @Override
public void process(PasswordChangeRequest event, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(PasswordChangeRequest event, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
final var persistenceManager = PersistenceManager.getInstance(); final var persistenceManager = PersistenceManager.getInstance();
final var user = persistenceManager.getUserByID(event.getID()); final var user = persistenceManager.getUserByID(event.getID());
final var logger = EnvoyLog.getLogger(PasswordChangeRequestProcessor.class); final var logger =
final var correctAuthentication = PasswordUtil.validate(event.getOldPassword(), user.getPasswordHash()); EnvoyLog.getLogger(PasswordChangeRequestProcessor.class);
final var correctAuthentication =
PasswordUtil.validate(event.getOldPassword(), user.getPasswordHash());
if (correctAuthentication) { if (correctAuthentication) {
user.setPasswordHash(PasswordUtil.hash(event.get())); user.setPasswordHash(PasswordUtil.hash(event.get()));
logger.log(Level.INFO, user + " changed his password"); logger.log(Level.INFO, user + " changed his password");
} else logger.log(Level.INFO, user + " tried changing his password but provided insufficient authentication"); } else
logger.log(Level.INFO,
user + " tried changing his password but provided insufficient authentication");
writeProxy.write(socketID, new PasswordChangeResult(correctAuthentication)); writeProxy.write(socketID, new PasswordChangeResult(correctAuthentication));
} }
} }

View File

@ -3,6 +3,7 @@ package envoy.server.processors;
import java.io.IOException; import java.io.IOException;
import envoy.event.ProfilePicChange; import envoy.event.ProfilePicChange;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
/** /**
@ -12,5 +13,6 @@ import envoy.server.net.ObjectWriteProxy;
public final class ProfilePicChangeProcessor implements ObjectProcessor<ProfilePicChange> { public final class ProfilePicChangeProcessor implements ObjectProcessor<ProfilePicChange> {
@Override @Override
public void process(ProfilePicChange event, long socketID, ObjectWriteProxy writeProxy) throws IOException {} public void process(ProfilePicChange event, long socketID, ObjectWriteProxy writeProxy)
throws IOException {}
} }

View File

@ -5,9 +5,10 @@ import java.util.logging.*;
import envoy.event.ElementOperation; import envoy.event.ElementOperation;
import envoy.event.contact.UserOperation; import envoy.event.contact.UserOperation;
import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.util.EnvoyLog;
/** /**
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
@ -16,7 +17,8 @@ import envoy.util.EnvoyLog;
public final class UserOperationProcessor implements ObjectProcessor<UserOperation> { public final class UserOperationProcessor implements ObjectProcessor<UserOperation> {
private static final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserOperationProcessor.class); private static final Logger logger =
EnvoyLog.getLogger(UserOperationProcessor.class);
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override @Override
@ -26,12 +28,14 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
final var sender = persistenceManager.getUserByID(userID); final var sender = persistenceManager.getUserByID(userID);
switch (evt.getOperationType()) { switch (evt.getOperationType()) {
case ADD: case ADD:
logger.log(Level.FINE, String.format("Adding %s to the contact list of user %d.", evt.get(), userID)); logger.log(Level.FINE,
String.format("Adding %s to the contact list of user %d.", evt.get(), userID));
persistenceManager.addContactBidirectional(userID, contactID); persistenceManager.addContactBidirectional(userID, contactID);
// Notify the contact if online // Notify the contact if online
if (connectionManager.isOnline(contactID)) if (connectionManager.isOnline(contactID))
writeProxy.write(connectionManager.getSocketID(contactID), new UserOperation(sender.toCommon(), ElementOperation.ADD)); writeProxy.write(connectionManager.getSocketID(contactID),
new UserOperation(sender.toCommon(), ElementOperation.ADD));
break; break;
case REMOVE: case REMOVE:
@ -45,7 +49,8 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
// Notify the removed contact if online // Notify the removed contact if online
if (connectionManager.isOnline(contactID)) if (connectionManager.isOnline(contactID))
writeProxy.write(connectionManager.getSocketID(contactID), new UserOperation(sender.toCommon(), ElementOperation.REMOVE)); writeProxy.write(connectionManager.getSocketID(contactID),
new UserOperation(sender.toCommon(), ElementOperation.REMOVE));
break; break;
} }
} }

View File

@ -5,6 +5,7 @@ import java.util.stream.Collectors;
import envoy.data.Contact; import envoy.data.Contact;
import envoy.event.contact.*; import envoy.event.contact.*;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.*; import envoy.server.net.*;
@ -16,19 +17,21 @@ import envoy.server.net.*;
public final class UserSearchProcessor implements ObjectProcessor<UserSearchRequest> { public final class UserSearchProcessor implements ObjectProcessor<UserSearchRequest> {
/** /**
* Writes a list of contacts to the client containing all {@link Contact}s * Writes a list of contacts to the client containing all {@link Contact}s matching the search
* matching the search phrase contained inside the request. The client and their * phrase contained inside the request. The client and their contacts are excluded from the
* contacts are excluded from the result. * result.
* *
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
@Override @Override
public void process(UserSearchRequest request, long socketID, ObjectWriteProxy writeProxy) throws IOException { public void process(UserSearchRequest request, long socketID, ObjectWriteProxy writeProxy)
throws IOException {
writeProxy.write(socketID, writeProxy.write(socketID,
new UserSearchResult(PersistenceManager.getInstance() new UserSearchResult(PersistenceManager.getInstance()
.searchUsers(request.get(), ConnectionManager.getInstance().getUserIDBySocketID(socketID)) .searchUsers(request.get(),
.stream() ConnectionManager.getInstance().getUserIDBySocketID(socketID))
.map(User::toCommon) .stream()
.collect(Collectors.toList()))); .map(User::toCommon)
.collect(Collectors.toList())));
} }
} }

View File

@ -4,9 +4,10 @@ import java.util.logging.Logger;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import envoy.event.UserStatusChange; import envoy.event.UserStatusChange;
import envoy.util.EnvoyLog;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/** /**
* This processor handles incoming {@link UserStatusChange}. * This processor handles incoming {@link UserStatusChange}.
@ -19,7 +20,8 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
private static ObjectWriteProxy writeProxy; private static ObjectWriteProxy writeProxy;
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserStatusChangeProcessor.class); private static final Logger logger =
EnvoyLog.getLogger(UserStatusChangeProcessor.class);
@Override @Override
public void process(UserStatusChange input, long socketID, ObjectWriteProxy writeProxy) { public void process(UserStatusChange input, long socketID, ObjectWriteProxy writeProxy) {
@ -32,8 +34,8 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
} }
/** /**
* Sets the {@link UserStatus} for a given user. Both offline contacts and * Sets the {@link UserStatus} for a given user. Both offline contacts and currently online
* currently online contacts are notified. * contacts are notified.
* *
* @param user the user whose status has changed * @param user the user whose status has changed
* @param newStatus the new status of that user * @param newStatus the new status of that user
@ -47,19 +49,21 @@ public final class UserStatusChangeProcessor implements ObjectProcessor<UserStat
persistenceManager.updateContact(user); persistenceManager.updateContact(user);
// Handling for contacts that are already online // Handling for contacts that are already online
writeProxy.writeToOnlineContacts(user.getContacts(), new UserStatusChange(user.getID(), user.getStatus())); writeProxy.writeToOnlineContacts(user.getContacts(),
new UserStatusChange(user.getID(), user.getStatus()));
} }
/** /**
* This method is only called by the LoginCredentialProcessor because every * This method is only called by the LoginCredentialProcessor because every user needs to login
* user needs to login (open a socket) before changing his status. * (open a socket) before changing his status. Needed to ensure propagation of events because an
* Needed to ensure propagation of events because an uninitialized writeProxy * uninitialized writeProxy would cause problems.
* would cause problems.
* *
* @param writeProxy the writeProxy that is used to send objects back to clients * @param writeProxy the writeProxy that is used to send objects back to clients
* @since Envoy Server Standalone v0.1-alpha * @since Envoy Server Standalone v0.1-alpha
*/ */
public static void setWriteProxy(ObjectWriteProxy writeProxy) { UserStatusChangeProcessor.writeProxy = writeProxy; } public static void setWriteProxy(ObjectWriteProxy writeProxy) {
UserStatusChangeProcessor.writeProxy = writeProxy;
}
// TODO may cause an problem if two clients log in at the same time. // TODO may cause an problem if two clients log in at the same time.
// Change Needed. // Change Needed.
} }

View File

@ -1,6 +1,5 @@
/** /**
* This package contains all classes that process data received from client * This package contains all classes that process data received from client connections.
* connections.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister

View File

@ -11,7 +11,8 @@ import java.security.SecureRandom;
public final class AuthTokenGenerator { public final class AuthTokenGenerator {
private static final int TOKEN_LENGTH = 128; private static final int TOKEN_LENGTH = 128;
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); private static final char[] CHARACTERS =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final char[] BUFF = new char[TOKEN_LENGTH]; private static final char[] BUFF = new char[TOKEN_LENGTH];
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();

View File

@ -7,9 +7,9 @@ import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEKeySpec;
/** /**
* Provides a password hashing and comparison mechanism using the * Provides a password hashing and comparison mechanism using the {@code PBKDF2WithHmacSHA1}
* {@code PBKDF2WithHmacSHA1} algorithm. * algorithm.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */
@ -37,7 +37,8 @@ public final class PasswordUtil {
byte[] salt = fromHex(parts[1]); byte[] salt = fromHex(parts[1]);
byte[] hash = fromHex(parts[2]); byte[] hash = fromHex(parts[2]);
var spec = new PBEKeySpec(current.toCharArray(), salt, iterations, KEY_LENGTH); var spec =
new PBEKeySpec(current.toCharArray(), salt, iterations, KEY_LENGTH);
var skf = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM); var skf = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
byte[] testHash = skf.generateSecret(spec).getEncoded(); byte[] testHash = skf.generateSecret(spec).getEncoded();
@ -53,7 +54,7 @@ public final class PasswordUtil {
/** /**
* Creates a parameterized salted password hash. * Creates a parameterized salted password hash.
* *
* @param password the password to hash * @param password the password to hash
* @return a result string in the form of {@code iterations:salt:hash} * @return a result string in the form of {@code iterations:salt:hash}
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta

View File

@ -3,8 +3,8 @@ package envoy.server.util;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* Implements a comparison algorithm between Envoy versions and defines minimal * Implements a comparison algorithm between Envoy versions and defines minimal and maximal client
* and maximal client versions compatible with this server. * versions compatible with this server.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
@ -25,13 +25,14 @@ public final class VersionUtil {
*/ */
public static final String MAX_CLIENT_VERSION = "0.2-beta"; public static final String MAX_CLIENT_VERSION = "0.2-beta";
private static final Pattern versionPattern = Pattern.compile("(?<major>\\d).(?<minor>\\d)(?:-(?<suffix>\\w+))?"); private static final Pattern versionPattern =
Pattern.compile("(?<major>\\d).(?<minor>\\d)(?:-(?<suffix>\\w+))?");
private VersionUtil() {} private VersionUtil() {}
/** /**
* Parses an Envoy Client version string and checks whether that version is * Parses an Envoy Client version string and checks whether that version is compatible with this
* compatible with this server. * server.
* *
* @param version the version string to parse * @param version the version string to parse
* @return {@code true} if the given version is compatible with this server * @return {@code true} if the given version is compatible with this server
@ -40,12 +41,14 @@ public final class VersionUtil {
public static boolean verifyCompatibility(String version) { public static boolean verifyCompatibility(String version) {
final var currentMatcher = versionPattern.matcher(version); final var currentMatcher = versionPattern.matcher(version);
if (!currentMatcher.matches()) return false; if (!currentMatcher.matches())
return false;
final var minMatcher = versionPattern.matcher(MIN_CLIENT_VERSION); final var minMatcher = versionPattern.matcher(MIN_CLIENT_VERSION);
final var maxMatcher = versionPattern.matcher(MAX_CLIENT_VERSION); final var maxMatcher = versionPattern.matcher(MAX_CLIENT_VERSION);
if (!minMatcher.matches() || !maxMatcher.matches()) throw new RuntimeException("Invalid min or max client version configured!"); if (!minMatcher.matches() || !maxMatcher.matches())
throw new RuntimeException("Invalid min or max client version configured!");
// Compare suffixes // Compare suffixes
{ {
@ -53,7 +56,8 @@ public final class VersionUtil {
final var minSuffix = convertSuffix(minMatcher.group("suffix")); final var minSuffix = convertSuffix(minMatcher.group("suffix"));
final var maxSuffix = convertSuffix(maxMatcher.group("suffix")); final var maxSuffix = convertSuffix(maxMatcher.group("suffix"));
if (currentSuffix < minSuffix || currentSuffix > maxSuffix) return false; if (currentSuffix < minSuffix || currentSuffix > maxSuffix)
return false;
} }
// Compare major // Compare major
@ -62,7 +66,8 @@ public final class VersionUtil {
final var minMajor = Integer.parseInt(minMatcher.group("major")); final var minMajor = Integer.parseInt(minMatcher.group("major"));
final var maxMajor = Integer.parseInt(maxMatcher.group("major")); final var maxMajor = Integer.parseInt(maxMatcher.group("major"));
if (currentMajor < minMajor || currentMajor > maxMajor) return false; if (currentMajor < minMajor || currentMajor > maxMajor)
return false;
} }
// Compare minor // Compare minor
@ -71,7 +76,8 @@ public final class VersionUtil {
final var minMinor = Integer.parseInt(minMatcher.group("minor")); final var minMinor = Integer.parseInt(minMatcher.group("minor"));
final var maxMinor = Integer.parseInt(maxMatcher.group("minor")); final var maxMinor = Integer.parseInt(maxMatcher.group("minor"));
if (currentMinor < minMinor || currentMinor > maxMinor) return false; if (currentMinor < minMinor || currentMinor > maxMinor)
return false;
} }
return true; return true;

View File

@ -1,6 +1,6 @@
/** /**
* This package contains utility classes used in Envoy Server. * This package contains utility classes used in Envoy Server.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-beta * @since Envoy Server Standalone v0.1-beta
*/ */

View File

@ -1,6 +1,5 @@
/** /**
* This module contains all classes defining the server application of the Envoy * This module contains all classes defining the server application of the Envoy project.
* project.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister