Wednesday, November 20, 2013

Customizing item provider adapter in EMF

By default all item provider adapters in EMF extend ItemProviderAdapter, however sometimes it's necessary to change base class for the adapters to something else. For example, today I was reading this post about suppressing DocumentRoot during model load and Ed Merks suggested specializing ResourceItemProvider to skip root. Great, but do I really need to update all my adapters to extend new class, manually? Nope, GenModel to the rescue once again this time with its attribute 'Provider Root Extends Class'. I just added 'org.eclipse.emf.edit.provider.resource.ResourceItemProvider' to the attribute, regenerated edit code and voilĂ , all my adapters extend ResourceItemProvider.


Tuesday, November 12, 2013

Making Annotation part of the model when generating EMF model

When generating EMF model from XSD or otherwise it is beneficial to make annotations such as documentation part of the model. This information can be used to communicate to the user what particular model object does. Unfortunately while this seems like a easy task it did not work for me right away.
 There is a nifty class EcoreUtil that has vast amount of information that you can get from your model. One of the methods that was useful to me was EcoreUtil.getDocumentation(EModelElement element) and to use it all I need to do is:
EObject myObj = ...
String documentation = EcoreUtil.getDocumentation(myObj.eClass());
Simple enough, but it did not work. I was always getting null for documentation. Looking further and doing some debugging in getDocumentation() method I saw that first line was:
protected static final String GEN_MODEL_PACKAGE_NS_URI = "http://www.eclipse.org/emf/2002/GenModel";
...
EAnnotation eAnnotation = eModelElement.getEAnnotation(GEN_MODEL_PACKAGE_NS_URI);
Which suggests that obviously code should look for annotations with GEN_MODEL_PACKAGE_NS_URI.
However, I was just not getting my annotation which in fact had URI of http://www.eclipse.org/emf/2002/GenModel. Looking in GenModel class that is responsible for generating my model, there was a method GenModel.setSuppressGenModelAnnotations(boolean bool). Naming of the method suggests what it does and in fact in my genmodel's Model properties there is an entry 'Suppress GenModel Annotations' which is set to 'true' by default. Setting this property to false generated my annotations. I am still not sure why it's set to 'true' by default as setting to 'false' would make more sense in my opinion.

Thursday, August 8, 2013

Performing conditional checkout when building with Maven/Tycho

One of the things I really loved about building with Buckminster was its support for conditional component materialization, more about it here. This is one of the features I missed when I moved build system to Maven/Tycho, how am I suppose to checkout pluginA and pluginB from trunk, but pluginC from branchA? I also wanted to brush up on my Groovy skills because I haven't used it in quite some time.

The idea I head on which components to checkout was to query parent (super) POM of my parent plugin. Each parent POM contains modules section that describes what are the modules that make up the build. One I query the parent POM, I would create a collection of modules and use that information to either checkout or update working directory, in my case Jenkins workspace. Below is the groovy script that I wrote.

package com.iwaysoftware.integration.tools.parent
/*
 * Tycho/Maven builds rely on the fact that components already exist in build workspace (Jenkins in our case).
 * There is no easy way to checkout what's needed especially with some logic involved.
 *
 * This groovy script checks out components from SVN repository and relies on information in parent POM, located in this plugin.
 * This script will parse POM file and construct a collection of <module> elements, currently module element looks as following:
 *
 *   <module>../{componentName}</module>
 *
 * Name of component will be normalized to just its name.
 *
 * The convention in our build is that parent plugin is checked out using Jenkin's SCM and build.xml is called, build.xml in turn executes
 * this groovy script. Parent plugin MUST end with ".parent".
 *
 * Following construction of collection, workspace directory will be checked for existance of component that ends with "parent".
 * If such directory exists and size of directories is 1, checkout will be performed. Otherwsie update will be done.
 */

import java.util.logging.ConsoleHandler;
import java.util.logging.Logger

import groovy.io.FileType
import groovy.util.logging.Log;
import groovy.util.slurpersupport.NodeChild;

public class Checkout{
 static def pomFile
 static def svnRoot
 static def svnProjectDir
 static def workspacePath
 static def svnDir
 static def queryFile
 static def ant
 static Logger logger = Logger.getLogger(Checkout.class.toString())

        // namespace of the components. This is used to decide whether checkout or update will be performed
 static def namespace = "com.iwaysoftware"

