Provide default values for optional cli args
[sig-chat-ex-feb-2019.git] / src / main / java / exercise / nio / chat / server / Server.java
blobb8647799d3719f8493e5240b2e95a3f63068caa4
1 package exercise.nio.chat.server;
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.net.InetSocketAddress;
6 import java.nio.channels.SocketChannel;
7 import java.util.Collections;
8 import java.util.Map;
9 import java.util.Set;
10 import java.util.concurrent.BlockingQueue;
11 import java.util.concurrent.Callable;
12 import java.util.concurrent.ExecutorService;
13 import java.util.concurrent.Executors;
14 import java.util.concurrent.LinkedBlockingQueue;
15 import java.util.concurrent.TimeUnit;
17 import org.apache.logging.log4j.LogManager;
18 import org.apache.logging.log4j.Logger;
20 import com.fasterxml.jackson.core.type.TypeReference;
21 import com.fasterxml.jackson.databind.ObjectMapper;
23 import exercise.nio.chat.server.accept.Listener;
24 import exercise.nio.chat.server.data.User;
25 import exercise.nio.chat.server.data.storage.MapDBStorage;
26 import exercise.nio.chat.server.http.dispatch.Dispatcher;
27 import picocli.CommandLine;
28 import picocli.CommandLine.Command;
29 import picocli.CommandLine.Option;
31 /**
32 * The main class and entry point for the chat server.
34 public class Server {
35 private static final Logger logger = LogManager.getLogger(Server.class);
36 static final String TEST_ADDRESS = "localhost";
37 static final int TEST_PORT = 3335;
38 static final String USER_CONTACTS_FILE = "contacts.json";
39 private static final ExecutorService ACCEPT =
40 Executors.newFixedThreadPool(1);
42 private final Listener listener;
43 private final Dispatcher dispatcher;
44 private final String address;
45 private final int port;
46 private final boolean shutdownDBBeforeExit;
47 private final Thread onShutdown = new Thread() {
48 public void run() {
49 logger.debug("Server shutdown hook running...");
50 ACCEPT.shutdown();
54 /**
55 * Constructor with TEST_ADDRESS and TEST_PORT.
57 public Server() {
58 this(TEST_ADDRESS, TEST_PORT, false);
61 /**
62 * Constructor.
63 * @param address the address to bind
64 * @param port the port to listen for connections
65 * @param shutdownDBBeforeExit true to shutdown the db before exiting
67 public Server(String address, int port, boolean shutdownDBBeforeExit) {
68 final BlockingQueue<SocketChannel> readyAcceptQ =
69 new LinkedBlockingQueue<>();
70 this.address = address;
71 this.port = port;
72 this.listener = new Listener(readyAcceptQ,
73 Collections.singletonList(new InetSocketAddress(address,
74 port)));
75 this.dispatcher = new Dispatcher(readyAcceptQ);
76 this.shutdownDBBeforeExit = shutdownDBBeforeExit;
77 Runtime.getRuntime().addShutdownHook(onShutdown);
80 /**
81 * Get the server address
82 * @return the server address
84 public String getAddress() {
85 return address;
88 /**
89 * Get the server port
90 * @return the server port
92 public int getPort() {
93 return port;
96 /**
97 * Wait for the server to be ready to accept connections
98 * @param timeout time to wait
99 * @param unit time unit for wait time
100 * @throws InterruptedException if the thread is interrupted while waiting
102 public void awaitReady(long timeout, TimeUnit unit)
103 throws InterruptedException {
104 listener.awaitReady(timeout, unit);
108 * Starting the will load user contacts, and start the Listener and the
109 * Dispatcher.
111 public void start() {
112 try {
113 loadUserContacts();
114 } catch (IOException e) {
115 logger.error("Could not load user contact lists", e);
117 ACCEPT.submit(listener);
118 try {
119 dispatcher.call();
120 } catch (IOException e) {
121 logger.error("Dispatcher failed", e);
122 } finally {
123 ACCEPT.shutdown();
124 try {
125 ACCEPT.awaitTermination(1000L, TimeUnit.MILLISECONDS);
126 } catch (InterruptedException | SecurityException e) {
127 logger.error("Couldn't shutdown the listener", e);
128 } finally {
129 if (shutdownDBBeforeExit) {
130 MapDBStorage.getInstance().shutdown();
137 * Load user contacts from class path resources and persist them in the DB.
138 * TODO: allow users to authenticate and manage their own contacts.
139 * @throws IOException if there is a problem reading the contacts
141 protected void loadUserContacts() throws IOException {
142 // load user contacts and add them to the DB
143 final ObjectMapper mapper = new ObjectMapper();
144 final InputStream cs = ClassLoader.getSystemClassLoader()
145 .getResourceAsStream(USER_CONTACTS_FILE);
146 if (null == cs) {
147 logger.fatal("cannot find user contacts resource {}, aborting...",
148 USER_CONTACTS_FILE);
149 throw new IllegalStateException("missing user contacts data");
151 final Map<String, Set<Long>> contacts = mapper.readValue(cs,
152 new TypeReference<Map<String, Set<Long>>> () {});
153 for (Map.Entry<String, Set<Long>> e : contacts.entrySet()) {
154 final User user = new User(Long.parseLong(e.getKey()),
155 e.getValue(), Collections.emptySet());
156 MapDBStorage.getInstance().persist(user);
158 logger.info("loaded {} users from contacts", contacts.size());
162 * For use with picocli to parse command line args from the main method
164 @Command(description = "Starts the chat server.", name = "simple-chat-server",
165 mixinStandardHelpOptions = true, version = "Simple chat server 0.0.1")
166 protected static class ServerOptions implements Callable<Server> {
167 @Option(names = {"-a", "--address"}, paramLabel = "ADDRESS",
168 description = "server address (default: ${DEFAULT-VALUE})")
169 private String address = TEST_ADDRESS;
170 @Option(names = {"-p", "--port"}, paramLabel = "PORT", required = true,
171 description = "server port")
172 private int port;
173 @Option(names = {"-f", "--dbFile"}, paramLabel = "DB_FILE",
174 description = "path to a file where the chat db is stored " +
175 "(default: ${DEFAULT-VALUE})")
176 private String dbFile = MapDBStorage.getDbFile();
178 public String getAddress() {
179 return address;
182 public int getPort() {
183 return port;
186 public String getDbFile() {
187 return dbFile;
190 @Override
191 public Server call() {
192 if (null != dbFile) {
193 MapDBStorage.setDbFile(dbFile);
195 return new Server(address, port, true);
200 * Main method to run the server as a command.
201 * @see ServerOptions
202 * @param args process args
204 public static void main(String[] args) {
205 final Server server = CommandLine.call(new ServerOptions(), args);
206 if (null != server) {
207 server.start();