Project

General

Profile

Runtime Hooks and Plug-Ins

Server Initialization and Termination Hooks

When the FWD server starts and ends, custom application code can be invoked to handle initialization, cleanup or other useful logic. One common use is to export custom APIs (see the chapter on Integrating External Applications) during the server startup hook. To accomplish this, in the startup hook, the RemoteObject.registerStaticNetworkServer() or RemoteObject.registerNetworkServer() method is called to “load" and export a set of APIs.

Any custom code that must be called at server startup or shutdown must implement the com.goldencode.p2j.main.InitTermListener interface.

The list of initialization/termination hooks is then registered in the list of server-hooks in the FWD server directory. An example server hook list follows:

<node class="strings" name="server-hooks">
   <node-attribute name="values" value="com.acme.corp.something.Hook1"/>
   <node-attribute name="values" value="com.acme.whatever.Hook2"/>
</node>

This section can appear in either /server/<server_id>/ (where <server_id> is the name of the server) or if not found there,/server/default/ is checked. It can exist in both locations but the only the server-specific path will be honored if the hooks list is there.

Each entry in the list represents the fully qualified package and class name of the hook class (without any .java or .class suffix).

When the server starts, it reads this list, instantiates an instance of each class and places that instance in an array. The referenced class must have a default constructor (a constructor that takes no parameters) so that the FWD server can instantiate it. An error will occur otherwise.

Once the server has almost completed initialization, but before any clients have been allowed to connect, each hook's InitTermListener.initialize() method is called. The calls occur on the same thread, the code has the security context of the server (usually this means unlimited permissions) and the calls occur sequentially in the order in which they were read from the directory.

When the FWD server shuts down, each hook's InitTermListener.terminate(Throwable) method will be called. If the server is exiting due to an abnormal condition, the related cause will be passed as the parameter. That parameter will be null if the server is shutting down normally. The calls occur on the same thread, the code has the security context of the server (usually this means unlimited permissions) and the calls occur sequentially in the reverse order from which they were initialized.

Consider an example. We have some user objects created on a session startup and deleted when session finishes. But because of unexpected errors these objects can remain even if they are not longer needed. For this case we can write a server hook which deletes all unnecessary user objects on server shutdown:

public class UserObjectServerHook
implements InitTermListener
{
   private static final Log LOG = LogFactory.getLog(UserObjectServerHook.class);

   public void initialize()
   {
   }

   public void terminate(Throwable t)
   {
      Database database = new Database("some_database");
      SessionFactory sessionFactory = DatabaseManager.getSessionFactory(database);
      Session session = sessionFactory.openSession();
      Transaction tr = null;

      try
      {
         // delete all user objects
         tr = session.beginTransaction();
         Connection connection = session.connection();
         Statement statement = connection.createStatement();
         statement.execute("delete from user_object");
         tr.commit();
      }
      catch(Throwable thr)
      {
         if (tr != null)
         {
            try
            {
               tr.rollback();
            }
            catch(HibernateException exc)
            {
               LOG.error(exc);
            }
         }

         LOG.error(thr);
      }
      finally
      {
         session.close();
      }
   }
}

Note that you cannot create a record in a server hook because IdentityManager cannot return a primary key outside of a session. This problem will be addressed in a future FWD runtime release.

Session Initialization and Termination Hooks

When a session starts and ends, custom application code can be invoked to handle session initialization, cleanup or other useful logic.

Any custom code that must be called at session initialization or termination must implement the com.goldencode.p2j.main.InitTermListener interface.

You can define a single session initialization/termination hook using the FWD server directory. An example follows:

<node class="string" name="context-hook">
   <node-attribute name="value" value="com.acme.corp.something.SomeHook"/>
</node>

This section can appear under the following paths (in the order of preference):

/server/<server_id>/runtime/<account_or_group>/

/server/<server_id>/runtime/default/

/server/default/runtime/<account_or_group>/

/server/default/runtime/default/

<server_id> is the name of the server. <account_or_group> is a user name or a process name (if a batch process is running). If an appropriate path cannot be found by user/process name then a search over groups to which user/process belongs to is performed.

The node-attribute value represents the fully qualified package and class name of the hook class (without any .java or .class suffix).

When business logic in a session is about to be started, an instance of the hook class is initiated. The referenced class must have a default constructor (a constructor that takes no parameters) so that the FWD server can instantiate it. An error will occur otherwise.

Once the hook has been instantiated, the InitTermListener.initialize() method is called. The calls occur on the same thread, the code has the security context of the corresponding user/process.

When business logic finishes its work, hook's InitTermListener.terminate(Throwable) method is called. If it was finished due to an abnormal condition, the related cause will be passed as the parameter. That parameter will be null if the session has finished normally. The calls occur on the same thread, the code has the security context of the corresponding user/process.

Consider an example. We need to create an user object each time a session is started and delete that object when session finishes. It can be implemented using a session hook:

public class UserObjectContextHook
implements InitTermListener
{
   private static final Log LOG = LogFactory.getLog(UserObjectContextHook.class);

   private static final Database DATABASE = new Database("some_database");

   private static final String TABLE_NAME = "user_object";

   // context-specific storage for the user object ids
   private static final ContextLocal<UserObjectContext> context =
   new ContextLocal<UserObjectContext>()
   {
      protected UserObjectContext initialValue()
      {
         return (new UserObjectContext());
      }
   };

   public void initialize()
   {
      Persistence persistence = PersistenceFactory.getInstance(DATABASE);
      SessionFactory sessionFactory = DatabaseManager.getSessionFactory(DATABASE);
      Session session = sessionFactory.openSession();
      RecordIdentifier ident = null;
      Transaction tr = null;

      try
      {
         // get the primary key for the new record and lock it
         Long pk = (Long) persistence.nextPrimaryKey(TABLE_NAME);
         ident = new RecordIdentifier(TABLE_NAME, pk);
         persistence.lock(LockType.EXCLUSIVE_NO_WAIT, ident, true);

         // create new user object
         tr = session.beginTransaction();
         UserObjectImpl userObject = new UserObjectImpl();
         userObject.setId(pk);
       ... // assign required properties of the user object
         session.save(userObject);
         tr.commit();

         // store id of the newly created user object
         context.get().id = pk;
      }
      catch (Throwable thr)
      {
         if (tr != null)
         {
            try
            {
               tr.rollback();
            }
            catch(HibernateException exc)
            {
               LOG.error(exc);
            }
         }

         LOG.error(thr);
      }
      finally
      {
         if (ident != null)
         {
            try
            {
               // release the lock
               persistence.lock(LockType.NONE, ident, true);
            }
            catch (LockUnavailableException exc)
            {
               // unexpected
            }
         }

         session.close();
      }

   }

   public void terminate(Throwable t)
   {
      // get the id of the created user object
      Long id = context.get().id;

      if (id != null)
      {
         Persistence persistence = PersistenceFactory.getInstance(DATABASE);
         SessionFactory sessionFactory = DatabaseManager.getSessionFactory(DATABASE);
         Session session = sessionFactory.openSession();
         RecordIdentifier ident = new RecordIdentifier(TABLE_NAME, id);
         Transaction tr = null;

         try
         {
            // load user object by id
            UserObjectImpl userObject = (UserObjectImpl) session.load(UserObjectImpl.class, id);

            if (userObject != null)
            {
               // lock the user object
               persistence.lock(LockType.EXCLUSIVE_NO_WAIT, ident, true);

               // delete the user object
               tr = session.beginTransaction();
               session.delete(userObject);
               tr.commit();
            }
         }
         catch (Throwable thr)
         {
            if (tr != null)
            {
               try
               {
                  tr.rollback();
               }
               catch(HibernateException exc)
               {
                  LOG.error(exc);
               }
            }

            LOG.error(thr);
         }
         finally
         {
            try
            {
               // release the lock
               persistence.lock(LockType.NONE, ident, true);
            }
            catch (LockUnavailableException exc)
            {
               // unexpected
            }

            session.close();
         }
      }
   }

   /**
    * Container for storing id of the created user object.
    */
   private static class UserObjectContext
   {
      private Long id = null;
   }
}

Note that you cannot create or delete a record using com.goldencode.p2j.persist.Persistence.save or com.goldencode.p2j.persist.Persistence.delete in a session hook initialize method because of runtime errors. This problem will be addressed in a future FWD runtime release. So far you can use org.hibernate.Session.save or org.hibernate.Session.delete functions (as in the examples above).

Context-Local Data

Each process or user that accesses the FWD server must first establish a security context. The security context is the state associated with the FWD session that exists from the time the process or user logged onto the FWD server until the process or user disconnects or logs off the server. Each security context is associated with a specific account in the server's database of accounts, however there can be more than one independent security context that references the same account. That means that the same account can be logged on more than once. Each logon is a separate session. There is a one to one relationship between logons, sessions and security contexts. It is valid to consider each of these things different aspects of the same thing.

The server logic that executes within a specific security context (or just context for short) will run on a thread that is bound to that security context for as long as the logic is being processed. Once that unit of work is complete, it is possible that another request in that same session will execute on a different thread, but so long as the request is within the context of the same session, the same security context will exist on the new thread. Since server code is not guaranteed to run on the same thread over time, even when the session has remained in existence, it is important to have access to a mechanism that can store and retrieve data that is specific to the context that represents that session.

Usually only one thread at a time should be executing bound to a given security context. Wherever there is a thread pool that services requests (as opposed to a dedicated thread that is always bound to one context), those threads are temporarily bound to the appropriate security context for the lifetime of a request. When the request is complete, the context is unbound from the thread and the thread is returned to the thread pool to await the next request (which may come from the same or a different context).

FWD provides the com.goldencode.p2j.security.ContextLocal class to make it easy to store and retrieve per-session data. The interface is a superset of the interface for the java.lang.ThreadLocal class. The idea is the same, but the data is not stored for each thread, but instead it is stored in the security context of the current session. The design of the FWD server with its support for thread pools and the binding/unbinding of security contexts on a per request basis is the reason that simple ThreadLocal data cannot be safely used in the FWD server.

The security context has other implications besides the storage of context-local data. For more details, see the Security Context and Threading sections of the Integrating Hand-Written Java chapter.

The simple use case for ContextLocal creates a single member of a class that must be stored/retrieved on a per-context basis. Once the context-specific instance of the member is obtained, it can be accessed or mutated as needed.

import com.goldencode.p2j.security.ContextLocal;

public class Something
{
   /** Context local instance of the whatever data member.  */
   private static final ContextLocal<Whatever> whatwhat = new ContextLocal<Whatever>()
   {
      protected Whatever initialValue()
      {
         // Other initialization can be done here too.
         return new Whatever();
      }
   };

...

   public static boolean someMethod()
   {
      Whatever what = whatwhat.get();

      // Do something with the context-specific instance of the data member.
      return what.callSomethingHere();
   }
}

The complex use case is where there is more than one data member that must be stored on a context-local basis. In this case, a special “container” class is created that is stored in/retrieved from the context. Once the context-specific instance of the container class is obtained, the data in it can be accessed or mutated as needed.

An example of the complex case:

import com.goldencode.p2j.security.ContextLocal;

public class Something
{
   /** Stores context-local state variables. */
   private static ContextContainer work = new ContextContainer();

...

   public static int getCounter()
   {
      // Use the context-local data. The data can be accessed or mutated.
      WorkArea wa = work.get();

      return wa.counter;
   }

...

   /**
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {
      private boolean flag = false;

      private int counter = 0;

      private Whatever data = new Whatever();
   }

   /**
    * Simple container that stores and returns a context-local instance of
    * the global work area.
    */
   private static class ContextContainer
   extends ContextLocal<WorkArea>
   {
      /**
       * Initializes the work area, the first time it is requested within a
       * new context.
       *
       * @return   The newly instantiated work area.
       */
      protected WorkArea initialValue()
      {
         WorkArea wa = new WorkArea();

         // Other initialization can be placed here.

         return wa;
      }
   }
}

As long as only one thread ever executes within the same context at a time, using data in this manner (either of the above use cases) is completely thread safe. That thread essentially has its own copy of the data and that copy is completely independent of the copies in every other context. However, it is possible to have simultaneous requests from custom access code (see the chapter on Integrating External Applications). In such a case, the developer of the server-side logic must handle all thread-safety issues manually. In particular, some form of concurrent usage model (locking, mutexes, Java synchronization...) must be used. Note that a session can not be used safely to concurrently execute remote APIs which rely on the FWD runtime, as the FWD runtime uses context-local variables to hold data specific to each session. See the Accessing Services section in the Integrating External Applications chapter of this book, for more details why this limitation exists.

