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

org.dbflute.bhv.exception.SQLExceptionHandler Maven / Gradle / Ivy

There is a newer version: 1.2.8
Show newest version
/*
 * Copyright 2014-2019 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 org.dbflute.bhv.exception;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.dbflute.bhv.core.context.ConditionBeanContext;
import org.dbflute.bhv.core.context.InternalMapContext;
import org.dbflute.bhv.core.context.InternalMapContext.InvokePathProvider;
import org.dbflute.bhv.core.context.ResourceContext;
import org.dbflute.cbean.ConditionBean;
import org.dbflute.dbway.DBDef;
import org.dbflute.exception.EntityAlreadyExistsException;
import org.dbflute.exception.SQLFailureException;
import org.dbflute.helper.message.ExceptionMessageBuilder;
import org.dbflute.outsidesql.OutsideSqlContext;
import org.dbflute.system.DBFluteSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author jflute
 */
public class SQLExceptionHandler {

    // ===================================================================================
    //                                                                          Definition
    //                                                                          ==========
    private static final Logger _log = LoggerFactory.getLogger(SQLExceptionHandler.class);

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    protected final SQLExceptionAdviser _adviser = createAdviser();

    // ===================================================================================
    //                                                                              Handle
    //                                                                              ======
    /**
     * @param e The instance of SQLException. (NotNull)
     * @param resource The resource, item and elements, of SQLException message. (NotNull)
     */
    public void handleSQLException(SQLException e, SQLExceptionResource resource) {
        if (resource.isUniqueConstraintHandling() && isUniqueConstraintException(e)) {
            throwEntityAlreadyExistsException(e, resource);
        } else {
            throwSQLFailureException(e, resource);
        }
    }

    protected boolean isUniqueConstraintException(SQLException e) {
        if (!ResourceContext.isExistResourceContextOnThread()) {
            return false;
        }
        return ResourceContext.isUniqueConstraintException(extractSQLState(e), e.getErrorCode());
    }

    // ===================================================================================
    //                                                                               Throw
    //                                                                               =====
    protected void throwEntityAlreadyExistsException(SQLException e, SQLExceptionResource resource) {
        final ExceptionMessageBuilder br = createExceptionMessageBuilder();
        br.addNotice("The entity already exists on the database.");
        br.addItem("Advice");
        br.addElement("Please confirm the primary key whether it already exists on the database.");
        br.addElement("And also confirm the unique constraint for other columns.");
        setupCommonElement(br, e, resource);
        final String msg = br.buildExceptionMessage();
        throw new EntityAlreadyExistsException(msg, e);
    }

    protected void throwSQLFailureException(SQLException e, SQLExceptionResource resource) {
        final ExceptionMessageBuilder br = createExceptionMessageBuilder();
        final List noticeList = resource.getNoticeList();
        if (!noticeList.isEmpty()) {
            for (String notice : noticeList) {
                br.addNotice(notice);
            }
        } else {
            br.addNotice("The SQL failed to execute."); // as default
        }
        br.addItem("Advice");
        br.addElement("Read the SQLException message.");
        final String advice = askAdvice(e, ResourceContext.currentDBDef());
        if (advice != null && advice.trim().length() > 0) {
            br.addElement("*" + advice);
        }
        setupCommonElement(br, e, resource);
        final String msg = br.buildExceptionMessage();
        throw new SQLFailureException(msg, e);
    }

    protected ExceptionMessageBuilder createExceptionMessageBuilder() {
        return new ExceptionMessageBuilder();
    }

    protected String askAdvice(SQLException e, DBDef dbdef) {
        return _adviser.askAdvice(e, dbdef);
    }

    // ===================================================================================
    //                                                                             Element
    //                                                                             =======
    protected void setupCommonElement(ExceptionMessageBuilder br, SQLException e, SQLExceptionResource resource) {
        br.addItem("SQLState");
        br.addElement(extractSQLState(e));
        br.addItem("ErrorCode");
        br.addElement(e.getErrorCode());
        setupSQLExceptionElement(br, e);
        final Map> resourceMap = resource.getResourceMap();
        for (Entry> entry : resourceMap.entrySet()) {
            br.addItem(entry.getKey());
            final List elementList = entry.getValue();
            for (Object element : elementList) {
                br.addElement(element);
            }
        }
        setupBehaviorElement(br);
        setupConditionBeanElement(br);
        setupOutsideSqlElement(br);
        setupTargetSqlElement(br, resource);
    }

    protected void setupSQLExceptionElement(ExceptionMessageBuilder br, SQLException e) {
        br.addItem("SQLException");
        br.addElement(e.getClass().getName());
        br.addElement(extractMessage(e));
        final SQLException nextEx = e.getNextException();
        if (nextEx != null) {
            br.addItem("NextException");
            br.addElement(nextEx.getClass().getName());
            br.addElement(extractMessage(nextEx));
            final SQLException nextNextEx = nextEx.getNextException();
            if (nextNextEx != null) {
                br.addItem("NextNextException");
                br.addElement(nextNextEx.getClass().getName());
                br.addElement(extractMessage(nextNextEx));
            }
        }
    }

    protected void setupBehaviorElement(ExceptionMessageBuilder br) {
        final Object invokePath = extractBehaviorInvokePath();
        if (invokePath != null) {
            br.addItem("Behavior");
            br.addElement(invokePath);
        }
    }

