org.odata4j.format.xml.AtomFeedFormatParserExt.java Source code

Java tutorial

Introduction

Here is the source code for org.odata4j.format.xml.AtomFeedFormatParserExt.java

Source

package org.odata4j.format.xml;

/*
 * #%L
 * interaction-odata4j-ext
 * %%
 * Copyright (C) 2012 - 2013 Temenos Holdings N.V.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

import java.io.Reader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.core.MediaType;
import javax.xml.stream.XMLInputFactory;

import org.apache.commons.lang.StringUtils;
import org.core4j.Enumerable;
import org.core4j.Func1;
import org.odata4j.core.OCollection;
import org.odata4j.core.OCollections;
import org.odata4j.core.OComplexObjects;
import org.odata4j.core.OEntities;
import org.odata4j.core.OEntity;
import org.odata4j.core.OEntityKey;
import org.odata4j.core.OLink;
import org.odata4j.core.OLinks;
import org.odata4j.core.OObject;
import org.odata4j.core.OProperties;
import org.odata4j.core.OProperty;
import org.odata4j.edm.EdmCollectionType;
import org.odata4j.edm.EdmComplexType;
import org.odata4j.edm.EdmDataServices;
import org.odata4j.edm.EdmEntitySet;
import org.odata4j.edm.EdmEntityType;
import org.odata4j.edm.EdmFunctionImport;
import org.odata4j.edm.EdmNavigationProperty;
import org.odata4j.edm.EdmProperty;
import org.odata4j.edm.EdmSimpleType;
import org.odata4j.edm.EdmStructuralType;
import org.odata4j.edm.EdmType;
import org.odata4j.format.Entry;
import org.odata4j.internal.FeedCustomizationMapping;
import org.odata4j.internal.InternalUtil;
import org.odata4j.stax2.Attribute2;
import org.odata4j.stax2.QName2;
import org.odata4j.stax2.StartElement2;
import org.odata4j.stax2.XMLEvent2;
import org.odata4j.stax2.XMLEventReader2;
import org.odata4j.stax2.XMLEventWriter2;
import org.odata4j.stax2.XMLFactoryProvider2;
import org.odata4j.stax2.staximpl.StaxXMLInputFactory2Ext;

import com.temenos.interaction.odataext.entity.MetadataOData4j;

public class AtomFeedFormatParserExt extends AtomFeedFormatParser {

    private static MetadataOData4j metadataOData4j;

    public AtomFeedFormatParserExt(MetadataOData4j metadataOData, String entitySetName, OEntityKey entityKey,
            FeedCustomizationMapping fcMapping) {
        super(metadataOData.getMetadata(), entitySetName, entityKey, fcMapping);

        metadataOData4j = metadataOData;
    }

    public AtomFeedFormatParserExt(EdmDataServices metadata, String entitySetName, OEntityKey entityKey,
            FeedCustomizationMapping fcMapping) {
        super(metadata, entitySetName, entityKey, fcMapping);

        metadataOData4j = null;
    }

    public static class DataServicesAtomEntry extends AtomEntry {
        public final String etag;
        public final List<OProperty<?>> properties;

        private OEntity entity;

        private DataServicesAtomEntry(String etag, List<OProperty<?>> properties) {
            this.etag = etag;
            this.properties = properties;
        }

        @Override
        public String toString() {
            return InternalUtil.reflectionToString(this);
        }

        public OEntity getEntity() {
            return this.entity;
        }

        void setEntity(OEntity entity) {
            this.entity = entity;
        }
    }

    @Override
    public AtomFeed parse(Reader reader) {
        return parseFeed(createXMLEventReader(reader), getEntitySet());
    }

    AtomFeed parseFeed(XMLEventReader2 reader, EdmEntitySet entitySet) {

        AtomFeed feed = new AtomFeed();
        List<AtomEntry> rt = new ArrayList<AtomEntry>();

        while (reader.hasNext()) {
            XMLEvent2 event = reader.nextEvent();

            if (isStartElement(event, ATOM_ENTRY)) {
                rt.add(parseEntry(reader, event.asStartElement(), entitySet));
            } else if (isStartElement(event, ATOM_LINK)) {
                if ("next".equals(event.asStartElement().getAttributeByName(new QName2("rel")).getValue())) {
                    feed.next = event.asStartElement().getAttributeByName(new QName2("href")).getValue();
                }
            } else if (isEndElement(event, ATOM_FEED)) {
                // return from a sub feed, if we went down the hierarchy
                break;
            }

        }
        feed.entries = Enumerable.create(rt).cast(Entry.class);

        return feed;

    }

    private EdmEntitySet getEdmEntitySet(String entitySetName) {
        EdmEntitySet result = null;

        if (metadataOData4j == null) {
            result = metadata.getEdmEntitySet(entitySetName);
        } else {
            result = metadataOData4j.getEdmEntitySetByEntitySetName(entitySetName);
        }

        return result;
    }

    private EdmEntitySet getEdmEntitySet(EdmEntityType type) {
        EdmEntitySet result = null;

        if (metadataOData4j == null) {
            result = metadata.getEdmEntitySet(type);
        } else {
            result = metadataOData4j.getEdmEntitySetByType(type);
        }

        return result;
    }

    private EdmEntityType findEdmEntityType(String entityTypeName) {
        EdmEntityType result = null;

        if (metadataOData4j == null) {
            result = (EdmEntityType) metadata.findEdmEntityType(entityTypeName);
        } else {
            result = (EdmEntityType) metadataOData4j.getEdmEntityTypeByTypeName(entityTypeName);
        }

        return result;
    }

    private AtomEntry parseEntry(XMLEventReader2 reader, StartElement2 entryElement, EdmEntitySet entitySet) {

        String id = null;
        String categoryTerm = null;
        String categoryScheme = null;
        String title = null;
        String summary = null;
        String updated = null;
        String contentType = null;
        List<AtomLink> atomLinks = new ArrayList<AtomLink>();

        String etag = getAttributeValueIfExists(entryElement, M_ETAG);

        AtomEntry rt = null;

        while (reader.hasNext()) {
            XMLEvent2 event = reader.nextEvent();

            if (event.isEndElement() && event.asEndElement().getName().equals(entryElement.getName())) {
                rt.id = id; //http://localhost:8810/Oneoff01.svc/Comment(1)
                rt.title = title;
                rt.summary = summary;
                rt.updated = updated;
                rt.categoryScheme = categoryScheme; //http://schemas.microsoft.com/ado/2007/08/dataservices/scheme
                rt.categoryTerm = categoryTerm; //NorthwindModel.Customer
                rt.contentType = contentType;
                rt.atomLinks = atomLinks;

                if (rt instanceof DataServicesAtomEntry) {
                    DataServicesAtomEntry dsae = (DataServicesAtomEntry) rt;
                    OEntity entity = entityFromAtomEntry(entitySet, dsae, fcMapping);
                    dsae.setEntity(entity);
                }
                return rt;
            }

            if (isStartElement(event, ATOM_ID)) {
                id = reader.getElementText();
            } else if (isStartElement(event, ATOM_TITLE)) {
                title = reader.getElementText();
            } else if (isStartElement(event, ATOM_SUMMARY)) {
                summary = reader.getElementText();
            } else if (isStartElement(event, ATOM_UPDATED)) {
                updated = reader.getElementText();
            } else if (isStartElement(event, ATOM_CATEGORY)) {
                categoryTerm = getAttributeValueIfExists(event.asStartElement(), "term");
                categoryScheme = getAttributeValueIfExists(event.asStartElement(), "scheme");
                if (categoryTerm != null)
                    entitySet = getEdmEntitySet(findEdmEntityType(categoryTerm));
            } else if (isStartElement(event, ATOM_LINK)) {
                AtomLink link = parseAtomLink(reader, event.asStartElement(), entitySet);
                atomLinks.add(link);
            } else if (isStartElement(event, M_PROPERTIES)) {
                rt = parseDSAtomEntry(etag, entitySet.getType(), reader, event);
            } else if (isStartElement(event, ATOM_CONTENT)) {
                contentType = getAttributeValueIfExists(event.asStartElement(), "type");
                if (MediaType.APPLICATION_XML.equals(contentType)) {
                    StartElement2 contentElement = event.asStartElement();
                    StartElement2 valueElement = null;
                    while (reader.hasNext()) {
                        XMLEvent2 event2 = reader.nextEvent();
                        if (valueElement == null && event2.isStartElement()) {
                            valueElement = event2.asStartElement();
                            if (isStartElement(event2, M_PROPERTIES)) {
                                rt = parseDSAtomEntry(etag, entitySet.getType(), reader, event2);
                            } else {
                                BasicAtomEntry bae = new BasicAtomEntry();
                                bae.content = innerText(reader, event2.asStartElement());
                                rt = bae;
                            }
                        }
                        if (event2.isEndElement()
                                && event2.asEndElement().getName().equals(contentElement.getName())) {
                            break;
                        }
                    }
                } else {
                    BasicAtomEntry bae = new BasicAtomEntry();
                    bae.content = innerText(reader, event.asStartElement());
                    rt = bae;
                }
            }
        }
        throw new RuntimeException();
    }

    private static String innerText(XMLEventReader2 reader, StartElement2 element) {
        StringWriter sw = new StringWriter();
        XMLEventWriter2 writer = XMLFactoryProvider2.getInstance().newXMLOutputFactory2().createXMLEventWriter(sw);
        while (reader.hasNext()) {

            XMLEvent2 event = reader.nextEvent();
            if (event.isEndElement() && event.asEndElement().getName().equals(element.getName())) {

                return sw.toString();
            } else {
                writer.add(event);
            }

        }
        throw new RuntimeException();
    }

    private DataServicesAtomEntry parseDSAtomEntry(String etag, EdmEntityType entityType, XMLEventReader2 reader,
            XMLEvent2 event) {
        List<OProperty<?>> properties = Enumerable
                .create(parseProperties(reader, event.asStartElement(), metadata, entityType)).toList();
        return new DataServicesAtomEntry(etag, properties);
    }

    private AtomLink parseAtomLink(XMLEventReader2 reader, StartElement2 linkElement, EdmEntitySet entitySet) {
        AtomLink rt = new AtomLink();
        rt.relation = getAttributeValueIfExists(linkElement, "rel");
        rt.type = getAttributeValueIfExists(linkElement, "type");
        rt.title = getAttributeValueIfExists(linkElement, "title");
        rt.href = getAttributeValueIfExists(linkElement, "href");
        rt.inlineContentExpected = false;

        String navPropertyName = rt.getNavProperty();
        EdmNavigationProperty navProperty = null;
        if (entitySet != null && navPropertyName != null)
            navProperty = entitySet.getType().findNavigationProperty(navPropertyName);
        EdmEntitySet targetEntitySet = null;
        if (navProperty != null)
            targetEntitySet = getEdmEntitySet(navProperty.getToRole().getType());

        // expected cases:
        // 1.  </link>                  - no inlined content, i.e. deferred
        // 2.  <m:inline/></link>       - inlined content but null entity or empty feed
        // 3.  <m:inline><feed>...</m:inline></link> - inlined content with 1 or more items
        // 4.  <m:inline><entry>..</m:inline></link> - inlined content 1 an item

        while (reader.hasNext()) {
            XMLEvent2 event = reader.nextEvent();

            if (event.isEndElement() && event.asEndElement().getName().equals(linkElement.getName())) {
                break;
            } else if (isStartElement(event, XmlFormatParser.M_INLINE)) {
                rt.inlineContentExpected = true; // may be null content.
            } else if (isStartElement(event, ATOM_FEED)) {
                rt.inlineFeed = parseFeed(reader, targetEntitySet);
            } else if (isStartElement(event, ATOM_ENTRY)) {
                rt.inlineEntry = parseEntry(reader, event.asStartElement(), targetEntitySet);
            }
        }
        return rt;
    }

    private OEntity entityFromAtomEntry(EdmEntitySet entitySet, DataServicesAtomEntry dsae,
            FeedCustomizationMapping mapping) {

        List<OProperty<?>> props = dsae.properties;
        if (mapping != null) {
            Enumerable<OProperty<?>> properties = Enumerable.create(dsae.properties);
            if (mapping.titlePropName != null)
                properties = properties.concat(OProperties.string(mapping.titlePropName, dsae.title));
            if (mapping.summaryPropName != null)
                properties = properties.concat(OProperties.string(mapping.summaryPropName, dsae.summary));

            props = properties.toList();
        }

        EdmEntityType entityType = entitySet.getType();
        if (dsae.categoryTerm != null) {
            // The type of an entity set is polymorphic...
            entityType = findEdmEntityType(dsae.categoryTerm);
            if (entityType == null) {
                throw new RuntimeException("Unable to resolve entity type " + dsae.categoryTerm);
            }
        }
        // favor the key we just parsed.

        OEntityKey key = dsae.id != null
                ? (dsae.id.endsWith(")") ? parseEntityKey(dsae.id) : OEntityKey.infer(entitySet, props))
                : null;

        if (key == null) {
            key = entityKey;
        }

        if (key == null)
            return OEntities.createRequest(entitySet, props, toOLinks(metadata, entitySet, dsae.atomLinks, mapping),
                    dsae.title, dsae.categoryTerm);

        return OEntities.create(entitySet, entityType, key, dsae.etag, props,
                toOLinks(metadata, entitySet, dsae.atomLinks, mapping), dsae.title, dsae.categoryTerm);
    }

    private List<OLink> toOLinks(final EdmDataServices metadata, EdmEntitySet fromRoleEntitySet,
            List<AtomLink> links, final FeedCustomizationMapping mapping) {
        List<OLink> rt = new ArrayList<OLink>(links.size());
        for (final AtomLink link : links) {

            if (link.relation.startsWith(XmlFormatWriter.related)) {
                if (link.type.equals(XmlFormatWriter.atom_feed_content_type)) {

                    if (link.inlineContentExpected) {
                        List<OEntity> relatedEntities = null;

                        if (link.inlineFeed != null && link.inlineFeed.entries != null) {

                            // get the entity set belonging to the from role type
                            EdmNavigationProperty navProperty = fromRoleEntitySet != null
                                    ? fromRoleEntitySet.getType().findNavigationProperty(link.getNavProperty())
                                    : null;
                            final EdmEntitySet toRoleEntitySet = metadata != null && navProperty != null
                                    ? getEdmEntitySet(navProperty.getToRole().getType())
                                    : null;

                            // convert the atom feed entries to OEntitys
                            relatedEntities = Enumerable.create(link.inlineFeed.entries)
                                    .cast(DataServicesAtomEntry.class)
                                    .select(new Func1<DataServicesAtomEntry, OEntity>() {
                                        public OEntity apply(DataServicesAtomEntry input) {
                                            return entityFromAtomEntry(toRoleEntitySet, input, mapping);
                                        }
                                    }).toList();
                        } // else empty feed.
                        rt.add(OLinks.relatedEntitiesInline(link.relation, link.title, link.href, relatedEntities));
                    } else {
                        // no inlined entities
                        rt.add(OLinks.relatedEntities(link.relation, link.title, link.href));
                    }
                } else if (link.type.equals(XmlFormatWriter.atom_entry_content_type))
                    if (link.inlineContentExpected) {
                        OEntity relatedEntity = null;
                        if (link.inlineEntry != null) {
                            EdmNavigationProperty navProperty = fromRoleEntitySet != null
                                    ? fromRoleEntitySet.getType().findNavigationProperty(link.getNavProperty())
                                    : null;
                            EdmEntitySet toRoleEntitySet = metadata != null && navProperty != null
                                    ? getEdmEntitySet(navProperty.getToRole().getType())
                                    : null;
                            relatedEntity = entityFromAtomEntry(toRoleEntitySet,
                                    (DataServicesAtomEntry) link.inlineEntry, mapping);
                        }
                        rt.add(OLinks.relatedEntityInline(link.relation, link.title, link.href, relatedEntity));
                    } else {
                        // no inlined entity
                        rt.add(OLinks.relatedEntity(link.relation, link.title, link.href));
                    }
            } else {
                if (!StringUtils.isEmpty(link.relation) && !StringUtils.isEmpty(link.title)
                        && !StringUtils.isEmpty(link.href)) {
                    rt.add(OLinks.relatedEntity(link.relation, link.title, link.href));
                }
            }
        }
        return rt;
    }

    private EdmEntitySet getEntitySet() {
        EdmEntitySet entitySet = null;
        if (!metadata.getSchemas().isEmpty()) {
            entitySet = getEdmEntitySet(entitySetName);
            if (entitySet == null) {
                // panic! could not determine the entity-set, is it a function?
                EdmFunctionImport efi = metadata.findEdmFunctionImport(entitySetName);
                if (efi != null)
                    entitySet = efi.getEntitySet();
            }
        }
        if (entitySet == null)
            throw new RuntimeException("Could not derive the entity-set " + entitySetName);
        return entitySet;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static Iterable<OProperty<?>> parseProperties(XMLEventReader2 reader, StartElement2 propertiesElement,
            EdmDataServices metadata, EdmStructuralType structuralType) {
        List<OProperty<?>> rt = new ArrayList<OProperty<?>>();

        while (reader.hasNext()) {
            XMLEvent2 event = reader.nextEvent();

            if (event.isEndElement() && event.asEndElement().getName().equals(propertiesElement.getName())) {
                return rt;
            }

            if (event.isStartElement()
                    && event.asStartElement().getName().getNamespaceUri().equals(NS_DATASERVICES)) {

                String name = event.asStartElement().getName().getLocalPart();
                Attribute2 typeAttribute = event.asStartElement().getAttributeByName(M_TYPE);
                Attribute2 nullAttribute = event.asStartElement().getAttributeByName(M_NULL);
                boolean isNull = nullAttribute != null && "true".equals(nullAttribute.getValue());

                OProperty<?> op = null;

                EdmType et = null;
                boolean isCollection = false;
                if (typeAttribute != null) {
                    String type = typeAttribute.getValue();
                    et = metadata.resolveType(type);
                    // Following is to make our parse compatible with Odata4j as well as Odata .NET clients
                    // Odata4j       represent collection as Bag(...)
                    // Odata.NET    represent collection as Collection(...) 
                    if (et == null && (type.startsWith("Bag") || type.startsWith("Collection"))) {
                        isCollection = true;
                        type = type.substring(type.indexOf("(") + 1, type.length() - 1);
                        et = metadata.resolveType(type);
                        if (et == null) {
                            et = metadataOData4j.getEdmEntityTypeByTypeName(type);
                        }
                    } else if (et == null) {
                        et = metadataOData4j.getEdmEntityTypeByTypeName(type);
                    }
                    if (et == null) {
                        // property arrived with an unknown type
                        throw new RuntimeException("unknown property type: " + type);
                    }
                } else if (structuralType instanceof EdmComplexType) {
                    //  Assume for now we're creating a bag
                    EdmProperty property = structuralType.findProperty(name);
                    if (property != null)
                        et = property.getType(); // Simple Property Of Bag
                    else
                        et = structuralType; // This is for <d:element>
                } else {
                    EdmProperty property = structuralType.findProperty(name);
                    if (property != null) {
                        et = property.getType();
                    } else {
                        property = structuralType.findProperty(structuralType.getName() + "_" + name);

                        if (property == null) {
                            et = EdmSimpleType.STRING; // we must support open types                     
                        } else {
                            et = property.getType();
                        }
                    }
                }

                if (isCollection) {
                    //
                    //  So we're on the Segments element right now, which means the next event should be the first d:element
                    //
                    //  Thus loop on the d:element's and for each one recurse again to create the complex object
                    //
                    OCollection.Builder bagBuilder = OCollections.newBuilder(et);
                    Enumerable<OProperty<?>> bagObjects = Enumerable
                            .create(parseProperties(reader, event.asStartElement(), metadata, (EdmComplexType) et));

                    for (OProperty<?> prop : bagObjects) {
                        bagBuilder.add((OObject) OComplexObjects.create((EdmComplexType) prop.getType(),
                                (List<OProperty<?>>) prop.getValue()));
                    }
                    OCollection<? extends OObject> bag = bagBuilder.build();
                    op = OProperties.collection(name,
                            new EdmCollectionType(EdmProperty.CollectionKind.List, (EdmComplexType) et), bag);
                } else if (et != null && (!et.isSimple())) {
                    EdmStructuralType est = (EdmStructuralType) et;
                    op = OProperties.complex(name, (EdmComplexType) et,
                            isNull ? null
                                    : Enumerable
                                            .create(parseProperties(reader, event.asStartElement(), metadata, est))
                                            .toList());
                } else {
                    op = OProperties.parseSimple(name, (EdmSimpleType<?>) et,
                            isNull ? null : reader.getElementText());
                }
                rt.add(op);
            }
        }
        throw new RuntimeException();
    }

    private XMLEventReader2 createXMLEventReader(Reader reader) {
        XMLInputFactory factory = XMLInputFactory.newInstance();

        //XXE on its own can be prevented by setting IS_SUPPORT_EXTERNAL_ENTITIES to false but this will not
        //prevent a billion laugh attack. Setting IS_REPLACING_ENTITY_REFERENCES to false does
        //not force the implementation to not process internal entity references.
        //Safest thing to do is to set SUPPORT_DTD to false to prevent both XXE and billion laugh.
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);

        XMLEventReader2 xmlEventReader2 = new StaxXMLInputFactory2Ext(factory).createXMLEventReader(reader);
        return xmlEventReader2;
    }
}