 public static void main(String[] args) {
  pomFile = args[0]
  svnRoot = args[1]
  svnProjectDir = args[2]
  workspacePath = args[3]
  svnDir = args[4]
  queryFile = args[5]
  
  /*CustomFormatter formatter = new CustomFormatter()
  ConsoleHandler handler = new ConsoleHandler()
  handler.setFormatter(formatter)
  logger.addHandler(handler)*/
  
  logger.info("Script arguements:")
  logger.info("pomFile = " + args[0])
  logger.info("svnRoot = " + args[1])
  logger.info("svnProjectDir = " + args[2])
  logger.info("workspacePath = " + args[3])
  logger.info("libDir = " + args[4])
  logger.info("queryFile = " + args[5])

  ant = new AntBuilder()
  ant.typedef(resource: 'org/tigris/subversion/svnant/svnantlib.xml'){
   classpath {
    fileset(dir: svnDir, includes: '*.jar')
   }
  }
  new Checkout().startCheckout()
 }

 Checkout(){
 }

 private void startCheckout(){
  // parse POM file and construct collection modules consisting of module names
  def records = new XmlSlurper().parse(new File(pomFile))
  def modules = records.depthFirst().findAll{
   it.name() == 'module'
  }
  logger.info("modules.size = " + modules.size())

  def advisors = getAdvisors(queryFile)

  // normalize collection items by stipping leading '../'
  modules = modules.collect {
   it.toString().substring(3, it.toString().size())
  }

  // Query workspace directory for presence of a directory (plugin), here convention is that
  // parent plugin with parent POM is checked out first following by call to build.xml in the plugin.
  // Depending on component size and its values either checkout or update will be performed.
  def components = []


  logger.info("workspace = " + workspacePath)
  
  def workspaceLocation = new File(workspacePath)
  workspaceLocation.eachFileRecurse (FileType.DIRECTORIES) { file ->
   if(file.name.startsWith(namespace)){
    components << file
   }
  }

  logger.info("components.size = " + components.size())

  // check if components collection is of size one and name of directory (plugin) ends with "parent"
  if(components.size() == 1 && components[0].toString().endsWith("parent")){
   logger.info("modules = " + modules)
   modules.each {
    def checkoutUrl = svnProjectDir + "/" + it
    logger.info("checkoutUrl = " + checkoutUrl)
    logger.info("workspace = " + workspacePath)
    def workingDirectoryPath = workspacePath + "/" + it
    logger.info("workingDirectoryPath = " + workingDirectoryPath)

    if(advisors.containsKey(it)){
     checkoutUrl = constructCheckoutUrl(it, advisors)
    }

    logger.info("checkout url:" +  checkoutUrl)

    // start checkout of components in modules collection
    ant.svn(javahl: 'false', svnkit: 'true'){
     checkout(url: checkoutUrl, destPath: workingDirectoryPath)
    }
   }
  }
  // workspace directory already contains checkout components, perform update
  else if(components.size() > 1){
   logger.info("modules.size = " + modules.size())
   modules.each {
    logger.info("updating " + it)
    def updateDir = workspacePath + "/" + it
    ant.svn(javahl: 'false', svnkit: 'true'){ 
     update(dir: updateDir) 
    }
   }
  }
 }

 /*
  * Iterates branches List and checks whether SVN location at ${svnRoot}/branches/${branch}/${module} exists.
  * If it does matched SVN location will be checked out, if not another branch from the List is considered,
  * if none of the SVN branch locations exist, module will be checked out from trunk.
  */
 private String constructCheckoutUrl(String module, Map advisors){
  def branches = advisors.get(module)

  for(branch in branches){
   try{
    ant.svn(javahl: 'false', svnkit: 'true'){
     info(target: svnRoot + "/branches/" + branch + "/" + module)
    }
    def url = svnRoot + "/branches/" + branch + "/" + module
    logger.info("constructed url: " + url)
    return url
   }
   catch(e){
    logger.warning("No SVN INFO for " + svnRoot + "/branches/" + branch + "/" + module)
    continue
   }
  }
  return svnProjectDir + "/" + module
 }

