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
;
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
;
32 * The main class and entry point for the chat 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() {
49 logger
.debug("Server shutdown hook running...");
55 * Constructor with TEST_ADDRESS and TEST_PORT.
58 this(TEST_ADDRESS
, TEST_PORT
, false);
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
;
72 this.listener
= new Listener(readyAcceptQ
,
73 Collections
.singletonList(new InetSocketAddress(address
,
75 this.dispatcher
= new Dispatcher(readyAcceptQ
);
76 this.shutdownDBBeforeExit
= shutdownDBBeforeExit
;
77 Runtime
.getRuntime().addShutdownHook(onShutdown
);
81 * Get the server address
82 * @return the server address
84 public String
getAddress() {
90 * @return the server port
92 public int getPort() {
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
111 public void start() {
114 } catch (IOException e
) {
115 logger
.error("Could not load user contact lists", e
);
117 ACCEPT
.submit(listener
);
120 } catch (IOException e
) {
121 logger
.error("Dispatcher failed", e
);
125 ACCEPT
.awaitTermination(1000L, TimeUnit
.MILLISECONDS
);
126 } catch (InterruptedException
| SecurityException e
) {
127 logger
.error("Couldn't shutdown the listener", e
);
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
);
147 logger
.fatal("cannot find user contacts resource {}, aborting...",
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")
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() {
182 public int getPort() {
186 public String
getDbFile() {
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.
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
) {