--
-- $Header: owb_lock.pkb 23-apr-2008.02:13:20 dwhittin Exp $
--
-- owb_lock.pkb
--
-- Copyright (c) 2006, 2008, Oracle. All rights reserved.  
--
--   NAME
--     owb_lock.pkb - This package provides locking routines for OWB product.
--
--   DESCRIPTION
--      This package provides locking routines for OWB product.
-- 
--      ALLOCATE_UNIQUE is a wrapper around DBMS_LOCK.ALLOCATE_UNIQUE, 
--      but it executes it in an autonomous transaction.
-- 
--      LOCK_WORKSPACE and UNLOCK_WORKSPACE can be used to lock/unlock the 
--      access to a workspace. 
--
--  	  LOCK_USER and UNLOCK_USER can be used to lock/unlock the 
--      access to a user.
--
--      In general, any object can be locked in one of the folowing three modes:
--        EXCLUSIVE_MODE - this lock mode conflicts with any other mode.
--        WRITE_MODE - this mode conflicts with EXCLUSIVE_MODE and WRITE_MODE.
--        SHARED_MODE - this mode conflicts only with EXCLUSIVE_MODE.
--      The full lock compatibility table is shown below.
--        When another process holds "held", an attempt to get "get" does
--        the following:
--
--        held           get->  SHARED_MODE  WRITE_MODE  EXCLUSIVE_MODE
--        SHARED_MODE            success      success     failure
--        WRITE_MODE             success      failure     failure
--        EXCLUSIVE_MODE         failure      failure     failure
--
--   MODIFIED   (MM/DD/YY)
--   dwhittin    05/06/08 - XbranchMerge dwhittin_bug-6944914 from tokyo
--   dwhittin    04/23/08 - Added new REQUEST function to handle existing lock
--   rvelisar    08/21/06 - Creation
--

CREATE OR REPLACE PACKAGE BODY OWB_LOCK AS

-----------------------------------------------------------------------------
-- Types and package variables
-----------------------------------------------------------------------------

-- This type represents the current state of the locking counts for the locks 
-- requested for an object.
TYPE lock_state_bucket_type IS RECORD (
  shared_count INTEGER := 0, 
  write_count INTEGER := 0, 
  exclusive_count INTEGER := 0);
 
-- This type defines a map of: id -> lock_state_bucket 
TYPE lock_state_bucket_list_type IS 
  TABLE OF lock_state_bucket_type INDEX BY BINARY_INTEGER;
 
-- This is a package variable containing the map of all object ids to their 
-- state buckets 
lock_state_bucket_list lock_state_bucket_list_type;


-----------------------------------------------------------------------------
-- Internal functions and procedures
-----------------------------------------------------------------------------

FUNCTION GET_CURRENT_LOCK_STATE(
id                 IN INTEGER)
RETURN INTEGER IS
current_state lock_state_bucket_type;
BEGIN
  IF lock_state_bucket_list.EXISTS(id) THEN
    current_state := lock_state_bucket_list(id);
  ELSE
    RETURN NONE_MODE;    
  END IF;
  IF current_state.exclusive_count > 0 THEN
    RETURN EXCLUSIVE_MODE;
  ELSIF current_state.write_count > 0 THEN
    RETURN WRITE_MODE;
  ELSIF current_state.shared_count > 0 THEN
    RETURN SHARED_MODE;
  ELSE
    RETURN NONE_MODE;
  END IF;  
END GET_CURRENT_LOCK_STATE;

FUNCTION GET_SHARED_LOCK_COUNT(
id                 IN INTEGER)
RETURN INTEGER IS
current_state lock_state_bucket_type;
BEGIN
  IF lock_state_bucket_list.EXISTS(id) THEN
    current_state := lock_state_bucket_list(id);
    RETURN current_state.shared_count;
  ELSE
    RETURN 0;    
  END IF;
END GET_SHARED_LOCK_COUNT;

