package net.xoetrope.xui;
import java.awt.Cursor;
import java.awt.Color;
import java.awt.event.FocusEvent;
import java.util.EventObject;
import java.util.Hashtable;
import java.util.Vector;
import net.xoetrope.debug.DebugLogger;
import net.xoetrope.xml.XmlElement;
import net.xoetrope.xui.build.BuildProperties;
import net.xoetrope.xui.data.XDataBinding;
import net.xoetrope.xui.events.XuiEventHandler;
import net.xoetrope.xui.style.XStyleFactory;
import net.xoetrope.xui.exception.XExceptionHandler;
import net.xoetrope.xui.helper.ReflectionHelper;
import net.xoetrope.xui.validation.XValidationFactory;
import net.xoetrope.xui.validation.XValidationHandler;
import net.xoetrope.xui.validation.XValidator;
/**
* <p>A class used by XPage to implement its functionality in a reusable way
* </p>
* <p>Copyright (c) Xoetrope Ltd., 2002-2004</p>
* <p>License: see license.txt</p>
* $Revision: 1.8 $
*/
public class XPageHelper
{
/**
* Hand Cursor Object instance
*/
public static final Cursor hand = Cursor.getPredefinedCursor( Cursor.HAND_CURSOR );
/**
* The XComponentFactory being used
*/
public XStyleFactory componentFactory;
/**
* The context for data binding management
*/
public XDataBindingContext dataBindingContext;
/**
* An evaluator of paths, attribs etc...
*/
public XPathEvaluator pathEvaluator;
/**
* Vector of components to be hidden or shown over page transitions
*/
public Vector hiddenComponents;
/**
* The XuiEventHandler being used by the XPage
*/
public XuiEventHandler eventHandler;
/**
* Hashtable of named component attributes
*/
public Hashtable attribs;
/**
* Indicates current state of the XPage in it�s lifecycle
*/
public int status;
/**
* boolean to indicate how to handle repaints of the page
*/
public boolean clearPage;
/**
* The currently active XProject instance
*/
public XProject currentProject;
/**
* For handling different component packages
*/
public WidgetAdapter adapter;
/**
* The PageSupport being helped
*/
public PageSupport page;
/**
* The default evaluator for databinding paths
*/
protected XPathEvaluator evaluator;
/**
* The name of the page
*/
public String pageName;
/**
* The file extension of this file, if any
*/
public String pageExtension;
/**
* Creates a new instance of XPageHelper
* @param project the owner project
* @param thePage The XPageHelper to be used
*/
public XPageHelper( XProject project, PageSupport thePage )
{
page = thePage;
currentProject = project;
adapter = WidgetAdapter.getInstance();
page.setBackground( Color.white );
page.setLayout( null );
status = XPage.UNKNOWN_PAGE_STATE;
pathEvaluator = new XPathEvaluator( currentProject, page );
dataBindingContext = null;
String contextClassName = project.getStartupParam( "XDataBindingContextClass" );
// if a context binding class has been specified in
// the startup file, try to create it by reflection
if ( contextClassName != null ) {
try {
Class dataBindingsClass = Class.forName( contextClassName );
if ( XDataBindingContext.class.isAssignableFrom( dataBindingsClass ))
dataBindingContext = (XDataBindingContext)dataBindingsClass.
getConstructor( new Class[]{ XPathEvaluator.class } ).
newInstance( new Object[]{ pathEvaluator } );
}
catch( Exception ex ) {
DebugLogger.logWarning( "couldn't instantiate binding context class: " + contextClassName );
dataBindingContext = null;
}
}
// set the default binding context class if none has been specified
if ( dataBindingContext == null )
dataBindingContext = new XDataBindingContext( pathEvaluator );
hiddenComponents = null;
XValidationHandler validationHandler = new XValidationHandler( page );
eventHandler = currentProject.createEventHandler( page, validationHandler );
componentFactory = new XStyleFactory( currentProject, currentProject.getWidgetPackageName() );
componentFactory.setParentComponent( page );
attribs = new Hashtable();
clearPage = true;
// Try to set the default exception handler to the page
try {
String peh = currentProject.getStartupParam( "PageExceptionHandler" );
if (( peh == null ) || ( peh.toLowerCase().compareTo( "true" ) == 0 )) {
setExceptionHandler( page );
}
}
catch ( Exception ex ) {}
}
/**
* Get the name of this page
* @return the page name
*/
public String getPageName()
{
return pageName;
}
/**
* Get the file extension of this page
* @return the page extension
*/
public String getPageExtension()
{
return pageExtension;
}
/**
* Get the name of this page
* @param name the page name
*/
public void setPageName( String name )
{
pageName = name;
}
/**
* Get the name of this page
* @param name the page name
*/
public void setPageExtension( String ext )
{
pageExtension = ext;
}
/**
* Modify the clearPage flag. This flag determines if the default behaviour is
* used to update the page whereby the background is first erased and then the
* content painted or alternatively if the erase is suppressed.
* @param value boolean to apply to clearPage
*/
public void setClearPage( boolean value )
{
clearPage = value;
}
/**
* Show or hide the components. In the AWT the heavyweight peers are created
* visible and paint themselves once created and therefore cause problems
* for page transitions.
* @param recursionLevel The level to which the components should be shown
* @param container the container
* @param visible the
*/
public void showComponents( Object container, boolean visible, int recursionLevel )
{
if ( visible ) {
if ( hiddenComponents != null ) {
// Only show components hidden by this method
int numComponents = hiddenComponents.size();
for ( int i = 0; i < numComponents; i++ ) {
Object comp = hiddenComponents.elementAt( i );
if ( comp != null )
adapter.setVisible( comp, true );
}
hiddenComponents = null;
}
}
else {
int numComponents = adapter.getComponentCount( container );
if ( recursionLevel == 0 )
hiddenComponents = new Vector( numComponents, 4 );
for ( int i = 0; i < numComponents; i++ ) {
Object comp = adapter.getComponent( container, i );
// Don't try to hide hidden components or the state will be lost
if ( adapter.isVisible( comp ) ) {
adapter.setVisible( comp, false );
hiddenComponents.addElement( comp );
}
if ( adapter.isContainer( comp ) )
showComponents( comp, false, ++recursionLevel );
}
}
}
/**
* Find a named component in the container. Any child containers of the
* container will be searched recursively till the named component is found.
* The first component with a matching name will be returned.
* @param name the name to locate
* @return the component ornull if nothing is found
*/
public Object findComponent( String name )
{
Object comp = findComponent( page, name );
if ( ( comp == null ) && BuildProperties.DEBUG )
DebugLogger.logWarning( "Unable to find the component: " + name );
return comp;
}
/**
* Find a named component in the container. Any child containers of the
* container will be searched recursively till the named component is found.
* The first component with a matching name will be returned.
* @param container the page or container to search
* @param name the name to locate
* @return the component or null if nothing is found
*/
public Object findComponent( Object container, String name )
{
int numComponents = adapter.getComponentCount( container );
for ( int i = 0; i < numComponents; i++ ) {
Object comp = adapter.getComponent( container, i );
String compName = adapter.getName( comp );
if (( compName != null ) && ( compName.compareTo( name ) == 0 ))
return comp;
else if ( adapter.isContainer( comp )) {
Object c = findComponent( comp, name );
if ( c != null )
return c;
}
}
return null;
}
//--Start of Validation-------------------------------------------------------
/**
* Set the validation exception handler called when a validation exception is trapped
* @param eh the new event handler
*/
public void setExceptionHandler( XExceptionHandler eh )
{
eventHandler.getValidationHandler().setExceptionHandler( eh );
}
/**
* A method called when a validation exeption has been trapped.
*
* @param comp Component being validated
* @param ex The exception caused
* @param validator The validator being used to validate.
* @return true to continue with error validation or false to suppress further
* validation.
*/
public boolean handleException( Object comp, Exception ex, Object validator )
{
ex.printStackTrace();
return true;
}
/**
* Handle an exception during the invocation of a page's event handler. The page
* normally implements this interface and has the first chance at handling the
* error. Thereafter if false is returned a central (optional) exception
* handler owned by the project is invoked.
*
* @param project the current project
* @param container the page
* @param error the exception or error that was thrown
* @return true to continue processing, false to stop processing
*/
public boolean handleEventHandlerException( XProject project, Object container, Throwable error )
{
return true;
}
/**
* Reset/removes all validations
*/
public void clearValidations()
{
eventHandler.getValidationHandler().clearValidations();
}
/**
* Check all validations for this page. Typically this method should be
* invoked prior to a page transition or a critical transaction.
* @return the maximum error level raised by the validators
*/
public int checkValidations()
{
XValidationHandler validationHandler = eventHandler.getValidationHandler();
validationHandler.accumulateMessages( true, 0 );
int ret = validationHandler.checkValidations();
ret = validationHandler.accumulateMessages( false, ret );
return ret;
}
/**
* Informs the handler when a page validation is starting or stopping. Typically
* when it starts the page will begin to accumulate message which are to be displayed.
* When the parameter is false the page will usually display the accumulated
* messages
* @param start boolean to indicate whether the accumulation is started or stopped.
* @param level int which indicates the most serious level of error encountered
* @return the new level which might be set to zero if a confirm dialog is displayed
*/
public int accumulateMessages( boolean start, int level )
{
return level;
}
/**
* Adds a validation to this page.
* @param comp the component being validated
* @param validationName the name of the validation in the validation file
* @param method the method used to get the component's value if any
* @param mask the event mask used to filter the events that trigger the validation
* @param pageEle the XML element which is declared in the page
* @return the new and initialized XValidator
*/
public XValidator addValidation( Object comp, String validationName, String method, int mask, XmlElement pageEle )
{
return eventHandler.getValidationHandler().addValidation( eventHandler, comp, validationName, method, mask, pageEle );
}
/**
* Adds a validation to this page. It is assumed that the validation will be
* invoked in response to FocusEvent.FOCUS_LOST events
* @param comp the component being validated
* @param validationName the name of the validation in the validation file
* @param method the method used to get the component's value if any
* @return the new and initialized XValidator
*/
public XValidator addValidation( Object comp, String validationName, String method )
{
return addValidation( comp, validationName, method, FocusEvent.FOCUS_LOST, null );
}
/**
* Adds a validation to this page. It is assumed that the validation will be
* invoked in response to FocusEvent.FOCUS_LOST events
* @param comp the component being validated
* @param validationName the name of the validation in the validation file
* @return the new and initialized XValidator
*/
public XValidator addValidation( Object comp, String validationName )
{
return addValidation( comp, validationName, null, FocusEvent.FOCUS_LOST, null );
}
/**
* Sets the factory used to create XValidator objects
* @param vf The XValidationFactory to be used
*/
public void setValidationFactory( XValidationFactory vf )
{
eventHandler.getValidationHandler().setValidationFactory( vf );
}
/**
* Gets the validation handler
* @return the validation handler
*/
public XValidationHandler getValidationHandler()
{
return eventHandler.getValidationHandler();
}
/**
* Invoke the validators for the last event. Multiple validations are checked
* in the order in which they were added.
* @param eventHandler the current event handler that this handler will use to
* listen to for events that should trigger validations
* @return the maximum level returned by the validators
*/
public int validationHandler( XuiEventHandler eventHandler )
{
return eventHandler.getValidationHandler().validationHandler( eventHandler );
}
//--End of Validation---------------------------------------------------------
//--Start of Event Handling---------------------------------------------------
/**
* Get the current event handler
* @return the event handler
*/
public XuiEventHandler getEventHandler()
{
return eventHandler;
}
/**
* Set the current event handler
* @param eh The XuiEventHandler to be used
*/
public void setEventHandler( XuiEventHandler eh )
{
eventHandler = eh;
}
/**
* Get the current event
* @return the AWTEvent that was last triggered
*/
public EventObject getCurrentEvent()
{
return eventHandler.getCurrentEvent();
}
/**
* Adds a listener for an event type. This method should not normally be
* called by an application
* @param comp the component that fires events
* @param listenerName the name of the listener interface
* @param argType the listener arguments
* @param listener the listener implementation
*/
public void addListener( Object comp, String listenerName, String argType, Object listener )
{
eventHandler.addListener( comp, listenerName, argType, listener );
}
/**
* Adds an event handler. A specific handler such as the addActionHandler should
* be used instead of calling this method
* @param comp the component that fires the event
* @param eventType the event ID/mask
* @param methodName the method to be invoked in response to the object
* @throws java.lang.ClassNotFoundException throw if the class cannot be found
* @throws java.lang.NoSuchMethodException throw if the method cannot be found
*/
public void addHandler( Object comp, long eventType, String methodName ) throws ClassNotFoundException,
NoSuchMethodException
{
eventHandler.addHandler( comp, eventType, methodName );
}
/**
* Check the focus change status
* @return true if the focus change events are being suppressed.
*/
public boolean isFocusChangeSuppressed()
{
return eventHandler.isFocusChangeSuppressed();
}
/**
* Adds a handler for action events
* @param srcObj the menu item that fires the events
* @param methodName the method to be invoked in response to the action event
* @param adderMethod the adder method name e.g. addActionListener
* @param listenerInterface the listener interface e.g. java.awt.event.ActionListener
* @param eventMask the event mask e.g. AWTEvent.ACTION_EVENT_MASK
* @param listener the listener implementation, usually the page's this pointer
* @see java.awt.event.ActionListener
* @see java.awt.event.ActionEvent
*/
public void addHandler( Object srcObj, String methodName, String adderMethod, String listenerInterface, long eventMask, Object listener )
{
eventHandler.addHandler( srcObj, methodName, adderMethod, listenerInterface, eventMask, listener );
}
/**
* A utility method used to determine if the last event corrseponds to a mouse
* click. The notion of a click is extended by assuming the a mouse press and
* release within a single component constitutes a click even if not at the
* same coordinate. A MouseEvent.MOUSE_CLICKED is only triggered when the press
* and release are at the same location and this is often inadequate for end-user
* interaction.
* @return true if the mouse was clicked
*/
public boolean wasMouseClicked()
{
return eventHandler.wasMouseClicked();
}
/**
* A utility method used to determine if the last event corrseponds to a mouse
* double click. The notion of a click is extended by assuming the a mouse press and
* release within a single component constitutes a click even if not at the
* same coordinate. A MouseEvent.MOUSE_CLICKED is only triggered when the press
* and release are at the same location and this is often inadequate for end-user
* interaction.
* @return true if the mouse was double clicked
*/
public boolean wasMouseDoubleClicked()
{
return eventHandler.wasMouseDoubleClicked();
}
/**
* A utility method used to determine if the last event corrseponds to a mouse
* right click. The notion of a click is extended by assuming the a mouse press and
* release within a single component constitutes a click even if not at the
* same coordinate. A MouseEvent.MOUSE_CLICKED is only triggered when the press
* and release are at the same location and this is often inadequate for end-user
* interaction.
* @return true if the mouse was right clicked
*/
public boolean wasMouseRightClicked()
{
return eventHandler.wasMouseRightClicked();
}
/**
* Show the hand/pointer cursor for this component
* @param comp the component
*/
public void showHandCursor( Object comp )
{
adapter.setCursor( comp, hand );
}
//--End of Event Handling-----------------------------------------------------
//--Start of Data Binding-----------------------------------------------------
/**
* Gets the XDataBindingContext object bound
* to this XPageHelper object
* @return XDataBindingContext objects
*/
public XDataBindingContext getDataBindingContext()
{
return dataBindingContext;
}
/**
* Retrieve the Vector of XDataBinding for the XPage
* @return Vector of XDataBinding
*/
public Vector getBindings()
{
return dataBindingContext.getBindings();
}
/**
* Add a binding of a component to the data model. If the page has already
* been activated this method will update the binding automatically.
* @param b the binding
*/
public void addBinding( XDataBinding b )
{
dataBindingContext.addBinding( b, (( status == XPage.CREATED ) || ( status == XPage.ACTIVATED )));
}
/**
* Remove a binding of a component to the data model.
* @param b the binding
*/
public void removeBinding( XDataBinding b )
{
dataBindingContext.removeBinding( b );
}
/**
* Iterate all of the bindings in the page to reflect the model state.
*/
public void updateBindings()
{
dataBindingContext.updateBindings();
}
/**
* Update the bound model node for the binding. First the output path is
* reevaluated and then updated by setting the output node. Then the source
* path is reevaluated and set. Evaluation of the paths allows derived classes
* to dynamically modify the bindings. Some bindings may save the selection or
* state information to the output node and subsequently use it to restore the
* component state. This method does not alter the data held by the bound model
* nodes. To actually save the data use saveBoundComponentValues and to update
* the UI use updateBoundComponentValues.
* @param binding The databinding to be updated
*/
public void updateBinding( XDataBinding binding )
{
dataBindingContext.updateBinding( binding );
}
/**
* Update the UI with values from the model
*/
public void updateBoundComponentValues()
{
dataBindingContext.updateBoundComponentValues();
}
/**
* Save the component values to the model
*/
public void saveBoundComponentValues()
{
dataBindingContext.saveBoundComponentValues();
}
/**
* Find the data binding associated with a component
* @param targetComp the component whose binding is required
* @return the binding or null if no binding is found
*/
public XDataBinding getBinding( Object targetComp )
{
return dataBindingContext.getBinding( targetComp );
}
/**
* Find the data binding associated with a data source path
* @param targetPath the path to the bound model
* @return the binding or null if no binding is found
*/
public XDataBinding getBinding( String targetPath )
{
return dataBindingContext.getBinding( targetPath );
}
/**
* Get the page status
* @return the current status
*/
public int getStatus()
{
return status;
}
//--End of Data Binding-------------------------------------------------------
//--Start of Page status------------------------------------------------------
/**
* Set the page status
* @param newStatus the new page status
*/
public void setStatus( int newStatus )
{
status = newStatus;
}
/**
* A method called once the page has been created and initialized but just
* prior to display
*/
public void pageActivated()
{
}
/**
* A method called once the page has been added to its parent container but
* not yet displayed
*/
public void pageAdded()
{
}
/**
* A method called once the page has been created but not yet initialized.
*/
public void pageCreated()
{
}
/**
* Called when the page is about to loose scope and be hidden.
*/
public void pageDeactivated()
{
}
//--Start of Page status------------------------------------------------------
//--End of Attribute Methods--------------------------------------------------
/**
* <p>Set a named attributes. The attributes are stored in a hashtable owned by
* the page. Derived classes may access the hashtable directly but the
* preferred method of access is the getAttribute method. Attributes are used
* by the XuiBuilder class for component attributes other than those it handles
* directly. The attributes can be thought of as component properties or extra
* data and need not be used directly by the component.</p>
* <p>
* Attributes are stored using a key in the form attribName_compName or just
* the attribName if compName is null.
* </p>
* @param attribName the attribute name
* @param compName the component name or null if it is a page attribute
* @param attribValue the attribute value
* @see #getAttribute
*/
public void setAttribute( String attribName, String compName, Object attribValue )
{
if ( attribValue != null )
attribs.put( attribName + ( compName != null ? "_" + compName : "" ), attribValue );
}
/**
* Gets an attribute value
* @param attribName the name of the attribute
* @return the value
*/
public Object getAttribute( String attribName )
{
EventObject evt = getCurrentEvent();
return getAttribute( attribName, evt == null ? "" : adapter.getName( evt.getSource() ) );
}
/**
* Gets an attribute value
* @param attribName the name of the attribute
* @param compName the component name
* @return the value
*/
public Object getAttribute( String attribName, String compName )
{
return attribs.get( attribName + ( compName != null ? "_" + compName : "" ) );
}
/**
* Gets the table of attributes used by this page. The method is not intended
* for general use and should be used with great care.
* @return the table of attributes
*/
public Object getAttributes()
{
return attribs;
}
/**
* Get a name for a component. If the component doesn't have one use the
* component hashcode
* @param comp the component
* @return the name
*/
public String getComponentName( Object comp )
{
String compName = adapter.getName( comp );
if ( ( compName == null ) || ( compName.length() == 0 ) )
compName = new Integer( comp.hashCode() ).toString();
return compName;
}
/**
* Gets an attribute value
* @return the value
* @param c Not used
* @param attribName the name of the attribute
*/
public Object getEventAttribute( Object c, String attribName )
{
return attribs.get( attribName );
}
/**
* Evaluates an attribute value. An attribute may be a value or a method call.
* If brackets are part of the value it is assumed that a method call is
* intended. The method call is indicated by the '$' symbol e.g. ${myMethod()}
* @param attribValue the raw attribute value
* @return the evaluated attribute
*/
public Object evaluateAttribute( String attribValue )
{
return pathEvaluator.evaluateAttribute( attribValue );
}
/**
* Evaluates a path (potentially) containing a method call
* @param path the raw path
* @return the evaluated path
*/
public String evaluatePath( String path )
{
return pathEvaluator.evaluatePath( path );
}
/**
* Remove the attribute paths from a path e.g. remove @value=ignore
* @param path the path to strip
* @return the stripped path
*/
public String stripAttributeValues( String path )
{
return pathEvaluator.stripAttributeValues( path );
}
/**
* Get the component factory instance being used by this page.
* @return the component factory
*/
public XComponentFactory getComponentFactory()
{
return componentFactory;
}
/**
* Set the component factory instance being used by this page when constructing
* new pages.
* @param factory The XComponentFactory to be used
*/
public void setComponentFactory( XStyleFactory factory )
{
componentFactory = factory;
componentFactory.setParentComponent( page );
}
/**
* Used to translate strings found in the 'content' attribute of a component
* declaration and translate it using the localization resource bundles
* @param key The key into the ResourceBundle
* @return The translated key if found otherwise the passed string
*/
public String translate( String key )
{
return componentFactory.translate( key );
}
//--End of Attribute Methods--------------------------------------------------
//--Start of MessageBox methods-----------------------------------------------
/**
* Shows a modal message box
* @param title the message dialog title
* @param msg the text of the message
*/
public void showMessage( String title, String msg )
{
showMessage( page.getOwner(), title, msg );
}
/**
* Shows a modal message box
* @param parent the message dialog parent
* @param title the message dialog title
* @param msg the text of the message
*/
public void showMessage( Object parent, String title, String msg )
{
if ( !WidgetAdapter.getInstance().requiresParent())
new MessageThread( parent, title, msg ).start();
else {
try {
XProjectManager.setCurrentProject( currentProject );
eventHandler.suppressFocusEvents( true );
XMessageBoxSetup msgbox = (XMessageBoxSetup)ReflectionHelper.constructViaReflection( null,
currentProject.getPackageName() + ".XMessageBox",
Object.class,
currentProject.getObject( "ClientShell" ) );
msgbox.setup( translate( title ), translate( msg ), page.getPageSize(), this );
eventHandler.suppressFocusEvents( false );
}
catch ( Exception ex ) {
if ( BuildProperties.DEBUG ) {
DebugLogger.logError( "Could not create the message box: " + msg );
ex.printStackTrace();
}
}
}
}
/**
* A class used to display the message box
* <p>Company: Xoetrope Ltd.</p>
*/
class MessageThread extends Thread
{
String msg, caption;
Object container;
public MessageThread( Object cont, String title, String message )
{
container = cont;
msg = message;
caption = title;
setPriority( Thread.NORM_PRIORITY );
}
public void run()
{
eventHandler.suppressFocusEvents( true );
try {
XMessageBoxSetup msgbox = (XMessageBoxSetup)Class.forName( currentProject.getPackageName() + ".XMessageBox" ).newInstance();
msgbox.setup( translate( caption ), translate( msg ), page.getPageSize(), container );
}
catch ( Exception ex ) {
if ( BuildProperties.DEBUG ) {
DebugLogger.logError( "Could not create the message box: " + msg );
ex.printStackTrace();
}
}
eventHandler.suppressFocusEvents( false );
}
}
//--End of MessageBox methods-------------------------------------------------
}