package uws.service.error; /* * This file is part of UWSLibrary. * * UWSLibrary is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * UWSLibrary 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with UWSLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.json.JSONException; import org.json.JSONWriter; import tap.TAPException; import uws.AcceptHeader; import uws.UWSException; import uws.UWSToolBox; import uws.job.ErrorSummary; import uws.job.ErrorType; import uws.job.UWSJob; import uws.job.serializer.UWSSerializer; import uws.job.user.JobOwner; import uws.service.log.UWSLog; import uws.service.log.UWSLog.LogLevel; /** * <p>Default implementation of a {@link ServiceErrorWriter} interface for a UWS service.</p> * * <p> * All errors are written using the function {@link #formatError(String, ErrorType, int, String, String, JobOwner, HttpServletResponse, String)} * in order to format the error in the most appropriate format. 2 formats are managed by default by this implementation: HTML (default) and JSON. * This format is chosen thanks to the "Accept" header of the HTTP request. If no request is provided or if there is no known format, * the HTML format is chosen by default. * </p> * * <p> * {@link UWSException}s may precise the HTTP error code to apply, * which will be used to set the HTTP status of the response. If it is a different kind of exception, * the HTTP status 500 (INTERNAL SERVER ERROR) will be used. * </p> * * <p> * Besides, all exceptions except {@link UWSException} and {@link TAPException} will be logged as FATAL in the TAP context * (with no event and no object). Thus the full stack trace is available to the administrator so that the error can * be understood as easily and quickly as possible. * <i>The stack trace is no longer displayed to the user.</i> * </p> * * @author Grégory Mantelet (CDS;ARI) * @version 4.1 (04/2015) */ public class DefaultUWSErrorWriter implements ServiceErrorWriter { /** List of all managed output formats. */ protected final String[] managedFormats = new String[]{"application/json","json","text/json","text/html","html"}; /** Logger to use when grave error must be logged or if a JSON error occurs. */ protected final UWSLog logger; /** * Build an error writer which will log any error in response of an HTTP request. * * @param logger Object to use to log errors. */ public DefaultUWSErrorWriter(final UWSLog logger){ if (logger == null) throw new NullPointerException("Missing logger! Can not write a default error writer without."); this.logger = logger; } @Override public boolean writeError(Throwable t, HttpServletResponse response, HttpServletRequest request, String reqID, JobOwner user, String action){ if (t == null || response == null) return true; boolean written = false; // If expected error, just write it: if (t instanceof UWSException){ UWSException ue = (UWSException)t; written = writeError(ue.getMessage(), ue.getUWSErrorType(), ue.getHttpErrorCode(), response, request, reqID, user, action); } // Otherwise, log it and write a message to the user: else{ // log the error as GRAVE/FATAL (because unexpected/unmanaged): logger.logUWS(LogLevel.FATAL, null, null, "[REQUEST N°" + reqID + "] " + t.getMessage(), t); // write a message to the user: written = writeError("INTERNAL SERVER ERROR! Sorry, this error is unexpected and no explanation can be provided for the moment. Details about this error have been reported in the service log files ; you should try again your request later or notify the administrator(s) by yourself (with the following 'Request ID').", ErrorType.FATAL, UWSException.INTERNAL_SERVER_ERROR, response, request, reqID, user, action); } return written; } @Override public boolean writeError(String message, ErrorType type, int httpErrorCode, HttpServletResponse response, HttpServletRequest request, String reqID, JobOwner user, String action){ if (message == null || response == null) return true; try{ // Just format and write the error message: formatError(message, type, httpErrorCode, reqID, action, user, response, (request != null) ? request.getHeader("Accept") : null); return true; }catch(IllegalStateException ise){ return false; }catch(IOException ioe){ return false; } } @Override public void writeError(Throwable t, ErrorSummary error, UWSJob job, OutputStream output) throws IOException{ UWSToolBox.writeErrorFile((t instanceof Exception) ? (Exception)t : new UWSException(t), error, job, output); } @Override public String getErrorDetailsMIMEType(){ return "text/plain"; } /** * Parses the header "Accept", splits it in a list of MIME type and compare each one to each managed formats ({@link #managedFormats}). * If there is a match (not case sensitive), return the corresponding managed format immediately. * * @param acceptHeader The header item named "Accept" (which lists all expected response formats). * @return The first format common to the "Accept" header and the managed formats of this writer. */ protected final String chooseFormat(final String acceptHeader){ if (acceptHeader != null && !acceptHeader.trim().isEmpty()){ // Parse the given MIME types list: AcceptHeader accept = new AcceptHeader(acceptHeader); ArrayList<String> lstMimeTypes = accept.getOrderedMimeTypes(); for(String acceptedFormat : lstMimeTypes){ for(String f : managedFormats){ if (acceptedFormat.equalsIgnoreCase(f)) return f; } } } return null; } /** * <p>Formats and writes the given error in the HTTP servlet response.</p> * <p>The format is chosen thanks to the Accept header of the HTTP request. * If unknown, the HTML output is chosen.</p> * * @param message Error message to write. * @param type Type of the error: FATAL or TRANSIENT. * @param httpErrorCode HTTP error code (i.e. 404, 500). * @param reqID ID of the request at the origin of the specified error. * @param action Action which generates the error <i><u>note:</u> displayed only if not NULL and not empty. * @param user User which is at the origin of the request/action which generates the error. * @param response Response in which the error must be written. * @param acceptHeader Value of the header named "Accept" (which lists all allowed response format). * * @throws IOException If there is an error while writing the given exception. * * @see #formatHTMLError(String, ErrorType, int, String, String, JobOwner, HttpServletResponse) * @see #formatJSONError(String, ErrorType, int, String, String, JobOwner, HttpServletResponse) */ protected void formatError(final String message, final ErrorType type, final int httpErrorCode, final String reqID, final String action, final JobOwner user, final HttpServletResponse response, final String acceptHeader) throws IOException{ String format = chooseFormat(acceptHeader); if (format != null && (format.equalsIgnoreCase("application/json") || format.equalsIgnoreCase("text/json") || format.equalsIgnoreCase("json"))) formatJSONError(message, type, httpErrorCode, reqID, action, user, response); else formatHTMLError(message, type, httpErrorCode, reqID, action, user, response); } /** * <p>Formats and writes the given error in the HTTP servlet response.</p> * <p>A full HTML response is printed with: the HTTP error code, the error type, the name of the exception, the message and the full stack trace.</p> * * @param message Error message to write. * @param type Type of the error: FATAL or TRANSIENT. * @param httpErrorCode HTTP error code (i.e. 404, 500). * @param reqID ID of the request at the origin of the specified error. * @param action Action which generates the error <i><u>note:</u> displayed only if not NULL and not empty. * @param user User which is at the origin of the request/action which generates the error. * @param response Response in which the error must be written. * * @throws IOException If there is an error while writing the given exception. */ protected void formatHTMLError(final String message, final ErrorType type, final int httpErrorCode, final String reqID, final String action, final JobOwner user, final HttpServletResponse response) throws IOException{ try{ // Erase anything written previously in the HTTP response: response.reset(); // Set the HTTP status: response.setStatus(httpErrorCode); // Set the MIME type of the answer (XML for a VOTable document): response.setContentType(UWSSerializer.MIME_TYPE_HTML); // Set the character encoding: response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); }catch(IllegalStateException ise){ /* If it is not possible any more to reset the response header and body, * the error is anyway written in order to corrupt the HTTP response. * Thus, it will be obvious that an error occurred and the result is * incomplete and/or wrong.*/ } PrintWriter out; try{ out = response.getWriter(); }catch(IllegalStateException ise){ /* This exception may occur just because either the writer or * the output-stream can be used (because already got before). * So, we just have to get the output-stream if getting the writer * throws an error.*/ out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))); } // Header: out.println("<html>\n\t<head>"); out.println("\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />"); out.println("\t\t<style type=\"text/css\">"); out.println("\t\t\tbody { background-color: white; color: black; }"); out.println("\t\t\th2 { font-weight: bold; font-variant: small-caps; text-decoration: underline; font-size: 1.5em; color: #4A4A4A; }"); out.println("\t\t\tul, ol { margin-left: 2em; margin-top: 0.2em; text-align: justify; }"); out.println("\t\t\tli { margin-bottom: 0.2em; margin-top: 0; }"); out.println("\t\t\tp, p.listheader { text-align: justify; text-indent: 2%; margin-top: 0; }"); out.println("\t\t\ttable { border-collapse: collapse; }"); out.println("\t\t\ttable, th, td { border: 1px solid #FC8813; }"); out.println("\t\t\tth { background-color: #F29842; color: white; font-size: 1.1em; }"); out.println("\t\t\ttr.alt { background-color: #FFDAB6; }"); out.println("\t\t</style>"); out.println("\t\t<title>SERVICE ERROR</title>"); out.println("\t</head>\n\t<body>"); // Title: String errorColor = (type == ErrorType.FATAL) ? "red" : "orange"; out.println("\t\t<h1 style=\"text-align: center; background-color:" + errorColor + "; color: white; font-weight: bold;\">SERVICE ERROR - " + httpErrorCode + "</h1>"); // Description part: out.println("\t\t<h2>Description</h2>"); out.println("\t\t<ul>"); out.println("\t\t\t<li><b>Type: </b>" + type + "</li>"); if (reqID != null) out.println("\t\t\t<li><b>Request ID: </b>" + reqID + "</li>"); if (action != null) out.println("\t\t\t<li><b>Action: </b>" + action + "</li>"); out.println("\t\t\t<li><b>Message:</b><p>" + message + "</p></li>"); out.println("\t\t</ul>"); out.println("\t</body>\n</html>"); out.flush(); } /** * <p>Formats and writes the given error in the HTTP servlet response.</p> * <p>A JSON response is printed with: the HTTP error code, the error type, the name of the exception, the message and the list of all causes' message.</p> * * @param message Error message to write. * @param type Type of the error: FATAL or TRANSIENT. * @param httpErrorCode HTTP error code (i.e. 404, 500). * @param reqID ID of the request at the origin of the specified error. * @param action Action which generates the error <i><u>note:</u> displayed only if not NULL and not empty. * @param user User which is at the origin of the request/action which generates the error. * @param response Response in which the error must be written. * * @throws IOException If there is an error while writing the given exception. */ protected void formatJSONError(final String message, final ErrorType type, final int httpErrorCode, final String reqID, final String action, final JobOwner user, final HttpServletResponse response) throws IOException{ try{ // Erase anything written previously in the HTTP response: response.reset(); // Set the HTTP status: response.setStatus(httpErrorCode); // Set the MIME type of the answer (JSON): response.setContentType(UWSSerializer.MIME_TYPE_JSON); // Set the character encoding: response.setCharacterEncoding(UWSToolBox.DEFAULT_CHAR_ENCODING); }catch(IllegalStateException ise){ /* If it is not possible any more to reset the response header and body, * the error is anyway written in order to corrupt the HTTP response. * Thus, it will be obvious that an error occurred and the result is * incomplete and/or wrong.*/ } PrintWriter out; try{ out = response.getWriter(); }catch(IllegalStateException ise){ /* This exception may occur just because either the writer or * the output-stream can be used (because already got before). * So, we just have to get the output-stream if getting the writer * throws an error.*/ out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))); } try{ JSONWriter json = new JSONWriter(out); json.object(); json.key("errorcode").value(httpErrorCode); json.key("errortype").value(type.toString()); if (reqID != null) json.key("requestid").value(reqID); if (action != null) json.key("action").value(action); json.key("message").value(message); json.endObject(); out.flush(); }catch(JSONException je){ logger.logUWS(LogLevel.ERROR, null, "FORMAT_ERROR", "Impossible to format/write an error in JSON!", je); throw new IOException("Error while formatting the error in JSON!", je); } } }