FUNCTION GET_WRITE_LOCK_COUNT(
id                 IN INTEGER)
RETURN INTEGER IS
current_state lock_state_bucket_type;
BEGIN
  IF lock_state_bucket_list.EXISTS(id) THEN
    current_state := lock_state_bucket_list(id);
    RETURN current_state.write_count;
  ELSE
    RETURN 0;    
  END IF; 
END GET_WRITE_LOCK_COUNT;

FUNCTION GET_EXCLUSIVE_LOCK_COUNT(
id                 IN INTEGER)
RETURN INTEGER IS
current_state lock_state_bucket_type;
BEGIN
  IF lock_state_bucket_list.EXISTS(id) THEN
    current_state := lock_state_bucket_list(id);
    RETURN current_state.exclusive_count;
  ELSE
    RETURN 0;    
  END IF; 
END GET_EXCLUSIVE_LOCK_COUNT;

PROCEDURE UPGRADE_LOCK_STATE(
id                 IN INTEGER,
lock_mode          IN INTEGER) IS
current_state lock_state_bucket_type;
BEGIN
  IF lock_state_bucket_list.EXISTS(id) THEN
    current_state := lock_state_bucket_list(id);
  END IF;
  IF lock_mode = EXCLUSIVE_MODE THEN
    current_state.exclusive_count := current_state.exclusive_count + 1;
  ELSIF lock_mode = WRITE_MODE THEN
    current_state.write_count := current_state.write_count + 1;
  ELSIF lock_mode = SHARED_MODE THEN
    current_state.shared_count := current_state.shared_count + 1;
  ELSE
    raise_application_error(-20061,
      'Invalid locking mode passed to function OWB_LOCK.UPGRADE_LOCK_STATE');
  END IF;
  lock_state_bucket_list(id) := current_state;
END UPGRADE_LOCK_STATE;

PROCEDURE DOWNGRADE_LOCK_STATE(
id                 IN INTEGER,
lock_mode          IN INTEGER) IS
current_state lock_state_bucket_type;
BEGIN
  IF lock_state_bucket_list.EXISTS(id) THEN
    current_state := lock_state_bucket_list(id);
  ELSE
    RETURN;   -- or throw some exception?
  END IF;
  IF lock_mode = EXCLUSIVE_MODE AND current_state.exclusive_count > 0 THEN
    current_state.exclusive_count := current_state.exclusive_count - 1;
  ELSIF lock_mode = WRITE_MODE AND current_state.write_count > 0 THEN
    current_state.write_count := current_state.write_count - 1;
  ELSIF lock_mode = SHARED_MODE AND current_state.shared_count > 0 THEN
    current_state.shared_count := current_state.shared_count - 1;
  ELSE
    raise_application_error(-20062,
      'Invalid locking mode passed to function OWB_LOCK.DOWNGRADE_LOCK_STATE');
  END IF;
  IF current_state.exclusive_count > 0 OR 
     current_state.write_count > 0 OR 
     current_state.shared_count > 0 THEN
    lock_state_bucket_list(id) := current_state;
  ELSE
    lock_state_bucket_list.DELETE(id);
  END IF;  
END DOWNGRADE_LOCK_STATE;

----------------------------------------------------------------------------

function REQUEST(
   lockhandle         IN  VARCHAR2,
   lockmode           IN  INTEGER DEFAULT DBMS_LOCK.X_MODE,
   timeout            IN  INTEGER DEFAULT DBMS_LOCK.MAXWAIT,
   release_on_commit  IN  BOOLEAN DEFAULT FALSE)
  RETURN INTEGER 
IS
request_result INTEGER;
BEGIN
  request_result := DBMS_LOCK.REQUEST(lockhandle, lockmode, timeout, release_on_commit);
  
  if request_result = 4
  then
    request_result := DBMS_LOCK.CONVERT(lockhandle, lockmode, timeout);
    
    if request_result != 0
    then
      request_result := DBMS_LOCK.RELEASE(lockhandle);
      request_result := DBMS_LOCK.REQUEST(lockhandle, lockmode, timeout, release_on_commit);
    end if;
  end if;

  return request_result;
