All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sdl.odata.renderer.atom.writer.AtomWriter Maven / Gradle / Ivy

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.atom.writer;

import com.sdl.odata.api.ODataSystemException;
import com.sdl.odata.api.edm.ODataEdmException;
import com.sdl.odata.api.edm.model.EntityDataModel;
import com.sdl.odata.api.edm.model.EntityType;
import com.sdl.odata.api.edm.model.NavigationProperty;
import com.sdl.odata.api.edm.model.StructuralProperty;
import com.sdl.odata.api.parser.ODataUri;
import com.sdl.odata.api.parser.ODataUriUtil;
import com.sdl.odata.api.renderer.ODataRenderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static com.sdl.odata.AtomConstants.ATOM_ENTRY;
import static com.sdl.odata.AtomConstants.ATOM_FEED;
import static com.sdl.odata.AtomConstants.ATOM_LINK;
import static com.sdl.odata.AtomConstants.HREF;
import static com.sdl.odata.AtomConstants.ID;
import static com.sdl.odata.AtomConstants.INLINE;
import static com.sdl.odata.AtomConstants.METADATA;
import static com.sdl.odata.AtomConstants.ODATA_ASSOCIATION_LINK_REL_NS_PREFIX;
import static com.sdl.odata.AtomConstants.ODATA_DATA;
import static com.sdl.odata.AtomConstants.ODATA_ENTRY_LINK_TYPE_PATTERN;
import static com.sdl.odata.AtomConstants.ODATA_FEED_LINK_TYPE_PATTERN;
import static com.sdl.odata.AtomConstants.ODATA_NAVIGATION_LINK_REL_NS_PREFIX;
import static com.sdl.odata.AtomConstants.REF;
import static com.sdl.odata.AtomConstants.REL;
import static com.sdl.odata.AtomConstants.TITLE;
import static com.sdl.odata.AtomConstants.TYPE;
import static com.sdl.odata.AtomConstants.XML_VERSION;
import static com.sdl.odata.ODataRendererUtils.checkNotNull;
import static com.sdl.odata.ODataRendererUtils.isForceExpandParamSet;
import static com.sdl.odata.api.parser.ODataUriUtil.asJavaList;
import static com.sdl.odata.api.parser.ODataUriUtil.getSimpleExpandPropertyNames;
import static com.sdl.odata.api.service.MediaType.ATOM_XML;
import static com.sdl.odata.api.service.MediaType.XML;
import static com.sdl.odata.util.edm.EntityDataModelUtil.formatEntityKey;
import static com.sdl.odata.util.edm.EntityDataModelUtil.getAndCheckEntityType;
import static com.sdl.odata.util.edm.EntityDataModelUtil.getEntityName;
import static com.sdl.odata.util.edm.EntityDataModelUtil.getPropertyValue;
import static com.sdl.odata.util.edm.EntityDataModelUtil.isSingletonEntity;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Writer capable of creating an Atom XML stream containing either a single entity (entry) or a list of OData V4
 * entities (feed).
 */
public class AtomWriter {

    private static final Logger LOG = LoggerFactory.getLogger(AtomWriter.class);
    private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance();

    private XMLStreamWriter xmlWriter = null;
    private OutputStream outputStream = null;
    private AtomMetadataWriter metadataWriter = null;
    private AtomDataWriter dataWriter = null;
    private final ZonedDateTime dateTime;
    private final ODataUri oDataUri;
    private final EntityDataModel entityDataModel;
    private final AtomNSConfigurationProvider nsConfigurationProvider;
    // Note: At the moment only a list of comma-separated properties are supported in the $expand operation
    private final List expandedProperties = new ArrayList<>();
    private String contextURL;
    private final boolean isWriteOperation;
    private final boolean isDeepInsert;
    private final boolean isActionCall;
    private final boolean forceExpand;

