/*
* � Copyright IBM Corp. 2010, 2014
*
* 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 com.ibm.domino.services;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.zip.GZIPOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lotus.domino.NotesException;
import com.ibm.commons.util.HtmlCommonsUtil;
import com.ibm.commons.util.StringUtil;
import com.ibm.commons.util.io.ByteStreamCache;
import com.ibm.commons.util.io.StreamUtil;
import com.ibm.domino.services.util.JsonWriter;
import static com.ibm.domino.services.HttpServiceConstants.*;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_CODE;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_DATA;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_MESSAGE;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_TEXT;
import static com.ibm.domino.services.rest.RestServiceConstants.ATTR_TYPE;
/**
* Abstract Service Engine.
*
* This provides the basic HTTP service required by REST services implementations.
*/
public abstract class HttpServiceEngine implements ServiceEngine {
private boolean preventCache;
private boolean preventGzip;
private HttpServletRequest httpRequest;
private HttpServletResponse httpResponse;
private boolean streamFlushed;
private ByteStreamCache bs;
private OutputStream os;
public HttpServiceEngine(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
this.httpRequest = httpRequest;
this.httpResponse = httpResponse;
}
public void recycle() {
}
public boolean isPreventGzip() {
return preventGzip;
}
public void setPreventGzip(boolean preventGzip) {
this.preventGzip = preventGzip;
}
public boolean isPreventCache() {
return preventCache;
}
public void setPreventCache(boolean preventCache) {
this.preventCache = preventCache;
}
public HttpServletRequest getHttpRequest() {
return httpRequest;
}
public HttpServletResponse getHttpResponse() {
return httpResponse;
}
public void resetStream() throws ServiceException {
bs = null;
os = null;
}
public void flushStream() throws ServiceException {
if(streamFlushed) {
return;
}
streamFlushed = true;
if(os!=null) {
try {
os.flush();
if(bs!=null) {
// Emit the GZIP header if necessary
if(os instanceof GZIPOutputStream) {
((GZIPOutputStream)os).finish();
httpResponse.setHeader(HEADER_CONTENT_ENCODING, ENCODING_GZIP);
}
// We have the length from the buffer
httpResponse.setContentLength((int)bs.getLength());
// And copy the entire content
InputStream is = bs.getInputStream();
OutputStream out = getHttpResponse().getOutputStream();
StreamUtil.copyStream(is, out);
out.flush();
} else {
// Finish the GZIP stream
if(os instanceof GZIPOutputStream) {
((GZIPOutputStream)os).finish();
httpResponse.setHeader(HEADER_CONTENT_ENCODING, ENCODING_GZIP);
}
}
} catch(IOException ex) {
throw new ServiceException(ex,"Error when sending data to the client"); // $NLX-HttpServiceEngine.Errorwhensendingdatatotheclient-1$
}
}
}
public OutputStream getOutputStream() throws ServiceException {
if(streamFlushed) {
throw new ServiceException(null,"Stream had previously been flushed"); // $NLX-HttpServiceEngine.Streamhadpreviouslybeenflushed-1$
}
if(os==null) {
try {
// Check if the Stream should a zipped one
boolean gzip = false;
if(!isPreventGzip()) {
String encodings=httpRequest.getHeader(HEADER_ACCEPT_ENCODING);
gzip = encodings!=null && encodings.indexOf(ENCODING_GZIP)>=0;
}
// Create the output stream
if(isPreventCache()) {
os = getHttpResponse().getOutputStream();
} else {
bs = new ByteStreamCache();
os = bs.getOutputStream();
}
if(gzip) {
os = new GZIPOutputStream(os);
}
} catch(IOException ex) {
throw new ServiceException(ex,"Unable to get the output stream"); // $NLX-HttpServiceEngine.Unabletogettheoutputstream-1$
}
}
return os;
}
public void processRequest() {
try {
renderService();
flushStream();
} catch(Exception ex) {
displayError(ex);
}
}
/**
* Process the request.
* @throws ServiceException
*/
public abstract void renderService() throws ServiceException;
/**
* Method that emits an error as the response.
*/
public void displayError(Exception ex) {
try {
// Log to the XPages log file
if( Loggers.SERVICES_LOGGER.isTraceDebugEnabled() ){
String debugMsg = "Problem encountered processing a request: {0}"; // $NON-NLS-1$
Loggers.SERVICES_LOGGER.traceDebugp(this, "processRequest", ex, debugMsg, ex.toString()); // $NON-NLS-1$
}
resetStream();
if (ex instanceof ServiceException) {
getHttpResponse().setStatus(((ServiceException)ex).getResponseCode().httpStatusCode);
} else {
getHttpResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
Writer writer = new OutputStreamWriter(getOutputStream(),ENCODING_UTF8);
String contentType = getHttpResponse().getContentType();
if (StringUtil.isNotEmpty(contentType) && contentType.contains(CONTENTTYPE_APPLICATION_JSON)) {
writeJSONException(writer, ex);
}
else{
getHttpResponse().setContentType(CONTENTTYPE_APPLICATION_JSON);
writeJSONException(writer, ex);
}
flushStream();
} catch(Throwable t) {
if( Loggers.SERVICES_LOGGER.isTraceDebugEnabled() ){
String debugMsg = "Problem encountered when generating an error page."; // $NON-NLS-1$
Loggers.SERVICES_LOGGER.traceDebugp(this, "displayError", t, debugMsg); // $NON-NLS-1$
}
}
}
/**
* Writes a HTML exception.
*
* @param jwriter
* @param exception
* @throws IOException
*/
public static void writeHTMLException(Writer writer, Exception exception) throws IOException {
PrintWriter pw = new PrintWriter(writer);
pw.println("<h2>" + // $NON-NLS-1$
"Service error" + // $NLX-HttpServiceEngine.Serviceerror-1$
"</h2>"); // $NON-NLS-1$
String message = exception.getMessage();
pw.println("<pre>"); // $NON-NLS-1$
// SPR#MKEE9M5JQ2 issue 6c, need to escape < in message to <
pw.println(HtmlCommonsUtil.toXhtml(message));
pw.println("</pre>"); // $NON-NLS-1$
pw.println("<h2>" + // $NON-NLS-1$
"Stack trace" + // $NLX-HttpServiceEngine.Stacktrace-1$
"</h2>"); // $NON-NLS-1$
pw.println("<pre>"); // $NON-NLS-1$
writeExceptionTrace2(pw, exception, 0);
pw.println("</pre>"); // $NON-NLS-1$
pw.flush();
}
private static void writeExceptionTrace2(PrintWriter writer, Throwable e, int rows) {
// SPR#MKEE9M5JQ2 issue 6c, need to escape < in stack trace to <
// code similar to com.ibm.xsp.webapp.XSPErrorPage.writeExceptionTrace2(PrintWriter, Throwable, int)
if(e!=null) {
StackTraceElement[] elt = e.getStackTrace();
writer.println(HtmlCommonsUtil.toXhtml(e.toString()));
for(int i=0; i<elt.length; i++) {
writer.println(" "+HtmlCommonsUtil.toXhtml(elt[i].toString()) ); //$NON-NLS-1$
// Limit the size of the trace, in case of an stackoverflow exception...
rows++;
if(rows>200) {
return;
}
}
writeExceptionTrace2(writer, e.getCause(), rows);
}
}
/**
* Writes a JSON exception.
*
* @param jwriter
* @param throwable
* @throws IOException
*/
public static void writeJSONException(Writer writer, Throwable throwable) throws IOException {
if (throwable == null) {
return;
}
JsonWriter jwriter = new JsonWriter(writer, false);
try {
jwriter.startObject();
ResponseCode status = ResponseCode.UNINITIALIZED;
if (throwable instanceof ServiceException) {
status = ((ServiceException)throwable).getResponseCode();
}
jwriter.startProperty(ATTR_CODE);
jwriter.outIntLiteral(status.httpStatusCode);
jwriter.endProperty();
jwriter.startProperty(ATTR_TEXT);
jwriter.outStringLiteral(status.httpStatusText);
jwriter.endProperty();
// message
jwriter.startProperty(ATTR_MESSAGE);
String message = "";
Throwable t = throwable;
while ((message == null || message.length() == 0) && t != null)
{
if (t instanceof NotesException)
message = ((NotesException)t).text;
else
message = t.getMessage();
t = t.getCause();
}
if (message == null)
jwriter.outStringLiteral(""); //$NON-NLS-1$
else
jwriter.outStringLiteral(message);
jwriter.endProperty();
// type
jwriter.startProperty(ATTR_TYPE);
jwriter.outStringLiteral(ATTR_TEXT);
jwriter.endProperty();
// data
jwriter.startProperty(ATTR_DATA);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
jwriter.outStringLiteral(sw.toString());
jwriter.endProperty();
}
finally {
jwriter.endObject();
}
jwriter.flush();
}
}