END REQUEST;

----------------------------------------------------------------------------

FUNCTION GET_FD_LOCKNAME(
workspace_id       IN INTEGER,
object_id          IN INTEGER)
RETURN VARCHAR2 IS
BEGIN
  RETURN 'OWB$FD$' || workspace_id || '$' || object_id;
END GET_FD_LOCKNAME;

FUNCTION GET_WR_LOCKNAME(
workspace_id       IN INTEGER,
object_id          IN INTEGER)
RETURN VARCHAR2 IS
BEGIN
  RETURN 'OWB$WR$' || workspace_id || '$' || object_id;
END GET_WR_LOCKNAME;

-----------------------------------------------------------------------------

FUNCTION LOCK_SHARED (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER) 
RETURN BOOLEAN IS
fd_lockhandle VARCHAR2(128);
request_result INTEGER;
current_mode INTEGER;
BEGIN
  current_mode := GET_CURRENT_LOCK_STATE(object_id);
  IF current_mode = NONE_MODE THEN
    ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
    request_result := REQUEST(fd_lockhandle, SHARED_MODE, 0, FALSE);
    IF request_result = 0 THEN
      UPGRADE_LOCK_STATE(object_id, SHARED_MODE);
      RETURN TRUE;
    ELSE 
      RETURN FALSE;
    END IF;  
  ELSE
    UPGRADE_LOCK_STATE(object_id, SHARED_MODE);
    RETURN TRUE;
  END IF;       

END LOCK_SHARED;

FUNCTION LOCK_EXCLUSIVE (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER) 
RETURN BOOLEAN IS
fd_lockhandle VARCHAR2(128);
request_result INTEGER;
current_mode INTEGER;
BEGIN
  current_mode := GET_CURRENT_LOCK_STATE(object_id);
  IF current_mode = EXCLUSIVE_MODE THEN      
    UPGRADE_LOCK_STATE(object_id, EXCLUSIVE_MODE);
    RETURN TRUE;
  ELSE  
    ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
    IF current_mode = NONE_MODE THEN
      request_result := REQUEST(
        fd_lockhandle, EXCLUSIVE_MODE, 0, FALSE);
    ELSE  
      request_result := DBMS_LOCK.CONVERT(
        fd_lockhandle, EXCLUSIVE_MODE, 0);
    END IF;
    IF request_result = 0 THEN
      UPGRADE_LOCK_STATE(object_id, EXCLUSIVE_MODE);
      RETURN TRUE;
    ELSE 
      RETURN FALSE;
    END IF;
  END IF;
END LOCK_EXCLUSIVE;