As an improvement over ThreadLocal data, ContextLocal adds context-specific finalization. Subclasses can get a notification or callback at the end of a context's life by overriding the cleanup(T) method to perform any necessary housekeeping. The server guarantees that this method will be called when each session exits, whether the session exits normally or abnormally.

Security Resource and Rights Plug-Ins

FWD supports application-specific (custom) resources, access to which is controlled for users, groups and processes. Resources can be specific to a system or to administrative, network or other functions. You can define your own resource type that will be used in your application. You can define the custom set of permissions for this resource type. Then you can instrument the application code to call the security manager to get access decisions for this resource. These features are implemented as plug-ins for the FWD SecurityManager.

In this section we will implement the abstract “function” resource. Each named “function” controls access to the corresponding function of the application and to the specific actions that can be performed using that function. Consider if the application has an “Order Management” function which allows the user to display, create, update and delete orders. Let's name the resource “orders” and check user rights against this resource in the following way:

  1. If READ permission is set, the user is allowed to enter “Order Management” and view orders.
  2. If CREATE permission is set, the user is allowed to create new orders.
  3. If UPDATE permission is set, the user is allowed to update existing orders.
  4. If DELETE permission is set, the user is allowed to delete orders.

Implementation

Our implementation will contain three java classes:

  • AppFunctionResource represents the plug-in which is initialized during FWD server startup procedure. This class is responsible for:
    • abstract “function” resource declaration,
    • initialization and life cycle of the plugin,
    • validation of function names and access rights,
    • actual check of access rights.
  • AppFunctionRights represents a rights object for the “function” resource. This provides encoding/decoding of the permissions for the directory as well as the formatting of the permissions for display in the administration UI.
  • AppFunctionRightsEditor represents the rights editor dialog for the function rights. This dialog will allow administrators to manage the custom rights in the administration UI.
AppFunctionResource Implementation

We will cover AppFunctionResource implementation step by step. The complete listing will be provided in the end of this subsection.

package com.company.admin;

public class AppFunctionResource
extends AbstractResource
{
   ...

First, our class extends AbstractResource.

private final static String PLUGIN_TYPE_NAME = "app-function";

public String getTypeName()
{
   return PLUGIN_TYPE_NAME;
}

Plugin resource type name, in our case “app-function”, is used during plugin registration and you can get the plug-in instance by resource type name using SecurityManager.getPluginInstance function. Also, type name determines the location of ACLs for that type in server configuration directory, in our case they will be placed under /security/acl/app-function branch.

public String getRightsEditorName()
{
   return "com.company.admin.AppFunctionRightsEditor";
}

This function determines the class with will be used as the rights editor for this type of resource in the administration UI.

public Description[] describeRights()
{
   Description[] items = new Description[1];
   items[0] = new Description(AttributeType.ATTR_BITFIELD,             // type - bit field
                              false,                                   // is mandatory field
                              false,                                   // is variable-size field
                              4,                                       // field size (not used)
                              AppFunctionRights.PERMISSIONS_ATTRIBUTE, // text label
                              "bit set to ON enables access",          // description text
                              new String[]                             // bit names
                              {
                                 "Read",
                                 "Create",
                                 "Update",
                                 "Delete" 
                              },
                              new BitSet(4));   // bit mask of unused fields - no unused
   return items;
}

This function returns array of descriptions of access rights items. In our case we have only one rights item - a 4-bit sized bitfield, which is sufficient in order to keep read, create, update and delete rights. If you want to have more complicated rights, you may need to have several rights items. For example, if you want to implement Unix-like access rights, you need to have five rights items: two strings (owner and group) and a 3-bit sized bitfield (permissions for owner, group and others).

The text label specified in the description object is used while reading saved rights object from the configuration directory. Bit names (applicable to bit fields only) and description text may be used in the UI.

public Rights getRightsInstance(Object[] rights)
{
   return new AppFunctionRights((BitField)rights[0]);
}

This function returns a Rights object which corresponds to this resource type for the given access rights items.

private final Set<String> allFunctions = new HashSet<String>();

public boolean isInstanceNameValid(String resource)
{
   return allFunctions.contains(resource);
}

public boolean isRightsSetValid(Object[] rights)
{
   return rights.length == 1 &&
          rights[0].getClass().isInstance(new BitField(1));
}

These functions validate resource name (in this case the “function”) and the rights represented by an array of access rights items. Validation is performed when a new ACL is added and when ACLs are read from the configuration directory on server startup. The function name is validated against the set containing all functions of your application.

public void init()
{
   refresh(null);
}

public void refresh(AdminAccountExtension ext)
{
   allFunctions.clear();
   allFunctions.addAll(YourFunctionsManager.getAllFunctions());
}
The init function overrides the one in the base class. It is called during server startup after all accounts have been initialized.

The refresh function also overrides the one in the base class. It is called when the plugin is refreshed and it can be called externally using SecurityAdmin.refreshPlugin or SecurityCache.refreshAll. Normally it is called when the security cache is updated (when you apply changes performed in the administration UI).

In this example, a list of functions is retrieved using some abstract YourFunctionsManager. Generally, it is a good idea to manage the list of “function” resources of your application through a custom administration UI extension.

public static boolean checkReadAccess(String function)
{
   return checkAccess(function, AppFunctionRights.BIT_READ_ACCESS);
}

public static boolean checkCreateAccess(String function)
{
   return checkAccess(function, AppFunctionRights.BIT_CREATE_ACCESS);
}

public static boolean checkUpdateAccess(String function)
{
   return checkAccess(function, AppFunctionRights.BIT_UPDATE_ACCESS);
}

public static boolean checkDeleteAccess(String function)
{
   return checkAccess(function, AppFunctionRights.BIT_DELETE_ACCESS);
}

private static boolean checkAccess(String function, int accessMode)
{
   SecurityManager sm = SecurityManager.getInstance();
   AppFunctionResource plugin =
            (AppFunctionResource) sm.getPluginInstance(PLUGIN_TYPE_NAME);
   return plugin.checkAccessWorker(function, accessMode);
}

The first four functions are static convenience functions for checking specific (read, create, update, delete) access rights from your application. The last function finds plug-in instance and passes actual rights checking work to the instance worker method checkAccessWorker.

private boolean checkAccessWorker(String function, int accessMode)
{
   // check to see if there is a cached decision (cache is context-specific)
   Boolean cached = sm.getCachedDecision(resourceIndex, function, accessMode);

   if (cached != null)
   {
      return cached;
   }

   // initiate the ACL search
   int handle = sm.openRightsSearch(resourceIndex, function, accessMode);

   // rights check loop
   boolean decision = false;
   AppFunctionRights rights = (AppFunctionRights) sm.getNextRights(handle);

   while (rights != null)
   {
      decision = rights.get(accessMode);

      if (decision)
         break;

      rights = (AppFunctionRights) sm.getNextRights(handle);
   }

   // close the ACL search and cache the decision
   sm.closeRightsSearch(handle, decision, true);

   return decision;
}

This function checks specific access rights of the current subject (user or process) to access the specific “function” resource. In this example we are using the caching abilities provided by SecurityManager, this cache is context-specific. Normally, you may want to implement more advanced caching behavior, for example - cache all permissions for all subjects for a given function when this function is accessed for the first time.

The resourceIndex is an integer variable declared in AbstractResource which represents the unique identifier assigned to this resource type.

The ACL search is performed using openRightsSearch, getNextRights and closeRightsSearch methods of the SecurityManager. This works in the following way: first, a check list is created, the first item in the check list is the user or process account whose access rights we are checking. If the account is a user account, the groups the user participates in are determined and these groups are added to the check list. Then a call of getNextRights takes the next account from the check list and returns the Rights object which corresponds the first (according to node identifier) ACL for this resource which contains this account in the list of subjects. If there is no such ACL, but some ACL exist for this resource with the “all_others” subject, the first of these ACL is returned. Finally, the access decision may be cached for the given subject, resource and access mode.

Full AppFunctionResource listing:

package com.company.admin;

import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;

import java.util.*;

/**
 * Implements the "function" abstract resource. Instance of this resource
 * controls access to named functions of the application.
 */
public class AppFunctionResource
extends AbstractResource
{
   /** The set containing names of all functions of the application. */
   private final Set<String> allFunctions = new HashSet<String>();

   /** Plugin resource type name. */
   private final static String PLUGIN_TYPE_NAME = "app-function";

   /**
    * Checks READ access rights of the current subject with regards to the
    * given "function" abstract resource.
    *
    * @param  function
    *         Function name.
    *
    * @return true if access is allowed.
*/
public static boolean checkReadAccess(String function) {
return checkAccess(function, AppFunctionRights.BIT_READ_ACCESS);
} /** * Checks CREATE access rights of the current subject with regards to the * given "function" abstract resource. * * @param function * Function name. * * @return true if access is allowed.
*/
public static boolean checkCreateAccess(String function) {
return checkAccess(function, AppFunctionRights.BIT_CREATE_ACCESS);
} /** * Checks UPDATE access rights of the current subject with regards to the * given "function" abstract resource. * * @param function * Function name. * * @return true if access is allowed.
*/
public static boolean checkUpdateAccess(String function) {
return checkAccess(function, AppFunctionRights.BIT_UPDATE_ACCESS);
} /** * Checks DELETE access rights of the current subject with regards to the * given "function" abstract resource. * * @param function * Function name. * * @return true if access is allowed.
*/
public static boolean checkDeleteAccess(String function) {
return checkAccess(function, AppFunctionRights.BIT_DELETE_ACCESS);
} /** * Checks access rights of the specified type of the current subject with * regards to the given "function" abstract resource. * * @param function * Function name. * @param accessMode * Required access mode ( * AppFunctionRights.BIT_READ_ACCESS, * AppFunctionRights.BIT_CREATE_ACCESS etc.). * * @return true if access is allowed.
*/
private static boolean checkAccess(String function, int accessMode) {
SecurityManager sm = SecurityManager.getInstance();
AppFunctionResource plugin =
(AppFunctionResource) sm.getPluginInstance(PLUGIN_TYPE_NAME);
return plugin.checkAccessWorker(function, accessMode);
} /** * Initializes the plugin, called by runtime when all accounts are created.
*/
public void init() {
refresh(null);
} /** * Returns the plugin resource type name as a string. * * @return plugin resource type name
*/
public String getTypeName() {
return PLUGIN_TYPE_NAME;
} /** * Returns the name of a class that implements the RightsEditor interface * for this resource type. * * @return name of a class implementing RightsEditor
*/
public String getRightsEditorName() {
return "com.company.admin.AppFunctionRightsEditor";
} /** * Returns an array of descriptions, one object per the plugin's access * rights item. * * @return array of Descriptions, one per each plugin's access * rights item.
*/
public Description[] describeRights() {
Description[] items = new Description1;
items0 = new Description(AttributeType.ATTR_BITFIELD, // type - bit field
false, // is mandatory field
false, // is variable-size field
4, // field size (not used)
AppFunctionRights.PERMISSIONS_ATTRIBUTE, // text label
"bit set to ON enables access", // description text
new String[] // bit names {
"Read",
"Create",
"Update",
"Delete"
},
new BitSet(4)); // bit mask of unused fields - no unused
return items;
} /** * Instantiates a plugin's class that implements the Rights interface, * using the array of objects representing a set of access rights fields. * * @param rights * An array of objects of proper types representing items in a set * of access rights. * * @return an object that implements the Rights interface and represents * the specified access rights.
*/
public Rights getRightsInstance(Object[] rights) {
return new AppFunctionRights((BitField)rights0);
} /** * Checks whether a given string is a syntactically valid resource name * for this resource type. * * @param resource * String naming a resource. * * @return true if the name is syntactically correct.
*/
public boolean isInstanceNameValid(String resource) {
return allFunctions.contains(resource);
} /** * Checks whether a given array of objects representing a set of access * rights fields is acceptable. * * @param rights * An array of objects of proper types representing items in a set * of access rights. * * @return true if the array is acceptable for an instance * of access rights.
*/
public boolean isRightsSetValid(Object[] rights) {
return rights.length == 1 &&
rights0.getClass().isInstance(new BitField(1));
} /** * Refresh request to plugin. * * @param ext * Custom admin server plugin.
*/
public void refresh(AdminAccountExtension ext) {
allFunctions.clear();
allFunctions.addAll(YourFunctionsManager.getAllFunctions());
} /** * Checks access rights of the specified type of the current subject with * regards to the given "function" abstract resource. * * @param function * Function name. * @param accessMode * Required access mode ( * AppFunctionRights.BIT_READ_ACCESS, * AppFunctionRights.BIT_CREATE_ACCESS etc.). * * @return true if access is allowed.
*/
private boolean checkAccessWorker(String function, int accessMode) {
// check to see if there is a cached decision (cache is context-specific)
Boolean cached = sm.getCachedDecision(resourceIndex, function, accessMode); if (cached != null) {
return cached;
} // initiate the ACL search
int handle = sm.openRightsSearch(resourceIndex, function, accessMode); // rights check loop
boolean decision = false;
AppFunctionRights rights = (AppFunctionRights) sm.getNextRights(handle); while (rights != null) {
decision = rights.get(accessMode); if (decision)
break; rights = (AppFunctionRights) sm.getNextRights(handle);
} // close the ACL search // close the ACL search sm.closeRightsSearch(handle, decision, true); return decision;
}
}
AppFunctionRights Implementation

