FRAMEWORK 101: Building XCF – Lesson 01 – Building the Core

September 24th, 2006

In this course of articles you will build a technology stack neutral framework that you can use as the basis of a variety of java based projects. The first question to ask is does the world need another java framework? The answer, of course, is no. So why build it? First and foremost, a framework is a complex application. Building one is an excellent opportunity to learn about common design patterns and on how to apply those design patterns. Furthermore, by building this framework not only will you understand better how to translate design ideas into implementation, you will also gain insight into how decisions translate to code that is either hard or easy to maintain down the road. But the most important reason is that when it comes down to it, we are a bunch of propeller heads and what can be more fun that building a framework?

Course outline:

NOTE: You can download the source code for today’s lesson here: Lesson 01 Source Code

What you will be building today

Today, you will build the core of the framework. We will be building this framework in layers. The layer we build today provides three foundations for the rest of the framework: the primary interfaces used to keep components decoupled, the logging facility, and the system facade. This facade is the object that wraps subsystems written in the framework. All functionallity is accessed via this facade (but more on that shortly). When you are done, you will test your framework by sending a login request to your facade.


  REQUEST_SimpleTest request = new REQUEST_SimpleTest("account", "login");
  request.setParameter("userName", "myloginname");
  request.setParameter("password", "mypassword");
  facade.process(request);

Design by path of least resistance

Here is a fact you can bank on. No matter how much documentation you put together, no matter how many examples. No matter how much you plan out how peope should use your framework, in the end, the developers using your framework will follow the path of least resistance. So the key to designing a good framework is to make the path of least resistance the right thing to do. Now, that doesn’t let you off the hook for the documentation and examples, it just means be aware that just those things are not enough.

Path of least resistance means limiting the number of things the developer needs to remember. When faced with a software problem, a typical developer will look how a similar problem was solved and copy that — many times literally copy and pasting the code. If your developers have good discipline, this could be a good thing as good practice will get copied. Consequently, one poor implementation, based on the same principle, can find itself quickly spreading through your systems like a disease. Leverage the fact that people will look at other code for instruction. Combine that with only requiring a small number of practices, and you will increase the chance that developers will not only model their code after the proper principles, but also correct those things that drift from them.

Path of least resistance means coming up with intuitive conventions. Most people won’t know all your conventions. But if you are consistent, developers (who will typically guess before looking up) will tend to guess correctly.

Path of least resistance means respecting DRY (don’t repeat yourself). Avoid systems that require duplicate definitions that need to be kept in synch.

What is XCF?

I switched to Java as my primary language in late ’96. At the time, Java 1.01 was not yet a year old. There was no swing, no servlets, just this cool language with a great set of object oriented libraries. At the time, people thought java was going to be about writing applets and client side applications. But then, at JavaOne in June of ’97, the Servlet specification was introduced and Java found its home — the server. While it can function on the client, it did not prove as portable as people had hoped as the physical look varied from os to os. But on the server, Java really hit a stride.

And this is where XCF comes in. From the beginning, it was clear to me that application technology stacks were going to be in flux for several years to come. In those early days, before servlets, I had to write 2-tiered applications. My client applications communicated with the database backends directly. Applications were build visually and user interface code was generated. Later, when servlets arrived, I needed application to become 3-tiered. I had to move the model logic to the server. Then, when thick clients fell out of favor, I needed to move view and controller logic to the server as well. Around ’99 the EBJ pattern of the day specs started coming out and I had to buffer my applications from the ever changing methods of object to relational mappings. It was also around that time that I started seeing formalized technology stacks supported by frameworks (i.e. Struts).

The problem was, during all this time, I was building systems for clients. I needed to be able to migrate these systems from one technology stack to another with a minimum amount ripple. I needed to be able to leverage new libraries and frameworks without asking my clients to rewrite what they had. From that need emerged the eXtensible Command Framework (XCF — with a name like that you know I don’t have a marketing degree).

The idea was to write my applications in a technology stack neutral framework. That way, if I had to change tiers, or move application logic from one tier to another (i.e. from client side to server side), I could do so without having to re-implement. I could changing the underlying Object to relational mapping tool (ORM) from EBJ to JDO to Hibernate. I could write the application as a plug-in to a reporting engine and then later reconfigure it as a stand-alone web application. As long as I wrote inside the XCF framework, I could adapt my code to the ever changing world of Java increasing my chances of being able to leverage best of bread technologies.

This has been no simple task. This course will actually build the sixth version of XCF (the first version was completed in 2001). Since its inception, over a dozen engineers have contributed either via code or design. It has been used in over twenty projects. Each project helped refine the framework. The technology stack for those applications has been varied and include:

  • single tier application
  • client-server application
  • classic web application
  • web-app with a thick client using synchronous and asynchronous communication
  • messaging bus used to route messages to partners (e.g. fedex and ups)
  • plugin engine to a reporting framework
  • simple tcp/ip server application