    protected void setupConditionBeanElement(ExceptionMessageBuilder br) {
        if (hasConditionBean()) {
            br.addItem("ConditionBean"); // only class name because of already existing displaySql
            br.addElement(getConditionBean().getClass().getName());
        }
    }

    protected void setupOutsideSqlElement(ExceptionMessageBuilder br) {
        if (hasOutsideSqlContext()) {
            br.addItem("OutsideSql");
            br.addElement(getOutsideSqlContext().getOutsideSqlPath());
        }
    }

    // *because displaySql exists instead which is enough to debug the exception
    //  (and for security to application data)
    //protected void setupParameterBeanElement(ExceptionMessageBuilder br) {
    //    if (hasOutsideSqlContext()) {
    //        br.addItem("ParameterBean");
    //        br.addElement(getOutsideSqlContext().getParameterBean());
    //    }
    //}

    // *because it's not an important thing
    //protected void setupStatementElement(ExceptionMessageBuilder br, Statement st) {
    //    if (st != null) {
    //        br.addItem("Statement");
    //        br.addElement(st.getClass().getName());
    //    }
    //}

    /**
     * Set up the element of target SQL. 
* It uses displaySql as default. *

* If you want to hide application data on exception message, * you should override and use executedSql instead of displaySql or set up nothing. * But you should consider the following things: *

*
    *
  • Debug process becomes more difficult.
  • *
  • If you use embedded variables in the SQL, executedSql may also have application data.
  • *
  • JDBC driver's message may also have application data about exception's cause.
  • *
*

* So if you want to COMPLETELY hide application data on exception message, * you should cipher your application logs (files). * (If you hide JDBC driver's message too, you'll be at a loss when you debug) *

* @param br The builder of exception message. (NotNull) * @param resource The resource, item and elements, of SQLException message. (NotNull) */ protected void setupTargetSqlElement(ExceptionMessageBuilder br, SQLExceptionResource resource) { final String displaySql = resource.getDisplaySql(); if (displaySql != null) { if (resource.isDisplaySqlPartHandling()) { br.addItem("Part of Display SQLs"); br.addElement(displaySql); br.addElement("..."); br.addElement("..."); br.addElement("(and other statements)"); } else { br.addItem("Display SQL"); br.addElement(displaySql); } } // this is example to use executed SQL //final String executedSql = resource.getExecutedSql(); //if (executedSql != null) { // br.addItem("Executed SQL"); // br.addElement(executedSql); // br.addElement("*NOT use displaySql for security"); //} } // =================================================================================== // Extract // ======= protected String extractMessage(SQLException e) { String message = e.getMessage(); // because a message of Oracle contains a line separator return message != null ? message.trim() : message; } protected String extractSQLState(SQLException e) { String sqlState = e.getSQLState(); if (sqlState != null) { return sqlState; } // Next SQLException nextEx = e.getNextException(); if (nextEx == null) { return null; } sqlState = nextEx.getSQLState(); if (sqlState != null) { return sqlState; } // Next Next SQLException nextNextEx = nextEx.getNextException(); if (nextNextEx == null) { return null; } sqlState = nextNextEx.getSQLState(); if (sqlState != null) { return sqlState; } // Next Next Next SQLException nextNextNextEx = nextNextEx.getNextException(); if (nextNextNextEx == null) { return null; } sqlState = nextNextNextEx.getSQLState(); if (sqlState != null) { return sqlState; } // It doesn't use recursive call by design because JDBC is unpredictable fellow. return null; } protected String extractBehaviorInvokePath() { try { final String provided = doExtractBehaviorInvokePathFromProvider(); if (provided != null) { // basically not null () return provided; } return doExtractBehabiorInvokePathFromSeparatedParts(); } catch (RuntimeException continued) { // this is additional info for debug so continue if (_log.isDebugEnabled()) { _log.debug("Failed to extract behavior invoke path for debug.", continued); } return null; } } protected String doExtractBehaviorInvokePathFromProvider() { final InvokePathProvider provider = InternalMapContext.getInvokePathProvider(); return provider != null ? provider.provide() : null; } protected String doExtractBehabiorInvokePathFromSeparatedParts() { final Object behaviorInvokeName = InternalMapContext.getBehaviorInvokeName(); if (behaviorInvokeName == null) { return null; } final Object clientInvokeName = InternalMapContext.getClientInvokeName(); final Object byPassInvokeName = InternalMapContext.getByPassInvokeName(); final StringBuilder sb = new StringBuilder(); boolean existsPath = false; if (clientInvokeName != null) { existsPath = true; sb.append(clientInvokeName); } if (byPassInvokeName != null) { existsPath = true; sb.append(byPassInvokeName); } sb.append(behaviorInvokeName); if (existsPath) { sb.append("..."); } return sb.toString(); } // =================================================================================== //   Adviser // ======= protected SQLExceptionAdviser createAdviser() { return new SQLExceptionAdviser(); } // =================================================================================== // Assist Helper // ============= protected boolean hasConditionBean() { return ConditionBeanContext.isExistConditionBeanOnThread(); } protected ConditionBean getConditionBean() { return ConditionBeanContext.getConditionBeanOnThread(); } protected boolean hasOutsideSqlContext() { return OutsideSqlContext.isExistOutsideSqlContextOnThread(); } protected OutsideSqlContext getOutsideSqlContext() { return OutsideSqlContext.getOutsideSqlContextOnThread(); } protected String ln() { return DBFluteSystem.ln(); } }