pelle.io

Delivering GUI applications with docker

A problem that I have to face from time to time, is to find a way to deliver a piece of software in a manner, that it will run on every computer without the need of a complex setup routine and without the risk of problems due to the unknown environment the software will run in.
The solution a few years back would have been to create a VirtualBox/VMWare/… image containing everything needed, but maintaining and keeping the image up to date is a very tedious and error prone task.
Luckily nowadays several tools exist to automate the creation of such images, as well as a somewhat different approach to the problem: Docker

From Wikipedia:

Docker is a tool that can package an application and its dependencies in a virtual container that can run on any Linux server

Docker makes heavy use of Linux capabilities to run multiple isolated Linux systems (LXC (LinuX Containers)) on a single control host. Because docker is very lightweight and not a full blown virtualization, the startup time for docker containers is next to nothing, making it the perfect solution for easy application distribution .

In the following post I will outline the important steps needed to integrate an Eclipse distribution into an docker container so it can be used in any environment taken from the lessons I learned while creating a ready-to-run solution to showcase a little framework of mine:

There are already several ways to integrate GUI applications into an docker environment:

  • docker-desktop eses a clever setup of Xpra + Xephyr to provide a fully X11 desktop inside a docker container
  • Here a solution is shown, that shares the unix domain socket (/tmp/.X11-unix) for X11 between host and docker
  • And of course many solutions use VNC to deliver the UI

Most of these solutions were to complex for my problem (just to deliver eclipse with a working environment), to error prone or needed additional software besides docker which I wanted to avoid for ease of use.

I opted for an simple SSH server inside the container using an SSH client to forward the UI to the local X11 server (ssh -X …) together with a nice little script that handles the docker setup and the SSH login.

First generate a passwordless set of SSH keys to avoid having to login manually when logging into the container

1
ssh-keygen -f ssh/id_rsa -q -N ""

Download the eclipse distribution to use

1
2
wget http://ftp-stud.fht-esslingen.de/pub/Mirrors/eclipse/technology/epp/downloads/release/luna/R/eclipse-standard-luna-R-linux-gtk-x86_64.tar.gz
tar -xzvf eclipse-standard-luna-R-linux-gtk-x86_64.tar.gz

Now for the Dockerfile (full file available here):

The usual docker preamble, we build upon Ubuntu 12.04 due to some X11 forwarding bugs in 14.04.

1
2
FROM     ubuntu:12.04
MAINTAINER Christian Pelster <pelle@pelle.io>

Update package repository and install needed software.

1
2
3
RUN apt-get update
RUN apt-get install -y openjdk-7-jdk
RUN apt-get install -y openssh-server

Configure SSH and store our public key, so we can login with SSH.

1
2
3
4
5
RUN mkdir /var/run/sshd

EXPOSE 22
ADD ssh/id_rsa.pub /root/.ssh/authorized_keys
RUN chown root:root /root/.ssh/authorized_keys

Add the eclipse distribution.

1
ADD eclipse /root/eclipse/

And finally start the SSH server on container startup.

1
CMD ["/usr/sbin/sshd", "-D"]

Now build the image

1
sudo docker build -t pellepelster/mango-demo .

A script to conveniently start the container is available here the individual steps are:

Name of the docker image to get, and the name of the container that will be started locally.

1
2
DOCKER_IMAGE_NAME="pellepelster/mango-demo"
DOCKER_CONTAINER_NAME="mango-demo"

Ask docker if there is any container with DOCKER_CONTAINER_NAME already running (-a display all containers, running or not)

1
2
TMP=$(docker ps -a | grep ${DOCKER_CONTAINER_NAME})
CONTAINER_FOUND=$?

Check if container is actually running.

1
2
TMP=$(docker ps | grep ${DOCKER_CONTAINER_NAME})
CONTAINER_RUNNING=$?

Depending on the containers status, create and run, or start the container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if [ $CONTAINER_FOUND -eq 0 ]; then

    echo -n "container '${DOCKER_CONTAINER_NAME}' found, "

    if [ $CONTAINER_RUNNING -eq 0 ]; then
        echo "already running"
    else
        echo -n "not running, starting..."
        TMP=$(docker start ${DOCKER_CONTAINER_NAME})
        echo "done"
    fi