This class represents the rights object for the “function” resource type. The full listing of AppFunctionRights is provided below.

A rights object class should implement Rights and Serializable interfaces.

Our class stores access rights in the permissions bit field. Each bit corresponds one of the following permissions: READ, CREATE, UPDATE or DELETE. Correspondence between the index of the bit in the bit field and the permission is encoded using the BIT_*_ACCESS constants.

The toString function returns the string representation of the object. Specifically, it is used in the administration UI for the “Rights” column of tables displaying ACLs. The representation has the {XXXX} form where X is the dot character (“.”) if the permission is not set, or it is the R, C, U or D character if the corresponding permission is set. For example: if the ACL allows READ, CREATE and DELETE but not EDIT, the result would be {RC.D}.

The toDirectoryNode function creates an external representation of the object as a directory node. It creates a node of the appFunctionRights class with the single attribute permissions which stores the content of the permissions bit field. Note that:

  • The attribute name should match the one specified in the Description object for this resource type (see AppFunctionResource.describeRights).
  • Rights class should be declared in the directory schema (see the “Configuration and Packaging” subsection).

Other methods are default and convenience constructors and functions for accessing permissions bit field.

Full listing:

package com.company.admin;

import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.security.*;
import java.io.*;

/**
 * Implements the "function" rights objects. Supports READ, CREATE, UPDATE and
 * DELETE access rights.
 */
public class AppFunctionRights
implements Rights,
           Serializable
{
   /** Index of READ permission bit in the permissions bit field. */
static final int BIT_READ_ACCESS = 0; /** Index of CREATE permission bit in the permissions bit field. */
static final int BIT_CREATE_ACCESS = 1; /** Index of UPDATE permission bit in the permissions bit field. */
static final int BIT_UPDATE_ACCESS = 2; /** Index of DELETE permission bit in the permissions bit field. */
static final int BIT_DELETE_ACCESS = 3; /** * Class of the rights node in the directory.
*/
static final String RIGHTS_CLASS = "appFunctionRights"; /** * Name of the "permissions" attribute in the directory.
*/
static final String PERMISSIONS_ATTRIBUTE = "permissions"; /** Stores the permissions for the specific function and subject. */
private final BitField permissions; /** * Creates new AppFunctionRights object with all permissions set to "false".
*/
public AppFunctionRights() {
permissions = new BitField(BIT_DELETE_ACCESS + 1);
} /** * Creates new AppFunctionRights object with the specified permissions. * * @param permisssions * Permissions to be set.
*/
public AppFunctionRights(BitField permisssions) {
this.permissions = permisssions;
} /** * Returns string representation of this object. * * @return string representation of the object.
*/
public String toString() {
StringBuffer sb = new StringBuffer(); sb.append("{");
if (permissions.get(BIT_READ_ACCESS))
sb.append("R");
else
sb.append("."); if (permissions.get(BIT_CREATE_ACCESS))
sb.append("C");
else
sb.append("."); if (permissions.get(BIT_UPDATE_ACCESS))
sb.append("U");
else
sb.append("."); if (permissions.get(BIT_DELETE_ACCESS))
sb.append("D");
else
sb.append("."); sb.append("}");
return new String(sb);
} /** * Creates an external representation of this instance as a directory node. * * @param ds * instance of DirectoryService to use * * @param node * full directory node name for the rights node being created * * @return true if success
*/
public boolean toDirectoryNode(DirectoryService ds, String node) {
Attribute[] data = new Attribute[] {
new Attribute(ds.getClassNodeAttribute(RIGHTS_CLASS,
PERMISSIONS_ATTRIBUTE),
new Object[] {permissions})
}; return ds.addNode(node, RIGHTS_CLASS, data);
} /** * Gets the indexed bit of permissions. * * @param index * Index of the permissions bit to query. * * @return the bit's value.
*/
public boolean get(int index) {
return permissions.get(index);
} /** * Sets the indexed bit of permissions. * * @param index * Index of the permissions bit to set. * * @param value * Value to be set.
*/
public void set(int index, boolean value) {
permissions.set(index, value);
}
}
AppFunctionRightsEditor Implementation

This class customizes the rights editor dialog (for the administration UI) for the “function” rights. Visually this dialog looks in this way:

A rights object editor class should implement RightsEditor interface. The main methods are initialize which creates the dialog and edit which displays the dialog and returns the edited rights object. The full class listing:

package com.company.admin;

import com.goldencode.p2j.admin.client.*;
import com.goldencode.p2j.security.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

/**
 * Dialog for editing "function" access rights.
 */
public class AppFunctionRightsEditor
implements RightsEditor
{
   /** Reference to AdminClient. */
   private AdminClient client = null;

   /** Checkbox for READ access right. */
   private JCheckBox chkRead = null;

   /** Checkbox for CREATE access right. */
   private JCheckBox chkCreate = null;

   /** Checkbox for UPDATE access right. */
   private JCheckBox chkUpdate = null;

   /** Checkbox for DELETE access right. */
   private JCheckBox chkDelete = null;

   /** Main dialog panel. */
   private JPanel mainPanel = null;

   /** The dialog. */
   private JDialog dialog = null;

   /** Indicates whether access rights should be saved. */
   private boolean saving;

   /**
    * Gives chance to editors to initialize their data structures.
    * <p>
    * This is a one time call for this kind of resource for the life time
    * of the admin session.
    *
    * @param ac
    *        Instance of AdminClient the editor is called from.
    *
    * @param description
    *        Rights structure description.
    */
   public void initialize(AdminClient ac, Description[] description)
   {
      // save the useful reference
      client = ac;

      // create the edit panel
      Component[][] comps = new Component[5][2];

      String[] bitNames = description[0].getBitNames();

      // 1st row - Run permission
      chkRead = new JCheckBox("R");
      chkRead.setToolTipText("allows to read objects");
      comps[0][0] = new JLabel(bitNames[0]);
      comps[0][1] = chkRead;

      // 2nd row - Create permission
      chkCreate = new JCheckBox("C");
      chkCreate.setToolTipText("allows to create objects");
      comps[1][0] = new JLabel(bitNames[1]);
      comps[1][1] = chkCreate;

      // 3rd row - Update permission
      chkUpdate = new JCheckBox("U");
      chkUpdate.setToolTipText("allows to update objects");
      comps[2][0] = new JLabel(bitNames[2]);
      comps[2][1] = chkUpdate;

      // 4th row - Delete permission
      chkDelete = new JCheckBox("D");
      chkDelete.setToolTipText("allows to delete objects");
      comps[3][0] = new JLabel(bitNames[3]);
      comps[3][1] = chkDelete;

      // last row - buttons
      JButton btnCancel = new JButton("Cancel");
      JButton btnSave = new JButton("Save");
      comps[4][0] = btnCancel;
      comps[4][1] = btnSave;

      // layout
      mainPanel = AdminUtils.groupComponents(comps, "App Function Rights", false);

      // event handlers
      btnSave.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent evt)
         {
            handleSave();
         }
      });

      btnCancel.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent evt)
         {
            handleCancel();
         }
      });
   }

   /**
    * Creates an instance of the Rights, which does not take
    * any input and is considered a default starting point in editing.
    *
    * @return an instance of Rights
*/
public Rights createDefaultRights() {
return new AppFunctionRights();
} /** * Edits the given instance of the Rights. * The implementation should prepare its modal dialog using the initial state * from the passed instance of the Rights, show the dialog, * provide event processing until the Save or Cancel action is applied, then * hide the dialog and return the appropriate result. * * @param name * Name of the resource instance being edited. * * @param exact * Nature of the name (exact or regexp). * @param rights * Instance of Rights to be edited. * * @return the resulting instance of Rights if the user saved * the results, otherwise null
*/
public Rights edit(String name, boolean exact, Rights rights) {
// prefill the edit panel with the current rights
AppFunctionRights funcRights = ((AppFunctionRights)rights); chkRead.setSelected(funcRights.get(AppFunctionRights.BIT_READ_ACCESS));
chkCreate.setSelected(funcRights.get(AppFunctionRights.BIT_CREATE_ACCESS));
chkUpdate.setSelected(funcRights.get(AppFunctionRights.BIT_UPDATE_ACCESS));
chkDelete.setSelected(funcRights.get(AppFunctionRights.BIT_DELETE_ACCESS)); // display the dialog
saving = false;
dialog = AdminUtils.modalDialog(client, "App Function Rights", mainPanel);
dialog.setVisible(true); // process the results
dialog = null; if (!saving) {
return null;
} funcRights.set(AppFunctionRights.BIT_READ_ACCESS, chkRead.isSelected());
funcRights.set(AppFunctionRights.BIT_CREATE_ACCESS, chkCreate.isSelected());
funcRights.set(AppFunctionRights.BIT_UPDATE_ACCESS, chkUpdate.isSelected());
funcRights.set(AppFunctionRights.BIT_DELETE_ACCESS, chkDelete.isSelected()); return funcRights;
} /** * Handles "Cancel" button.
*/
private void handleCancel() {
saving = false;
dialog.setVisible(false);
} /** * Handles "Save" button.
*/
private void handleSave() {
saving = true;
dialog.setVisible(false);
}
}

Configuration and Packaging

The following steps are required in order to register the new resource type:

  1. In order to read and write rights to directory, rights object class for the new resource type should be registered in the extended directory schema, which is contained in dir_schema_ext.xml file in the root directory of your application. This is built into the application's jar file and is loaded from there by the server in order to understand the application-specific directory entries. It should contain an object-class entry which describes the rights class and nested class-attribute entries which describe the rights items. In our case the registration entry looks like this:
    <?xml version="1.0"?>
    <schema-root>
          ...
    
          <object-class name="appFunctionRights" leaf="false" immutable="false">
             <class-attribute name="permissions" type="BITFIELD" mandatory="true" 
                              multiple="false" immutable="false" />
          </object-class>
    </schema-root>
    

    For information about entries' properties see the “Schema Definition File Format” section of src/com/goldencode/p2j/directory/package.html.
  2. In order to register the new resource type, the *Resource class should be registered in the server configuration directory by adding a new node attribute to the /security/config/resource-plugins node. The value of the node attribute should match the fully qualified *Resource class name. In our case the registration entry looks like this:
    <node class="strings" name="resource-plugins">
       ...
       <node-attribute name="values" value="com.company.admin.AppFunctionResource"/>
    </node>
    
  3. *Resource, *Rights and *RightsEditor files should be included in one of the administration UI applet jars. You don't have to register this jar, just include it in the server class path, it will be automatically loaded by administration UI. Do not include these classes in the application's main jar file unless you expect that jar to be sent down to the administration UI applet (which is a really bad idea).

Managing ACLs

You can manage ACLs for the new resource type using the administration UI, specifically using the Access Control menu. Let's add an ACL by calling Access Control → Show All → Add ACL. The following parameters will be used:

After the changes have been applied the following node appears in the configuration directory:

<node class="container" name="000101">
   <node class="resource" name="resource-instance">
      <node-attribute name="reference" value="orders"/>
      <node-attribute name="reftype" value="TRUE"/>
   </node>
   <node class="appFunctionRights" name="rights">
      <node-attribute name="permissions" value="'1011'B"/>
   </node>
   <node class="strings" name="subjects">
      <node-attribute name="values" value="developers"/>
      <node-attribute name="values" value="admin"/>
   </node>
</node>

You could add this node manually as well (by editing the configuration directory when the FWD server is down). In case of manual edits, the entered values will not be validated until server start up.

Use in Your Application

Now we can make use of our “function” resource type by checking user or process rights against the named “functions” in proper places of your code using the AppFunctionResource.check*Access static functions. You can use them in manually written code or integrate it in the converted code (the most convenient way to do it is to use customer-specific conversion annotations).

Consider we have an “Order Management” function which displays the list of orders and allows users to create, update or delete orders. The code skeleton with integrated security checkups looks in this way:

package com.company;

import com.company.admin.*;
import com.goldencode.p2j.util.*;
import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.util.CompareOps.*;
import static com.goldencode.p2j.ui.LogicalTerminal.*;

