ErrorHandler.java
/*
** Module : ErrorHandler.java
** Abstract : Error handler for custom database functions implemented in Java
**
** Copyright (c) 2004-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------
** 001 ECF 20071212 @36497 Created initial version. An error handler for custom database
** functions implemented in Java.
** 002 SVL 20080425 @38118 ErrorManager.setHeadless() is set only if the class is loaded
** inside PL/Java JVM.
** 003 OM 20140728 Methods initError() and checkError() annotated as volatile
** (non immutable) java functions for PL/SQL.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist.pl;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.logging.*;
/**
* An error handler for custom database functions implemented in Java.
* <p>
* Normally, exceptions thrown in Java code invoked by function and operator
* implementations are allowed to propagate normally. However, in certain
* cases, exceptions must be caught and handled silently. In these cases, the
* fact that an error was handled must affect the outcome of a subexpression
* within an SQL statement. Currently, the only case in which errors must be
* handled in this fashion is within a converted CAN-FIND statement, nested
* within a where clause (including within the where clause of an enclosing,
* converted, CAN-FIND statement). In this case, a nested CAN-FIND is
* converted into a subquery (a.k.a., subselect). If that subquery contains
* any built-in functions or operators which are implemented in Java, the
* functions implemented within this class are used to safely handle any
* potential exceptions.
* <p>
* Functions and operators implemented in Java, which are used in such a
* circumstance, must be wrapped with a call to {@link
* #checkError(Boolean, Boolean)}, such that the nearest enclosing
* subexpression which can evaluate to a boolean either evaluates normally
* (if no error occurs), or evaluates to <code>false</code> in the event an
* error occurs. For instance, the subexpression
* <pre>
* toInt('foo') = 5
* </pre>
* must evaluate to <code>false</code> if it occurs within a subquery, because
* the left side of the equals operation will fail with an error. Such an
* expression would be allowed to fail normally, however, if it occurred
* outside a subquery. To accomplish the former, the above subexpression,
* when occurring within a subquery, would actually be expanded to
* <pre>
* checkError(initError(false), toInt('foo') = 5)
* </pre>
* <p>
* The call to <code>initError(false)</code> initializes the current scope of
* the handler to a known (error-free) state. In the event of an error within
* the <code>toInt()</code> function, {@link #handleError(RuntimeException)}
* is invoked, the current scope is marked as having an error, and
* <code>null</code> is returned by <code>toInt()</code>. When the enclosing
* <code>checkError()</code> function is invoked, the current scope is checked
* for errors. If an error occurred, <code>checkError()</code> returns
* <code>false</code>. If no error occurred, the value of the second
* parameter to {@link #checkError(Boolean, Boolean)} (i.e., the result of the
* subexpression) is returned.
* <p>
* <b>Implementation Notes</b><br>
* This implementation relies on several critical assumptions:
* <ul>
* <li>The first parameter to <code>checkError()</code> will always be
* <code>initError(false)</code> (see next bullet as to why this is
* important). This is the responsibility of the SQL author.
* <li>Parameters to a database function are always evaluated in order,
* from left to right. It is imperative that the {@link
* #initError(Boolean)} method is invoked before the subexpression
* whose boolean result is passed as the second parameter to the {@link
* #checkError(Boolean, Boolean)} method. The former initializes the
* state of the <code>ErrorHandler</code> for the current
* subexpression's scope. If an error occurs within that subexpression,
* the state is assumed to be initialized properly.
* <li>An SQL expression which calls into the JVM for function services will
* always use the same JVM thread across multiple function calls. The
* implementation of this class' methods relies upon a
* <code>ThreadLocal</code> variable to share state across these
* multiple calls. If different threads were to be used, this scheme
* clearly would not work.
* <li>Every database function/operator implemented in Java will enclose its
* main body within a try-catch block which catches
* <code>RuntimeException</code>, will call {@link
* #handleError(RuntimeException)} from within that catch block, and
* will return <code>null</code> after that call.
* </ul>
*/
public final class ErrorHandler
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get("");
/** Stack of error flags used to track errors by function scope */
private static final ThreadLocal<Deque<Boolean>> errors =
new ThreadLocal<Deque<Boolean>>()
{
protected Deque<Boolean> initialValue()
{
return (new LinkedList<>());
}
};
static
{
// if class is loaded inside PL/Java JVM
if (SecurityManager.getInstance() == null)
{
// Ensure ErrorManager does no logging and does not use UI.
com.goldencode.p2j.util.ErrorManager.setHeadless();
}
}
/**
* Initialize the error flag stack for the current function scope by
* pushing the given error state onto the stack.
*
* @param state
* Error state to push onto the stack.
*
* @return <code>Boolean.TRUE</code>.
*/
@HQLFunction(immutable = false)
public static Boolean initError(Boolean state)
{
Deque<Boolean> stack = errors.get();
stack.push(state);
return Boolean.TRUE;
}
/**
* A special wrapper function for a function implemented in Java and called
* within a SUBSELECT SQL phrase which represents a converted CAN-FIND
* statement, where the wrapped function can potentially produce an error
* condition. For instance:
* <pre>
* ...where (select id
* from some_table
* where checkError(initError(false), toInt_safe(...) = 5)
* or ...
* limit 1)
* is not null
* </pre>
* <p>
* The intended convention is to call <code>initError(false)</code> as the
* first parameter to the method, in order to initialize the error state
* for the current scope, and to pass as the second parameter a boolean
* sub-expression which contains a call to one of the *_safe() functions
* implemented in Java.
* <p>
* This method checks the error flag for the current scope and does the
* following:
* <ul>
* <li>if the error flag for the current scope is <code>true</code>
* (indicating an error has occurred in a Java method invoked from
* the enclosed boolean sub-expression), this method returns
* <code>false</code>;
* <li>if the error flag for the current scope is <code>false</code>
* (indicating no error has occurred in Java within the boolean
* sub-expression), the result of executing the second parameter is
* returned.
* </ul>
*
* @param init
* The result of the <code>initError(false)</code> function
* called as the first parameter; not used.
* @param result
* The result of executing the boolean expression containing the
* Java function or operator. This result indicates the
* success/failure of the enclosed, boolean sub-expression.
*
* @return <code>false</code> if an error occurred in this scope;
* otherwise, the result of the boolean sub-expression.
*/
@HQLFunction(immutable = false)
public static Boolean checkError(Boolean init, Boolean result)
{
Deque<Boolean> stack = errors.get();
Boolean flag = stack.pop();
return (flag ? Boolean.FALSE : result);
}
/**
* Handle a P2J runtime error encountered in the current function scope.
* If the error handler has been initialized with a previous invocation of
* the {@link #initError(Boolean)} function, the error state for the
* current function scope is set to <code>true</code>. Otherwise, the
* exception passed to this method is simply rethrown.
*
* @param exc
* The exception to be handled.
*
* @see #initError(Boolean)
* @see #checkError(Boolean, Boolean)
*/
static void handleError(RuntimeException exc)
{
Deque<Boolean> stack = errors.get();
// If there is nothing on the error stack, it was not initialized. This
// indicates we are not handling errors in the current scope, but rather
// letting them propagate. Rethrow the exception.
if (stack.peek() == null)
{
throw exc;
}
// Replace the existing error flag for the current scope with true, to
// indicate an error occurred.
stack.pop();
stack.push(true);
// Log the error message text as a warning.
if (LOG.isLoggable(Level.WARNING))
{
LOG.warning(exc.getMessage());
}
}
}