In working on these projects and with all these technology stacks, I discoverd an architectural pattern which was mappable to all of these technology stacks. I do not claim it is the best pattern, but it is one that is abstract enough to be neutral. All XCF applications adhere to the following pattern:

We will revisit this pattern in a later lesson in more detail. For now, just know that at its core, XCF is a request based pattern. Specifically, clients of subsystems written in XCF invoke functionality by creating requests and sending those requests to a facade for processing. The facade does the work of finding the subsystem that handles the request and marshaling that request to the request handler. And speaking of the facade…

The Facade

The primary encapsulation method used by XCF is the facade pattern. All functionality is wrapped inside the facade and invoke via the facade.

The intent of the facade is to provide a unified interface to a set of interfaces in a subsystem. A facade turns a bunch of heterogeneous objects into a component, by putting the interface of interaction with those objects on one object, the facade.

We use a facade for these two reasons:

  • to decouple the subsystem from the clients and other subsystems, thereby promoting subsystem independence and portability.
  • to define an entry point to each subsystem level. Dependencies between subsystem are simplified because the subsystems communicate with each other solely through the facade.

The facade is responsible for:

  • knowing which subsystem classes are responsible for a request.
  • delegating client requests to appropriate subsystem objects.

The implentation for the XCFFacade is:


package com.eternal.xcf.core;

import java.util.HashMap;
import java.util.Iterator;