 /*
  * Reads advisor elements from .query file and constucts map of component:modules[:]
  *  <advisor component="com.iwaysoftware.eclipse.workspace" branch="adapter,test"/>
  */
 private Map getAdvisors(String queryFile) {
  def root = new XmlSlurper().parse(new File(queryFile))
  def advisorsList = root.depthFirst().findAll{
   it.name() == 'advisor'
  }

  def advisorsMap = [:]
  advisorsList.each {
   def key
   def branches = []

   if(it instanceof NodeChild){
    def attributes = ((NodeChild)it).attributes()
    key = attributes.get("component")
    branches = attributes.get("branch").split(",")
    advisorsMap.put(key, branches)
   }
  }
  logger.info("advisors: " + advisorsMap)
  return advisorsMap
 }
}
And here the ant script that calls this groovy script
<?xml version="1.0" encoding="UTF-8"?>
<project name="svnCheckout" default="checkout">
 <property environment="env"/>
 <property name="svn.root" value="/path/to/svn/root"/>
 <property name="svn.project.dir" value="/path/to/svn/project/root"/>
 <property name="svn.lib" location="/path/to/svn/libs" />
 <property name="groovy.lib" location="/path/to/groovy/libs" />
 <property name="workspace" location="${env.WORKSPACE}" />
 <property name="basedir" value="." />
 <property name="groovy.script" location="/path/to/groovy/script/Checkout.groovy" />
 <property name="advisors" location="${basedir}/adapterBranch.query"/>

 <taskdef name="groovy"
  classpath="${groovy.lib}/groovy-all-2.1.3.jar"
  classname="org.codehaus.groovy.ant.Groovy" />

 <target name="checkout">
   <echo>${groovy.script}</echo>
   <groovy src="${groovy.script}">
    <!-- pom file -->
    <arg line="${basedir}/pom.xml"/>
    
    <!-- SVN root -->
    <arg line="${svn.root}"/>
    
    <!-- SVN location of project that contains components to be build -->
    <arg line="${svn.project.dir}"/>
    
    <!-- workspace, if using Jenkins use ${env.WORKSPACE} -->
    <arg line="${workspace}"/>
    
    <!-- location of SVNAnt jars -->
    <arg line="${svn.lib}"/>
    
    <!-- location of advisors query file -->
    <arg line="${advisors}"/>
   </groovy>
 </target>
</project>
As you can see this script just calls groovy script with needed parameters.

Here is the .query file that is used by the script to decide whether to checkout components from branch or trunk.
<advisors>
   <advisor component="pluginA" branch="adapter"/>
   <advisor component="pluginB" branch="adapter"/>
   <advisor component="featureA" branch="adapter"/>
   <advisor component="featureX" branch="adapter"/>
 </advisors>
There are still things to be done to improve this script. Adding regular expressions to .query files to make file smaller in cases that there are a lot of repetition of components. Addition of consideration of tags SVN directory is something that will be useful as well. Right now during update I go through collection of checked out components and update them all. Updating can be costly task (time wise), so using svn info command to compare revisions will cut update time.

Thursday, July 25, 2013

Editing TreeViewer's cell with two clicks

With introduction of ColumnViewerEditorActivationStrategy in Eclipse 3.3 there is an ability to control when cell of a viewer gets activated for editing. Unfortunately the events for cell selection is limited to ColumnViewerEditorActivationEvent.MOUSE_CLICK_SELECTION and ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION as far mouse events go. MOUSE_CLICK_SELECTION was not something I wanted to do because I didn't want cell editing to be activated each time selection changes in TreeViewer. I did not want to resort to MOUSE_DOUBLE_CLICK_SELECTION either because something else was suppose to happen on double-click in my case. I needed a way to enable cell editing when user click on a cell once and then again to edit. The solution I found was suggested by David Green (http://greensopinion.blogspot.com/2009/11/treeviewer-two-clicks-to-edit.html). However it did not work in my case. I am not sure exactly why, may be because I was using Nebula's Grid Widget (although it uses the same interfaces as TreeViewer) or custom EditingSupport. I've spend some time figuring out what was the problem, but then decided to take a different approach. Below is the code for my ColumnViewerEditorActivationStrategy implementation.
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationStrategy;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.events.MouseEvent;

class SecondClickColumnViewerEditorActivationStrategy extends ColumnViewerEditorActivationStrategy implements ISelectionChangedListener {

 private int counter = 0;

 public SecondClickColumnViewerEditorActivationStrategy(ColumnViewer viewer) {
  super(viewer);
  viewer.addSelectionChangedListener(this);
 }