FUNCTION LOCK_WRITE (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER) 
RETURN BOOLEAN IS
fd_lockhandle VARCHAR2(128);
wr_lockhandle VARCHAR2(128);
request_result INTEGER;
release_result INTEGER;
current_mode INTEGER;
BEGIN
  current_mode := GET_CURRENT_LOCK_STATE(object_id);
  IF current_mode = WRITE_MODE THEN
    UPGRADE_LOCK_STATE(object_id, WRITE_MODE);
    RETURN TRUE;
  ELSIF current_mode = NONE_MODE THEN
    ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
    request_result := REQUEST(
      fd_lockhandle, SHARED_MODE, 0, FALSE);
    IF request_result != 0 THEN
      RETURN FALSE;
    END IF;  
    ALLOCATE_UNIQUE(GET_WR_LOCKNAME(workspace_id, object_id), wr_lockhandle);
    request_result := REQUEST(
      wr_lockhandle, EXCLUSIVE_MODE, 0, FALSE);
    IF request_result !=0 THEN
      release_result := DBMS_LOCK.RELEASE(fd_lockhandle);
      IF release_result != 0 THEN
        raise_application_error(-20056,
          'Could not release the lock ' || GET_FD_LOCKNAME(workspace_id, object_id));        
      END IF;
      RETURN FALSE;
    END IF;
    UPGRADE_LOCK_STATE(object_id, WRITE_MODE);
    RETURN TRUE;
  ELSIF current_mode = SHARED_MODE THEN
    ALLOCATE_UNIQUE(GET_WR_LOCKNAME(workspace_id, object_id), wr_lockhandle);
    request_result := REQUEST(
      wr_lockhandle, EXCLUSIVE_MODE, 0, FALSE);
    IF request_result =0 THEN
      UPGRADE_LOCK_STATE(object_id, WRITE_MODE);
      RETURN TRUE;
    ELSE
      RETURN FALSE;
    END IF;
  ELSIF current_mode = EXCLUSIVE_MODE THEN
    IF GET_WRITE_LOCK_COUNT(object_id) > 0 THEN
      UPGRADE_LOCK_STATE(object_id, WRITE_MODE);
      RETURN TRUE;          
    ELSE
      ALLOCATE_UNIQUE(GET_WR_LOCKNAME(workspace_id, object_id), wr_lockhandle);
      request_result := REQUEST(
        wr_lockhandle, EXCLUSIVE_MODE, 0, FALSE);
      IF request_result = 0 THEN
        UPGRADE_LOCK_STATE(object_id, WRITE_MODE);
        RETURN TRUE;
      ELSE
        -- should not happen since we already have the exclusive lock
        raise_application_error(-20054,
          'Could not obtain lock ' || GET_WR_LOCKNAME(workspace_id, object_id) || 
          ' in exclusive mode. Possible locks name conflict.');
      END IF;        
    END IF;
  END IF; 
END LOCK_WRITE;

FUNCTION LOCK_OBJECT (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER,
lock_mode          IN  INTEGER) 
RETURN BOOLEAN IS
BEGIN
  CASE lock_mode
    WHEN SHARED_MODE THEN
      RETURN LOCK_SHARED(workspace_id, object_id);
    WHEN EXCLUSIVE_MODE THEN
      RETURN LOCK_EXCLUSIVE(workspace_id, object_id);    
    WHEN WRITE_MODE THEN
      RETURN LOCK_WRITE(workspace_id, object_id);   
    ELSE
    raise_application_error(-20053,
      'Invalid locking mode passed to function OWB_LOCK.LOCK_OBJECT');
  END CASE;      
  
END LOCK_OBJECT;

----------------------------------------------------------------------------

PROCEDURE UNLOCK_SHARED (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER) 
IS
fd_lockhandle VARCHAR2(128);
BEGIN
  IF GET_SHARED_LOCK_COUNT(object_id) > 0 THEN
    DOWNGRADE_LOCK_STATE(object_id, SHARED_MODE);
    IF GET_CURRENT_LOCK_STATE(object_id) = NONE_MODE THEN
      ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
      IF DBMS_LOCK.RELEASE(fd_lockhandle) != 0 THEN
        raise_application_error(-20056,
          'Could not release the lock ' || GET_FD_LOCKNAME(workspace_id, object_id));
      END IF;
    END IF;
  ELSE 
    -- we don't have the shared lock, throw exception 
    raise_application_error(-20057,
      'Could not unlock object with id ' || object_id || 
      '. You do not have the SHARED lock on this object.');
  END IF;
 
END UNLOCK_SHARED;

