/* * Copyright 2011 Future Systems * * 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 org.krakenapps.httpd.impl; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.handler.codec.http.DefaultHttpChunk; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpHeaders.Names; import org.jboss.netty.handler.codec.http.HttpHeaders.Values; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.HttpVersion; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Response implements HttpServletResponse { private final Logger logger = LoggerFactory.getLogger(Response.class.getName()); private final int bufferSize = 128 * 1024; private BundleContext bc; private ChannelHandlerContext ctx; private HttpServletRequest req; private ServletOutputStream os; private PrintWriter writer; private HttpResponseStatus status = HttpResponseStatus.OK; private Map<String, List<String>> headers = new HashMap<String, List<String>>(); private Set<Cookie> cookies = new HashSet<Cookie>(); public Response(BundleContext bc, ChannelHandlerContext ctx, HttpServletRequest req) { this.bc = bc; this.ctx = ctx; this.req = req; this.os = new ResponseOutputStream(); this.writer = new PrintWriter(new OutputStreamWriter(os, Charset.forName("utf-8"))); } private class ResponseOutputStream extends ServletOutputStream { private boolean closed = false; private boolean sentHeader = false; private ChannelBuffer buf = ChannelBuffers.dynamicBuffer(); @Override public void write(int b) throws IOException { buf.writeByte(b); if (buf.readableBytes() > bufferSize) flush(); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) throws IOException { buf.writeBytes(b, off, len); if (buf.readableBytes() > bufferSize) flush(); } @Override public void close() throws IOException { if (closed) { if (logger.isDebugEnabled()) logger.debug("kraken httpd: response output closed"); return; } closed = true; flush(true); String transferEncoding = getHeader(HttpHeaders.Names.TRANSFER_ENCODING); if (logger.isDebugEnabled()) logger.debug("kraken httpd: transfer encoding header [{}]", transferEncoding); if (transferEncoding != null && transferEncoding.equals("chunked")) { ctx.getChannel().write(new DefaultHttpChunk(ChannelBuffers.EMPTY_BUFFER)); if (logger.isDebugEnabled()) logger.debug("kraken httpd: channel [{}], last empty chunk", ctx.getChannel()); } if (logger.isDebugEnabled()) logger.debug("kraken httpd: closing channel [{}]", ctx.getChannel()); if (!isKeepAlive()) { if (logger.isDebugEnabled()) logger.debug("kraken httpd: channel [{}] will be closed", ctx.getChannel()); ctx.getChannel().close(); } } @Override public void flush() throws IOException { flush(false); } private void flush(boolean force) { String transferEncoding = getHeader(HttpHeaders.Names.TRANSFER_ENCODING); boolean isChunked = transferEncoding != null && transferEncoding.equals("chunked"); if ((force || isChunked) && !sentHeader) { // send response if not sent HttpResponse resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); HttpSessionImpl session = (HttpSessionImpl) req.getSession(false); if (session != null) { if (session.isNew()) { resp.addHeader(HttpHeaders.Names.SET_COOKIE, "JSESSIONID=" + session.getId() + "; path=/"); session.setNew(false); } session.setLastAccess(new Date()); } if (!isChunked) resp.setHeader(HttpHeaders.Names.CONTENT_LENGTH, buf.readableBytes()); for (Cookie c : cookies) { resp.addHeader(HttpHeaders.Names.SET_COOKIE, c.getName() + "=" + c.getValue()); } for (String name : headers.keySet()) resp.setHeader(name, headers.get(name)); if (logger.isDebugEnabled()) logger.debug("kraken httpd: channel [{}], sent header", ctx.getChannel()); // write http header ctx.getChannel().write(resp); sentHeader = true; } if (isChunked) { if (logger.isDebugEnabled()) logger.debug("kraken httpd: channel [{}], flush chunk [{}]", ctx.getChannel(), buf.readableBytes()); ctx.getChannel().write(new DefaultHttpChunk(buf)); buf = ChannelBuffers.dynamicBuffer(); } else if (sentHeader) { if (logger.isDebugEnabled()) logger.debug("kraken httpd: channel [{}], flush response [{}]", ctx.getChannel(), buf.readableBytes()); ctx.getChannel().write(buf); buf = ChannelBuffers.dynamicBuffer(); } } private boolean isKeepAlive() { String connection = req.getHeader(Names.CONNECTION); if (connection != null && Values.CLOSE.equalsIgnoreCase(connection)) return false; if (req.getProtocol().equals("HTTP/1.1")) { return !Values.CLOSE.equalsIgnoreCase(connection); } else { return Values.KEEP_ALIVE.equalsIgnoreCase(connection); } } } @Override public String getCharacterEncoding() { List<String> contentTypes = (List<String>) headers.get(HttpHeaders.Names.CONTENT_TYPE); if (contentTypes == null) return null; String contentType = contentTypes.get(0); if (!contentType.contains("charset")) return null; for (String t : contentType.split(";")) { if (t.trim().startsWith("charset")) return t.split("=")[1].trim(); } return null; } @Override public ServletOutputStream getOutputStream() throws IOException { return os; } @Override public PrintWriter getWriter() throws IOException { return writer; } @Override public void setContentLength(int len) { headers.put(HttpHeaders.Names.CONTENT_LENGTH, Arrays.asList(Integer.toString(len))); } @Override public void setContentType(String type) { headers.put(HttpHeaders.Names.CONTENT_TYPE, Arrays.asList(type)); } @Override public void addCookie(Cookie cookie) { cookies.add(cookie); } @Override public boolean containsHeader(String name) { return headers.containsKey(name); } @Override public String encodeRedirectURL(String url) { // TODO Auto-generated method stub return null; } @Deprecated @Override public String encodeRedirectUrl(String url) { return encodeRedirectURL(url); } @Override public String encodeURL(String url) { // TODO Auto-generated method stub return null; } @Deprecated @Override public String encodeUrl(String url) { return encodeURL(url); } @Override public void sendError(int sc, String msg) throws IOException { setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html"); this.status = HttpResponseStatus.valueOf(sc); if (msg == null) msg = ""; String body = "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n" // + "<html><head><title>" + sc + " " + status.getReasonPhrase() + "</title></head>\n" // + "<body><h1>" + sc + " " + status.getReasonPhrase() + "</h1><pre>" + msg + "</pre><hr/><address>Kraken HTTPd/" + bc.getBundle().getHeaders().get(Constants.BUNDLE_VERSION) + "</address></body></html>"; writer.append(body); writer.close(); } @Override public void sendError(int sc) throws IOException { sendError(sc, null); } @Override public void sendRedirect(String location) throws IOException { this.status = HttpResponseStatus.MOVED_PERMANENTLY; setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html"); setHeader(HttpHeaders.Names.LOCATION, location); } public void close() { writer.close(); } @Override public String getContentType() { // TODO Auto-generated method stub return null; } @Override public void setCharacterEncoding(String charset) { // TODO Auto-generated method stub } @Override public void setBufferSize(int size) { // TODO Auto-generated method stub } @Override public int getBufferSize() { // TODO Auto-generated method stub return 0; } @Override public void flushBuffer() throws IOException { // TODO Auto-generated method stub } @Override public void resetBuffer() { // TODO Auto-generated method stub } @Override public boolean isCommitted() { // TODO Auto-generated method stub return false; } @Override public void reset() { // TODO Auto-generated method stub } @Override public void setLocale(Locale loc) { // TODO Auto-generated method stub } @Override public Locale getLocale() { // TODO Auto-generated method stub return null; } @Override public String getHeader(String name) { List<String> l = headers.get(name); if (l == null || l.size() == 0) return null; return l.get(0); } @Override public Collection<String> getHeaders(String name) { return headers.get(name); } @Override public Collection<String> getHeaderNames() { return headers.keySet(); } @Override public void addDateHeader(String name, long date) { addHeader(name, new Date(date).toString()); } @Override public void addIntHeader(String name, int value) { addHeader(name, Integer.toString(value)); } @Override public void addHeader(String name, String value) { List<String> l = headers.get(name); if (l == null) { l = new ArrayList<String>(); headers.put(name, l); } l.add(value); } @Override public int getStatus() { return status.getCode(); } @Override public void setDateHeader(String name, long date) { setHeader(name, new Date(date).toString()); } @Override public void setIntHeader(String name, int value) { setHeader(name, Integer.toString(value)); } @Override public void setHeader(String name, String value) { if (logger.isDebugEnabled()) logger.debug("kraken httpd: set response header [name: {}, value: {}]", name, value); headers.put(name, Arrays.asList(value)); } @Deprecated @Override public void setStatus(int sc, String sm) { setStatus(sc); } @Override public void setStatus(int sc) { this.status = HttpResponseStatus.valueOf(sc); } }