Java tutorial
/* * Copyright 2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.cettia.transport.http; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.cettia.asity.action.Action; import io.cettia.asity.action.Actions; import io.cettia.asity.action.ConcurrentActions; import io.cettia.asity.action.VoidAction; import io.cettia.asity.http.HttpStatus; import io.cettia.asity.http.ServerHttpExchange; import io.cettia.transport.BaseServerTransport; import io.cettia.transport.ServerTransport; import io.cettia.transport.TransportServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * HTTP implementation of {@link TransportServer}. * <p/> * It processes transport whose URI whose protocol is either {@code http} or * {@code https} and transport parameter is either {@code stream} or * {@code longpoll} like {@code http://localhost:8080/cettia?transport=stream}. * * @author Donghwan Kim */ public class HttpTransportServer implements TransportServer<ServerHttpExchange> { private final Logger log = LoggerFactory.getLogger(HttpTransportServer.class); private Map<String, BaseTransport> transports = new ConcurrentHashMap<>(); private Actions<ServerTransport> transportActions = new ConcurrentActions<ServerTransport>() .add(new Action<ServerTransport>() { @Override public void on(final ServerTransport t) { final BaseTransport transport = (BaseTransport) t; log.trace("{}'s request has opened", transport); transports.put(transport.id(), transport); transport.onclose(new VoidAction() { @Override public void on() { log.trace("{}'s request has been closed", transport); transports.remove(transport.id()); } }); } }); /** * For internal use only. */ public static Map<String, String> parseQuery(String uri) { Map<String, String> map = new LinkedHashMap<>(); String query = URI.create(uri).getQuery(); if (query == null || query.equals("")) { return Collections.unmodifiableMap(map); } String[] params = query.split("&"); for (String param : params) { try { String[] pair = param.split("=", 2); String name = URLDecoder.decode(pair[0], "UTF-8"); if (name.equals("")) { continue; } map.put(name, pair.length > 1 ? URLDecoder.decode(pair[1], "UTF-8") : ""); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } return Collections.unmodifiableMap(map); } /** * For internal use only. */ public static String formatQuery(Map<String, String> params) { StringBuilder query = new StringBuilder(); for (Entry<String, String> entry : params.entrySet()) { try { query.append(URLEncoder.encode(entry.getKey(), "UTF-8")).append("=") .append(URLEncoder.encode(entry.getValue(), "UTF-8")).append("&"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } return query.deleteCharAt(query.length() - 1).toString(); } @Override public void on(final ServerHttpExchange http) { final Map<String, String> params = parseQuery(http.uri()); http.setHeader("cache-control", "no-cache, no-store, must-revalidate").setHeader("pragma", "no-cache") .setHeader("expires", "0") .setHeader("access-control-allow-origin", http.header("origin") != null ? http.header("origin") : "*") .setHeader("access-control-allow-headers", "content-type") .setHeader("access-control-allow-credentials", "true"); switch (http.method()) { case OPTIONS: { http.end(); break; } case GET: { switch (params.get("when")) { case "open": { String transportName = params.get("transport"); switch (transportName) { case "stream": transportActions.fire(new StreamTransport(http)); break; case "longpoll": transportActions.fire(new LongpollTransport(http)); break; default: log.error("Transport, {}, is not implemented", transportName); http.setStatus(HttpStatus.NOT_IMPLEMENTED).end(); break; } break; } case "poll": { String id = params.get("id"); BaseTransport transport = transports.get(id); if (transport != null && transport instanceof LongpollTransport) { ((LongpollTransport) transport).refresh(http); } else { log.error("Long polling transport#{} is not found", id); http.setStatus(HttpStatus.INTERNAL_SERVER_ERROR).end(); } break; } case "abort": { String id = params.get("id"); BaseTransport transport = transports.get(id); if (transport != null) { transport.close(); } http.setHeader("content-type", "text/javascript; charset=utf-8").end(); break; } default: log.error("when, {}, is not supported", params.get("when")); http.setStatus(HttpStatus.NOT_IMPLEMENTED).end(); break; } break; } case POST: { final String id = params.get("id"); switch (http.header("content-type") == null ? "" : http.header("content-type").toLowerCase()) { case "text/plain; charset=utf-8": case "text/plain; charset=utf8": case "text/plain;charset=utf-8": case "text/plain;charset=utf8": http.onbody(new Action<String>() { @Override public void on(String body) { BaseTransport transport = transports.get(id); if (transport != null) { transport.handleText(body.substring("data=".length())); } else { log.error("A POST message arrived but no transport#{} is found", id); http.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); } http.end(); } ; }).readAsText(); break; case "application/octet-stream": http.onbody(new Action<ByteBuffer>() { @Override public void on(ByteBuffer body) { BaseTransport transport = transports.get(id); if (transport != null) { transport.handleBinary(body); } else { log.error("A POST message arrived but no transport#{} is found", id); http.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); } http.end(); } ; }).readAsBinary(); break; default: BaseTransport transport = transports.get(id); if (transport != null) { // TODO improve transport.handleError(new RuntimeException("protocol")); transport.close(); } http.setStatus(HttpStatus.INTERNAL_SERVER_ERROR).end(); break; } break; } default: log.error("HTTP method, {}, is not supported", http.method()); http.setStatus(HttpStatus.METHOD_NOT_ALLOWED).end(); break; } } @Override public HttpTransportServer ontransport(Action<ServerTransport> action) { transportActions.add(action); return this; } /** * Base class for HTTP transport. * * @author Donghwan Kim */ private static abstract class BaseTransport extends BaseServerTransport { protected final ServerHttpExchange http; protected final Map<String, String> params; protected String id = UUID.randomUUID().toString(); // For JSON processing in long polling and Base64 processing in streaming protected ObjectMapper mapper = new ObjectMapper(); public BaseTransport(ServerHttpExchange http) { this.params = parseQuery(http.uri()); this.http = http; } public String id() { return id; } @Override public String uri() { return http.uri(); } public void handleText(String text) { textActions.fire(text); } public void handleBinary(ByteBuffer binary) { binaryActions.fire(binary); } public void handleError(Throwable error) { errorActions.fire(error); } /** * {@link ServerHttpExchange} is available. */ @Override public <T> T unwrap(Class<T> clazz) { return ServerHttpExchange.class.isAssignableFrom(clazz) ? clazz.cast(http) : null; } } /** * Represents a server-side HTTP Streaming transport. * * @author Donghwan Kim */ private static class StreamTransport extends BaseTransport { private final static String TEXT_2KB = CharBuffer.allocate(2048).toString().replace('\0', ' '); public StreamTransport(ServerHttpExchange http) { super(http); Map<String, String> query = new LinkedHashMap<>(); query.put("id", id); http.onfinish(new VoidAction() { @Override public void on() { closeActions.fire(); } }).onerror(new Action<Throwable>() { @Override public void on(Throwable throwable) { errorActions.fire(throwable); } }).onclose(new VoidAction() { @Override public void on() { closeActions.fire(); } }).setHeader("content-type", "text/" + ("true".equals(params.get("sse")) ? "event-stream" : "plain") + "; charset=utf-8") .write(TEXT_2KB + "\ndata: ?" + formatQuery(query) + "\n\n"); } @Override protected void doSend(String data) { sendEventStreamMessage("1" + data); } @Override protected void doSend(ByteBuffer data) { sendEventStreamMessage("2" + mapper.convertValue(data, String.class)); } private synchronized void sendEventStreamMessage(String data) { String payload = ""; for (String line : data.split("\r\n|\r|\n")) { payload += "data: " + line + "\n"; } payload += "\n"; http.write(payload); } @Override public synchronized void doClose() { http.end(); } } /** * Represents a server-side HTTP Long Polling transport. * * @author Donghwan Kim */ private static class LongpollTransport extends BaseTransport { private AtomicReference<ServerHttpExchange> httpRef = new AtomicReference<>(); private AtomicBoolean aborted = new AtomicBoolean(); // Regard it as http.endedWithMessage private AtomicBoolean endedWithMessage = new AtomicBoolean(); private AtomicReference<Timer> closeTimer = new AtomicReference<>(); private Queue<Object> cache = new ConcurrentLinkedQueue<>(); public LongpollTransport(ServerHttpExchange http) { super(http); refresh(http); } public void refresh(ServerHttpExchange http) { final Map<String, String> parameters = parseQuery(http.uri()); http.onfinish(new VoidAction() { @Override public void on() { if (parameters.get("when").equals("poll") && !endedWithMessage.get()) { closeActions.fire(); } else { Timer timer = new Timer(true); timer.schedule(new TimerTask() { @Override public void run() { closeActions.fire(); } }, 3000); closeTimer.set(timer); } } }).onerror(new Action<Throwable>() { @Override public void on(Throwable throwable) { errorActions.fire(throwable); } }).onclose(new VoidAction() { @Override public void on() { closeActions.fire(); } }); String when = parameters.get("when"); switch (when) { case "open": Map<String, String> query = new LinkedHashMap<>(); query.put("id", id); endWithMessage(http, "?" + formatQuery(query)); break; case "poll": endedWithMessage.set(false); Timer timer = closeTimer.getAndSet(null); if (timer != null) { timer.cancel(); } if (aborted.get()) { http.end(); } else { Object cached = cache.poll(); if (cached != null) { // As cached is either String or ByteBuffer if (cached instanceof String) { endWithMessage(http, (String) cached); } else { endWithMessage(http, (ByteBuffer) cached); } } else { httpRef.set(http); } } break; default: // TODO improve errorActions.fire(new RuntimeException("protocol")); close(); break; } } @Override protected void doSend(String data) { ServerHttpExchange http = httpRef.getAndSet(null); if (http != null) { endWithMessage(http, data); } else { cache.offer(data); } } // Regard it as http.endWithMessage private void endWithMessage(ServerHttpExchange http, String data) { endedWithMessage.set(true); boolean jsonp = "true".equals(params.get("jsonp")); if (jsonp) { try { data = params.get("callback") + "(" + mapper.writeValueAsString(data) + ");"; } catch (JsonProcessingException e) { throw new RuntimeException(e); } } http.setHeader("content-type", "text/" + (jsonp ? "javascript" : "plain") + "; " + "charset=utf-8") .end(data); } @Override protected void doSend(ByteBuffer data) { ServerHttpExchange http = httpRef.getAndSet(null); if (http != null) { endWithMessage(http, data); } else { cache.offer(data); } } // Regard it as http.endWithMessage private void endWithMessage(ServerHttpExchange http, ByteBuffer data) { endedWithMessage.set(true); http.setHeader("content-type", "application/octet-stream").end(data); } @Override public void doClose() { ServerHttpExchange http = httpRef.getAndSet(null); if (http != null) { http.end(); } else { aborted.set(true); } } } }