package songbook.server;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import io.undertow.server.handlers.ExceptionHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.util.*;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import songbook.song.IndexDatabase;
import songbook.song.SongDatabase;
import songbook.song.SongUtils;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Server {
public final static int DEFAULT_PORT = 8080;
public final static String DEFAULT_HOST = "localhost";
public final static String DEFAULT_WEB_ROOT = "web";
public final static String DEFAULT_DATA_ROOT = "data";
public static final String ADMINISTRATOR_KEY_PATH = "administrator.key";
public static final String ADMINISTRATOR_ACTIVATED_PATH = "administrator.activated";
public static final String MIME_TEXT_HTML = "text/html";
public static final String MIME_TEXT_PLAIN = "text/plain";
public static final String MIME_TEXT_SONG = "text/song";
public static final String SESSION_KEY = "SessionKey";
public static final AttachmentKey<String> ADMIN_KEY = AttachmentKey.create(String.class);
private Logger logger;
private SongDatabase songDb;
private IndexDatabase indexDb;
private boolean showKeyCreationAlert = false;
private String administratorKey = null;
private String userKey = null;
public void start() {
logger = Logger.getLogger("Songbook");
Path dataRoot = getDataRoot();
Templates.setTemplatesPath(getWebRoot().resolve("templates"));
try {
if (!Files.exists(dataRoot)) Files.createDirectories(dataRoot);
} catch (IOException e) {
error("Cannot start server data root isn't accessible.", e);
return;
}
readKeys();
// creates admin key if needed
if (administratorKey == null) createAdminKey();
try {
// initializes songDb
songDb = new SongDatabase(getSongsPath());
} catch (IOException e) {
error("Can't initialize songs database in " + getSongsPath(), e);
}
Path index = getDataRoot().resolve("index");
try {
// initializes index.
indexDb = new IndexDatabase(index, songDb);
} catch (IOException e) {
error("Can't initialize index in " +index , e);
}
// creates server
Undertow undertow = createServer(pathTemplateHandler());
undertow.start();
}
/**
* Create And initialize undertow and handlers stack
* @param appHandler application handler to use for this server
* @return
*/
protected Undertow createServer(HttpHandler appHandler) {
// Fifth Handler Session
HttpHandler sessionHandler = sessionHandler(appHandler);
// Fourth Handler crossOrigin
HttpHandler crossOriginHandler = allowCrossOriginHandler(sessionHandler);
// Third Handler exception
HttpHandler exceptionHandler = exceptionHandler(crossOriginHandler);
// Second Handler log
HttpHandler logHandler = log(exceptionHandler);
// First Handler GracefulShutdown
GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(logHandler);
Undertow.Builder builder = Undertow.builder();
final int port = getPort();
final String host = getHost();
builder.addHttpListener(port, host);
builder.setHandler(gracefulShutdownHandler);
info("Listens on '" + host + ":" + port + "'");
return builder.build();
}
/**
* Log All requests
* @param next
* @return
*/
protected HttpHandler log(HttpHandler next) {
return (exchange) -> {
long start = System.currentTimeMillis();
next.handleRequest(exchange);
long end = System.currentTimeMillis();
long time = end - start < 0 ? 0 : end - start;
info("[" + exchange.getRequestMethod() + "]" + exchange.getRequestURI() + " in " + time + " ms");
};
}
/**
* Handles exceptions (including ServerException)
* @param next
* @return
*/
protected HttpHandler exceptionHandler(HttpHandler next) {
ExceptionHandler exceptionHandler = Handlers.exceptionHandler(next);
exceptionHandler.addExceptionHandler(ServerException.class, (exchange) -> {
Throwable exception = exchange.getAttachment(ExceptionHandler.THROWABLE);
if (exception instanceof ServerException) {
((ServerException) exception).serveError(getRole(exchange), exchange);
} else {
// TODO create real error message
String message = exception.getMessage();
exchange.setResponseCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.getResponseSender().send(createMessage(message, isAskingForJson(exchange)));
}
});
return exceptionHandler;
}
/**
* Adds Cross Origin if needed
* @param next Handler in the stack
* @return
*/
protected HttpHandler allowCrossOriginHandler(HttpHandler next) {
return (exchange) -> {
String origin = getHeader(exchange, Headers.ORIGIN);
if (origin != null) {
exchange.getResponseHeaders().put(Headers.ORIGIN, origin);
}
next.handleRequest(exchange);
};
}
/**
* Adds admin attribute and checks user attribute
* @param next next Handler in the stack
* @return
*/
protected HttpHandler sessionHandler(HttpHandler next) {
// TODO may be use SessionCookieConfig if it helps
return exchange -> {
String sessionKey = null;
Map<String, Cookie> cookies = Cookies.parseRequestCookies(10, false, exchange.getRequestHeaders().get(Headers.COOKIE));
for (Cookie cookie : cookies.values()) {
if (SESSION_KEY.equals(cookie.getName())) {
sessionKey = cookie.getValue();
}
}
String key = getParameter(exchange, "key");
if (key != null && !key.isEmpty() && !key.equals(sessionKey)) {
sessionKey = key;
// Set Cookie
Cookie cookie = new CookieImpl(SESSION_KEY, sessionKey);
cookie.setMaxAge(Integer.MAX_VALUE);
exchange.setResponseCookie(cookie);
}
if (isAdministrator(sessionKey)) {
// gets administrator key, remove alert (if present)
if (showKeyCreationAlert) {
showKeyCreationAlert = false;
try {
Files.createFile(getDataRoot().resolve(ADMINISTRATOR_ACTIVATED_PATH));
} catch (IOException e) {
error("Can't create file '" + ADMINISTRATOR_ACTIVATED_PATH + "'", e);
}
}
}
if ( isUser(sessionKey) ) {
// Need to understand this what's the difference between ADMIN_KEY and SESSION_KEY
exchange.putAttachment(ADMIN_KEY, sessionKey);
next.handleRequest(exchange);
} else { // It's too early to decide if we are authorize or not
throw new ServerException(StatusCodes.UNAUTHORIZED);
}
};
}
private HttpHandler pathTemplateHandler() {
HttpHandler fallThrough = Handlers.resource(new FileResourceManager(getWebRoot().toFile(), 1024));
//// To Update ////
PathTemplateHandler pathHandler = new PathTemplateHandler(fallThrough);
pathHandler.add("/", this::homePage); // Home Page
pathHandler.add("/view/{id}", this::viewSongPage);
pathHandler.add("/edit/{id}", adminAccess(this::editSongPage));
pathHandler.add("/delete/{id}", adminAccess(this::deleteSongPage));
pathHandler.add("/new", adminAccess(this::editSongPage));
pathHandler.add("/search/{query}", this::searchPage);
pathHandler.add("/search", this::searchPage);
pathHandler.add("/artists/{artist}", this::songsByArtistPage);
pathHandler.add("/artists", this::listArtistPage);
pathHandler.add("/songs/{id}", this::restSong);
pathHandler.add("/consoleApi", this::consoleApiPage);
pathHandler.add("/signin", this::signinPage);
pathHandler.add("/admin/{section}/{command}", adminAccess(this::adminCommand));
pathHandler.add("/admin", adminAccess(this::adminPage));
return pathHandler;
}
private void homePage(HttpServerExchange exchange) throws Exception {
searchPage(exchange);
}
private void viewSongPage(HttpServerExchange exchange) throws Exception {
getSong(exchange);
}
private void editSongPage(final HttpServerExchange exchange) throws Exception{
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
String id = getParameter(exchange, "id");
if (id != null) {
id = URLEncoder.encode(id, "utf-8");
}
// Serves song
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");
StringBuilder out = new StringBuilder();
String role = getRole(exchange);
if (id != null && !id.isEmpty()) {
String songContents = songDb.getSongContents(id);
if (songContents == null) throw new SongNotFoundException(id);
String title = SongUtils.getTitle(songContents);
Templates.header(out, "Edit - " + title + " - My SongBook", role);
Templates.editSong(out, id, songContents, role);
Templates.footer(out);
exchange.getResponseSender().send(out.toString());
} else {
Templates.header(out, "Create Song - My SongBook", role);
Templates.editSong(out, "", Templates.newSong(new StringBuilder()), role);
Templates.footer(out);
exchange.getResponseSender().send(out.toString());
}
}
private void deleteSongPage(HttpServerExchange exchange) throws Exception {
deleteSong(exchange);
}
private void searchPage(final HttpServerExchange exchange) throws Exception {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
// Serve all songs
String query = getParameter(exchange, "query");
String title = "My SongBook";
if (query != null && !query.isEmpty()) {
title = query + " - " + title;
}
StringBuilder out = new StringBuilder();
String mimeType = MimeParser.bestMatch(getHeader(exchange, Headers.ACCEPT), MIME_TEXT_SONG, MIME_TEXT_PLAIN, MIME_TEXT_HTML);
switch (mimeType) {
case MIME_TEXT_HTML:
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");
String role = getRole(exchange);
Templates.header(out, title, role);
if (showKeyCreationAlert) {
Templates.alertKeyCreation(out, administratorKey, exchange.getRequestPath());
}
StringBuilder result = new StringBuilder();
indexDb.search(query, result, mimeType);
Templates.search(out,result, role);
Templates.footer(out);
break;
default:
indexDb.search(query, out, mimeType);
break;
}
exchange.getResponseSender().send(out.toString());
}
private void restSong(final HttpServerExchange exchange) throws Exception {
switch (exchange.getRequestMethod().toString()) {
case Methods.GET_STRING:
this.getSong(exchange);
break;
case Methods.POST_STRING:
adminAccess(this::createSong).handleRequest(exchange);
break;
case Methods.PUT_STRING:
adminAccess(this::modifySong).handleRequest(exchange);
break;
case Methods.DELETE_STRING:
adminAccess(this::deleteSong).handleRequest(exchange);
break;
default:
throw ServerException.METHOD_NOT_ALLOWED;
}
}
private void getSong(final HttpServerExchange exchange) throws Exception {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
String id = URLEncoder.encode(getParameter(exchange, "id"), "utf-8");
// Serves song
String songContents = songDb.getSongContents(id);
if (songContents == null) throw new SongNotFoundException(id);
String mimeType = MimeParser.bestMatch(getHeader(exchange, Headers.ACCEPT), MIME_TEXT_SONG, MIME_TEXT_PLAIN, MIME_TEXT_HTML);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, mimeType);
switch (mimeType) {
case MIME_TEXT_HTML:
exchange.getResponseSender().send(htmlSong(exchange, id, songContents, exchange.getRequestPath()));
break;
default:
case MIME_TEXT_PLAIN:
case MIME_TEXT_SONG:
exchange.getResponseSender().send(songContents);
break;
}
logger.info("Serve Song " + id);
}
private String htmlSong(HttpServerExchange exchange, String id, String songData, String path) {
StringBuilder out = new StringBuilder();
// Todo use a songmark object to extract title and then generate html
String title = SongUtils.getTitle(songData);
String role = getRole(exchange);
Templates.header(out, title + " - My SongBook", getRole(exchange));
if (showKeyCreationAlert) Templates.alertKeyCreation(out, administratorKey, path);
Templates.viewSong(out, id, SongUtils.writeHtml(new StringBuilder(), songData), role);
Templates.footer(out);
return out.toString();
}
private void createSong(final HttpServerExchange exchange) throws Exception {
String songData = ChannelUtil.getStringContents(exchange.getRequestChannel());
// indexes updated song
Document document = SongUtils.indexSong(songData);
String title = document.get("title");
String artist = document.get("artist");
if (title == null || title.isEmpty() || artist == null) {
throw new MissingArgumentsException("title", "artist");
}
String id = songDb.generateId(title, artist);
// prepares new document
document.add(new StringField("id", id, Field.Store.YES));
indexDb.addOrUpdateDocument(document);
WritableByteChannel songChannel = songDb.writeChannelForSong(id);
if (songChannel == null) throw new ServerException(500, "Can't write song");
ChannelUtil.writeStringContents(songData, songChannel);
exchange.getResponseSender().send(id);
}
private void modifySong(final HttpServerExchange exchange) throws Exception {
String songData = ChannelUtil.getStringContents(exchange.getRequestChannel());
// indexes updated song
Document document = SongUtils.indexSong(songData);
String id = URLEncoder.encode(getParameter(exchange, "id"), "utf-8");
// Verify that song exists
if (!songDb.exists(id)) throw ServerException.NOT_FOUND;
// prepares new document
document.add(new StringField("id", id, Field.Store.YES));
indexDb.addOrUpdateDocument(document);
WritableByteChannel songChannel = songDb.writeChannelForSong(id);
if (songChannel == null) throw new ServerException(500, "Can't write song");
ChannelUtil.writeStringContents(songData, songChannel);
exchange.getResponseSender().send(id);
}
private void deleteSong(final HttpServerExchange exchange) throws Exception {
if (!exchange.getRequestMethod().equals(Methods.DELETE)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
String id = URLEncoder.encode(getParameter(exchange, "id"), "utf-8");
// Verify that song exists
if (!songDb.exists(id)) throw ServerException.NOT_FOUND;
// removes file
songDb.delete(id);
String title = indexDb.getTitle(id);
// removes document from index
indexDb.removeDocument(id);
String mimeType = MimeParser.bestMatch(getHeader(exchange, Headers.ACCEPT), MIME_TEXT_SONG, MIME_TEXT_PLAIN, MIME_TEXT_HTML);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, mimeType);
switch (mimeType) {
case MIME_TEXT_HTML:
StringBuilder out = new StringBuilder();
Templates.header(out, "My SongBook", getRole(exchange));
// show home page with message
Templates.alertSongRemovedSuccessfully(out, title == null ? id : title);
Templates.footer(out);
exchange.getResponseSender().send(out.toString());
break;
default:
case MIME_TEXT_PLAIN:
case MIME_TEXT_SONG:
exchange.getResponseSender().send(id);
break;
}
}
private void songsByArtistPage(final HttpServerExchange exchange) throws Exception {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
String artist = getParameter(exchange, "artist");
StringBuilder out = new StringBuilder();
String role = getRole(exchange);
Templates.header(out, "Artists", role);
StringBuilder result = new StringBuilder();
indexDb.songsByArtist(artist, result, MIME_TEXT_HTML);
Templates.search(out, result, role);
Templates.footer(out);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MIME_TEXT_HTML);
exchange.getResponseSender().send(out.toString());
}
private void listArtistPage(final HttpServerExchange exchange) throws Exception {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
StringBuilder out = new StringBuilder();
String role = getRole(exchange);
Templates.header(out, "Artists", role);
StringBuilder result = new StringBuilder();
indexDb.listArtists(result, MIME_TEXT_HTML);
Templates.search(out, result, role);
Templates.footer(out);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MIME_TEXT_HTML);
exchange.getResponseSender().send(out.toString());
}
private void consoleApiPage(final HttpServerExchange exchange) throws ServerException {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
StringBuilder out = new StringBuilder();
Templates.header(out, "Song Console Api", getRole(exchange));
Templates.consoleApi(out);
Templates.footer(out);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MIME_TEXT_HTML);
exchange.getResponseSender().send(out.toString());
}
private void signinPage(final HttpServerExchange exchange) throws ServerException {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
StringBuilder out = new StringBuilder();
Templates.header(out, "SongBook Admin Page", getRole(exchange));
Templates.signin(out);
Templates.footer(out);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MIME_TEXT_HTML);
exchange.getResponseSender().send(out.toString());
}
private void adminPage(final HttpServerExchange exchange) throws ServerException {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
StringBuilder out = new StringBuilder();
Templates.header(out, "SongBook Admin Page", getRole(exchange));
Templates.admin(out);
Templates.footer(out);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MIME_TEXT_HTML);
exchange.getResponseSender().send(out.toString());
}
private void adminCommand(final HttpServerExchange exchange) throws Exception {
if (!exchange.getRequestMethod().equals(Methods.GET)) {
throw ServerException.METHOD_NOT_ALLOWED;
}
StringBuilder out = new StringBuilder();
Templates.header(out, "Administration - My SongBook", getRole(exchange));
String section = getParameter(exchange, "section");
String command = getParameter(exchange, "command");
switch (section) {
case "index":
switch (command) {
case "reset":
try {
long start = System.currentTimeMillis();
songDb.clearCache();
indexDb.analyzeSongs();
long end = System.currentTimeMillis();
logger.info("Opened index in " + (end - start) + " milliseconds.");
Templates.alertSongReindexed(out);
Templates.admin(out);
} catch (IOException e) {
error("Can't initialize index in " + getDataRoot().resolve("index"), e);
Templates.alertIndexingError(out);
Templates.admin(out);
}
break;
default:
Templates.alertCommandNotSupported(out);
Templates.admin(out);
break;
}
break;
default:
throw ServerException.BAD_REQUEST;
}
Templates.footer(out);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MIME_TEXT_HTML);
exchange.getResponseSender().send(out.toString());
}
private Path getWebRoot() {
final String webRoot = System.getenv("WEB_ROOT");
return Paths.get(webRoot == null ? DEFAULT_WEB_ROOT : webRoot);
}
private static Path getDataRoot() {
final String dataRoot = System.getenv("DATA_ROOT");
return Paths.get(dataRoot == null ? DEFAULT_DATA_ROOT : dataRoot);
}
private Path getSongsPath() {
final String songRoot = System.getenv("SONGS_ROOT");
return songRoot == null ? getDataRoot().resolve("songs") : Paths.get(songRoot);
}
private int getPort() {
final String portString = System.getenv("PORT");
int port = DEFAULT_PORT;
if (portString != null) {
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
// doesn't matter;
}
}
return port;
}
private String getHost() {
String host = System.getenv("HOST");
if (host == null) host = System.getenv("HOSTNAME");
return host == null ? DEFAULT_HOST : host;
}
// Security
private void createAdminKey() {
// creates administrator key when it's null
long timestamp = System.currentTimeMillis();
String timestampString = Long.toHexString(timestamp);
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(timestampString.getBytes(), 0, timestampString.length());
administratorKey = new BigInteger(1, digest.digest()).toString(16);
} catch (NoSuchAlgorithmException e) {
administratorKey = timestampString;
}
logger.info("Created administrator key: '" + administratorKey + "'.");
writeKeys();
}
/**
* Searches for keys on server to initialize administratorKey and userKey.
*/
private void readKeys() {
try {
final Path administratorKeyPath = getDataRoot().resolve(ADMINISTRATOR_KEY_PATH);
if (Files.exists(administratorKeyPath)) {
final List<String> allLines = Files.readAllLines(administratorKeyPath);
if (!allLines.isEmpty()) {
administratorKey = allLines.get(allLines.size() - 1);
showKeyCreationAlert = !Files.exists(getDataRoot().resolve(ADMINISTRATOR_ACTIVATED_PATH));
}
}
} catch (IOException e) {
error("Could not read administrator key", e);
}
try {
final Path userKeyPath = getDataRoot().resolve("user.key");
if (Files.exists(userKeyPath)) {
final List<String> allLines = Files.readAllLines(userKeyPath);
if (!allLines.isEmpty()) {
userKey = allLines.get(allLines.size() - 1);
}
}
} catch (IOException e) {
error("Could not read user key", e);
}
}
/**
* Writes administratorKey and userKey to file system.
*/
private void writeKeys() {
if (administratorKey != null) {
try {
final Path administratorKeyPath = getDataRoot().resolve(ADMINISTRATOR_KEY_PATH);
Files.write(administratorKeyPath, Collections.singleton(administratorKey));
showKeyCreationAlert = true;
final Path administratorActivatedPath = getDataRoot().resolve(ADMINISTRATOR_ACTIVATED_PATH);
if (Files.exists(administratorActivatedPath)) {
Files.delete(administratorActivatedPath);
}
} catch (IOException e) {
error("Could not write administrator key", e);
}
}
if (userKey != null) {
try {
final Path userKeyPath = getDataRoot().resolve("user.key");
Files.write(userKeyPath, Collections.singleton(userKey));
showKeyCreationAlert = true;
} catch (IOException e) {
error("Could not write user key", e);
}
}
}
/**
* Checks if key allows to be administrator
*/
private boolean isAdministrator(String requestKey) {
return administratorKey == null || administratorKey.equals(requestKey);
}
/**
* Checks if key allows to be user
*/
private boolean isUser(String requestKey) {
return userKey == null || userKey.equals(requestKey);
}
private String getRole(HttpServerExchange exchange) {
return isAdministrator(exchange.getAttachment(ADMIN_KEY)) ? "admin" : "user";
}
protected boolean isAskingForJson(HttpServerExchange exchange) {
HeaderValues values = exchange.getRequestHeaders().get(Headers.ACCEPT);
return values != null && values.getFirst().contains("application/json");
}
protected String createMessage(String message, boolean json) {
return json ? "{ \"message\": \"" + message + "\"}" : message;
}
protected String getHeader(HttpServerExchange exchange, HttpString header) {
Deque<String> deque = exchange.getRequestHeaders().get(header);
return deque == null ? null : deque.element();
}
protected String getParameter(HttpServerExchange exchange, String parameter) {
StringBuilder sb = new StringBuilder();
Deque<String> deque = exchange.getQueryParameters().get(parameter);
if (deque != null) {
URLUtils.decode(deque.element(), "utf-8", true, sb);
return sb.toString();
}
return null;
}
private HttpHandler adminAccess(HttpHandler handler) {
return exchange -> {
String sessionKey = exchange.getAttachment(ADMIN_KEY);
if (isAdministrator(sessionKey)) {
handler.handleRequest(exchange);
} else {
throw new ServerException(StatusCodes.UNAUTHORIZED);
}
};
}
private HttpHandler methodFilterHandler(HttpHandler handler, HttpString method, HttpHandler next) {
return exchange -> {
if (method.equals(exchange.getRequestMethod())) {
handler.handleRequest(exchange);
} else if (next != null) {
next.handleRequest(exchange);
} else {
throw new ServerException(StatusCodes.METHOD_NOT_ALLOWED);
}
};
}
private void info(String message) {
logger.log(Level.INFO, message);
}
private void error(String message, IOException e) {
logger.log(Level.SEVERE, message, e);
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}