PROCEDURE UNLOCK_EXCLUSIVE (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER) 
IS
fd_lockhandle VARCHAR2(128);
current_mode INTEGER;
BEGIN
  IF GET_EXCLUSIVE_LOCK_COUNT(object_id) > 0 THEN
    DOWNGRADE_LOCK_STATE(object_id, EXCLUSIVE_MODE);
    current_mode := GET_CURRENT_LOCK_STATE(object_id);
    IF current_mode = NONE_MODE THEN
      ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
      IF DBMS_LOCK.RELEASE(fd_lockhandle) != 0 THEN
        raise_application_error(-20056,
          'Could not release the lock ' || GET_FD_LOCKNAME(workspace_id, object_id));
      END IF;
    ELSIF current_mode = SHARED_MODE OR current_mode = WRITE_MODE THEN
      ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
      IF DBMS_LOCK.CONVERT(fd_lockhandle, SHARED_MODE, 0) != 0 THEN
        raise_application_error(-20058,
          'Could not convert the lock ' || GET_FD_LOCKNAME(workspace_id, object_id) || 
          'into SHARED mode.');
      END IF;      
    END IF;
  ELSE
    -- we don't have the exclusive lock, throw exception
    raise_application_error(-20059,
      'Could not unlock object with id ' || object_id || 
      '. You do not have the EXCLUSIVE lock on this object.');
  END IF;  
END UNLOCK_EXCLUSIVE;

PROCEDURE UNLOCK_WRITE (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER) 
IS
fd_lockhandle VARCHAR2(128);
wr_lockhandle VARCHAR2(128);
BEGIN
  IF GET_WRITE_LOCK_COUNT(object_id) > 0 THEN
    DOWNGRADE_LOCK_STATE(object_id, WRITE_MODE);
    IF GET_WRITE_LOCK_COUNT(object_id) <= 0 THEN
      ALLOCATE_UNIQUE(GET_WR_LOCKNAME(workspace_id, object_id), wr_lockhandle);
      IF DBMS_LOCK.RELEASE(wr_lockhandle) != 0 THEN
        raise_application_error(-20056,
          'Could not release the lock ' || GET_WR_LOCKNAME(workspace_id, object_id));
      END IF;
      IF GET_CURRENT_LOCK_STATE(object_id) = NONE_MODE THEN
        ALLOCATE_UNIQUE(GET_FD_LOCKNAME(workspace_id, object_id), fd_lockhandle);
        IF DBMS_LOCK.RELEASE(fd_lockhandle) != 0 THEN
          raise_application_error(-20056,
            'Could not release the lock ' || GET_FD_LOCKNAME(workspace_id, object_id));
        END IF;   
      END IF;
    END IF;  
  ELSE
    raise_application_error(-20060,
      'Could not unlock object with id ' || object_id || 
      '. You do not have the WRITE lock on this object.');
  END IF;  
END UNLOCK_WRITE;

PROCEDURE UNLOCK_OBJECT (
workspace_id       IN  INTEGER,
object_id          IN  INTEGER,
lock_mode          IN  INTEGER) 
IS
BEGIN
  CASE lock_mode
    WHEN SHARED_MODE THEN
      UNLOCK_SHARED(workspace_id, object_id);
    WHEN EXCLUSIVE_MODE THEN
      UNLOCK_EXCLUSIVE(workspace_id, object_id);
    WHEN WRITE_MODE THEN
      UNLOCK_WRITE(workspace_id, object_id);
    ELSE
      raise_application_error(-20055,
        'Invalid locking mode passed to function OWB_LOCK.UNLOCK_OBJECT');      
  END CASE;      

END UNLOCK_OBJECT;


-----------------------------------------------------------------------------
-- Public functions and procedures
-----------------------------------------------------------------------------

-- Procedure ALLOCATE_UNIQUE is a wrapper around DBMS_LOCK.ALLOCATE_UNIQUE, 
-- but it executes it in an autonomous transaction. The parameters are the 
-- same as for DBMS_LOCK.ALLOCATE_UNIQUE.

PROCEDURE ALLOCATE_UNIQUE (
lockname         IN  VARCHAR2,
lockhandle       OUT VARCHAR2,
expiration_secs  IN  INTEGER DEFAULT 864000) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE(lockname, lockhandle, expiration_secs);
END ALLOCATE_UNIQUE;


