Enabling Spring in Scout applications

Today I am attending the first Scout User Day 2014 in Ludwigsburg, which is aligned with EclipseCon Europe 2014 starting tomorrow. Yesterday we had a pre-event dinner with some attendees and the organizers at the Rossknecht restaurant. IMG_3161IMG_3160 I got into a chat with Nejc Gasper, who will give a talk titled “Build a Scout backend with Spring” today. I was a bit surprised as he told me he did not manage to get Spring’s classpath scanning working yet. Since we are doing this in our application, I think it is worth writing now down what we had to do to get this working. The goal in our application is primarely to use Spring as dependency injection container, since the customer uses Spring in all their other Java based applications, too, and wanted us to do so also.

Spring Configuration

The Spring configuration files are located in the folder META-INF/spring of the *.client, *.shared, *.server projects. In this configuration files, we mainly activate classpath scanning:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:annotation-config />
    <context:component-scan base-package="com.rhenus.fl" />
    <!--  http://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/html/validation.html#core-convert-Spring-config -->
    <beans:bean id="conversionService" class="org.springframework.core.convert.support.DefaultConversionService" />
</beans:beans>

The next important thing is to copy the files spring.handlers, spring.schemas<, spring.tooling into the META-INF folder. The files can be found in the META-INF directory of bundle org.springframework.context. screenshot 21 Without doing this, you will get errors while loading the Spring configuration like this:

Caused by: 
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/context]|Offending resource: URL [bundleresource://9.fwk1993775065:1/META-INF/spring/fl_client.xml]|
	at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:70)
	at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85)
	at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:80)
	at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:316)
	at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1421)
	at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1414)
	at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:187)
	at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:141)
	at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:110)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:508)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:391)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:335)
	at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:303)
	at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:180)
	at org.springframework.context.support.GenericXmlApplicationContext.load(GenericXmlApplicationContext.java:116)

Bundle Activator

The Spring configuration files are loaded in the Bundle Activator classes of the three “main” Scout projects (client/shared/server). The Activator can also be used then to access the ApplicationContext. We use the GenericXmlApplicationContext to initialize the context from the XML configuration above. One important thing is that this class uses the ClassLoader of the Activator. Otherwise you will get again the error mentioned in the section above. The Activator class looks then as follows:


public class Activator extends Plugin {

 // The plug-in ID
 public static final String PLUGIN_ID = "com.rhenus.fl.application.client";
 public final static String SPRING_CONFIG_FILE = "META-INF/spring/fl_client.xml";

 // The shared instance
 private static Activator plugin;
 private ApplicationContext ctx;

 @Override
 public void start(BundleContext context) throws Exception {
  super.start(context);
  plugin = this;
  init(context);
 }

 @Override
 public void stop(BundleContext context) throws Exception {
  plugin = null;
  super.stop(context);
 }

 public static Activator getDefault() {
  return plugin;
 }

 private void init(BundleReference bundleContext) {
  URL url = getClass().getClassLoader().getResource(SPRING_CONFIG_FILE);

  UrlResource usr = new UrlResource(url);

  ctx = new GenericXmlApplicationContext() {
  @Override
  public ClassLoader getClassLoader() {
   return Activator.class.getClassLoader();
  }
 };
  ((GenericXmlApplicationContext) ctx).load(usr);
  ((AbstractApplicationContext) ctx).refresh();

 }

 public ApplicationContext getContext() {
  return ctx;
 }
}

Service Factory

In order to use dependency injection in Scout services, the services themselves must be instantiated through the Spring ApplicationContext. The default implementation of course is not aware of Spring, so we need to customize this. Unfortunately we have to copy the class org.eclipse.scout.rt.server.services.ServerServiceFactory. We need just to exchange one single line in the method updateInstanceCache(), where the service is instantiated, but this method is private in Scout. The line

m_service = m_serviceClass.newInstance();

is replaced by

m_service = getContext().getBean(m_serviceClass);

Since we have to provide different ApplicationContexts in the different plugins, we put this into the abstract class AbstractSpringAwareServerServiceFactory (full code):

public abstract class AbstractSpringAwareServerServiceFactory implements IServiceFactory {

 private void updateInstanceCache(ServiceRegistration registration) {
  synchronized (m_serviceLock) {
   if (m_service == null) {
    try {
     // CUSTOMIZING BEGIN
//     m_service = m_serviceClass.newInstance();
     m_service = getContext().getBean(m_serviceClass);
     // CUSTOMIZING END
     if (m_service instanceof IService2) {
      ((IService2) m_service).initializeService(registration);
     } else if (m_service instanceof IService) {
      ((IService) m_service).initializeService(registration);
     }
    } catch (Throwable t) {
     LOG.error("Failed creating instance of " + m_serviceClass,
       t);
    }
   }
  }
 }
 