/**
 * XCFFacade
* Implements the system facade. The facade contains all the services and modules. * It triggers all the startup and shutdown functionality. * It routes requests to the appropriate modules for processing. * Module or service functionality may use the facade in processing requests or services. *
* The facade also contains helper methods for logging. * */ public final class XCFFacade { /** * XCF_SERVER_TAG: Use this tag as the key any time you are going to * put the object in a hash. */ public static String XCF_TAG = "xcf-server"; private boolean configurationValid = true; private HashMap modules = new HashMap(); private HashMap services = new HashMap(); private XCFLogManager logManager; /** * XCFServer Constructor * For now, it creates the two requiered services: LogManager and DataElementManager. * This should be modified to support the service pattern. */ public XCFFacade() { setLogManager(new XCFLogManager()); } /** * Gets the logManager * @return Returns a LogManager */ public XCFLogManager getLogManager() { return logManager; } /** * Sets the logManager * @param logManager The logManager to set */ public void setLogManager(XCFLogManager logManager) { this.logManager = logManager; } /** * Gets the configurationValid. If true, assume that the facade was constructed successfully * @return Returns true if the facade was contructed successfully */ public boolean getConfigurationValid() { return configurationValid; } /** * Sets the configurationValid. Call this after constructing the facade. * @param configurationValid The configurationValid to set */ public void setConfigurationValid(boolean configurationValid) { this.configurationValid = configurationValid; } /** * Adds a module to the facade..
* * @param String path the string name of the module. * This name is used by server clients to identify the module. * @param XCFModule module the module to add */ public void putModule(String path, XCFModule module) { module.setFacade(this); modules.put(path, module); } /** * Gets a module from the facade. Modules are used to handle requests. * * @param String path the string name of the module. * This name is used by facade clients to identify the module. * @return XCFModule the module bound to the path. */ public XCFModule getModule(String path) { return (XCFModule)modules.get(path); } /** * Gets the modules * @return Returns a HashMap */ public HashMap getModules() { return modules; } /** * Adds a service the the facade.
Services are accessed by module handlers * and other services and model functionality whose state is available across the lifecycle of the facade. * * @param String serviceName the string name of the service. * @param XCFService service the service to add. */ public void putService(String serviceName, XCFService service) throws XCFException { service.setFacade(this); services.put(serviceName, service); service.start(); logInfo("STARTED SERVICE " + serviceName + " USING " + service.getClass().getName()); } /** * Gets a service from the facade.
* * @param String serviceName the string name of the service. * @return XCFService the service bound to the name. */ public XCFService getService(String serviceName) { return services.get(serviceName); } /** * Shuts down all the server modules and services. */ public void shutdown() throws XCFException { boolean errors = false; StringBuffer msgs = null; // stop all the services Iterator iter = services.values().iterator(); while (iter.hasNext()) { XCFService service = (XCFService)iter.next(); try { service.stop(); } catch (XCFException e) { if (msgs == null) { msgs = new StringBuffer(); } msgs.append(e.getMessage()); msgs.append("\\n"); errors = true; } } // shutdown the log manager logManager.stop(); if (errors) { throw new XCFException(msgs.toString()); } } /** * Processes the XCF request.
* * @param XCFRequest req the request to process. */ public void process(XCFRequest req) throws XCFException { if (req.getModule() == null) { throw new XCFException("request's module not set."); } XCFModule module = getModule(req.getModule()); if (module == null) { throw new XCFException("module '" + req.getModule() + "' not found"); } module.process(req); } //////////////////////////////////////// // HELPER METHODS FOR LOGGING /** * Log an error message */ public void logError(String msg) { logManager.log(null, XCFLogger.LogTypes.ERROR, msg); } /** * Log an error exception * @param e */ public void logError(Exception e) { logManager.log(null, XCFLogger.LogTypes.ERROR, e); } /** * Log an info message * @param msg */ public void logInfo(String msg) { logManager.log(null, XCFLogger.LogTypes.INFO, msg); } /** * Log a debug message * @param msg */ public void logDebug(String msg) { logManager.log(null, XCFLogger.LogTypes.DEBUG, msg); } /** * Log a debug exception * @param e */ public void logDebug(Exception e) { logManager.log(null, XCFLogger.LogTypes.DEBUG, e); } }

The job of the facade is simple:

  • to act as a container for services and modules
  • to provide access to the service and modules
  • to provide a single request processing function (process)
  • to provide access to logging functionality

The most important method above is the process method. Interestingly, you will not actually have to invoke this method often as that will typically be done by support objects (such as the listener which we will cover in detail in a future lesson). But it is important as that one method is the primary point of access to sub-system functionality.

The other point of access to the sub-system functionality is the getService method. This method is intended to be used by sub-system objects vs. clients of the sub-system. The reason is that getService always returns a local object. The process method, on the other hand, may marshall the request to another physical machine or server.

The other methods of note here are the logging methods. They are put here as a convienience the facade object is the most accessible object in the entire system.

Exceptions

Exceptions are important, but from a design point of view, dangerous classes. The danger comes from the coupling they introduce. Sub-system clients are required to catch (or throw) any exception that might be thrown by the sub-system. For that reason, only introduce new exception classes if your sub-system needs to differentiate how errors are handled. The class XCFException is the root exception class in XCF (and likely the main one you will use). When you catch an XCFException, it is intended that you log the error. You will only need to introduce new exceptions if you have specific error conditions which are recoverable. In my six years of using XCF, I have not needed to subclass XCFException. The implementation is a simple wrapper over Exception:


package com.eternal.xcf.core;

/**
 * Base exception class for the xcf system
 * 
 * Creation date: (9/13/2001 10:36:59 AM)
 * @author: Sonjaya Tandon
 */
public class XCFException extends Exception {
	private static final long serialVersionUID = 5674131086115249254L;
	
	/**
	 * Constsruct an XCFException using the message passed in.
	 * @param s java.lang.String
	 */
	public XCFException(String s) {
		super(s);
	}

	/**
	 * Constsruct an XCFException using the message passed in. Retain the cause.
	 * 
	 * @param original
	 * @param msg
	 */
	public XCFException(Exception original, String msg) {
		super(msg);
		super.initCause(original);
	}
	
}

Abstracting the Request

As the goal is to create a technology stack neutral framework, it is critical that we create an abstraction that models sub-system requests. Clients of the sub-sysystems will invoke functionality via these request objects and use the facade to process the requests. The facade will locate the handler, whether that handler is on the same machine, written using a different stack, or on an entirely different machine. Also, because sub-system clients only deal with abstracted requests, the implementation of the sub-system can change 100% and not impact the sub-system client.

I have organized the methods into common methods, rare methods, and dangerous methods. See the comments in the code for a description of the three types of methods:


package com.eternal.xcf.core;

import java.util.Iterator;

/**
 * Abstract interface used to model requests sent to the facade for processing.  Concrete implementations
 * of this interface are typically technology stack dependant.  For example, an HTTPServletRequest based implementation
 * will adapt the HTTServletRequest interface to XCFRequest.  This allows XCF based classes to access the information in 
 * the HTTPServletRequest without being coupled to the web application technology stack.  Later, if the technology stack 
 * changes, the only code that needs changing is the code that instantiates the concrete request object (typically a 
 * framework level class).  
 * @author sonjaya
 *
 */
public interface XCFRequest 
{
	//allowing in XML for different processes to use different requests
	String XCF_TAG = "xcfrequest";
	
	/////////////////////////////////////////////////////////
	// COMMON METHODS
	//  You will use these ones a lot.
	/**
	 * Get a parameter value
	 * @param paramName named used to lookup parameter value
	 * @return parameter value
	 */
	String getParameter(String paramName);

	/**
	 * Set a string parameter value using a string as a key
	 * @param paramName  named used to lookup parameter value
	 * @param paramValue value of the parameter
	 * @return
	 */
	void setParameter(String paramName, String paramValue);

	/**
	 * Returns the requests context
	 * @return
	 */
	XCFContext getContext();
	
	/////////////////////////////////////////////////////////
	// RARE METHODS
	//  These methods are for more advanced uses.  If you 
	//  find yourself using them often, consider refactoring
	//  as you are likely in danger of breaking cohesion 
	/**
	 * Returns an enumeration of parameter names 
	 */
	Iterator getParameterNames();
	
	/**
	 * Sets the request's context.  This is typically set by the object
	 * that creates the concrete request object
	 * @param context
	 */
	void setContext(XCFContext context);	

	/**
	 * Sets the name of the module that should handle this request.  This is typically set by the object
	 * that creates the concrete request object.
	 * @param module
	 */
	void setModule(String module);
	
	/**
	 * Returns the name of the module that should handle this request.
	 * @return
	 */
	String getModule();
	
	/**
	 * Returns the module operation requested.
	 * @return
	 */
	String getOperation();
	
	/**
	 * Sets the module operation that should be performed. This is typically set by the object
	 * that creates the concrete request object.
	 * @param operation
	 */
	void setOperation(String operation);
	
	/**
	 * Adds a notice message to the request.  A notice message is typically an error message delivered
	 * back to the request invoker.  The error is usually with parameter processing, but could also be
	 * due to issues with the request handling.
	 * @param message
	 */
	void addNotice(String message);
	
	/**
	 * Clears all the notice messages.
	 *
	 */
	void clearNotice();
	
	/**
	 * Returns an iterator over the notice messages.
	 * @return
	 */
	Iterator getNotices();

	/////////////////////////////////////////////////////////
	// DANGEROUS METHODS
	//  These methods are for the most advanced uses.  Using
	//  these methods BREAKS coupling.  The code that uses 
	//  these methods will be tightly coupled to a specific
	//  technology stack.  If the technology stack changes
	//  objects that use these methods will need to be 
	//  re-written or replaced.
	//
	//  So, the question is, why do we even have these methods.  Glad you asked!
	//  To leverage the underlying technology stack, you will need to create adaptive
	//  classes that map the technology stack specific methods to the technology
	//  stack neutral XCF interfaces.  It is likely that these adaptive classes
	//  will need to make use of the methods below to talk to each other.
	/**
	 * Sets the technology stack specific request object.  For example, this may be
	 * the HTTPServletRequest object.
	 * @param req
	 */
	void setNativeRequest(Object req);
	
	/**
	 * Gets the technology stack specific request object.
	 * @return
	 */
	Object getNativeRequest();

	/**
	 * Sets the technology stack specific response object.  For example, this may be
	 * the HTTPServletResponse object
	 * @param resp
	 */
	void setNativeResponse(Object resp);
	
	/**
	 * Gets the technology stack specific response object.
	 * @return
	 */
	Object getNativeResponse();

	/**
	 * Sets the native listener object.  This is the object that listens for requests and sends responses.
	 * For example, this could be the HTTPServlet.
	 * @param listener
	 */
	void setNativeListener(Object listener);
	
	/**
	 * Gets the native listener object.
	 * @return
	 */
	Object getNativeListener();
	
}

Persisting state across requests

If you examine the XCFRequest interface you will see a getter and setter for a context. The context object is a blackboard style objects that request handlers write. Thus, if a request is part of a sequence of requests, handlers can communicate to each other by modifying the state of the context. This allows concreate hanlder classes to be decoupled from each other.


package com.eternal.xcf.core;

public interface XCFContext {
		
	/**
	 * Puts a value in the dimension at the address specified.  This method will overrite
	 * any value already in the dimension.
	 * @param address identifies a dimension
	 * @param value an object to store in the address
	 */
	public void putValue(String address, Object value) throws XCFException;

	/**
	 * Returns the value stored in the dimension referenced by address.
	 * Creation date: (10/8/2001 2:27:11 PM)
	 * @return a value stored in a dimension.
	 * @param address java.lang.String an address of a dimension.
	 */
	public Object getValue(String address) throws XCFException;
	
	/**
	 * Gets the system facade.  The facade provides a decoupled interface
	 * to the rest of the system.
	 * @return
	 * @throws XCFException
	 */
	public XCFFacade getFacade() throws XCFException;
	
	/**
	 * Setter for the facade.  This should be set when the context object is created.
	 * @param facade
	 */
	public void setFacade(XCFFacade facade);

}

The key functionality that context objects provide is the ability to put and get values using string tags. The other important function they provide is providing access to the facade object.

Abstracting Logging

So, with log4j, why on earth did I need to abstract logging. Well, for one, log4j didn’t exist when I wrote the logging abstraction. The other reason is that the XCF logger allows for more business level logging. It is also easy to adapt to things like log4j (as has been done with a few of the XCF projects). By writing to the XCF abstracted loggers, the door stays open to leverage a variety of logging systems. The log types defined are the same as the ones that log4j offers with one addition: REPORT. Use the REPORT log type when you want to log a message that is supposed to be displayed to the end user.

The biggest difference you will see is the inclusion of the XCFContext object in the logging API. Most of the time, this object will be null. However, if you have a business level logger which needs to log audit information, the inclusion of the context object becomes critical.


package com.eternal.xcf.core;

/**
 * XCF Logger interface.  This interface provides a higher lever
 * abstraction over the basic logging functionality.  As a result,
 * loggers can be attributed with business functionality, but still kept
 * decoupled from other components.
 * 
* In general, you should not access this interface directly. The most common way * of accessing logging functionality is via the helper methods in XCFFacade * * Creation date: (11/21/2001 11:25:58 AM) */ public interface XCFLogger { enum LogTypes {TRACE, DEBUG, INFO, WARN, ERROR, FATAL, REPORT}; static final String LOG_AS_MESSAGE = "message"; static final String LOG_TO_CONSOLE = "console"; static final String LOG_NONE = "none"; static final String LOG_TO_FILE = "file"; String LOG_PAD = ":::"; /** * Opens the logger * @throws XCFException */ public void open() throws XCFException; /** * Closes the logger * @throws XCFException */ public void close() throws XCFException; /** * Log the exception * @param context concrete loggers can use this to extract additional info. NOTE: context may be null * @param logType * @param e */ void logException(XCFContext context, LogTypes logType, Exception e); /** * Log the message * @param context * @param logType * @param message */ void logMessage(XCFContext context, LogTypes logType, String message); }

Like log4j, XCF also provides a log manager. Like log4j, you can log using the type to identify the specific logger. You can also get a logger by its registered name. You will only need to use that mechanism for business level logging (like the audit logging example mentioned earlier). Most of the time, you will not invoke the log manager directly, rather you will make use of the helper methods in the facade.


package com.eternal.xcf.core;

import java.util.HashMap;
import java.util.Iterator;

import com.eternal.xcf.core.loggers.LOGGER_Console;
import com.eternal.xcf.core.loggers.LOGGER_Null;

/**
 * Log manager class that provides access to all facade loggers.  You will not typically call this class directly
 * Instead, you will usually use the helper methods in XCFFacade.
 * Creation date: (11/21/2001 10:53:27 AM)
 */
public final class XCFLogManager {
	private HashMap registeredLoggers = new HashMap();
	private HashMap activeLoggers = new HashMap();

	/**
	 * LogManager constructor comment.
	 */
	public XCFLogManager() {
		super();
		
		// register the two default loggers: none and console
		try {
			registerLogger(XCFLogger.LOG_NONE, LOGGER_Null.s());
			registerLogger(XCFLogger.LOG_TO_CONSOLE, LOGGER_Console.s());
		} catch (XCFException e) {/*these won't fail because they have empty open methods*/}
		
		// default ERROR and INFO to log to the console, turn off the others
		activeLoggers.put(XCFLogger.LogTypes.ERROR, LOGGER_Console.s());
		activeLoggers.put(XCFLogger.LogTypes.FATAL, LOGGER_Console.s());
		activeLoggers.put(XCFLogger.LogTypes.INFO, LOGGER_Console.s());
		activeLoggers.put(XCFLogger.LogTypes.REPORT, LOGGER_Null.s());
		activeLoggers.put(XCFLogger.LogTypes.TRACE, LOGGER_Null.s());
		activeLoggers.put(XCFLogger.LogTypes.DEBUG, LOGGER_Null.s());
		activeLoggers.put(XCFLogger.LogTypes.WARN, LOGGER_Null.s());
	}
	
	/**
	 * Registers a named logger to the manager.  All loggers must be registered prior to being used.
	 * @param loggerName
	 * @param logger
	 * @throws XCFException
	 */
	public void registerLogger(String loggerName, XCFLogger logger) throws XCFException {
		logger.open();
		registeredLoggers.put(loggerName, logger);
	}
	
	/**
	 * Returns the logger currently bound to a logging type.  Should never return null.
	 * Creation date: (11/21/2001 2:04:20 PM)
	 * @return XCFLogger
	 * @param loggerName java.lang.String
	 */
	public XCFLogger getLogger(XCFLogger.LogTypes logType) {
		return activeLoggers.get(logType);
	}
	
	/**
	 * Returns a logger registered using the name passed in.  Throws an exception if the logger doesn't exist.
	 * Creation date: (11/21/2001 2:04:20 PM)
	 * @param loggerName java.lang.String
	 * @return XCFLogger
	 * @throws XCFException if loggerName not registered.
	 */
	public XCFLogger getLogger(String loggerName) throws XCFException {
		XCFLogger logger = registeredLoggers.get(loggerName);
		if (logger == null) throw new XCFException(loggerName + " is not a registered logger.");
		return registeredLoggers.get(loggerName);
	}
	
	/**
	 * Binds a logger to a log type
	 * Creation date: (11/21/2001 11:55:31 AM)
	 * @param logType XCFLogger.LogTypes
	 * @param log XCFLogger
	 */
	public void setLogger(XCFLogger.LogTypes logType, XCFLogger log) {
		if (log != null) {
			activeLoggers.put(logType, log);
		}
	}
	
	/**
	 * Binds the logger referenced by loggerName to the logType.
	 * @param logType
	 * @param loggerName
	 * @throws XCFException
	 */
	public void setLogger(XCFLogger.LogTypes logType, String loggerName) throws XCFException {
		XCFLogger logger = getLogger(loggerName);
		setLogger(logType, logger);
	}
	
	/**
	 * Logs the exception.
	 * Creation date: (11/21/2001 11:53:03 AM)
	 * @param logType int
	 * @param message java.lang.String
	 */
	public void log(XCFContext context, XCFLogger.LogTypes logType, Exception e) {
		activeLoggers.get(logType).logException(context, logType, e);
	}
	
	/**
	 * Logs the message.
	 * Creation date: (11/21/2001 11:53:03 AM)
	 * @param logType int
	 * @param message java.lang.String
	 */
	public void log(XCFContext context, XCFLogger.LogTypes logType, String message) {
		activeLoggers.get(logType).logMessage(context, logType, message);
	}
	
	/**
	 * Closes all registered loggers.
	 *
	 */
	public void stop() {
		Iterator iter =  registeredLoggers.values().iterator();
		while (iter.hasNext()) {
			XCFLogger logger = iter.next();
			try {
				logger.close();
			} catch  (XCFException e) {
				System.out.println("Unable to close: " + logger);
				e.printStackTrace();
			}
		}
	}
}

Here is an example of a concrete logger. This logger logs to files:


package com.eternal.xcf.core.loggers;

import java.io.PrintWriter;
import java.io.IOException;
import java.text.DateFormat;

import java.io.File;
import java.io.FileWriter;

import com.eternal.xcf.core.XCFContext;
import com.eternal.xcf.core.XCFException;
import com.eternal.xcf.core.XCFLogger;

/**
 * Logger that will log to a physical file.
 * 
 * Creation date: (11/21/2001 12:02:09 PM)
 */
public class LOGGER_File implements XCFLogger {
	private File logFile = null;
	private DateFormat dateFormatter = DateFormat.getDateTimeInstance();
	private FileWriter writer = null;
	private PrintWriter pwriter = null;

	/**
	 * Constructs a file logger
	 */
	public LOGGER_File() {
		super();
	}
	/**
	 * Return the log file File object
	 * @return java.io.File
	 */
	public java.io.File getLogFile() {
		return logFile;
	}

	/**
	 * Logs the exception to the file.  Prepends message with logtype, timestamp, and thread id.
	 *  @param context unused
	 *  @param logType 
	 *  @param exception
	 */
	public void logException(XCFContext context, LogTypes logType, java.lang.Exception e) {
	
		if (writer == null) {
			LOGGER_Console.s().logException(context, logType, e);
			return;
		}
		
		synchronized (writer) {
				
		    pwriter.flush();
		    pwriter.write("\n");
		    pwriter.write(logType.toString());
	   	    pwriter.write(LOG_PAD);
			pwriter.write(dateFormatter.format(new java.util.Date(System.currentTimeMillis())));
		    pwriter.write(LOG_PAD);
		    pwriter.write(getThreadId());
		    pwriter.write("  ");
	
	    	e.printStackTrace(pwriter);
		    	
			pwriter.flush();
		}
	}
	
	/**
	 * Logs the exception to the file.  Prepends message with logtype, timestamp, and thread id.
	 * Creation date: (11/21/2001 12:02:09 PM)
	 * @param message java.lang.String
	 */
	public void logMessage(XCFContext context, LogTypes logType, String message) {
	
		if (writer == null) {
			LOGGER_Console.s().logMessage(context, logType, message);
			return;
		}
		
		synchronized (writer) {	
		    pwriter.flush();
	
		    pwriter.println();
		    pwriter.write(logType.toString());
		    pwriter.write(LOG_PAD);
			pwriter.write(dateFormatter.format(new java.util.Date(System.currentTimeMillis())));
		    pwriter.write(LOG_PAD);
		    pwriter.write(getThreadId());
		    pwriter.write(LOG_PAD);
	    	pwriter.write(message);
	   	
			pwriter.flush();
		}
	}
		
	/**
	 * Sets the log file name and creates the File object over that file.
	 * This will also create the writer's needed.
	 * Creation date: (11/26/2001 12:25:39 PM)
	 * @param newLogFile java.io.File
	 */
	public void setLogFile(String logFileName) {
		if (logFile != null) return;
		
		logFile = new File(logFileName);
	
	}
	
	/**
	 * Return the thread id
	 * Creation date: (11/21/2001 3:07:44 PM)
	 * @return java.lang.String
	 */
	
	protected String getThreadId()
	{
		String threadName = Thread.currentThread().getName();
	
	    if (threadName == null || threadName.length() == 0)
	    {
	        threadName = "THREAD_UNKNOWN";
	    }
	    
		return threadName;	
	}
	
	/**
	 * Opens the resources necessary to log to a file.
	 */
	public void open() throws XCFException {
		if (logFile == null) throw new XCFException("No log file set for LOGGER_File");
		try {
			writer = new FileWriter(logFile);
			pwriter = new PrintWriter(writer);
			logMessage(null, LogTypes.INFO, "Starting Log");	
		} catch (IOException e1) {
			System.out.println("The log file: " + logFile.getAbsolutePath() + " is not a valid file name.");
			e1.printStackTrace();
			logFile = null;
		}
	}

	/**
	 * Closes the file logger resources.
	 */
	public void close() throws XCFException {
		try {
			if (writer != null) writer.flush();
			writer.close();
		} catch (IOException e) {}
		if (pwriter != null) pwriter.close();
	}
	
	/**
	 * Return the name of this logger
	 */
	public String toString() {
		return LOG_TO_FILE;
	}
	
}

Abstracting Request Handling

I will go into a lot more detail in lesson 2 on this subject. The key decoupling feature XCF provides is its request processing mechanism. XCFModule is an important part of that mechanism. It is a very simple interface that follows a simple model. Sub-system clients request functionality by providing a request for a module and operation of that module. These requests are submitted to a facade. The facade will match the request to a module and forward the request for processing. The module, then, is responsible for executing the requested operation. Modules need only implement the XCFModule interface:


/**
 * Abstract interface for request handlers.  The function of the module is to 
 * handle requests sent to the facade.  The facade will map the request to the appropriate module
 * and that module, in turn, will forward the request the the appropriate object or method intended to handle
 * that request.
* System events and end user functionality will typically map to requests. * @author sonjaya * */ public interface XCFModule { public String XCF_TAG = "xcf-module"; /** * Sets the facade that this module is part of * @param server the module's server */ void setFacade(XCFFacade facade); /** * Sets the name * @param name The name to set */ void setName(String name); /** * Returns the modules name * @return */ String getName(); /** * Gets the defaultOperation * @return Returns a String */ String getDefaultOperation(); /** * Sets the defaultOperation * @param defaultOperation The defaultOperation to set */ void setDefaultOperation(String defaultOperation); /** * Execute the request by using the information in req * to lookup the appropriate request handler. * @param req * @throws XCFException */ void process(XCFRequest req) throws XCFException; }

Abstracting Common Services

Some functionality in sub-systems to not map well to request oriented processing. For example, a connection pool or a thread manager is better accessed directly through an object that models that service. These service objects are used by request handlers (and other services objects). We allow service object clients to be somewhat coupled to the service objects. The abstraction XCF provides is simply to support those services being contained in the facade.


/**
 * Abstract interface for shared singleton objects.  An XCFService is typically an object used by
 * multiple request handlers (and sometimes even other XCFService objects).  The singleton instance is typically
 * created when the facade is built; it may contain mulitple threads; and it is typically terminiated when 
 * the facade is shut down.  An object that provides abstracted access to database connections is an example.
 * 
* Handlers typicallly access services in request handlers as follows: *
  [ConcreteServiceType]service = [ConcreteServiceType]request.getContext().getFacade().getService([ConcreteServiceType].XCF_TAG); * @author sonjaya * */ public interface XCFService { /** * Starts the service object * @throws XCFException */ void start() throws XCFException; /** * Stops the service object * @throws XCFException */ void stop() throws XCFException; /** * Returns the service object's name * @return */ String getName(); /** * Sets the service object's name. This method is typically called by the object that instantiated the service. * @param name */ void setName(String name); /** * Sets the facade that this service is part of * @param facade the service's facade */ void setFacade(XCFFacade facade); }

Testing it out

To test out what we have so far, you will need to implement a concrete module and a concrete request object. Our concrete module will simply print the request object sent to it.


package com.eternal.stubs;

import java.util.Iterator;

import com.eternal.xcf.core.XCFException;
import com.eternal.xcf.core.XCFFacade;
import com.eternal.xcf.core.XCFModule;
import com.eternal.xcf.core.XCFRequest;

public class MODULE_SimpleTest implements XCFModule {

	String name;
	String defaultOperation;
	XCFFacade facade;

	public String getDefaultOperation() {
		return defaultOperation;
	}

	public String getName() {
		return name;
	}

	/**
	 * Process the request by logging out:
	 *   [module].[operation]([paramName1]=[paramValue1],...)
	 */
	public void process(XCFRequest req) throws XCFException {
		StringBuffer buf = new StringBuffer();
		buf.append("PROCESSING " + req.getModule() + "." + req.getOperation() + "(");
		Iterator paramNames =  req.getParameterNames();
		
		
		String separator = "";
		while (paramNames.hasNext()) {
			String paramName = paramNames.next();
			buf.append(separator);
			buf.append(paramName + "=" + req.getParameter(paramName));
			separator=", ";
		}
		buf.append(")");
		facade.logInfo(buf.toString());

	}

	public void setDefaultOperation(String defaultOperation) {
		this.defaultOperation = defaultOperation;
	}

	public void setFacade(XCFFacade facade) {
		this.facade = facade;
	}

	public void setName(String name) {
		this.name = name;
	}

}

Your concrete request object need only contain a hashmap of parameters (and also store the module and operation). For this simple test, don’t worry about providing a context object.


package com.eternal.stubs;

import java.util.HashMap;
import java.util.Iterator;

import com.eternal.xcf.core.XCFContext;
import com.eternal.xcf.core.XCFRequest;

public class REQUEST_SimpleTest implements XCFRequest {
	private String module;
	private String operation;
	private XCFContext context;
	private HashMap parameters = new HashMap();

	public REQUEST_SimpleTest(String module, String operation) {
		this.module = module;
		this.operation = operation;
	}
	
	public XCFContext getContext() {
		return context;
	}

	public String getModule() {
		return module;
	}

	public Object getNativeListener() {
		return null;
	}

	public Object getNativeRequest() {
		return null;
	}

	public Object getNativeResponse() {
		return null;
	}

	public String getOperation() {
		return operation;
	}

	public String getParameter(String paramName) {
		return parameters.get(paramName);
	}

	public Iterator getParameterNames() {
		return parameters.keySet().iterator();
	}

	public void setContext(XCFContext context) {
		this.context = context;
	}

	public void setModule(String module) {
		this.module = module;
	}

	public void setNativeListener(Object listener) {
	}

	public void setNativeRequest(Object req) {
	}

	public void setNativeResponse(Object resp) {
	}

	public void setOperation(String operation) {
		this.operation = operation;
	}

	public void setParameter(String paramName, String paramValue) {
		parameters.put(paramName, paramValue);
	}

	public void addNotice(String message) {
	}

	public void clearNotice() {
	}

	public Iterator getNotices() {
		return null;
	}

}

Now create a junit class to test what you built. You will build a simple facade with one module (account) and test request processing by sending that facade the request: account.login


package com.eternal.xcf.core.test;

import com.eternal.stubs.MODULE_SimpleTest;
import com.eternal.stubs.REQUEST_SimpleTest;
import com.eternal.xcf.core.XCFFacade;

import junit.framework.TestCase;

public class TestSimpleFacade extends TestCase {
	private XCFFacade facade;

	protected void setUp() throws Exception {
		super.setUp();
		// Build the facade.  The facade will support 
		// requests to an account module.
		facade = new XCFFacade();
		MODULE_SimpleTest accountModule = new MODULE_SimpleTest();
		facade.putModule("account", accountModule);
	}

	protected void tearDown() throws Exception {
		super.tearDown();
	}
	
	public void testSimpleRequestProcess() throws Exception {
		// create the request account.login(userName=myloginname, password=mypassword)
		REQUEST_SimpleTest request = new REQUEST_SimpleTest("account", "login");
		request.setParameter("userName", "myloginname");
		request.setParameter("password", "mypassword");
		
		// process the request
		facade.process(request);
	}

}

Conclusions

Congratulations! You have successfully built the core layer of the XCF framework. With just this core you have decoupled your sub-system clients from the sub-sytem. This will give you the freedom to change underlying sub-sytem implementations and leverage emerging technologies. Using these classes as the foundation, you will build up a sophisticated set of systems to support context management, parameter processing, state management, and asynchronous communication. Enjoy!

Entry Filed under: Software Development,XCF

Trackbacks

7 Comments Add your own

  • 1. geoffreychew  |  October 5th, 2006 at 3:12 pm

    Great article! It would be nice if the Logging interface provided a mechanism for checking whether or not a specific logging level is enabled as log4j does. This is helpful when you want to avoid performance penalities associated with constructing numerous string objects when debugging is not enabled.

  • 2. sonjaya  |  October 5th, 2006 at 3:43 pm

    Thanks.
    So perhaps a
    boolean isLoggingTypeEnabled(LogType)
    in the log manager interface? There really isn’t a notion of logging levels as you can selectivly turn on and off types.

    Also, just finished the refactoring pass on request processing — that includes parameter processing and commands. I should have the next article available sometime in the next five days.

  • 3. Sonjaya Tandon » FR&hellip  |  November 5th, 2006 at 10:59 am

    […] Lesson 01: Building the core […]

  • 4. Sonjaya Tandon » FR&hellip  |  November 5th, 2006 at 6:36 pm

    […] Lesson 01: Building the core […]

  • 5. Sonjaya Tandon » Fr&hellip  |  December 23rd, 2006 at 2:00 pm

    […] Lesson 01: Building the core […]

  • 6. Sonjaya Tandon » FR&hellip  |  January 7th, 2007 at 2:37 pm

    […] Lesson 01: Building the core […]

  • 7. Sonjaya Tandon » FR&hellip  |  February 10th, 2007 at 2:14 pm

    […] Lesson 01: Building the core […]

Leave a Comment

You must be logged in to post a comment.

Trackback this post  |  Subscribe to the comments via RSS Feed


Calendar

September 2006
M T W T F S S
« Aug   Oct »
 123
45678910
11121314151617
18192021222324
252627282930  

Most Recent Posts