-- Function LOCK_WORKSPACE can be used to request a lock for a workspace, 
-- with a given mode. 
-- Input parameters:
--   workspace_id - the id (elementid) of the workspace to be locked
--   lock_mode    - the mode the lock is requested. Valid modes are: 
--                  SHARED_MODE, WRITE_MODE and EXCLUSIVE_MODE.
-- Return value:
--   true         - if the lock was obtained
--   false        - otherwise

FUNCTION LOCK_WORKSPACE (
workspace_id       IN  INTEGER,
lock_mode          IN  INTEGER) 
RETURN BOOLEAN IS
BEGIN
  RETURN LOCK_OBJECT(workspace_id, workspace_id, lock_mode);
END LOCK_WORKSPACE;


-- Procedure UNLOCK_WORKSPACE can be used to release a lock for a workspace, 
-- you already hold with a given mode. If you don't hold that lock, the 
-- procedure will raise an exception.
-- Input parameters:
--   workspace_id - the id (elementid) of the workspace to be unlocked
--   lock_mode    - the mode the lock was obtained. Valid modes are: 
--                  SHARED_MODE, WRITE_MODE and EXCLUSIVE_MODE.

PROCEDURE UNLOCK_WORKSPACE (
workspace_id       IN  INTEGER,
lock_mode          IN  INTEGER) 
IS
BEGIN
  UNLOCK_OBJECT(workspace_id, workspace_id, lock_mode);
END UNLOCK_WORKSPACE;


-- Function LOCK_USER can be used to request a lock for a user, 
-- with a given mode. 
-- Input parameters:
--   workspace_id - the id (elementid) of the workspace containing the user.
--   user_id      - the id (elementid) of the user to be locked.
--   lock_mode    - the mode the lock is requested. Valid modes are: 
--                  WRITE_MODE and EXCLUSIVE_MODE, and they both behave the 
--                  same.
-- Return value:
--   true         - if the lock was obtained
--   false        - otherwise

FUNCTION LOCK_USER (
workspace_id       IN  INTEGER,
user_id            IN  INTEGER,
lock_mode          IN  INTEGER) 
RETURN BOOLEAN IS
wksp_result BOOLEAN;
user_result BOOLEAN;
BEGIN
  IF lock_mode = WRITE_MODE OR lock_mode = EXCLUSIVE_MODE THEN
    -- first get the SHARED lock on the workspace
    wksp_result := LOCK_OBJECT(workspace_id, workspace_id, SHARED_MODE);
    IF NOT wksp_result THEN
      RETURN FALSE;
    END IF;
    -- then the lock on the user
    user_result := LOCK_OBJECT(workspace_id, user_id, WRITE_MODE); 
    IF NOT user_result THEN
      UNLOCK_OBJECT(workspace_id, workspace_id, SHARED_MODE);    
    END IF;  
    RETURN user_result;
  ELSE
    raise_application_error(-20051,
      'Invalid locking mode passed to function OWB_LOCK.LOCK_USER');
  END IF;  
END LOCK_USER;


-- Procedure UNLOCK_USER can be used to release a lock for a user, 
-- you already hold with a given mode. If you don't hold that lock, the 
-- procedure will raise an exception.
-- Input parameters:
--   workspace_id - the id (elementid) of the workspace containing the user.
--   user_id      - the id (elementid) of the user to be unlocked.
--   lock_mode    - the mode the lock was obtained. Valid modes are: 
--                  WRITE_MODE and EXCLUSIVE_MODE.

PROCEDURE UNLOCK_USER (
workspace_id       IN  INTEGER,
user_id            IN  INTEGER,
lock_mode          IN  INTEGER) 
IS
BEGIN
  IF lock_mode = WRITE_MODE OR lock_mode = EXCLUSIVE_MODE THEN
    UNLOCK_OBJECT(workspace_id, user_id, WRITE_MODE);
    UNLOCK_OBJECT(workspace_id, workspace_id, SHARED_MODE);
  ELSE
    raise_application_error(-20052,
      'Invalid locking mode passed to function OWB_LOCK.UNLOCK_USER');
  END IF;  
END UNLOCK_USER;


END OWB_LOCK;