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; } }