public class Test
{
   public void execute()
   {
      externalProcedure(new Block()
      {
         logical selection = new logical(false);

         public void init()
         {
            TransactionManager.register(selection);
         }

         public void body()
         {
            if (!AppFunctionResource.checkReadAccess("orders"))
            {
               messageBox("You do not authorized to access 'Order Management'!",
                          ALERT_MESSAGE, BTN_OK, null);
               returnNormal();
            }

            repeat("loopLabel0", new Block()
            {
               public void body()
               {
                  // Orders display code.
                  ...

                  message("Select action: (N)ext, (P)rev, (C)reate, (U)pdate, (D)elete",
                          false, new AccessorWrapper(selection));

                  if (_isEqual(selection, "C"))
                  {
                     if (!AppFunctionResource.checkCreateAccess("orders"))
                     {
                        messageBox("You do not authorized to create orders!",
                                   ALERT_MESSAGE, BTN_OK, null);
                        next("loopLabel0");
                     }

                     // Order create code.
                     ...
                  }
                  else
                  {
                     if (_isEqual(selection, "U"))
                     {
                        if (!AppFunctionResource.checkUpdateAccess("orders"))
                        {
                           messageBox("You do not authorized to update orders!",
                                      ALERT_MESSAGE, BTN_OK, null);
                           next("loopLabel0");
                        }

                        // Order update code.
                        ...
                     }
                     else
                     {
                        if (_isEqual(selection, "D"))
                        {
                           if (!AppFunctionResource.checkDeleteAccess("orders"))
                           {
                              messageBox("You do not authorized to delete orders!",
                                         ALERT_MESSAGE, BTN_OK, null);
                              next("loopLabel0");
                           }
                        }

                        // Order delete code.
                        ...
                     }
                  }
               }
            });
         }
      });
   }
}

Authentication Plug-Ins

You can write your own authentication plugin which will be responsible for authentication processing in your application. FWD supports the following authentication modes:

  1. Using certificate.
  2. Using user name and password.
  3. Using user name and password + certificate.
  4. Custom authentication (using a plug-in).

You can specify the authentication mode for a user or a group, but note that the server side can apply the authentication type only if it receives the user ID from the client, which can be specified using a certificate or using access:subject:id configuration parameter. Processes always use certificate-based authentication. For other case the default authentication mode is applied (specified in the configuration directory).

Implementation

Authentication plug-in consists of server-side and client-side parts. These parts can be implemented using two classes or a single class. In either case, both sides of the processing are handled by implementing the interface Authenticator.

If you implement the plug-in using a single class then make sure that all imported classes (classes upon which the code depends) can be accessed on both server and client sides. The plug-in itself does not have to reside on the client system because at authentication time it is dynamically sent down over the network by the server. However, the code in the plug-in may have dependencies and since the client does not normally have the application-specific jar file there, these dependencies may be highly limited.

In regard to a two-part plug-in:

  • In the server-side class the clientAuthHook and clientFinalize functions should be NOPs (a no-operation), in the client-side class the serverAuthHook should be a NOP.
  • You don't have to implement the server-side part at all, in this case SecurityManager.serverAuthHook function will perform default server-side processing.
  • Classes should be named <some path>.<some class name>Client and <some path>.<some class name>Server.
  • Make sure that the server-side part does not use any classes that can be accessed only on the client side and that the client-side part does not use any classes that can be accessed only on the server side.

Let's implement the following custom login screen:

The implementation contains the single class containing the client-side part because SecurityManager.serverAuthHook function checks user ID and password which perfectly suits our server-side processing needs.

On the client side you cannot use the high-level UI managing functions available on server (which are used for converted code), you have to use the low-level UI functions available on client.

The full plug-in listing:

package com.company.client;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.chui.*;
import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.util.*;
import java.util.*;

/**
 * Custom login screen.
 */
public class CustomLoginClient
implements Authenticator
{
   /** Main frame ID (an arbitrary integer * 100). */
   private final static int FRAME_ID = 100;

   /** User ID fill-in ID. */
   private final static int USER_ID = FRAME_ID + 1;

   /** Password fill-in ID. */
   private final static int PASSWORD_ID = FRAME_ID + 3; // + skip between User ID and Password fill-ins

   /** CHUI driver reference. */
   private final ThinClient client = ThinClient.getInstance();

   /** Screen buffer for the main frame. */
   private final ScreenBuffer frameScreenBuffer = new ScreenBuffer(FRAME_ID, 3);

   /** Last entered user ID. */
   private String userId = null;

   /**
    * Implements client side custom authentication logic.
    *
    * @param    parameters
    *           Additional configuration parameters.
    * @param    code
    *           The result of the most recent attempt to authenticate or
    *           AUTH_RESULT_NONE if this is the first attempt.
    *
    * @return   Array of bytes to be transmitted to the server as the
    *           authorization input.
*/
public byte[] clientAuthHook(Map<String, Object> parameters, int code) {
OutputManager tk = OutputManager.instance(); // First iteration of an authentication attempt.
if (code == AUTH_RESULT_NONE) {
ScreenDefinition screenDefinition = new ScreenDefinition(FRAME_ID); // Frame configuration.
FrameConfig frameConfig = new FrameConfig(ComponentConfig.FRAME, null);
frameConfig.setRow(10);
frameConfig.setHeightChars(4);
frameConfig.setWidthChars(39);
frameConfig.setBox(true);
frameConfig.setCentered(true);
frameConfig.setOverlay(true);
frameConfig.setSideLabels(true);
screenDefinition.addConfig(frameConfig, FRAME_ID); // User ID fill-in configuration.
FillInConfig fic = new FillInConfig(ComponentConfig.FILL_IN, null);
fic.setDataType("character");
fic.setFormat("x(25)");
fic.setLabel("User Name");
screenDefinition.addConfig(fic, USER_ID); // Skip between User ID and Password fill-ins configuration.
SkipConfig sc = new SkipConfig(ComponentConfig.SKIP, null);
sc.setVertical(true); // skip not space
sc.setHeight(0);
screenDefinition.addConfig(sc, USER_ID + 1); // Password fill-in configuration.
fic = new FillInConfig(ComponentConfig.FILL_IN, null);
fic.setDataType("character");
fic.setFormat("x(25)");
fic.setLabel(" Password");
fic.setBlank(true); // Password is not displayed during typing.
screenDefinition.addConfig(fic, PASSWORD_ID); // Instantiate screen data.
client.pushScreenDefinition(new ScreenDefinition[] {screenDefinition});
client.statusInputRevert();
}
else {
// Subsequent authentication attempt - display result of the
// previous (unsuccessful) attempt.
String errorMessage = "Unknown error!"; switch (code) {
case AUTH_RESULT_INVALID_USERID:
errorMessage = "Invalid user ID!";
break; case AUTH_RESULT_INVALID_PASSWORD:
errorMessage = "Invalid password!";
break;
} // Display error.
client.message(errorMessage);
} // Preserve previously typed user ID.
if (userId != null) {
frameScreenBuffer.putWidgetValue(USER_ID, new character(userId));
} String password; while (true) {
// Drive the main frame I/O.
tk.setInvalidate(true);
client.enable(FRAME_ID, null, frameScreenBuffer, true);
client.clear(FRAME_ID, true);
client.view(FRAME_ID, frameScreenBuffer, null, true);
frameScreenBuffer.resetChanged();
tk.setInvalidate(false); ScreenBuffer sb = null;
try {
client.setAuthMode(true);
sb = client.waitFor(new EventList(true), // Wait for GO event.
USER_ID, -1, frameScreenBuffer);
}
catch (ConditionException cx) {
continue;
}
finally {
client.setAuthMode(false);
} tk.setInvalidate(true);
client.enable(FRAME_ID, null, frameScreenBuffer, false); // Disable main frame.
tk.setInvalidate(false); // Get User ID and Password.
character charId = (character) sb.getWidgetValue(USER_ID);
if (charId != null && !charId.isUnknown())
userId = charId.getValue(); character charPassword = (character) sb.getWidgetValue(PASSWORD_ID);
password = charPassword null || charPassword.isUnknown() ? "" : charPassword.getValue(); break;
} // Return a byte array.
return SecurityManager.packageIdPassword(userId, password);
} /** * Finalizes any resources allocated during authentication by the client.
*/
public void clientFinalize() {
client.statusInputRevert();
client.hideAll(true);
client.destroyFrame(FRAME_ID);
} /** * Implements server side custom authentication logic. * &lt;p&gt; * Accepts the byte array produced by the client side authentication hook * as authentication input and custom parameters. * * @param auth * The authorization input from the client. * @param parameter * Additional configuration parameters taken from the directory. * * @return The result of the authentication processing.
*/
public AuthenticationResponse serverAuthHook(byte[] auth, String parameter) {
return null; // Not used for the client side.
}
}

Registration and Packaging

In order to be available to use the new authentication plug-in for specific users or groups it should be registered into server configuration directory under /security/config/auth-plugins branch. The registration entry is represented by a container with an arbitrary name and the following parameters:

  • classname - fully qualified plug-in class name. If it is a two-part plug-in (even if it has no server-side part) then “Client” or “Server” suffix should be dropped.
  • description - arbitrary description for display in the administration UI.
  • option - optional string parameter that can be obtained on the client side using parameters map of the clientAuthHook function and on the server side using parameter parameter of the serverAuthHook function.

In our case the registration entry looks in this way:

<node class="container" name="auth-plugins">
   ...
   <node class="container" name="custom_login">
      <node class="string" name="classname">
         <node-attribute name="value" value="com.company.client.CustomLogin"/>
      </node>
      <node class="string" name="description">
         <node-attribute name="value" value="Custom Login Screen"/>
      </node>
   </node>
</node>

Now you can select the new authentication plug-in in the administration UI in the user or group account properties:

In order to set the new plug-in as the new default authentication method for all users you should set the following attributes of the /security/config/auth-mode node:

  • mode to “4” (custom authentication mode);
  • plugin to the fully qualified plug-in class name (the same rules as for the classname parameter above are applied);
  • retries parameter represents the number of allowed authentication retries, or you can set “-1” for infinite number of retries.

In our case authentication mode specifications will look in this way in the directory:

<node class="authMode" name="auth-mode">
   <node-attribute name="mode" value="4"/>
   <node-attribute name="plugin" value="com.company.client.CustomLogin"/>
   <node-attribute name="retries" value="-1"/>
</node>

The client-side plug-in class is transferred to client over network from the server, so you can put the plug-in class(es) in the jar containing your application.

Password Change Plug-In

You can implement custom user's password change plug-in. This plug-in is called automatically after a user's password expires (the expiration interval can be set in the configuration directory) when the user logs in for the next time, just before the entry procedure is called. It can also be called manually using the SecurityOps.changePassword() method.

Implementation

A password change plug-in should implement the PasswordInput interface. The plug-in code runs after the FWD client is initialized, so it can take advantage of normal 4GL UI features, just like normal converted code. This means it can create a dialog using converted 4GL code.

Our custom password change plug-in is the screen will prompt the user for a new password and a second time for its verification. The new password should be at least 6 characters in length and shouldn't match the old password. Password changing procedure can be canceled by pressing F4. The screen looks like this:

Our implementation contains two files: PasswordChange which contains the plug-in code and PassMainFrame which represents the password changing frame definition. The plug-in code was based on manullay modified converted 4GL code.

PasswordChange listing:

package com.company;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.*;
import java.util.*;

import static com.goldencode.p2j.ui.LogicalTerminal.*;
import static com.goldencode.p2j.util.BlockManager.*;
import static com.goldencode.p2j.util.CompareOps.*;
import static com.goldencode.p2j.util.character.*;
import static com.goldencode.p2j.util.logical.*;

/**
 * Password change screen.
 */