 @Override
 protected boolean isEditorActivationEvent(ColumnViewerEditorActivationEvent event) {
  counter++;
  
  if(counter == 1){
   return false;
  }
  // this is in case when user clicked on tree node once, but second time activated context menu by clicking on right button,
  // we don't want to activate cell editing in this case
  else if(counter > 1 && isRightMouseClick(event)){
   return false;
  }
  else{
   return true;
  }
 }

 public void selectionChanged(SelectionChangedEvent event) {
  counter = 0;
 }
 
 /**
  * Determine if right mouse button was clicked
  * @param event
  * @return true if right mouse button was clicked, false otherwise.
  */
 private boolean isRightMouseClick(ColumnViewerEditorActivationEvent event){
  return event.eventType == ColumnViewerEditorActivationEvent.MOUSE_CLICK_SELECTION && ((MouseEvent)event.sourceEvent).button == 3;
 }
}
As you can see the editing enablement is controlled by counters. The important thing here is chain of calls between selectionChanged() and isEditorActivationEvent() methods. When user clicks on cell selectionChanged() gets called, and all subsequential clicks are handled by isEditorActivationEvent() method where counter is being incremented and appropriate return value is set. Note use of isRightMouseClick() call in isEditorActivationEvent(), I did not want to activate both context menu on the cell and editing when user clicked on a tree node and then activated context menu by clicking on right mouse button.

Friday, July 12, 2013

Scrolling draw2d ScrollPane using mouse wheel

Today I was working on a project where there was a need to scroll draw2d ScrollPane using only mouse wheel. Why? Because in this particular project there is a tree drawn using draw2d Figures and to the left of it a Nebula Grid. I won't go in details of why that is, but both trees needed to be scrolled using Grid's scroll bar. However, since draw2d tree had no scroll bar visible (by design), I still wanted to give user ability to scroll the tree using mouse wheel since the wheel is pretty much standard on all modern mice. There is no mouse wheel listener for ScrollPane or Viewport, but I found a nice little helper interface MouseWheelHelper. This interface has just one method:
void handleMouseWheelScrolled(Event event);
From there it was just a matter of finding the right variable that stores scroll value:
public class MyEditPart extends TreeEditPart implements MouseWheelHelper{
 private static final int SCROLL_OFFSET = 10;
 ...
        ...
 @Override
 // subtracting or adding values below controls direction of the scrolling
 public void handleMouseWheelScrolled(Event event) {
  pane.scrollVerticalTo(pane.getViewport().getVerticalRangeModel().getValue() - (event.count * SCROLL_OFFSET));
 }
}
That's it. Now when user clicks into the tree and uses scroll wheel the tree with scroll. What's neat is that by changing value of SCROLL_OFFSET you can speed up or slow down the scrolling. Of course this would not work for draw2d only case, because my ScrollPane was inside an EditPart that was already part of gef stack and that's why I could take advantage of MouseWheelHelper interface.

Wednesday, April 10, 2013

Adding additional composites to Eclipse editor

I've been working on DSL editor that we have in our product. One of the features we wanted to add is a way to examine AST generated based on expression. This AST viewer would allow user to examine parts of language statement and see what's going in the tree so he can of sort of debug each step of the execution.

 On the UI side we needed a way to present the AST tree and synchronize it with the statement that is being entered. One way to do it is add a view that would synchronized with editor, however drawback in this approach is not seeing the view when editor is maximized. What I wanted to achieve is something that Eclipse Compare editor offers - and editor which is split into two parts. I started looking at
org.eclipse.compare code. It's pretty involved and had a lot things that I really didn't need. I took a closer look at our DSL editor that extends TextEditor and decided to play with createPartControl(Composite parent) method since this is where UI elements were build. The result was just what I needed.

What I ended up doing is shown here:

public void createPartControl(Composite parent) {
   SashForm sashForm = new SashForm(parent, SWT.VERTICAL);
   sashForm.setLayout(new FillLayout());

   super.createPartControl(sashForm);
 
   new ASTComposite(sashForm, SWT.BORDER);
}
As you can see SashForm is used to achieve split pane. Top pane is where editor is added, and bottom is where any Composite goes, in my case it's a specialized Composite that has TreeViewer holding AST of the expression. Pretty simple and effective.

Monday, April 8, 2013

Making location in target definition version agnostic

When adding custom target definition location make version of the software site "0.0.0" in order to pick up latest version. This works just like when defining feature that your feature depends on to pick up latest version.

Blogger Syntax Highliter