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 + "'.");
}
}
}