You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@olingo.apache.org by mi...@apache.org on 2014/09/11 14:06:08 UTC

git commit: [OLINGO-422] Support for expand in server serializer

Repository: olingo-odata4
Updated Branches:
  refs/heads/OLINGO-422-SelectExpandSupport 1976c3407 -> b41129cf9


[OLINGO-422] Support for expand in server serializer


Project: http://git-wip-us.apache.org/repos/asf/olingo-odata4/repo
Commit: http://git-wip-us.apache.org/repos/asf/olingo-odata4/commit/b41129cf
Tree: http://git-wip-us.apache.org/repos/asf/olingo-odata4/tree/b41129cf
Diff: http://git-wip-us.apache.org/repos/asf/olingo-odata4/diff/b41129cf

Branch: refs/heads/OLINGO-422-SelectExpandSupport
Commit: b41129cf91401822451e74fe0bff355d9043dc58
Parents: 1976c34
Author: Michael Bolz <mi...@sap.com>
Authored: Thu Sep 11 14:05:43 2014 +0200
Committer: Michael Bolz <mi...@sap.com>
Committed: Thu Sep 11 14:05:43 2014 +0200

----------------------------------------------------------------------
 .../serializer/json/ODataJsonSerializer.java    | 121 ++++++------
 .../serializer/utils/ExpandSelectHelper.java    | 127 ++++++++++++
 .../json/ODataJsonSerializerTest.java           | 191 ++++++++++++++++++-
 3 files changed, 370 insertions(+), 69 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/b41129cf/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java
index 665c258..2e4402b 100644
--- a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializer.java
@@ -20,7 +20,6 @@ package org.apache.olingo.server.core.serializer.json;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -29,12 +28,14 @@ import org.apache.olingo.commons.api.Constants;
 import org.apache.olingo.commons.api.data.ContextURL;
 import org.apache.olingo.commons.api.data.Entity;
 import org.apache.olingo.commons.api.data.EntitySet;
+import org.apache.olingo.commons.api.data.Link;
 import org.apache.olingo.commons.api.data.LinkedComplexValue;
 import org.apache.olingo.commons.api.data.Property;
 import org.apache.olingo.commons.api.edm.Edm;
 import org.apache.olingo.commons.api.edm.EdmComplexType;
 import org.apache.olingo.commons.api.edm.EdmEntitySet;
 import org.apache.olingo.commons.api.edm.EdmEntityType;
+import org.apache.olingo.commons.api.edm.EdmNavigationProperty;
 import org.apache.olingo.commons.api.edm.EdmPrimitiveType;
 import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
 import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
@@ -44,12 +45,10 @@ import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory;
 import org.apache.olingo.server.api.ODataServerError;
 import org.apache.olingo.server.api.serializer.ODataSerializer;
 import org.apache.olingo.server.api.serializer.ODataSerializerException;
-import org.apache.olingo.server.api.uri.UriResource;
-import org.apache.olingo.server.api.uri.UriResourceProperty;
 import org.apache.olingo.server.api.uri.queryoption.ExpandItem;
-import org.apache.olingo.server.api.uri.queryoption.SelectItem;
 import org.apache.olingo.server.core.serializer.utils.CircleStreamBuffer;
 import org.apache.olingo.server.core.serializer.utils.ContextURLBuilder;
+import org.apache.olingo.server.core.serializer.utils.ExpandSelectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -143,11 +142,7 @@ public class ODataJsonSerializer implements ODataSerializer {
         json.writeNumberField(Constants.JSON_COUNT, entitySet.getCount());
       }
       json.writeFieldName(Constants.VALUE);
-      json.writeStartArray();
-      for (Entity entity : entitySet.getEntities()) {
-        writeEntity(edmEntitySet, entity, null, options, json);
-      }
-      json.writeEndArray();
+      writeEntitySet(edmEntitySet.getEntityType(), entitySet, options, json);
       if (entitySet.getNext() != null) {
         json.writeStringField(Constants.JSON_NEXT_LINK, entitySet.getNext().toASCIIString());
       }
@@ -169,7 +164,7 @@ public class ODataJsonSerializer implements ODataSerializer {
     CircleStreamBuffer buffer = new CircleStreamBuffer();
     try {
       JsonGenerator json = new JsonFactory().createGenerator(buffer.getOutputStream());
-      writeEntity(edmEntitySet, entity, contextURL, options, json);
+      writeEntity(edmEntitySet.getEntityType(), entity, contextURL, options, json);
       json.close();
     } catch (final IOException e) {
       throw new ODataSerializerException("An I/O exception occurred.", e,
@@ -178,9 +173,17 @@ public class ODataJsonSerializer implements ODataSerializer {
     return buffer.getInputStream();
   }
 
-  protected void writeEntity(final EdmEntitySet entitySet, final Entity entity, final ContextURL contextURL,
+  protected void writeEntitySet(final EdmEntityType entityType, final EntitySet entitySet,
+      final ExpandItem options, final JsonGenerator json) throws IOException, ODataSerializerException {
+    json.writeStartArray();
+    for (final Entity entity : entitySet.getEntities()) {
+      writeEntity(entityType, entity, null, options, json);
+    }
+    json.writeEndArray();
+  }
+
+  protected void writeEntity(final EdmEntityType entityType, final Entity entity, final ContextURL contextURL,
       final ExpandItem options, final JsonGenerator json) throws IOException, ODataSerializerException {
-    final EdmEntityType entityType = entitySet.getEntityType();
     json.writeStartObject();
     if (format != ODataFormat.JSON_NO_METADATA) {
       if (contextURL != null) {
@@ -198,67 +201,66 @@ public class ODataJsonSerializer implements ODataSerializer {
         }
       }
     }
-    final boolean all = isAll(options);
-    final Set<String> selected = all ? null : getSelectedPropertyNames(options.getSelectOption().getSelectItems());
+    writeProperties(entityType, entity, options, json);
+    writeNavigationProperties(entityType, entity, options, json);
+    json.writeEndObject();
+  }
+
+  protected void writeProperties(final EdmEntityType entityType, final Entity entity, final ExpandItem options,
+      final JsonGenerator json) throws IOException, ODataSerializerException {
+    final boolean all = ExpandSelectHelper.isAll(options);
+    final Set<String> selected = all ? null :
+        ExpandSelectHelper.getSelectedPropertyNames(options.getSelectOption().getSelectItems());
     for (final String propertyName : entityType.getPropertyNames()) {
       if (all || selected.contains(propertyName)) {
         final EdmProperty edmProperty = (EdmProperty) entityType.getProperty(propertyName);
         final Property property = entity.getProperty(propertyName);
         final Set<List<String>> selectedPaths = all || edmProperty.isPrimitive() ? null :
-            getSelectedPaths(options.getSelectOption().getSelectItems(), propertyName);
+            ExpandSelectHelper.getSelectedPaths(options.getSelectOption().getSelectItems(), propertyName);
         writeProperty(edmProperty, property, selectedPaths, json);
       }
     }
-    json.writeEndObject();
   }
 
-  private boolean isAll(final ExpandItem options) {
-    if (options == null || options.getSelectOption() == null
-        || options.getSelectOption().getSelectItems() == null
-        || options.getSelectOption().getSelectItems().isEmpty()) {
-      return true;
-    } else {
-      for (final SelectItem item : options.getSelectOption().getSelectItems()) {
-        if (item.isStar()) {
-          return true;
+  protected void writeNavigationProperties(final EdmEntityType entityType, final Entity entity,
+      final ExpandItem options, final JsonGenerator json) throws ODataSerializerException, IOException {
+    if (options != null && (options.isRef() || options.getLevelsOption() != null)) {
+      throw new ODataSerializerException("Expand options $ref and $levels are not supported.",
+          ODataSerializerException.MessageKeys.NOT_IMPLEMENTED);
+    }
+    if (ExpandSelectHelper.hasExpand(options)) {
+      final boolean expandAll = ExpandSelectHelper.isExpandAll(options);
+      final Set<String> expanded = expandAll ? null :
+          ExpandSelectHelper.getExpandedPropertyNames(options.getExpandOption().getExpandItems());
+      for (final String propertyName : entityType.getNavigationPropertyNames()) {
+        if (expandAll || expanded.contains(propertyName)) {
+          final EdmNavigationProperty property = entityType.getNavigationProperty(propertyName);
+          final Link navigationLink = entity.getNavigationLink(property.getName());
+          final ExpandItem innerOptions = expandAll ? null :
+              ExpandSelectHelper.getExpandItem(options.getExpandOption().getExpandItems(), propertyName);
+          writeExpandedNavigationProperty(property, navigationLink, innerOptions, json);
         }
       }
-      return false;
     }
   }
 
-  private Set<String> getSelectedPropertyNames(final List<SelectItem> selectItems) {
-    Set<String> selected = new HashSet<String>();
-    for (final SelectItem item : selectItems) {
-      final UriResource resource = item.getResourcePath().getUriResourceParts().get(0);
-      if (resource instanceof UriResourceProperty) {
-        selected.add(((UriResourceProperty) resource).getProperty().getName());
+  protected void writeExpandedNavigationProperty(final EdmNavigationProperty property, final Link navigationLink,
+      final ExpandItem innerOptions, JsonGenerator json) throws IOException, ODataSerializerException {
+    json.writeFieldName(property.getName());
+    if (property.isCollection()) {
+      if (navigationLink == null || navigationLink.getInlineEntitySet() == null) {
+        json.writeStartArray();
+        json.writeEndArray();
+      } else {
+        writeEntitySet(property.getType(), navigationLink.getInlineEntitySet(), innerOptions, json);
       }
-    }
-    return selected;
-  }
-
-  private Set<List<String>> getSelectedPaths(final List<SelectItem> selectItems, final String propertyName) {
-    Set<List<String>> selectedPaths = new HashSet<List<String>>();
-    for (final SelectItem item : selectItems) {
-      final List<UriResource> parts = item.getResourcePath().getUriResourceParts();
-      final UriResource resource = parts.get(0);
-      if (resource instanceof UriResourceProperty
-          && propertyName.equals(((UriResourceProperty) resource).getProperty().getName())) {
-        if (parts.size() > 1) {
-          List<String> path = new ArrayList<String>();
-          for (final UriResource part : parts.subList(1, parts.size())) {
-            if (part instanceof UriResourceProperty) {
-              path.add(((UriResourceProperty) part).getProperty().getName());
-            }
-          }
-          selectedPaths.add(path);
-        } else {
-          return null;
-        }
+    } else {
+      if (navigationLink == null || navigationLink.getInlineEntity() == null) {
+        json.writeNull();
+      } else {
+        writeEntity(property.getType(), navigationLink.getInlineEntity(), null, innerOptions, json);
       }
     }
-    return selectedPaths.isEmpty() ? null : selectedPaths;
   }
 
   protected void writeProperty(final EdmProperty edmProperty, final Property property,
@@ -338,8 +340,7 @@ public class ODataJsonSerializer implements ODataSerializer {
   }
 
   protected void writePrimitiveValue(final EdmProperty edmProperty, final Object primitiveValue,
-      final JsonGenerator json)
-      throws EdmPrimitiveTypeException, IOException {
+      final JsonGenerator json) throws EdmPrimitiveTypeException, IOException {
     final EdmPrimitiveType type = (EdmPrimitiveType) edmProperty.getType();
     final String value = type.valueToString(primitiveValue,
         edmProperty.isNullable(), edmProperty.getMaxLength(),
@@ -360,7 +361,7 @@ public class ODataJsonSerializer implements ODataSerializer {
     }
   }
 
-  private void writeComplexValue(final EdmProperty edmProperty, final List<Property> properties,
+  protected void writeComplexValue(final EdmProperty edmProperty, final List<Property> properties,
       final Set<List<String>> selectedPaths, JsonGenerator json)
       throws IOException, EdmPrimitiveTypeException, ODataSerializerException {
     final EdmComplexType type = (EdmComplexType) edmProperty.getType();
@@ -385,7 +386,7 @@ public class ODataJsonSerializer implements ODataSerializer {
     return null;
   }
 
-  private boolean isSelected(final Set<List<String>> selectedPaths, final String propertyName) {
+  private static boolean isSelected(final Set<List<String>> selectedPaths, final String propertyName) {
     for (final List<String> path : selectedPaths) {
       if (propertyName.equals(path.get(0))) {
         return true;
@@ -394,7 +395,7 @@ public class ODataJsonSerializer implements ODataSerializer {
     return false;
   }
 
-  private Set<List<String>> getReducedSelectedPaths(final Set<List<String>> selectedPaths,
+  private static Set<List<String>> getReducedSelectedPaths(final Set<List<String>> selectedPaths,
       final String propertyName) {
     Set<List<String>> reducedPaths = new HashSet<List<String>>();
     for (final List<String> path : selectedPaths) {

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/b41129cf/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ExpandSelectHelper.java
----------------------------------------------------------------------
diff --git a/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ExpandSelectHelper.java b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ExpandSelectHelper.java
new file mode 100644
index 0000000..09bb50d
--- /dev/null
+++ b/lib/server-core/src/main/java/org/apache/olingo/server/core/serializer/utils/ExpandSelectHelper.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.olingo.server.core.serializer.utils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.olingo.server.api.serializer.ODataSerializerException;
+import org.apache.olingo.server.api.uri.UriResource;
+import org.apache.olingo.server.api.uri.UriResourceNavigation;
+import org.apache.olingo.server.api.uri.UriResourceProperty;
+import org.apache.olingo.server.api.uri.queryoption.ExpandItem;
+import org.apache.olingo.server.api.uri.queryoption.SelectItem;
+
+public abstract class ExpandSelectHelper {
+
+  public static boolean isAll(final ExpandItem options) {
+    if (options == null || options.getSelectOption() == null
+        || options.getSelectOption().getSelectItems() == null
+        || options.getSelectOption().getSelectItems().isEmpty()) {
+      return true;
+    } else {
+      for (final SelectItem item : options.getSelectOption().getSelectItems()) {
+        if (item.isStar()) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  public static Set<String> getSelectedPropertyNames(final List<SelectItem> selectItems) {
+    Set<String> selected = new HashSet<String>();
+    for (final SelectItem item : selectItems) {
+      final UriResource resource = item.getResourcePath().getUriResourceParts().get(0);
+      if (resource instanceof UriResourceProperty) {
+        selected.add(((UriResourceProperty) resource).getProperty().getName());
+      }
+    }
+    return selected;
+  }
+
+  public static Set<List<String>> getSelectedPaths(final List<SelectItem> selectItems, final String propertyName) {
+    Set<List<String>> selectedPaths = new HashSet<List<String>>();
+    for (final SelectItem item : selectItems) {
+      final List<UriResource> parts = item.getResourcePath().getUriResourceParts();
+      final UriResource resource = parts.get(0);
+      if (resource instanceof UriResourceProperty
+          && propertyName.equals(((UriResourceProperty) resource).getProperty().getName())) {
+        if (parts.size() > 1) {
+          List<String> path = new ArrayList<String>();
+          for (final UriResource part : parts.subList(1, parts.size())) {
+            if (part instanceof UriResourceProperty) {
+              path.add(((UriResourceProperty) part).getProperty().getName());
+            }
+          }
+          selectedPaths.add(path);
+        } else {
+          return null;
+        }
+      }
+    }
+    return selectedPaths.isEmpty() ? null : selectedPaths;
+  }
+
+  public static boolean hasExpand(final ExpandItem options) {
+    return options != null && options.getExpandOption() != null && options.getExpandOption().getExpandItems() != null
+        && !options.getExpandOption().getExpandItems().isEmpty();
+  }
+
+  public static boolean isExpandAll(final ExpandItem options) {
+    for (final ExpandItem item : options.getExpandOption().getExpandItems()) {
+      if (item.isStar()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static Set<String> getExpandedPropertyNames(final List<ExpandItem> expandItems)
+      throws ODataSerializerException {
+    Set<String> expanded = new HashSet<String>();
+    for (final ExpandItem item : expandItems) {
+      final List<UriResource> resourceParts = item.getResourcePath().getUriResourceParts();
+      if (resourceParts.size() == 1) {
+        final UriResource resource = resourceParts.get(0);
+        if (resource instanceof UriResourceNavigation) {
+          expanded.add(((UriResourceNavigation) resource).getProperty().getName());
+        }
+      } else {
+        throw new ODataSerializerException("Expand is not supported within complex properties.",
+            ODataSerializerException.MessageKeys.NOT_IMPLEMENTED);
+      }
+    }
+    return expanded;
+  }
+
+  public static ExpandItem getExpandItem(final List<ExpandItem> expandItems, final String propertyName) {
+    for (final ExpandItem item : expandItems) {
+      final UriResource resource = item.getResourcePath().getUriResourceParts().get(0);
+      if (resource instanceof UriResourceNavigation
+          && propertyName.equals(((UriResourceNavigation) resource).getProperty().getName())) {
+        return item;
+      }
+    }
+    return null;
+  }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/olingo-odata4/blob/b41129cf/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java
----------------------------------------------------------------------
diff --git a/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java b/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java
index e33e1dd..66363ae 100644
--- a/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java
+++ b/lib/server-test/src/test/java/org/apache/olingo/server/core/serializer/json/ODataJsonSerializerTest.java
@@ -31,19 +31,24 @@ import org.apache.olingo.commons.api.data.Entity;
 import org.apache.olingo.commons.api.data.EntitySet;
 import org.apache.olingo.commons.api.data.ValueType;
 import org.apache.olingo.commons.api.edm.Edm;
+import org.apache.olingo.commons.api.edm.EdmElement;
 import org.apache.olingo.commons.api.edm.EdmEntityContainer;
 import org.apache.olingo.commons.api.edm.EdmEntitySet;
+import org.apache.olingo.commons.api.edm.EdmNavigationProperty;
 import org.apache.olingo.commons.api.edm.EdmProperty;
 import org.apache.olingo.commons.api.edm.EdmStructuredType;
 import org.apache.olingo.commons.api.edm.FullQualifiedName;
+import org.apache.olingo.commons.api.edm.constants.EdmTypeKind;
 import org.apache.olingo.commons.api.format.ODataFormat;
 import org.apache.olingo.server.api.OData;
 import org.apache.olingo.server.api.serializer.ODataSerializer;
 import org.apache.olingo.server.api.serializer.ODataSerializerException;
 import org.apache.olingo.server.api.uri.UriInfoResource;
 import org.apache.olingo.server.api.uri.UriResource;
+import org.apache.olingo.server.api.uri.UriResourceNavigation;
 import org.apache.olingo.server.api.uri.UriResourceProperty;
 import org.apache.olingo.server.api.uri.queryoption.ExpandItem;
+import org.apache.olingo.server.api.uri.queryoption.ExpandOption;
 import org.apache.olingo.server.api.uri.queryoption.SelectItem;
 import org.apache.olingo.server.api.uri.queryoption.SelectOption;
 import org.apache.olingo.server.tecsvc.data.DataProvider;
@@ -55,11 +60,11 @@ import org.mockito.Mockito;
 
 public class ODataJsonSerializerTest {
 
-  private final Edm edm = OData.newInstance().createEdm(new EdmTechProvider());
-  private final EdmEntityContainer entityContainer = edm.getEntityContainer(
+  private static final Edm edm = OData.newInstance().createEdm(new EdmTechProvider());
+  private static final EdmEntityContainer entityContainer = edm.getEntityContainer(
       new FullQualifiedName("olingo.odata.test1", "Container"));
   private final DataProvider data = new DataProvider();
-  private ODataSerializer serializer = new ODataJsonSerializer(ODataFormat.JSON);
+  private final ODataSerializer serializer = new ODataJsonSerializer(ODataFormat.JSON);
 
   @Test
   public void entitySimple() throws Exception {
@@ -399,18 +404,173 @@ public class ODataJsonSerializerTest {
         resultString);
   }
 
-  private SelectItem mockSelectItem(final EdmEntitySet edmEntitySet, final String... names) {
+  @Test
+  public void expand() throws Exception {
+    final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESTwoPrim");
+    final Entity entity = data.readAll(edmEntitySet).getEntities().get(3);
+    final ExpandOption expand = mockExpandOption(Arrays.asList(
+        mockExpandItem(edmEntitySet, "NavPropertyETAllPrimOne")));
+    ExpandItem options = Mockito.mock(ExpandItem.class);
+    Mockito.when(options.getExpandOption()).thenReturn(expand);
+    InputStream result = serializer.entity(edmEntitySet, entity,
+        ContextURL.Builder.create().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build(),
+        options);
+    final String resultString = IOUtils.toString(result);
+    Assert.assertEquals("{\"@odata.context\":\"$metadata#ESTwoPrim/$entity\","
+        + "\"PropertyInt16\":32767,\"PropertyString\":\"Test String4\","
+        + "\"NavPropertyETAllPrimOne\":{"
+        + "\"PropertyInt16\":32767,"
+        + "\"PropertyString\":\"First Resource - positive values\","
+        + "\"PropertyBoolean\":true,"
+        + "\"PropertyByte\":255,"
+        + "\"PropertySByte\":127,"
+        + "\"PropertyInt32\":2147483647,"
+        + "\"PropertyInt64\":9223372036854775807,"
+        + "\"PropertySingle\":1.79E20,"
+        + "\"PropertyDouble\":-1.79E19,"
+        + "\"PropertyDecimal\":34,"
+        + "\"PropertyBinary\":\"ASNFZ4mrze8=\","
+        + "\"PropertyDate\":\"2012-12-03\","
+        + "\"PropertyDateTimeOffset\":\"2012-12-03T07:16:23Z\","
+        + "\"PropertyDuration\":\"PT6S\","
+        + "\"PropertyGuid\":\"01234567-89ab-cdef-0123-456789abcdef\","
+        + "\"PropertyTimeOfDay\":\"03:26:05\"}}",
+        resultString);
+  }
+
+  @Test
+  public void expandSelect() throws Exception {
+    final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESTwoPrim");
+    final Entity entity = data.readAll(edmEntitySet).getEntities().get(3);
+    final SelectOption select = mockSelectOption(Arrays.asList(
+        mockSelectItem(entityContainer.getEntitySet("ESAllPrim"), "PropertyDate")));
+    ExpandItem expandItem = mockExpandItem(edmEntitySet, "NavPropertyETAllPrimOne");
+    Mockito.when(expandItem.getSelectOption()).thenReturn(select);
+    final ExpandOption expand = mockExpandOption(Arrays.asList(expandItem));
+    ExpandItem options = Mockito.mock(ExpandItem.class);
+    Mockito.when(options.getExpandOption()).thenReturn(expand);
+    InputStream result =
+        new ODataJsonSerializer(ODataFormat.JSON_NO_METADATA) // serializer
+            .entity(edmEntitySet, entity,
+                null, // ContextURL.Builder.create().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build(),
+                options);
+    final String resultString = IOUtils.toString(result);
+    Assert.assertEquals("{"
+        // + "\"@odata.context\":\"$metadata#ESTwoPrim(NavPropertyETAllPrimOne(PropertyDate))/$entity\","
+        + "\"PropertyInt16\":32767,\"PropertyString\":\"Test String4\","
+        + "\"NavPropertyETAllPrimOne\":{\"PropertyDate\":\"2012-12-03\"}}",
+        resultString);
+  }
+
+  @Test
+  public void expandAll() throws Exception {
+    final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESAllPrim");
+    final Entity entity = data.readAll(edmEntitySet).getEntities().get(0);
+    final ExpandItem expandItem = mockExpandItem(edmEntitySet, "NavPropertyETTwoPrimOne");
+    ExpandItem expandItemAll = Mockito.mock(ExpandItem.class);
+    Mockito.when(expandItemAll.isStar()).thenReturn(true);
+    final ExpandOption expand = mockExpandOption(Arrays.asList(expandItem, expandItem, expandItemAll));
+    final SelectOption select = mockSelectOption(Arrays.asList(mockSelectItem(edmEntitySet, "PropertySByte")));
+    ExpandItem options = Mockito.mock(ExpandItem.class);
+    Mockito.when(options.getExpandOption()).thenReturn(expand);
+    Mockito.when(options.getSelectOption()).thenReturn(select);
+    InputStream result =
+        new ODataJsonSerializer(ODataFormat.JSON_NO_METADATA) // serializer
+            .entity(edmEntitySet, entity,
+                null, // ContextURL.Builder.create().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build(),
+                options);
+    final String resultString = IOUtils.toString(result);
+    Assert.assertEquals("{"
+        // + "\"@odata.context\":\"$metadata#ESAllPrim(PropertySByte)/$entity\","
+        + "\"PropertySByte\":127,"
+        + "\"NavPropertyETTwoPrimOne\":{\"PropertyInt16\":32767,\"PropertyString\":\"Test String4\"},"
+        + "\"NavPropertyETTwoPrimMany\":[{\"PropertyInt16\":-365,\"PropertyString\":\"Test String2\"}]}",
+        resultString);
+  }
+
+  @Test
+  public void expandNoData() throws Exception {
+    final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESAllPrim");
+    final Entity entity = data.readAll(edmEntitySet).getEntities().get(1);
+    ExpandItem expandItemAll = Mockito.mock(ExpandItem.class);
+    Mockito.when(expandItemAll.isStar()).thenReturn(true);
+    final ExpandOption expand = mockExpandOption(Arrays.asList(expandItemAll));
+    final SelectOption select = mockSelectOption(Arrays.asList(mockSelectItem(edmEntitySet, "PropertyTimeOfDay")));
+    ExpandItem options = Mockito.mock(ExpandItem.class);
+    Mockito.when(options.getExpandOption()).thenReturn(expand);
+    Mockito.when(options.getSelectOption()).thenReturn(select);
+    InputStream result =
+        new ODataJsonSerializer(ODataFormat.JSON_NO_METADATA) // serializer
+            .entity(edmEntitySet, entity,
+                null, // ContextURL.Builder.create().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build(),
+                options);
+    final String resultString = IOUtils.toString(result);
+    Assert.assertEquals("{"
+        // + "\"@odata.context\":\"$metadata#ESAllPrim(PropertyTimeOfDay)/$entity\","
+        + "\"PropertyTimeOfDay\":\"23:49:14\","
+        + "\"NavPropertyETTwoPrimOne\":null,\"NavPropertyETTwoPrimMany\":[]}",
+        resultString);
+  }
+
+  @Test
+  public void expandTwoLevels() throws Exception {
+    final EdmEntitySet edmEntitySet = entityContainer.getEntitySet("ESTwoPrim");
+    final EdmEntitySet innerEntitySet = entityContainer.getEntitySet("ESAllPrim");
+    final Entity entity = data.readAll(edmEntitySet).getEntities().get(1);
+    ExpandItem expandItemSecond = Mockito.mock(ExpandItem.class);
+    Mockito.when(expandItemSecond.isStar()).thenReturn(true);
+    final ExpandOption expandInner = mockExpandOption(Arrays.asList(expandItemSecond));
+    ExpandItem expandItemFirst = mockExpandItem(edmEntitySet, "NavPropertyETAllPrimMany");
+    Mockito.when(expandItemFirst.getExpandOption()).thenReturn(expandInner);
+    final SelectOption select = mockSelectOption(Arrays.asList(
+        mockSelectItem(innerEntitySet, "PropertyInt32")));
+    Mockito.when(expandItemFirst.getSelectOption()).thenReturn(select);
+    final ExpandOption expand = mockExpandOption(Arrays.asList(expandItemFirst));
+    ExpandItem options = Mockito.mock(ExpandItem.class);
+    Mockito.when(options.getExpandOption()).thenReturn(expand);
+    InputStream result =
+        new ODataJsonSerializer(ODataFormat.JSON_NO_METADATA) // serializer
+            .entity(edmEntitySet, entity,
+                null, // ContextURL.Builder.create().entitySet(edmEntitySet).suffix(Suffix.ENTITY).build(),
+                options);
+    final String resultString = IOUtils.toString(result);
+    Assert.assertEquals("{"
+        // + "\"@odata.context\":\"$metadata#ESTwoPrim(NavPropertyETAllPrimMany(PropertyInt32))/$entity\","
+        + "\"PropertyInt16\":-365,\"PropertyString\":\"Test String2\","
+        + "\"NavPropertyETAllPrimMany\":["
+        + "{\"PropertyInt32\":-2147483648,\"NavPropertyETTwoPrimOne\":null,\"NavPropertyETTwoPrimMany\":[]},"
+        + "{\"PropertyInt32\":0,\"NavPropertyETTwoPrimOne\":null,"
+        + "\"NavPropertyETTwoPrimMany\":["
+        + "{\"PropertyInt16\":32766,\"PropertyString\":\"Test String1\"},"
+        + "{\"PropertyInt16\":-32766,\"PropertyString\":\"Test String3\"},"
+        + "{\"PropertyInt16\":32767,\"PropertyString\":\"Test String4\"}]}]}",
+        resultString);
+  }
+
+  private UriInfoResource mockResource(final EdmEntitySet edmEntitySet, final String... names) {
     EdmStructuredType type = edmEntitySet.getEntityType();
     List<UriResource> elements = new ArrayList<UriResource>();
     for (final String name : Arrays.asList(names)) {
-      UriResourceProperty element = Mockito.mock(UriResourceProperty.class);
-      final EdmProperty property = (EdmProperty) type.getProperty(name);
-      Mockito.when(element.getProperty()).thenReturn(property);
-      elements.add(element);
-      type = property.isPrimitive() ? null : (EdmStructuredType) property.getType();
+      final EdmElement edmElement = type.getProperty(name);
+      if (edmElement.getType().getKind() == EdmTypeKind.ENTITY) {
+        UriResourceNavigation element = Mockito.mock(UriResourceNavigation.class);
+        Mockito.when(element.getProperty()).thenReturn((EdmNavigationProperty) edmElement);
+        elements.add(element);
+      } else {
+        final EdmProperty property = (EdmProperty) edmElement;
+        UriResourceProperty element = Mockito.mock(UriResourceProperty.class);
+        Mockito.when(element.getProperty()).thenReturn(property);
+        elements.add(element);
+        type = property.isPrimitive() ? null : (EdmStructuredType) property.getType();
+      }
     }
     UriInfoResource resource = Mockito.mock(UriInfoResource.class);
     Mockito.when(resource.getUriResourceParts()).thenReturn(elements);
+    return resource;
+  }
+
+  private SelectItem mockSelectItem(final EdmEntitySet edmEntitySet, final String... names) {
+    final UriInfoResource resource = mockResource(edmEntitySet, names);
     SelectItem selectItem = Mockito.mock(SelectItem.class);
     Mockito.when(selectItem.getResourcePath()).thenReturn(resource);
     return selectItem;
@@ -421,4 +581,17 @@ public class ODataJsonSerializerTest {
     Mockito.when(select.getSelectItems()).thenReturn(selectItems);
     return select;
   }
+
+  private ExpandItem mockExpandItem(final EdmEntitySet edmEntitySet, final String... names) {
+    final UriInfoResource resource = mockResource(edmEntitySet, names);
+    ExpandItem expandItem = Mockito.mock(ExpandItem.class);
+    Mockito.when(expandItem.getResourcePath()).thenReturn(resource);
+    return expandItem;
+  }
+
+  private ExpandOption mockExpandOption(final List<ExpandItem> expandItems) {
+    ExpandOption expand = Mockito.mock(ExpandOption.class);
+    Mockito.when(expand.getExpandItems()).thenReturn(expandItems);
+    return expand;
+  }
 }