public class PasswordChange
implements PasswordInput
{
   /** New password. */
   private String newPasswordStr = null;

   /** Hash of the old password. */
   private byte[] oldHash = null;

   /** Main frame. */
   private PassMainFrame mainFrameFrame = GenericFrame.createFrame(PassMainFrame.class, "main-frame");

   /**
    * Gets and verifies the new password.
    *
    * @param  oldHash
    *         Digest of the current password for comparisons.
    *
    * @return new password.
    */
   public String obtainPassword(byte[] oldHash)
   {
      this.oldHash = oldHash;
      execute();
      return newPasswordStr;
   }

   /**
    * Compares the password with the old one.
    *
    * @param  password
    *         Password to be compared with the old one.
    *
    * @return true if the passwords produce the same hash.
*/
private boolean checkPassword(character password) {
if (password.isUnknown() || oldHash == null)
return false; byte[] newHash = HashPassword.hashPassword(password.getValue()); return Arrays.equals(newHash, oldHash);
} /** * Main password changing code which displays the frame and handles * password update loop.
*/
private void execute() {
externalProcedure(new Block() {
character newPassword = new character(""); character verifyPassword = new character(""); public void init() {
TransactionManager.register(newPassword, verifyPassword);
} public void body() {
mainFrameFrame.openScope(); FrameElement[] elementList0 = new FrameElement[] {
new Element(newPassword, mainFrameFrame.widgetNewPassword()),
new Element(verifyPassword, mainFrameFrame.widgetVerifyPassword())
};
mainFrameFrame.display(elementList0); OnPhrase[] onPhrase0 = new OnPhrase[] {
new OnPhrase(BlockManager.Condition.ENDKEY, BlockManager.Action.LEAVE, "updateLoop")
}; repeat("updateLoop", onPhrase0, new Block() {
public void body() {
hideMessage(false); FrameElement[] elementList1 = new FrameElement[] {
new Element(newPassword, mainFrameFrame.widgetNewPassword()),
new Element(verifyPassword, mainFrameFrame.widgetVerifyPassword())
}; mainFrameFrame.update(elementList1); if (checkPassword(newPassword)) {
message("You should enter a new password, not re-use the old one!");
undoRetry("updateLoop");
}
if (_or(isUnknown(newPassword), isLessThan(length(newPassword), 6))) {
message("Password should be at least 6 characters in length!");
undoRetry("updateLoop");
}
if (_isNotEqual(newPassword, verifyPassword)) {
message("Passwords do not match!");
undoRetry("updateLoop");
} newPasswordStr = newPassword.getValue(); leave("updateLoop");
}
});
}
});
}
}

PassMainFrame listing:

package com.company;

import com.goldencode.p2j.util.*;
import com.goldencode.p2j.ui.*;

/**
 * Password changing frame.
 */
public interface PassMainFrame
extends CommonFrame
{
   public static final Class configClass = PassMainFrameDef.class;

   public character getNewPassword();

   public void setNewPassword(character parm);

   public void setNewPassword(String parm);

   public void setNewPassword(BaseDataType parm);

   public FillInWidget widgetNewPassword();

   public character getVerifyPassword();

   public void setVerifyPassword(character parm);

   public void setVerifyPassword(String parm);

   public void setVerifyPassword(BaseDataType parm);

   public FillInWidget widgetVerifyPassword();

   public static class PassMainFrameDef
   extends WidgetList
   {
      FillInWidget newPassword = new FillInWidget();

      SkipEntity expr2 = new SkipEntity();

      FillInWidget verifyPassword = new FillInWidget();

      public void setup(CommonFrame frame)
      {
         frame.setDown(1);
         frame.setTitle("You password has expired");
         frame.setSideLabels(true);
         frame.setCentered(true);
         frame.setRow(10);
         newPassword.setDataType("character");
         newPassword.setHelp("At least 6 characters, F4 to cancel.");
         newPassword.setBlank(true);
         expr2.setVertical(true);
         expr2.setHeight(0);
         verifyPassword.setDataType("character");
         verifyPassword.setBlank(true);
         newPassword.setFormat("x(25)");
         newPassword.setLabel("   New Password");
         verifyPassword.setFormat("x(25)");
         verifyPassword.setLabel("Verify Password");
      }

      {
         addWidget("newPassword", "new-password", newPassword);
         addWidget("expr2", "", expr2);
         addWidget("verifyPassword", "verify-password", verifyPassword);
      }
   }
}

Registration and Packaging

In order to set the new password change plug-in you should set the following attributes of the /security/config/change node:

  • maxage to the integer value which corresponds the number of days after which user passwords expire or “0” if passwords do not expire;
  • plugin to the fully qualified plug-in class name.

In our case the registration entry looks like this:

<node class="container" name="change">
   <node class="integer" name="maxage">
      <node-attribute name="value" value="30"/>
   </node>
   <node class="string" name="plugin">
      <node-attribute name="value" value="com.company.PasswordChange"/>
   </node>
</node>

Plug-in class(es) can be packaged in the jar containing your application as they don't run directly on the client, but instead thy run on the server side just like converted code.

Server Side Extension Plug-In

The server side extension plugin manages the account extension data (for user and process accounts). The core routines of the server can recognize only a predefined set of directory nodes that serve as account definitions. However, many application need to keep some extra data associated with each account. To make this possible, a server side extension plug-in has to be written. The plug-in will use the child nodes of the account section in the configuration directory to store all extra data. These extra nodes are fully transparent to the core FWD server. The plug-in can chose the number of and the format for the account extensions. The plug-in can also perform required actions on server start up and can implement a custom server-side API.

Consider the case of adding two extension fields: user employee number and user home directory. The resulting directory node for a user will look like this:

<node class="user" name="admin">
   ...standard entries here...
   <node class="integer" name="employee-num">
      <node-attribute name="value" value="123"/>
   </node>
   <node class="string" name="home-dir">
      <node-attribute name="value" value="/home/admin"/>
   </node>
</node>