else
    echo -n "container '${DOCKER_CONTAINER_NAME}' not found, creating..."
    TMP=$(docker run -d -P --name ${DOCKER_CONTAINER_NAME} ${DOCKER_IMAGE_NAME})
    echo "done"
fi

Get the exposed SSH port and split the address of the listening interface & port and store them for later use.

1
2
3
4
5
SSH_URL=$(docker port ${DOCKER_CONTAINER_NAME} 22)
SSH_URL_REGEX="(.*):(.*)"

SSH_INTERFACE=$(echo $SSH_URL | awk -F  ":" '/1/ {print $1}')
SSH_PORT=$(echo $SSH_URL | awk -F  ":" '/1/ {print $2}')

Embed our SSH private key into the script so the script can be used as-is (distributing SSH keys is never a wise idea, but this one is only used to ease login into the container and should never be used anywhere else).

1
2
3
4
5
6
7
8
9
SSH_KEY_PRIVATE="-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxoeqJlmVd+8MdnESy/cDEjLvsnddGswsHszq9dECD8obrBkG
[...]
HBiLnCEctS6DP0K+6/WaHsY1UnxIG8L3+MiNZOAvPW6zcRhT9VYM
-----END RSA PRIVATE KEY-----"


# write ssh key to temp. file
SSH_KEY_FILE_PRIVATE=$(tempfile)
echo "${SSH_KEY_PRIVATE}" > ${SSH_KEY_FILE_PRIVATE}

Now log in via SSH, using X11 forwarding and the private key, whose public part we stored in the image earlier.

1
ssh -i ${SSH_KEY_FILE_PRIVATE} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -Y -X root@${SSH_INTERFACE} -p ${SSH_PORT} eclipse/eclipse -data workspace


Easy metrics for your web application

Almost always during the development of a web application you will come to the point (preferably after the first release that has gone live) where the need arises to measure and monitor the performance of your shiny new application because it doesn’t do what you wanted it to do.
A very easy and quick way to introduce measurable metrics into your application is metrics. Metrics provides a central registry that can be used to track several types of measurable values (gauges, counters, meters…) and also provides several ways to analyse this values.
Despite a nice spring integration being available for metrics, I chose to use it as it is, because in my opinion the very intuitive API looses its simplicity when configured via XML/annotations.

First thing to do is to add the central metrics registry as a new spring bean so you can use it inside your own beans:

1
<bean id="metricRegistry" class="com.codahale.metrics.MetricRegistry"/>


Metrics provides a simple servlet, that provides access to you values in a restful way. But because the registry is managed in a spring context and the metrics servlet obviously doesn’t know anything about this spring context you have to use some glue code that thankfully is already provided by spring:

So in your web xml, add the metrics servlet:

