Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "Skalli/User Guide/Tutorial/Extensions/Extended Tutorial"

m (Creating the Data Model)
m (Creating the Data Model)
Line 68: Line 68:
 
<source lang="java">
 
<source lang="java">
 
public static final String MODEL_VERSION = "1.0";
 
public static final String MODEL_VERSION = "1.0";
public static final String NAMESPACE = "http://org.example.skalli/2011/04/ProjectPortal/Model/Extension-HelloWorld";
+
public static final String NAMESPACE = "http://org.example.skalli/2011/04/Skalli/Model/Extension-HelloWorld";
 
</source>
 
</source>
  

Revision as of 05:41, 1 June 2011

Introduction

This tutorial describes how to create a custom extension for Skalli called Hello World.

This Hello World extension will contain properties which can be edited by the user and which will be displayed on the project detail page in a separate info box.

The final Hello World Extension will look like this:

Skalli-HelloWorldExtantionInfo.jpg

Prerequisites

You have set up Skalli and you have at least basic knowledge of OSGi.

Creating the Project Skeleton

Create a new bundle project org.example.skalli.ext.helloworld.

Ensure you have the following directories created in your project's root directory:

  • META-INF
  • OSGI-INF
  • and a source folder (e.g. src/main/java)

Take care that the property bin.incudes in the build.properties file contains these folders as well:

bin.includes = META-INF/,\
               .,\
               OSGI-INF/

Creating the Data Model

Since we are going to allow the user to input data in our extension, we need a model to persist this data. A model is a POJO that extends org.eclipse.skalli.model.ext.ExtensionEntityBase and contains the description of the data structure.

Create the class org.example.skalli.model.ext.helloworld.HelloWorldProjectExt.java:

package org.example.skalli.model.ext.helloworld;
 
import org.eclipse.skalli.model.ext.ExtensionServiceBase;
 
public class HelloWorldProjectExt extends ExtensionEntityBase {
   ...
}

In this Hello World extension we want to have 2 properties:

  1. name - the name of the user
  2. friends - a list of friends which should be greeted (illustrating how to work with collections)

Each property must have a unique name specified as a string constant that is annotated with @PropertyName. The argument position is used to determine the order of UI elements in the edit form associated with the extension.

@PropertyName(position = 0)
public static final String PROPERTY_NAME = "name";
 
@PropertyName(position = 1)
public static final String PROPERTY_FRIENDS = "friends";

Furthermore, you should specify a MODEL_VERSION and NAMESPACE. Although currently not used, in future versions of Skalli the model version might become important for automatic model migrations.

public static final String MODEL_VERSION = "1.0";
public static final String NAMESPACE = "http://org.example.skalli/2011/04/Skalli/Model/Extension-HelloWorld";

For each property a corresponding field, a getter and a setter method are needed as well (for collections setters are optional):

private String name = "";
private TreeSet<String> friends = new TreeSet<String>();
 
public String getName() {
  return name;
}
 
public void setName(final String name) {
  this.name = name;
}
 
public Collection<String> getFriends() {
  return this.friends;
}

Notes:

  • The names of the fields must match the values of the string constants defined above, and the getters/setters must be named according to bean conventions.
  • All fields must be initialized since some UI elements of Vaadin (the UI framework used by Skalli) cannot handle null pointers.
  • Note the declaration of friends referring explicitly to TreeSet. Currently Skalli uses the XStream framework to convert models to XML and must be able to deduce the proper type of the field from its declaration.

For more details about mapping and naming conventions see the Javadoc of @PropertyName.

Optionally you may define some convenience methods, e.g. to add some more friends to the model:

public void setFriends(TreeSet<String> friends) {
  this.friends = new TreeSet<String>(friends);
}
 
public void addFriends(Collection<String> additionalFriends) {
  this.friends.addAll(additionalFriends);
}
 
public void addFriend(String friend) {
  this.friends.add(friend);
}

Making Skalli Aware of the Hello World Extension

Skalli needs to know a few additional things about the Hello World extension before it can use it. In general, all Skalli extensions are OSGI services, and for model extensions you must provide an OSGi service which implements the interface ExtensionService<T>. There is a default implementation ExtensionServiceBase from which you should derive your service, with <T> being your newly created HelloWorldProjectExt.

package org.example.skalli.ext.helloworld.internal;
 
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
 
import org.eclipse.skalli.common.util.CollectionUtils;
import org.eclipse.skalli.common.util.StringLengthValidator;
import org.eclipse.skalli.model.ext.ExtensionServiceBase;
import org.eclipse.skalli.model.ext.PropertyValidator;
import org.eclipse.skalli.model.ext.Severity;
import org.example.skalli.model.ext.helloworld.HelloWorldProjectExt;
 
public class ExtensionServiceHelloWorld extends ExtensionServiceBase<HelloWorldProjectExt> {
 