If an extended field is not specified (i.e. is null), then the corresponding node should be missing. In the directory you can use standard directory data types (for information on them see the Types of Directory Objects section of the Directory chapter in the FWD Runtime Installation, Configuration and Administration Guide.

Implementation

Consider we want to extend an account with two new fields: employee number and user home directory. Our implementation contains two files:

  • CustomExtDef is the container for all extension fields.
  • CustomAccountExtension is the server-side extension plug-in.

CustomExtDef along with a field for each extended field, contains array EXT_FIELDS which describes all extended fields. There is no predefined schema for extension fields, these fields are interpreted at runtime and EXT_FIELDS array serves only convenience purposes. Since this class is used in communication between the administration UI and server, it implements the Serializable interface.

CustomExtDef listing:

package com.company.admin;

import java.io.Serializable;

/**
 * This class defines account extension fields and is a container for them.
 */
public class CustomExtDef
implements Serializable
{
   /**
    * Extension fields definitions (directory node id, Java field name,
    * directory data type).
    */
   public static final String[][] EXT_FIELDS =
   {
      {"employee-num",  "employeeNum",  "integer" },
      {"home-dir",      "homeDir",      "string"  }
   };

   /**
    * Employee number.
    */
   public Integer employeeNum;

   /**
    * Home directory.
    */
   public String homeDir;
}

Server-side extension plug-in should:

  • Implement AdminAccountExtension interface.
  • Have public static void initialize() method, which is called on server start up.
  • Have public static AdminAccountExtension getInterface() method which is used to get the instance of the plug-in.

CustomAccountExtension implements the following functions:

  • initialize - called on server start up, creates a static instance of the plug-in and registers server exports (these exports are related to the “Implementing a Custom Server-Side API” section below).
  • getInterface - returns the static instance of the plug-in.
  • isExtended - checks whether the given account node has custom extensions or not.
  • addExtension - creates the directory nodes to save the extensions data to the directory.
  • setExtension - changes the directory nodes that represent the extensions to match the new set of data.
  • cloneExtension - clones existing extension data into another account directory node.
  • deleteExtension - removes the existing extension data, if any.

The server-side plug-in provides the facilities to manage the extension fields using the administration UI. It does not provide facilities to read or use those extension fields in your application. You must write that code separately and you can use the standard directory APIs to create your own custom server-side API for this purpose. Then you would call that custom API from your application to read the values for the current account.

CustomAccountExtension listing:

package com.company.server;

import com.company.admin.*;
import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.net.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * Custom admin server plugin responsible for the maintaining the custom
 * account extension data in a solid state.
 */
public class CustomAccountExtension
implements AdminAccountExtension
{
   /**
    * Instance of the admin server plugin.
    */
   private static CustomAccountExtension instance;

   /**
    * Mandatory function, called by StandardServer at startup.
    */
   public static void initialize()
   {
      instance = new CustomAccountExtension();

      // Register custom server exports.
      Class<?>[] ifaces = new Class[] { CustomServerExports.class };
      RemoteObject.registerNetworkServer(ifaces, new CustomServerExportsImpl());
   }

   /**
    * Mandatory function, called by SecurityAdmin.getExtension().
    *
    * @return instance of the admin server plugin.
    */
   public static AdminAccountExtension getInterface()
   {
      return instance;
   }

   /**
    * Inspects the given account directory node and tells whether this account
    * has custom extension data associated with it.
    *
    * @param    ds
    *           instance of DirectoryService that may be used
    *           for all directory access needs
    *
    * @param    node
    *           full account directory node path and ID
    *
    * @return   true if this account possesses any extension data
*/
public boolean isExtended(DirectoryService ds, String node) {
// check the node type
String nodeClass = ds.getNodeClass(node);
if (!nodeClass.equals("/meta/class/user") &&
!nodeClass.equals("/meta/class/process")) {
return false;
} // check if there are children
String[] ch = ds.enumerateNodes(node);
if (ch null || ch.length 0) {
// no extensions found
return false;
} // this is an extended account
return true;
} /** * Adds custom account extension data to the newly created account * directory node. * * @param ds * instance of DirectoryService that may be used * for all directory access needs * * @param node * full account directory node path and ID * * @param ext * a container with extension data as received from the admin * client extension plugin * * @return true if operation was successful; otherwise * false, which will cause the batch rollback * * @throws AccountExtValidationException * if any exception is encountered while validating fields or * computing default field values, an exception will be thrown.
*/
public boolean addExtension(DirectoryService ds, String node, Serializable ext)
throws AccountExtValidationException {
// no extension provided, exit
if (ext == null) {
return true;
} boolean res = true; // add the extension fields
for (int i = 0; res && i < CustomExtDef.EXT_FIELDS.length; i++) {
String fieldNode = CustomExtDef.EXT_FIELDS[i][0];
String field = CustomExtDef.EXT_FIELDS[i][1];
String clazz = CustomExtDef.EXT_FIELDS[i][2]; StringBuilder sb = new StringBuilder(node);
sb.append("/");
sb.append(fieldNode);
String extNode = sb.toString(); try {
Field f = ext.getClass().getField(field);
Object fieldVal = f.get(ext); // if field is not specified, do not add the node
if (fieldVal == null) {
continue;
} Attribute attr = new Attribute(
ds.getClassNodeAttribute(clazz, "value"),
new Object[] { fieldVal }
); res = ds.addNode(extNode, clazz, new Attribute[] { attr });
}
catch (Exception e) {
throw new AccountExtValidationException("Error adding field " + field);
}
} return res;
} /** * Changes custom account extension data associated with the exsiting * account directory node. * Changes may include synchronizaion, addition or deletion of the whole * set of extension data, based on the current state of the node and the * provided data input. * * @param ds * instance of DirectoryService that may be used * for all directory access needs * * @param node * full account directory node path and ID * * @param ext * a container with extension data as received from the admin * client extension plugin * * @return true if operation was successful; otherwise * false, which will cause the batch rollback * * @throws AccountExtValidationException * if any exception is encountered while validating fields or * computing default field values, an exception will be thrown.
*/
public boolean setExtension(DirectoryService ds, String node, Serializable ext)
throws AccountExtValidationException {
boolean hasExtension = isExtended(ds, node); // no existing extensions and none provided - exit
if (!hasExtension && ext null) {
return true;
} // existing extensions, none provided - delete
if (hasExtension && ext null) {
return deleteExtension(ds, node);
} // no existing extensions, new ones are provided - add
if (!hasExtension) {
return addExtension(ds, node, ext);
} // existing extensions, provided new values - set boolean res = true; // check the existing nodes
String[] extNodes = ds.enumerateNodes(node);
Set<String> existingExtNodes = new HashSet<String>(Arrays.asList(extNodes)); // add the extension fields
for (int i = 0; res && i < CustomExtDef.EXT_FIELDS.length; i++) {
String fieldNode = CustomExtDef.EXT_FIELDS[i][0];
String field = CustomExtDef.EXT_FIELDS[i][1];
String clazz = CustomExtDef.EXT_FIELDS[i][2]; StringBuilder sb = new StringBuilder(node);
sb.append("/");
sb.append(fieldNode);
String extNode = sb.toString(); try {
Field f = ext.getClass().getField(field);
Object val = f.get(ext); if (val == null) {
// remove the attribute, if it was set ...
if (existingExtNodes.contains(fieldNode)) {
res = ds.deleteNode(extNode);
}
continue;
} Attribute attr = new Attribute(
ds.getClassNodeAttribute(clazz, "value"),
new Object[] { val }
); if (existingExtNodes.contains(fieldNode)) {
res = ds.setNodeAttributes(extNode, new Attribute[] { attr });
}
else {
res = ds.addNode(extNode, clazz, new Attribute[] { attr });
}
}
catch (Exception e) {
throw new AccountExtValidationException("Error adding field " + field);
}
} return res;
} /** * Clones existing custom account extension data to the newly cloned * account directory node. * * @param ds * instance of DirectoryService that may be used * for all directory access needs * * @param fromNode * full account directory node path and ID of the source account * * @param toNode * full account directory node path and ID of the target account * * @return true if operation was successful; otherwise * false, which will cause the batch rollback * * @throws AccountExtValidationException * if any exception is encountered while validating fields or * computing default field values, an exception will be thrown.
*/
public boolean cloneExtension(DirectoryService ds, String fromNode, String toNode)
throws AccountExtValidationException {
// enumerate children
String[] children = ds.enumerateNodes(fromNode);
if (children null || children.length 0) {
// no extensions found;
return true;
} // this is an extended account; clone extensions
for (String child : children) {
StringBuilder from = new StringBuilder(fromNode);
from.append("/");
from.append(child); StringBuilder to = new StringBuilder(toNode);
to.append("/");
to.append(child); if (!ds.copyNode(from.toString(), to.toString(), false)) {
// directory modification error
return false;
}
} return true;
} /** * Deletes custom account extension data associated with the account * directory node being deleted. * * @param ds * instance of DirectoryService that may be used * for all directory access needs * * @param node * full account directory node path and ID * * @return true if operation was successful; otherwise * false, which will cause the batch rollback
*/
public boolean deleteExtension(DirectoryService ds, String node) {
// enumerate children
String[] children = ds.enumerateNodes(node);
if (children null || children.length 0) {
// no extensions found
return true;
} // this is an extended account; remove extensions
for (String child : children) {
StringBuilder sb = new StringBuilder(node);
sb.append("/");
sb.append(child); if (!ds.deleteNode(sb.toString())) {
// directory modification error
return false;
}
} return true;
}
}

Registration

Server-side plug-in is registered by specifying the fully qualified class name in the /security/config/extensions/server node of the configuration directory. In our case the registration entry looks in this way:

<node class="string" name="server">
   <node-attribute name="value" value="com.company.server.CustomAccountExtension"/>
</node>

Accessing Extension Fields From Your Application

The most convenient way to access extension fields from your application is through a service that will provide these values for the currently logged in user. In our case we implemented CustomExtAccess class which provide the set of static functions for getting each of the extension fields. Class listing:

package com.company.server;

import com.company.admin.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;

/**
 * This class provides static functions for accessing extension fields of the
 * current (logged in) user.
 */
public class CustomExtAccess
{
   /**
    * Security manager instance.
    */
   private static SecurityManager sm = SecurityManager.getInstance();

   /**
    * Get employee number of the current user.
    *
    * @return See above.
    */
   public static integer getEmployeeNumber()
   {
      Integer num = sm.getExtInteger(CustomExtDef.EXT_FIELDS[0][0]);
      return new integer(num);
   }

   /**
    * Get home directory of the current user.
    *
    * @return See above.
    */
   public static character getHomeDirectory()
   {
      String dir = sm.getExtString(CustomExtDef.EXT_FIELDS[1][0]);
      return new character(dir);
   }
}

Example of use in the application:

message("Employee Number: " + CustomExtAccess.getEmployeeNumber().toStringMessage() +
        " Home Directory: " + CustomExtAccess.getHomeDirectory().toStringMessage());

You can use these functions in manually written code or integrate them in the converted code (the most convenient way to do it is to use customer-specific conversion annotations).

Administration User Interface Extensions

In this section we will consider how to implement different kinds of administration UI extensions, like:

  • client-side extension plug-in,
  • custom server-side API,
  • client-side screens (printable),
  • menus.

All client-side classes should be included in one of the administration UI applet jars. You don't have to register such jar, just include it in the server class path, it will be automatically loaded by administration UI. Do not place these classes in the application's main jar file because that jar will be sent down to the administration applet (which is a really bad idea).

Client-side Extension Plug-in

Client-side extension plug-in performs the following functions:

  • extends the user/process account edit/create dialog to handle any extension data,
  • adds custom menu items, both the major items and sub-menu items,
  • performs the menu initiated actions.

The client-side extension plug-in should implement both ClientAccountExtension (specifies functions performed by a user account extension editor) and AdminCustomMenu (specifies functions for handling custom administration UI menus) interfaces.

User Account Extension Editor

Consider we want to implement editor for the extension fields previously declared in the “Server Side Extension Plug-In” section: user employee number and user home directory. Our user account editor should look like this:

By default the user and process dialogs are both extended in the same way (i.e. in our case the process account editor has the same “Extension Fields” panel).

Our client-side plug-in class is called ClientExtension, and as an account extension editor it implements the following functions:

  • getLayoutMode - determines whether plug-in is responsible for layout of all widgets or only extension widgets.
  • layoutAllWidgets - performs layout of default and extension widgets (NOP if the plug-in is responsible for layout of only extension widgets), executed on each call of an account editing dialog.
  • layoutExtWidgets - performs layout of extension widgets (NOP if the plug-in is responsible for layout of all widgets), executed on each call of an account editing dialog.
  • getExtContainer - returns the container for extension fields (NOP if the plug-in is responsible for layout of all widgets).
  • initialize - sets IAccountScreen (which provides references to default account widgets) to be used in the plug-in.
  • validateExtensions - provides client-side validation of the extension data.
  • resetDialog - resets all extension widgets to a predefined state, called when a new account is created with the account editing dialog.
  • getPreviewHeaders - returns headers of the extension fields for printing (including print preview). In order to call print preview:
    1. select required account(s) in the accounts management screen (users or processes);
    2. click “Print” in the top menu;
    3. refine your request in the printing dialog if necessary and click “Print” in it.

You will get to the print preview screen, in our case it looks in this way:

  • getPreviewRow - returns data of the extension fields for printing.
  • getExtendedHeaders - returns headers of the extension fields for printing (including print preview) in the extended mode (when detailed account information is displayed). In order to select extended mode check the “show all details” check box in the print preview screen and click the “Print” button next to it. In our case the extended print preview screen looks in this way:

    The extension columns "Attribute Name" and "Attribute Value" appear on the right side.
  • getExtendedRows - returns the number of rows per account for printing in the extended mode.
  • fillExtendedRow - fills the data of the extension fields for printing in the extended mode.
  • buildUserFindPanel - builds the panel for the user find dialog. This dialog is called by clicking “Find User” on the user accounts management screen. In our case the dialog allows finding a user by employee number or by user name and looks like this:

Our client-side plug-in class ClientExtension is a menu handler implements the following functions:

  • attach - called once when the plug-in is loaded, you can put some plug-in initialization actions here.
  • insertCustomMenuItems - inserts custom menu items, called when a user logs into the administration UI.
  • removeCustomMenuItems - removes custom menu items, called when a user logs off the administration UI.

In our case we add major menu item “Custom Menu” with the “Employee Report” sub-menu item (it is visible when a user is logged into the administration UI):

ClientExtension Implementation

Full listing:

package com.company.admin;

import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.admin.client.*;
import com.goldencode.p2j.net.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.List;

/**
 * Custom admin client plugin responsible for handling customer-specific
 * extensions.
 */
public class ClientExtension
implements ClientAccountExtension,
           AdminCustomMenu
{
   /**
    * Standard account screen.
    */
   private IAccountScreen accountScreen = null;

   /**
    * Text field for employee number.
    */
   private JTextField tfEmployeeNumber = null;

   /**
    * Text field for home directory.
    */
   private JTextField tfHomeDir = null;

   /**
    * Reference to custom server-side API.
    */
   private CustomServerExports serverExports = null;

   /**
    * Custom menu item.
    */
   private JMenu customMenu = null;

   /**
    * Reference to AdminClient instance.
    */
   private AdminClient client = null;

   /**
    * Employees Report screen.
    */
   private EmployeesReport employeesReport = null;

   /**
    * Get the layout mode of the plugin:
    * <p>
    * {@link #LAYOUT_ALL_WIDGETS}: plugin is responsible to layout all widgets
    * <p>
    * {@link #LAYOUT_EXT_WIDGETS}: plugin is responsible to layout only ext
    * widgets
    *
    * @return   plugin layout mode.
    */
   public int getLayoutMode()
   {
      return LAYOUT_ALL_WIDGETS;
   }

   /**
    * Layout the extension fields; this method can re-layout the default
    * widgets too, if needed. If userName is provided, it
    * will populate the extension fields with its extension data, if any.
    *
    * @param    userName
    *           The user for which extension field layout is needed. If
    *           null, it means a new user is added.
    *
    * @param    writable
    *           true if this is for enabled inputs.
    *           false for readonly panel
    *
    * @return   the root panel which will be shown as a modal dialog.
*/
public JPanel layoutAllWidgets(String userName, boolean writable) {
// layout dialog
JPanel buttonsPanel = new JPanel();
buttonsPanel.add(accountScreen.getCloseButton());
buttonsPanel.add(accountScreen.getSaveButton()); tfEmployeeNumber = new JTextField(5);
tfEmployeeNumber.setMaximumSize(tfEmployeeNumber.getPreferredSize());
tfHomeDir = new JTextField(); JPanel extPanel = AdminUtils.groupComponents(
new Component[][]{{new JLabel("Employee Number"), tfEmployeeNumber}, {new JLabel("Home Directory"), tfHomeDir}});
extPanel.setBorder(new TitledBorder
(new EtchedBorder(EtchedBorder.RAISED), " Extension Fields ")); // fill account data
if (userName != null) {
CustomExtDef ext = serverExports.getAccountExtensions(userName);
if (ext != null) {
if (ext.employeeNum != null)
tfEmployeeNumber.setText(ext.employeeNum.toString());
if (ext.homeDir != null)
tfHomeDir.setText(ext.homeDir);
}
} tfEmployeeNumber.setEnabled(writable);
tfHomeDir.setEnabled(writable); return AdminUtils.groupComponents(
new Component[][]{{accountScreen.getDefaultWidgetPanel()}, {extPanel}, {buttonsPanel}});
} /** * If the plugin is in {@link #LAYOUT_EXT_WIDGETS} mode, it must return * the container to which extension fields will be added. * * @return a container to which extension fields will be added.
*/
public Container getExtContainer() {
return null;
} /** * Layout the extension fields in the specified container. If * userName is provided, it will populate the extension fields * with its extension data, if any. * * @param c * the container to which widgets will be added. * * @param userName * The user for which extension field layout is needed. If * null, it means a new user is added. * * @param writable * true if this is for enabled inputs. * false for readonly panel
*/
public void layoutExtWidgets(Container c, String userName, boolean writable) {
} /** * Client-side validation of the extension data. This means only following * validation is done: * - check if the mandatory widgets have data</li> * - check if the numeric, date, etc fields are valid</li> * - any other special validation which can be done on client-side</li> * * @param userName * The user for which extension field validation is needed. If * null, it means a new user is added. * * @return the extension object which holds extension data * * @throws AccountExtValidationException if client-side validation fails
*/
public Serializable validateExtensions(String userName)
throws AccountExtValidationException {
CustomExtDef def = new CustomExtDef(); String txt = tfEmployeeNumber.getText().trim();
if (!txt.isEmpty()) {
try {
def.employeeNum = Integer.parseInt(txt);
}
catch (Exception e) {
throw new AccountExtValidationException("Invalid employee number!");
}
} def.homeDir = tfHomeDir.getText().trim();
return def;
} /** * Set the current {@link IAccountScreen} instance to be used in the plugin. * * @param screen * the current {@link IAccountScreen} instance.
*/
public void initialize(IAccountScreen screen) {
accountScreen = screen;
} /** * Reset all extension widgets to a predefined state.
*/
public void resetDialog() {
tfHomeDir.setText("");
tfEmployeeNumber.setText("");
} /** * Returns the print preview table headers for the account extension data. * An ampty array disables extension data printout.
*/
public String[] getPreviewHeaders() {
return new String[] {"Employee Number", "Home Directory"};
} /** * Gathers and returns all extra columns info for the specific account * as strings. The number and the order of values should correspond to * the headers returned by {@link #getPreviewHeaders}. * * @param subjectId * account name
*/
public String[] getPreviewRow(String subjectId) {
String[] res = new String[] {"", ""}; CustomExtDef ext = serverExports.getAccountExtensions(subjectId); if (ext != null) {
if (ext.employeeNum != null)
res0 = ext.employeeNum.toString(); if (ext.homeDir != null)
res1 = ext.homeDir;
} return res;
} /** * Returns the extended print preview table headers for the account * extension data. An empty array disables extended print for the * extension data printout.
*/
public String[] getExtendedHeaders() {
return new String[]{"Attribute Name", "Attribute Value"};
} /** * Returns the extended print preview table rows required for the account * extension data per account.
*/
public int getExtendedRows() {
return CustomExtDef.EXT_FIELDS.length;
} /** * Fills the extended print preview table row columns with the account * extension data. * * @param subjectId * account name * * @param tm * table model to be used * * @param row * index of the table model row to start filling from * * @param column * index of the table model column to start filling from
*/
public void fillExtendedRow(String subjectId, TableModel tm, int row, int column) {
CustomExtDef extDef = serverExports.getAccountExtensions(subjectId); if (extDef == null) {
// no extension data; leave all blanks
for (int i = 0; i < CustomExtDef.EXT_FIELDS.length; i++) {
for (int j = 0; j < 2; j++) {
tm.setValueAt("", row + i, column + j);
}
} return;
} String val = extDef.employeeNum null ? "" :
extDef.employeeNum.toString();
tm.setValueAt("Employee Number", row, column);
tm.setValueAt(val, row, column + 1); val = extDef.homeDir null ? "" : extDef.homeDir;
tm.setValueAt("Home Directory", row + 1, column);
tm.setValueAt(val, row + 1, column + 1);
} /** * Create and return the find panel for the given find screen handler. * * @param findScreen * Target find screen handler. * * @return created find panel.
*/
public JPanel buildUserFindPanel(final IFindAccountScreen findScreen) {
final JTextField input = new JTextField(); JPanel buttonsPanel = new JPanel();
buttonsPanel.add(findScreen.getCancelButton());
buttonsPanel.add(findScreen.getFindButton()); JPanel targetPanel = AdminUtils.groupComponents(new Component[][] { { new JLabel("Enter User Name or Employee Number:") }, { input }, { buttonsPanel }
}); findScreen.getFindButton().addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
String txt = input.getText().trim(); Integer empNum = null;
try {
empNum = Integer.parseInt(txt);
}
catch (NumberFormatException e) {
// suppress
} // if user entered a number, try to find a user by employee number
String user = null;
if (empNum != null) {
user = serverExports.getUserNameByEmployeeNumber(empNum);
} // find account by the name which was entered or found by employee
// number
findScreen.findAccountByName(user != null ? user : txt);
}
}); findScreen.getCancelButton().addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
findScreen.handleCancel();
}
}); return targetPanel;
} /** * Attaches the plugin to the AdminClient instance. * Called once upon plugin loading. * * @param client * instance of AdminClient
*/
public void attach(AdminClient client) {
this.client = client;
serverExports =
(CustomServerExports) RemoteObject.obtainNetworkInstance(CustomServerExports.class,
client.getSession()); } /** * Inserts custom major and minor menu items. * The inserted menu items have to have the event listeners attached * for the menu items to have effect. * * @param menu * the main menu list
*/
public void insertCustomMenuItems(java.util.List<JMenuItem> menu) {
// Adds top-level "Custom Menu" -> "Employee Report"
customMenu = new JMenu("Custom Menu");
menu.add(5, customMenu); JMenuItem item = new JMenuItem("Employee Report");
customMenu.add(item); item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (employeesReport == null)
employeesReport = new EmployeesReport(client, serverExports); employeesReport.display();
}
});
} /** * Conditionally removes custom major and minor menu items. * This method should remove all menu items that were inserted by the * last call to {@link #insertCustomMenuItems} * * @param menu * the main menu list
*/
public void removeCustomMenuItems(List<JMenuItem> menu) {
menu.remove(customMenu);
}
}
Registration

