/* ****************************************************************************
* CIShell: Cyberinfrastructure Shell, An Algorithm Integration Framework.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Apache License v2.0 which accompanies
* this distribution, and is available at:
* http://www.apache.org/licenses/LICENSE-2.0.html
*
* Created on Jun 16, 2006 at Indiana University.
*
* Contributors:
* Indiana University -
* ***************************************************************************/
package org.cishell.reference.gui.menumanager.menu;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.cishell.framework.CIShellContext;
import org.cishell.framework.algorithm.AlgorithmFactory;
import org.cishell.framework.algorithm.AlgorithmProperty;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class MenuAdapter implements AlgorithmProperty {
public static final String DEFAULT_MENU_FILE_NAME = "default_menu.xml";
// Tags in DEFAULT_MENU_FILE_NAME.
public static final String TAG_TOP_MENU = "top_menu";
public static final String TAG_MENU = "menu";
public static final String TYPE_ATTRIBUTE = "type";
public static final String NAME_ATTRIBUTE = "name";
public static final String PID_ATTRIBUTE = "pid";
public static final String PRESERVED_GROUP = "group";
public static final String PRESERVED_BREAK = "break";
public static final String PRESERVED_EXIT = "Exit";
public static final String PRESERVED_SERVICE_PID = "service.pid";
public static final String PRESERVED = "preserved";
public static final String HELP_DESK_EMAIL_ADDRESS = "nwb-helpdesk@googlegroups.com";
private String toolName;
private String toolTicketURL;
private IMenuManager menuManager;
private Shell shell;
private BundleContext bundleContext;
private CIShellContext ciShellContext;
private Map<String, Action> algorithmsToActions;
private Map<Action, IMenuManager> actionsToMenuManagers;
private ContextListener contextListener;
private IWorkbenchWindow workbenchWindow;
private LogService logger;
/*
* This map holds a pid as a key and the corresponding ServiceReference as a value.
* It is built when preprocessServiceBundles() is invoked.
* Then the entries are gradually removed when the pids are specified in
* DEFAULT_MENU_FILE_NAME.
* If any entries are left, in processLeftServiceBundles(), those plug-ins that have specified
* the menu_path and label but are not listed in DEFAULT_MENU_FILE_NAME will be added on to
* the menu.
*/
private Map<String, ServiceReference> pidsToServiceReferences;
/*
* This is the exactly same copy of pidsToServiceReferences.
* Since some plug-ins could display on menu more than once, it provides a map between a pid
* and a serviceReference while in pidsToServiceReferences that pid has been removed.
*/
private Map<String, ServiceReference> pidsToServiceReferencesCopy;
private Document documentObjectModel;
private Runnable updateAction = new Runnable() {
public void run() {
MenuAdapter.this.menuManager.updateAll(true);
}
};
private Runnable stopAction = new Runnable() {
public void run() {
String[] algorithmKeys =
MenuAdapter.this.algorithmsToActions.keySet().toArray(new String[]{});
for (int ii = 0; ii < algorithmKeys.length; ii++) {
Action item = MenuAdapter.this.algorithmsToActions.get(algorithmKeys[ii]);
IMenuManager targetMenu = MenuAdapter.this.actionsToMenuManagers.get(item);
targetMenu.remove(item.getId());
MenuAdapter.this.algorithmsToActions.remove(algorithmKeys[ii]);
MenuAdapter.this.actionsToMenuManagers.remove(item);
}
}
};
public MenuAdapter(
String toolName,
String toolTicketURL,
IMenuManager menuManager,
Shell shell,
BundleContext bundleContext,
CIShellContext ciShellContext,
IWorkbenchWindow workbenchWindow) {
this.toolName = toolName;
this.toolTicketURL = toolTicketURL;
this.menuManager = menuManager;
this.shell = shell;
this.bundleContext = bundleContext;
this.ciShellContext = ciShellContext;
this.workbenchWindow = workbenchWindow;
this.algorithmsToActions = new HashMap<String, Action>();
this.actionsToMenuManagers = new HashMap<Action, IMenuManager>();
this.pidsToServiceReferences = new HashMap<String, ServiceReference>();
this.pidsToServiceReferencesCopy = new HashMap<String, ServiceReference>();
this.logger = (LogService) this.ciShellContext.getService(LogService.class.getName());
/*
* The intention of this clearShortcuts was to programmatically clear all of the
* bound shortcuts, so any found in our own plugins wouldn't have conflicts.
* Doing this doesn't immediately seem very possible, though there may be some promise in
* "redirecting" the actions taken by the shortcuts.
* (See: http://dev.eclipse.org/newslists/news.eclipse.platform/msg79882.html )
* As a note, the keyboard shortcuts are actually called accelerator key codes, and the
* machinery that makes them work is very deeply ingrained in Eclipse.
* The difficulties I faced with trying to clear already-bound shortcuts has led me to
* suspect three possible things:
* We may be "abusing" Eclipse by using it as the foundation for our own applications.
* There may be a way to customize/configure (or interface with) the culprit plugin
* that's binding the shortcuts before us (org.eclipse.ui.workbench). This seems to be
* highly-likely, and more research on the matter can probably be justified at
* some point.
* The intended way TO work around the shortcuts already being bound is to redirect the
* actions taken by them, as mentioned above.
* Either way, to get these shortcuts working, I'm just going to use non-standard key
* combinations.
*/
//clearShortcuts();
/*
* Appears to add a context listener which updates the menu item whenever a corresponding
* change occurs in the bundle (registers, unregisters, etc...).
*/
String filter = "(" + Constants.OBJECTCLASS + "=" + AlgorithmFactory.class.getName() + ")";
this.contextListener = new ContextListener();
try {
bundleContext.addServiceListener(this.contextListener, filter);
preprocessServiceBundles();
String applicationLocation = System.getProperty("osgi.configuration.area");
/*
* Comments below refer to problems with earlier versions of this document.
* Keeping these around for now, as well as the system.out.printlns, until we are sure
* that the current fix works.
*/
/*
* This is a temporary fix. A bit complex to explain the observation I got so far.
* On Windows XP,
* app_location =
* file:/C:/Documents and Settings/huangb/Desktop/nwb-sept4/nwb/configuration/
* If I didn't trim "file:/", on some windows machines
* new File(fileFullpath).exists()
* will return false, and initializaMenu() will be invoked.
* When initializaMenu() is invoked, not all required top menus will show up.
* So either Bruce code or Tim's fix has some problems. Can not append top menu such as
* Tools-->Scheduler if Tools is not specified in the XML.
* If pass trimmed file path
* C:/Documents and Settings/huangb/Desktop/nwb-sept4/nwb/configuration/
* to createMenuFromXML, on some machines,
* URL = C:/Documents and Settings/huangb/Desktop/nwb-sept4/nwb/configuration/
* is a bad one, and can not create a document builder instance and the
* DOM representation of the XML file.
*
* This piece of code needs to be reviewed and refactored!!!
*/
/*
* Better to use System.err, since it prints the stream immediately instead of storing
* it in a buffer which might be lost if there is a crash.
*/
String fileFullPath = applicationLocation + DEFAULT_MENU_FILE_NAME;
URL configurationDirectoryURL = new URL(fileFullPath);
try {
configurationDirectoryURL.getContent();
//System.out.println(">>>config.ini Exists!");
createMenuFromXML(fileFullPath);
processLeftServiceBundles();
} catch (IOException ioException) {
ioException.printStackTrace();
//System.err.println("config.ini does not exist... Reverting to backup plan");
initializeMenu();
}
Display.getDefault().asyncExec(this.updateAction);
} catch (InvalidSyntaxException invalidSyntaxException) {
// TODO: Improve this error message. "Invalid Syntax" is terrible!
this.logger.log(LogService.LOG_DEBUG, "Invalid Syntax", invalidSyntaxException);
} catch (Throwable exception) {
/*
* TODO: Improve this.
* Should catch absolutely everything catchable. Will hopefully reveal the error coming
* out of the URI constructor.
* No time to test today, just commiting this for testing later.
*/
exception.printStackTrace();
}
}
/*
* This method scans all service bundles. If a bundle specifies menu_path and label,
* get service.pid of this bundle (key), let the service reference as the value, and put
* key/value pair to pidsToServiceReferences for further processing.
*/
private void preprocessServiceBundles() throws InvalidSyntaxException {
ServiceReference[] serviceReferences =
this.bundleContext.getAllServiceReferences(AlgorithmFactory.class.getName(), null);
if (serviceReferences != null){
for (int ii = 0; ii < serviceReferences.length; ii++) {
String path = (String)serviceReferences[ii].getProperty(MENU_PATH);
if (path == null) {
continue;
} else {
String pid = (String)serviceReferences[ii].getProperty(PRESERVED_SERVICE_PID);
this.pidsToServiceReferences.put(pid.toLowerCase().trim(), serviceReferences[ii]);
this.pidsToServiceReferencesCopy.put(
pid.toLowerCase().trim(), serviceReferences[ii]);
}
}
}
}
/*
* Parse DEFAULT_MENU_FILE_NAME file.
* For each menu node, get the value of the attribute pid.
* Check if the pid exists in pidsToServiceReferences.
* If so, get the action and add to the parent menu.
* If not, ignore this menu.
* At the end of each top menu or subgroup menu or before help menu, add "additions" so that
* new algorithms can be added on later.
*
* What is the reasonable logic?
* If a plug-in has been specified in the DEFAULT_MENU_FILE_NAME, always use that menu layout
* If a plug-in specified in the DEFAULT_MENU_FILE_NAME, use the menu_path specified in the
* properties file.
* If a plug-in specifies a label in the properties file, always use it.
*/
private void createMenuFromXML(String menuFilePath) throws InvalidSyntaxException{
parseXMLFile(menuFilePath);
// Get the root elememt.
Element documentElement = this.documentObjectModel.getDocumentElement();
// Get a nodelist of the top menu elements.
NodeList topMenuList = documentElement.getElementsByTagName(TAG_TOP_MENU);
if ((topMenuList != null) && (topMenuList.getLength() > 0)) {
for (int ii = 0; ii < topMenuList.getLength(); ii++) {
Element element = (Element)topMenuList.item(ii);
processTopMenu(element);
}
}
}
private void processTopMenu(Element topMenuNode) {
/*
* The File and Help menus are created in ApplicationActionBarAdvisor.java.
* This function now parses the XML file and appends the new menus/menu items to the
* correct group.
* We first check to see if the menu already exists in our MenuBar.
* If it does, we modify the already existing menu. If not, then we create a new Menu.
*
* Additional code at: org.cishell.reference.gui.workspace.ApplicationActionBarAdvisor.java
*/
String topMenuName = topMenuNode.getAttribute(NAME_ATTRIBUTE);
MenuManager topMenuBar = (MenuManager)this.menuManager.findUsingPath(topMenuName);
if (topMenuBar == null) {
topMenuBar = new MenuManager(topMenuName, topMenuName.toLowerCase());
this.menuManager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, topMenuBar);
}
// Second process submenu.
processSubMenu(topMenuNode, topMenuBar);
}
/*
* Recursively process sub menu and group menu.
*/
private void processSubMenu(Element menuNode, MenuManager parentMenuBar){
NodeList subMenuList = menuNode.getElementsByTagName(TAG_MENU);
if ((subMenuList != null) && (subMenuList.getLength() > 0)) {
for (int ii = 0; ii < subMenuList.getLength(); ii++) {
Element element = (Element)subMenuList.item(ii);
/*
* Only process direct children nodes and drop all grand or grand of grand
* children nodes.
* TODO: Why?
*/
if (!element.getParentNode().equals(menuNode)) {
continue;
}
String menuType = element.getAttribute(TYPE_ATTRIBUTE);
String algorithmPID = element.getAttribute(PID_ATTRIBUTE);
if (((menuType == null) || (menuType.length() == 0)) && (algorithmPID != null)) {
processAMenuNode(element, parentMenuBar);
} else if (menuType.equalsIgnoreCase(PRESERVED_GROUP)) {
String groupName = element.getAttribute(NAME_ATTRIBUTE);
MenuManager groupMenuBar = new MenuManager(groupName, groupName.toLowerCase());
parentMenuBar.add(groupMenuBar);
processSubMenu(element, groupMenuBar);
}
else if (menuType.equalsIgnoreCase(PRESERVED_BREAK)){
/*
* It seems that the framework automatically takes care of issues such as
* double separators and a separator at the top or bottom.
*/
parentMenuBar.add(new Separator());
}
else if (menuType.equalsIgnoreCase(PRESERVED)){
String menuName = element.getAttribute(NAME_ATTRIBUTE);
if(menuName.equalsIgnoreCase(PRESERVED_EXIT) ){
//allow to add more menu before "File/Exit"
if(parentMenuBar.getId().equalsIgnoreCase(IWorkbenchActionConstants.M_FILE)){
parentMenuBar.add(new GroupMarker(START_GROUP));
parentMenuBar.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS));
parentMenuBar.add(new GroupMarker(END_GROUP));
}
IWorkbenchAction exitAction = ActionFactory.QUIT.create(workbenchWindow);
parentMenuBar.add(new Separator());
parentMenuBar.add(exitAction);
}
}
}
//allow to append new submenu(s) at the bottom under each top menu
//except "File" and "Help"
if(!parentMenuBar.getId().equalsIgnoreCase(IWorkbenchActionConstants.M_FILE)&&
!parentMenuBar.getId().equalsIgnoreCase(IWorkbenchActionConstants.M_HELP))
{
parentMenuBar.add(new GroupMarker(START_GROUP));
parentMenuBar.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS));
parentMenuBar.add(new GroupMarker(END_GROUP));
}
}
}
/*
* TODO: Better name?
* Process a menu (algorithm).
*/
private void processAMenuNode(Element menuNode, MenuManager parentMenuBar) {
String menuName = menuNode.getAttribute(NAME_ATTRIBUTE);
String pid = menuNode.getAttribute(PID_ATTRIBUTE);
if ((pid == null) || (pid.length() == 0)) {
/*
* TODO: Check if the name is one of the preserved one, and add the default action if
* it is?
*/
} else {
// Check if the PID has registered in pidsToServiceReferences.
if (this.pidsToServiceReferencesCopy.containsKey(pid.toLowerCase().trim())){
ServiceReference serviceReference =
this.pidsToServiceReferencesCopy.get(pid.toLowerCase().trim());
this.pidsToServiceReferences.remove(pid.toLowerCase().trim());
AlgorithmAction action =
new AlgorithmAction(serviceReference, this.bundleContext, this.ciShellContext);
String menuLabel = (String)serviceReference.getProperty(LABEL);
if ((menuName!= null) && (menuName.trim().length() > 0)) {
// Use the name specified in the XML to overwrite the label.
action.setText(menuName);
action.setId(getItemID(serviceReference));
parentMenuBar.add(action);
handleActionAccelerator(action, parentMenuBar, serviceReference);
} else {
if ((menuLabel != null) && (menuLabel.trim().length() > 0)) {
action.setText(menuLabel);
action.setId(getItemID(serviceReference));
parentMenuBar.add(action);
handleActionAccelerator(action, parentMenuBar, serviceReference);
} else {
/*
* TODO: This is a problem: No label is specified in the plug-in's
* properties file and no name is specified in the XML file.
*/
}
}
} else {
String algorithmNotFoundFormat =
"Oops! %s tried to place an algorithm with the id '%s' " +
"on the menu, but the algorithm could not be found.";
String algorithmNotFoundMessage =
String.format(algorithmNotFoundFormat, this.toolName, pid);
// String algorithmNotFoundMessage =
// "Oops! Network Workbench tried to place an algorithm with the id '" +
// pid +
// "' on the menu, but the algorithm could not be found.";
String contactInformationFormat =
"If you see this error, please contact %s, " +
"or post a ticket on our bug tracker at: %s .";
String contactInformationMessage = String.format(
contactInformationFormat, HELP_DESK_EMAIL_ADDRESS, this.toolTicketURL);
// String contactInformationMessage =
// "If you see this error, please contact nwb-helpdesk@googlegroups.com, or " +
// "post a ticket on our bug tracker at: " +
// "http://cns-trac.slis.indiana.edu/trac/nwb .";
this.logger.log(LogService.LOG_DEBUG, algorithmNotFoundMessage);
this.logger.log(LogService.LOG_DEBUG, contactInformationMessage);
}
}
}
private void parseXMLFile(String menuFilePath){
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setCoalescing(true);
try {
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
this.documentObjectModel = documentBuilder.parse(menuFilePath);
} catch(ParserConfigurationException parserConfigurationException) {
parserConfigurationException.printStackTrace();
} catch(SAXException saxException) {
saxException.printStackTrace();
} catch(IOException ioException) {
ioException.printStackTrace();
}
}
/*
* Handle some service bundles that have specified the menu_path and label
* but not specified in the DEFAULT_MENU_FILE_NAME
*/
private void processLeftServiceBundles() {
if (!this.pidsToServiceReferences.isEmpty()){
for (String key : this.pidsToServiceReferences.keySet()) {
ServiceReference serviceReference = this.pidsToServiceReferences.get(key);
makeMenuItem(serviceReference);
}
// Object[] keys = this.pidsToServiceReferences.keySet().toArray();
//
// for (int ii = 0; ii < keys.length; ii++) {
// ServiceReference serviceReference =
// (ServiceReference)this.pidsToServiceReferences.get((String)keys[ii]);
// makeMenuItem(serviceReference);
// }
}
}
private void initializeMenu() throws InvalidSyntaxException {
ServiceReference[] serviceReferences = this.bundleContext.getAllServiceReferences(
AlgorithmFactory.class.getName(), null);
if (serviceReferences != null) {
for (int ii = 0; ii < serviceReferences.length; ii++) {
makeMenuItem(serviceReferences[ii]);
}
}
}
private class ContextListener implements ServiceListener {
public void serviceChanged(ServiceEvent event) {
switch (event.getType()) {
case ServiceEvent.REGISTERED:
makeMenuItem(event.getServiceReference());
break;
case ServiceEvent.UNREGISTERING:
removeMenuItem(event.getServiceReference());
break;
case ServiceEvent.MODIFIED:
updateMenuItem(event.getServiceReference());
break;
}
}
}
private void makeMenuItem(ServiceReference serviceReference) {
String path = (String) serviceReference.getProperty(MENU_PATH);
String[] items = null;
if (path != null) {
items = path.split("/");
}
IMenuManager menu = null;
if ((items != null) && (items.length > 1)) {
AlgorithmAction action =
new AlgorithmAction(serviceReference, this.bundleContext, this.ciShellContext);
action.setId(getItemID(serviceReference));
IMenuManager targetMenu = this.menuManager;
String group = items[items.length - 1];
for (int ii = 0; ii < items.length - 1; ii++) {
menu = targetMenu.findMenuUsingPath(items[ii]);
if ((menu == null) && (items[ii] != null)) {
menu = targetMenu.findMenuUsingPath(items[ii].toLowerCase());
}
if (menu == null) {
menu = createMenu(items[ii], items[ii]);
targetMenu.appendToGroup(ADDITIONS_GROUP, menu);
}
targetMenu = menu;
}
group = items[items.length - 1];
IContributionItem groupItem = targetMenu.find(group);
if (groupItem == null) {
groupItem = new GroupMarker(group);
targetMenu.appendToGroup(ADDITIONS_GROUP, groupItem);
}
targetMenu.appendToGroup(group, action);
handleActionAccelerator(action, targetMenu, serviceReference);
targetMenu.appendToGroup(group, new Separator());
algorithmsToActions.put(getItemID(serviceReference), action);
actionsToMenuManagers.put(action, targetMenu);
Display.getDefault().asyncExec(this.updateAction);
} else {
this.logger.log(
LogService.LOG_DEBUG,
"Bad menu path for Algorithm: " + serviceReference.getProperty(LABEL));
}
}
private String getItemID(ServiceReference serviceReference) {
return
serviceReference.getProperty("PID:" + Constants.SERVICE_PID) +
"-SID:" +
serviceReference.getProperty(Constants.SERVICE_ID);
}
private MenuManager createMenu(String name, String id){
MenuManager menu = new MenuManager(name, id);
menu.add(new GroupMarker(START_GROUP));
menu.add(new GroupMarker(ADDITIONS_GROUP));
menu.add(new GroupMarker(END_GROUP));
return menu;
}
private void updateMenuItem(ServiceReference serviceReference) {
Action item = (Action)this.algorithmsToActions.get(getItemID(serviceReference));
if (item != null) {
this.logger.log(
LogService.LOG_DEBUG, "updateMenuItem for " + getItemID(serviceReference));
item.setText("" + serviceReference.getProperty(LABEL));
}
}
private void removeMenuItem(ServiceReference serviceReference) {
String path = (String)serviceReference.getProperty(MENU_PATH);
final Action item = this.algorithmsToActions.get(getItemID(serviceReference));
if ((path != null) && (item != null)) {
int index = path.lastIndexOf('/');
if (index != -1) {
path = path.substring(0, index);
final IMenuManager targetMenu = this.menuManager.findMenuUsingPath(path);
if (targetMenu != null) {
if (!this.shell.isDisposed()) {
this.shell.getDisplay().syncExec(new Runnable() {
public void run() {
targetMenu.remove(item.getId());
}
});
}
this.algorithmsToActions.remove(getItemID(serviceReference));
this.actionsToMenuManagers.remove(item);
}
}
}
}
public void stop() {
this.bundleContext.removeServiceListener(this.contextListener);
if (!this.shell.isDisposed()) {
this.shell.getDisplay().syncExec(this.stopAction);
}
}
//private void clearShortcuts() {
/*IWorkbench workbench = this.window.getWorkbench();
IBindingService bindingService =
(IBindingService)workbench.getService(IBindingService.class);
Binding[] bindings = bindingService.getBindings();
IHandlerService handlerService =
(IHandlerService)workbench.getService(IHandlerService.class);*/
//getLog().log(LogService.LOG_WARNING, "handlerService: " + handlerService);
//for (int ii = 0; ii < bindings.length; ii++) {
// getLog().log(LogService.LOG_INFO, "Binding[" + ii + "]: " + bindings[ii]);
/*String bindingInfo =
"\tcontextID: " + bindings[ii].getContextId() + "\n" +
"\tparameterized command: " + bindings[ii].getParameterizedCommand() + "\n" +
"\ttrigger sequence: " + bindings[ii].getTriggerSequence().format();
getLog().log(LogService.LOG_INFO, "Binding:\n" + bindingInfo);*/
/*KeyBinding(
bindings[ii].getTriggerSequence(),
null,
bindings[ii].getSchemeId(),
bindings[ii].getContextId(),
null,
null,
null,
Binding.SYSTEM);*/
//}
//}
private void handleActionAccelerator(
Action action, IMenuManager parentMenuBar, ServiceReference serviceReference) {
action.setAccelerator(determineActionAcceleratorKeyCode(serviceReference, action));
}
private static int determineActionAcceleratorKeyCode(
ServiceReference serviceReference, Action action) {
String shortcutString = (String)serviceReference.getProperty(SHORTCUT);
if (shortcutString != null) {
return Action.convertAccelerator(shortcutString);
} else {
return 0;
}
}
/*
* printElementAttributes takes in a xml document, gets the nodes, then prints the attributes.
* Copied from Java Tutorial on XML Parsing by Tim Kelley for debugging purposes.
*/
static void printElementAttributes(Document doc)
{
NodeList nl = doc.getElementsByTagName("*");
Element e;
Node n;
NamedNodeMap nnm;
String attrname;
String attrval;
int i, len;
len = nl.getLength();
for (int j=0; j < len; j++)
{
e = (Element)nl.item(j);
System.err.println(e.getTagName() + ":");
nnm = e.getAttributes();
if (nnm != null)
{
for (i=0; i<nnm.getLength(); i++)
{
n = nnm.item(i);
attrname = n.getNodeName();
attrval = n.getNodeValue();
System.err.print(" " + attrname + " = " + attrval);
}
}
System.err.println();
}
}
}