    /**
     * Creates an instance of {@link AtomWriter} specifying the local date and time to stamp in the XML to write.
     *
     * @param dateTime                The given date and time. It can not be {@code null}.
     * @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}.
     * @param nsConfigurationProvider The configuration provider for namespaces
     * @param isWriteOperation        True if this is a write operation or false if its a read operation
     * @param isActionCall            True if this is a action call
     */
    public AtomWriter(ZonedDateTime dateTime, ODataUri oDataUri, EntityDataModel entityDataModel,
                      AtomNSConfigurationProvider nsConfigurationProvider,
                      boolean isWriteOperation, boolean isActionCall) {
        this(dateTime, oDataUri, entityDataModel, nsConfigurationProvider, isWriteOperation, isActionCall, false);
    }

    /**
     * Creates an instance of {@link AtomWriter} specifying the local date and time to stamp in the XML to write.
     *
     * @param dateTime                The given date and time. It can not be {@code null}.
     * @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}.
     * @param nsConfigurationProvider The configuration provider for namespaces
     * @param isWriteOperation        True if this is a write operation or false if its a read operation
     * @param isActionCall            True if this is a action call
     * @param isDeepInsert            True if this is a deep insert
     */
    public AtomWriter(ZonedDateTime dateTime, ODataUri oDataUri, EntityDataModel entityDataModel,
                      AtomNSConfigurationProvider nsConfigurationProvider,
                      boolean isWriteOperation, boolean isActionCall, boolean isDeepInsert) {

        this.dateTime = checkNotNull(dateTime);
        this.oDataUri = checkNotNull(oDataUri);
        this.entityDataModel = checkNotNull(entityDataModel);
        this.isWriteOperation = checkNotNull(isWriteOperation);
        this.nsConfigurationProvider = checkNotNull(nsConfigurationProvider);
        this.isDeepInsert = isDeepInsert;
        this.isActionCall = isActionCall;

        expandedProperties.addAll(asJavaList(getSimpleExpandPropertyNames(oDataUri)));
        forceExpand = isForceExpandParamSet(oDataUri);
    }

    /**
     * Start the XML stream document by defining things like the type of encoding, and prefixes used. It needs to be
     * used before calling any write method.
     *
     * @throws ODataRenderException if unable to render the feed
     */
    public void startDocument() throws ODataRenderException {
        startDocument(new ByteArrayOutputStream());
    }

    /**
     * Start the XML stream document by defining things like the type of encoding, and prefixes used. It needs to be
     * used before calling any write method.
     *
     * @param os {@link OutputStream} to write to.
     * @throws ODataRenderException if unable to render the feed
     */
    public void startDocument(OutputStream os) throws ODataRenderException {
        try {
            outputStream = os;
            xmlWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(os, UTF_8.name());
            metadataWriter = new AtomMetadataWriter(xmlWriter, oDataUri, entityDataModel, nsConfigurationProvider);
            dataWriter = new AtomDataWriter(xmlWriter, entityDataModel, nsConfigurationProvider);
            xmlWriter.writeStartDocument(UTF_8.name(), XML_VERSION);
            xmlWriter.setPrefix(METADATA, nsConfigurationProvider.getOdataMetadataNs());
            xmlWriter.setPrefix(ODATA_DATA, nsConfigurationProvider.getOdataDataNs());
        } catch (XMLStreamException e) {
            LOG.error("Not possible to start stream XML", e);
            throw new ODataRenderException("Not possible to start stream XML: ", e);
        }
    }

    /**
     * End the XML stream document.
     *
     * @throws ODataRenderException if unable to render
     */
    public void endDocument() throws ODataRenderException {
        endDocument(true);
    }

    /**
     * End the XML stream document.
     *
     * @param flush flush result flag
     * @throws ODataRenderException if unable to render
     */
    public void endDocument(boolean flush) throws ODataRenderException {
        try {
            xmlWriter.writeEndDocument();
            if (flush) {
                xmlWriter.flush();
            }
        } catch (XMLStreamException e) {
            LOG.error("Not possible to end stream XML", e);
            throw new ODataRenderException("Not possible to end stream XML: ", e);
        }
    }