The client-side plug-in is registered by specifying the fully qualified class name in the /security/config/extensions/client node of the configuration directory. In our case the registration entry looks like this:

<node class="string" name="client">
   <node-attribute name="value" value="com.company.admin.ClientExtension"/>
</node>

Rights Editors

Information on how to implement custom rights editor you can find in the “Security Resource and Rights Plug-Ins section”.

Implementing a Custom Server-Side API

The client side extension plug-in that provides additional functionality like handling account extension fields or some other custom entities, or various reporting, needs some backing services, or APIs, on the server. Those are custom extension APIs. They can be implemented and exported on the server side and accessed on client side, which must know the meaning of those APIs.

An API consists of two parts:

  • API declaration, an interface which is used on the client side for accessing functions of this API;
  • API implementation, an instance of the implementation class is created on the server side in order to handle requests from the client side.

In our case API interface is called CustomServerExports, the API implementation is CustomServerExportsImpl. Our API provides the following functions:

  • getAccountExtensions - returns extension data of the specific account, used in the account editing dialog.
  • getUserNameByEmployeeNumber - finds a user by employee number, used in the find user dialog.
  • getEmployeeAccounts - returns all accounts which have an employee number assigned, used in the employees report (this report is provided as an example in the “Custom Screens and Printing” section below).

CustomServerExports full listing:

package com.company.admin;

import com.goldencode.p2j.admin.*;

/**
 * Custom server-side API definition.
 */
public interface CustomServerExports
{
   /**
    * Get extension data of the specific account.
    *
    * @param  account
    *         Account which extension fields should be returned.
    *
    * @return extension data of the specified account or null if
    *         the account does not exist or is not extended.
*/
public CustomExtDef getAccountExtensions(String account); /** * Get user name by the employee number. * * @param empNum * Employee number. * * @return name of the user account which corresponds the specified employee * number or null if no such account exist.
*/
public String getUserNameByEmployeeNumber(Integer empNum); /** * Get all accounts which have an employee number assigned. * * @return list of accounts which have an employee number assigned, each * TaggedName contains user name, person and employee number.
*/
public TaggedName[] getEmployeeAccounts();
}

CustomServerExportsImpl full listing:

package com.company.server;

import com.company.admin.*;
import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.directory.*;

import java.lang.reflect.*;
import java.util.*;

/**
 * Custom server-side API implementation.
 */
public class CustomServerExportsImpl
implements CustomServerExports
{
   /**
    * Get extension data of the specific account.
    *
    * @param  account
    *         Account which extension fields should be returned.
    *
    * @return extension data of the specified account or null if
    *         the account does not exist or is not extended.
*/
public CustomExtDef getAccountExtensions(String account) {
DirectoryService ds = AdminServerImpl.getDirectoryService(); // check if the specified account exists
String node1 = "/security/accounts/users/" + account;
String node2 = "/security/accounts/processes/" + account; boolean isUser = ds.getNodeClass(node1) != null;
boolean isProcess = ds.getNodeClass(node2) != null; if (!isUser && !isProcess) {
return null;
} String node = isUser ? node1 : node2; // get the existing nodes
String[] nodes = ds.enumerateNodes(node);
if (nodes null || nodes.length 0) {
return null;
} Set<String> existing = new HashSet<String>(Arrays.asList(nodes)); CustomExtDef ext = new CustomExtDef(); // add the extension fields to the container
for (int i = 0; i < CustomExtDef.EXT_FIELDS.length; i++) {
String fieldNode = CustomExtDef.EXT_FIELDS[i][0];
String field = CustomExtDef.EXT_FIELDS[i][1]; // if the field is not defined, skip it
if (!existing.contains(fieldNode)) {
continue;
} StringBuilder extNode = new StringBuilder(node);
extNode.append("/");
extNode.append(fieldNode); try {
Field f = ext.getClass().getField(field); Attribute[] attrs = ds.getNodeAttributes(extNode.toString());
for (Attribute attr : attrs) {
if (attr.getName().equals("value")) {
Object val = attr.getValue(0); if (val != null) {
f.set(ext, val);
} break;
}
}
}
catch (Exception e) {
// send message to client
AdminServerImpl.message("error getting extension field " + fieldNode);
}
} return ext;
} /** * Get user name by the employee number. * * @param empNum * Employee number. * * @return name of the user account which corresponds the specified employee * number or null if no such account exist.
*/
public String getUserNameByEmployeeNumber(Integer empNum) {
TaggedName[] users = AdminServerImpl.listUsers();
if (users == null)
return null; for (TaggedName user : users) {
String userName = user.getName();
Integer num = (Integer) getAccountExtension("/security/accounts/users",
userName,
CustomExtDef.EXT_FIELDS0[0]);
if (empNum.equals(num))
return userName;
} return null;
} /** * Get all accounts which have an employee number assigned. * * @return list of accounts which have an employee number assigned, each * TaggedName contains user name, person and employee number.
*/
public TaggedName[] getEmployeeAccounts() {
TaggedName[] users = AdminServerImpl.listUsers();
if (users == null)
return null; List<TaggedName> res = new ArrayList<TaggedName>(); for (TaggedName user : users) {
String userName = user.getName();
Integer num = (Integer) getAccountExtension("/security/accounts/users",
userName,
CustomExtDef.EXT_FIELDS0[0]);
if (num != null)
res.add(new TaggedName(userName, user.getTag(), num.toString()));
} return res.toArray(new TaggedName[res.size()]);
} /** * Get specific extension of the specified account. * * @param basePath * Base directory path, e.g. "/security/accounts/users". * @param accountName * Account name. * @param extension * Extension name. * * @return Extension field value or null if there is no such * account or extension.
*/
private Object getAccountExtension(String basePath,
String accountName,
String extension) {
StringBuilder extNode = new StringBuilder(basePath);
extNode.append("/");
extNode.append(accountName);
extNode.append("/");
extNode.append(extension); DirectoryService ds = DirectoryService.getInstance();
Attribute[] attrs = ds.getNodeAttributes(extNode.toString()); if (attrs null)
return null; for (Attribute attr : attrs) {
if (attr.getName().equals("value")) {
return attr.getValue(0);
}
} return null;
}
}

An instance of an API implementation should be registered on server side using RemoteObject.registerNetworkServer function. A good place to do it is the initialize method of the server-side extension plug-in. In our case registration looks like this (CustomAccountExtension.java):

public static void initialize()
{
   ...
   // Register custom server exports.
   Class<?>[] ifaces = new Class[] { CustomServerExports.class };
   RemoteObject.registerNetworkServer(ifaces, new CustomServerExportsImpl());
}

You can find the full listing of CustomAccountExtension in the “Server Side Extension Plug-In” section.

On the client side you can get a proxy instance for accessing the server-side API implementation using RemoteObject.obtainNetworkInstance function. A good place to do it is the attach method of the client-side extension plug-in. In our case it looks lie this (ClientExtension.java):
public void attach(AdminClient client)
{
   ...
   serverExports =
         (CustomServerExports) RemoteObject.obtainNetworkInstance(CustomServerExports.class,
                                                                  client.getSession());
}

You can find the full listing of ClientExtension in the “Client-side Extension Plug-In” section.

Custom Screens and Printing

Screens

Some menu items work through modal dialogs, which are not persistent. Other items maintain some persistent GUI panels, which are called screens. The administration UI provides the way to switch between screens and preserve their state.

A screen is usually associated with a menu item. When the item is selected for the first time within the session, the event handler creates the screen and adds it to the switcher with some arbitrary name. Subsequent selections simply switch to the named screen. Screens are implemented as panels of a container managed using CardLayout.

Screens are added to the switcher by calling the AdminClient.add methods and can be switched by calling AdminClient.show. There is a special blank screen, AdminClient.SCREEN_BLANK, that can be switched to whenever the current screen has to be hidden.

Printing

Client screens have the capability to print the displayed tabular data. To print the currently displayed screen, use the “Print” top menu item which handles the print requests for all screens by delegating the request to a screen-specific processor that must have been registered.

Thus, printable screens have to register their own print request processors, which implement the PrintRequest interface. The interface declares the following functions:

  • print - prints the screen data;
  • canPrintExtended - queries whether the extended printing is implemented;
  • printExtended - performs extended printing.

The print processor may provide either just the regular printing, or both the regular and the extended printing. The regular printing may be sufficient for screens where there is not too much detail and all pieces of information fit a simple table. If this is not the case, then the regular printing means printing a subset of data items, which are deemed to be the most important. Then the administrator is given an option to get the extended printing. The latter is a differently formatted table, where a single item can take as many rows and cells as necessary to cover all data items.

When the “Print” menu item is called, the print function is executed. If you want to use extended printing you should create UI controls that call printExtended and use the value provided by canPrintExtended if necessary (i.e. despite the fact that printExtended and canPrintExtended are a part of the PrintRequest interface, they are not called by administration console by default if a screen was registered as a print target). However you can use the standard printing function AdminClient.printOn which displays the print preview panel and allows the switch to extended mode (printExtended is called). That preview panel also allows the user to confirm or cancel printing. Another standard function, AdminClient.printExtendedOn calls the standard extended print preview panel. Or you can implement your own printing workflow. At a low level, tables are printed using JTable.print.

A screen can be registered as a print target using AdminClient.add(JPanel, String, PrintRequest) function.

Printable Screen Example

We will implement the custom screen which is called “Employees Report” and displays all users which have an employee number assigned. The screen itself:

We will use standard means of printing (AdminClient.printOn and AdminClient.printExtendedOn). Print preview panel:

Extended print preview panel:

Report screen class listing:

package com.company.admin;

import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.admin.client.*;

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import java.util.*;

/**
 * Report which displays all users which have an employee number assigned.
 */
