Move REST lookup support to Scout
diff --git a/eclipse-scout-core/src/index.js b/eclipse-scout-core/src/index.js
index 891e156..1a3c1d3 100644
--- a/eclipse-scout-core/src/index.js
+++ b/eclipse-scout-core/src/index.js
@@ -62,6 +62,7 @@
 export {default as AjaxError} from './ajax/AjaxError';
 export {default as LookupCall} from './lookup/LookupCall';
 export {default as LookupRow} from './lookup/LookupRow';
+export {default as RestLookupCall} from './lookup/RestLookupCall';
 export {default as RemoteLookupCall} from './lookup/RemoteLookupCall';
 export {default as RemoteLookupRequest} from './lookup/RemoteLookupRequest';
 export {default as QueryBy} from './lookup/QueryBy';
diff --git a/eclipse-scout-core/src/lookup/RestLookupCall.js b/eclipse-scout-core/src/lookup/RestLookupCall.js
new file mode 100644
index 0000000..b176426
--- /dev/null
+++ b/eclipse-scout-core/src/lookup/RestLookupCall.js
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+import {arrays, LookupCall, scout} from '../index';
+import $ from 'jquery';
+
+export default class RestLookupCall extends LookupCall {
+
+  constructor() {
+    super();
+    this.resourceUrl = null;
+    this.maxTextLength = null;
+
+    // for predefined restrictions only (e.g. in JSON or sub-classes), don't change this attribute! this instance is shared with all clones!
+    this.restriction = null;
+
+    // dynamically added restrictions. after setting this attribute, this instance is shared with all following clones!
+    this._restriction = null;
+
+    this._ajaxCall = null;
+    this._deferred = null;
+
+    // RestLookupCall implements getByKeys
+    this.batch = true;
+  }
+
+  /**
+   * Use this function with caution! Added restrictions will be shared among cloned instances
+   * and the current instance if this function was also called before cloning!
+   */
+  addRestriction(key, value) {
+    if (!this._restriction) {
+      this._restriction = {};
+    }
+    this._restriction[key] = value;
+  }
+
+  _getAll() {
+    return this._call();
+  }
+
+  _getByText(text) {
+    this.addRestriction('text', text);
+    return this._call();
+  }
+
+  _getByKey(key) {
+    this.addRestriction('ids', arrays.ensure(key));
+    return this._call();
+  }
+
+  _getByKeys(keys) {
+    this.addRestriction('ids', arrays.ensure(keys));
+    return this._call();
+  }
+
+  cloneForAll() {
+    let clone = super.cloneForAll();
+    clone.addRestriction('active', true);
+    return clone;
+  }
+
+  cloneForText(text) {
+    let clone = super.cloneForText(text);
+    clone.addRestriction('active', true);
+    return clone;
+  }
+
+  _acceptLookupRow(lookupRowDo) {
+    return true;
+  }
+
+  _createLookupRowFromDo(lookupRowDo) {
+    // propagate all properties from lookup row do to scout lookup row (there might be custom ones on specific lookup row dos)
+    let clonedLookupRowDo = $.extend({}, lookupRowDo);
+    // text, enabled and active are the same for Scout LookupRow and Studio LookupRowDo
+
+    // id -> key
+    clonedLookupRowDo.key = clonedLookupRowDo.id;
+    delete clonedLookupRowDo.id;
+
+    // parentId -> parentKey
+    clonedLookupRowDo.parentKey = clonedLookupRowDo.parentId;
+    delete clonedLookupRowDo.parentId;
+
+    // unused on Scout LookupRow
+    delete clonedLookupRowDo._type;
+
+    if (this.maxTextLength) {
+      let text = clonedLookupRowDo.text;
+      if (text.length > this.maxTextLength) {
+        clonedLookupRowDo.text = text.substr(0, this.maxTextLength) + '...';
+        clonedLookupRowDo.tooltipText = text;
+      }
+    }
+
+    return scout.create('LookupRow', clonedLookupRowDo);
+  }
+
+  _call() {
+    this._deferred = $.Deferred();
+    this._ajaxCall = this._createAjaxCall();
+
+    this._ajaxCall.call()
+      .then((data, textStatus, jqXHR) => {
+        let lookupRows = arrays.ensure(data ? data.rows : null)
+          .filter(this._acceptLookupRow.bind(this))
+          .map(this._createLookupRowFromDo.bind(this));
+        this._deferred.resolve({
+          queryBy: this.queryBy,
+          text: this.searchText,
+          key: this.key,
+          lookupRows: lookupRows
+        });
+      })
+      .catch(ajaxError => {
+        this._deferred.resolve({
+          queryBy: this.queryBy,
+          text: this.searchText,
+          key: this.key,
+          lookupRows: [],
+          exception: this.session.text('ErrorWhileLoadingData')
+        });
+      });
+
+    return this._deferred.promise();
+  }
+
+  abort() {
+    this._deferred.reject({
+      canceled: true
+    });
+    this._ajaxCall.abort();
+    super.abort();
+  }
+
+  _createAjaxCall() {
+    let url = this.resourceUrl;
+    let data = this.restriction || this._restriction ? JSON.stringify($.extend({}, this.restriction, this._restriction)) : null;
+    let ajaxOptions = {
+      type: 'POST',
+      data: data,
+      dataType: 'json',
+      contentType: 'application/json; charset=UTF-8',
+      cache: false,
+      url: url,
+      timeout: 0
+    };
+    return scout.create('AjaxCall', {
+      ajaxOptions: ajaxOptions,
+      name: 'RestLookupCall',
+      retryIntervals: [100, 500, 500, 500]
+    }, {
+      ensureUniqueId: false
+    });
+  }
+}
diff --git a/license_files/scoutLicenceHeaderJava.xml b/license_files/scoutLicenceHeaderJava.xml
index 7dd3858..c664d7c 100644
--- a/license_files/scoutLicenceHeaderJava.xml
+++ b/license_files/scoutLicenceHeaderJava.xml
@@ -13,7 +13,7 @@
 -->
 <additionalHeaders>
     <scout_javadoc_style>
-        <firstLine>/*******************************************************************************</firstLine>
+        <firstLine>/*</firstLine>
         <beforeEachLine> * </beforeEachLine>
         <endLine> */</endLine>
 <!--         <afterEachLine></afterEachLine> -->
diff --git a/org.eclipse.scout.rt.client.test/org.eclipse.scout.rt.client.test.iml b/org.eclipse.scout.rt.client.test/org.eclipse.scout.rt.client.test.iml
index 1388edf..10f2155 100644
--- a/org.eclipse.scout.rt.client.test/org.eclipse.scout.rt.client.test.iml
+++ b/org.eclipse.scout.rt.client.test/org.eclipse.scout.rt.client.test.iml
@@ -19,7 +19,6 @@
     <orderEntry type="library" name="Maven: org.quartz-scheduler:quartz:2.3.2" level="project" />
     <orderEntry type="module" module-name="org.eclipse.scout.rt.shared" />
     <orderEntry type="module" module-name="org.eclipse.scout.rt.security" />
-    <orderEntry type="module" module-name="org.eclipse.scout.rt.dataobject" />
     <orderEntry type="library" name="Maven: com.google.http-client:google-http-client:1.34.2" level="project" />
     <orderEntry type="library" name="Maven: org.apache.httpcomponents:httpcore:4.4.13" level="project" />
     <orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:3.0.2" level="project" />
@@ -37,13 +36,20 @@
     <orderEntry type="library" name="Maven: org.slf4j:jcl-over-slf4j:1.7.25" level="project" />
     <orderEntry type="module" module-name="org.eclipse.scout.rt.shared.test" />
     <orderEntry type="module" module-name="org.eclipse.scout.rt.platform.test" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-math3:3.6.1" level="project" />
+    <orderEntry type="module" module-name="org.eclipse.scout.rt.dataobject.test" scope="TEST" />
+    <orderEntry type="module" module-name="org.eclipse.scout.rt.dataobject" />
     <orderEntry type="library" name="Maven: junit:junit:4.12" level="project" />
     <orderEntry type="library" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
     <orderEntry type="library" name="Maven: org.mockito:mockito-core:2.23.4" level="project" />
     <orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy:1.9.3" level="project" />
     <orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy-agent:1.9.3" level="project" />
     <orderEntry type="library" name="Maven: org.objenesis:objenesis:2.6" level="project" />
-    <orderEntry type="library" name="Maven: org.apache.commons:commons-math3:3.6.1" level="project" />
+    <orderEntry type="module" module-name="org.eclipse.scout.rt.jackson.test" scope="TEST" />
+    <orderEntry type="module" module-name="org.eclipse.scout.rt.jackson" scope="TEST" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.9.10.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.9.10" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.fasterxml.jackson.core:jackson-core:2.9.10" level="project" />
     <orderEntry type="library" scope="TEST" name="Maven: ch.qos.logback:logback-classic:1.2.3" level="project" />
     <orderEntry type="library" scope="TEST" name="Maven: ch.qos.logback:logback-core:1.2.3" level="project" />
     <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.25" level="project" />
diff --git a/org.eclipse.scout.rt.client.test/pom.xml b/org.eclipse.scout.rt.client.test/pom.xml
index 46df4b1..8055eb1 100644
--- a/org.eclipse.scout.rt.client.test/pom.xml
+++ b/org.eclipse.scout.rt.client.test/pom.xml
@@ -43,6 +43,16 @@
     </dependency>
 
     <dependency>