    /**
     * 

Write a list of entities (feed) to the XML stream.

Note: Make sure {@link * AtomWriter#startDocument()} has been previously invoked to start the XML stream document, and {@link * AtomWriter#endDocument()} is invoked after to end it.

* * @param entities The list of entities to fill in the XML stream. It can not {@code null}. * @param requestContextURL The 'Context URL' to write for the feed. It can not {@code null}. * @param meta Additional metadata to write. * @throws ODataRenderException In case it is not possible to write to the XML stream. */ public void writeFeed(List entities, String requestContextURL, Map meta) throws ODataRenderException { writeStartFeed(requestContextURL, meta); writeBodyFeed(entities); writeEndFeed(); } /** * Write start feed to the XML stream. * * @param requestContextURL The 'Context URL' to write for the feed. It can not {@code null}. * @param meta Additional metadata to write. * @throws ODataRenderException In case it is not possible to write to the XML stream. */ public void writeStartFeed(String requestContextURL, Map meta) throws ODataRenderException { this.contextURL = checkNotNull(requestContextURL); try { startFeed(false); if (ODataUriUtil.hasCountOption(oDataUri) && meta != null && meta.containsKey("count")) { metadataWriter.writeCount(meta.get("count")); } metadataWriter.writeFeedId(null, null); metadataWriter.writeTitle(); metadataWriter.writeUpdate(dateTime); metadataWriter.writeFeedLink(null, null); } catch (XMLStreamException | ODataEdmException e) { LOG.error("Not possible to marshall feed stream XML", e); throw new ODataRenderException("Not possible to marshall feed stream XML: ", e); } } /** * Write feed body. * * @param entities The list of entities to fill in the XML stream. It can not {@code null}. * @throws ODataRenderException In case it is not possible to write to the XML stream. */ public void writeBodyFeed(List entities) throws ODataRenderException { checkNotNull(entities); try { for (Object entity : entities) { writeEntry(entity, true); } } catch (XMLStreamException | IllegalAccessException | NoSuchFieldException | ODataEdmException e) { LOG.error("Not possible to marshall feed stream XML", e); throw new ODataRenderException("Not possible to marshall feed stream XML: ", e); } } /** * Write end feed. * * @throws ODataRenderException In case it is not possible to write to the XML stream. */ public void writeEndFeed() throws ODataRenderException { try { endFeed(); } catch (XMLStreamException e) { LOG.error("Not possible to marshall feed stream XML", e); throw new ODataRenderException("Not possible to marshall feed stream XML: ", e); } } /** *

Write a single entity to the XML stream.

Note: Make sure {@link AtomWriter#startDocument()} * has been previously invoked to start the XML stream document, and {@link AtomWriter#endDocument()} is invoked * after to end it.

* * @param entity The entity to fill in the XML stream. It can not be {@code null}. * @param requestContextURL The 'Context URL' to write for the feed. It can not {@code null}. * @throws ODataRenderException In case it is not possible to write to the XML stream. */ public void writeEntry(Object entity, String requestContextURL) throws ODataRenderException { checkNotNull(entity); this.contextURL = checkNotNull(requestContextURL); try { writeEntry(entity, false); } catch (XMLStreamException | IllegalAccessException | NoSuchFieldException | ODataEdmException e) { LOG.error("Not possible to render single entity stream XML", e); throw new ODataRenderException("Not possible to render single entity stream XML: ", e); } } /** * Get the generated XML. * * @return The generated XML. */ public String getXml() { try { return ((ByteArrayOutputStream) outputStream).toString(StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new ODataSystemException(e); } } /** *

Write a list of entities (feed) to the XML stream.

The entities to write will be either the main * content of the feed XML stream, or in-lined as part of the content of navigation property (relevant for the * $expand operation).

* * @param entities The list of entities to fill in the XML stream. * @param enclosingEntity Entity that enclose this list of entities. * If it is null, it implies that the feed is at the root. * @param property The NavigationProperty for which the feed is generated. * @param meta Additional metadata to write. * @throws XMLStreamException if unable to render the feed * @throws ODataRenderException if unable to render the feed * @throws NoSuchFieldException if unable to render the feed * @throws IllegalAccessException if unable to render the feed */ private void writeFeed(Collection entities, Object enclosingEntity, NavigationProperty property, Map meta) throws XMLStreamException, ODataRenderException, NoSuchFieldException, IllegalAccessException, ODataEdmException { final boolean isInlineFeed = (enclosingEntity != null); startFeed(isInlineFeed); if (ODataUriUtil.hasCountOption(oDataUri) && meta != null && meta.containsKey("count")) { metadataWriter.writeCount(meta.get("count")); } metadataWriter.writeFeedId(enclosingEntity, property); metadataWriter.writeTitle(); metadataWriter.writeUpdate(dateTime); metadataWriter.writeFeedLink(enclosingEntity, property); for (Object entity : entities) { writeEntry(entity, true); } endFeed(); } private void writeEntry(Object entity, boolean isFeedEntry) throws XMLStreamException, ODataRenderException, NoSuchFieldException, IllegalAccessException, ODataEdmException { EntityType entityType = getAndCheckEntityType(entityDataModel, entity.getClass()); startEntry(isFeedEntry); metadataWriter.writeEntryId(entity); metadataWriter.writeTitle(); metadataWriter.writeSummary(); metadataWriter.writeUpdate(dateTime); metadataWriter.writeAuthor(); metadataWriter.writeEntryEntityLink(entity); for (StructuralProperty property : entityType.getStructuralProperties()) { if (property instanceof NavigationProperty) { // Nullable navigation properties that have null values should not be included in the output of writes if (isWriteOperation) { final Object value = getPropertyValue(property, entity); if (value != null) { NavigationProperty navigationProperty = (NavigationProperty) property; writeEntryPropertyLink(entity, navigationProperty); } } else { NavigationProperty navigationProperty = (NavigationProperty) property; writeEntryPropertyLink(entity, navigationProperty); } } } metadataWriter.writeEntryCategory(entity); // Note Iterate through all the entity properties in order to write elements of type // (including nested entities) dataWriter.writeData(entity, entityType); endEntry(); } private void startFeed(boolean isInline) throws XMLStreamException { xmlWriter.writeStartElement(ATOM_FEED); if (!isInline) { metadataWriter.writeODataMetadata(contextURL); } } private void endFeed() throws XMLStreamException { xmlWriter.writeEndElement(); } private void startEntry(boolean isFeedEntry) throws XMLStreamException { xmlWriter.writeStartElement(ATOM_ENTRY); if (!isFeedEntry) { metadataWriter.writeODataMetadata(contextURL); } } private void endEntry() throws XMLStreamException { xmlWriter.writeEndElement(); } private void writeEntryPropertyLink(Object entity, NavigationProperty property) throws XMLStreamException, ODataRenderException, NoSuchFieldException, IllegalAccessException, ODataEdmException { String linkType = property.isCollection() ? ODATA_FEED_LINK_TYPE_PATTERN : ODATA_ENTRY_LINK_TYPE_PATTERN; // The navigation link startLink(); xmlWriter.writeAttribute(REL, ODATA_NAVIGATION_LINK_REL_NS_PREFIX + property.getName()); xmlWriter.writeAttribute(TYPE, String.format(linkType, ATOM_XML.toString())); xmlWriter.writeAttribute(TITLE, property.getName()); // Deep inserts allow us to create referenced entities as part of a single create entity operation. See spec: // http://docs.oasis-open.org/odata/odata-atom-format/v4.0/cs02/odata-atom-format-v4.0-cs02.html#_Toc372792739: if (isDeepInsert) { // Handle deep insert create operations (only applicable to POST) xmlWriter.writeAttribute(HREF, getHrefAttributeValue(entity, property)); final Object value = getPropertyValue(property, entity); // Only write inline elements for referenced entities that have values if (property.isCollection()) { if (value != null && ((Collection) value).size() > 0) { startMetadata(); writeFeed((Collection) value, entity, property, null); endMetadata(); } } else if (value != null) { startMetadata(); writeEntry(value, true); endMetadata(); } } else if (isWriteOperation && !isActionCall) { // Handle Bind operations final Object value = getPropertyValue(property, entity); if (property.isCollection()) { xmlWriter.writeAttribute(HREF, String.format("%s(%s)/%s", getEntityName(entityDataModel, entity), formatEntityKey(entityDataModel, entity), property.getName())); if (((Collection) value).size() > 0) { writeCollectionRefs(((Collection) value)); } } else if (value != null) { if (isSingletonEntity(entityDataModel, getPropertyValue(property, entity))) { xmlWriter.writeAttribute(HREF, String.format("%s", getEntityName(entityDataModel, value))); } else { xmlWriter.writeAttribute(HREF, String.format("%s(%s)", getEntityName(entityDataModel, value), formatEntityKey(entityDataModel, value))); } } } else if (isActionCall || expandedProperties.contains(property.getName()) || forceExpand) { xmlWriter.writeAttribute(HREF, getHrefAttributeValue(entity, property)); final Object value = getPropertyValue(property, entity); startMetadata(); if (value != null) { if (property.isCollection()) { writeFeed((Collection) value, entity, property, null); } else { writeEntry(value, true); } } endMetadata(); } else { xmlWriter.writeAttribute(HREF, getHrefAttributeValue(entity, property)); } endLink(); // The association link startLink(); xmlWriter.writeAttribute(REL, ODATA_ASSOCIATION_LINK_REL_NS_PREFIX + property.getName()); xmlWriter.writeAttribute(TYPE, XML.toString()); xmlWriter.writeAttribute(TITLE, property.getName()); if (isSingletonEntity(entityDataModel, entity)) { xmlWriter.writeAttribute(HREF, String.format("%s/%s/$ref", getEntityName(entityDataModel, entity), property.getName())); } else { xmlWriter.writeAttribute(HREF, String.format("%s(%s)/%s/$ref", getEntityName(entityDataModel, entity), formatEntityKey(entityDataModel, entity), property.getName())); } endLink(); } private void writeCollectionRefs(Collection collection) throws XMLStreamException, ODataEdmException { startMetadata(); startFeed(true); for (Object entity : collection) { writeMetadataRef(entity); } endFeed(); endMetadata(); } private void writeMetadataRef(Object entity) throws XMLStreamException, ODataEdmException { xmlWriter.writeStartElement(METADATA, REF, ""); xmlWriter.writeAttribute(ID, String.format("%s(%s)", getEntityName(entityDataModel, entity), formatEntityKey(entityDataModel, entity))); xmlWriter.writeEndElement(); } private void startMetadata() throws XMLStreamException { xmlWriter.writeStartElement(METADATA, INLINE, ""); } private void endMetadata() throws XMLStreamException { xmlWriter.writeEndElement(); } private void startLink() throws XMLStreamException { xmlWriter.writeStartElement(ATOM_LINK); } private void endLink() throws XMLStreamException { xmlWriter.writeEndElement(); } private String getHrefAttributeValue(Object entity, NavigationProperty property) throws ODataEdmException { if (isSingletonEntity(entityDataModel, entity)) { return String.format("%s/%s", getEntityName(entityDataModel, entity), property.getName()); } else { return String.format("%s(%s)/%s", getEntityName(entityDataModel, entity), formatEntityKey(entityDataModel, entity), property.getName()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy