uws.service.error.DefaultUWSErrorWriter.java Source code

Java tutorial

Introduction

Here is the source code for uws.service.error.DefaultUWSErrorWriter.java

Source

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 Donnes 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&eacute;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);
        }
    }

}