/*
* Copyright (C) NetStruxr, Inc. All rights reserved.
*
* This software is published under the terms of the NetStruxr
* Public Software License version 0.5, a copy of which has been
* included with this distribution in the LICENSE.NPL file. */
package er.directtoweb.delegates;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Enumeration;
import java.util.Iterator;
import org.apache.log4j.Logger;
import com.webobjects.appserver.WOComponent;
import com.webobjects.directtoweb.D2WContext;
import com.webobjects.directtoweb.NextPageDelegate;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import er.directtoweb.ERDirectToWeb;
import er.directtoweb.interfaces.ERDMessagePageInterface;
import er.directtoweb.pages.ERD2WPage;
import er.extensions.foundation.ERXArrayUtilities;
import er.extensions.foundation.ERXDictionaryUtilities;
import er.extensions.localization.ERXLocalizer;
/**
* The branch delegate is used in conjunction with the
* {@link ERDMessagePageInterface ERDMessagePageInterface} to allow
* flexible branching for message pages. Branch delegates can
* only be used with templates that implement the
* {@link ERDBranchInterface ERDBranchInterface}.
*/
public abstract class ERDBranchDelegate implements ERDBranchDelegateInterface {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
/**
* Runtime flags for the delegate, so you can have one delegate for all tasks.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface D2WDelegate {
/**
* Returns the names of the scope where you can have this method. One of "selection,object"
*/
public String scope() default "";
/**
* Returns the names of the tasks where you can have this method. Example "query,select"
*/
public String availableTasks() default "";
/**
* Returns the names of the pages where you can have this method. Example "ListWebMaster,QueryWebMaster"
*/
public String availablePages() default "";
}
/** logging support */
public final static Logger log = Logger.getLogger(ERDBranchDelegate.class);
/** holds the WOComponent class array used to lookup branch delegate methods */
// MOVEME: Should belong in a WO constants class
public final static Class[] WOComponentClassArray = new Class[] { WOComponent.class };
public static final String BRANCH_CHOICES = "branchChoices";
public static final String BRANCH_BUTTON_ID = "branchButtonID";
public static final String BRANCH_NAME = "branchName";
public static final String BRANCH_LABEL = "branchButtonLabel";
public static final String BRANCH_PREFIX = "Button";
/**
* Implementation of the {@link NextPageDelegate NextPageDelegate}
* interface. This method provides the dynamic dispatch based on
* the selected branch provided by the sender. Will call the
* method <branchName>(WOComponent) on itself returning the
* result.
* @param sender template invoking the branch delegate
* @return result of dynamic method lookup and execution on itself.
*/
public final WOComponent nextPage(WOComponent sender) {
WOComponent nextPage = null;
if (sender instanceof ERDBranchInterface) {
String branchName = ((ERDBranchInterface)sender).branchName();
if( branchName != null ) {
if (log.isDebugEnabled())
log.debug("Branching to branch: " + branchName);
try {
Method m = getClass().getMethod(branchName, WOComponentClassArray);
nextPage = (WOComponent)m.invoke(this, new Object[] { sender });
} catch (InvocationTargetException ite) {
log.error("Invocation exception occurred in ERBranchDelegate: " + ite.getTargetException() + " for branch name: " + branchName, ite.getTargetException());
throw new NSForwardException(ite.getTargetException());
} catch (Exception e) {
log.error("Exception occurred in ERBranchDelegate: " + e.toString() + " for branch name: " + branchName);
throw new NSForwardException(e);
}
}
} else {
log.warn("Branch delegate being used with a component that does not implement the ERBranchInterface");
}
return nextPage;
}
/**
* Utility to build branch choice dictionaries in code.
* @param method name of the method in question
* @param label label for the button, a beautified method name will be used if set to null.
* @return NSDictionary suitable as a branch choice.
*/
protected NSDictionary branchChoiceDictionary(String method, String label) {
if(label == null) {
label = ERXLocalizer.currentLocalizer().localizedDisplayNameForKey(BRANCH_PREFIX, method);
}
return ERXDictionaryUtilities.dictionaryWithObjectsAndKeys(new Object [] { method, BRANCH_NAME, label, BRANCH_LABEL, method + "Action", BRANCH_BUTTON_ID});
}
/**
* Calculates which branches to show in the display first
* asking the context for the key <b>branchChoices</b>. If
* this returns null then
* @param context current D2W context
* @return array of branch names.
*/
public NSArray branchChoicesForContext(D2WContext context) {
NSArray choices = (NSArray)context.valueForKey(BRANCH_CHOICES);
if (choices == null) {
choices = defaultBranchChoices(context);
} else {
NSMutableArray translatedChoices = new NSMutableArray();
for (Iterator iter = choices.iterator(); iter.hasNext();) {
Object o = iter.next();
String method = null;
String label = null;
NSMutableDictionary entry = new NSMutableDictionary();
if (o instanceof NSDictionary) {
entry.addEntriesFromDictionary((NSDictionary) o);
method = (String) entry.objectForKey(BRANCH_NAME);
label = (String) entry.objectForKey(BRANCH_LABEL);
} else if (o instanceof String) {
method = (String) o;
entry.setObjectForKey(method, BRANCH_NAME);
}
if (label == null) {
label = ERXLocalizer.currentLocalizer().localizedDisplayNameForKey(BRANCH_PREFIX, method);
} else if(label.startsWith(BRANCH_PREFIX + ".")){
String localizerKey = label;
String localized = ERXLocalizer.currentLocalizer().localizedStringForKey(label);
if(localized == null) {
label = ERXLocalizer.currentLocalizer().localizedDisplayNameForKey(BRANCH_PREFIX, method);
ERXLocalizer.currentLocalizer().takeValueForKey(label, localizerKey);
} else {
label = localized;
}
} else {
// assume it's a user-provided value. If we have an entry in the localizer, use it
// otherwise just return it.
label = ERXLocalizer.currentLocalizer().localizedStringForKeyWithDefault(label);
}
entry.setObjectForKey(label, BRANCH_LABEL);
entry.setObjectForKey(method + "Action", BRANCH_BUTTON_ID);
translatedChoices.addObject(entry);
}
choices = translatedChoices;
}
return choices;
}
/**
* Uses reflection to find all of the public methods that don't start with
* an underscore and take a single WOComponent as a parameter are returned.
* The methods are sorted by this key.
* @param context current D2W context
*/
protected NSArray defaultBranchChoices(D2WContext context) {
NSArray choices = NSArray.EmptyArray;
try {
String task = context.task();
String pageName = context.dynamicPage();
NSMutableArray methodChoices = new NSMutableArray();
Method methods[] = getClass().getMethods();
for (Enumeration e = new NSArray(methods).objectEnumerator(); e.hasMoreElements();) {
Method method = (Method)e.nextElement();
if (method.getParameterTypes().length == 1
&& method.getParameterTypes()[0] == WOComponent.class
&& !method.getName().equals("nextPage")
&& method.getName().charAt(0) != '_'
&& ((method.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC)
) {
boolean isAllowed = true;
if(method.isAnnotationPresent(D2WDelegate.class)) {
D2WDelegate info = method.getAnnotation(D2WDelegate.class);
String scope = info.scope();
String availableTasks = info.availableTasks();
String availablePages = info.availablePages();
if(scope.length() > 0) {
if("object".equals(scope) && context.valueForKey("object") == null) {
isAllowed = false;
}
if(!"object".equals(scope) && context.valueForKey("object") != null) {
isAllowed = false;
}
}
if(availableTasks.length() > 0 && !availableTasks.contains(task)) {
isAllowed = false;
}
if(availablePages.length() > 0 && !availablePages.contains(pageName)) {
isAllowed = false;
}
}
if(isAllowed) {
NSDictionary branch = branchChoiceDictionary(method.getName(), null);
methodChoices.addObject(branch);
}
}
}
choices = ERXArrayUtilities.sortedArraySortedWithKey(methodChoices, BRANCH_LABEL);
} catch (SecurityException e) {
log.error("Caught security exception while calculating the branch choices for delegate: "
+ this + " exception: " + e.getMessage());
}
return choices;
}
/**
* Gets the D2W context from the innermost enclosing D2W component of the sender.
* @param sender
*/
protected D2WContext d2wContext(WOComponent sender) {
if(ERDirectToWeb.D2WCONTEXT_SELECTOR.implementedByObject(sender)) {
return (D2WContext) sender.valueForKey(ERDirectToWeb.D2WCONTEXT_SELECTOR.name());
}
throw new IllegalStateException("Can't figure out d2wContext from: " + sender);
}
/**
* return the innermost object which might be of interest
* @param sender
*/
protected EOEnterpriseObject object(WOComponent sender) {
return object(d2wContext(sender));
}
/**
* Returns the current object form the d2w context
* @param context
*/
protected EOEnterpriseObject object(D2WContext context) {
return (EOEnterpriseObject) context.valueForKey(ERD2WPage.Keys.object);
}
/**
* Utility to remove entries based on an array of keys
* @param keys
* @param choices
*/
protected NSArray choiceByRemovingKeys(NSArray keys, NSArray choices) {
NSMutableArray result = new NSMutableArray(choices.count());
for (Enumeration e = choices.objectEnumerator(); e.hasMoreElements();) {
NSDictionary choice = (NSDictionary) e.nextElement();
if(!keys.containsObject(choice.objectForKey(BRANCH_NAME))) {
result.addObject(choice);
}
}
return result;
}
/**
* Utility to leave entries based on an array of keys
* @param keys
* @param choices
*/
protected NSArray choiceByLeavingKeys(NSArray keys, NSArray choices) {
NSMutableArray result = new NSMutableArray(choices.count());
for (Enumeration e = choices.objectEnumerator(); e.hasMoreElements();) {
NSDictionary choice = (NSDictionary) e.nextElement();
if(keys.containsObject(choice.objectForKey(BRANCH_NAME))) {
result.addObject(choice);
}
}
return result;
}
}