public class EmployeesReport
implements PrintRequest
{
   /**
    * Unique identifier for this screen.
    */
   private final static String EMPLOYEES_SCREEN = "employees_screen";

   /**
    * Column names of the data table.
    */
   private final static String[] COLUMN_NAMES =
                     new String[]{"User Name", "Person", "Employee Number"};

   /**
    * Column names of the data table of the extended report.
    */
   private final static String[] EXT_COLUMN_NAMES =
                     new String[]{"User Name", "Person", "Employee Number", "Home Directory"};

   /**
    * AdminClient reference.
    */
   private final AdminClient client;

   /**
    * Reference to custom server-side API.
    */
   private final CustomServerExports serverExports;

   /**
    * Main table.
    */
   private final JTable mainTable;

   /**
    * Creates and initializes the report screen.
    *
    * @param client
    *        AdminClient reference.
    * @param serverExports
    *        Reference to custom server-side API.
    */
   public EmployeesReport(AdminClient client, CustomServerExports serverExports)
   {
      this.client = client;
      this.serverExports = serverExports;

      mainTable = new JTable();
      mainTable.setEnabled(false);  // read-only
      JScrollPane scroller = new JScrollPane();
      scroller.getViewport().add(mainTable);

      JButton refreshButton = new JButton("Refresh");

      JPanel panel = AdminUtils.groupComponents(new Component[][]{{scroller},
                                                                  {refreshButton}});

      // Add the screen to admin UI and register it as a printable target.
      client.add(panel, EMPLOYEES_SCREEN, this);

      refreshButton.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent e)
         {
            refresh();
         }
      });
   }

   /**
    * Display the report screen and refresh the report.
    */
   public void display()
   {
      client.show(EMPLOYEES_SCREEN, "Employees Report");
      refresh();
   }

   /**
    * Refresh the list of employees.
    */
   public void refresh()
   {
      AdminUtils.busy(client);
      TaggedName[] users = serverExports.getEmployeeAccounts();

      Object[][] data;
      if (users == null || users.length == 0)
      {
         data = new Object[0][3];
      }
      else
      {
         data = new Object[users.length][3];
         for(int i = 0; i < users.length; i++)
         {
            data[i][0] = users[i].get(0);
            data[i][1] = users[i].get(1);
            data[i][2] = users[i].get(2);
         }
      }

      mainTable.setModel(new DefaultTableModel(data, COLUMN_NAMES));
      AdminUtils.idle(client);
   }

   /**
    * Performs printing of the current screen data.
    */
   public void print()
   {
      // clone main table
      DefaultTableModel model = (DefaultTableModel) mainTable.getModel();
      Vector data = (Vector) model.getDataVector().clone();

      DefaultTableModel newModel = new DefaultTableModel(data,
                                       new Vector<String>(Arrays.asList(COLUMN_NAMES)));
      JTable table = new JTable(newModel);
      table.setEnabled(false);

      client.printOn(table, table.getModel(),
            new MessageFormat("Employees Report"),
            new MessageFormat("page {0, number}"));
   }

   /**
    * Queries the extended printing capability. If the extended capability
    * is not provided, the printExtended() method should not be called.
    *
    * @return true if the extended printing is implemented
*/
public boolean canPrintExtended() {
return true;
} /** * Performs extended printing of the current screen data. * This method will only be used if {@link #canPrintExtended} returns * true.
*/
public void printExtended() {
// copy data from main table
DefaultTableModel model = (DefaultTableModel) mainTable.getModel();
Vector data = (Vector) model.getDataVector().clone(); // get home directories from the server
AdminUtils.busy(client);
for (Object rowData : data) {
Vector rowVector = (Vector) rowData;
String username = (String) rowVector.get(0);
CustomExtDef ext = serverExports.getAccountExtensions(username);
rowVector.add(ext.homeDir == null ? "" : ext.homeDir);
}
AdminUtils.idle(client); DefaultTableModel newModel = new DefaultTableModel(data,
new Vector<String>(Arrays.asList(EXT_COLUMN_NAMES)));
JTable table = new JTable(newModel);
table.setEnabled(false); client.printExtendedOn(table,
new MessageFormat("Employees Extended Report"),
new MessageFormat("page {0, number}"));
}
}

The screen is called using “Custom Menu” → “Employee Report” menu item. This item was added in ClientExtension:

private EmployeesReport employeesReport = null;
...
public void insertCustomMenuItems(java.util.List<JMenuItem> menu)
{
   // Adds top-level "Custom Menu" -> "Employee Report" 
   customMenu = new JMenu("Custom Menu");
   menu.add(5, customMenu);

   JMenuItem item = new JMenuItem("Employee Report");
   customMenu.add(item);

   item.addActionListener(new ActionListener()
   {
      public void actionPerformed(ActionEvent e)
      {
         if (employeesReport == null)
            employeesReport = new EmployeesReport(client, serverExports);

         employeesReport.display();
      }
   });
}

For more information about menu items and full ClientExtension listing see the “Client-side Extension Plug-in” section.

Database Triggers

Database triggers allow to react at specific database events, like record creation, update or deletion. It may be very useful for debugging purposes: you can track changes in a specific set of records. FWD contains a partial implementation of 4GL schema triggers. So far assign and delete triggers are implemented.

Assign triggers allow us to track changes of the specific fields of specific types of records. They are fired when a new value has been assigned to a specific record field. Note that the trigger will not be fired if the new value is the same as the old one. Also note that field validation is performed before the trigger will be invoked (so the trigger will not be fired if validation fails).

Delete triggers is fired when a record is about to be deleted.

The triggers are registered in the /server/<server_id>/database/<database_name>/p2j@/*schema_triggers* section of the FWD server directory (where <server_id> is the name of the server and <database_name>@ is the name of the target database, i.e. triggers are database-specific). Note that you can specify triggers for the temp database too.

Into this section you can specify:

  1. enable - boolean parameter which should be true in order to enable triggers for the given database (set it to false if you want to disable them).
  2. The list of container objects which represent triggers. The name of such container should equal the fully qualified package and class name of the trigger class (without any .java or .class suffix). Each trigger container has the following content:
    1. String parameter type which can have “assign” or “delete” value depending on the trigger type.
    2. The list of container objects which represent the target DMO interfaces on which the trigger should react. The name of such container should equal the fully qualified package and interface name (without any .java or .class suffix). For delete triggers this list is sufficient. For assign triggers you should specify for each DMO interface the strings parameter properties which represents the set of target fields (fields should be named in the same way as into the DMO implementation class).

An example follows:

<node class="container" name="schema_triggers">
   <node class="boolean" name="enable">
      <node-attribute name="value" value="TRUE"/>
   </node>
   <node class="container" name="com.acme.corp.SomeAssignTrigger">
      <node class="string" name="type">
         <node-attribute name="value" value="assign"/>
      </node>
      <node class="container" name="targets">
         <node class="container" name="com.acme.corp.dmo.SomeRecord">
            <node class="strings" name="properties">
               <node-attribute name="values" value="someField"/>
               <node-attribute name="values" value="anotherField"/>
            </node>
         </node>
         <node class="container" name="com.acme.corp.dmo.AnotherRecord">
            <node class="strings" name="properties">
               <node-attribute name="values" value="yetAnotherField"/>
            </node>
         </node>
      </node>
   </node>
   <node class="container" name="com.acme.corp.SomeDeleteTrigger">
      <node class="string" name="type">
         <node-attribute name="value" value="delete"/>
      </node>
      <node class="container" name="targets">
         <node class="container" name="com.acme.corp.dmo.SomeRecord"/>
         <node class="container" name="com.acme.corp.dmo.YetAnotherRecord"/>
      </node>
   </node>
</node>

Assign triggers should implement com.goldencode.p2j.persist.trigger.AssignTrigger interface. When assign event is fired then the execute(DataModelObject dmo, BaseDataType newFieldValue, BaseDataType oldFieldValue) is called. dmo parameter represents the DMO that has been changed (with the new value applied); newFieldValue was added for 4GL-compatibility, isn't used so far; oldFieldValue contains the old value of the modified field.

Delete triggers should implement com.goldencode.p2j.persist.trigger.DeleteTrigger interface. When delete event is fired then the execute (DataModelObject dmo) is called. dmo parameter represents the DMO that is about to be deleted.

An appropriate trigger is instantiated each time it should be called. A trigger class must have a default constructor (a constructor that takes no parameters) so that the FWD server can instantiate it. An error will occur otherwise.

The call of a trigger occur on the same thread, the code has the security context of the corresponding user/process. There is no guaranteed sequence of calls for several triggers designated to the same DMO type as well as there is no guaranteed sequence of calls of the assign trigger(s) designated for several fields that were assigned in a single batch.

You shouldn't use FWD blocks (externalProcedure, doBlock, etc.) inside triggers. However you can throw ConditionException*s (*ErrorConditionException probably is the most useful for this case) - that will cancel assignment/deletion and condition will be propagated upward in a usual way.

Consider an example. We have some problems with the ItemLocation.qtyOnHand property which is improperly assigned. To find out the cause of misbehavior we should log all assignments of the qtyOnHand field and deletions of ItemLocation objects. In order to do that we will create an assign trigger (which reacts on changes into qtyOnHand only) and a delete trigger. In order to get maximum of useful information we will log ItemLocation content (we are interested only in a specific subset of ItemLocation objects, so we will perform some filtering), old qtyOnHand value (for assign trigger), thread name (will reveal the user name as well), and stacktrace information (only useful stacktrace elements).

Sample assign trigger:

public class ItemLocationAssignTrigger
implements AssignTrigger
{
   private static final Log LOG = LogFactory.getLog(ItemLocationAssignTrigger.class);

   static final String LINE_SEP = System.getProperty("line.separator");

   public void execute(DataModelObject dmo,
                       BaseDataType newFieldValue,
                       BaseDataType oldFieldValue)
   throws ConditionException
   {
      if (dmo != null)
      {
         character item = ((Item) dmo).getItem();
         if (item != null && item.getValue().startsWith("T")) // tools only
         {
            StringBuilder sb = new StringBuilder();
            sb.append("ItemLocation qtyOnHand assignment (old value ");
            sb.append(oldFieldValue);
            sb.append("). Buffer: ");
            sb.append(LINE_SEP);
            sb.append(dmo);
            sb.append("Thread: ");
            sb.append(Thread.currentThread().getName());
            sb.append(LINE_SEP);
            sb.append("Stacktrace: ");
            sb.append(LINE_SEP);
            sb.append(getFilteredStackTrace());
            LOG.info(sb);
         }
      }
   }

   /**
    * Returns filtered stacktrace representation which contains only com.acme.* entries.
    */
   private String getFilteredStackTrace()
   {
      StringBuilder sb = new StringBuilder();
      StackTraceElement[] els = new Exception().getStackTrace();

      for(StackTraceElement el : els)
      {
         String className = el.getClassName();
         if (className.startsWith("com.acme."))
         {
            sb.append(className);
            sb.append(": ");
            sb.append(el.getLineNumber());
            sb.append(LINE_SEP);
         }
      }

      return sb.toString();
   }
}

Sample delete trigger:

public class ItemLocationDeleteTrigger
implements DeleteTrigger
{
   private static final Log LOG = LogFactory.getLog(ItemLocationDeleteTrigger.class);

   static final String LINE_SEP = System.getProperty("line.separator");

   public void execute(DataModelObject dmo) throws ConditionException
   {
      if (dmo != null)
      {
         character item = ((ItemLocation) dmo).getItem();
         if (item != null && item.getValue().startsWith("T")) // tools only
         {
            StringBuilder sb = new StringBuilder();
            sb.append("ItemLocation deletion. Buffer: ");
            sb.append(ItemLocationAssignTrigger.LINE_SEP);
            sb.append(dmo);
            sb.append("Thread: ");
            sb.append(Thread.currentThread().getName());
            sb.append(ItemLocationAssignTrigger.LINE_SEP);
            sb.append("Stacktrace: ");
            sb.append(LINE_SEP);
            sb.append(getFilteredStackTrace());
            LOG.info(sb);
         }
      }
   }

   /**
    * Returns filtered stacktrace representation which contains only com.acme.* entries.
    */
   private String getFilteredStackTrace()
   {
      StringBuilder sb = new StringBuilder();
      StackTraceElement[] els = new Exception().getStackTrace();

      for(StackTraceElement el : els)
      {
         String className = el.getClassName();
         if (className.startsWith("com.acme."))
         {
            sb.append(className);
            sb.append(": ");
            sb.append(el.getLineNumber());
            sb.append(LINE_SEP);
         }
      }

      return sb.toString();
   }
}

© 2004-2017 Golden Code Development Corporation. ALL RIGHTS RESERVED.