  private static final Map<String, String> CAPTIONS = CollectionUtils
      .asMap(new String[][] {
          { HelloWorldProjectExt.PROPERTY_NAME, "Your name" },
          { HelloWorldProjectExt.PROPERTY_FRIENDS, "Your friends" } });
 
  @Override
  public Class<HelloWorldProjectExt> getExtensionClass() {
    return HelloWorldProjectExt.class;
  }
 
  @Override
  public String getModelVersion() {
    return HelloWorldProjectExt.MODEL_VERSION;
  }
 
  @Override
  public String getShortName() {
    return "helloWorldService";
  }
 
  @Override
  public String getCaption() {
    return "Hello World Extension";
  }
 
  @Override
  public String getCaption(String propertyName) {
    String caption = CAPTIONS.get(propertyName);
    if (caption == null)
      caption = super.getCaption(propertyName);
    return caption;
  }
 
  @Override
  public String getDescription() {
    return "Hello World Extension - a simple example.";
  }
 
  @Override
  public String getNamespace() {
   return HelloWorldProjectExt.NAMESPACE;
  }
 
  @Override
  public String getXsdFileName() {
   return "HelloWorldProjectExt.xsd";
  }
}

The given captions will show up as labels for UI elements in the edit form.

Once everything is in place you need to make your extension service known to Skalli. This is achieved by providing a service component.

Create a file OSGI-INF/ExtensionServiceHelloWorld.xml to register ExtensionServiceHelloWorld as an implementation of ExtensionService:

<?xml version="1.0" encoding="UTF-8"?>
  <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true"
    name="org.example.skalli.ext.ExtensionServiceHelloWorld">
    <implementation class="org.example.skalli.ext.helloworld.internal.ExtensionServiceHelloWorld"/>
      <service>
        <provide interface="org.eclipse.skalli.model.ext.ExtensionService"/>
    </service>
  </scr:component>

You also need to add this service to the Service-Component section of the bundle manifest:

  Service-Component: OSGI-INF/ExtensionServiceHelloWorld.xml

User Interface

An extension usually is associated with an info box that is displayed on a project's detail page and an edit form that allows project committers to edit the data stored in the extension. Note that both are optional but usually you want your model extensions to be visible and editable. For the Hello World extension we will provide both, and for that we have to implement two additional service components:

  1. An info box is an implementation of the interface ProjectInfoBox
  2. An edit form is an implementation of the interface ExtensionFormService

Note that the form typically corresponds 1:1 to the model, i.e. for each property in the model there is a corresponding field in the form. The info box does not need to necessarily. It is also possible that an info box consolidates data from multiple model extensions or is not used at all.

Providing the Edit Functionality

When in project edit mode, the user will enter data in forms which are rendered by Skalli. In order to provide such a form we have to implement the interface ExtensionFormService<T extends ExtensionEntityBase>. For the Hello World extension, T is HelloWorldProjectExt.

There is a base class, AbstractExtensionFormService, which implements the difficult stuff already.

The following screenshot shows the final EditFrom:

Skalli UserGuide Tutorial Extensions Extended Tutorial HelloWorldExtensionEdit.jpg

Create a new class org.example.skalli.ext.helloworld.ui.ExtensionServiceHelloWorldProjectExtEditForm:

package org.example.skalli.ext.helloworld.ui
 
import org.example.skalli.model.ext.helloworld.HelloWorldProjectExt;
import org.eclipse.skalli.view.ext.AbstractExtensionFormService;
 
public class ExtensionServiceHelloWorldProjectExtEditForm extends
    AbstractExtensionFormService<HelloWorldProjectExt> {
...
}

Implement the mandatory methods:

@Override
public float getRank() {
  return 100.0f; // put it at the bottom of the page, the hello world extension is not an important one
}
 
@Override
public Class<HelloWorldProjectExt> getExtensionClass() {
  return HelloWorldProjectExt.class;
}
 
@Override
public HelloWorldProjectExt newExtensionInstance() {
  return new HelloWorldProjectExt();
}
 
@Override
public String getIconPath() {
  return null;
}
 
@Override
protected Item getItemDataSource(Project project) {
  return new BeanItem<HelloWorldProjectExt>(getExtension(project));
}

The fields which you will see in edit mode are created and configured by a FormFieldFactory. If your model extension has only string properties, you do not need to implement anything. Skalli will automatically create the necessary text input fields. In that case, getFieldFactory() should return null.

@Override
protected FormFieldFactory getFieldFactory(Project project, ProjectEditContext context) {
  return null;
}

Since a collection is used in our Hello World example, the default field factory is not sufficient. You have to provide your own field factory to create a suitable input element for the collection:

@Override
protected FormFieldFactory getFieldFactory(Project project,
    ProjectEditContext context) {
  return new MyFieldFactory(project, context);
}
 