 // CUSTOMIZING BEGIN
 protected abstract ApplicationContext getContext();
 // CUSTOMIZING END

}

The concrete classes implement the method getContext() by accessing the method from the Bundle Activator:

public class ServerServiceFactory extends AbstractSpringAwareServerServiceFactory {

  /**
   * @param serviceClass
   */
  public ServerServiceFactory(Class<?> serviceClass) {
    super(serviceClass);
  }

  @Override
  protected ApplicationContext getContext() {
    return Activator.getDefault().getContext();
  }

}

plugin.xml

The service factory class implemented above must be used now to create the services. This is done in the plugin.xml file:

<service
 factory="com.rhenus.fl.application.server.services.ServerServiceFactory"
 class="com.rhenus.fl.tmi.server.tmirln010.TMIRLN010Service"
 session="com.rhenus.fl.application.server.ServerSession">
</service>

Use Dependency Injection

Now we are finally able to use Dependency Injection with javax.inject.Inject with Scout services.

import org.springframework.stereotype.Component;
import javax.inject.Inject;
...

@Component
@InputValidation(IValidationStrategy.PROCESS.class)
public class TMIRLN010Service extends AbstractTMIRLN010Service {
  @Inject
  protected ConversionService conversionService;
  ...
}

Go!

If everything is correct, you will now recognize the following lines in the console when starting up the Scout application:

Okt 27, 2014 8:45:28 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from URL [bundleresource://9.fwk1993775065:1/META-INF/spring/fl_client.xml]
Okt 27, 2014 8:45:29 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from URL [bundleresource://10.fwk1993775065:1/META-INF/spring/fl_shared.xml]
Okt 27, 2014 8:45:29 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing com.rhenus.fl.application.shared.Activator$1@b40d694: startup date [Mon Oct 27 08:45:29 CET 2014]; root of context hierarchy
Okt 27, 2014 8:45:29 AM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
Okt 27, 2014 8:45:29 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing com.rhenus.fl.application.client.Activator$1@292f062b: startup date [Mon Oct 27 08:45:29 CET 2014]; root of context hierarchy
Okt 27, 2014 8:45:29 AM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 
INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring

Scout tables with fixed columns

For tables with many columns it is often better if some of the first columns stay fixed when the user scrolls the table content horizontally. Since RAP 2.0, tables and trees support fixed columns, but Scout (current: 4.0/Eclipse Luna) does not provide this feature for its Table implementation. Since Scout supports different UI frameworks (Swing,SWT,RAP) and only RAP provides a table implementation with fixed columns support, this might be a reason for this missing feature.

The necessary additions can be added to Scout without changing Scout sources themselves. Scout allows to customize UI components, including existing ones. We can add this feature with fragments. The code mentioned below can be fetched from Github “scout-experimental”.

Client Extension Plugin

AbstractTableWithFixedColumns

First, we need a Table implementation that provides an additional configuration property fixedColumns. The value is stored using the element’s Property Support:

package org.eclipse.scout.rt.extension.client.ui.basic.table;

import org.eclipse.scout.commons.annotations.ConfigProperty;

/**
 * A table that supports Fixed Columns.
 *
 * @see http://eclipse.org/rap/developers-guide/devguide.php?topic=tree-table.html&version=2.0
 */
public class AbstractTableWithFixedColumns extends AbstractExtensibleTable {
	public static final String PROP_FIXED_COLUMNS = "org.eclipse.rap.rwt.fixedColumns"; // =RWT.FIXED_COLUMNS
	/**
	 * Configures how many of the visible columns should be fixed.
	 * @return A value greater than 0
	 */
	@ConfigProperty(ConfigProperty.INTEGER)
	protected int getConfiguredFixedColumns () {
		return -1;
	}

	public void setFixedColumns (int n) {
		propertySupport.setPropertyInt(PROP_FIXED_COLUMNS, n);
	}

	public int getFixedColumns () {
		return propertySupport.getPropertyInt(PROP_FIXED_COLUMNS);
	}

	@Override
	protected void initConfig() {
		super.initConfig();
		setFixedColumns(getConfiguredFixedColumns());
	}

}

This custom class can be specified as default base class for tables in the Scout workspace preferences:
Scout base class

In the client code, the table field would be defined like this:

public class CtyTableField
      extends
      AbstractTableField<CtyTableField.Table> {
          public class Table extends AbstractTableWithFixedColumns {
             @Override
             protected int getConfiguredFixedColumns() {
                return 2;
             }
          }
}

Scout RAP UI extension fragment

RwtScoutTableExt

The class that must be customized is org.eclipse.scout.rt.ui.rap.basic.table.RwtScoutTable. I’m not good in creating names, so I call its extension just RwtScoutTableExt.


package org.eclipse.scout.rt.ui.rap.basic.table;

import org.eclipse.rap.rwt.RWT;
import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn;
import org.eclipse.scout.rt.ui.rap.ext.table.TableEx;
import org.eclipse.swt.widgets.Composite;

public class RwtScoutTableExt extends RwtScoutTable {
	@Override
	protected void initializeUi(Composite parent) {
	    super.initializeUi(parent);
	    TableEx table = getUiField();
		// Fixed Columns Support
		// see
		// http://eclipse.org/rap/developers-guide/devguide.php?topic=tree-table.html&version=2.0
		// the configured fixed columns refer to visible columns only, but for
		// the RWT table the number of fixed
		// columns include also the non-visible ones. Compute how many columns
		// should be fixed in the RWT table.
		Integer configuredFixedColumns = (Integer) getScoutObject().getProperty(
				RWT.FIXED_COLUMNS);
		if (configuredFixedColumns != null && configuredFixedColumns > 0) {
			int fixedColumns = 0;
			int visibleColumns = 0;
			for (IColumn<?> column : getScoutObject().getColumns()) {
				fixedColumns++;
				if (column.isDisplayable() && column.isInitialVisible()) {
					visibleColumns++;
				}
				if (visibleColumns == configuredFixedColumns) {
					break;
				}
			}
			table.setData(RWT.FIXED_COLUMNS, new Integer(fixedColumns));
		}
	}
}

After Scout has initialized the table component (call of super), the table has to be configured with the number of fixed columns:


table.setData(RWT.FIXED_COLUMNS, new Integer(fixedColumns));

The number of configured fixed columns has to be specified by a property of the table field. For now, you might wonder why we do not simply set the configured fixed column value. The reason is, that some columns might not be visible, and when configuring the fixed columns, you would expect that only the visible ones should count.

RwtScoutTableFieldExt

The table class itself is constructed by RwtScoutTableField. In order to construct our customized class, RwtScoutTableField must be subclassed and override the createRwtScoutTable() method:

package org.eclipse.scout.rt.ui.rap.form.fields.tablefield;

import org.eclipse.scout.rt.client.ui.form.fields.smartfield.IContentAssistFieldProposalForm;
import org.eclipse.scout.rt.ui.rap.basic.table.IRwtScoutTable;
import org.eclipse.scout.rt.ui.rap.basic.table.RwtScoutTable;
import org.eclipse.scout.rt.ui.rap.basic.table.RwtScoutTableExt;
import org.eclipse.scout.rt.ui.rap.util.RwtUtility;

public class RwtScoutTableFieldExt extends RwtScoutTableField {
	@Override
	protected IRwtScoutTable createRwtScoutTable() {
		if (getScoutObject().getForm() instanceof IContentAssistFieldProposalForm) {
			return new RwtScoutTable(RwtUtility.VARIANT_PROPOSAL_FORM);
		} else {
			return new RwtScoutTableExt();
		}
	}

}

MANIFEST.MF

The created fragment is a fragment of the bundle org.eclipse.scout.rt.ui.rap. At least from Scout version 3.9.0 on, likely earlier, the mentioned solution should work.

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Scout UI RAP - Extension
Bundle-SymbolicName: de.kthoms.scout.rt.ui.rap;singleton:=true
Bundle-Version: 1.0.0.qualifier
Fragment-Host: org.eclipse.scout.rt.ui.rap;bundle-version="3.9.0"
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Export-Package: org.eclipse.scout.rt.ui.rap.form.fields.tablefield

fragment.xml

The extended field class has to be configured in the fragment’s descriptor fragment.xml by using the org.eclipse.scout.rt.ui.rap.formfields extension point:


<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<fragment>
  <extension
        point="org.eclipse.scout.rt.ui.rap.formfields">
      <formField
            active="true"
            modelClass="org.eclipse.scout.rt.client.ui.form.fields.tablefield.ITableField"
            name="Table field"
            scope="global">
            <uiClass
              class="com.itemis.scout.rt.ui.rap.form.fields.tablefield.RwtScoutTableFieldExt">
            </uiClass>
      </formField>
  </extension>

</fragment>

Note the scope="global" configuration. The interface org.eclipse.scout.rt.ui.rap.extension.IFormFieldExtension defines 3 scopes: default, global, local. The description for the global scope is:

IFormFieldExtension.SCOPE_GLOBAL to indicate this extension to have a global scope (whole eclipse). Global defined extensions overwrite the default implementation.

Overwriting the default implementation is exactly what we try to achieve here.

Result

The first 2 columns are marked as fixed. In fact, in front of the first visible column the table has a non-displayable Id column. When scrolling the table horizontally, the columns “Iso 2 Code” and “Description” are fixed.

screenshot 2014-07-07 um 12.10.50
screenshot 2014-07-07 um 12.11.02