All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ninja.NinjaDefault Maven / Gradle / Ivy

There is a newer version: 7.0.0
Show newest version
/**
 * Copyright (C) the original author or authors.
 *
 * 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 ninja;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;

import javax.management.RuntimeErrorException;

import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import ninja.diagnostics.DiagnosticError;
import ninja.diagnostics.DiagnosticErrorBuilder;
import ninja.exceptions.BadRequestException;
import ninja.exceptions.ForbiddenRequestException;
import ninja.exceptions.RenderingException;
import ninja.exceptions.RequestNotFoundException;
import ninja.i18n.Messages;
import ninja.lifecycle.LifecycleService;
import ninja.utils.Message;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.ResultHandler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;

public class NinjaDefault implements Ninja {

    private static final Logger logger = LoggerFactory.getLogger(NinjaDefault.class);

    /**
     * The most important thing: A cool logo.
     */
    private static final String NINJA_LOGO = "\n"
            + " _______  .___ _______        ____.  _____   \n"
            + " \\      \\ |   |\\      \\      |    | /  _  \\  \n"
            + " /   |   \\|   |/   |   \\     |    |/  /_\\  \\ \n"
            + "/    |    \\   /    |    \\/\\__|    /    |    \\  https://www.ninjaframework.org\n"
            + "\\____|__  /___\\____|__  /\\________\\____|__  /  @ninjaframework\n"
            + "     web\\/framework   \\/                  \\/   {}\n";

    private static final String NINJA_LOGO_LOCATION = "ninja/logo.txt";

    private static final String NINJA_BUILTIN_PROPERTIES_FILE = "ninja/ninja-builtin.properties";
    private static final String NINJA_BUILTIN_PROPERTIES_NINJA_VERSION_KEY = "ninja.version";

    @Inject
    protected LifecycleService lifecycleService;

    @Inject
    protected Router router;

    @Inject
    protected ResultHandler resultHandler;

    @Inject
    protected Messages messages;

    @Inject
    protected NinjaProperties ninjaProperties;

    /**
     * Whether diagnostics are enabled. If enabled then the default system/views
     * will be skipped and a detailed diagnostic error result will be returned
     * by the various methods in this class. You get precise feedback where
     * an error occurred including original source code.
     * 
     * @return True if diagnostics are enabled otherwise false.
     */
    public boolean isDiagnosticsEnabled() {
        // extra safety: only disable detailed diagnostic error pages
        // if both in DEV mode and diagnostics are enabled 0
        return ninjaProperties.isDev() && ninjaProperties.getBooleanWithDefault(NinjaConstant.DIAGNOSTICS_KEY_NAME, Boolean.TRUE);
    }

    @Override
    public void onRouteRequest(Context.Impl context) {

        String httpMethod = context.getMethod();

        Route route = router.getRouteFor(httpMethod, context.getRequestPath());

        context.setRoute(route);

        if (route != null) {

            Result underlyingResult = null;

            try {

                underlyingResult = route.getFilterChain().next(context);

                resultHandler.handleResult(underlyingResult, context);

            } catch (Exception exception) {

                // call special handler to capture the underlying result if there is one
                Result result = onException(context, exception, underlyingResult);
                renderErrorResultAndCatchAndLogExceptions(result, context);

            } finally {

                context.cleanup();

            }

        } else {

            // throw a 404 "not found" because we did not find the route
            Result result = getNotFoundResult(context);
            renderErrorResultAndCatchAndLogExceptions(result, context);

        }

    }

    @Override
    public void renderErrorResultAndCatchAndLogExceptions(
            Result result, Context context) {

        try {
            if (context.isAsync()) {
                context.returnResultAsync(result);
            } else {
                resultHandler.handleResult(result, context);
            }
        } catch (Exception exceptionCausingRenderError) {
            logger.error("Unable to handle result. That's really really fishy.",
                    exceptionCausingRenderError);
        }
    }

    @Override
    public void onFrameworkStart() {
        if (ninjaProperties.getBooleanWithDefault(NinjaConstant.NINJA_SPLASH_DISPLAY, true)) {
            showSplashScreenViaLogger();
        }
        lifecycleService.start();
    }

    @Override
    public void onFrameworkShutdown() {
        lifecycleService.stop();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Results for exceptions (404, 500 etc)
    ////////////////////////////////////////////////////////////////////////////

    @Override
    public Result onException(Context context, Exception exception) {

        return onException(context, exception, null);

    }

    public Result onException(Context context, Exception exception, Result underlyingResult) {

        Result result;

        // log the exception as debug
        logger.debug("Unable to process request", exception);

        if (exception instanceof BadRequestException) {

            result = getBadRequestResult(context, (BadRequestException) exception);

        } else if (exception instanceof ForbiddenRequestException) {

            result = getForbiddenResult(context, (ForbiddenRequestException) exception);

        } else if (exception instanceof RequestNotFoundException) {

            result = getNotFoundResult(context, (RequestNotFoundException) exception);

        } else if (exception instanceof RenderingException) {

            result = getRenderingExceptionResult(context, (RenderingException) exception, underlyingResult);

        } else {

            result = getInternalServerErrorResult(context, exception, underlyingResult);

        }

        return result;

    }

    public Result getRenderingExceptionResult(Context context, RenderingException exception, Result underlyingResult) {

        if (isDiagnosticsEnabled()) {

            // prefer provided title and underlying cause
            DiagnosticError diagnosticError = DiagnosticErrorBuilder
                .buildDiagnosticError(
                    (exception.getTitle() == null ? "Rendering exception" : exception.getTitle()),
                    (exception.getCause() == null ? exception : exception.getCause()),
                    exception.getSourcePath(),
                    exception.getLineNumber(),
                    underlyingResult);

            return Results.internalServerError().render(diagnosticError);

        }

        return getInternalServerErrorResult(context, exception, underlyingResult);

    }

    /**
     * Deprecated. Check {@link Ninja#getInternalServerErrorResult(Context, Exception, Result)}.
     */
    @Deprecated
    public Result getInternalServerErrorResult(Context context, Exception exception) {
        return getInternalServerErrorResult(context, exception, null);
    }

    @Override
    public Result getInternalServerErrorResult(Context context, Exception exception, Result underlyingResult) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                DiagnosticErrorBuilder.build500InternalServerErrorDiagnosticError(exception, true, underlyingResult);

            return Results.internalServerError().render(diagnosticError);

        }

        logger.error(
                "Emitting bad request 500. Something really wrong when calling route: {} (class: {} method: {})",
                context.getRequestPath(), 
                context.getRoute().getControllerClass(), 
                context.getRoute().getControllerMethod(), 
                exception);

        Message message = buildErrorMessage(
                context, 
                NinjaConstant.I18N_NINJA_SYSTEM_INTERNAL_SERVER_ERROR_TEXT_KEY, 
                NinjaConstant.I18N_NINJA_SYSTEM_INTERNAL_SERVER_ERROR_TEXT_DEFAULT, 
                Optional.ofNullable(exception), 
                Optional.empty());

        return Results
                .internalServerError()
                .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                .fallbackContentType(Result.TEXT_HTML)
                .render(message)
                .template(
                        ninjaProperties.getWithDefault(
                                NinjaConstant.LOCATION_VIEW_HTML_INTERNAL_SERVER_ERROR_KEY,
                                NinjaConstant.LOCATION_VIEW_FTL_HTML_INTERNAL_SERVER_ERROR));
    }

    @Override
    public Result getNotFoundResult(Context context) {
        return getNotFoundResult(context, null);
    }

    @Override
    public Result getNotFoundResult(Context context, RequestNotFoundException exception) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                    exception != null
                    ? DiagnosticErrorBuilder.build404NotFoundDiagnosticError(exception, true)
                    : DiagnosticErrorBuilder.build404NotFoundDiagnosticError(true);

            return Results.notFound().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context, 
                NinjaConstant.I18N_NINJA_SYSTEM_NOT_FOUND_TEXT_KEY, 
                NinjaConstant.I18N_NINJA_SYSTEM_NOT_FOUND_TEXT_DEFAULT, 
                Optional.ofNullable(exception), 
                Optional.empty());

        return Results
                .notFound()
                .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                .fallbackContentType(Result.TEXT_HTML)
                .render(message)
                .template(
                        ninjaProperties.getWithDefault(
                                NinjaConstant.LOCATION_VIEW_HTML_NOT_FOUND_KEY,
                                NinjaConstant.LOCATION_VIEW_FTL_HTML_NOT_FOUND));

    }

    @Override
    public Result getBadRequestResult(Context context, BadRequestException exception) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                DiagnosticErrorBuilder.build400BadRequestDiagnosticError(exception, true);

            return Results.badRequest().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context, 
                NinjaConstant.I18N_NINJA_SYSTEM_BAD_REQUEST_TEXT_KEY, 
                NinjaConstant.I18N_NINJA_SYSTEM_BAD_REQUEST_TEXT_DEFAULT, 
                Optional.ofNullable(exception), 
                Optional.empty());

        return Results
                .badRequest()
                .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                .fallbackContentType(Result.TEXT_HTML)
                .render(message)
                .template(
                        ninjaProperties.getWithDefault(
                                NinjaConstant.LOCATION_VIEW_HTML_BAD_REQUEST_KEY,
                                NinjaConstant.LOCATION_VIEW_FTL_HTML_BAD_REQUEST));

    }

    @Override
    public Result getUnauthorizedResult(Context context) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                DiagnosticErrorBuilder.build401UnauthorizedDiagnosticError();

            return Results.unauthorized().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context, 
                NinjaConstant.I18N_NINJA_SYSTEM_UNAUTHORIZED_REQUEST_TEXT_KEY, 
                NinjaConstant.I18N_NINJA_SYSTEM_UNAUTHORIZED_REQUEST_TEXT_DEFAULT, 
                Optional.empty(), 
                Optional.empty());

        // WWW-Authenticate must be included per the spec
        // http://www.ietf.org/rfc/rfc2617.txt 3.2.1 The WWW-Authenticate Response Header
        return Results
                .unauthorized()
                .addHeader(Result.WWW_AUTHENTICATE, "None")
                .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                .fallbackContentType(Result.TEXT_HTML)
                .render(message)
                .template(
                        ninjaProperties.getWithDefault(
                                NinjaConstant.LOCATION_VIEW_HTML_UNAUTHORIZED_KEY,
                                NinjaConstant.LOCATION_VIEW_FTL_HTML_UNAUTHORIZED));

    }

    @Override
    public Result getForbiddenResult(Context context) {
        return getForbiddenResult(context, null);
    }

    @Override
    public Result getForbiddenResult(Context context, ForbiddenRequestException exception) {

        // diagnostic mode
        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                exception != null
                ? DiagnosticErrorBuilder.build403ForbiddenDiagnosticError(exception, true)
                : DiagnosticErrorBuilder.build403ForbiddenDiagnosticError();

            return Results.forbidden().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context, 
                NinjaConstant.I18N_NINJA_SYSTEM_FORBIDDEN_REQUEST_TEXT_KEY, 
                NinjaConstant.I18N_NINJA_SYSTEM_FORBIDDEN_REQUEST_TEXT_DEFAULT, 
                Optional.ofNullable(exception), 
                Optional.empty());

        return Results
                .forbidden()
                .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                .fallbackContentType(Result.TEXT_HTML)
                .render(message)
                .template(
                        ninjaProperties.getWithDefault(
                                NinjaConstant.LOCATION_VIEW_HTML_FORBIDDEN_KEY,
                                NinjaConstant.LOCATION_VIEW_FTL_HTML_FORBIDDEN));

    }

    protected Message buildErrorMessage(Context context,
                                        String errorTextKey,
                                        String errorTextDefault,
                                        Optional exception,
                                        Optional underlyingResult) {

        String messageI18n 
                = messages.getWithDefault(
                        errorTextKey,
                        errorTextDefault,
                        context,
                        underlyingResult);

        String errorI18n 
                = !exception.isPresent()
                    ? null 
                    : messages.getWithDefault(
                        exception.get().getMessage(),
                        exception.get().getLocalizedMessage(),
                        context,
                        underlyingResult);

        return new Message(messageI18n, errorI18n);
    }

    /**
     * Simply reads a property resource file that contains the version of this
     * Ninja build. Helps to identify the Ninja version currently running.
     * 
     * @return The version of Ninja. Eg. "1.6-SNAPSHOT" while developing of "1.6" when released.
     */
    private String readNinjaVersion() {

        String ninjaVersion;
        try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(NINJA_BUILTIN_PROPERTIES_FILE)){

            Properties prop = new Properties();
            prop.load(stream);

            ninjaVersion = prop.getProperty(NINJA_BUILTIN_PROPERTIES_NINJA_VERSION_KEY);

        } catch (Exception e) {
            //this should not happen. Never.
            throw new RuntimeErrorException(new Error("Something is wrong with your build. Cannot find resource " + NINJA_BUILTIN_PROPERTIES_FILE));
        }

        return ninjaVersion;

    }

    private void showSplashScreenViaLogger() {

        String ninjaVersion = readNinjaVersion();

        // log Ninja splash screen, from resources if available (so it can be overridden)
        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(NINJA_LOGO_LOCATION);
             InputStreamReader reader = new InputStreamReader(Objects.requireNonNull(is), Charsets.UTF_8)) {
            logger.info(CharStreams.toString(reader), ninjaVersion);
        } catch (IOException | NullPointerException e) {
            // if anything happens, use the old one
            logger.info(NINJA_LOGO, ninjaVersion);
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy