/*==========================================================================*\
| $Id: WCComponent.java,v 1.4 2012/05/09 14:25:30 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2006-2010 Virginia Tech
|
| This file is part of Web-CAT.
|
| Web-CAT is free software; you can redistribute it and/or modify
| it under the terms of the GNU Affero General Public License as published
| by the Free Software Foundation; either version 3 of the License, or
| (at your option) any later version.
|
| Web-CAT is distributed in the hope that it will be useful,
| but WITHOUT ANY WARRANTY; without even the implied warranty of
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
| GNU General Public License for more details.
|
| You should have received a copy of the GNU Affero General Public License
| along with Web-CAT; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/
package org.webcat.core;
import com.webobjects.appserver.*;
import com.webobjects.eocontrol.*;
import com.webobjects.foundation.*;
import org.webcat.core.Application;
import org.webcat.core.PageWithNavigation;
import org.webcat.core.Session;
import org.webcat.core.TabDescriptor;
import org.webcat.core.User;
import org.webcat.core.WCComponent;
import org.webcat.core.WCComponentWithErrorMessages;
import org.webcat.core.WizardPage;
import org.apache.log4j.Logger;
import org.webcat.core.messaging.UnexpectedExceptionMessage;
import org.webcat.woextensions.WCEC;
// -------------------------------------------------------------------------
/**
* This class is the root class for all "pages" that are to be nested
* inside a {@link PageWithNavigation} or {@link WizardPage}.
*
* It provides signatures and/or default implementations for the
* callback operations used by these page wrappers.
* <p>
* Typically, a subsytem will create its own custom subclass of
* <code>WCComponent</code> that provides a subsystem-specific default
* implementation for {@link #title()} returning a general-purpose title
* that can be used for the subsystem pages that do not provide their own.
* Such a subclass can also return subsystem-specific data stored in
* the session object.
* </p>
* <p>
* The default implementations in this base class will provide
* will provide unique {@link #helpRelativeURL()} and {@link #feedbackId()}
* values derived from the page's actual class name.
* </p>
* <p>
* Instead, individual wizard pages will need to override the wizard control
* button callback functions to give appropriate semantics
* ({@link #cancel()}, {@link #back()},
* {@link #next()}, {@link #apply()}, and {@link #finish()}, together with
* their related <code>-Enabled</code> functions).
* </p>
*
* @author Stephen Edwards
* @author latest changes by: $Author: stedwar2 $
* @version $Revision: 1.4 $, $Date: 2012/05/09 14:25:30 $
*/
public class WCComponent
extends WCComponentWithErrorMessages
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new WCComponent object.
*
* @param context The page's context
*/
public WCComponent( WOContext context )
{
super( context );
}
//~ KVC Attributes (must be public) .......................................
public WCComponent nextPage;
public boolean cancelsForward;
public boolean nextPerformsSave;
public static final String ALL_TASKS = "all";
//~ Public Methods ........................................................
// ----------------------------------------------------------
/**
* Returns the current session object as the application-specific
* subtype <code>Session</code>. This avoids the need for downcasting
* on each <code>session</code> call.
*
* @return The current session
*/
public Session wcSession()
{
return (Session)session();
}
// ----------------------------------------------------------
/**
* Returns the current application object as the application-specific
* subtype <code>Application</code>. This avoids the need for
* downcasting on each <code>application</code> call.
*
* @return The current application
*/
public Application wcApplication()
{
return (Application)application();
}
// ----------------------------------------------------------
/**
* Returns the page's title string.
*
* This generic implementation returns null, which will force the
* use of the default title "Web-CAT", which will be used for pages
* that do not provide their own title. Ideally, subsystems will
* override this default.
*
* @return The page title
*/
public String title()
{
return null;
}
// ----------------------------------------------------------
/**
* Returns the base URL at which all <code>helpRelativeURL</code>
* values are rooted.
*
* The generic implementation returns the root for all of this
* installation's Web-CAT documentation.
*
* @return The base URL
*/
public static String helpBaseURL()
{
return Application.configurationProperties().getProperty(
"help.baseURL", "http://web-cat.org/Web-CAT.help/" );
}
// ----------------------------------------------------------
/**
* Returns the URL for this page's help documentation, relative
* to <code>helpBaseURL</code>.
*
* This generic implementation returns the current page's actual
* class name, with "org.webcat." stripped from the front,
* with periods transformed to slashes (/), and with ".html" appended
* on the end.
*
* @return The page URL
*/
public String helpRelativeURL()
{
final String base = "org.webcat.";
String url = this.getClass().getName();
if ( url.startsWith( base ) )
{
url = url.substring( base.length() );
}
return url.replace( '.', '/' ) + ".php";
}
// ----------------------------------------------------------
/**
* Returns the URL for this page's help documentation.
*
* The URL is formed by concatenating <code>helpBaseURL</code> and
* <code>helpRelativeURL</code>.
*
* @return The URL
*/
public String helpURL()
{
// System.out.println("The help url is " + helpBaseURL() + helpRelativeURL());
return helpBaseURL() + helpRelativeURL();
}
// ----------------------------------------------------------
/**
* Returns the page's feedback ID for use in feedback e-mail.
*
* This generic implementation returns the fully qualified class name
* of the current page.
*
* @return The feedback ID
*/
public String feedbackId()
{
return this.getClass().getName();
}
// ----------------------------------------------------------
/**
* Get the selected tab that corresponds to this component's page.
* @return this page's navigation tab
*/
public TabDescriptor currentTab()
{
if (currentTab == null)
{
currentTab = wcSession().tabs.selectedDescendant();
}
return currentTab;
}
// ----------------------------------------------------------
/**
* Set the tab that corresponds to this component's page.
* @param current this page's navigation tab
*/
public void setCurrentTab(TabDescriptor current)
{
currentTab = current;
}
// ----------------------------------------------------------
public void reselectCurrentTab()
{
if (currentTab() != null)
{
currentTab().select();
}
}
// ----------------------------------------------------------
/**
* Determines whether the wizard page's "Cancel" button is visible.
*
* This generic implementation returns true. This callback is
* not used by {@link PageWithNavigation}; it is only meaningful inside
* a {@link WizardPage} container.
*
* @return True if "Cancel" should appear
*/
public boolean cancelEnabled()
{
return true;
}
// ----------------------------------------------------------
/**
* Returns the page to go to when "Cancel" is pressed.
*
* This generic implementation moves to the default sibling of the
* currently selected tab.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return The page to go to
*/
public WOComponent cancel()
{
clearMessages();
cancelLocalChanges();
TabDescriptor parent = currentTab().parent();
if ( parent.parent().parent() != null )
{
// If we're on a third-level tab, jump up one level so that
// we move to the default second-level tab.
parent = parent.parent();
}
if (cancelsForward)
{
return internalNext(false);
}
else
{
changeWorkflow();
return pageWithName( parent.selectDefault().pageName() );
}
}
// ----------------------------------------------------------
/**
* Determines whether the wizard page's "Back" button is visible.
*
* This generic implementation looks at the currently selected tab
* and calls its {@link TabDescriptor#hasPreviousSibling()} method to
* get the name of the page to create.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return True if "Back" should appear
*/
public boolean backEnabled()
{
return nextPage != null || currentTab().hasPreviousSibling();
}
// ----------------------------------------------------------
/**
* Returns the page to go to when "Back" is pressed.
*
* This generic implementation looks at the current tab
* and calls its {@link TabDescriptor#previousSibling()} method to
* get the name of the page to create.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return The page to go to
*/
public WOComponent back()
{
if ( hasMessages() )
{
return null;
}
if ( nextPage != null )
{
if (breakWorkflow)
{
breakWorkflow = false;
}
else if (nextPage.peerContextManager == null
&& peerContextManager != null)
{
nextPage.peerContextManager = peerContextManager;
nextPage.alreadyGrabbed = true;
}
return nextPage;
}
else
{
return pageWithName(
currentTab().previousSibling().select().pageName() );
}
}
// ----------------------------------------------------------
/**
* Determines whether the wizard page's "Next" button is visible.
*
* This generic implementation looks at the currently selected tab
* and calls its {@link TabDescriptor#hasNextSibling()} method to
* get the name of the page to create.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return True if "Next" should appear
*/
public boolean nextEnabled()
{
return !hasBlockingErrors()
&& ( nextPage != null || currentTab().hasNextSibling() );
}
// ----------------------------------------------------------
/**
* Returns the page to go to when "Next" is pressed.
*
* This generic implementation looks at the current tab
* and calls its {@link TabDescriptor#nextSibling()} method to
* get the name of the page to create.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return The page to go to
*/
public WOComponent next()
{
return internalNext(true);
}
// ----------------------------------------------------------
/**
* Determines whether the wizard page's "Apply All" button is visible.
*
* This generic implementation returns false, but should be overridden
* by wizard pages that have recordable settings on them.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return True if "Apply All" should appear
*/
public boolean applyEnabled()
{
return false;
}
// ----------------------------------------------------------
/**
* Saves all local changes. This is the core "save" behavior that
* is called by both {@link #apply()} and {@link #finish()}. Override
* this (and call super) if you need to extend these actions. This
* method calls {@link Session#commitSessionChanges()}.
* @return True if the commit action succeeds, or false if some error
* occurred
*/
public boolean applyLocalChanges()
{
if ( hasBlockingErrors() )
{
return false;
}
try
{
commitLocalChanges();
return true;
}
catch ( com.webobjects.foundation.NSValidation.ValidationException e )
{
cancelLocalChanges();
warning( e.getMessage() );
return false;
}
catch ( Exception e )
{
new UnexpectedExceptionMessage(e, context(), null,
"Exception trying to save component's local changes" )
.send();
// forces revert and refaultAllObjects
cancelLocalChanges();
String msg =
"An exception occurred while trying to save your changes";
String eMsg = e .getMessage();
if (eMsg != null && eMsg.length() > 0)
{
msg += ": " + eMsg;
}
if (!msg.endsWith("."))
{
msg += ".";
}
warning( msg + " As a result, your changes were canceled. "
+ "Please try again." );
return false;
}
}
// ----------------------------------------------------------
/**
* Cancel all local changes and revert to the default editing context
* state.
*/
public void cancelLocalChanges()
{
if (peerContextManager != null)
{
try
{
// Make sure to grab the lock, in case this EC hasn't been
// used for anything yet in this RR-loop and Wonder hasn't
// auto-locked it yet.
peerContextManager.editingContext().lock();
peerContextManager.editingContext().revert();
peerContextManager.editingContext().refaultAllObjects();
}
finally
{
peerContextManager.editingContext().unlock();
}
}
else
{
wcSession().cancelSessionChanges();
}
}
// ----------------------------------------------------------
/**
* Returns the page to go to when "Apply All" is pressed.
*
* This generic implementation commits changes but remains on the
* same page.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return The page to go to (always null in this default implementation)
*/
public WOComponent apply()
{
applyLocalChanges();
return null;
}
// ----------------------------------------------------------
/**
* Determines whether the wizard page's "Finish" button is visible.
*
* This generic implementation returns true.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return True if "Finish" should appear
*/
public boolean finishEnabled()
{
return !hasBlockingErrors();
}
// ----------------------------------------------------------
/**
* Returns the page to go to when "Finish" is pressed.
*
* This generic implementation commits changes, then moves to the
* default sibling page.
*
* This callback is not used by {@link PageWithNavigation}; it is only
* meaningful inside a {@link WizardPage} container.
*
* @return The page to go to
*/
public WOComponent finish()
{
if ( applyLocalChanges() && !hasMessages() )
{
TabDescriptor parent = currentTab().parent();
if ( parent.parent().parent() != null )
{
// If we're on a third-level tab, jump up one level so that
// we move to the default second-level tab.
parent = parent.parent();
}
changeWorkflow();
return pageWithName( parent.selectDefault().pageName() );
}
else
{
return null;
}
}
// ----------------------------------------------------------
/**
* For wizard pages, implements the default action that takes place
* when the user presses "Enter" on a wizard page. This implementation
* calls next (if enabled) or else applyChanges (if enabled) or simply
* remains on this page. Subclasses can override this to
* provide their own desired behavior.
*
* @return The page to go to
*/
public WOComponent defaultAction()
{
log.debug( "defaultAction()" );
if ( nextEnabled() )
{
return next();
}
else if ( applyEnabled() )
{
return apply();
}
else if ( finishEnabled() )
{
return finish();
}
else
return null;
}
// ----------------------------------------------------------
/**
* Attempt to set any session-specific local data for this page from
* the given dictionary so that this page can be rendered successfully.
* @param params a dictionary of key/value pairs specifying local
* data for this page
* @return True if the page can be rendered using the info from params,
* or false if required parameters are missing.
*/
public boolean startWith( NSDictionary<String, Object> params )
{
return true;
}
// ----------------------------------------------------------
public void pushValuesToParent()
{
// Make sure to handle logout actions on form pages correctly
// by blocking value pushing if the session is terminating
if ( hasSession() && !session().isTerminating() )
{
super.pushValuesToParent();
}
}
// ----------------------------------------------------------
public void awake()
{
if (log.isDebugEnabled())
{
log.debug("awake(): " + getClass().getName());
}
localContext();
if (currentTab != null)
{
reselectCurrentTab();
}
super.awake();
// Force currentTab to be initialized
currentTab();
}
// ----------------------------------------------------------
@Override
public void sleep()
{
if (peerContextManager != null)
{
peerContextManager.sleep();
}
super.sleep();
}
// ----------------------------------------------------------
public void willCachePermanently()
{
//
}
// ----------------------------------------------------------
/**
* Access this session's child editing context for storing multi-page
* changes.
* @return The child editing context
*/
public EOEditingContext localContext()
{
return peerContextManager().editingContext();
// To turn off the use of peer editing context and revert to
// performing all modifications in the single, shared session
// default editing context, comment out the line above and
// uncommet this line instead:
// return wcSession().sessionContext();
}
// ----------------------------------------------------------
/**
* Returns the current user, or null if one is not logged in.
* This object lives in the page's child editing context.
* @return The current user
*/
public User user()
{
if (user == null && wcSession().user() != null)
{
user = wcSession().user().localInstance( localContext() );
}
return user;
}
// ----------------------------------------------------------
/**
* Change the local user for the session, to support impersonation of
* students by administrators and instructors.
* @param u the new user to impersonate
*/
public void setLocalUser( User u )
{
user = null;
wcSession().setLocalUser(
u.localInstance( wcSession().sessionContext() ));
}
// ----------------------------------------------------------
/**
* This is a typesafe version of the WO {@link #pageWithName(String)}
* method, and should be used instead of the string version.
* @param pageClass the class of the page to create
* @return a new instance of the class, appropriately typed.
*/
@SuppressWarnings("unchecked")
public final <T> T pageWithName(Class<T> pageClass)
{
reselectCurrentTab();
return (T)pageWithName(pageClass.getName());
}
// ----------------------------------------------------------
@Override
/**
* Where possible, use {@link #pageWithName(Class)} instead.
*/
public WOComponent pageWithName( String name )
{
if (log.isDebugEnabled())
{
log.debug("pageWithName(" + name + ") from "
+ getClass().getName());
}
String managerKey = null;
if (breakWorkflow)
{
breakWorkflow = false;
}
else if (peerContextManager != null)
{
if (log.isDebugEnabled())
{
log.debug("storing " + peerContextManager
+ " on manager tag = "
+ Thread.currentThread().toString());
}
managerKey = Thread.currentThread().toString();
wcSession().transientState().takeValueForKey(
peerContextManager, managerKey);
}
WOComponent result = super.pageWithName( name );
if (managerKey != null)
{
wcSession().transientState().removeObjectForKey(managerKey);
}
return result;
}
// ----------------------------------------------------------
public void changeWorkflow()
{
breakWorkflow = true;
}
// ----------------------------------------------------------
/**
* Retrieve an NSMutableDictionary used to hold transient settings for
* this page (data that is not database-backed).
* @return A map of transient settings
*/
public NSMutableDictionary<String, Object> transientState()
{
return peerContextManager().transientState();
}
//~ Protected Methods .....................................................
// ----------------------------------------------------------
protected String stringValueForKey(
NSDictionary<String, Object> dict, String key)
{
Object value = dict.valueForKey( key );
if ( value != null && value instanceof NSArray )
{
NSArray<?> values = (NSArray<?>)value;
if ( values.count() == 1 )
{
value = values.objectAtIndex( 0 );
}
}
return value == null ? null : value.toString();
}
//~ Private Methods .......................................................
// ----------------------------------------------------------
private WOComponent internalNext(boolean save)
{
if ( hasMessages() )
{
return null;
}
if (save && nextPerformsSave)
{
if (!applyLocalChanges())
{
return null;
}
}
if ( nextPage != null )
{
if (breakWorkflow)
{
breakWorkflow = false;
}
else if (nextPage.peerContextManager == null
&& peerContextManager != null)
{
nextPage.peerContextManager = peerContextManager;
nextPage.alreadyGrabbed = true;
}
return nextPage;
}
else
{
try
{
return pageWithName(
currentTab().nextSibling().select().pageName() );
}
catch (NullPointerException e)
{
log.error("exception selecting next tab from "
+ currentTab().printableTabLocationDetails(), e);
// Assume something is broken w/ the current tab selection
return pageWithName(
wcSession().tabs.selectDefault().pageName());
}
}
}
// ----------------------------------------------------------
private WCComponent outermostWCComponent()
{
WCComponent result = null;
WOComponent ancestor = parent();
while (ancestor != null)
{
if (ancestor instanceof WCComponent)
{
result = (WCComponent)ancestor;
}
ancestor = ancestor.parent();
}
return result;
}
// ----------------------------------------------------------
private void grabTaskECManagerIfNecessary()
{
if (alreadyGrabbed)
{
log.debug("grabTaskECManagerIfNecessary(): "
+ getClass().getName() + "(" + hashCode() + ") "
+ "has already grabbed");
}
else
{
// First, check to see if the top-level ancestor has one
WCComponent outer = outermostWCComponent();
if (outer != null)
{
peerContextManager = outer.peerContextManager;
}
if (peerContextManager == null)
{
String managerKey = Thread.currentThread().toString();
// set up nested ec for this task, if there is one
peerContextManager = (WCEC.PeerManager)wcSession()
.transientState().valueForKey(managerKey);
if (log.isDebugEnabled())
{
log.debug("manager tag = " + managerKey );
log.debug("grabTaskECManagerIfNecessary(): "
+ getClass().getName() + "(" + hashCode() + ") "
+ "childContextManager = " + peerContextManager);
}
}
alreadyGrabbed = true;
}
}
// ----------------------------------------------------------
private WCEC.PeerManager peerContextManager()
{
if (peerContextManager == null)
{
if (log.isDebugEnabled())
{
log.debug("localContext(): attempting to grab from "
+ getClass().getName() + "(" + hashCode() + ")");
}
grabTaskECManagerIfNecessary();
if (peerContextManager == null)
{
if (log.isDebugEnabled())
{
log.debug("localContext(): creating manager for "
+ getClass().getName() + "(" + hashCode() + ")");
}
peerContextManager =
wcSession().createManagedPeerEditingContext();
}
}
return peerContextManager;
}
// ----------------------------------------------------------
/**
* Save all child context changes to the default editing context, then
* commit them to the database.
*/
private void commitLocalChanges()
{
log.debug( "commitLocalChanges()" );
if (peerContextManager != null)
{
try
{
// Make sure to grab the lock, in case this EC hasn't been
// used for anything yet in this RR-loop and Wonder hasn't
// auto-locked it yet.
peerContextManager.editingContext().lock();
peerContextManager.editingContext().saveChanges();
peerContextManager.editingContext().revert();
peerContextManager.editingContext().refaultAllObjects();
}
finally
{
peerContextManager.editingContext().unlock();
}
}
else
{
wcSession().commitSessionChanges();
}
// if (childContextManager != null)
// {
// childContextManager.editingContext().saveChanges();
// }
// wcSession().commitSessionChanges();
// if (childContextManager != null)
// {
// childContextManager.editingContext().revert();
// childContextManager.editingContext().refaultAllObjects();
// }
}
//~ Instance/static variables .............................................
private TabDescriptor currentTab;
private WCEC.PeerManager peerContextManager;
private User user;
private boolean alreadyGrabbed;
private boolean breakWorkflow;
static Logger log = Logger.getLogger( WCComponent.class );
}