+      <groupId>org.eclipse.scout.rt</groupId>
+      <artifactId>org.eclipse.scout.rt.dataobject.test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.scout.rt</groupId>
+      <artifactId>org.eclipse.scout.rt.jackson.test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
       <scope>test</scope>
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/AbstractRestLookupCallTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/AbstractRestLookupCallTest.java
new file mode 100644
index 0000000..7dde902
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/AbstractRestLookupCallTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.client.services.lookup;
+
+import static org.junit.Assert.*;
+import java.util.function.Function;
+
+import org.eclipse.scout.rt.dataobject.fixture.FixtureUuId;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRowDo;
+import org.eclipse.scout.rt.dataobject.lookup.LookupResponse;
+import org.eclipse.scout.rt.platform.IgnoreBean;
+import org.eclipse.scout.rt.platform.util.TriState;
+import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(PlatformTestRunner.class)
+public class AbstractRestLookupCallTest {
+
+  public static final FixtureUuId TEST_SETTING_ID_1 = FixtureUuId.of("06469416-545c-449b-8440-4acffcf69063");
+  public static final FixtureUuId TEST_SETTING_ID_2 = FixtureUuId.of("0768a234-bbaf-47fa-a174-f25a92855f20");
+
+  @Test
+  public void testClone() {
+    P_FixtureUuIdLookupCall call = new P_FixtureUuIdLookupCall();
+    call.setText("ABC");
+    P_FixtureUuIdLookupCall call2 = (P_FixtureUuIdLookupCall) call.copy();
+    assertNotSame(call, call2);
+    assertNotSame(call.getRestriction(), call2.getRestriction());
+    Assert.assertEquals("ABC", call2.getText());
+  }
+
+  @Test
+  public void testSetters() {
+    P_FixtureUuIdLookupCall call = new P_FixtureUuIdLookupCall();
+
+    call.setText("ABC");
+    Assert.assertEquals("ABC", call.getText());
+
+    call.setKey(TEST_SETTING_ID_1);
+    Assert.assertEquals(1, call.getKeys().size());
+    Assert.assertEquals(TEST_SETTING_ID_1, call.getKey());
+
+    call.setKey(TEST_SETTING_ID_2);
+    Assert.assertEquals(1, call.getKeys().size());
+    Assert.assertEquals(TEST_SETTING_ID_2, call.getKey());
+
+    call.setKeys(TEST_SETTING_ID_1, TEST_SETTING_ID_2);
+    Assert.assertEquals(2, call.getKeys().size());
+    Assert.assertEquals(TEST_SETTING_ID_1, call.getKey());
+
+    call.setKey(TEST_SETTING_ID_2);
+    Assert.assertEquals(1, call.getKeys().size());
+    Assert.assertEquals(TEST_SETTING_ID_2, call.getKey());
+
+    call.setKey(null);
+    assertNull(call.getKey());
+    Assert.assertEquals(0, call.getKeys().size());
+
+    assertFalse(call.getRestriction().active().exists());
+    call.setActive(null);
+    Assert.assertEquals(TriState.UNDEFINED, call.getActive());
+    assertTrue(call.getRestriction().active().exists());
+    assertNull(call.getRestriction().active().get());
+    call.setActive(TriState.TRUE);
+    Assert.assertEquals(TriState.TRUE, call.getActive());
+    assertTrue(call.getRestriction().active().exists());
+    Assert.assertEquals(Boolean.TRUE, call.getRestriction().active().get());
+    call.setActive(TriState.FALSE);
+    Assert.assertEquals(TriState.FALSE, call.getActive());
+    assertTrue(call.getRestriction().active().exists());
+    Assert.assertEquals(Boolean.FALSE, call.getRestriction().active().get());
+    call.setActive(TriState.UNDEFINED);
+    Assert.assertEquals(TriState.UNDEFINED, call.getActive());
+    assertTrue(call.getRestriction().active().exists());
+    assertNull(call.getRestriction().active().get());
+  }
+
+  @IgnoreBean
+  static class P_FixtureUuIdLookupCall extends AbstractRestLookupCall<FixtureUuIdLookupRestrictionDo, FixtureUuId> {
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected Function<FixtureUuIdLookupRestrictionDo, LookupResponse<? extends AbstractLookupRowDo<?, FixtureUuId>>> remoteCall() {
+      // Not used in this test
+      return null;
+    }
+  }
+}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/FixtureUuIdLookupRestrictionDo.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/FixtureUuIdLookupRestrictionDo.java
new file mode 100644
index 0000000..1683475
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/FixtureUuIdLookupRestrictionDo.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.client.services.lookup;
+
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.dataobject.fixture.FixtureUuId;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRestrictionDo;
+
+@TypeName("scout.FixtureUuIdLookupRestriction")
+public class FixtureUuIdLookupRestrictionDo extends AbstractLookupRestrictionDo<FixtureUuIdLookupRestrictionDo, FixtureUuId> {
+
+  @Override
+  public DoList<FixtureUuId> ids() {
+    return createIdsAttribute(this);
+  }
+}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/FixtureUuIdLookupRowDo.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/FixtureUuIdLookupRowDo.java
new file mode 100644
index 0000000..d8d7f65
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/services/lookup/FixtureUuIdLookupRowDo.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.client.services.lookup;
+
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.dataobject.fixture.FixtureUuId;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRowDo;
+
+@TypeName("scout.FixtureUuIdLookupRow")
+public class FixtureUuIdLookupRowDo extends AbstractLookupRowDo<FixtureUuIdLookupRowDo, FixtureUuId> {
+
+  @Override
+  public DoValue<FixtureUuId> id() {
+    return createIdAttribute(this);
+  }
+}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractRestLookupSmartColumnTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractRestLookupSmartColumnTest.java
new file mode 100644
index 0000000..bf5d203
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractRestLookupSmartColumnTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+
+package org.eclipse.scout.rt.client.ui.basic.table.columns;
+
+import static org.junit.Assert.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.scout.rt.client.testenvironment.TestEnvironmentClientSession;
+import org.eclipse.scout.rt.client.ui.basic.table.AbstractTable;
+import org.eclipse.scout.rt.client.ui.basic.table.ITableRow;
+import org.eclipse.scout.rt.client.ui.form.fields.smartfield.ISmartField;
+import org.eclipse.scout.rt.dataobject.fixture.FixtureUuId;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRowDo;
+import org.eclipse.scout.rt.dataobject.lookup.LookupResponse;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.BeanMetaData;
+import org.eclipse.scout.rt.platform.IBean;
+import org.eclipse.scout.rt.platform.IgnoreBean;
+import org.eclipse.scout.rt.platform.util.StringUtility;
+import org.eclipse.scout.rt.client.services.lookup.AbstractRestLookupCall;
+import org.eclipse.scout.rt.testing.client.runner.ClientTestRunner;
+import org.eclipse.scout.rt.testing.client.runner.RunWithClientSession;
+import org.eclipse.scout.rt.testing.platform.BeanTestingHelper;
+import org.eclipse.scout.rt.testing.platform.runner.RunWithSubject;
+import org.eclipse.scout.rt.client.services.lookup.FixtureUuIdLookupRestrictionDo;
+import org.eclipse.scout.rt.client.services.lookup.FixtureUuIdLookupRowDo;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractRestLookupSmartColumnTest.P_Table.Column;
+
+@RunWith(ClientTestRunner.class)
+@RunWithSubject("default")
+@RunWithClientSession(TestEnvironmentClientSession.class)
+public class AbstractRestLookupSmartColumnTest {
+
+  public static final FixtureUuId TEST_FIXTURE_ID_1 = FixtureUuId.of("bc47c139-994f-4c9f-a1a0-0489fdc10ee7");
+  public static final FixtureUuId TEST_FIXTURE_ID_2 = FixtureUuId.of("779b73b4-6c49-4aa7-b7f5-ad06f6ecfa6b");
+  public static final FixtureUuId TEST_FIXTURE_ID_3 = FixtureUuId.of("032d2759-1164-415f-af19-fabd100b4229");
+  public static final FixtureUuId TEST_FIXTURE_ID_4 = FixtureUuId.of("19dc4190-0e0f-4d6f-bf73-92f7656dde24");
+
+  private static final List<IBean<?>> TEST_BEANS = new ArrayList<>();
+  private static final P_RemoteService REMOTE_SERVICE = new P_RemoteService();
+  private static final Set<P_FixtureUuIdLookupCall> USED_LOOKUP_CALL_INSTANCES = new HashSet<>();
+
+  private P_Table m_table;
+
+  @BeforeClass
+  public static void setUpClass() {
+    TEST_BEANS.add(BeanTestingHelper.get().registerBean(new BeanMetaData(P_FixtureUuIdLookupCall.class)));
+  }
+
+  @AfterClass
+  public static void tearDownClass() {
+    BeanTestingHelper.get().unregisterBeans(TEST_BEANS);
+  }
+
+  @Before
+  public void setUp() {
+    REMOTE_SERVICE.resetCalls();
+    USED_LOOKUP_CALL_INSTANCES.clear();
+    m_table = new P_Table();
+  }
+
+  @Test
+  public void testCallRemoteServiceOnlyOnce() {
+    m_table.addRowsByArray(new FixtureUuId[]{TEST_FIXTURE_ID_3, TEST_FIXTURE_ID_1});
+
+    assertEquals(2, m_table.getRowCount());
+    assertEquals("Test-Fixture #3", m_table.getColumn().getDisplayText(m_table.getRow(0)));
+    assertEquals("Test-Fixture #1", m_table.getColumn().getDisplayText(m_table.getRow(1)));
+    assertEquals(1, REMOTE_SERVICE.getTotalCalls());
+  }
+
+  @Test
+  public void testEditCell() {
+    m_table.addRowsByArray(new FixtureUuId[]{null, null});
+    assertEquals(2, m_table.getRowCount());
+    assertNull(m_table.getColumn().getDisplayText(m_table.getRow(0)));
+    assertNull(m_table.getColumn().getDisplayText(m_table.getRow(1)));
+    assertEquals(0, REMOTE_SERVICE.getTotalCalls());
+    assertEquals(0, USED_LOOKUP_CALL_INSTANCES.size());
+
+    parseAndSetValue(0, "Test-Fixture #4");
+    parseAndSetValue(1, "Test-Fixture #2");
+
+    assertEquals(2, m_table.getRowCount());
+    // The following tests fail when the REST lookup call accumulates keys.
+    // When calling setKey(), the previous key should be cleared.
+    assertEquals("Test-Fixture #4", m_table.getColumn().getDisplayText(m_table.getRow(0)));
+    assertEquals("Test-Fixture #2", m_table.getColumn().getDisplayText(m_table.getRow(1)));
+    assertEquals(6, REMOTE_SERVICE.getTotalCalls());
+    assertEquals(4, REMOTE_SERVICE.getCallsByKey());
+    assertEquals(2, REMOTE_SERVICE.getCallsByText());
+    // The following test fails when the batch lookup did not create a clone of the prototype lookup call.
+    assertEquals(6, USED_LOOKUP_CALL_INSTANCES.size());
+  }
+
+  private void parseAndSetValue(int rowIndex, String text) {
+    ITableRow row = m_table.getRow(rowIndex);
+    Column column = m_table.getColumn();
+
+    ISmartField<?> cellEditor = (ISmartField<?>) column.prepareEdit(row);
+    cellEditor.parseAndSetValue(text);
+    column.completeEdit(row, cellEditor);
+  }
+
+  static class P_Table extends AbstractTable {
+
+    public Column getColumn() {
+      return getColumnSet().getColumnByClass(Column.class);
+    }
+
+    public static class Column extends AbstractRestLookupSmartColumn<FixtureUuId> {
+
+      @Override
+      protected Class<? extends AbstractRestLookupCall<?, FixtureUuId>> getConfiguredLookupCall() {
+        return P_FixtureUuIdLookupCall.class;
+      }
+
+      @Override
+      protected boolean getConfiguredEditable() {
+        return true;
+      }
+    }
+  }
+
+  @IgnoreBean
+  static class P_FixtureUuIdLookupCall extends AbstractRestLookupCall<FixtureUuIdLookupRestrictionDo, FixtureUuId> {
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected Function<FixtureUuIdLookupRestrictionDo, LookupResponse<? extends AbstractLookupRowDo<?, FixtureUuId>>> remoteCall() {
+      USED_LOOKUP_CALL_INSTANCES.add(this); // cloning an object does not necessarily call the constructor
+      return REMOTE_SERVICE::lookupRows;
+    }
+  }
+
+  static class P_RemoteService {
+
+    private int m_callsByKey = 0;
+    private int m_callsByText = 0;
+    private int m_callsByAll = 0;
+    private final List<FixtureUuIdLookupRowDo> m_allRows;
+
+    public P_RemoteService() {
+      m_allRows = Arrays.asList(
+          BEANS.get(FixtureUuIdLookupRowDo.class).withId(TEST_FIXTURE_ID_1).withText("Test-Fixture #1"),
+          BEANS.get(FixtureUuIdLookupRowDo.class).withId(TEST_FIXTURE_ID_2).withText("Test-Fixture #2"),
+          BEANS.get(FixtureUuIdLookupRowDo.class).withId(TEST_FIXTURE_ID_3).withText("Test-Fixture #3"),
+          BEANS.get(FixtureUuIdLookupRowDo.class).withId(TEST_FIXTURE_ID_4).withText("Test-Fixture #4"));
+    }
+
+    public LookupResponse<FixtureUuIdLookupRowDo> lookupRows(FixtureUuIdLookupRestrictionDo restriction) {
+      if (restriction.getIds().size() > 0) {
+        m_callsByKey++;
+        return LookupResponse.create(m_allRows.stream()
+            .filter(row -> restriction.getIds().contains(row.getId()))
+            .collect(Collectors.toList()));
+      }
+      else if (StringUtility.hasText(restriction.getText())) {
+        m_callsByText++;
+        final String filterText = restriction.getText().replaceAll("\\*", "").toLowerCase();
+        return LookupResponse.create(m_allRows.stream()
+            .filter(row -> row.getText().toLowerCase().contains(filterText))
+            .collect(Collectors.toList()));
+      }
+      else {
+        m_callsByAll++;
+        return LookupResponse.create(m_allRows);
+      }
+    }
+
+    public int getTotalCalls() {
+      return m_callsByKey + m_callsByText + m_callsByAll;
+    }
+
+    public int getCallsByKey() {
+      return m_callsByKey;
+    }
+
+    public int getCallsByText() {
+      return m_callsByText;
+    }
+
+    public int getCallsByAll() {
+      return m_callsByAll;
+    }
+
+    public void resetCalls() {
+      m_callsByKey = 0;
+      m_callsByText = 0;
+      m_callsByAll = 0;
+    }
+  }
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/lookup/AbstractRestLookupCall.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/lookup/AbstractRestLookupCall.java
new file mode 100644
index 0000000..d96ae96
--- /dev/null
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/services/lookup/AbstractRestLookupCall.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.client.services.lookup;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.eclipse.scout.rt.dataobject.DataObjectHelper;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRestrictionDo;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRowDo;
+import org.eclipse.scout.rt.dataobject.lookup.LookupResponse;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.context.RunContext;
+import org.eclipse.scout.rt.platform.exception.PlatformException;
+import org.eclipse.scout.rt.platform.job.IFuture;
+import org.eclipse.scout.rt.platform.job.Jobs;
+import org.eclipse.scout.rt.platform.util.CollectionUtility;
+import org.eclipse.scout.rt.platform.util.NumberUtility;
+import org.eclipse.scout.rt.platform.util.TriState;
+import org.eclipse.scout.rt.platform.util.TypeCastUtility;
+import org.eclipse.scout.rt.shared.services.lookup.ILookupCall;
+import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
+import org.eclipse.scout.rt.shared.services.lookup.ILookupRowFetchedCallback;
+import org.eclipse.scout.rt.shared.services.lookup.LookupRow;
+
+/**
+ * IMPORTANT: If you use subclasses of this lookup call in a smart column, make sure to use an
+ * {@code org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractRestLookupSmartColumn}, not
+ * {@code org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractSmartColumn}. Otherwise, batch lookups are
+ * performed via service tunnel instead of calling a REST service.
+ */
+public abstract class AbstractRestLookupCall<RESTRICTION extends AbstractLookupRestrictionDo<?, ID>, ID> implements ILookupCall<ID> {
+  private static final long serialVersionUID = 1L;
+
+  protected RESTRICTION m_restrictionDo;
+
+  protected AbstractRestLookupCall() {
+    m_restrictionDo = BEANS.get(resolveRestrictionClass());
+  }
+
+  /**
+   * Overwrite to resolve to a specific restriction class.
+   * <p>
+   * By default, the super hierarchy is looked for the restriction class type in the generic type declaration.
+   */
+  @SuppressWarnings("unchecked")
+  protected Class<RESTRICTION> resolveRestrictionClass() {
+    return TypeCastUtility.getGenericsParameterClass(getClass(), AbstractRestLookupCall.class, 0);
+  }
+
+  /**
+   * @return the live restriction object (not {@code null})
+   */
+  public RESTRICTION getRestriction() {
+    return m_restrictionDo;
+  }
+
+  @Override
+  public ID getKey() {
+    return CollectionUtility.firstElement(m_restrictionDo.getIds());
+  }
+
+  @Override
+  public void setKey(ID key) {
+    m_restrictionDo.getIds().clear();
+    if (key != null) {
+      m_restrictionDo.getIds().add(key);
+    }
+  }
+
+  public List<ID> getKeys() {
+    return m_restrictionDo.getIds();
+  }
+
+  /**
+   * @deprecated will be removed in 1.3, use {@link #getKeys()} instead
+   */
+  @Deprecated
+  public List<ID> getIds() {
+    return getKeys();
+  }
+
+  public void setKeys(Collection<? extends ID> keys) {
+    m_restrictionDo.withIds(keys);
+  }
+
+  @SafeVarargs
+  public final void setKeys(@SuppressWarnings("unchecked") ID... keys) {
+    m_restrictionDo.withIds(keys);
+  }
+
+  @Override
+  public void setText(String text) {
+    m_restrictionDo.withText(text);
+  }
+
+  @Override
+  public void setAll(String s) {
+    // NOP - unsupported
+  }
+
+  @Override
+  public String getAll() {
+    return null;
+  }
+
+  @Override
+  public void setRec(ID parent) {
+    // NOP - unsupported
+  }
+
+  @Override
+  public ID getRec() {
+    return null;
+  }
+
+  @Override
+  public void setMaster(Object master) {
+    // NOP - unsupported
+  }
+
+  @Override
+  public Object getMaster() {
+    return null;
+  }
+
+  @Override
+  public void setActive(TriState activeState) {
+    m_restrictionDo.withActive(activeState == null ? null : activeState.getBooleanValue());
+  }
+
+  @Override
+  public TriState getActive() {
+    return TriState.parse(m_restrictionDo.getActive());
+  }
+
+  @Override
+  public String getText() {
+    return m_restrictionDo.getText();
+  }
+
+  @Override
+  public List<? extends ILookupRow<ID>> getDataByKey() {
+    if (!m_restrictionDo.ids().exists() || m_restrictionDo.ids().isEmpty()) {
+      return Collections.emptyList();
+    }
+    return getData();
+  }
+
+  @Override
+  public IFuture<Void> getDataByKeyInBackground(RunContext runContext, ILookupRowFetchedCallback<ID> callback) {
+    return loadDataInBackground(this::getDataByKey, runContext, callback);
+  }
+
+  @Override
+  public List<? extends ILookupRow<ID>> getDataByText() {
+    return getData();
+  }
+
+  @Override
+  public IFuture<Void> getDataByTextInBackground(RunContext runContext, ILookupRowFetchedCallback<ID> callback) {
+    return loadDataInBackground(this::getDataByText, runContext, callback);
+  }
+
+  @Override
+  public List<? extends ILookupRow<ID>> getDataByAll() {
+    return getData();
+  }
+
+  @Override
+  public IFuture<Void> getDataByAllInBackground(RunContext runContext, ILookupRowFetchedCallback<ID> callback) {
+    return loadDataInBackground(this::getDataByAll, runContext, callback);
+  }
+
+  @Override
+  public List<? extends ILookupRow<ID>> getDataByRec() {
+    throw new UnsupportedOperationException("not implemented");
+  }
+
+  @Override
+  public IFuture<Void> getDataByRecInBackground(RunContext runContext, ILookupRowFetchedCallback<ID> callback) {
+    throw new UnsupportedOperationException("not implemented");
+  }
+
+  @Override
+  public int getMaxRowCount() {
+    return NumberUtility.nvl(m_restrictionDo.getMaxRowCount(), 0);
+  }
+
+  @Override
+  public void setMaxRowCount(int n) {
+    m_restrictionDo.withMaxRowCount(n);
+  }
+
+  @Override
+  public String getWildcard() {
+    return null;
+  }
+
+  @Override
+  public void setWildcard(String wildcard) {
+    // NOP - unsupported
+  }
+
+  @Override
+  public void setMultilineText(boolean b) {
+    // NOP - unsupported
+  }
+
+  @Override
+  public boolean isMultilineText() {
+    return false;
+  }
+
+  protected List<? extends ILookupRow<ID>> getData() {
+    execPrepareRestriction(m_restrictionDo);
+    LookupResponse<? extends AbstractLookupRowDo<?, ID>> response = remoteCall().apply(m_restrictionDo);
+    return transformLookupResponse(response);
+  }
+
+  /**
+   * Called before remote call to lookup service resource
+   */
+  protected void execPrepareRestriction(RESTRICTION restriction) {
+  }
+
+  /**
+   * Return function to fetch {@link LookupResponse} from remote
+   */
+  protected abstract Function<RESTRICTION, LookupResponse<? extends AbstractLookupRowDo<?, ID>>> remoteCall();
+
+  /**
+   * Loads data asynchronously, and calls the specified callback once completed.
+   */
+  protected IFuture<Void> loadDataInBackground(final Supplier<List<? extends ILookupRow<ID>>> supplier, final RunContext runContext, final ILookupRowFetchedCallback<ID> callback) {
+    return Jobs.schedule(() -> loadData(supplier, callback), Jobs.newInput()
+        .withRunContext(runContext)
+        .withName("Fetching lookup data [lookupCall={}]", getClass().getName()));
+  }
+
+  /**
+   * Loads data synchronously, and calls the specified callback once completed.
+   */
+  protected void loadData(final Supplier<List<? extends ILookupRow<ID>>> supplier, final ILookupRowFetchedCallback<ID> callback) {
+    try {
+      callback.onSuccess(supplier.get());
+    }
+    catch (RuntimeException e) {
+      callback.onFailure(e);
+    }
+  }
+
+  /**
+   * Transforms {@link LookupResponse} with a list of {@link AbstractLookupRowDo} into a list of {@link ILookupRow}.
+   */
+  protected List<? extends ILookupRow<ID>> transformLookupResponse(LookupResponse<? extends AbstractLookupRowDo<?, ID>> response) {
+    return response.getRows().stream()
+        .map(this::transformLookupRow)
+        .collect(Collectors.toList());
+  }
+
+  /**
+   * Transforms one {@link AbstractLookupRowDo} into a {@link ILookupRow}.
+   */
+  protected ILookupRow<ID> transformLookupRow(AbstractLookupRowDo<?, ID> row) {
+    return new LookupRow<>(row.getId(), row.getText())
+        .withActive(row.isActive())
+        .withEnabled(row.isEnabled())
+        .withParentKey(row.getParentId());
+  }
+
+  @Override
+  public AbstractRestLookupCall<RESTRICTION, ID> copy() {
+    try {
+      @SuppressWarnings("unchecked")
+      AbstractRestLookupCall<RESTRICTION, ID> clone = (AbstractRestLookupCall<RESTRICTION, ID>) super.clone();
+      clone.m_restrictionDo = BEANS.get(DataObjectHelper.class).clone(m_restrictionDo);
+      return clone;
+    }
+    catch (CloneNotSupportedException e) {
+      throw new PlatformException("could not clone rest lookup call", e);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "[ m_restrictionDo=" + m_restrictionDo + "]";
+  }
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractRestLookupSmartColumn.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractRestLookupSmartColumn.java
new file mode 100644
index 0000000..7541cab
--- /dev/null
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractRestLookupSmartColumn.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.client.ui.basic.table.columns;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.scout.rt.client.ui.basic.table.ITableRow;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.classid.ClassId;
+import org.eclipse.scout.rt.platform.exception.ExceptionHandler;
+import org.eclipse.scout.rt.platform.util.ObjectUtility;
+import org.eclipse.scout.rt.client.services.lookup.AbstractRestLookupCall;
+import org.eclipse.scout.rt.shared.services.lookup.ILookupCall;
+import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
+
+/**
+ * Abstract smart column implementation with support for batch lookup calls using a {@link AbstractRestLookupCall}
+ * implementation.
+ * <p>
+ * <b>Note:</b> This implementation uses one single lookup call instance for batch lookups, therefore
+ * {@link #execPrepareLookup(ILookupCall, ITableRow)} is never called. Use
+ * {@link #execPrepareLookup(AbstractRestLookupCall)} instead.
+ */
+@ClassId("f8f5dfa4-cca7-4c61-8d40-641e70154641")
+public abstract class AbstractRestLookupSmartColumn<VALUE> extends AbstractSmartColumn<VALUE> {
+
+  @Override
+  protected abstract Class<? extends AbstractRestLookupCall<?, VALUE>> getConfiguredLookupCall();
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public AbstractRestLookupCall<?, VALUE> getLookupCall() {
+    return (AbstractRestLookupCall<?, VALUE>) super.getLookupCall();
+  }
+
+  @Override
+  public void updateDisplayTexts(List<ITableRow> rows) {
+    if (rows.isEmpty()) {
+      return;
+    }
+    // No need to call remove service when no non-null keys are present
+    final Set<VALUE> keys = getValues().stream()
+        .filter(Objects::nonNull)
+        .collect(Collectors.toSet());
+    if (keys.isEmpty()) {
+      return;
+    }
+    AbstractRestLookupCall<?, VALUE> call = getLookupCall();
+    if (call == null) {
+      return;
+    }
+    try {
+      // Create a copy of the prototype call before modifying it
+      AbstractRestLookupCall<?, VALUE> lookupCall = call.copy();
+      execPrepareLookup(lookupCall);
+      lookupCall.setKeys(keys);
+      List<? extends ILookupRow<VALUE>> dataByKey = lookupCall.getDataByKey();
+
+      for (ITableRow row : rows) {
+        List<? extends ILookupRow<VALUE>> lookupResult = dataByKey.stream()
+            .filter(lookupRow -> ObjectUtility.equals(lookupRow.getKey(), row.getCell(this).getValue()))
+            .collect(Collectors.toList());
+        applyLookupResult(row, lookupResult);
+      }
+    }
+    catch (RuntimeException e) {
+      BEANS.get(ExceptionHandler.class).handle(e);
+    }
+  }
+
+  /**
+   * Called before lookup is performed. This method may be used to set any custom restrictions to lookup call.
+   */
+  protected void execPrepareLookup(AbstractRestLookupCall<?, VALUE> call) {
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/LookupHelperTest.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/LookupHelperTest.java
new file mode 100644
index 0000000..45e61f5
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/LookupHelperTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import static org.eclipse.scout.rt.testing.platform.util.ScoutAssert.assertThrows;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.eclipse.scout.rt.dataobject.fixture.FixtureEnum;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.util.Assertions.AssertionException;
+import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.eclipse.scout.rt.dataobject.lookup.fixture.FixtureData;
+import org.eclipse.scout.rt.dataobject.lookup.fixture.FixtureDataLookupRestrictionDo;
+import org.eclipse.scout.rt.dataobject.lookup.fixture.FixtureDataLookupRowDo;
+import org.eclipse.scout.rt.dataobject.lookup.fixture.FixtureEnumLookupRestrictionDo;
+import org.eclipse.scout.rt.dataobject.lookup.fixture.FixtureEnumLookupRowDo;
+
+@RunWith(PlatformTestRunner.class)
+public class LookupHelperTest {
+
+  protected final Map<Long, FixtureData> m_testData = new LinkedHashMap<>();
+
+  private LookupHelper m_helper;
+
+  @Before
+  public void before() {
+    m_helper = new LookupHelper();
+    m_testData.clear();
+    m_testData.put(1L, new FixtureData(1L, "foo", "additional foo data", true));
+    m_testData.put(2L, new FixtureData(2L, "bar", "additional bar data", false));
+    m_testData.put(3L, new FixtureData(3L, "baz", "additional baz data", null));
+  }
+
+  @Test
+  public void testFilterData() {
+    LongLookupRestrictionDo restriction = BEANS.get(LongLookupRestrictionDo.class);
+
+    // get all, no restrictions
+    LookupResponse<LongLookupRowDo> lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 2L, 3L, 1L);
+
+    // get by id
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withIds(1L, 3L);
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 3L, 1L);
+
+    // get by text (full)
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withText("bar");
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 2L);
+
+    // get by text (b*)
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withText("b*");
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 2L, 3L);
+
+    // get by text (*a*)
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withText("*a*");
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 2L, 3L);
+
+    // get by text (*o)
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withText("*o");
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 1L);
+
+    // get by text (*)
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withText("*");
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 2L, 3L, 1L);
+
+    // get by active true
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withActive(true);
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 1L);
+
+    // get by active false
+    restriction = BEANS.get(LongLookupRestrictionDo.class).withActive(false);
+    lookupResponse = m_helper.filterData(restriction, m_testData.values().stream(), FixtureData::getId, FixtureData::getText, FixtureData::getActive, LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 2L);
+  }
+
+  @Test
+  public void testFilterData_additionalFilter() {
+    LookupHelper helper = new LookupHelper();
+    FixtureDataLookupRestrictionDo restriction = BEANS.get(FixtureDataLookupRestrictionDo.class);
+    restriction.withStartsWith("f");
+
+    // get all, custom filter for text start with "f"
+    LookupResponse<LongLookupRowDo> lookupResponse = helper.filterData(restriction,
+        m_testData.values().stream(),
+        FixtureData::getId,
+        FixtureData::getText,
+        FixtureData::getActive,
+        data -> data.getText().startsWith(restriction.getStartsWith()),
+        LongLookupRowDo.class);
+    assertLookupResponse(lookupResponse, 1L);
+  }
+
+  @Test
+  public void testFilterData_additionalMapperAndComparator() {
+    LookupHelper helper = new LookupHelper();
+    FixtureDataLookupRestrictionDo restriction = BEANS.get(FixtureDataLookupRestrictionDo.class);
+
+    // get all, no restrictions, custom additional data mapper
+    LookupResponse<FixtureDataLookupRowDo> lookupResponse = helper.filterData(restriction,
+        m_testData.values().stream(),
+        FixtureData::getId,
+        FixtureData::getText,
+        FixtureData::getActive,
+        LookupHelper.truePredicate(),
+        FixtureDataLookupRowDo.class,
+        (row, data) -> row.withAdditionalData(data.getAdditionalData()),
+        Comparator.comparing(AbstractLookupRowDo::getText, Comparator.reverseOrder())); // alphabetically in reverse order
+    assertLookupResponse(lookupResponse, 1L, 3L, 2L);
+
+    assertEquals("additional foo data", lookupResponse.getRows().get(0).getAdditionalData());
+    assertEquals("additional baz data", lookupResponse.getRows().get(1).getAdditionalData());
+    assertEquals("additional bar data", lookupResponse.getRows().get(2).getAdditionalData());
+  }
+
+  protected void assertLookupResponse(LookupResponse<LongLookupRowDo> lookupResponse, boolean expectAdditionalData, Long... expectedIds) {
+    for (int i = 0; i < expectedIds.length; i++) {
+      Long id = expectedIds[i];
+      assertEquals(id, lookupResponse.getRows().get(i).getId());
+      assertEquals(m_testData.get(id).getText(), lookupResponse.getRows().get(i).getText());
+    }
+  }
+
+  @Test
+  @SuppressWarnings("ResultOfMethodCallIgnored")
+  public void testLookupRowDoComparatorByText() {
+    assertThrows(NullPointerException.class, () -> LookupHelper.lookupRowDoComparatorByText().compare(null, null));
+
+    FixtureEnumLookupRowDo r1 = BEANS.get(FixtureEnumLookupRowDo.class);
+    FixtureEnumLookupRowDo r2 = BEANS.get(FixtureEnumLookupRowDo.class);
+    assertThrows(NullPointerException.class, () -> LookupHelper.lookupRowDoComparatorByText().compare(r1, r2));
+
+    r1.withText("a");
+    assertThrows(NullPointerException.class, () -> LookupHelper.lookupRowDoComparatorByText().compare(r1, r2));
+
+    r2.withText("b");
+    assertEquals(-1, LookupHelper.lookupRowDoComparatorByText().compare(r1, r2));
+    assertEquals(0, LookupHelper.lookupRowDoComparatorByText().compare(r1, r1));
+    assertEquals(1, LookupHelper.lookupRowDoComparatorByText().compare(r2, r1));
+  }
+
+  @Test
+  public void testEnumTextResolver() {
+    assertNull(LookupHelper.enumTextResolver().apply(null));
+  }
+
+  @Test
+  public void testFilterEnum() {
+    LookupHelper helper = new LookupHelper();
+
+    // get all
+    FixtureEnumLookupRestrictionDo restriction = BEANS.get(FixtureEnumLookupRestrictionDo.class);
+    assertLookupResponse(helper.filterEnumKeepSorting(restriction, FixtureEnum.class, FixtureEnumLookupRowDo.class),
+      FixtureEnum.ONE, FixtureEnum.TWO, FixtureEnum.THREE);
+
+    // get by id
+    restriction = BEANS.get(FixtureEnumLookupRestrictionDo.class).withIds(FixtureEnum.THREE);
+    assertLookupResponse(helper.filterEnumKeepSorting(restriction, FixtureEnum.class, FixtureEnumLookupRowDo.class),
+        FixtureEnum.THREE);
+
+    // get by text
+    restriction = BEANS.get(FixtureEnumLookupRestrictionDo.class).withText("non-existing fixture enum text");
+    assertLookupResponse(helper.filterEnumKeepSorting(restriction, FixtureEnum.class, FixtureEnumLookupRowDo.class));
+
+    // get by text using wildcard
+    restriction = BEANS.get(FixtureEnumLookupRestrictionDo.class).withText("*");
+    assertLookupResponse(helper.filterEnumKeepSorting(restriction, FixtureEnum.class, FixtureEnumLookupRowDo.class),
+      FixtureEnum.ONE, FixtureEnum.TWO, FixtureEnum.THREE);
+  }
+
+  @Test
+  public void testTextPatternPredicate() {
+    Predicate<Object> nullTextPatternPredicate = m_helper.textPatternPredicate(null, null);
+    assertTrue(nullTextPatternPredicate.test(null));
+    assertTrue(nullTextPatternPredicate.test(new Object()));
+    assertTrue(nullTextPatternPredicate.test("foo"));
+
+    assertThrows(AssertionException.class, () -> m_helper.textPatternPredicate("a*", null));
+
+    Predicate<String> textPatternPredicateWithIdentity = m_helper.textPatternPredicate("a*", Function.identity());
+    assertFalse(textPatternPredicateWithIdentity.test(null));
+    assertFalse(textPatternPredicateWithIdentity.test(""));
+    assertFalse(textPatternPredicateWithIdentity.test("b"));
+    assertTrue(textPatternPredicateWithIdentity.test("a"));
+    assertTrue(textPatternPredicateWithIdentity.test("aa"));
+    assertTrue(textPatternPredicateWithIdentity.test("ab"));
+
+    Predicate<FixtureData> textPatternPredicateFixtureData = m_helper.textPatternPredicate("a*", FixtureData::getText);
+    assertFalse(textPatternPredicateFixtureData.test(null));
+    assertFalse(textPatternPredicateFixtureData.test(new FixtureData(1L, null, null, null)));
+    assertFalse(textPatternPredicateFixtureData.test(new FixtureData(1L, "b", null, null)));
+    assertTrue(textPatternPredicateFixtureData.test(new FixtureData(1L, "a", null, null)));
+    assertTrue(textPatternPredicateFixtureData.test(new FixtureData(1L, "aa", null, null)));
+    assertTrue(textPatternPredicateFixtureData.test(new FixtureData(1L, "ab", null, null)));
+  }
+
+  @SafeVarargs
+  protected final <ID, LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ID>> void assertLookupResponse(LookupResponse<LOOKUP_ROW> response, @SuppressWarnings("unchecked") ID... expectedIds) {
+    assertNotNull(response);
+    List<ID> actualIds = response.rows().stream()
+        .map(AbstractLookupRowDo::getId)
+        .collect(Collectors.toList());
+    assertEquals(Arrays.asList(expectedIds), actualIds);
+  }
+
+  @Test
+  public void testMaxRowCount() {
+    assertEquals(LookupHelper.DEFAULT_MAX_ROWS, m_helper.maxRowCount(BEANS.get(FixtureEnumLookupRestrictionDo.class)));
+    assertEquals(LookupHelper.DEFAULT_MAX_ROWS, m_helper.maxRowCount(BEANS.get(FixtureEnumLookupRestrictionDo.class).withMaxRowCount(null)));
+    assertEquals(42, m_helper.maxRowCount(BEANS.get(FixtureEnumLookupRestrictionDo.class).withMaxRowCount(42)));
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureData.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureData.java
new file mode 100644
index 0000000..9ab9a67
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureData.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) BSI Business Systems Integration AG. All rights reserved.
+ * http://www.bsiag.com/
+ */
+package org.eclipse.scout.rt.dataobject.lookup.fixture;
+
+public class FixtureData {
+  private final Long m_id;
+  private final String m_text;
+  private final String m_additionalData;
+  private final Boolean m_active;
+
+  public FixtureData(Long id, String text, String additionalData, Boolean active) {
+    m_id = id;
+    m_text = text;
+    m_additionalData = additionalData;
+    m_active = active;
+  }
+
+  public Long getId() {
+    return m_id;
+  }
+
+  public String getText() {
+    return m_text;
+  }
+
+  public String getAdditionalData() {
+    return m_additionalData;
+  }
+
+  public Boolean getActive() {
+    return m_active;
+  }
+
+  @Override
+  public String toString() {
+    return "FixtureData [m_id=" + m_id + ", m_text=" + m_text + ", m_additionalData=" + m_additionalData + ", m_active=" + m_active + "]";
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureDataLookupRestrictionDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureDataLookupRestrictionDo.java
new file mode 100644
index 0000000..16ff8aa
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureDataLookupRestrictionDo.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) BSI Business Systems Integration AG. All rights reserved.
+ * http://www.bsiag.com/
+ */
+package org.eclipse.scout.rt.dataobject.lookup.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRestrictionDo;
+
+@TypeName("start.FixtureDataLookupRestriction")
+public class FixtureDataLookupRestrictionDo extends AbstractLookupRestrictionDo<FixtureDataLookupRestrictionDo, Long> {
+
+  @Override
+  public DoList<Long> ids() {
+    return createIdsAttribute(this);
+  }
+
+  public DoValue<String> startsWith() {
+    return doValue("startsWith");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public FixtureDataLookupRestrictionDo withStartsWith(String startsWith) {
+    startsWith().set(startsWith);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getStartsWith() {
+    return startsWith().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureDataLookupRowDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureDataLookupRowDo.java
new file mode 100644
index 0000000..32f3eda
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureDataLookupRowDo.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) BSI Business Systems Integration AG. All rights reserved.
+ * http://www.bsiag.com/
+ */
+package org.eclipse.scout.rt.dataobject.lookup.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRowDo;
+
+@TypeName("start.FixtureDataLookupRow")
+public class FixtureDataLookupRowDo extends AbstractLookupRowDo<FixtureDataLookupRowDo, Long> {
+
+  @Override
+  public DoValue<Long> id() {
+    return createIdAttribute(this);
+  }
+
+  public DoValue<String> additionalData() {
+    return doValue("additionalData");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public FixtureDataLookupRowDo withAdditionalData(String text) {
+    additionalData().set(text);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getAdditionalData() {
+    return additionalData().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureEnumLookupRestrictionDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureEnumLookupRestrictionDo.java
new file mode 100644
index 0000000..ab1b4a5
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureEnumLookupRestrictionDo.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) BSI Business Systems Integration AG. All rights reserved.
+ * http://www.bsiag.com/
+ */
+package org.eclipse.scout.rt.dataobject.lookup.fixture;
+
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.dataobject.fixture.FixtureEnum;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRestrictionDo;
+
+@TypeName("start.FixtureEnumLookupRestriction")
+public class FixtureEnumLookupRestrictionDo extends AbstractLookupRestrictionDo<FixtureEnumLookupRestrictionDo, FixtureEnum> {
+
+  @Override
+  public DoList<FixtureEnum> ids() {
+    return createIdsAttribute(this);
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureEnumLookupRowDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureEnumLookupRowDo.java
new file mode 100644
index 0000000..40742ca
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/lookup/fixture/FixtureEnumLookupRowDo.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) BSI Business Systems Integration AG. All rights reserved.
+ * http://www.bsiag.com/
+ */
+package org.eclipse.scout.rt.dataobject.lookup.fixture;
+
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.dataobject.fixture.FixtureEnum;
+import org.eclipse.scout.rt.dataobject.lookup.AbstractLookupRowDo;
+
+@TypeName("start.FixtureEnumLookupRow")
+public class FixtureEnumLookupRowDo extends AbstractLookupRowDo<FixtureEnumLookupRowDo, FixtureEnum> {
+
+  @Override
+  public DoValue<FixtureEnum> id() {
+    return createIdAttribute(this);
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/AbstractLookupRestrictionDo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/AbstractLookupRestrictionDo.java
new file mode 100644
index 0000000..760c894
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/AbstractLookupRestrictionDo.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.DoValue;
+
+/**
+ * Abstract base class for lookup call restrictions
+ *
+ * @param <SELF>
+ *          Type reference to concrete sub-class, used to implement with() methods returning concrete sub-class type
+ * @param <ID>
+ *          Lookup row id type
+ */
+public abstract class AbstractLookupRestrictionDo<SELF extends AbstractLookupRestrictionDo<SELF, ID>, ID> extends DoEntity {
+
+  /**
+   * A subclass should implement this method to specify the concrete attribute type.
+   *
+   * @see AbstractLookupRestrictionDo#createIdsAttribute(AbstractLookupRestrictionDo)
+   */
+  public abstract DoList<ID> ids();
+
+  public DoValue<String> text() {
+    return doValue("text");
+  }
+
+  public DoValue<Boolean> active() {
+    return doValue("active");
+  }
+
+  public DoValue<Integer> maxRowCount() {
+    return doValue("maxRowCount");
+  }
+
+  /* **************************************************************************
+   * HELPER METHODS
+   * *************************************************************************/
+
+  @SuppressWarnings("unchecked")
+  protected SELF self() {
+    return (SELF) this;
+  }
+
+  protected static <ID> DoList<ID> createIdsAttribute(AbstractLookupRestrictionDo<?, ID> self) {
+    return self.doList("ids");
+  }
+
+  /* **************************************************************************
+   * CONVENIENCE METHODS
+   * *************************************************************************/
+
+  public List<ID> getIds() {
+    return ids().get();
+  }
+
+  public Boolean getActive() {
+    return active().get();
+  }
+
+  public SELF withIds(Collection<? extends ID> ids) {
+    ids().clear();
+    ids().get().addAll(ids);
+    return self();
+  }
+
+  public SELF withIds(@SuppressWarnings("unchecked") ID... ids) {
+    return withIds(Arrays.asList(ids));
+  }
+
+  public SELF withText(String text) {
+    text().set(text);
+    return self();
+  }
+
+  public SELF withActive(Boolean active) {
+    active().set(active);
+    return self();
+  }
+
+  public SELF withMaxRowCount(Integer maxRowCount) {
+    maxRowCount().set(maxRowCount);
+    return self();
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getText() {
+    return text().get();
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public Integer getMaxRowCount() {
+    return maxRowCount().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/AbstractLookupRowDo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/AbstractLookupRowDo.java
new file mode 100644
index 0000000..e524326
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/AbstractLookupRowDo.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+
+/**
+ * Abstract base class for lookup rows with generic key type T.
+ *
+ * @param <SELF>
+ *          Type reference to concrete sub-class, used to implement with() methods returning concrete sub-class type
+ * @param <ID>
+ *          Lookup row id type
+ */
+public abstract class AbstractLookupRowDo<SELF extends AbstractLookupRowDo<SELF, ID>, ID> extends DoEntity {
+
+  protected AbstractLookupRowDo() {
+    withEnabled(true); // lookup rows are enabled by default
+    withActive(true); // lookup rows are active by default
+  }
+
+  /**
+   * A subclass should implement this method to specify the concrete attribute type.
+   *
+   * @see AbstractLookupRowDo#createIdAttribute(AbstractLookupRowDo)
+   */
+  public abstract DoValue<ID> id();
+
+  public DoValue<String> text() {
+    return doValue("text");
+  }
+
+  public DoValue<Boolean> enabled() {
+    return doValue("enabled");
+  }
+
+  public DoValue<Boolean> active() {
+    return doValue("active");
+  }
+
+  public DoValue<ID> parentId() {
+    return doValue("parentId");
+  }
+
+  /* **************************************************************************
+   * HELPER METHODS
+   * *************************************************************************/
+
+  @SuppressWarnings("unchecked")
+  protected SELF self() {
+    return (SELF) this;
+  }
+
+  protected static <ID> DoValue<ID> createIdAttribute(AbstractLookupRowDo<?, ID> self) {
+    return self.doValue("id");
+  }
+
+  /* **************************************************************************
+   * CONVENIENCE METHODS
+   * *************************************************************************/
+
+  public SELF withId(ID id) {
+    id().set(id);
+    return self();
+  }
+
+  public SELF withText(String text) {
+    text().set(text);
+    return self();
+  }
+
+  public SELF withEnabled(Boolean enabled) {
+    enabled().set(enabled);
+    return self();
+  }
+
+  public SELF withActive(Boolean active) {
+    active().set(active);
+    return self();
+  }
+
+  public SELF withParentId(ID parentId) {
+    parentId().set(parentId);
+    return self();
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public ID getId() {
+    return id().get();
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getText() {
+    return text().get();
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public Boolean isEnabled() {
+    return enabled().get();
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public Boolean isActive() {
+    return active().get();
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public ID getParentId() {
+    return parentId().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LongLookupRestrictionDo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LongLookupRestrictionDo.java
new file mode 100644
index 0000000..4996050
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LongLookupRestrictionDo.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.util.ObjectUtility;
+
+@TypeName("scout.LongLookupRestriction")
+public class LongLookupRestrictionDo extends AbstractLookupRestrictionDo<LongLookupRestrictionDo, Long> {
+
+  @Override
+  public DoList<Long> ids() {
+    return createIdsAttribute(this);
+  }
+
+  /**
+   * @return Specified {@code restriction} if not null, otherwise a new {@link LongLookupRestrictionDo} instance.
+   */
+  public static LongLookupRestrictionDo ensure(LongLookupRestrictionDo restriction) {
+    return ObjectUtility.nvl(restriction, BEANS.get(LongLookupRestrictionDo.class));
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LongLookupRowDo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LongLookupRowDo.java
new file mode 100644
index 0000000..d559a59
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LongLookupRowDo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.LongLookupRow")
+public class LongLookupRowDo extends AbstractLookupRowDo<LongLookupRowDo, Long> {
+
+  @Override
+  public DoValue<Long> id() {
+    return createIdAttribute(this);
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LookupHelper.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LookupHelper.java
new file mode 100644
index 0000000..3dd745a
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LookupHelper.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.scout.rt.dataobject.enumeration.IEnum;
+import org.eclipse.scout.rt.platform.ApplicationScoped;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.nls.CollatorProvider;
+import org.eclipse.scout.rt.platform.text.TEXTS;
+import org.eclipse.scout.rt.platform.util.Assertions;
+import org.eclipse.scout.rt.platform.util.NumberUtility;
+import org.eclipse.scout.rt.platform.util.StreamUtility;
+import org.eclipse.scout.rt.platform.util.StringUtility;
+
+/**
+ * Helper class filtering and mapping a stream of (in-memory available) data items to list of lookup rows.
+ */
+@ApplicationScoped
+public class LookupHelper {
+
+  protected static final int DEFAULT_MAX_ROWS = 100;
+
+  protected static final String WILDCARD = "*";
+  protected static final String WILDCARD_REPLACE = "@wildcard@";
+  protected static final String MATCH_ALL_REGEX = ".*";
+
+  /**
+   * Filter stream of {@code data} according to specified {@code restriction} and converts the stream to a
+   * LookupResponse containing a list of mapped lookup rows.
+   *
+   * @param <LOOKUP_ROW>
+   *          Type of lookup row (subclass of {@link AbstractLookupRowDo})
+   * @param <ID>
+   *          Primary key type of data items
+   * @param <RESTRICTION>
+   *          Type of lookup restriction (subclass of {@link AbstractLookupRestrictionDo}
+   * @param <DATA>
+   *          Type of data items
+   * @param restriction
+   *          Lookup call restriction object used to filter the stream of data
+   * @param data
+   *          Stream of data items
+   * @param idAccessor
+   *          Accessor method to get the ID of a data item
+   * @param textAccessor
+   *          Accessor method to get the text of a data item
+   * @param rowClass
+   *          Class type of lookup row to create
+   */
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ID>, ID, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ID>, DATA> LookupResponse<LOOKUP_ROW>
+      filterData(RESTRICTION restriction,
+          Stream<DATA> data,
+          Function<DATA, ID> idAccessor,
+          Function<DATA, String> textAccessor,
+          Class<LOOKUP_ROW> rowClass) {
+    return filterData(restriction, data, idAccessor, textAccessor, null, truePredicate(), rowClass, identityMapper(), lookupRowDoComparatorByText());
+  }
+
+  /**
+   * Filter stream of {@code data} according to specified {@code restriction} and converts the stream to a
+   * LookupResponse containing a list of mapped lookup rows.
+   *
+   * @param <LOOKUP_ROW>
+   *          Type of lookup row (subclass of {@link AbstractLookupRowDo})
+   * @param <ID>
+   *          Primary key type of data items
+   * @param <RESTRICTION>
+   *          Type of lookup restriction (subclass of {@link AbstractLookupRestrictionDo}
+   * @param <DATA>
+   *          Type of data items
+   * @param restriction
+   *          Lookup call restriction object used to filter the stream of data
+   * @param data
+   *          Stream of data items
+   * @param idAccessor
+   *          Accessor method to get the ID of a data item
+   * @param textAccessor
+   *          Accessor method to get the text of a data item
+   * @param rowClass
+   *          Class type of lookup row to create
+   */
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ID>, ID, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ID>, DATA> LookupResponse<LOOKUP_ROW>
+      filterData(RESTRICTION restriction,
+          Stream<DATA> data,
+          Function<DATA, ID> idAccessor,
+          Function<DATA, String> textAccessor,
+          Class<LOOKUP_ROW> rowClass,
+          Comparator<LOOKUP_ROW> lookupRowDoComparator) {
+    return filterData(restriction, data, idAccessor, textAccessor, null, truePredicate(), rowClass, identityMapper(), lookupRowDoComparator);
+  }
+
+  /**
+   * Filter stream of {@code data} according to specified {@code restriction} and converts the stream to a
+   * LookupResponse containing a list of mapped lookup rows.
+   *
+   * @param <LOOKUP_ROW>
+   *          Type of lookup row (subclass of {@link AbstractLookupRowDo})
+   * @param <ID>
+   *          Primary key type of data items
+   * @param <RESTRICTION>
+   *          Type of lookup restriction (subclass of {@link AbstractLookupRestrictionDo}
+   * @param <DATA>
+   *          Type of data items
+   * @param restriction
+   *          Lookup call restriction object used to filter the stream of data
+   * @param data
+   *          Stream of data items
+   * @param idAccessor
+   *          Accessor method to get the ID of a data item
+   * @param textAccessor
+   *          Accessor method to get the text of a data item
+   * @param activeAccessor
+   *          Accessor method to get the active state of a data item
+   * @param rowClass
+   *          Class type of lookup row to create
+   */
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ID>, ID, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ID>, DATA> LookupResponse<LOOKUP_ROW>
+      filterData(RESTRICTION restriction,
+          Stream<DATA> data,
+          Function<DATA, ID> idAccessor,
+          Function<DATA, String> textAccessor,
+          Function<DATA, Boolean> activeAccessor,
+          Class<LOOKUP_ROW> rowClass) {
+    return filterData(restriction, data, idAccessor, textAccessor, activeAccessor, truePredicate(), rowClass, identityMapper(), lookupRowDoComparatorByText());
+  }
+
+  /**
+   * Filter stream of {@code data} according to specified {@code restriction} and <b> using an additional custom filter
+   * {@code additionalFilter}</b> and converts the stream to a LookupResponse containing a list of mapped lookup rows.
+   *
+   * @see LookupHelper#filterData(AbstractLookupRestrictionDo, Stream, Function, Function, Class)
+   * @param additionalFilter
+   *          Additional filter for stream of data
+   */
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ID>, ID, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ID>, DATA> LookupResponse<LOOKUP_ROW>
+      filterData(RESTRICTION restriction,
+          Stream<DATA> data,
+          Function<DATA, ID> idAccessor,
+          Function<DATA, String> textAccessor,
+          Function<DATA, Boolean> activeAccessor,
+          Predicate<DATA> additionalFilter,
+          Class<LOOKUP_ROW> rowClass) {
+    return filterData(restriction, data, idAccessor, textAccessor, activeAccessor, additionalFilter, rowClass, identityMapper(), lookupRowDoComparatorByText());
+  }
+
+  /**
+   * Filter stream of {@code data} according to specified {@code restriction} and <b> using an additional custom filter
+   * {@code additionalFilter}</b>. Converts the stream to a LookupResponse containing a list of mapped lookup rows
+   * <b>using an additional custom {@code additionalMapper}</b>
+   *
+   * @see LookupHelper#filterData(AbstractLookupRestrictionDo, Stream, Function, Function, Class)
+   * @param additionalFilter
+   *          Additional filter for stream of data
+   * @param additionalMapper
+   *          Additional mapper to map custom properties from data object to lookup row type
+   * @param lookupRowDoComparator
+   *          Comparator the resulting {@link AbstractLookupRowDo} as sorted with. No sorting if Comparator is null.
+   */
+  @SuppressWarnings("squid:S00107")
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ID>, ID, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ID>, DATA> LookupResponse<LOOKUP_ROW>
+      filterData(RESTRICTION restriction,
+          Stream<DATA> dataStream,
+          Function<DATA, ID> idAccessor,
+          Function<DATA, String> textAccessor,
+          Function<DATA, Boolean> activeAccessor,
+          Predicate<DATA> additionalFilter,
+          Class<LOOKUP_ROW> rowClass,
+          BiFunction<LOOKUP_ROW, DATA, LOOKUP_ROW> additionalMapper,
+          Comparator<LOOKUP_ROW> lookupRowDoComparator) {
+
+    Stream<LOOKUP_ROW> stream = dataStream
+        .filter(restrictionPredicate(restriction, idAccessor, textAccessor, activeAccessor))
+        .filter(additionalFilter)
+        .map(data -> {
+          LOOKUP_ROW row = BEANS.get(rowClass)
+              .withId(idAccessor.apply(data))
+              .withText(textAccessor.apply(data));
+          return additionalMapper.apply(row, data);
+        });
+    if (lookupRowDoComparator != null) {
+      stream = stream.sorted(lookupRowDoComparator);
+    }
+    List<LOOKUP_ROW> rows = stream.collect(Collectors.toList());
+    return LookupResponse.create(rows);
+  }
+
+  /**
+   * Convenience method for filtering values of an {@link IEnum} and transforming them into
+   * {@link AbstractLookupRowDo}s.
+   * <p>
+   * This method does not support applying additional filters or mapping additional properties. Use one of the
+   * {@code filterData} methods instead.
+   *
+   * @param restriction
+   *          Lookup call restriction object used to filter the stream of data
+   * @param enumClass
+   *          {@link IEnum} class all values are taken from
+   * @param rowClass
+   *          Class type of lookup row to create
+   */
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ENUM>, ENUM extends Enum<?> & IEnum, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ENUM>> LookupResponse<LOOKUP_ROW>
+      filterEnum(RESTRICTION restriction,
+          Class<ENUM> enumClass,
+          Class<LOOKUP_ROW> rowClass) {
+    return filterData(restriction,
+        Arrays.stream(enumClass.getEnumConstants()),
+        Function.identity(),
+        enumTextResolver(),
+        rowClass);
+  }
+
+  /**
+   * Convenience method for filtering values of an {@link IEnum} and transforming them into
+   * {@link AbstractLookupRowDo}s. It keeps the natural sorting of the value as defined in {@link IEnum}.
+   * <p>
+   * This method does not support applying additional filters or mapping additional properties. Use one of the
+   * {@code filterData} methods instead.
+   *
+   * @param restriction
+   *          Lookup call restriction object used to filter the stream of data
+   * @param enumClass
+   *          {@link IEnum} class all values are taken from
+   * @param rowClass
+   *          Class type of lookup row to create
+   */
+  public <LOOKUP_ROW extends AbstractLookupRowDo<LOOKUP_ROW, ENUM>, ENUM extends Enum<?> & IEnum, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ENUM>> LookupResponse<LOOKUP_ROW>
+      filterEnumKeepSorting(RESTRICTION restriction,
+          Class<ENUM> enumClass,
+          Class<LOOKUP_ROW> rowClass) {
+    return filterData(restriction,
+        Arrays.stream(enumClass.getEnumConstants()),
+        Function.identity(),
+        enumTextResolver(),
+        null,
+        truePredicate(),
+        rowClass,
+        identityMapper(),
+        null);
+  }
+
+  /**
+   * @return {@link AbstractLookupRestrictionDo#maxRowCount()} if not {@code null}, {@link #DEFAULT_MAX_ROWS} otherwise.
+   */
+  public int maxRowCount(AbstractLookupRestrictionDo<?, ?> restriction) {
+    return NumberUtility.nvl(restriction.getMaxRowCount(), DEFAULT_MAX_ROWS);
+  }
+
+  /**
+   * {@link Predicate} which is always true
+   */
+  public static <T> Predicate<T> truePredicate() {
+    return t -> true;
+  }
+
+  /**
+   * Identity data mapper
+   */
+  public static <T, R> BiFunction<T, R, T> identityMapper() {
+    return (r, d) -> r;
+  }
+
+  /**
+   * {@link Function} that resolves the text of an {@link IEnum} using {@link IEnum#text()} and
+   * {@link TEXTS#get(String)}. Both, <code>null</code> enums as well as <code>null</code> textKeys are resolved to
+   * <code>null</code>.
+   */
+  public static <ENUM extends IEnum> Function<ENUM, String> enumTextResolver() {
+    return e -> e == null ? null : e.text();
+  }
+
+  /**
+   * {@link Comparator} working on {@link AbstractLookupRowDo#getText()}.
+   */
+  public static <LOOKUP_ROW extends AbstractLookupRowDo<?, ?>> Comparator<LOOKUP_ROW> lookupRowDoComparatorByText() {
+    Collator collator = BEANS.get(CollatorProvider.class).getInstance();
+    return (o1, o2) -> collator.compare(o1.getText(), o2.getText());
+  }
+
+  /**
+   * {@link Predicate} using the given {@code restriction} and the provided accessors {@code idAccessor} and
+   * {@code textAccessor}. The predicate reflects the restrictions provided with
+   * {@link AbstractLookupRestrictionDo#ids()} and {@link AbstractLookupRestrictionDo#text()}.
+   * <p>
+   * If {@code restriction} is {@code null}, {@link #truePredicate()} is returned.
+   */
+  public <ID, RESTRICTION extends AbstractLookupRestrictionDo<RESTRICTION, ID>, DATA> Predicate<DATA> restrictionPredicate(RESTRICTION restriction,
+      Function<DATA, ID> idAccessor,
+      Function<DATA, String> textAccessor,
+      Function<DATA, Boolean> activeAccessor) {
+    if (restriction == null) {
+      return truePredicate();
+    }
+    List<ID> ids = restriction.getIds();
+    Predicate<DATA> predicate = textPatternPredicate(restriction.getText(), textAccessor);
+    if (idAccessor != null && !ids.isEmpty()) {
+      predicate = predicate.and(data -> ids.contains(idAccessor.apply(data)));
+    }
+    if (activeAccessor != null) {
+      predicate = predicate.and(activePredicate(restriction.getActive(), activeAccessor));
+    }
+    return predicate;
+  }
+
+  /**
+   * {@link Predicate} using the given {@code textPattern} converted into a {@link Pattern} and the given
+   * {@code textAccessor} applied on the object the predicate is working on. If {@code textPattern} is {@code null},
+   * {@link #truePredicate()} is returned.
+   */
+  public <DATA> Predicate<DATA> textPatternPredicate(String textPattern, Function<DATA, String> textAccessor) {
+    if (textPattern == null) {
+      return truePredicate();
+    }
+    Assertions.assertNotNull(textAccessor, "textAccessor is required");
+    Pattern pattern = createTextSearchPattern(textPattern);
+    return data -> {
+      if (data == null) {
+        return false;
+      }
+      String text = textAccessor.apply(data);
+      return text != null && pattern.matcher(text).matches();
+    };
+  }
+
+  /**
+   * {@link Predicate} using the given {@code active} value to filter.
+   * <p>
+   * If {@code active} is {@code null}, {@link #truePredicate()} is returned.
+   */
+  public <DATA> Predicate<DATA> activePredicate(Boolean active, Function<DATA, Boolean> activeAccessor) {
+    if (active == null) {
+      return truePredicate();
+    }
+    Assertions.assertNotNull(activeAccessor, "activeAccessor is required");
+    boolean activeBoolean = active.booleanValue();
+    return data -> {
+      if (data == null) {
+        return false;
+      }
+      Boolean dataActive = activeAccessor.apply(data);
+      return dataActive != null && dataActive.booleanValue() == activeBoolean;
+    };
+  }
+
+  /**
+   * Text lookup pattern like CRM CoreUtility
+   */
+  protected Pattern createTextSearchPattern(String text) {
+    if (text == null) {
+      text = "";
+    }
+    text = text.replace(WILDCARD, WILDCARD_REPLACE);
+    text = StringUtility.escapeRegexMetachars(text);
+    text = text.replace(WILDCARD_REPLACE, MATCH_ALL_REGEX);
+    if (!text.contains(MATCH_ALL_REGEX)) {
+      text = text + MATCH_ALL_REGEX;
+    }
+    if (!text.startsWith(MATCH_ALL_REGEX)) {
+      text = MATCH_ALL_REGEX + text;
+    }
+    return Pattern.compile(text, Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+  }
+
+  /**
+   * Extracts ids form the given values and resolves them.
+   */
+  public <V, ID> Map<ID, String> resolve(
+      Stream<V> values,
+      Function<V, ID> idExtractor,
+      Function<Set<ID>, List<? extends AbstractLookupRowDo<?, ID>>> textResolver) {
+    Set<ID> ids = values
+        .map(idExtractor)
+        .filter(Objects::nonNull)
+        .collect(Collectors.toSet());
+    return textResolver.apply(ids)
+        .stream()
+        .collect(StreamUtility.toMap(AbstractLookupRowDo::getId, AbstractLookupRowDo::getText));
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LookupResponse.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LookupResponse.java
new file mode 100644
index 0000000..7db5959
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/LookupResponse.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.platform.BEANS;
+
+@TypeName("scout.LookupResponse")
+@SuppressWarnings("unchecked")
+public class LookupResponse<T extends AbstractLookupRowDo<?, ?>> extends DoEntity {
+
+  public DoList<T> rows() {
+    return doList("rows");
+  }
+
+  /**
+   * Convenience method to create a {@link LookupResponse} with specified collection of rows.
+   */
+  public static <T extends AbstractLookupRowDo<?, ?>> LookupResponse<T> create(Collection<T> rows) {
+    return BEANS.get(LookupResponse.class).withRows(rows);
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public LookupResponse<T> withRows(Collection<T> rows) {
+    rows().clear();
+    rows().get().addAll(rows);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public LookupResponse<T> withRows(T... rows) {
+    return withRows(Arrays.asList(rows));
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public List<T> getRows() {
+    return rows().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/StringLookupRestrictionDo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/StringLookupRestrictionDo.java
new file mode 100644
index 0000000..d1900a9
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/StringLookupRestrictionDo.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import org.eclipse.scout.rt.dataobject.DoList;
+import org.eclipse.scout.rt.dataobject.TypeName;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.util.ObjectUtility;
+
+@TypeName("scout.StringLookupRestriction")
+public class StringLookupRestrictionDo extends AbstractLookupRestrictionDo<StringLookupRestrictionDo, String> {
+
+  @Override
+  public DoList<String> ids() {
+    return createIdsAttribute(this);
+  }
+
+  /**
+   * @return Specified {@code restriction} if not null, otherwise a new {@link StringLookupRestrictionDo} instance.
+   */
+  public static StringLookupRestrictionDo ensure(StringLookupRestrictionDo restriction) {
+    return ObjectUtility.nvl(restriction, BEANS.get(StringLookupRestrictionDo.class));
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/StringLookupRowDo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/StringLookupRowDo.java
new file mode 100644
index 0000000..ed3a22f
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/lookup/StringLookupRowDo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2010-2020 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.lookup;
+
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.StringLookupRow")
+public class StringLookupRowDo extends AbstractLookupRowDo<StringLookupRowDo, String> {
+
+  @Override
+  public DoValue<String> id() {
+    return createIdAttribute(this);
+  }
+}