RelatedResource.java

/*
** Module   : RelatedResource.java
** Abstract : base class for concurrency primitives that are reference counted and thread safe
**
** Copyright (c) 2020, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 GES 20200116 First version.
*/

/*
** 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.concurrent;

import java.util.*;
import java.util.function.*;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager; // must be explicit, conflicts with Java SM
import com.goldencode.util.*;
import com.goldencode.p2j.util.*;

/**
 * Implements the base class for a named, reference counted concurrency primitive that can be
 * naturally shared across "related" threads.  The core logic for managing create/open and
 * close/destroy is implemented here.  This class is abstract since it does not represent any
 * real resource type, but rather just provides shared logic.
 * <p>
 * All access to instances of these resources MUST only be through static factory methods. This
 * means that each subclass should have a private constructor and should use the {@code create}
 * and {@code open} methods in this class to manage the access.   {@code create} is used to
 * instantiate a new resource of a given name.  Only one resource of that name is possible at
 * any given time.  When a {@code create} has occurred for a given name (once and only once),
 * any other thread needing access must use {@code open} to obtain the instance.  <b>It is very
 * important that these APIs are used for access. DO NOT pass references to these resources to
 * other threads. Doing so will damage the reference counted resource management.</b>  Calling
 * {@code create} for a named resource that already exists will cause an error.
 * <p>
 * The end of a thread's use of a resource is managed using the {@code close} or {@code destroy}
 * methods.  {@code close} is used to decrement the reference count and allow the resource to
 * do some cleanup.  When the last reference uses {@code close}, an implicit {@code destroy}
 * occurs.  At that point, no further usage of the resource is possible and any access will
 * generate an error.  One may also use {@code destroy} directly, but doing so while other
 * threads are accessing the resource will typically release all waiting threads and cause
 * errors for all subsequent access.  It is OK to call {@code create} on a name that has been
 * destroyed.
 * <p>
 * Each subclass must have a unique resource type name. This is used to namespace all resource
 * instances of a given type.  The names are in a format "/resource_type/instance_name" where
 * the instance name must be unique for all instances of the same resource type.  Generally, the
 * following rules are enforced:
 * <ul>
 *    <li> Resource type names must be unique for each subclass.  They must be non-null and
 *         non-empty.  
 *    <li> Instance names must be non-null and non-empty.
 *    <li> Slash (/) characters and whitespace may not be included in either resource type names
 *         or in instance names. 
 * </ul> 
 * There are two modes of usage for these resources.  In local mode the resource may only be used
 * by "related" threads.  A related thread is one that shares the same security account, has its
 * own context (including transaction state) and was started using {@code RUN AS-THREAD}. This
 * mode allows all related threads full access to all operations, without any security manager
 * checks.  In contrast, non-local mode is a resource that can be used by any thread in the same
 * JVM.  These threads can be related threads but they do not need to be related.  In non-local
 * (i.e. cross-session) mode, all access is checked by the security manager to enforce security
 * permissions as defined in the directory.  The {@code ConcurrentResource} security plugin
 * implements these security checks and even related threads will have these access checks
 * enforced.  This non-local mode allows for an arbitrarily complex inter-thread communication
 * pattern to be implemented (across users, batch processes, appserver agents, REST agents...)
 * but at the cost of additional configuration and the runtime security checks.  If one can
 * implement in local mode it is much simpler so this is often preferred where possible.
 * <p>
 * For non-local resources, the rights are represented by a small set of flags: 
 * <ul>
 *   <li> create - the subject may instantiate this resource (e.g. call {@code create})
 *   <li> delete - the subject may delete this resource (e.g. call {@code destroy})
 *   <li> read - the state or data associated with an existing runtime instance can be accessed
 *        by this subject
 *   <li> write - the state or data associated with an existing runtime instance can be modified
 *        by this subject
 *   <li> denied - no access is allowed (overrides any other access flags)
 * </ul>
 */
public abstract class RelatedResource
implements BitFlagsConstants
{
   /** Cross-session (JVM-wide) registry of resources. */
   private static final HashMap shared = new HashMap<String, RelatedResource>();
   
   /** Hidden key used for storing/retrieving values between related threads. */
   private static final Object key = new Object();
   
   /** SecurityManager singleton instance. */
   private static final SecurityManager sm = SecurityManager.getInstance();
   
   /** Track how many threads are referencing this resource. */
   private long references = 1;
   
   /** Local or non-local resource. */
   private boolean local;
   
   /** Flag to identify a destroyed resource. */
   protected boolean active = true;
   
   /** The name of this resource. */
   protected final String name;
   
   /** Used to lock access to this instance. */
   protected final Object lock = new Object();
   
   /**
    * Constructor.
    *
    * @param    name
    *           Resource name, must not be {@code null}.
    */
   protected RelatedResource(String name)
   {
      this.name = name;
   }
   
   /**
    * Factory method which creates a new instance with the given name.
    * <p>
    * In non-local mode, this requires CREATE access.
    *
    * @param    prefix
    *           The non-null, non-empty resource type name which is a constant for all resources
    *           of the same type.  It MUST NOT contain any "/" character.
    * @param    iname
    *           The instance name, excluding the leading "/resource_type/" prefix.  It MUST NOT
    *           contain any "/" character or whitespace.
    * @param    creator
    *           Code for delegated creation of the resource.
    * @param    local
    *           {@code true} to create a local instance (one that is only accessible between
    *           threads in the same security context.  {@code false} creates an instance that
    *           is accessible from any session in the JVM.
    *
    * @return   The instance if it was created.
    *
    * @throws   ErrorConditionException
    *           If there was any problem in creation.
    */
   protected static RelatedResource create(String                                      prefix,
                                           String                                      iname,
                                           Function<String, ? extends RelatedResource> creator,
                                           boolean                                     local)
   throws ErrorConditionException
   {
      String name = RelatedResource.formatName(prefix, iname);
      
      // this will throw an error if bad
      checkName(prefix, name);
      
      // non-local needs C access right, local is always allowed as long as the name is valid
      if (!local && !checkAccess(name, BIT_CREATE_ACCESS))
      {
         throw new ErrorConditionException("CREATE access NOT allowed for non-local resource '" +
                                           name + "'.");
      }
      
      RelatedResource instance = null;
      
      synchronized (key)
      {
         HashMap<String, RelatedResource> registry = obtain(local);
         
         // confirm it does not already exist
         if (registry.containsKey(name))
         {
            throw new ErrorConditionException((local ? "Local" : "Non-local") + " resource '" +
                                              name + "' already exists.");
         }
         
         // create new instance with the correct state; since it cannot be referenced (yet) by
         // any other thread, we can change the state without locking explicitly
         instance = creator.apply(name);
         instance.local = local;
         
         // store it
         registry.put(name, instance);
      }
      
      return instance;
   }
   
   /**
    * Opens an existing instance with the given name.
    * <p>
    * In non-local mode, this requires READ access.
    *
    * @param    prefix
    *           The non-null, non-empty resource type name which is a constant for all resources
    *           of the same type.  It MUST NOT contain any "/" character.
    * @param    iname
    *           The instance name, excluding the leading "/resource_type/" prefix.  It MUST NOT
    *           contain any "/" character or whitespace.
    * @param    local
    *           {@code true} to open a local instance (one that is only accessible between
    *           threads in the same security context.  {@code false} opens an instance that
    *           is accessible from any session in the JVM.
    *
    * @return   The instance if it exists and was opened.
    *
    * @throws   ErrorConditionException
    *           If there was any problem in opening.
    */
   protected static RelatedResource open(String prefix, String iname, boolean local)
   throws ErrorConditionException
   {
      String name = RelatedResource.formatName(prefix, iname);
      
      // this will throw an error if bad
      checkName(prefix, name);
      
      // non-local needs R access right, local is always allowed as long as the name is valid
      if (!local && !checkAccess(name, BIT_READ_ACCESS))
      {
         throw new ErrorConditionException("READ access NOT allowed for non-local resource '" +
                                           name + "'.");
      }
      
      RelatedResource instance = null;
      
      synchronized (key)
      {
         HashMap<String, RelatedResource> registry = obtain(local);
         
         instance = registry.get(name);
         
         // confirm it exists
         if (instance == null)
         {
            throw new ErrorConditionException((local ? "Local" : "Non-local") + " resource '" +
                                              name + "' DOES NOT exist.");
         }
      
         // increment the reference count
         instance.increment();
      }
      
      return instance;
   }
   
   /**
    * Raise an error if the resource is non-local AND the current context does not have READ
    * access to this resource name.
    *
    * @throws   ErrorConditionException
    *           If non-local and access is denied.
    */
   public void checkRead()
   throws ErrorConditionException
   {
      checkWorker(BIT_READ_ACCESS, "READ");
   }
   
   /**
    * Raise an error if the resource is non-local AND the current context does not have WRITE
    * access to this resource name.
    *
    * @throws   ErrorConditionException
    *           If non-local and access is denied.
    */
   public void checkWrite()
   throws ErrorConditionException
   {
      checkWorker(BIT_WRITE_ACCESS, "WRITE");
   }
   
   /**
    * Raise an error if the resource is non-local AND the current context does not have DELETE
    * access to this resource name.
    *
    * @throws   ErrorConditionException
    *           If non-local and access is denied.
    */
   public void checkDelete()
   throws ErrorConditionException
   {
      checkWorker(BIT_DELETE_ACCESS, "DELETE");
   }
   
   /**
    * Release all waiting threads and delete the resource.  Any waiting threads will see the
    * {@code WaitStatus.DESTROYED}.
    * <p>
    * In non-local mode, this requires DELETE access.
    *
    * @throws   ErrorConditionException
    *           If there was any problem in destroying the resource.
    */
   public void destroy()
   throws ErrorConditionException
   {
      errorIfDead();
      checkDelete();
      
      // we know that we are going to modify the registry, so we need to exclude others
      synchronized (key)
      {
         synchronized (lock)
         {
            references = 0;
            cleanup();
            
            release();
         }
      }
   }
   
   /**
    * Close the current thread's access to this resource.  This will decrement the reference
    * count.  If this is the last thread referencing the resource, then the resource will be
    * destroyed and no further usage will be possible without re-creating the resource.
    * <p>
    * In non-local mode, this requires READ access.
    *
    * @throws   ErrorConditionException
    *           If there was any problem in closing the resource.
    */
   public void close()
   throws ErrorConditionException
   {
      errorIfDead();
      checkRead();
      
      // we MAY need to modify the registry, thus we must exclude access
      synchronized (key)
      {
         synchronized (lock)
         {
            if (!decrement())
            {
               cleanup();
            }
         }
      }
   }
   
   /**
    * Reports on the status of the resource.
    * <p>
    * In non-local mode, this requires READ access.
    *
    * @return   {@code WaitStatus.OK} or {@code WaitStatus.DESTROYED}.
    */
   public WaitStatus status()
   {
      // don't error on dead, one should be able to safely check status anytime
      checkRead();
      
      WaitStatus status = WaitStatus.OK;
      
      synchronized (lock)
      {
         if (!active)
         {
            status = WaitStatus.DESTROYED;
         }
      }
      
      return status;
   }
   
   /**
    * Obtain the resource name (which makes up the prefix for valid names of this resource type).
    *
    * @return   The non-null, non-empty resource type without spaces or '/' characters. This
    *           must be the same for all resources of this type.
    */
   public abstract String resourceName();
   
   /**
    * Check with the security manager to see if the current context has the listed access to the
    * named resource.  If the ConcurrentResource security plugin is not registered, all access is
    * DISALLOWED.
    *
    * @param    name
    *           Resource name to check.
    * @param    perms
    *           The access type requested (see {@code BitFlagsConstants}.
    *
    * @return   {@code true} if the access is allowed.
    */
   protected static boolean checkAccess(String name, int perms)
   {
      ConcurrentResource ccr = (ConcurrentResource) sm.getPluginInstance("ConcurrentResource");
      
      return (ccr != null && ccr.accessWorker(name, perms));
   }
   
   /**
    * Confirm that the inputs are non-null and non-empty, then format as a properly namespaced
    * resource name (in /resource_name/instance_name format).
    *
    * @param    prefix
    *           The non-null, non-empty resource type name which is a constant for all resources
    *           of the same type.  It MUST NOT contain any "/" character.
    * @param    instance
    *           The instance name, excluding the leading "/resource_type/" prefix.  It MUST NOT
    *           contain any "/" character or whitespace.
    *
    * @return   The properly namespaced name.
    *
    * @throws   ErrorConditionException
    *           If the inputs are invalid.
    */
   protected static String formatName(String prefix, String instance)
   throws ErrorConditionException
   {
      if (prefix == null || prefix.length() == 0)
      {
         throw new ErrorConditionException("Resource name cannot be null or empty.");
      }
      
      if (instance == null || instance.length() == 0)
      {
         throw new ErrorConditionException("Instance name cannot be null or empty.");
      }
      
      return "/" + prefix + "/" + instance;
   }
   
   /**
    * Check the name raise an error if it is not valid.  The proper format is
    * "/resource_type_name/instance_name".  The instance name portion MUST NOT contain any "/"
    * character or whitespace.
    *
    * @param    prefix
    *           The non-null, non-empty resource type name which is a constant for all resources
    *           of the same type.  It MUST NOT contain any "/" character.
    * @param    name
    *           The fully formatted name to check.
    *
    * @throws   ErrorConditionException
    *           If the inputs are invalid.
    */
   protected static void checkName(String prefix, String name)
   throws ErrorConditionException
   {
      if (name == null || name.length() == 0)
      {
         throw new ErrorConditionException("Names cannot be null or empty.");
      }
      
      String[] parts = name.split("/");
      
      // if name is valid input with format "/resource_type_name/instance_name", the result
      // will be an array with 3 elements:
      // ""
      // "resource_type_name"
      // "instance_name"
      
      if (parts.length != 3)
      {
         throw new ErrorConditionException("Resource name must have 2 segments (" +
                                           "/resource_name/instance_name).");
      }
      
      if (parts[0].length() != 0)
      {
         throw new ErrorConditionException("Resource name must start with slash " +
                                           "instead of '" + parts[0] + "'.");
      }
      
      if (parts[1].length() == 0 || !parts[1].equals(prefix))
      {
         throw new ErrorConditionException("Resource type must be '" + prefix + "' " +
                                           "instead of '" + parts[1] + "'.");
      }
      
      if (parts[2].length() == 0 || StringHelper.containsWhitespace(parts[2]))
      {
         throw new ErrorConditionException("Resource type must be non-empty and without " +
                                           "whitespace (not '" + parts[2] + "').");
      }
   }
   
   /**
    * Raise an error if the resource has already been destroyed (and thus it cannot be used).
    *
    * @throws   ErrorConditionException
    *           If the active flag is {@code false}.
    */
   protected void errorIfDead()
   throws ErrorConditionException
   {
      synchronized (lock)
      {
         if (!active)
         {
            throw new ErrorConditionException("Resource " + name + " is dead.");
         }
      }
   }
   
   /**
    * Release all waiting threads.  By default this just calls {@code lock.notifyAll()}. 
    * Override this if the blocking mechanism is NOT using the {@code lock} object
    * monitor.
    */
   protected void release()
   {
      synchronized (lock)
      {
         lock.notifyAll();
      }
   }
   
   /**
    * Access the internal storage used to map names to resources based on whether this is a local
    * or non-local request.  If this is a local resource request and there is no storage
    * allocated yet, then the storage will be allocated before this returns.
    *
    * @param    local
    *           {@code true} if this is a resource that is private to a set of related threads.
    *           {@code false} for resources that can be shared JVM-wide.
    *
    * @return   The storage.
    */
   private static HashMap<String, RelatedResource> obtain(boolean local)
   {
      HashMap<String, RelatedResource> storage = shared;
      
      if (local)
      {
         storage = (HashMap<String, RelatedResource>) RelatedThread.get(key);
         
         if (storage == null)
         {
            storage = new HashMap<String, RelatedResource>();
            
            RelatedThread.set(key, storage);
         }
      }
      
      return storage;
   }
   
   /**
    * Cleanup all resources associated with this instance and set the status to destroyed.
    *
    * @throws   ErrorConditionException
    *           If there was any problem in cleanup of the resources.
    */
   private void cleanup()
   throws ErrorConditionException
   {
      synchronized (lock)
      {
         if (references != 0)
         {
            throw new ErrorConditionException("Cannot cleanup resource that is still " +
                                              "referenced (" + Long.toString(references) +
                                              " references).");
         }
      
         synchronized (key)
         {
            // remove from the registry
            HashMap<String, RelatedResource> storage = obtain(local);
            storage.remove(name);
         }
      
         active = false;
      }
   }
   
   /**
    * Increment the reference count.
    */
   private void increment()
   {
      synchronized (lock)
      {
         references++;
      }
   }
   
   /**
    * Decrement the reference count.
    *
    * @return   {@code true} if there are still active references.
    */
   private boolean decrement()
   {
      synchronized (lock)
      {
         references--;
         
         return (references > 0);
      }
   }
   
   /**
    * Raise an error if the resource is non-local AND the current context does not have the
    * requested access to this resource name.
    *
    * @param    perms
    *           The permissions needed (see {@code BitFlagsConstants}).
    * @param    descr
    *           The permission name for the error message.
    *
    * @throws   ErrorConditionException
    *           If non-local and access is denied.
    */
   private void checkWorker(int perms, String descr)
   throws ErrorConditionException
   {
      if (!local && !checkAccess(name, perms))
      {
         throw new ErrorConditionException(descr + " access NOT allowed for non-local resource" +
                                           " '" + name + "'.");
      }
   }
}