facebook
Dimitry Karpenko
Java/Eclipse developer in MyEclipse and Webclipse teams.
Posted on Sep 14th 2015

The Challenge

Hopefully you have already discovered what a great tool Sapphire is for effectively creating GUI editors for xml and xml-based formats in Eclipse. When talking about standard xml editing, Sapphire is very powerful. We can get a fully-functional GUI editor for xml with little effort, in which we can edit properties and add or remove pieces of an xml tree.  And, with just a bit of work, we can even select files or show custom dialogs for obtaining values for our xml.

Sapphire easily deals with properties bound to a value either inside tag text (e.g., `<tag>value</tag>`) or with the attribute value inside a tag (e.g., `<tag name=”value“>`). However, in some cases we need to do more than simply set an attribute or element value. For example, we may want to edit the value for a preference that is placed inside a <preference> tag with a specified preference name such as `<preference name=”Fullscreen” value=”true” />`. This syntax is used in Phonegap config.xml files, as well as many other xml-based formats. Sapphire uses custom bindings to handle these situations.

An Example with Custom Bindings

Let’s take a look at a sample project in which we have edited a few aspects of Phonegap’s config.xml to show you how it works. To proceed with this example, you will need a basic understanding of Eclipse plug-in development and Sapphire. For questions on basic Sapphire principles, please see https://eclipse.org/sapphire/releases/9/documentation/index.html.

If you haven’t installed Sapphire, go to https://eclipse.org/sapphire/releases/9/ and install the latest release (currently ver 9) into Eclipse, or add it to your target platform. Please note that this example requires Java 8 and Eclipse 4 to work. If you are on an earlier version you can follow along but you may encounter some differences from what is described.

After downloading and installing Sapphire, you can download the sample project to follow along with the example. Or, you can create an Eclipse UI plug-in project, add Sapphire dependencies, and use the example to create your own project.

Create a Model

Let’s take a look at a simple model for our future editor. We have created an interface `com.genuitec.sapphire.tutorials.phonegap.model.IWidget` extending `org.eclipse.sapphire.Element`. This interface contains descriptions for three basic properties and two properties reflecting preferences; let’s see how these preferences are described in the interface. For additional details, please refer to the code in the sample project.

// *** Fullscreen *** - preference
@Type(base = Boolean.class) //Preference type
@XmlBinding(path = "@fullscreen")
@Label(standard = "Fullscreen") //Readable label for property
@DefaultValue(text = "false") //Default value for property
@CustomXmlValueBinding(impl = PreferenceBinding.class) //Custom binding class
@PreferenceName("fullscreen") //Additional parameters for custom binding
ValueProperty PROP_FULLSCREEN = new ValueProperty(TYPE, "Fullscreen"); //$NON-NLS-1$

Value<Boolean> getFullscreen();
void setFullscreen(Boolean value);
void setFullscreen(String value);

// *** Orientation *** - preference
@Type(base = Orientation.class) //Preference type
@XmlBinding(path = "@orientation")
@Label(standard = "Orientation")
@CustomXmlValueBinding(impl = PreferenceBinding.class)
@PreferenceName("orientation")
ValueProperty PROP_ORIENTATION = new ValueProperty(TYPE, "Orientation"); //$NON-NLS-1$

Value<Orientation> getOrientation();
void setOrientation(Orientation value);
void setOrientation(String value);

You can see that the `@PreferenceName` annotation isn’t present yet, so let’s see how to create it. This simple annotation is used to set the actual preference name for the property representing preference in the model.

@Retention(RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE,
 java.lang.annotation.ElementType.FIELD })
/**
 * Helper annotation for specifying preference name
 * Used by PreferenceBinding
 */
public @interface PreferenceName {
 public abstract String value(); 
}

 

Add XML Binding

That was easy enough! Class `PreferenceBinding` will be a bit more interesting—it’s doing most of the “magic” for preference-targeting properties:

public class PreferenceBinding extends StandardXmlValueBindingImpl {

	private static final String VALUE_ATTRIBUTE_NAME = "value";
	private static final String PREFERENCE_ELEMENT_NAME = "preference";
	private Node lastDomNode;
	private String preferenceName;

	@Override
	protected void initBindingMetadata() { // Here we initialize, which
											// preference this Binding will
											// handle
		super.initBindingMetadata();
		ValueProperty pdef = (ValueProperty) property().definition();
		PreferenceName nameAnnotation = pdef
				.getAnnotation(PreferenceName.class);
		preferenceName = nameAnnotation.value();
	}

