/*
* Copyright 2013 Simon Taddiken
*
* This file is part of Polly HTTP API.
*
* Polly HTTP API is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Polly HTTP API is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with Polly HTTP API. If not, see http://www.gnu.org/licenses/.
*/
package de.skuzzle.polly.http.internal;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import org.apache.commons.lang.StringEscapeUtils;
import com.sun.net.httpserver.HttpExchange;
import de.skuzzle.polly.http.api.HttpEvent;
import de.skuzzle.polly.http.api.HttpEventListener;
import de.skuzzle.polly.http.api.HttpServer;
import de.skuzzle.polly.http.api.HttpSession;
import de.skuzzle.polly.http.api.ServerFactory;
import de.skuzzle.polly.http.api.answers.HttpAnswer;
import de.skuzzle.polly.http.api.answers.HttpAnswerHandler;
import de.skuzzle.polly.http.api.answers.HttpBinaryAnswer;
import de.skuzzle.polly.http.api.answers.HttpTemplateAnswer;
import de.skuzzle.polly.http.api.handler.BinaryAnswerHandler;
import de.skuzzle.polly.http.api.handler.HttpEventHandler;
import de.skuzzle.polly.http.api.handler.TemplateAnswerHandler;
class HttpServerImpl implements HttpServer {
private final static Random RANDOM = new Random();
private final URLMap<List<HttpEventHandler>> handlers;
private final Map<InetSocketAddress, HttpSessionImpl> ipToSession;
private final Map<InetSocketAddress, HttpSessionImpl> pending;
private final Collection<HttpEventListener> httpListeners;
private final Queue<HttpSession> sessionHistory;
private final Map<String, HttpSessionImpl> idToSession;
private final AnswerHandlerMap handler;
private final ServerFactory factory;
private int sessionType;
private int sessionLiveTime;
private String encoding;
private final TrafficInformationImpl traffic;
private com.sun.net.httpserver.HttpServer server;
private boolean isrunning;
public HttpServerImpl(ServerFactory factory) {
this.traffic = new TrafficInformationImpl(null);
this.sessionHistory = new ArrayDeque<>();
this.handlers = new URLMap<>();
this.ipToSession = new HashMap<>();
this.idToSession = new HashMap<>();
this.pending = new HashMap<>();
this.handler = new AnswerHandlerMap();
this.factory = factory;
this.sessionType = SESSION_TYPE_COOKIE;
this.httpListeners = new ArrayList<>();
this.encoding = Charset.defaultCharset().name();
// default handler
this.setAnswerHandler(HttpBinaryAnswer.class, new BinaryAnswerHandler());
this.setAnswerHandler(HttpTemplateAnswer.class, new TemplateAnswerHandler());
}
/**
* Creates a new {@link TrafficInformationImpl} which will also update the cumulative
* traffic information of this server.
*
* @return A new {@link TrafficInformationImpl} instance.
*/
TrafficInformationImpl newTrafficInformation() {
return new TrafficInformationImpl(this.traffic);
}
@Override
public void setEncoding(String encoding) {
Charset.forName(encoding);
this.encoding = encoding;
}
@Override
public String getEncoding() {
return this.encoding;
}
@Override
public TrafficInformationImpl getTraffic() {
return this.traffic;
}
@Override
public boolean isRunning() {
return this.isrunning;
}
@Override
public void start() throws IOException {
if (this.isRunning()) {
throw new IllegalStateException("server already running");
}
this.server = this.factory.create();
this.server.createContext("/", new BasicEventHandler(this));
this.server.start();
this.isrunning = true;
}
@Override
public void shutdown(int timeout) {
if (!this.isRunning()) {
throw new IllegalStateException("server not running");
}
this.server.stop(timeout);
synchronized (this.idToSession) {
this.idToSession.clear();
}
synchronized (this.ipToSession) {
this.idToSession.clear();
}
this.server = null;
this.isrunning = false;
}
@Override
public Collection<HttpSession> getSessionHistory() {
return this.sessionHistory;
}
@Override
public HttpAnswerHandler getHandler(HttpAnswer answer) {
final Class<?> cls = answer.getClass();
return this.handler.resolve(cls);
}
@Override
public void addHttpEventListener(HttpEventListener listener) {
this.httpListeners.add(listener);
}
@Override
public void removeHttpEventListener(HttpEventListener listener) {
this.httpListeners.remove(listener);
}
@Override
public void setAnswerHandler(Class<?> answerType, HttpAnswerHandler handler) {
this.handler.registerHandler(answerType, handler);
}
@Override
public Collection<String> getURLs() {
return this.handlers.keySet();
}
@Override
public void addHttpEventHandler(String url, HttpEventHandler handler) {
url = url.startsWith("/") ? url : "/" + url;
List<HttpEventHandler> handlers = this.handlers.get(url);
if (handlers == null) {
handlers = new ArrayList<>();
this.handlers.put(url, handlers);
}
handlers.add(handler);
}
@Override
public void removeHttpEventHandler(String url, HttpEventHandler handler) {
final List<HttpEventHandler> handlers = this.handlers.get(url);
if (handlers == null) {
return;
}
handlers.remove(handlers);
}
URLMap<List<HttpEventHandler>> getHandlers() {
return this.handlers;
}
@Override
public int getSessionType() {
return this.sessionType;
}
@Override
public void setSessionType(int sessionType) {
this.sessionType = sessionType;
}
@Override
public int sessionLiveTime() {
return this.sessionLiveTime;
}
@Override
public void setSessionLiveTime(int sessionLiveTime) {
this.sessionLiveTime = sessionLiveTime;
}
private final String createSessionId(InetSocketAddress ip) {
final long id = RANDOM.nextLong() * System.currentTimeMillis() *
ip.hashCode();
return Long.toHexString(id);
}
Map<InetSocketAddress, HttpSessionImpl> getTempStorage() {
return this.pending;
}
synchronized HttpSessionImpl byID(HttpExchange t, Map<String, String> parameters) {
synchronized (this.idToSession) {
// id sent with the cookie or get parameters
String id = parameters.get(SESSION_ID_NAME);
if (id == null) {
// client sent no id
id = this.createSessionId(t.getRemoteAddress());
final HttpSessionImpl temp = new HttpSessionImpl(
this, id);
temp.setPending(true);
this.idToSession.put(id, temp);
return temp;
}
// client sent an id, so it can be removed from pending ones
HttpSessionImpl session = this.idToSession.get(id);
if (session == null) {
session = new HttpSessionImpl(this, id);
this.idToSession.put(id, session);
}
session.setPending(false);
return session;
}
}
HttpSessionImpl byIP(InetSocketAddress ip) {
synchronized (this.ipToSession) {
HttpSessionImpl session = this.ipToSession.get(ip);
if (session == null) {
final String id = this.createSessionId(ip);
session = new HttpSessionImpl(this, id);
this.ipToSession.put(ip, session);
}
return session;
}
}
private void rememberSession(HttpSession session) {
synchronized (this.sessionHistory) {
this.sessionHistory.add(session);
if (this.sessionHistory.size() > SESSION_HISTORY_SIZE) {
this.sessionHistory.poll();
}
}
}
void cleanSessions() {
final Date now = new Date();
if (this.getSessionType() == SESSION_TYPE_COOKIE) {
synchronized (this.idToSession) {
final Iterator<HttpSessionImpl> it = this.idToSession.values().iterator();
while (it.hasNext()) {
final HttpSessionImpl session = it.next();
final Date exp = session.getExpirationDate() == null
? new Date(session.getTimestamp())
: session.getExpirationDate();
if (session.shouldKill() ||
now.getTime() - exp.getTime() > this.sessionLiveTime) {
it.remove();
this.rememberSession(session);
}
}
}
}
}
synchronized void killSession(HttpSessionImpl session) {
switch (this.getSessionType()) {
case SESSION_TYPE_COOKIE:
case SESSION_TYPE_GET:
synchronized (this.idToSession) {
this.idToSession.remove(session.getId());
}
break;
case SESSION_TYPE_IP:
synchronized (this.ipToSession) {
this.ipToSession.remove(session);
}
}
this.rememberSession(session);
}
@Override
public Collection<HttpSession> getSessions() {
final Collection<HttpSession> result = new ArrayList<>();
switch (this.getSessionType()) {
case SESSION_TYPE_COOKIE:
case SESSION_TYPE_GET:
synchronized (this.idToSession) {
result.addAll(this.idToSession.values());
}
break;
case SESSION_TYPE_IP:
synchronized (this.ipToSession) {
result.addAll(this.ipToSession.values());
}
}
return Collections.unmodifiableCollection(result);
}
@Override
public HttpSession findSession(String id) {
switch (this.getSessionType()) {
case SESSION_TYPE_COOKIE:
case SESSION_TYPE_GET:
synchronized (this.idToSession) {
return this.idToSession.get(id);
}
default:
case SESSION_TYPE_IP:
synchronized (this.ipToSession) {
for (final HttpSessionImpl session : this.ipToSession.values()) {
if (session.getId().equals(id)) {
return session;
}
}
return null;
}
}
}
void fireOnRequest(HttpEvent e) {
for (final HttpEventListener listener : this.httpListeners) {
listener.onRequest(e);
}
}
@Override
public String esc(String s) {
return StringEscapeUtils.escapeHtml(s);
}
}