1
2
3
4
5
6
7
8
9
10
<servlet>
    <servlet-name>metrics</servlet-name>
    <servlet-class>com.codahale.metrics.servlets.MetricsServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>metrics</servlet-name>
    <url-pattern>/metrics/*</url-pattern>
</servlet-mapping>


Then in your application context wire the already defined registry to the servlet:

1
2
3
4
5
6
7
8
9
<bean class="org.springframework.web.context.support.ServletContextAttributeExporter">
    <property name="attributes">
        <map>
            <entry key="com.codahale.metrics.servlets.MetricsServlet.registry">
                <ref bean="metricRegistry"/>
            </entry>
        </map>
    </property>
</bean>


This step is important, because otherwise the servlet will explode with:

1
javax.servlet.ServletException: Couldn't find a MetricRegistry instance.


After these initial steps you are good to go to add your first metric. This example is taken from a DAO and measures the time needed for the create method to persist a newly created entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class BaseEntityDAO extends BaseDAO implements IBaseEntityDAO {

    private Timer createTimer;

    public <T extends IBaseEntity> T create(T entity) {

        final Timer.Context context = createTimer.time();

        try {
                    // [...]
        } finally {
            context.stop();
        }
    }

    @Autowired
    public void setMetricRegistry(MetricRegistry metricRegistry) {
        createTimer = metricRegistry.timer(name(BaseEntityDAO.class, "create"));
    }
}


When you point point your browser to your deployed application http://…./metrics you are happily greeted with the following rest snippet, containing our newly created timer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
    "version": "3.0.0",
    "gauges": { },
    "counters": { },
    "histograms": { },
    "meters": { },
    "timers": {
        "io.pelle.mango.db.dao.BaseEntityDAO.create": {
            "count": 0,
            "max": 0,
            "mean": 0,
            "min": 0,
            "p50": 0,
            "p75": 0,
            "p95": 0,
            "p98": 0,
            "p99": 0,
            "p999": 0,
            "stddev": 0,
            "m15_rate": 0,
            "m1_rate": 0,
            "m5_rate": 0,
            "mean_rate": 0,
            "duration_units": "seconds",
            "rate_units": "calls/second"
        }
    }
}


The numbers are computed after each run, so after a few minutes you will see something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{

    "version": "3.0.0",
    "gauges": { },
    "counters": { },
    "histograms": { },
    "meters": { },
    "timers": {
        "io.pelle.mango.db.dao.BaseEntityDAO.create": {
            "count": 12,
            "max": 0.07745373600000001,
            "mean": 0.013087282166666667,
            "min": 0.003739955,
            "p50": 0.005874474,
            "p75": 0.0095503635,
            "p95": 0.07745373600000001,
            "p98": 0.07745373600000001,
            "p99": 0.07745373600000001,
            "p999": 0.07745373600000001,
            "stddev": 0.020545095705159343,
            "m15_rate": 0.009253575484539516,
            "m1_rate": 0.051815018154721594,
            "m5_rate": 0.02384354183090678,
            "mean_rate": 0.015714856921605576,
            "duration_units": "seconds",
            "rate_units": "calls/second"
        }
    }
}


Obviously you don’t always want to read and graph the rest values by yourself, so metrics comes with a set of predefined reporting facilities that can easily be integrated with other external tools, to analyse the metrics via JMX just add the JmxReporter to your application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ApplicationContext implements InitializingBean {

    @Autowired
    private MetricRegistry metricRegistry;

    @Override
    public void afterPropertiesSet() throws Exception {
        final JmxReporter reporter = JmxReporter.forRegistry(metricRegistry).build();
        reporter.start();
    }

    public void setMetricRegistry(MetricRegistry metricRegistry) {
        this.metricRegistry = metricRegistry;
    }

}


After that fire up your VisualVM and install the MBeans plugin (via Tools -> Plugins) and you see your metrics nicely formatted (double-click values to add graph)
metrics_jmx

To learn more about the different value types, see the very good documentation at the metrics web site


Testing Asynchronous GWT applications

While moving the client code from my old DSL project into the new one I came across my solution for testing the asynchronous client code that my be useful in other usecases. So following an example how the code generally works and and little graphic to illustrate its inner structure:

For example in Mango, the test for opening an editor and testing its title would roughly look like this:

1
2
3
4
5
6
7
8
@Test
public void testEntity1CreateAndSearch() {

    EditorAsyncHelper editor = EditorAsyncHelper.openEditor(DictionaryModel.DICTIONARY1);
    editor.assertTitle("New Editor");

    runAsyncTests();
}

The basic idea is, that all calls are recorded as a list of asynchronous callbacks that get fed with the result of the previous asynchronous call. All callbacks then are replayed (in this case executed) in the stored order.

  • The openEditor call creates a new AsyncTestItem (1) and directly returns an EditorAsyncHelper (2)
  • The AsyncTestItem then calls the actual openEditor method (3), waits for the callback and stores the result in a map with the id #7A8DAC (4)
  • By calling assertTitle(“New Editor”) another callback is created that is fed with the result stored under the id of the previous callback #7A8DAC. Because the previous call was an openEditor call the result is an editor that can be used to assert the title is correct.
  • runAsyncTests() starts the ordered execution of the callbacks. When the last callback is reached the GWT’s finishTest() is called to end the test successfully.

mango_async_client_test

Below the openEditor snippet from DictionaryEditorModuleTestUIAsyncHelper, where a new BaseAsyncTestItem is added and stored in the results list after the callback completes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public DictionaryEditorModuleTestUIAsyncHelper<VOType> openEditor(final BaseModel baseModel, final long id) {
    final String uuid = UUID.uuid();

    this.asyncTestItems.add(new BaseAsyncTestItem() {
        @Override
        public void run(final AsyncCallback<Object> asyncCallback) {

            AsyncCallback<IModuleUI> moduleCallback = new BaseErrorAsyncCallback<IModuleUI>() {

                @Override
                public void onSuccess(IModuleUI result) {
                    asyncTestItemResults.put(uuid, result);
                    asyncCallback.onSuccess(result);
                }
            };

            ModuleHandler.getInstance().startUIModule(DictionaryEditorModule.getModuleUrlForDictionary(baseModel.getName(), id), null, new HashMap<String, Object>(), Optional.of(moduleCallback));
        }

        @Override
        public String getDescription() {
            return "openEditor(" + baseModel.getName() + ")";
        }
    });

    return new DictionaryEditorModuleTestUIAsyncHelper<VOType>(uuid, this.asyncTestItems, this.asyncTestItemResults);
}

The actual processing of the callbacks is done in RecursiveAsyncCallback (see below). After all callbaks are done, a final callback is called, that finally runs GWT’s finishTest method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RecursiveAsyncCallback implements AsyncCallback<Object> {

    private final LinkedList<AsyncTestItem> asyncTestItems;

    private final AsyncCallback finalCallback;

    public RecursiveAsyncCallback(LinkedList<AsyncTestItem> asyncTestItems, AsyncCallback<?> finalCallback) {
        super();
        this.asyncTestItems = asyncTestItems;
        this.finalCallback = finalCallback;
    }

    @Override
    public void onFailure(Throwable caught) {
        throw new RuntimeException(caught);
    }

    @Override
    public void onSuccess(Object result) {

        if (this.asyncTestItems.isEmpty()) {
            this.finalCallback.onSuccess(result);
        } else {
            AsyncTestItem asyncTestItem = this.asyncTestItems.removeFirst();
            GWT.log("running async test item '" + asyncTestItem.getDescription() + "'");
            asyncTestItem.run(new RecursiveAsyncCallback(this.asyncTestItems, this.finalCallback));
        }

    }
}

Mango uses this approach internaly for its client JUnit tests but a major drawback is, that stacktraces are hard to read beacause of the asychronous calls. To ease the this, each test item has to provide a description that will be contained in the callstack so you can easily see what went wrong.

See DemoClientDictionary1Test for a fully working example.


Web development proxy

While developing a web based application I once again hit the same-origin policy wall. Many solutions around this problem use some kind of proxy that serves the static HTML/JavaScript files and forwards all Ajax calls to another server. The problem with many of these solutions is you need a varying amount of more or less complicated steps to set them up, so this time I took some time to create an (hopefully) easy to use solution.
To illustrate the solution (and because I like ASCII art) see below for a overview diagram of what the debug-proxy does:

 _______________________________
|           browser             |
|-------------------------------|
|   localhost:9090/index.html   |
|-------------------------------|
|                               |
|                               |
|                               |
|_______________________________|
       |                  |
       |                  |
       V                  V
 ____________     _________________
|            |   |                 |
| index.html |   | rest/getorder/1 |
|____________|   |_________________|
       |                  |
       |                  |
       V                  V
  /-----------------------------------------/
 / requested files exists in document root /
/-----------------------------------------/
       |                  |
       |                  |
       V                  V
    _______            _______
   |       |          |       |
   |  yes  |          |  no   |
   |_______|          |_______|
       |                  |
       |                  |
       V                  V
 _______________     _______________________________________________
| document root |   |  forward url (http://myserver:8080/services   |
|---------------|   |-----------------------------------------------|
| |- index.html |   |                                               |
| |- about.html |   | http://myserver:8080/services/rest/getorder/1 |
| |- styles.css |   |_______________________________________________|
| |- ...        |
|_______________|
 

Of course document root, port, etc., can be changed via command line parameters:

Parameter Name Description Default
-documentRoot Directory containing the static files The current directory
-port Port to listen on. Note that the debug proxy will always listen on localhost. 9090
-forwardUrl Url, where all request that don’t match files from document root will be forwarded to. http://Localhost:8080

Installation

  • Download debug-proxy
    wget http://zoidberg.pelle.io/jenkins/job/debug-proxy/lastSuccessfulBuild/artifact/debug-proxy.zip
  • Unzip distribution
    unzip debug-proxy.zip
  • Start debug-proxy
    ./debug-proxy/bin/debug-proxy
    ================================================================================
    |                          debug proxy configuration                           |
    --------------------------------------------------------------------------------
    | port:                    9090                                                |
    | document root:           /home/pelle/git/debug-proxy/.                       |
    | forward url:             http://localhost:8080                               |
    --------------------------------------------------------------------------------
    | debug proxy started at http://localhost:9090                                 |
    ================================================================================
    

MyAdmin…the good the bad and the ugly

One of the best things when developing open source software, in my opinion, is feedback. Positive feedback is good and always welcome, but the really interesting feedback is about the things that did not work/were bad/complete nonsense, for these are the things you can learn from. During the last months, a few people looked at my library/framework/call-it-what-you-want and pointed out a few shortcomings that I already knew as well as some new ones. To sum things up for myself, I had a little Lessons Learned session and like to share the results:

The good

The posititve feedback I got was mostly about the short time from “New Project wizard” to a running usable web application. Especially the ability to create a model for the UI to later work in the browser with real data from a DB without the need for even one line of Java code received much praise (I must admit that the whole process broke in many environments due to the fact that it wasn’t very well tested, but when it worked it was fast ;-)).
Some people were excited by how many of the always needed and annoying boilerplate code was automatically generated, but this credit goes more to model-driven development in general.

The bad

Ant

I already noticed during development that for a framework with as many different usages and environments, using Ant as build solution was not as flexible as needed. Despite the good IDE integration for the IVY based dependency management, Gradle is now definitely the better choice (which also comes with the benefit of an already integrated dependency management, as well as a full blown language that makes it easier to cover all the corner cases that were very hard to implement in Ant). Also Gradle’s extensibility with plugins as well as the possibility to manipulate tasks of an already existing build from within, your own build script makes it a top choice for frameworks that have to deliver a customizable build solution.

Code generation roundtrip

Of course if you develop a DSL in an eclipse environment, Xtext is the obvious choice. When I started with my first Xtext experiments, I wrote a bunch of XPand templates for code generation which now lead to a small but very annoying development roundtrip. Once you are finished working with your model, you have to manually start the task that generates all the artefacts. On fast systems the generation was (even for larger models) a matter of seconds but it would be nicer to have “live” generated artefacts, as it is the case when you use an Xtend based generator. Also the integration of Java types was a bit clumsy and the usage of XBase would have made it much easier. Also Xtend feels more powerful (Java-like) and easier to integrate with Java.

DB queries

To work with the generated Entities/Value Objects a querydsl like approach would have been much easier to use and understand. The current “Filter Object” is quite unintuitive to work with. Also, thanks to the model based approach, all information to build a typesafe query language is already present (querydsl itself first needs to generate a bunch of classes, holding all type information for the used entities).

Unclear architecture

The distinction between entities and value objects was not strict enough and it should be easier to use either entities or value objects depending on your current needs. Also many people did not like the XML based configuration of the application context and would have preferred a programmatic approach via the @Configuration annotation.

The ugly

As always when developing software, more tests would have been very useful. Most problems came from the very different environments people used to test the software, especially the new project wizard had a lot of bugs, but sadly the automated UI tests for the Eclipse extensions would have required me to give up sleeping =).
Last but not least, there were a lot of subtle bugs and performance issues in the persistence layer. In many places I had to choose a working but ugly/slow solution due to time constraints.

Because the good parts are already used by some people and some of the bad parts can be fixed by refactoring, I began to transfer and refactor the code into a new project which will hopefully address most of the urgent issues. The code is available at https://github.com/pellepelster/mango. I will blog about the more interesting findings here, especially the Ant -> Gradle migration as well as the migration of the XPand templates.



MyAdmin Tutorial Part 7: Importing/Exporting data with XML

For every entity in the model a XML schema is generated, that can be used to export and import value objects. For example for the entity City the following four XML elements are generated:

CityList
The root element for every XML file containing City describes list of City elements.

1
2
3
4
5
6
7
<xsd:element name="CityList">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:element name="City" type="CityType" minOccurs="0" maxOccurs="unbounded" />
    </xsd:sequence>
  </xsd:complexType>       
</xsd:element>

CityType
The CityType is the entity itself with all its attributes.

1
2
3
4
5
6
7
8
<xsd:complexType name="CityType">
    <xsd:sequence>
        <xsd:element name="id" type="xsd:integer"/>
        <xsd:element name="cityName" type="xsd:string" />
        <xsd:element name="cityDescription" type="xsd:string" />
        <xsd:element name="country" type="country:CountryReferenceType" />
    </xsd:sequence>
</xsd:complexType>

CityReferenceType
To reference City entities from other files the CityReferenceType is generated. Entities can always be referenced via its id, but because the id is a very technical field (that may change) it is always a good idea to give each entity a natural key which will be included in the generated reference type so it can be used to reference that particular entity.

1
2
3
4
5
6
<xsd:complexType name="CityReferenceType">
    <xsd:sequence>
        <xsd:element name="id" type="xsd:integer"/>
        <xsd:element name="cityName" type="xsd:string" />
    </xsd:sequence>
</xsd:complexType>

CityReferenceList
The reference list type can be used when a list of an entity should be referenced from another one.
It is basically a sequence of reference types.

1
2
3
4
5
<xsd:complexType name="CityReferenceList">
    <xsd:sequence>
        <xsd:element name="CityReference" type="CityReferenceType" minOccurs="0" maxOccurs="unbounded" />
    </xsd:sequence>
</xsd:complexType>

To see it all in action, below an example instance of an XML file to import a country value object:

1
2
3
4
5
6
7
<?xml version="1.0" ?>
<CountryList>
    <Country>
        <id>1</id>
        <countryName>Germany</countryName>
    </Country>
</CountryList>

and an example file to import a city, referencing the previously imported country by its natural key:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" ?>
<CityList>
    <City>
        <id>1</id>
        <cityName>Hamburg</cityName>
        <country>
        <CountryReference>
            <countryName>Germany</countryName>
        </CountryReference>
        </country>
    </City>
</CityList>

The testdata folder in the generated example *.server.test project contains two files, containing a country and a currency to import. By running the testdata.import target from the build.xml. The importer looks at the first bytes of the contained *.xml files and looks for matching elements from the generated schemas. When a matching schema is found the file is imported. You don’t have to worry about the order of the files, because the import order is calculated from the relations of the value objects that are about to be imported.



MyAdmin Tutorial Part 3.2: The Model (UI)

Before we dig into the model part responsible for the UI let’s have a look at the default layout of a MyAdmin application.
The CRUD operations are provided through a Search that provides facilities to filter for value objects and display the result.

navigation_search

The create/update part happens in an Editor that can be opened from a result list or from the navigation.

navigation_editor

See below for the Currency UI definition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
dictionary Currency {

    entity entities.Currency
   
    dictionarycontrols {
        textcontrol CurrencyName {
            entityattribute entities.Currency.currencyName
        }

        textcontrol CountryIsoCode {
            entityattribute entities.Currency.currencyIsoCode
        }
    }
   
    dictionarysearch CurrencySearch {
        dictionaryfilter CurrencyFilter {
            textcontrol ref CurrencyName   
        }
       
        dictionaryresult CurrencyResult {
            textcontrol ref CurrencyName
            textcontrol ref CountryIsoCode
        }
    }
   
    dictionaryeditor CountryEditor {
        textcontrol ref CurrencyName
        textcontrol ref CountryIsoCode
    }
}

The structure should be pretty self explaining for this little example. The entity entities.Currency links the UI to its entity. The controls defined in dictionarycontrols are linked to the attribute of the value object given by entity and are referenced from the filter/result and editor to avoid multiple declarations of the same control.

The control definitions support a basic inheritance to reduce verbosity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dictionary Currency {
   
    ...

    dictionarycontrols {

        textcontrol BaseName {
            mandatory
        }
       
        textcontrol CurrencyName ref BaseName {
            entityattribute entities.Currency.currencyName
        }

    }
   
   ...
   
    dictionaryeditor CountryEditor {
        textcontrol ref CurrencyName {
            label "New Label"
        }
    }
}

MyAdmin Tutorial Part 3.1: The Model (Entities)

The first thing to start with is an entity definition, se below for a very basic (and mostly self speaking) example:

1
2
3
4
5
6
7
8
9
10
textdatatype CurrencyName { }

textdatatype CurrencyIsoCode {
    maxLength 3
}

entity Currency {
    datatype CurrencyName currencyName
    datatype CurrencyIsoCode currencyIsoCode
}

The above code is a pretty minimalistic definition for an entity with two text attributes, one being the name of the currency and the other one the ISO code, which is limited to a maximum length of 3 by maxLength 3. From this definition two classes will be generated for once the JPA entity class containing all annotations needed to work with hibernate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.example.project1.server.entities;

@javax.persistence.Entity
@javax.persistence.Table(name = "currency")
public class Currency extends de.pellepelster.myadmin.db.BaseEntity
        implements
            de.pellepelster.myadmin.db.IBaseEntity {

    @javax.persistence.Id
    @javax.persistence.Column(name = "currency_id")
    @javax.persistence.GeneratedValue(strategy = javax.persistence.GenerationType.TABLE, generator = "currency_id_seq")
    @javax.persistence.SequenceGenerator(name = "currency_id_seq", sequenceName = "currency_id_seq", allocationSize = 1)
    private long id;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @javax.persistence.Column(name = "currency_currencyname")
    private java.lang.String currencyName;

    public java.lang.String getCurrencyName() {
        return currencyName;
    }

    public void setCurrencyName(java.lang.String currencyName) {
        this.currencyName = currencyName;
    }

    @javax.persistence.Column(name = "currency_currencyisocode")
    private java.lang.String currencyIsoCode;

    public java.lang.String getCurrencyIsoCode() {
        return currencyIsoCode;
    }

    public void setCurrencyIsoCode(java.lang.String currencyIsoCode) {
        this.currencyIsoCode = currencyIsoCode;
    }

}

org.example.project1.server.entities.Currency

And a second mostly identical class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package org.example.project1.client.web.entities;

public class CurrencyVO
        extends
            de.pellepelster.myadmin.client.base.db.vos.BaseVO
        implements
            de.pellepelster.myadmin.client.base.db.vos.IBaseVO {

    private static final long serialVersionUID = 1L;

    private long id = de.pellepelster.myadmin.client.base.db.vos.IBaseVO.NEW_VO_ID;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public static final de.pellepelster.myadmin.client.base.db.vos.IAttributeDescriptor<?> FIELD_CURRENCYNAME = new de.pellepelster.myadmin.client.base.db.vos.AttributeDescriptor(
            "currencyName", java.lang.String.class, java.lang.String.class);

    public static final de.pellepelster.myadmin.client.base.db.vos.IAttributeDescriptor<?> FIELD_CURRENCYISOCODE = new de.pellepelster.myadmin.client.base.db.vos.AttributeDescriptor(
            "currencyIsoCode", java.lang.String.class, java.lang.String.class);

    public static de.pellepelster.myadmin.client.base.db.vos.IAttributeDescriptor[] getFieldDescriptors() {
        return new de.pellepelster.myadmin.client.base.db.vos.IAttributeDescriptor[]{

        FIELD_CURRENCYNAME, FIELD_CURRENCYISOCODE

        };
    }

    public Object get(java.lang.String name) {

        if ("oid".equals(name)) {
            return getOid();
        }

        if ("id".equals(name)) {
            return getId();
        }

        if ("currencyName".equals(name)) {
            return this.currencyName;
        }

        if ("currencyIsoCode".equals(name)) {
            return this.currencyIsoCode;
        }

        throw new java.lang.RuntimeException("no such attribute '" + name + "'");

    }

    public void set(java.lang.String name, java.lang.Object value) {

        getChangeTracker().addChange(name, value);

        if ("currencyName".equals(name)) {

            setCurrencyName((java.lang.String) value);

            return;
        }

        if ("currencyIsoCode".equals(name)) {

            setCurrencyIsoCode((java.lang.String) value);

            return;
        }

        throw new java.lang.RuntimeException("no such attribute '" + name + "'");

    }

    public java.lang.String getCurrencyName() {
        return currencyName;
    }

    public void setCurrencyName(java.lang.String currencyName) {
        getChangeTracker().addChange("currencyName", currencyName);

        this.currencyName = currencyName;

    }

    private java.lang.String currencyIsoCode;

    public java.lang.String getCurrencyIsoCode() {
        return currencyIsoCode;
    }

    public void setCurrencyIsoCode(java.lang.String currencyIsoCode) {
        getChangeTracker().addChange("currencyIsoCode", currencyIsoCode);

        this.currencyIsoCode = currencyIsoCode;

    }

    public CurrencyVO cloneVO() {

        CurrencyVO newVO = new CurrencyVO();

        newVO.currencyName = this.currencyName;

        newVO.currencyIsoCode = this.currencyIsoCode;

        return newVO;
    }

    public de.pellepelster.myadmin.client.base.db.vos.IAttributeDescriptor<?> getAttributeDescriptor(
            String name) {

        for (de.pellepelster.myadmin.client.base.db.vos.IAttributeDescriptor<?> attributeDescriptor : getFieldDescriptors()) {
            if (attributeDescriptor.getAttributeName().equals(name)) {
                return attributeDescriptor;
            }
        }

        throw new RuntimeException("unsupported attribute '" + name + "'");
    }

}

org.example.project1.client.web.entities.CurrencyVO.

The JPA entities are only used in the layer directly above hibernate. MyAdmin then copies all entities to their corresponding value objects so you will almost never work with the JPA entities. Note the generic getters and setters that are needed to circumvent the lack of reflection on the client side (GWT).
To create/read/… value objects use the IBaseEntityService on the server side, respectively IBaseEntityServiceGWTAsync on the client side. For the client a service locator is generated, that can be used to access the service.

1
MyAdmin.getInstance().getRemoteServiceLocator().getBaseEntityService()..

on the server side you can either let spring inject the service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseEntityServiceServerExample {

    @Autowired
    private IBaseEntityService baseEntityService;

    public void doSomeThing() {
         baseEntityService....
    }

    public void setBaseEntityService(IBaseEntityService baseEntityService) {
        this.baseEntityService = baseEntityService;
    }

}

or use the generated service locator for the server side:

1
MyAdminServiceLocator.getBaseEntityService()..

A simple example using the IBaseEntityService to search for a CurrencyVO with a specific name shows the usage of the generated descriptors for the VO attributes

1
2
3
4
5
6
7
8
9
public List<CurrencyVO> getCurrencyByName(String name) {

    GenericFilterVO<CurrencyVO> filter = ServerGenericFilterBuilder
            .createGenericFilter(CurrencyVO.class)
            .addCriteria(CurrencyVO.FIELD_CURRENCYNAME, name)
            .getGenericFilter();

    return baseEntityService.filter(filter);
}

For more examples see BaseEntityServiceServerExample from the MyAdmin examples project.