private class MyFieldFactory extends
    DefaultProjectFieldFactory<HelloWorldProjectExt> {
 
  private static final long serialVersionUID = -6604268736563790136L;
  private HelloWorldProjectExt extension;
 
  public MyFieldFactory(Project project, ProjectEditContext context) {
    super(project, HelloWorldProjectExt.class, context);
    extension = getExtension(project);
  }
 
  @Override
  protected Field createField(Object propertyId, String caption) {
    Field field = null;
    if (HelloWorldProjectExt.PROPERTY_FRIENDS.equals(propertyId)) {
      field = new MultiTextField(caption, extension.getFriends(), projectTemplate.getMaxSize(extensionClassName, propertyId));
    }
    return field;
  }
}

Note that we only created a field for PROPERTY_FRIENDS, but not for PROPERTY_NAME. The later is a string field and Skalli will create a text edit field for us automatically.

In order make your ExtensionFormService implementation known to Skalli you have to register it as service component. Create a file OSGI-INF/ExtensionServiceHelloWorldProjectExtEditForm.xml with the following content (and don't forget to add another line to the Service-Component property in the manifest):

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="org.example.skalli.ext.helloworld.form">
   <implementation class="org.example.skalli.ext.helloworld.ui.ExtensionServiceHelloWorldProjectExtEditForm"/>
   <service>
      <provide interface="org.eclipse.skalli.view.ext.ExtensionFormService"/>
   </service>
</scr:component>

Now you will be able to see a Hello World edit form when you are in edit mode. By default, the Hello World edit form for a project is switched off. Click on the edit link in the header of the form. This will assign a Hello World model extension to the project and you can start editing.

Providing the Info Box

Model extensions usually render their information as separate info boxes on a project's detail page. In order to provide such an info box, you have to implement interface ProjectInfoBox.

Create a new class org.example.skalli.ext.helloworld.ui.ExtensionServiceProjectHelloWorldBox:

package org.example.skalli.ext.helloworld.ui;
 
import org.example.skalli.model.ext.helloworld.HelloWorldProjectExt;
 
import org.eclipse.skalli.common.Services;
import org.eclipse.skalli.model.core.Project;
import org.eclipse.skalli.view.ext.ExtensionUtil;
import org.eclipse.skalli.view.ext.ProjectInfoBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
 
public class ExtensionServiceProjectHelloWorldBox implements ProjectInfoBox {
  @Override
  public String getIconPath() {
    return null;
  }
 
  @Override
  public String getCaption() {
    return "Hello World";
  }
 
  @Override
  public float getPositionWeight() {
    return 1.01f;
  }
 
  @Override
  public int getPreferredColumn() {
    return COLUMN_WEST;
  }
 
  @Override
  public boolean isVisible(Project project, String userId) {
    return project.getExtension(HelloWorldProjectExt.class) != null;
  }
 
  @Override
  public Component getContent(Project project, ExtensionUtil util) {
    HelloWorldProjectExt ext = project.getExtension(HelloWorldProjectExt.class);
 
    Layout layout = new CssLayout();
    layout.setSizeFull();
 
    layout.addComponent(new Label(ext.getName() + " greets the following friends: "));
 
    for (String friend : ext.getFriends()) {
      layout.addComponent(new Label("Hello " + friend + "!"));
    }
 
    return layout;
  }
}

Again you have to register the implementation as service component. Create a file OSGI-INF/ExtensionServiceProjectHelloWorldBox.xml (and don't forget the manifest entry):

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="org.example.skalli.ext.helloworld.infobox">
     <implementation class="org.example.skalli.ext.helloworld.ui.ExtensionServiceProjectHelloWorldBox"/>
     <service>
        <provide interface="org.eclipse.skalli.view.ext.ProjectInfoBox"/>
     </service>
</scr:component>

Now you will be able to see the Hello World info box for all projects to which a Hello World model extension is assigned. If you do not see the info box on a project's detail page, click on Edit in the navigation panel (if you are a committer of the project), and switch on the model extension by enabling the corresponding Hello World edit form.

Validating User Input

With validators you can check project data for syntactical and semantical correctness. Violations will be displayed to project committers on the project's detail page and in the edit dialog.

ExtensionServiceBase provides a default implementation for getPropertyValidators() where no validation is done.

In our Hello World example we want to ensure that a name is entered and that its length is between 2 and 100 characters (friends are not validated for the time being). For that we overwrite the getPropertyValidators() method in ExtensionServiceHelloWorld:

package org.example.skalli.ext.helloworld.internal;
 
import org.eclipse.skalli.common.util.StringLengthValidator;
import org.eclipse.skalli.model.ext.PropertyValidator;
import org.eclipse.skalli.model.ext.Severity;
....
 
public class ExtensionServiceHelloWorld extends
    ExtensionServiceBase<HelloWorldProjectExt> {
 
  ....
 
  @Override
  public Set<PropertyValidator> getPropertyValidators(String propertyName, String caption) {
    if (HelloWorldProjectExt.PROPERTY_NAME.equals(propertyName)) {
      return getNameValidators(caption);
    } else {
      return super.getPropertyValidators(propertyName, caption);
    }
  }
 
  private Set<PropertyValidator> getNameValidators(String caption) {
    Set<PropertyValidator> validators = new HashSet<PropertyValidator>();
    validators.add(getLengthValidator(caption));
    return validators;
  }
 
  private StringLengthValidator getLengthValidator(String caption) {
    StringLengthValidator stringLengthValidator = new StringLengthValidator(Severity.ERROR, HelloWorldProjectExt.class,
        HelloWorldProjectExt.PROPERTY_NAME, caption, 2, 200);
    stringLengthValidator.setValueRequired(true);
    return stringLengthValidator;
  }
}

Note that the returned StringLengthValidator is configured to report validation issues with severity ERROR. There are currently four severities defined in Skalli: FATAL, ERROR, WARNING and INFO. Only issues with severity FATAL prevent a change to project data from being saved. You must correct them immediately before you can leave the edit dialog. Issues with severity below FATAL are tolerated by Skalli, but corresponding warnings on the project's detail page will always remember you, that there is something to do.

If you need a validator to check more than one property at a time, you may alternatively implement getExtensionValidators().

Providing a Rest API

Using the rest API it is possible for clients to retrieve data in an XML representation. The rest API can be accessed by simply inserting "api" in the URL as shown below.

Example:

You have selected a project named myproject in Skalli, so the URL might look like this:

http://skalli.example.org/projects/myproject

Now change this to:

http://skall.example.orgi/api/projects/myproject?referer=guest

This will show you the project's data as XML document. Note, the referer parameter (or alternatively a Referer HTTP header) is used to track clients that access project resources through the rest API and inform them about changes in the API. Enter your user name here (or just guest as in the example).

Calling this URL will return an XML output similar to the following:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://xml.sap.com/2010/08/ProjectPortal/API" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.eclipse.org/skalli/2010/API http://localhost:8080/schemas/project.xsd"
    apiVersion="1.4" lastModified="2011-04-18T11:59:40.604Z" modifiedBy="guest">
  <id>myproject</id>
  ....
  <extensions>
    <Hello xmlns="http://www.eclipse.org/skalli/2010/API/Extension-HelloWorld"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.eclipse.org/skalli/2010/API/Extension-HelloWorld http://localhost:8080/schemas/null"
           apiVersion="1.0"
           modifiedBy="guest"  ... >
      <name>Robert</name>
      <friends>
        <friend>Britta</friend>
        <friend>Maria</friend>
      </friends>
    </Hello>
  </extensions>
</project>

You will not get that for free though. You have to write a converter.

Read Access

Create a class HelloWorldConverter:

package org.example.skalli.ext.helloworld.internal;
 
import org.eclipse.skalli.model.ext.AbstractConverter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
 
public class HelloWorldConverter extends AbstractConverter<HelloWorldProjectExt> {
 
  public static final String API_VERSION = "1.0"; //$NON-NLS-1$
  public static final String NAMESPACE = "http://www.eclipse.org/skalli/2010/API/Extension-HelloWorld"; //$NON-NLS-1$
 
  public HelloWorldConverter(String host) {
    super(HelloWorldProjectExt.class, "helloWorld", host);
  }
 
  @Override
  public String getApiVersion() {
    return API_VERSION;
  }
 
  @Override
  public String getNamespace() {
    return NAMESPACE;
  }
 
  @Override
  public String getXsdFileName() {
    return null;
  }
 
  @Override
  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
    HelloWorldProjectExt ext = (HelloWorldProjectExt) source;
    writeNode(writer, "name", ext.getName());
    writeFriends(writer, ext);
    writer.endNode();
 
  }
 
  private void writeFriends(HierarchicalStreamWriter writer, HelloWorldProjectExt ext) {
    writer.startNode("friends");
    for (String friend : ext.getFriends()) {
      writeNode(writer, "friend", friend); //$NON-NLS-1
    }
  }
}

Finally, you must overwrite the getConverter() method ExtensionServiceHelloWorld to return a HelloWorldConverter instance:

package org.example.skalli.ext.helloworld.internal;
 
import org.eclipse.skalli.model.ext.AliasedConverter;
...
public class ExtensionServiceHelloWorld extends ExtensionServiceBase<HelloWorldProjectExt> {
  ...
  @Override
  public AliasedConverter getConverter(String host) {
    return new HelloWorldConverter(host);
  }
 
}

Back to the top