package org.odata4j.format.xml; import java.io.Reader; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.core.MediaType; import org.core4j.Enumerable; import org.core4j.Func1; import org.odata4j.core.OBindableEntities; import org.odata4j.core.OBindableEntity; import org.odata4j.core.OCollection; 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.OProperties; import org.odata4j.core.OProperty; import org.odata4j.core.StreamEntity; 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.EdmFunctionImport.FunctionKind; 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.format.Feed; import org.odata4j.format.FormatParser; 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.util.StaxUtil; import org.odata4j.urlencoder.ConversionUtil; public class AtomFeedFormatParser extends XmlFormatParser implements FormatParser<Feed> { protected EdmDataServices metadata; protected String entitySetName; protected OEntityKey entityKey; protected FeedCustomizationMapping fcMapping; protected EdmFunctionImport function; public AtomFeedFormatParser(EdmDataServices metadata, String entitySetName, OEntityKey entityKey, FeedCustomizationMapping fcMapping, EdmFunctionImport function) { this.metadata = metadata; this.entitySetName = entitySetName; this.entityKey = entityKey; this.fcMapping = fcMapping; this.function = function; } public static class AtomFeed implements Feed { public String next; public Iterable<Entry> entries; @Override public Iterable<Entry> getEntries() { return entries; } @Override public String getNext() { return next; } } abstract static class AtomEntry implements Entry { public String id; public String title; public String summary; public String updated; public String categoryTerm; public String categoryScheme; public String contentType; public String mediaSource; public List<AtomLink> atomLinks; public String getUri() { return null; } public String getETag() { return null; } public String getType() { return MediaType.APPLICATION_ATOM_XML; } public String getMediaSource() { return null; } public String getMediaContentType() { return null; } } static class AtomLink { public String relation; public String title; public String type; public String href; public AtomFeed inlineFeed; public AtomEntry inlineEntry; public boolean inlineContentExpected; public String getNavProperty() { if (relation != null && relation.startsWith(XmlFormatWriter.related)) return relation.substring(XmlFormatWriter.related.length()); else return null; } } static class AtomFunction { public String relation; public String metadata; public String title; public String href; public String getFQFunctionName() { if (relation == null) { int pos = href.lastIndexOf("/"); if (pos != -1) { return href.substring(pos + 1); } else { return href; } } else { return relation; } } } static class BasicAtomEntry extends AtomEntry { public String content; @Override public String toString() { return InternalUtil.reflectionToString(this); } @Override public OEntity getEntity() { return 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); } @Override public OEntity getEntity() { return this.entity; } void setEntity(OEntity entity) { this.entity = entity; } } @Override public AtomFeed parse(Reader reader) { return parseFeed(StaxUtil.newXMLEventReader(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; } 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; if (typeAttribute != null) { String type = typeAttribute.getValue(); et = metadata.resolveType(type); if (et == null) { // property arrived with an unknown type throw new RuntimeException("unknown property type: " + type); } } else { EdmProperty property = (EdmProperty) structuralType.findProperty(name); if (property != null) et = property.getType(); else et = EdmSimpleType.STRING; // we must support open types } if (et != null && (!et.isSimple())) { if (et instanceof EdmCollectionType) { OCollection colV = AtomCollectionFormatParser.parse(reader, event.asStartElement(), metadata, ((EdmCollectionType) et).getItemType()); op = OProperties.collection(name, (EdmCollectionType) et, colV); } else { 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 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 = metadata.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 AtomFunction parseAtomFunction(XMLEventReader2 reader, StartElement2 functionElement) { AtomFunction ft = new AtomFunction(); ft.relation = getAttributeValueIfExists(functionElement, "rel"); ft.metadata = getAttributeValueIfExists(functionElement, "metadata"); ft.title = getAttributeValueIfExists(functionElement, "title"); ft.href = getAttributeValueIfExists(functionElement, "target"); return ft; } 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 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 static final Pattern ENTITY_SET_NAME = Pattern.compile("\\/([^\\/\\(]+)\\("); public static OEntityKey parseEntityKey(String atomEntryId) { //we have to decode this atomEntry id which is actually uri of entity. atomEntryId = ConversionUtil.decodeString(atomEntryId); Matcher m = ENTITY_SET_NAME.matcher(atomEntryId); // Fix for NPE when // 1. nested entity like /Categories(1)/Products(76) is requested and // 2. entity with keys like /PointSetField(attribute='X (EASTING)',point_set_id=19) int count = 0; int index = 0; while (m.find()) { count++; index = m.end(); } if (count == 0) throw new RuntimeException("Unable to parse the entity-key from atom entry id: " + atomEntryId); //key(s) is the last occurrence in the pattern match return OEntityKey.parse(atomEntryId.substring(index - 1)); } private EdmEntitySet getEntitySet() { EdmEntitySet entitySet = null; if (!metadata.getSchemas().isEmpty()) { entitySet = metadata.findEdmEntitySet(entitySetName); if (entitySet == null && function != null) { // panic! could not determine the entity-set, is it a function? entitySet = function.getEntitySet(); } } if (entitySet == null) throw new RuntimeException("Could not derive the entity-set " + entitySetName); return entitySet; } 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; String mediaSource = null; List<AtomLink> atomLinks = new ArrayList<AtomLink>(); Map<String, EdmFunctionImport> functions = new HashMap<String, EdmFunctionImport>(); Map<String, EdmFunctionImport> actions = new HashMap<String, EdmFunctionImport>(); 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; rt.contentType = contentType; rt.mediaSource = mediaSource; if (rt instanceof DataServicesAtomEntry) { DataServicesAtomEntry dsae = (DataServicesAtomEntry) rt; OBindableEntity bindableExtension = null; if (functions.size() > 0 || actions.size() > 0) { bindableExtension = OBindableEntities.createBindableExtension(actions, functions); } if (rt.mediaSource != null) { List<Object> mediaList = new ArrayList<Object>(); StreamEntity streamEntity = new StreamEntity(); streamEntity.setAtomEntitySource(rt.mediaSource); streamEntity.setAtomEntityType(rt.contentType); mediaList.add(streamEntity); OEntity entity = entityFromAtomEntry(metadata, entitySet, dsae, fcMapping, bindableExtension, mediaList.get(0)); dsae.setEntity(entity); } else { OEntity entity = entityFromAtomEntry(metadata, entitySet, dsae, fcMapping, bindableExtension); 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 = metadata.getEdmEntitySet((EdmEntityType) metadata.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, M_ACTION)) { AtomFunction function = parseAtomFunction(reader, event.asStartElement()); actions.put(function.getFQFunctionName(), metadata.findEdmFunctionImport(function.title, entitySet.getType(), FunctionKind.Action)); } else if (isStartElement(event, M_FUNCTION)) { AtomFunction function = parseAtomFunction(reader, event.asStartElement()); functions.put(function.getFQFunctionName(), metadata.findEdmFunctionImport(function.title, entitySet.getType(), FunctionKind.Function)); } else if (isStartElement(event, ATOM_CONTENT)) { contentType = getAttributeValueIfExists(event.asStartElement(), "type"); mediaSource = getAttributeValueIfExists(event.asStartElement(), "src"); 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 OEntity entityFromAtomEntry( EdmDataServices metadata, EdmEntitySet entitySet, DataServicesAtomEntry dsae, FeedCustomizationMapping mapping, Object... extensions) { 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 = (EdmEntityType) metadata.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.equals("")) ? (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, extensions); } 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 ? metadata.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>() { @Override public OEntity apply( DataServicesAtomEntry input) { return entityFromAtomEntry(metadata, 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 ? metadata.getEdmEntitySet(navProperty.getToRole().getType()) : null; relatedEntity = entityFromAtomEntry(metadata, 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 (link.relation.startsWith(XmlFormatWriter.edit_media)) { rt.add(OLinks.namedStreamLink(link.relation, link.title, link.href, link.type)); } else if (link.relation.startsWith(XmlFormatWriter.mediaresource)) { rt.add(OLinks.namedStreamLink(link.relation, link.title, link.href, link.type)); } } return rt; } }