	/**
	 * Searches an element for current preference by matching element's and it's
	 * attribute names with preset ones
	 * 
	 * @return XML element for current preference
	 */
	private XmlElement getElement() {
		XmlElement xml = xml(false);
		if (xml == null) {
			return null;
		}
		List<XmlElement> childElements = xml.getChildElements();
		for (XmlElement xmlElement : childElements) {
			if (xmlElement.getLocalName().equals(PREFERENCE_ELEMENT_NAME)
					&& attibutesMatches(xmlElement)) {
				return xmlElement;
			}
		}
		return null;
	}

	/**
	 * Checks whether given element's attributes matches preset ones
	 * 
	 * @param xmlElement
	 *            element to check
	 * @return matches or not
	 */
	private boolean attibutesMatches(XmlElement xmlElement) {
		return preferenceName.equalsIgnoreCase(obtainAttribute(xmlElement,
				"name"));
	}

	/**
	 * Helper method to get attribute value
	 * 
	 * @param xmlElement
	 *            XML Element
	 * @param key
	 *            Attr name
	 * @return Attr value
	 */
	private String obtainAttribute(XmlElement xmlElement, String key) {
		return xmlElement.getDomNode().getAttribute(key);
	}

	/**
	 * Stores value for preference
	 * 
	 * @param value
	 *            Value to store
	 */
	@Override
	public void write(String value) {
		if (xml(false) == null) {
			return;
		}
		XmlElement element = getElement();
		if (value == null || value.isEmpty()) { // Remove whole preference
												// element, if value became
												// empty
			if (element != null) {
				element.remove();
			}
			return;
		}
		if (element == null) { // Add new element, if no element was present,
								// and add name attribute with preset preference
								// name to it
			element = xml().addChildElement(PREFERENCE_ELEMENT_NAME);
			setAttribute(element, "name", preferenceName);
		}
		element.setAttributeText(VALUE_ATTRIBUTE_NAME, value, true);
	}

	/**
	 * Helper method for setting Attribute value
	 * 
	 * @param element
	 *            XML Element
	 * @param key
	 *            Attr key
	 * @param value
	 *            Attr value
	 */
	private void setAttribute(XmlElement element, String key, String value) {
		element.getDomNode().setAttribute(key, value);
	}

	/**
     * Obtains XML model element for property
     * @return Node for value attribute in our case
     */
	@Override
	public XmlNode getXmlNode() {
		XmlElement element = getElement();
		if (element != null) {
			return element.getAttribute(VALUE_ATTRIBUTE_NAME, false);
		}
		return null;
	}

	/**
	 * Reads preference value and returns it as a string
	 * @return preference value as a string
	 */
	@Override
	public String read() {
		checkElementRegistered();
		XmlElement element = getElement();
		if (element != null) {
			String attributeText = element
					.getAttributeText(VALUE_ATTRIBUTE_NAME);
			return attributeText;
		}
		return null;
	}

	private void checkElementRegistered() {
		XmlNode xmlNode = getXmlNode();
		if (xmlNode == null) {
			return;
		}
		Node domNode = xmlNode.getParent().getDomNode();
		if (lastDomNode != domNode) {
			XmlResourceStore store = ((RootXmlResource) resource().adapt(
					RootXmlResource.class)).store();
			IWidget root = property().element().adapt(IWidget.class);
			if (lastDomNode != null) {
				store.unregisterModelElement(lastDomNode, root);
			}
			store.registerModelElement(domNode, root);
			lastDomNode = domNode;
		}
	}

}

The responsibilities of the `PreferenceBinding` object are quite simple. On initialization, it gets the preference name to work with from the annotation of the property passed to its initializer. After that, the binding object is responsible for finding the xml element presenting a given preference, reading its value and storing the changed value into it (or creating a new element with necessary attributes if it wasn’t present). More details can be seen in the comments.

Create the Editor

Now that the model and I/O are complete, we are ready to create the editor. We’ll need to create the declarative editor ui description, PhonegapConfigEditor.sdef, and add an extension description for our editor into plugin.xml. See the attached project for more details.

Try It Out

Now we are ready to give it a try! Launch Eclipse, add the config.xml file to a new or existing project, and then double-click the file. You will see something like the following:

sapphire eclipse framework
Sapphire user interface

Try changing Fullscreen or Orientation values. You will see changes are correctly reflected. If you edit the corresponding values in the source, changes are reflected in the UI. The binding we created works well in both directions.

Note: If you want to use real config, many Phonegap config.xml files can be found by searching the internet for “phonegap config.xml sample”.

Attachment

sapphire_binding_tutorial.zip—Sample project showing code in action