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());
      }
   }
}