com.sdl.odata.renderer.json.writer.JsonWriter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of odata_renderer Show documentation
Show all versions of odata_renderer Show documentation
Tridion OData Framework Renderer Implementation
The newest version!
/**
* Copyright (c) 2014-2024 All Rights Reserved by the RWS Group for and on behalf of its affiliates and subsidiaries.
*
* Licensed 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 com.sdl.odata.renderer.json.writer;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.sdl.odata.api.edm.ODataEdmException;
import com.sdl.odata.api.edm.model.EntityDataModel;
import com.sdl.odata.api.edm.model.EntitySet;
import com.sdl.odata.api.edm.model.EntityType;
import com.sdl.odata.api.edm.model.EnumType;
import com.sdl.odata.api.edm.model.NavigationProperty;
import com.sdl.odata.api.edm.model.PrimitiveType;
import com.sdl.odata.api.edm.model.StructuralProperty;
import com.sdl.odata.api.edm.model.StructuredType;
import com.sdl.odata.api.edm.model.Type;
import com.sdl.odata.api.edm.model.TypeDefinition;
import com.sdl.odata.api.parser.ODataUri;
import com.sdl.odata.api.renderer.ODataRenderException;
import com.sdl.odata.renderer.json.util.JsonWriterUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static com.sdl.odata.JsonConstants.CONTEXT;
import static com.sdl.odata.JsonConstants.COUNT;
import static com.sdl.odata.JsonConstants.ID;
import static com.sdl.odata.JsonConstants.TYPE;
import static com.sdl.odata.JsonConstants.VALUE;
import static com.sdl.odata.ODataRendererUtils.checkNotNull;
import static com.sdl.odata.ODataRendererUtils.isForceExpandParamSet;
import static com.sdl.odata.api.edm.model.MetaType.COMPLEX;
import static com.sdl.odata.api.edm.model.MetaType.ENTITY;
import static com.sdl.odata.api.parser.ODataUriUtil.asJavaList;
import static com.sdl.odata.api.parser.ODataUriUtil.getSimpleExpandPropertyNames;
import static com.sdl.odata.api.parser.ODataUriUtil.hasCountOption;
import static com.sdl.odata.util.edm.EntityDataModelUtil.formatEntityKey;
import static com.sdl.odata.util.edm.EntityDataModelUtil.getEntityName;
import static com.sdl.odata.util.edm.EntityDataModelUtil.visitProperties;
/**
* Writer capable of creating a JSON stream containing either
* a single entity (entry) or a list of OData V4 entities (feed).
*/
public class JsonWriter {
private static final Logger LOG = LoggerFactory.getLogger(JsonWriter.class);
private static final JsonFactory JSON_FACTORY = new JsonFactory();
private JsonGenerator jsonGenerator;
private final ODataUri odataUri;
private final EntityDataModel entityDataModel;
private EntitySet entitySet;
private List expandedProperties = new ArrayList<>();
private String contextURL = null;
private final boolean forceExpand;
/**
* Create an OData JSON Writer.
*
* @param oDataUri The OData parsed URI. It can not be {@code null}.
* @param entityDataModel The Entity Data Model (EDM). It can not be {@code null}.
*/
public JsonWriter(ODataUri oDataUri, EntityDataModel entityDataModel) {
this.odataUri = checkNotNull(oDataUri);
this.entityDataModel = checkNotNull(entityDataModel);
expandedProperties.addAll(asJavaList(getSimpleExpandPropertyNames(oDataUri)));
forceExpand = isForceExpandParamSet(odataUri);
}
/**
* Write a list of entities (feed) to the JSON stream.
*
* @param entities The list of entities to fill in the JSON stream.
* @param contextUrl The 'Context URL' to write.
* @param meta Additional metadata for the writer.
* @return the rendered feed.
* @throws ODataRenderException In case it is not possible to write to the JSON stream.
*/
public String writeFeed(List> entities, String contextUrl, Map meta)
throws ODataRenderException {
this.contextURL = checkNotNull(contextUrl);
try {
return writeJson(entities, meta);
} catch (IOException | IllegalAccessException | NoSuchFieldException
| ODataEdmException | ODataRenderException e) {
LOG.error("Not possible to marshall feed stream JSON");
throw new ODataRenderException("Not possible to marshall feed stream JSON: ", e);
}
}
/**
* Write a single entity (entry) to the JSON stream.
*
* @param entity The entity to fill in the JSON stream. It can not be {@code null}.
* @param contextUrl The 'Context URL' to write. It can not be {@code null}.
* @return the rendered entry
* @throws ODataRenderException In case it is not possible to write to the JSON stream.
*/
public String writeEntry(Object entity, String contextUrl) throws ODataRenderException {
this.contextURL = checkNotNull(contextUrl);
try {
return writeJson(entity, null);
} catch (IOException | IllegalAccessException | NoSuchFieldException |
ODataEdmException | ODataRenderException e) {
LOG.error("Not possible to marshall single entity stream JSON");
throw new ODataRenderException("Not possible to marshall single entity stream JSON: ", e);
}
}
/**
* Writes raw json to the JSON stream.
*
* @param json JSON to write
* @param contextUrl context URL
* @return JSON result
* @throws ODataRenderException OData render exception
*/
public String writeRawJson(final String json, final String contextUrl) throws ODataRenderException {
this.contextURL = checkNotNull(contextUrl);
try {
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
jsonGenerator = JSON_FACTORY.createGenerator(stream, JsonEncoding.UTF8);
jsonGenerator.writeRaw(json);
jsonGenerator.close();
return stream.toString(StandardCharsets.UTF_8.name());
} catch (final IOException e) {
throw new ODataRenderException("Not possible to write raw json to stream JSON: ", e);
}
}
/**
* Write the given data to the JSON stream. The data to write will be either a single entity or a feed depending on
* whether it is a single object or list.
*
* @param data The given data.
* @param meta Additional values to write.
* @return The written JSON stream.
* @throws ODataRenderException if unable to render
*/
private String writeJson(Object data, Map meta) throws IOException, NoSuchFieldException,
IllegalAccessException, ODataEdmException, ODataRenderException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
jsonGenerator = JSON_FACTORY.createGenerator(stream, JsonEncoding.UTF8);
jsonGenerator.writeStartObject();
// Write @odata constants
entitySet = (data instanceof List) ? getEntitySet((List>) data) : getEntitySet(data);
jsonGenerator.writeStringField(CONTEXT, contextURL);
// Write @odata.count if requested and provided.
if (hasCountOption(odataUri) && data instanceof List &&
meta != null && meta.containsKey("count")) {
long count;
Object countObj = meta.get("count");
if (countObj instanceof Integer) {
count = ((Integer) countObj).longValue();
} else {
count = (long) countObj;
}
jsonGenerator.writeNumberField(COUNT, count);
}
if (!(data instanceof List)) {
if (entitySet != null) {
jsonGenerator.writeStringField(ID, String.format("%s(%s)", getEntityName(entityDataModel, data),
formatEntityKey(entityDataModel, data)));
} else {
jsonGenerator.writeStringField(ID, String.format("%s", getEntityName(entityDataModel, data)));
}
}
// Write feed
if (data instanceof List) {
marshallEntities((List>) data);
} else {
marshall(data, this.entityDataModel.getType(data.getClass()));
}
jsonGenerator.writeEndObject();
jsonGenerator.close();
return stream.toString(StandardCharsets.UTF_8.name());
}
private void marshallEntities(List> entities) throws IOException,
ODataRenderException, ODataEdmException, NoSuchFieldException, IllegalAccessException {
jsonGenerator.writeArrayFieldStart(VALUE);
for (Object entity : entities) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(ID, String.format("%s(%s)", getEntityName(entityDataModel, entity),
formatEntityKey(entityDataModel, entity)));
marshall(entity, entityDataModel.getType(entity.getClass()));
jsonGenerator.writeEndObject();
}
jsonGenerator.writeEndArray();
}
private void marshall(Object object, Type type)
throws IOException, ODataRenderException, NoSuchFieldException, IllegalAccessException {
// Decide what to do depending on what kind of type this is
switch (type.getMetaType()) {
case ABSTRACT:
throw new UnsupportedOperationException("Marshalling abstract OData types is not supported");
case PRIMITIVE:
marshallPrimitive(object, (PrimitiveType) type);
break;
case ENTITY:
case COMPLEX:
marshallStructured(object, (StructuredType) type);
break;
case ENUM:
marshallEnum(object, (EnumType) type);
break;
case TYPE_DEFINITION:
marshallPrimitive(object, ((TypeDefinition) type).getUnderlyingType());
break;
default:
throw new UnsupportedOperationException("Unsupported type: " + type);
}
}
private void marshallPrimitive(Object value, PrimitiveType primitiveType) throws IOException {
LOG.trace("Primitive value: {} of type: {}", value, primitiveType);
if (value != null) {
JsonWriterUtil.writePrimitiveValue(value, jsonGenerator);
} else {
jsonGenerator.writeNull();
}
}
private void marshallStructured(final Object object, StructuredType structuredType)
throws ODataRenderException, IOException, NoSuchFieldException, IllegalAccessException {
LOG.trace("Start structured value of type: {}", structuredType);
if (object != null) {
writeODataType(structuredType);
visitProperties(entityDataModel, structuredType, property -> {
try {
if (property instanceof NavigationProperty) {
LOG.trace("Start marshalling navigation property: {}", property.getName());
NavigationProperty navProperty = (NavigationProperty) property;
if (forceExpand || isExpandedProperty(navProperty)) {
final Object value = getValueFromProperty(object, navProperty);
if (value != null) {
if (navProperty.isCollection()) {
jsonGenerator.writeArrayFieldStart(navProperty.getName());
for (Object propertyValue : (Collection>) value) {
jsonGenerator.writeStartObject();
marshall(propertyValue, entityDataModel.getType(propertyValue.getClass()));
jsonGenerator.writeEndObject();
}
jsonGenerator.writeEndArray();
} else {
jsonGenerator.writeObjectFieldStart(navProperty.getName());
marshall(value, entityDataModel.getType(value.getClass()));
jsonGenerator.writeEndObject();
}
}
}
LOG.trace("Navigation property: {} marshalled", property.getName());
} else {
LOG.trace("Started marshalling property: {}", property.getName());
marshallStructuralProperty(object, property);
LOG.trace("Property: {} marshalled", property.getName());
}
} catch (IOException | IllegalAccessException | NoSuchFieldException e) {
throw new ODataRenderException("Error while writing property: " + property.getName(), e);
}
});
} else {
jsonGenerator.writeNull();
LOG.trace("Structured value is null");
}
LOG.trace("End structured value of type: {}", structuredType);
}
/**
* This methods write @odata.type of complex type.
* If complex type has root-level, @odata.type won't be written.
*
* @param structuredType structuredType
*/
private void writeODataType(StructuredType structuredType) throws IOException {
if (entitySet != null) {
String typeName = entitySet.getTypeName();
String type = typeName.substring(typeName.lastIndexOf(".") + 1, typeName.length());
if (!type.equals(structuredType.getName())) {
jsonGenerator.writeStringField(TYPE, String.format("#%s.%s",
structuredType.getNamespace(), structuredType.getName()));
} else {
LOG.trace("{} has root level. {} won't be written here", entitySet.getName(), TYPE);
}
}
}
private void marshallStructuralProperty(Object object, StructuralProperty property)
throws ODataRenderException, IOException, NoSuchFieldException, IllegalAccessException {
String propertyName = property.getName();
// Get the property value through reflection
Object propertyValue;
Field field = property.getJavaField();
try {
field.setAccessible(true);
propertyValue = field.get(object);
} catch (IllegalAccessException e) {
LOG.error("Error getting field value of field: " + field.toGenericString());
throw new ODataRenderException("Error getting field value of field: " + field.toGenericString());
}
// Collection properties and non-nullable properties should not be null
if (propertyValue == null) {
if (property.isCollection()) {
throw new ODataRenderException("Collection property has null value: " + property);
} else if (!property.isNullable()) {
throw new ODataRenderException("Non-nullable property has null value: " + property);
}
}
// Check if the property is a collection
if (property.isCollection()) {
// Get an iterator for the array or collection
Iterator> iterator;
if (propertyValue.getClass().isArray()) {
iterator = Arrays.asList((Object[]) propertyValue).iterator();
} else if (Collection.class.isAssignableFrom(propertyValue.getClass())) {
iterator = ((Collection>) propertyValue).iterator();
} else {
throw new UnsupportedOperationException("Unsupported collection type: " +
propertyValue.getClass().getName() + " for property: " + propertyName);
}
// Get the OData type of the elements of the collection
Type elementType = entityDataModel.getType(property.getElementTypeName());
if (elementType == null) {
throw new ODataRenderException("OData type not found for elements of property: " + property);
}
LOG.trace("Start collection property: {}", propertyName);
if (((Collection) propertyValue).isEmpty()) {
jsonGenerator.writeArrayFieldStart(propertyName);
jsonGenerator.writeEndArray();
} else {
while (iterator.hasNext()) {
Object element = iterator.next();
if (element instanceof Number | element instanceof String | element.getClass().isEnum()) {
marshallToArray(propertyName, element, iterator);
} else {
marshallCollection(propertyName, iterator, element, elementType);
}
}
}
LOG.trace("End collection property: {}", propertyName);
} else {
// Single value (non-collection) property
LOG.trace("Start property: {}", propertyName);
// Get the OData type of the property
Type propertyType = entityDataModel.getType(property.getTypeName());
if (propertyType == null) {
throw new ODataRenderException("OData type not found for property: " + property);
}
jsonGenerator.writeFieldName(propertyName);
if (propertyType.getMetaType().equals(COMPLEX) && propertyValue != null) {
jsonGenerator.writeStartObject();
}
marshall(propertyValue, propertyType);
if (propertyType.getMetaType().equals(COMPLEX) && propertyValue != null) {
jsonGenerator.writeEndObject();
}
LOG.trace("End property: {}", propertyName);
}
}
private void marshallCollection(String propertyName, Iterator> iterator, Object first, Type elementType)
throws IOException, ODataRenderException, NoSuchFieldException, IllegalAccessException {
jsonGenerator.writeArrayFieldStart(propertyName);
jsonGenerator.writeStartObject();
marshall(first, elementType);
jsonGenerator.writeEndObject();
while (iterator.hasNext()) {
Object element = iterator.next();
jsonGenerator.writeStartObject();
marshall(element, elementType);
jsonGenerator.writeEndObject();
}
jsonGenerator.writeEndArray();
}
private void marshallToArray(String propertyName, Object first, Iterator> iterator) throws IOException {
jsonGenerator.writeArrayFieldStart(propertyName);
jsonGenerator.writeObject(first.toString());
while (iterator.hasNext()) {
Object element = iterator.next();
jsonGenerator.writeObject(element.toString());
}
jsonGenerator.writeEndArray();
}
/**
* Marshall an enum value.
*
* @param value The value to marshall. Can be {@code null}.
* @param enumType The OData enum type.
*/
private void marshallEnum(Object value, EnumType enumType) throws IOException {
LOG.trace("Enum value: {} of type: {}", value, enumType);
jsonGenerator.writeString(value.toString());
}
private boolean isExpandedProperty(NavigationProperty property) {
return expandedProperties.contains(property.getName());
}
private Object getValueFromProperty(Object entity, NavigationProperty property)
throws NoSuchFieldException, IllegalAccessException {
Field propertyField = property.getJavaField();
propertyField.setAccessible(true);
return propertyField.get(entity);
}
private EntitySet getEntitySet(Object entity) {
String entityTypeName = getEntityType(entity).getFullyQualifiedName();
for (EntitySet eSet : entityDataModel.getEntityContainer().getEntitySets()) {
if (eSet.getTypeName().equals(entityTypeName)) {
return eSet;
}
}
return null;
}
private EntitySet getEntitySet(List> entityList) {
return entityList.size() > 0 ? getEntitySet(entityList.get(0)) : null;
}
private EntityType getEntityType(Object entity) {
final Type type = entityDataModel.getType(entity.getClass());
if (type.getMetaType() != ENTITY) {
throw new UnsupportedOperationException("Unsupported type: " + type);
}
return (EntityType) type;
}
}