Move Envoy Client to client/ subdirectory
This commit is contained in:
65
client/src/main/java/envoy/client/data/Cache.java
Normal file
65
client/src/main/java/envoy/client/data/Cache.java
Normal file
@ -0,0 +1,65 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Stores elements in a queue to process them later.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Cache.java</strong><br>
|
||||
* Created: <strong>6 Feb 2020</strong><br>
|
||||
*
|
||||
* @param <T> the type of cached elements
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public final class Cache<T> implements Consumer<T>, Serializable {
|
||||
|
||||
private final Queue<T> elements = new LinkedList<>();
|
||||
private transient Consumer<T> processor;
|
||||
|
||||
private static final Logger logger = EnvoyLog.getLogger(Cache.class);
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
/**
|
||||
* Adds an element to the cache.
|
||||
*
|
||||
* @param element the element to add
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@Override
|
||||
public void accept(T element) {
|
||||
logger.log(Level.FINE, String.format("Adding element %s to cache", element));
|
||||
elements.offer(element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("Cache[elements=" + elements + "]"); }
|
||||
|
||||
/**
|
||||
* Sets the processor to which cached elements are relayed.
|
||||
*
|
||||
* @param processor the processor to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setProcessor(Consumer<T> processor) { this.processor = processor; }
|
||||
|
||||
/**
|
||||
* Relays all cached elements to the processor.
|
||||
*
|
||||
* @throws IllegalStateException if the processor is not initialized
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void relay() {
|
||||
if (processor == null) throw new IllegalStateException("Processor is not defined");
|
||||
elements.forEach(processor::accept);
|
||||
elements.clear();
|
||||
}
|
||||
}
|
66
client/src/main/java/envoy/client/data/CacheMap.java
Normal file
66
client/src/main/java/envoy/client/data/CacheMap.java
Normal file
@ -0,0 +1,66 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Stores a heterogeneous map of {@link Cache} objects with different type
|
||||
* parameters.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>CacheMap.java</strong><br>
|
||||
* Created: <strong>09.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class CacheMap implements Serializable {
|
||||
|
||||
private final Map<Class<?>, Cache<?>> map = new HashMap<>();
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Adds a cache to the map.
|
||||
*
|
||||
* @param <T> the type accepted by the cache
|
||||
* @param key the class that maps to the cache
|
||||
* @param cache the cache to store
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); }
|
||||
|
||||
/**
|
||||
* Returns a cache mapped by a class.
|
||||
*
|
||||
* @param <T> the type accepted by the cache
|
||||
* @param key the class that maps to the cache
|
||||
* @return the cache
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param <T> the type accepted by the cache
|
||||
* @param key the class that maps to the cache
|
||||
* @return the cache
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public <T> Cache<? super T> getApplicable(Class<T> key) {
|
||||
Cache<? super T> cache = get(key);
|
||||
if (cache == null)
|
||||
for (var e : map.entrySet())
|
||||
if (e.getKey().isAssignableFrom(key))
|
||||
cache = (Cache<? super T>) e.getValue();
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the map in which the caches are stored
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public Map<Class<?>, Cache<?>> getMap() { return map; }
|
||||
}
|
153
client/src/main/java/envoy/client/data/Chat.java
Normal file
153
client/src/main/java/envoy/client/data/Chat.java
Normal file
@ -0,0 +1,153 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.MessageStatusChange;
|
||||
|
||||
/**
|
||||
* Represents a chat between two {@link User}s
|
||||
* as a list of {@link Message} objects.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Chat.java</strong><br>
|
||||
* Created: <strong>19 Oct 2019</strong><br>
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @author Leon Hofmeister
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public class Chat implements Serializable {
|
||||
|
||||
protected final Contact recipient;
|
||||
protected final List<Message> messages = new ArrayList<>();
|
||||
|
||||
protected int unreadAmount;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Provides the list of messages that the recipient receives.
|
||||
* <p>
|
||||
* Saves the Messages in the corresponding chat at that Point.
|
||||
*
|
||||
* @param recipient the user who receives the messages
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public Chat(Contact recipient) {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
|
||||
|
||||
/**
|
||||
* Generates a hash code based on the recipient.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() { return Objects.hash(recipient); }
|
||||
|
||||
/**
|
||||
* Tests equality to another object based on the recipient.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Chat)) return false;
|
||||
Chat other = (Chat) obj;
|
||||
return Objects.equals(recipient, other.recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status of all chat messages received from the recipient to
|
||||
* {@code READ} starting from the bottom and stopping once a read message is
|
||||
* found.
|
||||
*
|
||||
* @param writeProxy the write proxy instance used to notify the server about
|
||||
* the message status changes
|
||||
* @throws IOException if a {@link MessageStatusChange} could not be
|
||||
* delivered to the server
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void read(WriteProxy writeProxy) throws IOException {
|
||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||
final Message m = messages.get(i);
|
||||
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
|
||||
else {
|
||||
m.setStatus(MessageStatus.READ);
|
||||
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
|
||||
}
|
||||
}
|
||||
unreadAmount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the newest message received in the chat doesn't have
|
||||
* the status {@code READ}
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param message the message to insert
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void insert(Message message) {
|
||||
for (int i = messages.size() - 1; i >= 0; --i)
|
||||
if (message.getCreationDate().isAfter(messages.get(i).getCreationDate())) {
|
||||
messages.add(i + 1, message);
|
||||
return;
|
||||
}
|
||||
messages.add(0, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the amount of unread messages.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void incrementUnreadAmount() { unreadAmount++; }
|
||||
|
||||
/**
|
||||
* @return the amount of unread mesages in this chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public int getUnreadAmount() { return unreadAmount; }
|
||||
|
||||
/**
|
||||
* @return all messages in the current chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public List<Message> getMessages() { return messages; }
|
||||
|
||||
/**
|
||||
* @return the recipient of a message
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public Contact getRecipient() { return recipient; }
|
||||
|
||||
/**
|
||||
* @return whether this {@link Chat} points at a {@link User}
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isUserChat() { return recipient instanceof User; }
|
||||
|
||||
/**
|
||||
* @return whether this {@link Chat} points at a {@link Group}
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isGroupChat() { return recipient instanceof Group; }
|
||||
}
|
115
client/src/main/java/envoy/client/data/ClientConfig.java
Normal file
115
client/src/main/java/envoy/client/data/ClientConfig.java
Normal file
@ -0,0 +1,115 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import static java.util.function.Function.identity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import envoy.client.ui.Startup;
|
||||
import envoy.data.Config;
|
||||
import envoy.data.ConfigItem;
|
||||
import envoy.data.LoginCredentials;
|
||||
|
||||
/**
|
||||
* Implements a configuration specific to the Envoy Client with default values
|
||||
* and convenience methods.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>ClientConfig.java</strong><br>
|
||||
* Created: <strong>01.03.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class ClientConfig extends Config {
|
||||
|
||||
private static ClientConfig config;
|
||||
|
||||
/**
|
||||
* @return the singleton instance of the client config
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static ClientConfig getInstance() {
|
||||
if (config == null) config = new ClientConfig();
|
||||
return config;
|
||||
}
|
||||
|
||||
private ClientConfig() {
|
||||
items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
|
||||
items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
|
||||
items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
|
||||
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
|
||||
items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
|
||||
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
|
||||
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
|
||||
items.put("user", new ConfigItem<>("user", "u", identity()));
|
||||
items.put("password", new ConfigItem<>("password", "pw", identity()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the host name of the Envoy server
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public String getServer() { return (String) items.get("server").get(); }
|
||||
|
||||
/**
|
||||
* @return the port at which the Envoy server is located on the host
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public Integer getPort() { return (Integer) items.get("port").get(); }
|
||||
|
||||
/**
|
||||
* @return the local database specific to the client user
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public File getLocalDB() { return (File) items.get("localDB").get(); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if the local database is to be ignored
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
|
||||
|
||||
/**
|
||||
* @return the directory in which all local files are saves
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
|
||||
|
||||
/**
|
||||
* @return the minimal {@link Level} to log inside the log file
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
|
||||
|
||||
/**
|
||||
* @return the minimal {@link Level} to log inside the console
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
|
||||
|
||||
/**
|
||||
* @return the user name
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public String getUser() { return (String) items.get("user").get(); }
|
||||
|
||||
/**
|
||||
* @return the password
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public String getPassword() { return (String) items.get("password").get(); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if user name and password are set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
|
||||
|
||||
/**
|
||||
* @return login credentials for the specified user name and password, without
|
||||
* the registration option
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
|
||||
}
|
55
client/src/main/java/envoy/client/data/GroupChat.java
Normal file
55
client/src/main/java/envoy/client/data/GroupChat.java
Normal file
@ -0,0 +1,55 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import envoy.client.net.WriteProxy;
|
||||
import envoy.data.Contact;
|
||||
import envoy.data.GroupMessage;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.data.User;
|
||||
import envoy.event.GroupMessageStatusChange;
|
||||
|
||||
/**
|
||||
* Represents a chat between a user and a group
|
||||
* as a list of messages.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>GroupChat.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public class GroupChat extends Chat {
|
||||
|
||||
private final User sender;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @param sender the user sending the messages
|
||||
* @param recipient the group whose members receive the messages
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public GroupChat(User sender, Contact recipient) {
|
||||
super(recipient);
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(WriteProxy writeProxy) throws IOException {
|
||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||
final GroupMessage gmsg = (GroupMessage) messages.get(i);
|
||||
if (gmsg.getSenderID() != sender.getID()) {
|
||||
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
|
||||
else {
|
||||
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
|
||||
writeProxy
|
||||
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
|
||||
}
|
||||
}
|
||||
}
|
||||
unreadAmount = 0;
|
||||
}
|
||||
}
|
205
client/src/main/java/envoy/client/data/LocalDB.java
Normal file
205
client/src/main/java/envoy/client/data/LocalDB.java
Normal file
@ -0,0 +1,205 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import envoy.data.*;
|
||||
import envoy.event.GroupResize;
|
||||
import envoy.event.MessageStatusChange;
|
||||
import envoy.event.NameChange;
|
||||
|
||||
/**
|
||||
* Stores information about the current {@link User} and their {@link Chat}s.
|
||||
* For message ID generation a {@link IDGenerator} is stored as well.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>LocalDB.java</strong><br>
|
||||
* Created: <strong>3 Feb 2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public abstract class LocalDB {
|
||||
|
||||
protected User user;
|
||||
protected Map<String, Contact> users = new HashMap<>();
|
||||
protected List<Chat> chats = new ArrayList<>();
|
||||
protected IDGenerator idGenerator;
|
||||
protected CacheMap cacheMap = new CacheMap();
|
||||
|
||||
{
|
||||
cacheMap.put(Message.class, new Cache<>());
|
||||
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a storage space for a user-specific list of chats.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void initializeUserStorage() {}
|
||||
|
||||
/**
|
||||
* Stores all users. If the client user is specified, their chats will be stored
|
||||
* as well. The message id generator will also be saved if present.
|
||||
*
|
||||
* @throws Exception if the saving process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void save() throws Exception {}
|
||||
|
||||
/**
|
||||
* Loads all user data.
|
||||
*
|
||||
* @throws Exception if the loading process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadUsers() throws Exception {}
|
||||
|
||||
/**
|
||||
* Loads all data of the client user.
|
||||
*
|
||||
* @throws Exception if the loading process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadUserData() throws Exception {}
|
||||
|
||||
/**
|
||||
* Loads the ID generator. Any exception thrown during this process is ignored.
|
||||
*
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void loadIDGenerator() {}
|
||||
|
||||
/**
|
||||
* Synchronizes the contact list of the client user with the chat and user
|
||||
* storage.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void synchronize() {
|
||||
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
|
||||
users.put(user.getName(), user);
|
||||
|
||||
// Synchronize user status data
|
||||
for (Contact contact : users.values())
|
||||
if (contact instanceof User)
|
||||
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
|
||||
|
||||
// Create missing chats
|
||||
user.getContacts()
|
||||
.stream()
|
||||
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
|
||||
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
|
||||
.forEach(chats::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||
* user names as keys
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Map<String, Contact> getUsers() { return users; }
|
||||
|
||||
/**
|
||||
* @return all saved {@link Chat} objects that list the client user as the
|
||||
* sender
|
||||
* @since Envoy Client v0.1-alpha
|
||||
**/
|
||||
public List<Chat> getChats() { return chats; }
|
||||
|
||||
/**
|
||||
* @param chats the chats to set
|
||||
*/
|
||||
public void setChats(List<Chat> chats) { this.chats = chats; }
|
||||
|
||||
/**
|
||||
* @return the {@link User} who initialized the local database
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public User getUser() { return user; }
|
||||
|
||||
/**
|
||||
* @param user the user to set
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void setUser(User user) { this.user = user; }
|
||||
|
||||
/**
|
||||
* @return the message ID generator
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public IDGenerator getIDGenerator() { return idGenerator; }
|
||||
|
||||
/**
|
||||
* @param idGenerator the message ID generator to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
|
||||
|
||||
/**
|
||||
* @return {@code true} if an {@link IDGenerator} is present
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public boolean hasIDGenerator() { return idGenerator != null; }
|
||||
|
||||
/**
|
||||
* @return the cache map for messages and message status changes
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public CacheMap getCacheMap() { return cacheMap; }
|
||||
|
||||
/**
|
||||
* Searches for a message by ID.
|
||||
*
|
||||
* @param id the ID of the message to search for
|
||||
* @return an optional containing the message
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public Optional<Message> getMessage(long id) {
|
||||
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a chat by recipient ID.
|
||||
*
|
||||
* @param recipientID the ID of the chat's recipient
|
||||
* @return an optional containing the chat
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
|
||||
|
||||
/**
|
||||
* Performs a contact name change if the corresponding contact is present.
|
||||
*
|
||||
* @param event the {@link NameChange} to process
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void replaceContactName(NameChange event) {
|
||||
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a group resize operation if the corresponding group is present.
|
||||
*
|
||||
* @param event the {@link GroupResize} to process
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void updateGroup(GroupResize event) {
|
||||
chats.stream()
|
||||
.map(Chat::getRecipient)
|
||||
.filter(Group.class::isInstance)
|
||||
.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
|
||||
.map(Group.class::cast)
|
||||
.findAny()
|
||||
.ifPresent(group -> {
|
||||
switch (event.getOperation()) {
|
||||
case ADD:
|
||||
group.getContacts().add(event.get());
|
||||
break;
|
||||
case REMOVE:
|
||||
group.getContacts().remove(event.get());
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import envoy.data.IDGenerator;
|
||||
import envoy.util.SerializationUtils;
|
||||
|
||||
/**
|
||||
* Implements a {@link LocalDB} in a way that stores all information inside a
|
||||
* folder on the local file system.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>PersistentLocalDB.java</strong><br>
|
||||
* Created: <strong>27.10.2019</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public final class PersistentLocalDB extends LocalDB {
|
||||
|
||||
private File dbDir, userFile, idGeneratorFile, usersFile;
|
||||
|
||||
/**
|
||||
* Constructs an empty local database. To serialize any user-specific data to
|
||||
* the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
|
||||
* and then {@link PersistentLocalDB#save()}.
|
||||
*
|
||||
* @param dbDir the directory in which to persist data
|
||||
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
public PersistentLocalDB(File dbDir) throws IOException {
|
||||
this.dbDir = dbDir;
|
||||
|
||||
// Test if the database directory is actually a directory
|
||||
if (dbDir.exists() && !dbDir.isDirectory())
|
||||
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||
|
||||
// Initialize global files
|
||||
idGeneratorFile = new File(dbDir, "id_gen.db");
|
||||
usersFile = new File(dbDir, "users.db");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database file for a user-specific list of chats.
|
||||
*
|
||||
* @throws IllegalStateException if the client user is not specified
|
||||
* @since Envoy Client v0.1-alpha
|
||||
*/
|
||||
@Override
|
||||
public void initializeUserStorage() {
|
||||
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||
userFile = new File(dbDir, user.getID() + ".db");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() throws IOException {
|
||||
// Save users
|
||||
SerializationUtils.write(usersFile, users);
|
||||
|
||||
// Save user data
|
||||
if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
|
||||
|
||||
// Save id generator
|
||||
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
|
||||
|
||||
@Override
|
||||
public void loadUserData() throws ClassNotFoundException, IOException {
|
||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||
chats = (ArrayList<Chat>) in.readObject();
|
||||
cacheMap = (CacheMap) in.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadIDGenerator() {
|
||||
try {
|
||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||
} catch (ClassNotFoundException | IOException e) {}
|
||||
}
|
||||
}
|
146
client/src/main/java/envoy/client/data/Settings.java
Normal file
146
client/src/main/java/envoy/client/data/Settings.java
Normal file
@ -0,0 +1,146 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import envoy.util.SerializationUtils;
|
||||
|
||||
/**
|
||||
* Manages all application settings, which are different objects that can be
|
||||
* changed during runtime and serialized them by using either the file system or
|
||||
* the {@link Preferences} API.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>Settings.java</strong><br>
|
||||
* Created: <strong>11 Nov 2019</strong><br>
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
// Actual settings accessible by the rest of the application
|
||||
private Map<String, SettingsItem<?>> items;
|
||||
|
||||
/**
|
||||
* Settings are stored in this file.
|
||||
*/
|
||||
private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
|
||||
|
||||
/**
|
||||
* Singleton instance of this class.
|
||||
*/
|
||||
private static Settings settings = new Settings();
|
||||
|
||||
/**
|
||||
* The way to instantiate the settings. Is set to private to deny other
|
||||
* instances of that object.
|
||||
*
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
private Settings() {
|
||||
// Load settings from settings file
|
||||
try {
|
||||
items = SerializationUtils.read(settingsFile, HashMap.class);
|
||||
} catch (ClassNotFoundException | IOException e) {
|
||||
items = new HashMap<>();
|
||||
}
|
||||
supplementDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to ensure that there is only one instance of Settings.
|
||||
*
|
||||
* @return the instance of Settings
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public static Settings getInstance() { return settings; }
|
||||
|
||||
/**
|
||||
* Updates the preferences when the save button is clicked.
|
||||
*
|
||||
* @throws IOException if an error occurs while saving the themes
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void save() throws IOException {
|
||||
|
||||
// Save settings to settings file
|
||||
SerializationUtils.write(settingsFile, items);
|
||||
}
|
||||
|
||||
private void supplementDefaults() {
|
||||
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
|
||||
items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "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."));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name of the currently active theme
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public String getCurrentTheme() { return (String) items.get("currentTheme").get(); }
|
||||
|
||||
/**
|
||||
* Sets the name of the current theme.
|
||||
*
|
||||
* @param themeName the name to set
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
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
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isUsingDefaultTheme() {
|
||||
final var theme = getCurrentTheme();
|
||||
return theme.equals("dark") || theme.equals("light");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true}, if pressing the {@code Enter} key suffices to send a
|
||||
* message. Otherwise it has to be pressed in conjunction with the
|
||||
* {@code Control} key.
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the {@code Enter} key. Otherwise it has to be pressed in
|
||||
* conjunction with the {@code Control} key.
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
|
||||
|
||||
/**
|
||||
* @return the current on close mode.
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
|
||||
|
||||
/**
|
||||
* Sets the current on close mode.
|
||||
*
|
||||
* @param currentOnCloseMode the on close mode that should be set.
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); }
|
||||
|
||||
/**
|
||||
* @return the items
|
||||
*/
|
||||
public Map<String, SettingsItem<?>> getItems() { return items; }
|
||||
|
||||
/**
|
||||
* @param items the items to set
|
||||
*/
|
||||
public void setItems(Map<String, SettingsItem<?>> items) { this.items = items; }
|
||||
}
|
99
client/src/main/java/envoy/client/data/SettingsItem.java
Normal file
99
client/src/main/java/envoy/client/data/SettingsItem.java
Normal file
@ -0,0 +1,99 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
|
||||
/**
|
||||
* Encapsulates a persistent value that is directly or indirectly mutable by the
|
||||
* user.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>SettingsItem.java</strong><br>
|
||||
* Created: <strong>23.12.2019</strong><br>
|
||||
*
|
||||
* @param <T> the type of this {@link SettingsItem}'s value
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public class SettingsItem<T> implements Serializable {
|
||||
|
||||
private T value;
|
||||
private String userFriendlyName, description;
|
||||
|
||||
private transient Consumer<T> changeHandler;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Initializes a {@link SettingsItem}. The default value's class will be mapped
|
||||
* to a {@link JComponent} that can be used to display this {@link SettingsItem}
|
||||
* to the user.
|
||||
*
|
||||
* @param value the default value
|
||||
* @param userFriendlyName the user friendly name (short)
|
||||
* @param description the description (long)
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public SettingsItem(T value, String userFriendlyName, String description) {
|
||||
this.value = value;
|
||||
this.userFriendlyName = userFriendlyName;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the value
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public T get() { return value; }
|
||||
|
||||
/**
|
||||
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
|
||||
* defined, it will be invoked with this value.
|
||||
*
|
||||
* @param value the value to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void set(T value) {
|
||||
if (changeHandler != null && value != this.value) changeHandler.accept(value);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the userFriendlyName
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public String getUserFriendlyName() { return userFriendlyName; }
|
||||
|
||||
/**
|
||||
* @param userFriendlyName the userFriendlyName to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
|
||||
|
||||
/**
|
||||
* @return the description
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public String getDescription() { return description; }
|
||||
|
||||
/**
|
||||
* @param description the description to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
/**
|
||||
* Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be
|
||||
* invoked with the current value once during the registration and every time
|
||||
* when the value changes.
|
||||
*
|
||||
* @param changeHandler the changeHandler to set
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public void setChangeHandler(Consumer<T> changeHandler) {
|
||||
this.changeHandler = changeHandler;
|
||||
changeHandler.accept(value);
|
||||
}
|
||||
}
|
15
client/src/main/java/envoy/client/data/TransientLocalDB.java
Normal file
15
client/src/main/java/envoy/client/data/TransientLocalDB.java
Normal file
@ -0,0 +1,15 @@
|
||||
package envoy.client.data;
|
||||
|
||||
/**
|
||||
* Implements a {@link LocalDB} in a way that does not persist any information
|
||||
* after application shutdown.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>TransientLocalDB.java</strong><br>
|
||||
* Created: <strong>3 Feb 2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
public final class TransientLocalDB extends LocalDB {
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Plays back audio from a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioPlayer.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioPlayer {
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
private Clip clip;
|
||||
|
||||
/**
|
||||
* Initializes the player with the default audio format.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
|
||||
|
||||
/**
|
||||
* Initializes the player with a given audio format.
|
||||
*
|
||||
* @param format the audio format to use
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioPlayer(AudioFormat format) {
|
||||
this.format = format;
|
||||
info = new DataLine.Info(Clip.class, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if audio play back is supported
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isSupported() { return AudioSystem.isLineSupported(info); }
|
||||
|
||||
/**
|
||||
* Plays back an audio clip.
|
||||
*
|
||||
* @param data the data of the clip
|
||||
* @throws EnvoyException if the play back failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void play(byte[] data) throws EnvoyException {
|
||||
try {
|
||||
clip = (Clip) AudioSystem.getLine(info);
|
||||
clip.open(format, data, 0, data.length);
|
||||
clip.start();
|
||||
} catch (final LineUnavailableException e) {
|
||||
throw new EnvoyException("Cannot play back audio", e);
|
||||
}
|
||||
}
|
||||
}
|
122
client/src/main/java/envoy/client/data/audio/AudioRecorder.java
Normal file
122
client/src/main/java/envoy/client/data/audio/AudioRecorder.java
Normal file
@ -0,0 +1,122 @@
|
||||
package envoy.client.data.audio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
|
||||
import envoy.exception.EnvoyException;
|
||||
|
||||
/**
|
||||
* Records audio and exports it as a byte array.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>AudioRecorder.java</strong><br>
|
||||
* Created: <strong>02.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public final class AudioRecorder {
|
||||
|
||||
/**
|
||||
* The default audio format used for recording and play back.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
|
||||
|
||||
private final AudioFormat format;
|
||||
private final DataLine.Info info;
|
||||
|
||||
private TargetDataLine line;
|
||||
private Path tempFile;
|
||||
|
||||
/**
|
||||
* Initializes the recorder with the default audio format.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
|
||||
|
||||
/**
|
||||
* Initializes the recorder with a given audio format.
|
||||
*
|
||||
* @param format the audio format to use
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public AudioRecorder(AudioFormat format) {
|
||||
this.format = format;
|
||||
info = new DataLine.Info(TargetDataLine.class, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if audio recording is supported
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isSupported() { return AudioSystem.isLineSupported(info); }
|
||||
|
||||
/**
|
||||
* @return {@code true} if the recorder is active
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public boolean isRecording() { return line != null && line.isActive(); }
|
||||
|
||||
/**
|
||||
* Starts the audio recording.
|
||||
*
|
||||
* @throws EnvoyException if starting the recording failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void start() throws EnvoyException {
|
||||
try {
|
||||
|
||||
// Open the line
|
||||
line = (TargetDataLine) AudioSystem.getLine(info);
|
||||
line.open(format);
|
||||
line.start();
|
||||
|
||||
// Prepare temp file
|
||||
tempFile = Files.createTempFile("recording", "wav");
|
||||
|
||||
// Start the recording
|
||||
final var ais = new AudioInputStream(line);
|
||||
AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
|
||||
} catch (IOException | LineUnavailableException e) {
|
||||
throw new EnvoyException("Cannot record voice", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the recording.
|
||||
*
|
||||
* @return the finished recording
|
||||
* @throws EnvoyException if finishing the recording failed
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public byte[] finish() throws EnvoyException {
|
||||
try {
|
||||
line.stop();
|
||||
line.close();
|
||||
final byte[] data = Files.readAllBytes(tempFile);
|
||||
Files.delete(tempFile);
|
||||
return data;
|
||||
} catch (final IOException e) {
|
||||
throw new EnvoyException("Cannot save voice recording", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the active recording.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public void cancel() {
|
||||
line.stop();
|
||||
line.close();
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Contains classes related to recording and playing back audio clips.
|
||||
* <p>
|
||||
* Project: <strong>envoy-client</strong><br>
|
||||
* File: <strong>package-info.java</strong><br>
|
||||
* Created: <strong>05.07.2020</strong><br>
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.data.audio;
|
9
client/src/main/java/envoy/client/data/package-info.java
Normal file
9
client/src/main/java/envoy/client/data/package-info.java
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* This package contains all data classes and classes related to persistence.
|
||||
*
|
||||
* @author Kai S. K. Engelbart
|
||||
* @author Leon Hofmeister
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
package envoy.client.data;
|
Reference in New Issue
Block a user