[OLINGO-988] Prevent duplicate ExpandItems in ExpandTreeBuilder
This commit is contained in:
parent
22a21a28ea
commit
44d6f5a171
|
@ -19,13 +19,17 @@
|
|||
package org.apache.olingo.server.core.deserializer.helper;
|
||||
|
||||
import org.apache.olingo.commons.api.edm.EdmNavigationProperty;
|
||||
import org.apache.olingo.server.api.uri.queryoption.ExpandOption;
|
||||
import org.apache.olingo.server.core.uri.UriInfoImpl;
|
||||
import org.apache.olingo.server.core.uri.UriResourceNavigationPropertyImpl;
|
||||
import org.apache.olingo.server.core.uri.queryoption.ExpandItemImpl;
|
||||
|
||||
public abstract class ExpandTreeBuilder {
|
||||
|
||||
public abstract ExpandTreeBuilder expand(EdmNavigationProperty edmNavigationProperty);
|
||||
|
||||
public abstract ExpandOption build();
|
||||
|
||||
protected ExpandItemImpl buildExpandItem(final EdmNavigationProperty edmNavigationProperty) {
|
||||
return new ExpandItemImpl()
|
||||
.setResourcePath(new UriInfoImpl()
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
*/
|
||||
package org.apache.olingo.server.core.deserializer.helper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.olingo.commons.api.edm.EdmNavigationProperty;
|
||||
import org.apache.olingo.server.api.uri.queryoption.ExpandOption;
|
||||
import org.apache.olingo.server.core.uri.queryoption.ExpandItemImpl;
|
||||
|
@ -25,43 +28,41 @@ import org.apache.olingo.server.core.uri.queryoption.ExpandOptionImpl;
|
|||
|
||||
public class ExpandTreeBuilderImpl extends ExpandTreeBuilder {
|
||||
|
||||
private final Map<String, ExpandTreeBuilder> childBuilderCache = new HashMap<String, ExpandTreeBuilder>();
|
||||
private final ExpandItemImpl parentItem;
|
||||
private ExpandOptionImpl expandOption = null;
|
||||
|
||||
private ExpandTreeBuilderImpl(final ExpandItemImpl parentItem) {
|
||||
this.parentItem = parentItem;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ExpandTreeBuilder expand(final EdmNavigationProperty edmNavigationProperty) {
|
||||
ExpandItemImpl expandItem = buildExpandItem(edmNavigationProperty);
|
||||
|
||||
if (expandOption == null) {
|
||||
expandOption = new ExpandOptionImpl();
|
||||
if(parentItem != null && parentItem.getExpandOption() == null){
|
||||
parentItem.setSystemQueryOption(expandOption);
|
||||
}
|
||||
}
|
||||
expandOption.addExpandItem(expandItem);
|
||||
|
||||
return new ExpandTreeBuilderInner(expandItem);
|
||||
|
||||
ExpandTreeBuilder builder = childBuilderCache.get(edmNavigationProperty.getName());
|
||||
if(builder == null){
|
||||
ExpandItemImpl expandItem = buildExpandItem(edmNavigationProperty);
|
||||
expandOption.addExpandItem(expandItem);
|
||||
builder = new ExpandTreeBuilderImpl(expandItem);
|
||||
childBuilderCache.put(edmNavigationProperty.getName(), builder);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpandOption build() {
|
||||
return expandOption;
|
||||
}
|
||||
|
||||
private class ExpandTreeBuilderInner extends ExpandTreeBuilder {
|
||||
private ExpandItemImpl parent;
|
||||
|
||||
public ExpandTreeBuilderInner(final ExpandItemImpl expandItem) {
|
||||
parent = expandItem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpandTreeBuilder expand(final EdmNavigationProperty edmNavigationProperty) {
|
||||
if (parent.getExpandOption() == null) {
|
||||
final ExpandOptionImpl expandOption = new ExpandOptionImpl();
|
||||
parent.setSystemQueryOption(expandOption);
|
||||
}
|
||||
|
||||
final ExpandItemImpl expandItem = buildExpandItem(edmNavigationProperty);
|
||||
((ExpandOptionImpl) parent.getExpandOption()).addExpandItem(expandItem);
|
||||
|
||||
return new ExpandTreeBuilderInner(expandItem);
|
||||
}
|
||||
|
||||
|
||||
public static ExpandTreeBuilder create(){
|
||||
return new ExpandTreeBuilderImpl(null);
|
||||
}
|
||||
}
|
|
@ -134,7 +134,7 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
throw new DeserializerException("Nested Arrays and primitive values are not allowed for an entity value.",
|
||||
DeserializerException.MessageKeys.INVALID_ENTITY);
|
||||
}
|
||||
EdmEntityType derivedEdmEntityType = (EdmEntityType)getDerivedType(edmEntityType, arrayElement);
|
||||
EdmEntityType derivedEdmEntityType = (EdmEntityType) getDerivedType(edmEntityType, arrayElement);
|
||||
entities.add(consumeEntityNode(derivedEdmEntityType, (ObjectNode) arrayElement, expandBuilder));
|
||||
}
|
||||
return entities;
|
||||
|
@ -149,9 +149,9 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
throws DeserializerException {
|
||||
try {
|
||||
final ObjectNode tree = parseJsonTree(stream);
|
||||
final ExpandTreeBuilderImpl expandBuilder = new ExpandTreeBuilderImpl();
|
||||
final ExpandTreeBuilder expandBuilder = ExpandTreeBuilderImpl.create();
|
||||
|
||||
EdmEntityType derivedEdmEntityType = (EdmEntityType)getDerivedType(edmEntityType, tree);
|
||||
EdmEntityType derivedEdmEntityType = (EdmEntityType) getDerivedType(edmEntityType, tree);
|
||||
|
||||
return DeserializerResultImpl.with().entity(consumeEntityNode(derivedEdmEntityType, tree, expandBuilder))
|
||||
.expandOption(expandBuilder.build())
|
||||
|
@ -279,8 +279,8 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
public Parameter parameter(final String content, final EdmParameter parameter) throws DeserializerException {
|
||||
try {
|
||||
JsonParser parser = new JsonFactory(new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, true))
|
||||
.createParser(content);
|
||||
.configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, true))
|
||||
.createParser(content);
|
||||
JsonNode node = parser.getCodec().readTree(parser);
|
||||
if (node == null) {
|
||||
throw new DeserializerException("Invalid JSON syntax.",
|
||||
|
@ -384,30 +384,29 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
final EdmNavigationProperty edmNavigationProperty) throws DeserializerException {
|
||||
Link link = new Link();
|
||||
link.setTitle(navigationPropertyName);
|
||||
final ExpandTreeBuilder childExpandBuilder = (expandBuilder != null) ?
|
||||
expandBuilder.expand(edmNavigationProperty) : null;
|
||||
EdmEntityType derivedEdmEntityType = (EdmEntityType)getDerivedType(
|
||||
edmNavigationProperty.getType(), jsonNode);
|
||||
if (jsonNode.isArray() && edmNavigationProperty.isCollection()) {
|
||||
link.setType(Constants.ENTITY_SET_NAVIGATION_LINK_TYPE);
|
||||
EntityCollection inlineEntitySet = new EntityCollection();
|
||||
inlineEntitySet.getEntities().addAll(
|
||||
consumeEntitySetArray(derivedEdmEntityType, jsonNode, childExpandBuilder));
|
||||
link.setInlineEntitySet(inlineEntitySet);
|
||||
} else if (!jsonNode.isArray() && (!jsonNode.isValueNode() || jsonNode.isNull())
|
||||
&& !edmNavigationProperty.isCollection()) {
|
||||
link.setType(Constants.ENTITY_NAVIGATION_LINK_TYPE);
|
||||
if (!jsonNode.isNull()) {
|
||||
Entity inlineEntity = consumeEntityNode(derivedEdmEntityType, (ObjectNode) jsonNode,
|
||||
childExpandBuilder);
|
||||
link.setInlineEntity(inlineEntity);
|
||||
}
|
||||
} else {
|
||||
throw new DeserializerException("Invalid value: " + jsonNode.getNodeType()
|
||||
+ " for expanded navigation property: " + navigationPropertyName,
|
||||
MessageKeys.INVALID_VALUE_FOR_NAVIGATION_PROPERTY, navigationPropertyName);
|
||||
}
|
||||
return link;
|
||||
final ExpandTreeBuilder childExpandBuilder = (expandBuilder != null) ? expandBuilder.expand(edmNavigationProperty)
|
||||
: null;
|
||||
EdmEntityType derivedEdmEntityType = (EdmEntityType) getDerivedType(
|
||||
edmNavigationProperty.getType(), jsonNode);
|
||||
if (jsonNode.isArray() && edmNavigationProperty.isCollection()) {
|
||||
link.setType(Constants.ENTITY_SET_NAVIGATION_LINK_TYPE);
|
||||
EntityCollection inlineEntitySet = new EntityCollection();
|
||||
inlineEntitySet.getEntities().addAll(
|
||||
consumeEntitySetArray(derivedEdmEntityType, jsonNode, childExpandBuilder));
|
||||
link.setInlineEntitySet(inlineEntitySet);
|
||||
} else if (!jsonNode.isArray() && (!jsonNode.isValueNode() || jsonNode.isNull())
|
||||
&& !edmNavigationProperty.isCollection()) {
|
||||
link.setType(Constants.ENTITY_NAVIGATION_LINK_TYPE);
|
||||
if (!jsonNode.isNull()) {
|
||||
Entity inlineEntity = consumeEntityNode(derivedEdmEntityType, (ObjectNode) jsonNode, childExpandBuilder);
|
||||
link.setInlineEntity(inlineEntity);
|
||||
}
|
||||
} else {
|
||||
throw new DeserializerException("Invalid value: " + jsonNode.getNodeType()
|
||||
+ " for expanded navigation property: " + navigationPropertyName,
|
||||
MessageKeys.INVALID_VALUE_FOR_NAVIGATION_PROPERTY, navigationPropertyName);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
private Link consumeBindingLink(final String key, final JsonNode jsonNode, final EdmEntityType edmEntityType)
|
||||
|
@ -477,7 +476,7 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
private void consumePropertySingleNode(final String name, final EdmType type,
|
||||
final boolean isNullable, final Integer maxLength, final Integer precision, final Integer scale,
|
||||
final boolean isUnicode, final EdmMapping mapping, final JsonNode jsonNode, final Property property)
|
||||
throws DeserializerException {
|
||||
throws DeserializerException {
|
||||
switch (type.getKind()) {
|
||||
case PRIMITIVE:
|
||||
case DEFINITION:
|
||||
|
@ -504,7 +503,7 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
|
||||
private Object readComplexNode(final String name, final EdmType type, final boolean isNullable,
|
||||
final JsonNode jsonNode)
|
||||
throws DeserializerException {
|
||||
throws DeserializerException {
|
||||
// read and add all complex properties
|
||||
ComplexValue value = readComplexValue(name, type, isNullable, jsonNode);
|
||||
|
||||
|
@ -520,7 +519,7 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
private void consumePropertyCollectionNode(final String name, final EdmType type,
|
||||
final boolean isNullable, final Integer maxLength, final Integer precision, final Integer scale,
|
||||
final boolean isUnicode, final EdmMapping mapping, final JsonNode jsonNode, final Property property)
|
||||
throws DeserializerException {
|
||||
throws DeserializerException {
|
||||
if (!jsonNode.isArray()) {
|
||||
throw new DeserializerException("Value for property: " + name + " must be an array but is not.",
|
||||
DeserializerException.MessageKeys.INVALID_JSON_TYPE_FOR_PROPERTY, name);
|
||||
|
@ -537,10 +536,8 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
isNullable, maxLength, precision, scale, isUnicode, mapping, arrayElement);
|
||||
valueArray.add(value);
|
||||
}
|
||||
property.setValue(type.getKind() == EdmTypeKind.ENUM ?
|
||||
ValueType.COLLECTION_ENUM :
|
||||
ValueType.COLLECTION_PRIMITIVE,
|
||||
valueArray);
|
||||
property.setValue(type.getKind() == EdmTypeKind.ENUM ? ValueType.COLLECTION_ENUM : ValueType.COLLECTION_PRIMITIVE,
|
||||
valueArray);
|
||||
break;
|
||||
case COMPLEX:
|
||||
while (iterator.hasNext()) {
|
||||
|
@ -631,14 +628,10 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
*/
|
||||
private Class<?> getJavaClassForPrimitiveType(final EdmMapping mapping, final EdmPrimitiveType type) {
|
||||
final EdmPrimitiveType edmPrimitiveType =
|
||||
type.getKind() == EdmTypeKind.ENUM ?
|
||||
((EdmEnumType) type).getUnderlyingType() :
|
||||
type.getKind() == EdmTypeKind.DEFINITION ?
|
||||
((EdmTypeDefinition) type).getUnderlyingType() :
|
||||
type;
|
||||
return mapping == null || mapping.getMappedJavaClass() == null ?
|
||||
edmPrimitiveType.getDefaultType() :
|
||||
mapping.getMappedJavaClass();
|
||||
type.getKind() == EdmTypeKind.ENUM ? ((EdmEnumType) type).getUnderlyingType() : type
|
||||
.getKind() == EdmTypeKind.DEFINITION ? ((EdmTypeDefinition) type).getUnderlyingType() : type;
|
||||
return mapping == null || mapping.getMappedJavaClass() == null ? edmPrimitiveType.getDefaultType() : mapping
|
||||
.getMappedJavaClass();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -831,7 +824,7 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
String odataType = odataTypeNode.asText();
|
||||
if (!odataType.isEmpty()) {
|
||||
odataType = odataType.substring(1);
|
||||
|
||||
|
||||
if (odataType.equalsIgnoreCase(edmType.getFullQualifiedName().getFullQualifiedNameAsString())) {
|
||||
return edmType;
|
||||
} else if (this.serviceMetadata == null) {
|
||||
|
@ -839,14 +832,14 @@ public class ODataJsonDeserializer implements ODataDeserializer {
|
|||
"Failed to resolve Odata type " + odataType + " due to metadata is not available",
|
||||
DeserializerException.MessageKeys.UNKNOWN_CONTENT);
|
||||
}
|
||||
|
||||
|
||||
EdmStructuredType currentEdmType = null;
|
||||
if(edmType instanceof EdmEntityType) {
|
||||
if (edmType instanceof EdmEntityType) {
|
||||
currentEdmType = serviceMetadata.getEdm()
|
||||
.getEntityType(new FullQualifiedName(odataType));
|
||||
.getEntityType(new FullQualifiedName(odataType));
|
||||
} else {
|
||||
currentEdmType = serviceMetadata.getEdm()
|
||||
.getComplexType(new FullQualifiedName(odataType));
|
||||
.getComplexType(new FullQualifiedName(odataType));
|
||||
}
|
||||
if (!isAssignable(edmType, currentEdmType)) {
|
||||
throw new DeserializerException(
|
||||
|
|
|
@ -30,11 +30,63 @@ import org.apache.olingo.commons.api.data.Entity;
|
|||
import org.apache.olingo.commons.api.data.Link;
|
||||
import org.apache.olingo.commons.api.format.ContentType;
|
||||
import org.apache.olingo.server.api.deserializer.DeserializerException;
|
||||
import org.apache.olingo.server.api.deserializer.DeserializerResult;
|
||||
import org.apache.olingo.server.api.uri.queryoption.ExpandItem;
|
||||
import org.apache.olingo.server.api.uri.queryoption.ExpandOption;
|
||||
import org.apache.olingo.server.core.deserializer.AbstractODataDeserializerTest;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ODataDeserializerDeepInsertTest extends AbstractODataDeserializerTest {
|
||||
|
||||
@Test
|
||||
public void unbalancedESAllPrim() throws Exception {
|
||||
final DeserializerResult result = deserializeWithResult("UnbalancedESAllPrimFeed.json");
|
||||
ExpandOption root = result.getExpandTree();
|
||||
assertEquals(1, root.getExpandItems().size());
|
||||
|
||||
ExpandItem etTwoPrimManyLevel = root.getExpandItems().get(0);
|
||||
assertEquals("NavPropertyETTwoPrimMany", etTwoPrimManyLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertEquals(1, etTwoPrimManyLevel.getExpandOption().getExpandItems().size());
|
||||
|
||||
ExpandItem etAllPrimOneLevel = etTwoPrimManyLevel.getExpandOption().getExpandItems().get(0);
|
||||
assertEquals("NavPropertyETAllPrimOne", etAllPrimOneLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertEquals(1, etAllPrimOneLevel.getExpandOption().getExpandItems().size());
|
||||
|
||||
ExpandItem etTwoPrimOneLevel = etAllPrimOneLevel.getExpandOption().getExpandItems().get(0);
|
||||
assertEquals("NavPropertyETTwoPrimOne", etTwoPrimOneLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertNull(etTwoPrimOneLevel.getExpandOption());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unbalancedESAllPrim2() throws Exception {
|
||||
final DeserializerResult result = deserializeWithResult("UnbalancedESAllPrimFeed2.json");
|
||||
ExpandOption root = result.getExpandTree();
|
||||
assertEquals(1, root.getExpandItems().size());
|
||||
|
||||
ExpandItem etTwoPrimManyLevel = root.getExpandItems().get(0);
|
||||
assertEquals("NavPropertyETTwoPrimMany", etTwoPrimManyLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertEquals(1, etTwoPrimManyLevel.getExpandOption().getExpandItems().size());
|
||||
|
||||
ExpandItem etAllPrimOneLevel = etTwoPrimManyLevel.getExpandOption().getExpandItems().get(0);
|
||||
assertEquals("NavPropertyETAllPrimOne", etAllPrimOneLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertEquals(2, etAllPrimOneLevel.getExpandOption().getExpandItems().size());
|
||||
|
||||
ExpandItem etTwoPrimOneLevel = etAllPrimOneLevel.getExpandOption().getExpandItems().get(0);
|
||||
assertEquals("NavPropertyETTwoPrimMany", etTwoPrimOneLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertNull(etTwoPrimOneLevel.getExpandOption());
|
||||
|
||||
etTwoPrimOneLevel = etAllPrimOneLevel.getExpandOption().getExpandItems().get(1);
|
||||
assertEquals("NavPropertyETTwoPrimOne", etTwoPrimOneLevel.getResourcePath().getUriResourceParts().get(0)
|
||||
.getSegmentValue());
|
||||
assertNull(etTwoPrimOneLevel.getExpandOption());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void esAllPrimExpandedToOne() throws Exception {
|
||||
final Entity entity = deserialize("EntityESAllPrimExpandedNavPropertyETTwoPrimOne.json");
|
||||
|
@ -152,4 +204,10 @@ public class ODataDeserializerDeepInsertTest extends AbstractODataDeserializerTe
|
|||
return ODataJsonDeserializerEntityTest.deserialize(getFileAsStream(resourceName),
|
||||
"ETAllPrim", ContentType.JSON);
|
||||
}
|
||||
|
||||
private DeserializerResult deserializeWithResult(final String resourceName) throws IOException,
|
||||
DeserializerException {
|
||||
return ODataJsonDeserializerEntityTest.deserializeWithResult(getFileAsStream(resourceName),
|
||||
"ETAllPrim", ContentType.JSON);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.apache.olingo.commons.api.format.ContentType;
|
|||
import org.apache.olingo.commons.core.edm.primitivetype.EdmDate;
|
||||
import org.apache.olingo.server.api.OData;
|
||||
import org.apache.olingo.server.api.deserializer.DeserializerException;
|
||||
import org.apache.olingo.server.api.deserializer.DeserializerResult;
|
||||
import org.apache.olingo.server.api.deserializer.ODataDeserializer;
|
||||
import org.apache.olingo.server.core.deserializer.AbstractODataDeserializerTest;
|
||||
import org.junit.Assert;
|
||||
|
@ -1347,6 +1348,12 @@ public class ODataJsonDeserializerEntityTest extends AbstractODataDeserializerTe
|
|||
.entity(stream, edm.getEntityType(new FullQualifiedName(NAMESPACE, entityTypeName)))
|
||||
.getEntity();
|
||||
}
|
||||
|
||||
protected static DeserializerResult deserializeWithResult(final InputStream stream, final String entityTypeName,
|
||||
final ContentType contentType) throws DeserializerException {
|
||||
return OData.newInstance().createDeserializer(contentType, metadata)
|
||||
.entity(stream, edm.getEntityType(new FullQualifiedName(NAMESPACE, entityTypeName)));
|
||||
}
|
||||
|
||||
private static Entity deserialize(final String entityString, final String entityTypeName,
|
||||
final ContentType contentType) throws DeserializerException {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"@odata.context": "$metadata#ESAllPrim\/$entity",
|
||||
"@odata.metadataEtag": "W\/\"4efd6576-89c0-487c-8d6c-584e2acbae16\"",
|
||||
"PropertyInt16": 1,
|
||||
"NavPropertyETTwoPrimMany": [
|
||||
{
|
||||
"PropertyInt16": 2,
|
||||
"NavPropertyETAllPrimOne": {
|
||||
"PropertyInt16": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"PropertyInt16": 2,
|
||||
"NavPropertyETAllPrimOne": {
|
||||
"PropertyInt16": 3,
|
||||
"NavPropertyETTwoPrimOne": {
|
||||
"PropertyInt16": 32766,
|
||||
"PropertyString": "Innermost Entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"@odata.context": "$metadata#ESAllPrim\/$entity",
|
||||
"@odata.metadataEtag": "W\/\"4efd6576-89c0-487c-8d6c-584e2acbae16\"",
|
||||
"PropertyInt16": 1,
|
||||
"NavPropertyETTwoPrimMany": [
|
||||
{
|
||||
"PropertyInt16": 2,
|
||||
"NavPropertyETAllPrimOne": {
|
||||
"PropertyInt16": 3,
|
||||
"NavPropertyETTwoPrimMany": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"PropertyInt16": 2,
|
||||
"NavPropertyETAllPrimOne": {
|
||||
"PropertyInt16": 3,
|
||||
"NavPropertyETTwoPrimOne": {
|
||||
"PropertyInt16": 32766,
|
||||
"PropertyString": "Innermost Entry"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue