/*==========================================================================*\
| $Id: WCLink.java,v 1.3 2010/10/28 00:37:30 aallowat Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2006-2008 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.ui;
import org.apache.log4j.Logger;
import org.webcat.ui.util.DojoRemoteHelper;
import org.webcat.ui.util.JSHash;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOApplication;
import com.webobjects.appserver.WOAssociation;
import com.webobjects.appserver.WOComponent;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WOElement;
import com.webobjects.appserver.WOPageNotFoundException;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver.WOResponse;
import com.webobjects.appserver._private.WOCGIFormValues;
import com.webobjects.appserver._private.WODynamicElementCreationException;
import com.webobjects.appserver._private.WOHTMLDynamicElement;
import com.webobjects.appserver._private.WONoContentElement;
import com.webobjects.appserver._private.WOStaticURLUtilities;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation._NSDictionaryUtilities;
import er.ajax.AjaxUtils;
import er.extensions.components.ERXComponentUtilities;
import er.extensions.components._private.ERXWOForm;
//------------------------------------------------------------------------
/**
* <p>
* A hyperlink that can execute an action, both in the form of an ordinary
* page-load and via an Ajax interface. Another enhancement compared to
* WOHyperlink is that WCLinks that are nested in a form cause that form to be
* submitted (causing bindings on the page to be synchronized, just as with
* submit buttons).
* </p><p>
* Unlike the other action elements such as WCButton, WCMenuItem, etc., the
* WCLink is an ordinary HTML anchor tag and <b>not</b> a Dojo widget. This
* means that, unlike those other elements, you cannot use embedded script tags
* with <tt>type="dojo/connect"</tt> to hook up event handlers. Instead, use
* the attributes/bindings <tt>onRemoteLoad</tt>, <tt>onRemoteError</tt>, and
* <tt>onRemoteEnd</tt> for this functionality.
* </p>
*
* <h2>Bindings</h2>
* <table>
* <tr>
* <td>{@code action}</td>
* <td>The action method to invoke when this element is activated.</td>
* </tr>
* <tr>
* <td>{@code directActionName}</td>
* <td>The name of the direct action method (minus the "Action" suffix) to
* invoke when this element is activated. Defaults to "default".</td>
* </tr>
* <tr>
* <td>{@code actionClass}</td>
* <td>The name of the class in which the method designated in
* <tt>directActionName</tt> can be found. Defaults to DirectAction.</td>
* </tr>
* <tr>
* <td>{@code pageName}</td>
* <td>The name of a WebObjects component that should be instantiated and
* returned.</td>
* </tr>
* <tr>
* <td>{@code ignoreForm}</td>
* <td>Normally, links inside forms cause that form to be submitted so that
* bindings are synchronized before the action is executed. If this is set to
* true, the link will not submit the form (duplicating the functionality of
* built-in WOHyperlinks).</td>
* </tr>
* <tr>
* <td>{@code onClick}</td>
* <td>JavaScript code that is executed in the browser when the element is
* clicked, before the action is invoked.</td>
* </tr>
* <tr>
* <td>{@code remote}</td>
* <td>If <tt>false</tt> or unspecified, the action is a traditional
* synchronous action. If <tt>true</tt>, the action is executed via an Ajax
* request. This defaults to false unless any of the other "remote.*" bindings
* are specified, in which case this is assumed to be true. Therefore, it is
* not necessary to explicitly use this binding unless you want an Ajax request
* that does not use any of the other "remote.*" bindings.</td>
* </tr>
* <tr>
* <td>{@code remote.responseType}</td>
* <td>A string indicating how the action response should be treated, and in
* which format it will be passed to the callback functions. Choices are "text"
* (the default), where the response will be sent to the callback as a string;
* "javascript", where the response is JavaScript code that will be executed
* before the callback is called; "json", "json-comment-optional",
* "json-comment-filtered", where the response is a JSON string that will be
* evaluated and passed to the callback as an object; and "xml", where the
* response is XML text that is parsed and passed to the callback as a Document
* DOM object.</td>
* </tr>
* <tr>
* <td>@{code remote.synchronous}</td>
* <td>A boolean value indicating that the request should be synchronous
* instead of asynchronous. Use this with caution as a long-running server-side
* action will cause the browser to appear hung.
* </td>
* </tr>
* <tr>
* <td>{@code onRemoteLoad}</td>
* <td>A two-argument JavaScript function(response, ioArgs) called upon a
* successful HTTP response code. The argument "response" is the response
* returned from the action (see <tt>remote.responseType</tt>), and "ioArgs" is
* defined as in dojo.xhr. <b>This function should return the response for
* proper callback chaining.</b></td>
* </tr>
* <tr>
* <td>{@code onRemoteError}</td>
* <td>A two-argument JavaScript function(response, ioArgs) called upon an
* unsuccessful HTTP response code. The argument "response" is the response
* returned from the action (see <tt>remote.responseType</tt>), and "ioArgs" is
* defined as in dojo.xhr. <b>This function should return the response for
* proper callback chaining.</b></td>
* </tr>
* <tr>
* <td>{@code onRemoteEnd}</td>
* <td>A two-argument JavaScript function(response, ioArgs) called upon the end
* of the request, regardless of the HTTP response code. This function will be
* called <b>after</b> <tt>remote.onLoad</tt> or <tt>remote.onError</tt> if
* either of those is also specified. The argument "response" is the response
* returned from the action (see <tt>remote.responseType</tt>), and "ioArgs" is
* defined as in dojo.xhr. <b>This function should return the response for
* proper callback chaining.</b></td>
* </tr>
* </table>
*
* @author Tony Allevato
* @version $Id: WCLink.java,v 1.3 2010/10/28 00:37:30 aallowat Exp $
*/
public class WCLink extends WOHTMLDynamicElement
{
//~ Constructor ...........................................................
// ----------------------------------------------------------
@SuppressWarnings("unchecked")
public WCLink(String name,
NSDictionary<String, WOAssociation> someAssociations,
WOElement template)
{
super("a", someAssociations, template);
_otherQueryAssociations =
_NSDictionaryUtilities.extractObjectsForKeysWithPrefix(
_associations, "?", true);
if (_otherQueryAssociations == null
|| _otherQueryAssociations.count() <= 0)
{
_otherQueryAssociations = null;
}
_action = _associations.removeObjectForKey("action");
_string = _associations.removeObjectForKey("string");
_href = _associations.removeObjectForKey("href");
_disabled = _associations.removeObjectForKey("disabled");
_queryDictionary = _associations.removeObjectForKey("queryDictionary");
_actionClass = _associations.removeObjectForKey("actionClass");
_directActionName =
_associations.removeObjectForKey("directActionName");
_pageName = _associations.removeObjectForKey("pageName");
_fragmentIdentifier =
_associations.removeObjectForKey("fragmentIdentifier");
_escapeHTML = _associations.removeObjectForKey("escapeHTML");
_onClick = _associations.removeObjectForKey("onClick");
_ignoreForm = _associations.removeObjectForKey("ignoreForm");
_remoteHelper = new DojoRemoteHelper(_associations);
if (_action == null && _href == null && _pageName == null
&& _directActionName == null && _actionClass == null)
{
throw new WODynamicElementCreationException("<"
+ getClass().getName()
+ "> Missing required attribute: 'action' or 'href' or "
+ "'pageName' or 'directActionName' or 'actionClass'");
}
if ((_action != null && _href != null)
|| (_action != null && _pageName != null)
|| (_href != null && _pageName != null)
|| (_action != null && _directActionName != null)
|| (_href != null && _directActionName != null)
|| (_pageName != null && _directActionName != null)
|| (_action != null && _actionClass != null))
{
throw new WODynamicElementCreationException("<"
+ getClass().getName()
+ "> At least two of these conflicting attributes are "
+ "present: 'action', 'href', 'pageName', "
+ "'directActionName', 'actionClass'.");
}
if (_action != null && _action.isValueConstant())
{
throw new WODynamicElementCreationException("<"
+ getClass().getName() + "> 'action' is a constant.");
}
}
//~ Methods ...............................................................
// ----------------------------------------------------------
@Override
protected void _appendOpenTagToResponse(WOResponse response,
WOContext context)
{
if(!isDisabledInContext(context))
{
super._appendOpenTagToResponse(response, context);
}
else
{
response.appendContentString("<span");
appendAttributesToResponse(response, context);
response.appendContentCharacter('>');
}
}
// ----------------------------------------------------------
@Override
protected void _appendCloseTagToResponse(WOResponse response,
WOContext context)
{
if(!isDisabledInContext(context))
{
super._appendCloseTagToResponse(response, context);
}
else
{
response.appendContentString("</span>");
}
}
// ----------------------------------------------------------
protected void _appendQueryStringToResponse(WOResponse response,
WOContext context, String aRequestHandlerPath,
boolean htmlEscapeURL, boolean defaultIncludeSessionID)
{
NSDictionary<String, Object> queryDict =
computeQueryDictionaryInContext(
aRequestHandlerPath == null ? "" : aRequestHandlerPath,
_queryDictionary, _otherQueryAssociations,
defaultIncludeSessionID, context);
if (queryDict.count() > 0)
{
String queryString =
WOCGIFormValues.getInstance().encodeAsCGIFormValues(
queryDict, htmlEscapeURL);
if (queryString.length() > 0)
{
int questionMarkIndex = aRequestHandlerPath == null ? -1 :
aRequestHandlerPath.indexOf("?");
if (questionMarkIndex > 0)
{
response.appendContentString(htmlEscapeURL ? "&" : "&");
}
else
{
response.appendContentCharacter('?');
}
response.appendContentString(queryString);
}
}
}
// ----------------------------------------------------------
protected void _appendFragmentToResponse(WOResponse response,
WOContext context)
{
String fragmentIdentifier = fragmentIdentifierInContext(context);
if (fragmentIdentifier.length() > 0)
{
response.appendContentCharacter('#');
response.appendContentString(fragmentIdentifier);
}
}
// ----------------------------------------------------------
protected void _appendCGIActionURLToResponse(WOResponse response,
WOContext context, boolean htmlEscapeURL)
{
String actionPath = computeActionStringInContext(_actionClass,
_directActionName, context);
NSDictionary<String, Object> queryDict =
computeQueryDictionaryInContext(actionPath, _queryDictionary,
_otherQueryAssociations, true, context);
response.appendContentString(context._directActionURL(actionPath,
queryDict, secureInContext(context), 0, htmlEscapeURL));
_appendFragmentToResponse(response, context);
}
// ----------------------------------------------------------
protected void _appendComponentActionURLToResponse(WOResponse response,
WOContext context, boolean escapeHTML)
{
String actionURL = context.componentActionURL(
WOApplication.application().componentRequestHandlerKey(),
secureInContext(context));
response.appendContentString(actionURL);
_appendQueryStringToResponse(response, context, actionURL, escapeHTML,
true);
_appendFragmentToResponse(response, context);
}
// ----------------------------------------------------------
protected void _appendStaticURLToResponse(WOResponse response,
WOContext context, boolean escapeHTML)
{
String staticURL = hrefInContext(context);
if (WOStaticURLUtilities.isRelativeURL(staticURL) &&
!WOStaticURLUtilities.isFragmentURL(staticURL))
{
String resourceURL =
context._urlForResourceNamed(staticURL, null, false);
if (resourceURL != null)
{
response.appendContentString(resourceURL);
staticURL = resourceURL;
}
else
{
response.appendContentString(context.component().baseURL());
response.appendContentCharacter('/');
response.appendContentString(staticURL);
}
}
else
{
response.appendContentString(staticURL);
}
_appendQueryStringToResponse(response, context, staticURL, escapeHTML,
false);
_appendFragmentToResponse(response, context);
}
// ----------------------------------------------------------
@Override
public void appendAttributesToResponse(WOResponse response,
WOContext context)
{
super.appendAttributesToResponse(response, context);
if (isDisabledInContext(context))
{
return;
}
if (_remoteHelper.isRemoteInContext(context))
{
response.appendContentString(" href=\"javascript:void(0);\"");
}
else
{
String formName = ERXWOForm.formName(context, null);
if (!isIgnoreFormInContext(context) && formName != null)
{
response.appendContentString(" href=\"javascript:void(0);\"");
}
else
{
_appendOpeningHrefToResponse(response, context);
if (_actionClass != null || _directActionName != null)
{
_appendCGIActionURLToResponse(response, context, true);
}
else if(_action != null || _pageName != null)
{
_appendComponentActionURLToResponse(response, context, true);
}
else if(_href != null)
{
_appendStaticURLToResponse(response, context, true);
}
else if(_fragmentIdentifier != null
&& fragmentIdentifierInContext(context).length() > 0)
{
_appendQueryStringToResponse(response, context, "", true, true);
_appendFragmentToResponse(response, context);
}
_appendClosingHrefToResponse(response, context);
}
}
appendOnClickAttributeToResponse(response, context);
}
// ----------------------------------------------------------
protected void _appendOpeningHrefToResponse(WOResponse response,
WOContext context)
{
response.appendContentCharacter(' ');
response.appendContentString("href");
response.appendContentCharacter('=');
response.appendContentCharacter('"');
String prefix = prefixInContext(context);
if (prefix.length() > 0)
{
response.appendContentString(prefix);
}
}
// ----------------------------------------------------------
protected void _appendClosingHrefToResponse(WOResponse response,
WOContext context)
{
String suffix = suffixInContext(context);
if (suffix.length() > 0)
{
response.appendContentString(suffix);
}
response.appendContentCharacter('"');
}
// ----------------------------------------------------------
public void appendContentStringToResponse(WOResponse response,
WOContext context)
{
if (_string != null)
{
WOComponent component = context.component();
Object val = _string.valueInComponent(component);
if (val != null)
{
String valueToAppend = val.toString();
boolean shouldEscapeHTML = true;
if (_escapeHTML != null)
{
shouldEscapeHTML =
_escapeHTML.booleanValueInComponent(component);
}
if (shouldEscapeHTML)
{
response.appendContentHTMLString(valueToAppend);
}
else
{
response.appendContentString(valueToAppend);
}
}
}
}
// ----------------------------------------------------------
@Override
public void appendChildrenToResponse(WOResponse response, WOContext context)
{
super.appendChildrenToResponse(response, context);
appendContentStringToResponse(response, context);
}
// ----------------------------------------------------------
protected String hrefInContext(WOContext context)
{
Object value = null;
if (_href != null)
{
value = _href.valueInComponent(context.component());
}
if (value != null)
{
return value.toString();
}
else
{
return "";
}
}
// ----------------------------------------------------------
protected String fragmentIdentifierInContext(WOContext context)
{
Object value = null;
if (_fragmentIdentifier != null)
{
value = _fragmentIdentifier.valueInComponent(context.component());
}
if (value != null)
{
return value.toString();
}
else
{
return "";
}
}
// ----------------------------------------------------------
protected boolean isDisabledInContext(WOContext context)
{
if (!isRenderedInContext(context))
{
return true;
}
if (_disabled != null)
{
return _disabled.booleanValueInComponent(context.component());
}
else
{
return false;
}
}
// ----------------------------------------------------------
protected boolean isIgnoreFormInContext(WOContext context)
{
if (_ignoreForm != null)
{
return _ignoreForm.booleanValueInComponent(context.component());
}
else
{
return false;
}
}
// ----------------------------------------------------------
protected void appendOnClickAttributeToResponse(WOResponse response,
WOContext context)
{
String onClick = null;
if (_onClick != null)
{
onClick = _onClick.valueInComponent(context.component()).toString();
}
if (_remoteHelper.isRemoteInContext(context))
{
response.appendContentString(" onclick=\"");
if (onClick != null)
{
response.appendContentString(onClick);
if (!onClick.endsWith(";"))
{
response.appendContentCharacter(';');
}
}
appendXhrGetToResponse(response, context);
response.appendContentString("\"");
}
else
{
String formName = ERXWOForm.formName(context, null);
if (!isIgnoreFormInContext(context) && formName != null)
{
String senderID = context.elementID();
if (onClick == null)
{
onClick = "";
}
onClick += "; webcat.fullSubmit('"
+ formName + "','" + senderID + "');";
}
if (onClick != null)
{
response.appendContentString(" onclick=\"");
response.appendContentString(onClick);
response.appendContentString("\"");
}
}
}
// ----------------------------------------------------------
@SuppressWarnings("unchecked")
protected void appendXhrGetToResponse(WOResponse response,
WOContext context)
{
WOComponent component = context.component();
String actionUrl = null;
if (_directActionName != null)
{
actionUrl = context.directActionURLForActionNamed(
(String) _directActionName.valueInComponent(component),
ERXComponentUtilities.queryParametersInComponent(
_associations, component)).replaceAll("&", "&");
}
else
{
actionUrl = AjaxUtils.ajaxComponentActionUrl(context);
}
JSHash requestOptions = new JSHash();
requestOptions.put("url", actionUrl);
// If we're inside a form, include the name of the element as the
// sender. This is unnecessary for buttons, but required for other
// types of action elements, like WCMenuItem.
String formName = ERXWOForm.formName(context, null);
if (!isIgnoreFormInContext(context) && formName != null)
{
requestOptions.put("sender", context.elementID());
}
response.appendContentString(_remoteHelper.remoteSubmitCall(
"this", requestOptions, context));
}
// ----------------------------------------------------------
public WOActionResults invokeAction(WORequest request, WOContext context)
{
if (AjaxUtils.isAjaxRequest(request) &&
AjaxUtils.shouldHandleRequest(request, context, null))
{
return invokeRemoteAction(request, context);
}
else
{
return invokeStandardAction(request, context);
}
}
// ----------------------------------------------------------
protected boolean shouldHandleAction(WORequest request, WOContext context)
{
return (context.elementID().equals(context.senderID())) ||
(context.wasFormSubmitted() && context.isMultipleSubmitForm()
&& request.formValueForKey(context.elementID()) != null);
}
// ----------------------------------------------------------
public WOActionResults invokeStandardAction(WORequest aRequest,
WOContext context)
{
String nextPageName = null;
WOActionResults invokedElement = null;
WOComponent component = context.component();
if (shouldHandleAction(aRequest, context))
{
if (_disabled == null
|| !_disabled.booleanValueInComponent(component))
{
context.setActionInvoked(true);
if (_pageName != null)
{
Object nextPageValue =
_pageName.valueInComponent(component);
if (nextPageValue != null)
{
nextPageName = nextPageValue.toString();
}
}
if (_action != null)
{
invokedElement =
(WOActionResults)_action.valueInComponent(component);
}
else
{
if (_pageName == null)
{
throw new IllegalStateException("<"
+ getClass().getName()
+ "> : Missing page name.");
}
if (nextPageName != null)
{
invokedElement =
WOApplication.application().pageWithName(
nextPageName, context);
}
else
{
throw new WOPageNotFoundException("<"
+ getClass().getName()
+ "> : cannot find page.");
}
}
}
else
{
invokedElement = new WONoContentElement();
}
if (invokedElement == null)
{
invokedElement = context.page();
}
}
return invokedElement;
}
// ----------------------------------------------------------
protected WOActionResults invokeRemoteAction(WORequest request,
WOContext context)
{
WOActionResults result = null;
WOComponent component = context.component();
AjaxUtils.createResponse(request, context);
AjaxUtils.mutableUserInfo(request);
context.setActionInvoked(true);
if (_action != null)
{
result = (WOActionResults) _action.valueInComponent(component);
}
AjaxUtils.updateMutableUserInfoWithAjaxInfo(context);
if (result == context.page())
{
log.warn("An Ajax request attempted to return the page, which "
+ "is almost certainly an error.");
result = null;
}
if (result == null)
{
result = AjaxUtils.createResponse(request, context);
}
return result;
}
//~ Static/instance variables .............................................
protected WOAssociation _action;
protected WOAssociation _string;
protected WOAssociation _pageName;
protected WOAssociation _href;
protected WOAssociation _disabled;
protected WOAssociation _fragmentIdentifier;
protected WOAssociation _escapeHTML;
protected WOAssociation _queryDictionary;
protected WOAssociation _actionClass;
protected WOAssociation _directActionName;
protected WOAssociation _onClick;
protected WOAssociation _ignoreForm;
protected NSDictionary<String, WOAssociation> _otherQueryAssociations;
protected DojoRemoteHelper _remoteHelper;
private static final Logger log = Logger.getLogger(WCLink.class);
}