/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.jooby;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Throwables;
/**
* An exception that carry a {@link Status}. The status field will be set in the HTTP
* response.
*
* See {@link Err.Handler} for more details on how to deal with exceptions.
*
* @author edgar
* @since 0.1.0
*/
@SuppressWarnings("serial")
public class Err extends RuntimeException {
/**
* Missing parameter/header or request attribute.
*
* @author edgar
*/
public static class Missing extends Err {
/**
* Creates a new {@link Missing} error.
*
* @param name Name of the missing parameter/header or request attribute.
*/
public Missing(final String name) {
super(Status.BAD_REQUEST, name);
}
}
/**
* Default err handler it does content negotation.
*
* On <code>text/html</code> requests the err handler creates an <code>err</code> view and use the
* {@link Err#toMap()} result as model.
*
* @author edgar
* @since 0.1.0
*/
public static class DefHandler implements Err.Handler {
/** Default err view. */
public static final String VIEW = "err";
/** logger, logs!. */
private final Logger log = LoggerFactory.getLogger(Err.class);
@Override
public void handle(final Request req, final Response rsp, final Err ex) throws Throwable {
log.error("execution of: {}{} resulted in exception\nRoute:\n{}\n\nStacktrace:",
req.method(), req.path(), req.route().print(6), ex);
rsp.send(
Results
.when(MediaType.html, () -> Results.html(VIEW).put("err", ex.toMap()))
.when(MediaType.all, () -> ex.toMap()));
}
}
/**
* Handle and render exceptions. Error handlers are executed in the order they were provided, the
* first err handler that send an output wins!
*
* The default err handler does content negotation on error, see {@link DefHandler}.
*
* @author edgar
* @since 0.1.0
*/
public interface Handler {
/**
* Handle a route exception by probably logging the error and sending a err response to the
* client.
*
* Please note you always get an {@link Err} whenever you throw it or not. For example if your
* application throws an {@link IllegalArgumentException} exception you will get an {@link Err}
* and you can retrieve the original exception by calling {@link Err#getCause()}.
*
* Jooby always give you an {@link Err} with an optional root cause and an associated status
* code.
*
* @param req HTTP request.
* @param rsp HTTP response.
* @param ex Error found and status code.
* @throws Throwable If something goes wrong.
*/
void handle(Request req, Response rsp, Err ex) throws Throwable;
}
/**
* The status code. Required.
*/
private int status;
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
* @param message A error message. Required.
* @param cause The cause of the problem.
*/
public Err(final Status status, final String message, final Throwable cause) {
super(message(status, message), cause);
this.status = status.value();
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
* @param message A error message. Required.
* @param cause The cause of the problem.
*/
public Err(final int status, final String message, final Throwable cause) {
super(message("", status, message), cause);
this.status = status;
}
/**
* Creates a new {@link Err}.
*
* @param status A web socket close status. Required.
* @param message Close message.
*/
public Err(final WebSocket.CloseStatus status, final String message) {
super(message(status.reason(), status.code(), message));
this.status = status.code();
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
* @param message A error message. Required.
*/
public Err(final Status status, final String message) {
super(message(status, message));
this.status = status.value();
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
* @param message A error message. Required.
*/
public Err(final int status, final String message) {
this(Status.valueOf(status), message);
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
* @param cause The cause of the problem.
*/
public Err(final Status status, final Throwable cause) {
super(message(status, null), cause);
this.status = status.value();
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
* @param cause The cause of the problem.
*/
public Err(final int status, final Throwable cause) {
this(Status.valueOf(status), cause);
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
*/
public Err(final Status status) {
super(message(status, null));
this.status = status.value();
}
/**
* Creates a new {@link Err}.
*
* @param status A HTTP status. Required.
*/
public Err(final int status) {
this(Status.valueOf(status));
}
/**
* @return The status code to send as response.
*/
public int statusCode() {
return status;
}
/**
* Produces a friendly view of the err, resulting map has these attributes:
*
* <pre>
* message: exception message (if present)
* stacktrace: array with the stacktrace
* status: status code
* reason: a status code reason
* </pre>
*
* @return A lightweight view of the err.
*/
public Map<String, Object> toMap() {
Status status = Status.valueOf(this.status);
Throwable cause = Optional.ofNullable(getCause()).orElse(this);
String message = Optional.ofNullable(cause.getMessage()).orElse(status.reason());
String[] stacktrace = Throwables.getStackTraceAsString(cause).replace("\r", "").split("\\n");
Map<String, Object> err = new LinkedHashMap<>();
err.put("message", message);
err.put("stacktrace", stacktrace);
err.put("status", status.value());
err.put("reason", status.reason());
return err;
}
/**
* Build an error message using the HTTP status.
*
* @param status The HTTP Status.
* @param tail A message to append.
* @return An error message.
*/
private static String message(final Status status, final String tail) {
return message(status.reason(), status.value(), tail);
}
/**
* Build an error message using the HTTP status.
*
* @param reason Reason.
* @param status The Status.
* @param tail A message to append.
* @return An error message.
*/
private static String message(final String reason, final int status, final String tail) {
return reason + "(" + status + ")" + (tail == null ? "" : ": " + tail);
}
}