/*
* 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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
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.regex.Matcher;
import java.util.regex.Pattern;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import de.skuzzle.polly.http.api.AlternativeAnswerException;
import de.skuzzle.polly.http.api.HttpCookie;
import de.skuzzle.polly.http.api.HttpEvent.RequestMode;
import de.skuzzle.polly.http.api.HttpServer;
import de.skuzzle.polly.http.api.answers.DefaultAnswers;
import de.skuzzle.polly.http.api.answers.HttpAnswer;
import de.skuzzle.polly.http.api.answers.HttpAnswerHandler;
import de.skuzzle.polly.http.api.answers.HttpAnswers;
import de.skuzzle.polly.http.api.handler.HttpEventHandler;
class BasicEventHandler implements HttpHandler {
private final HttpServerImpl server;
/**
* Regex for splitting GET style parameters from the request uri
*/
private final static Pattern GET_PARAMETERS = Pattern.compile(
"(\\w+)=([^&]+)");
public BasicEventHandler(HttpServerImpl server) {
this.server = server;
}
private final void parseParameters(String in, Map<String, String> params) {
final Matcher m = GET_PARAMETERS.matcher(in);
while (m.find()) {
final String key = in.substring(m.start(1), m.end(1));
String value = in.substring(m.start(2), m.end(2));
try {
value = URLDecoder.decode(value, this.server.getEncoding());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (params.containsKey(key)) {
final String val = params.get(key);
params.put(key, val + ";" + value);
} else {
params.put(key, value);
}
}
}
private final String parsePostParameters(HttpExchange t, Map<String, String> result)
throws IOException {
BufferedReader r = null;
try {
r = new BufferedReader(new InputStreamReader(t.getRequestBody(),
this.server.getEncoding()));
String line = null;
final StringBuilder b = new StringBuilder();
while ((line = r.readLine()) != null) {
b.append(line);
b.append("\n");
if (!line.equals("")) {
parseParameters(line, result);
}
}
return b.toString();
} finally {
if (r != null) {
r.close();
}
}
}
private final Map<String, String> parseCookies(HttpExchange t) {
final List<String> cookies = t.getRequestHeaders().get("Cookie");
if (cookies == null) {
return Collections.emptyMap();
}
final Map<String, String> result = new HashMap<String, String>();
for (final String cookie : cookies) {
final String[] s = cookie.split("=");
if (s.length != 2) {
continue;
}
result.put(s[0], s[1]);
}
return result;
}
private final HttpEventImpl createEvent(final HttpExchange t) throws IOException {
final String requestUri = t.getRequestURI().toString();
final Map<String, String> get = new HashMap<>();
final Map<String, String> post = new HashMap<>();
// set stream to count incoming traffic
final TrafficInformationImpl temporary = new TrafficInformationImpl(null);
final CountingInputStream in = new CountingInputStream(
t.getRequestBody(), temporary);
t.setStreams(in, null);
// extract GET parameters
final int questIdx = requestUri.indexOf('?');
final String plainUri;
if (questIdx != -1) {
final String[] parts = requestUri.split("\\?", 2);
this.parseParameters(parts[1], get);
plainUri = requestUri.substring(0, questIdx);
} else {
plainUri = requestUri;
}
// extract POST parameters
final String requestBody = this.parsePostParameters(t, post);
// extract cookies
final Map<String, String> cookies = this.parseCookies(t);
// get session
final HttpSessionImpl session;
switch (this.server.getSessionType()) {
case HttpServer.SESSION_TYPE_COOKIE:
session = this.server.byID(t, cookies);
break;
case HttpServer.SESSION_TYPE_GET:
session = this.server.byID(t, get);
break;
case HttpServer.SESSION_TYPE_IP:
session = this.server.byIP(t.getRemoteAddress());
break;
default:
// should not be reachable anyway
throw new RuntimeException("illegal session type: " +
this.server.getSessionType());
}
final RequestMode mode;
final String m = t.getRequestMethod().toLowerCase();
if (m.equals("get")) {
mode = RequestMode.GET;
} else {
mode = RequestMode.POST;
}
// as session is resolved now, update the session's traffic
final TrafficInformationImpl ti = session.getTrafficInfo();
ti.updateFrom(temporary);
final HttpEventImpl event = new HttpEventImpl(this.server, mode,
t.getRequestURI(), t.getRemoteAddress(), plainUri, session, cookies,
get, post, requestBody) {
public void discard() {
super.discard();
t.close();
}
};
return event;
}
@Override
public void handle(HttpExchange t) throws IOException {
this.server.cleanSessions();
// copy list to avoid further synchronization
final HttpEventImpl httpEvent = this.createEvent(t);
this.server.fireOnRequest(httpEvent);
final HttpSessionImpl session = (HttpSessionImpl) httpEvent.getSession();
session.addEvent(httpEvent.copy());
session.setLastAction(new Date());
// handle the event
final List<HttpEventHandler> handler;
final String registered;
synchronized (this.server.getHandlers()) {
registered = this.server.getHandlers().findKey(httpEvent.getPlainUri());
handler = new ArrayList<>(
this.server.getHandlers().get(registered));
}
if (handler == null || handler.isEmpty()) {
this.handleAnswer(DefaultAnswers.FILE_NOT_FOUND, t, httpEvent);
return;
}
final Iterator<HttpEventHandler> it = handler.iterator();
final HttpEventHandler first = it.next();
final HttpEventHandler chain = new HttpEventHandlerChain(it);
HttpAnswer answer;
try {
answer = first.handleHttpEvent(registered, httpEvent, chain);
if (answer != null) {
this.handleAnswer(answer, t, httpEvent);
return;
}
} catch (AlternativeAnswerException e) {
this.handleAnswer(e.getAnswer(), t, httpEvent);
return;
} catch (Exception e) {
e.printStackTrace();
this.handleAnswer(HttpAnswers.newStackTraceAnswer(200, e), t, httpEvent);
return;
}
// event could not be handled
this.handleAnswer(DefaultAnswers.FILE_NOT_FOUND, t, httpEvent);
}
private final void handleAnswer(HttpAnswer answer, HttpExchange t,
final HttpEventImpl httpEvent) throws IOException {
if (httpEvent.isDiscarded()) {
// nothing to do here if already discarded
return;
}
final HttpSessionImpl session = (HttpSessionImpl) httpEvent.getSession();
// set stream to count outgoing traffic and report whether it was closed
final TrafficInformationImpl ti = session.getTrafficInfo();
final CountingOutputStream out = new CountingOutputStream(
t.getResponseBody(), ti) {
@Override
public void close() throws IOException {
super.close();
httpEvent.fireOnClose();
}
};
t.setStreams(null, out);
// null answer means to discard this event
if (answer == null) {
t.close();
return;
}
try {
// add cookies as response header
final Collection<HttpCookie> cookies = new ArrayList<>(answer.getCookies());
if (this.server.getSessionType() == HttpServer.SESSION_TYPE_COOKIE &&
session.isPending()) {
session.setPending(false);
// we must send a new session cookie
cookies.add(new HttpCookie(HttpServerImpl.SESSION_ID_NAME,
session.getId(), this.server.sessionLiveTime() / 1000));
} else if (session.shouldKill()) {
// send cookie with max age 0 to remove it at client side
cookies.add(new HttpCookie(HttpServer.SESSION_ID_NAME,
session.getId(), 0));
this.server.killSession(session);
}
if (!cookies.isEmpty()) {
t.getResponseHeaders().add("Set-Cookie",
this.generateCookieString(cookies));
}
t.getResponseHeaders().putAll(answer.getResponseHeaders());
if (!httpEvent.isDiscarded()) {
t.sendResponseHeaders(answer.getResponseCode(), 0);
// handle different types of answers
final HttpAnswerHandler handler = this.server.getHandler(answer);
if (handler == null) {
// TODO: react
throw new RuntimeException("no handler");
}
handler.handleAnswer(answer, httpEvent, t.getResponseBody());
}
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
t.close();
}
}
private final String generateCookieString(Collection<HttpCookie> cookies) {
final StringBuilder b = new StringBuilder();
final Iterator<HttpCookie> it = cookies.iterator();
while (it.hasNext()) {
final HttpCookie next = it.next();
b.append(next.toString());
if (it.hasNext()) {
b.append(",");
}
}
return b.toString();
}
}