/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors/Changelog:
* IBM Corporation - initial API and implementation.
* This is a rewrite of XSWT as described at
* http://xswt.sourceforge.net/cgi-bin/wiki?ProjectDocumentation
* It borrows code and ideas from the original work done by:
* Chris McLaren and Bob Foster
*
* Bob Foster - The color manager idea; XSWT top-level node idea; some other
* important stuff
* David Orme - Rewrote: switched to a reflection-based implementation
* Jan Petersen - JFace handling; other important details
* Yu You - XML Pull parser port, based on kXML engine
* Dave Orme - Genericized JFace code to anything implementing getControl()
* Dave Orme - Made x:children optional; factored editor out of core library
*******************************************************************************
* TODO: Localize error messages
* TODO: Move to KXML2 since KXML1 is no longer maintained
******************************************************************************/
package com.swtworkbench.community.xswt;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Widget;
import org.xmlpull.v1.XmlPullParserException;
import com.swtworkbench.community.xswt.dataparser.DataParser;
import com.swtworkbench.community.xswt.dataparser.IDataParser;
import com.swtworkbench.community.xswt.dataparser.IDataParserContext;
import com.swtworkbench.community.xswt.dataparser.parsers.ClassDataParser;
import com.swtworkbench.community.xswt.dataparser.parsers.WidgetDataParser;
import com.swtworkbench.community.xswt.layoutbuilder.ILayoutBuilder;
import com.swtworkbench.community.xswt.layoutbuilder.ObjectStub;
import com.swtworkbench.community.xswt.layoutbuilder.ReflectionSupport;
import com.swtworkbench.community.xswt.layoutbuilder.SWTLayoutBuilder;
import com.swtworkbench.community.xswt.metalogger.Logger;
import com.swtworkbench.community.xswt.scripting.Bindings;
import com.swtworkbench.community.xswt.scripting.BindingsListener;
import com.swtworkbench.community.xswt.scripting.EvaluationContext;
import com.swtworkbench.community.xswt.scripting.IScriptable;
import com.swtworkbench.community.xswt.scripting.ScriptingEngine;
import com.swtworkbench.community.xswt.xmlhandler.IAttributeHandler;
import com.swtworkbench.community.xswt.xmlhandler.IElementHandler;
import com.swtworkbench.community.xswt.xmlhandler.IHandlerContext;
import com.swtworkbench.community.xswt.xmlparser.IMinimalOM;
import com.swtworkbench.community.xswt.xmlparser.IMinimalParser;
import com.swtworkbench.community.xswt.xmlparser.MinimalPullParser;
/**
* XSWT Parser
*
* @author Dave Orme <djo@coconut-palm-software.com>
*/
public class XSWT implements Bindings, IHandlerContext {
private static final String AMBIGUOUS_ERROR_MSG = "Ambiguous error. If node was parsed as a property node (1) is the correct error; else if node was parsed as an object constructor (2) is the correct error:";
public static final String XSWT_NS = "http://sweet_swt.sf.net/xswt";
private static Map customNSRegistry = new HashMap();
/**
* Register a custom namespace handler.
*
* @param namespace
* @param handler
*/
public static void registerNSHandler(String namespace,
ICustomNSHandler handler) {
XSWT.customNSRegistry.put(namespace, handler);
}
/**
* Method create. Create an SWT layout from a file located in a position in
* the file system (or Jar file) relative to some Java class.
*
* @param file
* The file name
* @param relativeTo
* The Class whose location should be used as the starting
* location for finding file
* @return The SWT control Map
* @throws XSWTException
* @throws XmlPullParserException
* @throws IOException
*/
public static XSWT create(String file, Class relativeTo)
throws XSWTException, IOException {
return create(relativeTo.getResource(file).openStream());
}
public static URIHandler defaultUriHandler = new DefaultURIHandler();
private URIHandler uriHandler = defaultUriHandler;
public URIHandler getUriHandler() {
return uriHandler;
}
public void setUriHandler(URIHandler uriHandler) {
this.uriHandler = uriHandler;
}
private String uri = null;
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
/**
* Method createl. Create an SWT layout from a file located in a position in
* the file system (or Jar file) relative to some Java class. Logs any
* exceptions and returns null on failure.
*
* @param file
* The file name
* @param relativeTo
* The Class whose location should be used as the starting
* location for finding file
* @return The SWT control Map
*/
public static XSWT createl(String file, Class relativeTo) {
try {
return create(relativeTo.getResource(file).openStream());
} catch (Exception e) {
Logger.log().error(e,
"Unable to create XSWT layout: " + e.getMessage());
}
return null;
}
/**
* Create a XML Pull parser and parse the XSWT
*
* @param inputStream
* the XSWT's input stream
* @return Map the map contains the controls. Null if error happends.
* @throws XSWTException
* @throws XmlPullParserException
*/
public static XSWT create(InputStream inputStream) throws XSWTException {
IMinimalParser parser = new MinimalPullParser();
return new XSWT(parser.build(inputStream), parser);
}
/**
* Create a XML Poll parser and parse the XSWT
*
* @param uri
* URI pointing to the XSWT file
* @return Map the map contains the controls. Null if error happends.
* @throws XSWTException
* @throws XmlPullParserException
*/
public static XSWT create(Reader reader) throws XSWTException {
IMinimalParser parser = new MinimalPullParser();
return new XSWT(parser.build(reader), parser);
}
/**
* Create a XML Poll parser and parse the XSWT
*
* @param uri
* URI pointing to the XSWT file
* @return Map the map contains the controls. Null if error happens.
* @throws XSWTException
*/
public static XSWT create(String uri) throws XSWTException {
try {
// return create(new FileReader(uri));
XSWT xswt = create(defaultUriHandler.getInputStream(uri));
xswt.setUri(uri);
return xswt;
} catch (IOException e) {
throw new XSWTException(e);
}
}
/**
* Parse XSWT XML file and return an interface implementing getters for each control
* in the XSWT Map. For example:<p>
*
* <p>
* <code>
* <text x:id="Name">
* </code>
* <p>
* could be retrieved using an interface defined as follows:
* <p>
* <code>
* interface DataEntry {
* Text getName();
* }
* </code>
* <p>
* This is the main loop where XSWT is parsed. This version logs any exceptions that
* occur rather than allowing them to propogate.<p>
*
* @param parent The parent control; for SWT, typically an org.eclipse.swt.widgets.Composite
* @param resultInterface The interface the returned object should implement
* @return An object implementing resultInterface or null on failure.
*/
public Object parsel(Object parent, Class resultInterface) {
try {
Map result = parse(parent);
return DuckMapper.implement(resultInterface, result);
} catch (XSWTException e) {
Logger.log().error(e, "Exception occured while parsing XSWT file");
return null;
}
}
/**
* Parse XSWT XML file and return an interface implementing getters for each control
* in the XSWT Map. For example:
*
* <p>
* <code>
* <text x:id="Name">
* </code>
* <p>
* could be retrieved using an interface defined as follows:
* <p>
* <code>
* interface DataEntry {
* Text getName();
* }
* </code>
* <p>
*
* This is the main loop where XSWT is parsed.
*
* @param parent The parent object; for SWT, typically an org.eclipse.swt.widgets.Composite
* @param resultInterface The interface the returned object should implement
* @return An object implementing resultInterface
* @throws XSWTException on error
*/
public Object parse(Object parent, Class resultInterface) throws XSWTException {
Map result = parse(parent);
return DuckMapper.implement(resultInterface, result);
}
/**
* Create SWT UI map from XSWT XML file.
*
* This is the main loop where XSWT is parsed. This version logs any exceptions that
* occur rather than allowing them to propogate.
*
* @param parent The parent object; for SWT, typically an org.eclipse.swt.widgets.Composite
* @return The Map containing the SWT controls that were created or null on failure
*/
public Map parsel(Object parent) {
try {
return parse(parent);
} catch (XSWTException e) {
Logger.log().error(e, "Exception occured while parsing XSWT file");
return null;
}
}
/**
* Create SWT UI map from XSWT XML file.
*
* This is the main loop where XSWT is parsed.
*
* @param parent
* The parent object; for SWT, typically an org.eclipse.swt.widgets.Composite
* @return The Map containing the SWT controls that were created
* @throws XSWTException if Something Bad Happens
*/
public synchronized Map parse(Object parent)
throws XSWTException {
if (parent != null) {
if (parent instanceof Composite) {
((Composite)parent).addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
dataParser.dispose();
}
});
}
}
String tagName = parser.getElementName(root);
if ("xswt".equals(tagName)) {
Logger.log().debug(
XSWT.class,
"Start XSWT:" + parser.getElementNamespace(root) + ":" + tagName);
fireProcessDocument(tagName, parent, false);
processTopLevelElement(root, parent);
if (fixupTable.size() != 0)
throw new XSWTException(missingIdRefError());
}
fireProcessDocument(tagName, getObjectMap(), true);
if (styleSheetParent == null) {
dispose();
}
return getObjectMap();
}
// private HashMap fixupTable = new HashMap();
private List fixupTable = new ArrayList();
private class FixupTableEntry {
public Object control;
public String attributeName;
public String attributeValue;
public int rowNumber;
public int colNumber;
public String toString() {
return "Row " + rowNumber + " Col " + colNumber;
}
}
private List listeners = null;
public void addBindingsListener(BindingsListener listener) {
if (listeners == null) {
listeners = new ArrayList();
}
listeners.add(listener);
}
public void removeBindingsListener(BindingsListener listener) {
if (listeners != null) {
listeners.remove(listener);
}
}
public void addXSWTListener(XSWTListener listener) {
addBindingsListener(listener);
}
public void removeXSWTListener(XSWTListener listener) {
removeBindingsListener(listener);
}
public void fireProcessElement(String name, Object o, boolean processed) {
for (int i = 0; listeners != null && i < listeners.size(); i++) {
if (! (listeners.get(i) instanceof XSWTListener))
continue;
XSWTListener listener = (XSWTListener)listeners.get(i);
if (processed) {
listener.elementProcessed(this, name, o);
} else {
listener.processElement(this, name, o);
}
}
}
public void fireProcessAttribute(String name, String value, Object o, boolean processed) {
for (int i = 0; listeners != null && i < listeners.size(); i++) {
if (! (listeners.get(i) instanceof XSWTListener))
continue;
XSWTListener listener = (XSWTListener)listeners.get(i);
if (processed) {
listener.attributeProcessed(this, name, value, o);
} else {
listener.processAttribute(this, name, value, o);
}
}
}
public static void fireProcessDocument(List listeners, XSWT xswt, String name, Object o, boolean processed) {
for (int i = 0; listeners != null && i < listeners.size(); i++) {
if (! (listeners.get(i) instanceof XSWTListener))
continue;
XSWTListener listener = (XSWTListener)listeners.get(i);
if (processed) {
listener.documentProcessed(xswt, o);
} else {
listener.processDocument(xswt, o);
}
}
}
public void fireProcessDocument(String name, Object o, boolean processed) {
fireProcessDocument(listeners, this, name, o, processed);
}
public void fireSetProperty(String name, Object o, Object value, boolean processed) {
for (int i = 0; listeners != null && i < listeners.size(); i++) {
if (! (listeners.get(i) instanceof XSWTListener))
continue;
XSWTListener listener = (XSWTListener)listeners.get(i);
if (processed) {
listener.setProperty(o, name, value);
} else {
listener.propertySet(o, name, value);
}
}
}
// handler management
private Map elementHandlers = new HashMap();
private Map attributeHandlers = new HashMap();
public void registerElementHandler(String uri, IElementHandler handler) {
elementHandlers.put(uri, handler);
}
public void registerAttributeHandler(String uri, IAttributeHandler handler) {
attributeHandlers.put(uri, handler);
}
public void registerElementHandler(String uri, String[] uris) {
elementHandlers.put(uri, uris);
}
public void registerAttributeHandler(String uri, String[] uris) {
attributeHandlers.put(uri, uris);
}
private IElementHandler[] getElementHandlers(String uri) {
Object o = elementHandlers.get(uri);
if (o instanceof IElementHandler[]) {
return (IElementHandler[])o;
}
if (o instanceof String) {
o = getElementHandlers((String)o);
}
if (o instanceof String[]) {
String[] uris = (String[])o;
IElementHandler[][] handlersArray = new IElementHandler[uris.length][];
int count = 0;
for (int i = 0; i < uris.length; i++) {
handlersArray[i] = getElementHandlers(uris[i]);
count += handlersArray[i].length;
}
IElementHandler[] handlers = new IElementHandler[count];
count = 0;
for (int i = 0; i < uris.length; i++) {
System.arraycopy(handlersArray[i], 0, handlers, count, handlersArray[i].length);
count += handlersArray[i].length;
}
o = handlers;
}
if (o instanceof IElementHandler[]) {
elementHandlers.put(uri, o);
return (IElementHandler[])o;
}
return null;
}
private IAttributeHandler[] getAttributeHandlers(String uri) {
Object o = elementHandlers.get(uri);
if (o instanceof IAttributeHandler[]) {
return (IAttributeHandler[])o;
}
if (o instanceof String) {
o = getElementHandlers((String)o);
}
if (o instanceof String[]) {
String[] uris = (String[])o;
IAttributeHandler[][] handlersArray = new IAttributeHandler[uris.length][];
int count = 0;
for (int i = 0; i < uris.length; i++) {
handlersArray[i] = getAttributeHandlers(uris[i]);
count += handlersArray[i].length;
}
IAttributeHandler[] handlers = new IAttributeHandler[count];
count = 0;
for (int i = 0; i < uris.length; i++) {
System.arraycopy(handlersArray[i], 0, handlers, count, handlersArray[i].length);
count += handlersArray[i].length;
}
o = handlers;
}
if (o instanceof IAttributeHandler[]) {
elementHandlers.put(uri, o);
return (IAttributeHandler[])o;
}
return null;
}
public IDataParserContext getDataParserContext() {
return dataParser;
}
public IMinimalOM getMinimalOM() {
return parser;
}
/**
*
* Add unsolved reference to Fixup Table
*
* @param attributeName
* @param obj
* @param attributeValue
*/
private void addUnresolvedIDRef(String attributeName, Object obj,
String attributeValue, int row, int col) {
Logger.log().debug(
XSWT.class,
"Found an unsolved refernce:" + attributeValue);
FixupTableEntry entry = new FixupTableEntry();
entry.control = obj;
entry.colNumber = col;
entry.rowNumber = row;
entry.attributeName = attributeName;
entry.attributeValue = attributeValue;
fixupTable.add(entry);
// LinkedList id = (LinkedList) fixupTable.get(attributeValue);
// if (id == null) {
// LinkedList newList = new LinkedList();
// newList.add(entry);
// fixupTable.put(attributeValue, newList);
// } else
// id.add(entry); // No need to put it back to the fixupMap
}
/**
* Try to resolve any unresolved reference objects from Fixup Table
*
* @param idRef
* @param object
*
* @throws XSWTException
*
*/
private void resolveIdRefs(String idRef, Object object)
throws XSWTException {
if (idRef == null || fixupTable.size() == 0)
return;
Iterator entries = fixupTable.iterator();
while (entries.hasNext()) {
FixupTableEntry entry = (FixupTableEntry)entries.next();
if (entry.attributeValue.startsWith(idRef)) {
if (layoutBuilder.setProperty(entry.attributeName, entry.control, entry.attributeValue, null)) {
entries.remove();
}
}
}
// Get the LinkedList
// LinkedList list = (LinkedList) fixupTable.get(idRef);
// if (list == null)
// return; // No unresolved reference
// for (int i = 0; i < list.size(); i++) {
// // We process every one alone the LinkedList
// FixupTableEntry entry = (FixupTableEntry) list.get(i);
// if (entry == null)
// continue; // almost impossible
// Logger.log().debug(
// XSWT.class,
// "Process unresolved reference (".concat(idRef).concat(
// ") at Row:")
// + entry.rowNumber + " Col:" + entry.colNumber);
// layoutBuilder
// .setProperty(entry.attributeName, entry.control, idRef, parser.getColumnNumber(), parser.getLineNumber());
//
// }
// // Clear the source
// fixupTable.remove(idRef);
}
/**
* Report all unsolved missing references
*
*/
private String missingIdRefError() {
// Iterator ids = fixupTable.keySet().iterator();
Iterator ids = fixupTable.iterator();
String msg = "";
while (ids.hasNext()) {
// String id = (String) ids.next();
// List list = (List) fixupTable.get(id);
//msg +="\n"; // for every reference ID we use a new line
// for (int i = 0; i < list.size(); i++) {
// FixupTableEntry entry = (FixupTableEntry) list.get(i);
FixupTableEntry entry = (FixupTableEntry)ids.next();
if (entry != null)
msg += "\nFound unresolved reference: "
+ entry.attributeName + "=\"" + entry.attributeValue + "\" at " + entry.toString();
// }
}
fixupTable.clear(); // Clean up the resources
return msg;
}
private IMinimalParser parser;
private Object root;
private DataParser dataParser;
/**
* Returns the underlying data parser;
*
* @return the DataParser
*/
public DataParser getDataParser() {
return dataParser;
}
public static class Configuration {
// ClassBuilder imports
private List packageImports;
private List classImports;
private List dataParsers;
private List scriptingEngines;
public Configuration(Configuration config) {
packageImports = new ArrayList(config != null ? config.packageImports : Collections.EMPTY_LIST);
classImports = new ArrayList(config != null ? config.classImports : Collections.EMPTY_LIST);
dataParsers = new ArrayList(config != null ? config.dataParsers : Collections.EMPTY_LIST);
scriptingEngines = new ArrayList(config != null ? config.scriptingEngines : Collections.EMPTY_LIST);
}
public Configuration() {
this(null);
}
public void addPackageImports(String pack) {
packageImports.add(pack);
}
public void addClassImports(String className) {
classImports.add(className);
}
// IDataParsers
public void addDataParser(String className, IDataParser parser) {
dataParsers.add(className);
dataParsers.add(parser);
}
// Scripting engines
public void addScriptingEngine(String name, ScriptingEngine engine, boolean isDefault) {
int pos = (isDefault ? 0 : scriptingEngines.size());
scriptingEngines.add(pos, engine);
scriptingEngines.add(pos, name);
}
}
/**
* XSWT Constructor
*
* @throws XSWTException
*/
private XSWT(Object root, IMinimalParser parser, Configuration config) {
// make sure processXswtPluginExtensions is called before supporting objects are created
if (! xswtPluginExtensionsProcessed) {
processXswtPluginExtensions();
}
if (config == null) {
config = defaultConfig;
}
this.root = root;
this.parser = parser;
// initialise dataParser first
dataParser = new DataParser();
Iterator dataParsers = config.dataParsers.iterator();
while (dataParsers.hasNext()) {
String className = (String)dataParsers.next();
IDataParser dataParser = (IDataParser)dataParsers.next();
try {
this.dataParser.addDataParser(Class.forName(className), dataParser);
} catch (ClassNotFoundException e) {
}
}
// must initialise layoutBuilder after dataParser
layoutBuilder = new SWTLayoutBuilder(this);
// must initialise widgetDataParser after dataParser
widgetDataParser = new WidgetDataParser();
dataParser.registerDataParser(Object.class, widgetDataParser);
classBuilder = ClassBuilder.getDefault();
Iterator imports = null;
imports = config.packageImports.iterator();
while (imports.hasNext()) {
String packageName = (String)imports.next();
classBuilder.importPackage(packageName);
}
imports = config.classImports.iterator();
while (imports.hasNext()) {
String className = (String)imports.next();
classBuilder.importClass(className);
}
dataParser.addDataParser(Class.class, new ClassDataParser(this));
this.scriptingEngines = new ArrayList(config.scriptingEngines);
}
private static boolean xswtPluginExtensionsProcessed = false;
private static void processXswtPluginExtensions() {
xswtPluginExtensionsProcessed = true;
try {
Class c = Class.forName("com.swtworkbench.community.xswt.XswtPlugin");
Object xswtPlugin = c.getMethod("getDefault", new Class[]{}).invoke(null, null);
defaultConfig = (Configuration)c.getMethod("getXSWTConfiguration", new Class[]{}).invoke(xswtPlugin, null);
} catch (Exception e) {
}
}
private static XSWT.Configuration defaultConfig = new Configuration();
private XSWT(Object root, IMinimalParser parser) {
this(root, parser, null);
}
/**
* Clean up all created resources
*
*/
private void dispose() {
classBuilder.dispose();
fixupTable.clear();
if (styleOwnerMap != null) {
List stylesheets = new ArrayList(styleOwnerMap.values());
for (int i = 0; i < stylesheets.size(); i++) {
XSWT xswt = (XSWT)stylesheets.get(i);
if (stylesheets.indexOf(xswt) == i) {
xswt.dispose();
}
}
}
}
// Helper object that caches Class objects and constructs SWT objects for us
// on the fly...
public ClassBuilder classBuilder;
private boolean parseAsStyleSheet = false;
private XSWT styleSheetParent = null;
/*
* Now that DataParser is no longer static (it can't be since it manages dispose()ing
* SWT controls), layoutBuilder can no longer be static either.
*/
//public static ILayoutBuilder layoutBuilder = new SWTLayoutBuilder(dataParser);
private ILayoutBuilder layoutBuilder;
/**
* Strategy Pattern: The ILayoutBuilder is responsible for "instantiating"
* objects and "setting property values". This is in quotes, because various
* other implementations may do things like generate Java code rather than
* generating a layout or other useful things.
*
* @param builder
*/
public void setLayoutBuilder(ILayoutBuilder builder) {
this.layoutBuilder = builder;
}
protected WidgetDataParser widgetDataParser;
/**
* Method getControlMap. Returns the map of ids to controls that was
* constructed as the layout was created.
*
* @return Map the id to control map
*/
public Map getObjectMap() {
return widgetDataParser.getWidgetMap();
}
/**
* Method processImport. Passes imports to the classBuilder object.
*
* @param Object
* The parent control
* @param parser
* XmlPullParser
* @throws XSWTException
* @throws XmlPullParserException
* @throws IOException
*/
private void processImports(Object element)
throws XSWTException {
int count = parser.getChildElementCount(element);
for (int i = 0; i < count; i++) {
Object child = parser.getChildElement(element, i);
if ("package".equals(parser.getElementName(child))) {
String thePackage = parser.getAttributeValue(child, 0); // get the 1st
// attr value
Logger.log().debug(XSWT.class, "Process package :" + thePackage);
if (thePackage != null) {
classBuilder.importPackage(thePackage);
} else {
throw new XSWTException("Unable to get import name", element);
}
} else if ("class".equals(parser.getElementName(child))) {
String theClass= parser.getAttributeValue(child, 0); // get the 1st
// attr value
Logger.log().debug(XSWT.class, "Process class :" + theClass);
if (theClass != null) {
classBuilder.importClass(theClass);
} else {
throw new XSWTException("Unable to get import name", element);
}
} else if ("stylesheet".equals(parser.getElementName(child))) {
processStylesheet(child);
}
}
}
/**
* Process the x:define keyword.
*
* @param composite the parent control
* @param parser the parser
*/
private void processDefine(Object element)
throws XSWTException
{
if (parser.getAttributeCount(element) != 0) {
throw new XSWTException("Usage: <x:define><prototypeObject x:id=\"idInMap\"/></x:extends>\n", element);
}
// Process the children.
int count = parser.getChildElementCount(element);
for (int i = 0; i < count; i++) {
processChild(parser.getChildElement(element, i), null);
}
}
/**
* Method processTopLevelElement. Looks for a <children>tag as a child node of the
* passed element and pass it to "processChildControls" for processing.
*
* @param composite
* The parent composite
* @param parser
* The element to search for the <children> tag
* @throws XSWTException
* @throws IOException
* @throws XmlPullParserException
*/
private void processTopLevelElement(Object root, Object composite) throws XSWTException {
int count = parser.getChildElementCount(root);
for (int i = 0; i < count; i++) {
Object child = parser.getChildElement(root, i);
String namespace = parser.getElementNamespace(child);
if (namespace != null && namespace.length() != 0 && !XSWT_NS.equals(namespace)) {
// Custom namespace handler
ICustomNSHandler handler = (ICustomNSHandler) customNSRegistry
.get(namespace);
if (handler != null) {
// handler.handleNamespace(parser, layoutBuilder);
} else
Logger.log().debug(XSWT.class,
"No custom namespace handler for " + namespace);
} else if (namespace != null && namespace.length() != 0
&& namespace.equals(XSWT.XSWT_NS)) {
processXSWTNode(child, composite);
} else {
// Process other nodes as child properties of parent composite
// or as child nodes of parent composite
Object parent = composite;
if (parent == null && styleSheetParent == null) {
parent = new ObjectStub("Composite");
}
XSWTException ex = null;
try {
if (parent != null) {
processNodeProperty(child, parent);
}
} catch (XSWTException e) {
ex = e;
}
if (ex != null || parent == null) {
// If it's not a property, see if it's a new (implicit) child node
try {
processChild(child, parent);
} catch (XSWTException e2) {
if (ex.isAmbiguous()) throw ex;
if (e2.isAmbiguous()) throw e2;
throw new XSWTException(AMBIGUOUS_ERROR_MSG, ex, e2);
}
}
}
}
}
/**
* @param composite
* @param parser
* @throws IOException
* @throws XmlPullParserException
* @throws XSWTException
*/
private void processXSWTNode(Object element, Object composite)
throws XSWTException {
String tagName = parser.getElementName(element);
if ("import".equals(tagName))
processImports(element);
else if ("define".equals(tagName))
processDefine(element);
else if ("children".equals(tagName))
processChildControls(element, composite);
else
throw new XSWTException("Unknown XSWT node", element);
}
private void processStylesheet(Object element) throws XSWTException {
String id = getWidgetName(element);
String path = getAttributeValue(element, null, "path"), absolutePath = null;
String baseUri = getAttributeValue(element, null, "relativeTo");
XSWT styleXswt = null;
if (baseUri != null) {
Class relativeTo = (Class)dataParser.parse(baseUri, Class.class);
if (relativeTo != null) {
try {
styleXswt = create(path, relativeTo);
} catch (IOException e) {
}
} else {
absolutePath = getUriHandler().resolve(path, baseUri);
}
}
if (styleXswt == null) {
if (absolutePath == null) {
absolutePath = getUriHandler().resolve(path, getUri());
}
styleXswt = create(absolutePath);
}
if (styleXswt == null) {
throw new XSWTException("No such file: " + absolutePath + " (" + path + " relative to " + baseUri +")");
}
styleXswt.styleSheetParent = this;
styleXswt.setUriHandler(getUriHandler());
try {
styleXswt.parseAsStyleSheet = true;
styleXswt.parse(null);
} finally {
styleXswt.parseAsStyleSheet = false;
}
addWidgetId(id, styleXswt);
}
/**
* Method processChildControls. We found a <children>tag and have to create
* all the controls represented by the tags inside it.
*
* @param parent
* The parent composite
* @param parser
* The <children>we have to process
* @throws IOException
* @throws XmlPullParserException
* @throws XSWTException
*/
private void processChildControls(Object element, Object parent)
throws XSWTException {
int count = parser.getChildElementCount(element);
for (int i = 0; i < count; i++) {
Object child = parser.getChildElement(element, i);
processChild(child, parent);
}
}
// Bindings methods
public boolean has(String name) {
return getObjectMap().containsKey(name);
}
public Object get(String name) {
return getObjectMap().get(name);
}
public void set(String name, Object value) {
getObjectMap().put(name, value);
fireBindingAdded(name, value);
}
public Iterator symbols() {
return getObjectMap().keySet().iterator();
}
private List evaluationContexts = new ArrayList();
private List scriptingEngines;
public void addScriptingEngine(String name, ScriptingEngine engine, boolean isDefault) {
int pos = (isDefault ? 0 : scriptingEngines.size());
scriptingEngines.add(pos, engine);
scriptingEngines.add(pos, name);
}
public ScriptingEngine getScriptingEngine(String name) throws XSWTException {
int pos = scriptingEngines.indexOf(name);
if (pos >= 0) {
return (ScriptingEngine)scriptingEngines.get(pos + 1);
}
String className = defaultScriptingEngineClassName(name);
ScriptingEngine engine = null;
try {
engine = (ScriptingEngine)Class.forName(className).newInstance();
} catch (Exception e) {
}
if (engine != null) {
addScriptingEngine(name, engine, false);
}
if (engine == null) {
throw new XSWTException(name + " scripting engine is not present");
}
return engine;
}
private String defaultScriptingEngineClassName(String name) {
return "com.swtworkbench.community.xswt." + name + "." + upperCaseFirstLetter(name + "ScriptingEngine");
}
public ScriptingEngine getDefaultScriptingEngine() throws XSWTException {
if (scriptingEngines == null) {
throw new XSWTException("No scripting engine is present");
}
return (scriptingEngines != null && scriptingEngines.size() > 1 ? (ScriptingEngine)scriptingEngines.get(1) : null);
}
private EvaluationContext getEvaluationContext(String lang, String name) throws XSWTException {
String key = lang + ":" + name;
int pos = evaluationContexts.indexOf(key);
if (pos >= 0) {
return (EvaluationContext)evaluationContexts.get(pos + 1);
}
ScriptingEngine engine = (lang != null ? getScriptingEngine(lang) : getDefaultScriptingEngine());
evaluationContexts.add(key);
EvaluationContext evaluationContext = engine.getEvaluationContext(name, this);
evaluationContexts.add(evaluationContext);
return evaluationContext;
}
private void addWidgetId(String id, Object widget) throws XSWTException {
if (id != null) {
if (widgetDataParser.get(id) != null) {
String message = "Duplicated widget ID found";
throw new XSWTException(message);
}
Object o = layoutBuilder.namedObject(widget);
widgetDataParser.put(id, o);
// process the Fixup Table to see if there're any unsolved references
resolveIdRefs(id, widget);
fireBindingAdded(id, o);
}
}
private void fireBindingAdded(String id, Object value) {
for (int i = 0; listeners != null && i < listeners.size(); i++) {
((BindingsListener)listeners.get(i)).bindingAdded(this, id, value);
}
}
/**
* Method processChild. We've found a tag inside a <children>tag. We now
* have to construct it and set its properties.
*
* @param parent
* The parent that will own the new child
* @param parser
* The parser representing the new child
* @return The new child
* @throws XmlPullParserException
* @throws XSWTException
* @throws IOException
*/
private Object processChild(Object element, Object parent) throws XSWTException {
String widgetName = getWidgetName(element);
if (widgetName != null) {
Object previousValue = widgetDataParser.get(widgetName);
if (previousValue != null) {
if (parent != null) // If it's an x:define, we'll just keep the existing one else throw an exception
throw new XSWTException("Duplicated widget ID found", element);
else
return previousValue;
}
}
if (parseAsStyleSheet) {
if (styleSheetParent != null && widgetName == null) {
throw new XSWTException("A style without widget ID found", element);
}
addWidgetId(widgetName, element);
styleSheetParent.registerStyle(this, element);
return element;
}
Object result = null;
String tagName = parser.getElementName(element); // Control's class name
fireProcessElement(tagName, parent, false);
XSWTException xe = null;
try {
Logger.log().debug(XSWT.class, "Constructing (" + parser.getElementNamespace(element) + ") " + tagName);
Class klass = (Class)dataParser.parse(tagName, Class.class);
if (klass == null) {
throw new XSWTException("Could not resolve the '" + tagName + "' class.");
}
int style = resolveStyles(element, new ArrayList(0)); // styles if available
result = layoutBuilder.construct(klass, parent, style, widgetName,element);
} catch (XSWTException e1) {
xe = e1;
}
if (result == null) {
try {
result = dataParser.parse(tagName, Widget.class);
} catch (XSWTException e) {
}
}
if (result == null) {
throw (xe != null ? xe : new XSWTException("No ID named " + tagName, element));
} else if (parser.isElement(result)) {
return getStyleOwner(result).processChild(result, parent);
}
processChildContent(element, result);
if (widgetName != null) {
addWidgetId(widgetName, result);
}
fireProcessElement(tagName, result, true);
return result;
}
private int resolveStyles(Object element, List idStyles) throws XSWTException {
String style = getAttributeValue(element, XSWT_NS, "style"); // styles if available
if (style == null) {
return SWT.NONE;
}
int styleValue = StyleParser.parse(style, getDataParser(), idStyles);
// collect styles from idStyles
List subStyles = null;
for (int i = 0; i < idStyles.size(); i++) {
Object styleElement = idStyles.get(i);
if (! parser.isElement(styleElement)) {
throw new XSWTException(styleElement + " is not a valid style element", element);
}
/*
if (! parser.getElementName(element).equals(parser.getElementName(styleElement))) {
throw new XSWTException(element + " and " + styleElement + " does not have the same name", element);
}
*/
XSWT styleXswt = (styleOwnerMap != null ? (XSWT)styleOwnerMap.get(styleElement) : this);
if (subStyles == null) {
subStyles = new ArrayList(0);
} else {
subStyles.clear();
}
styleValue |= (styleXswt != null ? styleXswt : this).resolveStyles(styleElement, subStyles);
}
return styleValue;
}
public void processChildContent(Object element, Object result) throws XSWTException {
List idStyles = new ArrayList(0);
resolveStyles(element, idStyles); // styles if available
for (int i = 0; i < idStyles.size(); i++) {
Object idStyle = idStyles.get(i);
getStyleOwner(idStyle).processChildContent(idStyle, result);
}
processChildAttributes(element, result);
if (result instanceof IScriptable) {
IScriptable script = (IScriptable)result;
if (script.getSource() == null) {
String text = parser.getElementText(element);
script.setSource(text.trim());
}
String lang = script.getLang();
try {
EvaluationContext evaluationContext = getEvaluationContext(lang, getWidgetName(element));
script.evaluateScript(evaluationContext);
} catch (Exception e) {
throw new XSWTException("Error when evaluating " + script.getSource(), e, element);
}
} else {
processSubNodes(element, result);
}
}
private Map styleOwnerMap;
private void registerStyle(XSWT xswt, Object element) {
if (styleOwnerMap == null) {
styleOwnerMap = new HashMap();
}
styleOwnerMap.put(element, xswt);
}
private XSWT getStyleOwner(Object element) {
XSWT styleOwner = null;
if (styleOwnerMap != null) {
styleOwner = (XSWT)styleOwnerMap.get(element);
}
return (styleOwner != null ? styleOwner : this);
}
public String getAttributeValue(Object element, String namespace, String name) {
return getAttributeValue(element, namespace, name, false);
}
public String getAttributeValue(Object element, String namespace, String name, boolean useStyle) {
int count = parser.getAttributeCount(element);
for (int i = 0; i < count; i++) {
String attrNamespace = parser.getAttributeNamespace(element, i);
if (name.equals(parser.getAttributeName(element, i)) &&
(namespace == attrNamespace || (namespace != null && namespace.equals(attrNamespace)))) {
return parser.getAttributeValue(element, i);
}
}
// if not such attr exists, try to find it in the style(s)
if (useStyle) {
List idStyles = new ArrayList();
try {
resolveStyles(element, idStyles);
} catch (XSWTException e) {
return null;
}
for (int i = 0; i < idStyles.size(); i++) {
String attrValue = getAttributeValue(idStyles.get(i), namespace, name);
if (attrValue != null) {
return attrValue;
}
}
}
return null;
}
/**
* Method getWidgetName. Returns the name of the current widget as specified
* by an ID attribute. If an ID attribute is not present, returns null.
*
* @param parser
* The element to search
* @return The name string or null if not found
*/
private String getWidgetName(Object element) {
return getAttributeValue(element, XSWT_NS, "id");
}
/**
* Method processChildAttributes. We've seen a tag and constructed its
* object. Now we have to process any other attributes on it. Attributes are
* considered to function in any of the following roles and are considered
* for each role in the following order of precedence:
* <p>
*
* <ol>
* <li>Built-in attribute with special meaning to XSWT (reserved word)
* <li>Attribute represents a property on the object
* <li>Attribute represents a field on the object
* </ol>
*
* @param obj
* The object on which the attribute should be applied
* @param parent
* The XML element representing the attribute
* @throws XSWTException
* If something really bad happened
*/
private void processChildAttributes(Object element, Object obj)
throws XSWTException {
// Generically handle JFace, Essential Data, etc., viewer-like things...
Object viewer = obj; // Assume we have a JFace Viewer-like thing
Object viewedObject = ReflectionSupport.invokei(obj, "getControl", new Object[] {});
if (viewedObject != null) {
obj = viewedObject;
((Widget) obj).setData("viewer", viewer);
}
int count = parser.getAttributeCount(element);
for (int i = 0; i < count; ++i) {
//Node attribute = attributes.item(i);
boolean recognized = false;
String attrName = parser.getAttributeName(element, i);
String attrValue = parser.getAttributeValue(element, i);
int colNum = -1; // parser.getColumnNumber();
int lineNum = -1; // parser.getLineNumber();
recognized = processBuiltInAttr(obj, attrName, parser.getAttributeNamespace(element, i), attrValue);
if (recognized)
continue;
fireProcessAttribute(attrName, attrValue, obj, false);
// Process Property
try {
recognized = layoutBuilder.setProperty(attrName, obj, attrValue, element);
} catch (XSWTException e) {
// We found an unsolved reference
addUnresolvedIDRef(attrName, obj, attrValue, lineNum, colNum);
recognized = true; // We treat as recognized at this moment
}
if (recognized) {
fireProcessAttribute(attrName, attrValue, obj, true);
continue;
}
// Process Field
try {
recognized = layoutBuilder.setField(attrName, obj, attrValue, element);
} catch (XSWTException e) {
// We found an unsolved reference
addUnresolvedIDRef(attrName, obj, attrValue, lineNum, colNum);
recognized = true; // We treat as recognized at this moment
}
if (recognized) {
fireProcessAttribute(attrName, attrValue, obj, true);
continue;
}
if (!recognized)
throw new XSWTException(attrName + " attribute not found", element);
}
}
/**
* Method processBuiltInAttr. Process any attributes that are reserved
* words. These currently are "style", "class", and "id".
*
* @param obj
* The object to operate on
* @param nodeName
* The attribute name to process
* @param namespace
* The attribute namespace to process
* @param value
* The attribute value to process
* @return true if we processed this attribute or there is no need to do so
* @throws XSWTException
* If something went horribly wrong
*/
private boolean processBuiltInAttr(Object obj, String nodeName,
String namespace, String value) throws XSWTException {
// TODO: an ugly hack to get rid of namespace prefix
int index = nodeName.indexOf(':');
nodeName = (index == -1) ? nodeName : nodeName.substring(index + 1);
if (XSWT_NS.equals(namespace)) {
// The object's "ID" (similar to its "name" property in the builder
if (nodeName.endsWith("id"))
return true;
// x:id.<key> - setData on Widget objects
if (nodeName.startsWith("id.")) {
String key = nodeName.substring("id.".length());
//String value = attribute.getNodeValue();
try {
obj.getClass().getMethod("setData", new Class[]{String.class, Object.class}).invoke(obj, new Object[] {key, value});
} catch (Exception e) {
}
// ReflectionSupport.invokei(obj, "setData", new Object[] {key, value});
return true;
}
// Constructor parameters have already been processed
if ("p".equals(nodeName.substring(0, 1)))
return true;
// "style" was already processed at construction time
if (nodeName.endsWith("style"))
return true;
// "class" was already processed (or is a reserved word in XSWT in
// any case)
if (nodeName.endsWith("class"))
return true;
}
// Didn't recognize anything here...
return false;
}
/**
* Method processSubNodes. Sub-elements of an XML tag denote one of three
* things:
* <p>
*
* <ul>
* <li>If the element is a <children>node, the node denotes SWT child
* controls in the SWT containership hierarchy.
* <li>Otherwise, the element is assumed to be a property set operation or
* a method call, if a corresponding property/method exists on parent
* <li>Otherwise, the element is assumed to be a new child node
* </ul>
*
* @param obj
* The current parent object
* @param parser
* The element we're processing
* @return true if the element was recognized.
* @throws XmlPullParserException
* @throws XSWTException
* @throws IOException
*/
private void processSubNodes(Object element, Object obj)
throws XSWTException {
Object viewedObject = ReflectionSupport.invokei(obj, "getControl", new Object[] {});
if (viewedObject != null) {
obj = viewedObject;
}
int count = parser.getChildElementCount(element);
for (int i = 0; i < count; i++) {
Object child = parser.getChildElement(element, i);
// IElementHandler[] handlers = getElementHandlers(parser.getElementNamespace(element));
// for (int j = 0; j < handlers.length; j++) {
// IElementHandler elementHandler = handlers[j];
// Object result = null;
// try {
// result = elementHandler.handleElement(element, obj, this);
// } catch (XSWTException xe) {
// }
// if (result != null) {
// }
// }
if ("children".equals(parser.getElementName(child))) {
processChildControls(child, obj);
} else {
// Otherwise, first try to treat it as a property setter.
// Failing that, treat it as a new object
try {
processNodeProperty(child, obj);
} catch (XSWTException e) {
try {
processChild(child, obj);
} catch (XSWTException e2) {
if (e.isAmbiguous()) throw e;
if (e2.isAmbiguous()) throw e2;
throw new XSWTException(AMBIGUOUS_ERROR_MSG, e, e2);
}
}
}
}
}
public void processAttributes(Object element, Object context) throws XSWTException {
Object viewer = context; // Assume we have a JFace Viewer-like thing
Object viewedObject = ReflectionSupport.invokei(context, "getControl", new Object[] {});
if (viewedObject != null) {
context = viewedObject;
((Widget)context).setData("viewer", viewer);
}
int count = parser.getAttributeCount(element);
for (int i = 0; i < count; ++i) {
//Node attribute = attributes.item(i);
boolean recognized = false;
String attrName = parser.getAttributeName(element, i);
String attrValue = parser.getAttributeValue(element, i);
String attributeUri = parser.getAttributeNamespace(element, i);
int colNum = -1; // parser.getColumnNumber();
int lineNum = -1; // parser.getLineNumber();
IAttributeHandler[] handlers = getAttributeHandlers(attributeUri);
for (int j = 0; j < handlers.length; j++) {
IAttributeHandler attributeHandler = handlers[j];
Object result = null;
try {
fireProcessAttribute(attrName, attrValue, context, false);
recognized = attributeHandler.handleAttribute(attrName, attrValue, attributeUri, context, this);
} catch (XSWTException xe) {
}
fireProcessAttribute(attrName, attrValue, context, true);
if (recognized) {
break;
}
}
if (! recognized) {
throw new XSWTException(attrName + " attribute not found", element);
}
}
}
public void processChildren(Object element, Object context, String uri) throws XSWTException {
Object viewedObject = ReflectionSupport.invokei(context, "getControl", new Object[] {});
if (viewedObject != null) {
context = viewedObject;
}
int count = parser.getChildElementCount(element);
for (int i = 0; i < count; i++) {
Object child = parser.getChildElement(element, i);
if (uri == null) {
uri = parser.getElementNamespace(child);
}
IElementHandler[] handlers = getElementHandlers(uri);
for (int j = 0; j < handlers.length; j++) {
IElementHandler elementHandler = handlers[j];
Object result = null;
try {
result = elementHandler.handleElement(child, context, this);
} catch (XSWTException xe) {
}
if (result != null) {
processAttributes(child, result);
if (! elementHandler.handlesChildElements(child, this)) {
processChildren(child, result, null);
}
return;
}
}
}
}
/**
* Method processNodeProperty. Deal with the case where a node represents a
* property/field set operation or a method call.
*
* @param obj
* The object that is the subject of the verb
* @param nodeChild
* The child node representing the value to be set
* @throws XmlPullParserException
* @throws XSWTException
* @throws IOException
*/
private void processNodeProperty(Object element, Object obj) throws XSWTException {
// The type of the value to construct
Class valueType = null;
// Either "setter" or "field" will be set but not both
Method[] setters = null;
Method setter = null;
Field field = null;
// The value to pass to the method or field
Object value = null;
// Check for a "class=" attribute
String classAttrib = getAttributeValue(element, XSWT_NS, "class", true);
if (classAttrib != null) {
// String className = upperCaseFirstLetter(classAttrib);
// valueType = classBuilder.getClass(className);
valueType = (Class)dataParser.parse(classAttrib, Class.class);
}
// Find the method or field
String nodeName = parser.getElementName(element);
setters = layoutBuilder.resolveAttributeSetMethod(obj, nodeName,
valueType);
// If we didn't find a setter,
// New: look for a zero-argument full qualified method name with a
// return class
// and then processSubNodes with the new parent object
// Get the return class name
if (setters == null) {
String parentReferenceId = getAttributeValue(element, XSWT_NS, "id");
Method getParentReferenceMethod = layoutBuilder.resolveAttributeGetMethod(obj, nodeName);
if (getParentReferenceMethod != null) {
Object temp_parent = layoutBuilder.getProperty(getParentReferenceMethod, obj, null, element);
if (temp_parent != null) {
if (parentReferenceId != null) {
addWidgetId(parentReferenceId, temp_parent);
}
processChildContent(element, temp_parent);
}
return;
}
}
if (setters == null) {
try {
field = layoutBuilder.getClass(obj).getField(nodeName);
} catch (Throwable t) {
// t.printStackTrace();
}
// If we didn't find a field either, give up
if (obj!= null && field == null)
throw new XSWTException("Property/method/field not found on " + obj.getClass().getName(), element);
// Make sure we have a ValueType
if (valueType == null) {
valueType = field.getType();
}
}
// if the valueType isn't set and we have a setter, set valueType
// from the method's type.
if (valueType == null && setters != null) {
XSWTException lastException = new XSWTException("Assert failure");
for (int i = 0; i < setters.length; i++) {
setter = setters[i];
Class[] paramTypes = setter.getParameterTypes();
valueType = paramTypes[0];
try {
value = constructValueObject(valueType, element);
} catch (XSWTException e) {
lastException = e;
}
break;
}
if (value == null) {
throw lastException;
}
} else if (valueType != null) {
value = constructValueObject(valueType, element);
} else {
throw new XSWTException("Cannot determine the valueType for " + nodeName);
}
Logger.log().debug(XSWT.class, "Field type: " + valueType.getName());
// Set its properties
processChildContent(element, value);
// Call the setter or assign the field...
if (setters != null) {
if (setter != null) {
layoutBuilder.setProperty(setter, obj, value, element);
} else {
for (int i = 0; i < setters.length; i++) {
layoutBuilder.setProperty(setters[i], obj, value, element);
}
}
} else if (obj != null) {
layoutBuilder.setField(field, obj, value, element);
}
}
/**
* Method constructValueObject. When processing a node property, it is
* possible to pass arguments to the object's constructor. This method
* resolves the correct constructor and constructs the value that will be
* set into the node property.
*
* @param valueType
* The class of the object to construct
* @param node
* The node representing the class
* @return The constructed object
* @throws XSWTException
* If a constructor could not be found or if something bad
* happened
*/
private Object constructValueObject(Class valueType, Object element)
throws XSWTException {
// Get the constructor argument strings (if any) out of the node
LinkedList argList = new LinkedList();
//NamedNodeMap nodeMap = node.getAttributes();
int count = parser.getAttributeCount(element);
StringBuffer arg = new StringBuffer("p");
for (int i = 0; i < count; ++i) {
arg.setLength(1);
arg.append(i);
String argName = arg.toString();
String value = getAttributeValue(element, XSWT_NS, argName, true);
// TODO: "break" may not be good
// TODO: how to process typo like "x:p0="2" x:p0="3"?
// TODO: how to process missing argument? like x:p0="1" x:p2="2"?
if (value == null)
break;
argList.addLast(value);
}
return layoutBuilder.construct(valueType, argList, element);
}
/**
* Method upperCaseFirstLetter. Returns source with the first letter
* guaranteed to be upper-case.
*
* @param source
* @return
*/
public static String upperCaseFirstLetter(String source) {
StringBuffer buf = new StringBuffer(source.substring(0, 1).toUpperCase());
buf.append(source.substring(1, source.length()));
return buf.toString();
}
/*
* Only needed if I'm going to support setData(key, value) private void
* processData(Widget widget, Element element) { NodeList nodeListData =
* element.getElementsByTagName("data");
*
* for (int i = 0; i < nodeListData.getLength(); i++) { Element elementData =
* (Element) nodeListData.item(i);
*
* if (elementData.getParentNode().equals(element)) { String attributeKey =
* elementData.getAttribute("key"); String attributeValue =
* elementData.getAttribute("value");
*
* if (attributeKey == null) widget.setData(attributeValue); else
* widget.setData(attributeKey, attributeValue); } } }
*/
}