http://git-wip-us.apache.org/repos/asf/olingo-odata2/blob/9e949e40/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlEntryDeserializer.java ---------------------------------------------------------------------- diff --git a/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlEntryDeserializer.java b/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlEntryDeserializer.java new file mode 100644 index 0000000..06bae41 --- /dev/null +++ b/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlEntryDeserializer.java @@ -0,0 +1,596 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.olingo.odata2.client.core.ep.deserializer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.olingo.odata2.api.edm.Edm; +import org.apache.olingo.odata2.api.edm.EdmEntitySet; +import org.apache.olingo.odata2.api.edm.EdmException; +import org.apache.olingo.odata2.api.edm.EdmLiteralKind; +import org.apache.olingo.odata2.api.edm.EdmMultiplicity; +import org.apache.olingo.odata2.api.edm.EdmNavigationProperty; +import org.apache.olingo.odata2.api.edm.EdmSimpleType; +import org.apache.olingo.odata2.api.ep.EntityProviderException; +import org.apache.olingo.odata2.api.ep.entry.ODataEntry; +import org.apache.olingo.odata2.api.exception.ODataApplicationException; +import org.apache.olingo.odata2.client.api.ep.DeserializerProperties; +import org.apache.olingo.odata2.client.api.ep.callback.OnDeserializeInlineContent; +import org.apache.olingo.odata2.core.commons.ContentType; +import org.apache.olingo.odata2.core.ep.aggregator.EntityInfoAggregator; +import org.apache.olingo.odata2.core.ep.aggregator.EntityPropertyInfo; +import org.apache.olingo.odata2.core.ep.aggregator.EntityTypeMapping; +import org.apache.olingo.odata2.core.ep.entry.EntryMetadataImpl; +import org.apache.olingo.odata2.core.ep.entry.MediaMetadataImpl; +import org.apache.olingo.odata2.core.ep.entry.ODataEntryImpl; +import org.apache.olingo.odata2.core.ep.feed.FeedMetadataImpl; +import org.apache.olingo.odata2.core.ep.feed.ODataDeltaFeedImpl; +import org.apache.olingo.odata2.core.ep.util.FormatXml; + +/** + * Atom/XML format reader/consumer for entries. + * + * {@link XmlEntryDeserializer} instance can be reused for several + * {@link #readEntry(XMLStreamReader, EntityInfoAggregator, EntityProviderReadProperties)} calls + * but be aware that the instance and their <code>readEntry*</code> methods are <b>NOT THREAD SAFE</b>. + * + */ +public class XmlEntryDeserializer { + + private ODataEntryImpl readEntryResult; + private Map<String, Object> properties; + private MediaMetadataImpl mediaMetadata; + private EntryMetadataImpl entryMetadata; + private EntityTypeMapping typeMappings; + private String currentHandledStartTagName; + + /** + * Deserializes payload entry + * @param reader + * @param eia + * @param readProperties + * @param isInline + * @return ODataEntry + * @throws EntityProviderException + */ + public ODataEntry readEntry(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties readProperties, final boolean isInline) throws EntityProviderException { + try { + initialize(readProperties); + + if (isInline) { + setETag(reader); + } + + while (reader.hasNext() && !isEntryEndTag(reader)) { + reader.nextTag(); + if (reader.isStartElement()) { + handleStartedTag(reader, eia, readProperties); + } + } + + return readEntryResult; + } catch (XMLStreamException e) { + throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass() + .getSimpleName()), e); + } catch (EdmException e) { + throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass() + .getSimpleName()), e); + } + } + + private boolean isEntryEndTag(final XMLStreamReader reader) { + return reader.isEndElement() + && Edm.NAMESPACE_ATOM_2005.equals(reader.getNamespaceURI()) + && FormatXml.ATOM_ENTRY.equals(reader.getLocalName()); + } + + /** + * Initializes the {@link XmlEntryDeserializer} to be ready for reading an entry. + * @param readProperties + * @throws EntityProviderException + */ + private void initialize(final DeserializerProperties readProperties) throws EntityProviderException { + properties = new HashMap<String, Object>(); + mediaMetadata = new MediaMetadataImpl(); + entryMetadata = new EntryMetadataImpl(); + + readEntryResult = new ODataEntryImpl(properties, mediaMetadata, entryMetadata, null); + typeMappings = EntityTypeMapping.create(readProperties.getTypeMappings()); + } + + private void handleStartedTag(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties readProperties) + throws EntityProviderException, XMLStreamException, EdmException { + + currentHandledStartTagName = reader.getLocalName(); + + if (FormatXml.ATOM_ID.equals(currentHandledStartTagName)) { + readId(reader); + } else if (FormatXml.ATOM_ENTRY.equals(currentHandledStartTagName)) { + readEntry(reader); + } else if (FormatXml.ATOM_LINK.equals(currentHandledStartTagName)) { + readLink(reader, eia, readProperties); + } else if (FormatXml.ATOM_CONTENT.equals(currentHandledStartTagName)) { + readContent(reader, eia, readProperties); + } else if (FormatXml.M_PROPERTIES.equals(currentHandledStartTagName)) { + readProperties(reader, eia, readProperties); + } else { + readCustomElement(reader, currentHandledStartTagName, eia, readProperties); + } + } + + private void readCustomElement(final XMLStreamReader reader, final String tagName, //NOSONAR + final EntityInfoAggregator eia, + final DeserializerProperties readProperties) + throws EdmException, EntityProviderException, XMLStreamException { //NOSONAR + EntityPropertyInfo targetPathInfo = eia.getTargetPathInfo(tagName); + NamespaceContext nsctx = reader.getNamespaceContext(); + + boolean skipTag = true; + if (!Edm.NAMESPACE_ATOM_2005.equals(reader.getName().getNamespaceURI())) { + + if (targetPathInfo != null) { + final String customPrefix = targetPathInfo.getCustomMapping().getFcNsPrefix(); + final String customNamespaceURI = targetPathInfo.getCustomMapping().getFcNsUri(); + + if (customPrefix != null && customNamespaceURI != null) { + String xmlPrefix = nsctx.getPrefix(customNamespaceURI); + String xmlNamespaceUri = reader.getNamespaceURI(customPrefix); + + if (customNamespaceURI.equals(xmlNamespaceUri) && customPrefix.equals(xmlPrefix)) { //NOSONAR + skipTag = false; + reader.require(XMLStreamConstants.START_ELEMENT, customNamespaceURI, tagName); + final String text = reader.getElementText(); + reader.require(XMLStreamConstants.END_ELEMENT, customNamespaceURI, tagName); + + final EntityPropertyInfo propertyInfo = getValidatedPropertyInfo(eia, tagName); + final Class<?> typeMapping = typeMappings.getMappingClass(propertyInfo.getName()); + final EdmSimpleType type = (EdmSimpleType) propertyInfo.getType(); + final Object value = type.valueOfString(text, EdmLiteralKind.DEFAULT, + readProperties == null || readProperties.isValidatingFacets() ? propertyInfo.getFacets() : null, + typeMapping == null ? type.getDefaultType() : typeMapping); + properties.put(tagName, value); + } + } + } else { + throw new EntityProviderException(EntityProviderException.INVALID_PROPERTY.addContent(tagName)); + } + } + + if (skipTag) { + skipStartedTag(reader); + } + } + + /** + * Skip the tag to which the {@link XMLStreamReader} currently points. + * Therefore it is read until an end element tag with current local name is found. + * + * @param reader + * @throws XMLStreamException + */ + private void skipStartedTag(final XMLStreamReader reader) throws XMLStreamException { + final String name = reader.getLocalName(); + int read = 1; + while (read > 0 && reader.hasNext()) { + reader.next(); + if (reader.hasName() && name.equals(reader.getLocalName())) { + if (reader.isEndElement()) { + read--; + } else if (reader.isStartElement()) { + read++; + } + } + } + } + + private void readEntry(final XMLStreamReader reader) throws XMLStreamException { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_ENTRY); + setETag(reader); + } + + private void setETag(final XMLStreamReader reader) { + final String etag = reader.getAttributeValue(Edm.NAMESPACE_M_2007_08, FormatXml.M_ETAG); + entryMetadata.setEtag(etag); + } + + /** + * + * @param reader + * @param eia + * @param readProperties + * @throws EntityProviderException + * @throws XMLStreamException + * @throws EdmException + */ + private void readLink(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties readProperties) throws EntityProviderException, XMLStreamException, + EdmException { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_LINK); + + final String rel = reader.getAttributeValue(null, FormatXml.ATOM_REL); + final String uri = reader.getAttributeValue(null, FormatXml.ATOM_HREF); + final String type = reader.getAttributeValue(null, FormatXml.ATOM_TYPE); + final String etag = reader.getAttributeValue(Edm.NAMESPACE_M_2007_08, FormatXml.M_ETAG); + + // read to next tag to check if <link> contains any further tags + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_LINK); + reader.nextTag(); + + if (rel == null || uri == null) { + throw new EntityProviderException(EntityProviderException.MISSING_ATTRIBUTE.addContent( + FormatXml.ATOM_HREF + "' and/or '" + FormatXml.ATOM_REL).addContent(FormatXml.ATOM_LINK)); + } else if (rel.startsWith(Edm.NAMESPACE_REL_2007_08)) { + final String navigationPropertyName = rel.substring(Edm.NAMESPACE_REL_2007_08.length()); + entryMetadata.putAssociationUri(navigationPropertyName, uri); + } else if (rel.equals(Edm.LINK_REL_EDIT_MEDIA)) { + mediaMetadata.setEditLink(uri); + mediaMetadata.setEtag(etag); + } + + if (!reader.isEndElement() && rel != null && rel.startsWith(Edm.NAMESPACE_REL_2007_08)) { + readInlineContent(reader, eia, readProperties, type, rel); + } + } + + /** + * Inline content was found and {@link XMLStreamReader} already points to <m:inline> tag. + * + * @param reader + * @param eia + * @param readProperties + * @param atomLinkType the atom <code>type</code> attribute value of the <code>link</code> tag + * @param atomLinkRel the atom <code>rel</code> attribute value of the <code>link</code> tag + * @throws XMLStreamException + * @throws EntityProviderException + * @throws EdmException + */ + private void readInlineContent(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties readProperties, + final String atomLinkType, final String atomLinkRel) + throws XMLStreamException, EntityProviderException, EdmException { + + // + String navigationPropertyName = atomLinkRel.substring(Edm.NAMESPACE_REL_2007_08.length()); + + EdmNavigationProperty navigationProperty = + (EdmNavigationProperty) eia.getEntityType().getProperty(navigationPropertyName); + EdmEntitySet entitySet = eia.getEntitySet().getRelatedEntitySet(navigationProperty); + EntityInfoAggregator inlineEia = EntityInfoAggregator.create(entitySet); + + final DeserializerProperties inlineProperties = createInlineProperties(readProperties, navigationProperty); + + // validations + boolean isFeed = isInlineFeedValidated(reader, atomLinkType, navigationProperty); + + List<ODataEntry> inlineEntries = new ArrayList<ODataEntry>(); + + while (!(reader.isEndElement() && Edm.NAMESPACE_M_2007_08.equals(reader.getNamespaceURI()) && FormatXml.M_INLINE + .equals(reader.getLocalName()))) { + + if (reader.isStartElement() && Edm.NAMESPACE_ATOM_2005.equals(reader.getNamespaceURI()) + && FormatXml.ATOM_ENTRY.equals(reader.getLocalName())) { + XmlEntryDeserializer xec = new XmlEntryDeserializer(); + ODataEntry inlineEntry = xec.readEntry(reader, inlineEia, inlineProperties, true); + inlineEntries.add(inlineEntry); + } + // next tag + reader.next(); + } + + updateReadProperties(navigationPropertyName, isFeed, inlineEntries); + + reader.require(XMLStreamConstants.END_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_INLINE); + } + + /** + * Updates the read properties ({@link #properties}) for this {@link ReadEntryResult} ({@link #readEntryResult}). + * + * @param readProperties + * @param navigationPropertyName + * @param navigationProperty + * @param isFeed + * @param inlineEntries + * @throws EntityProviderException + */ + private void updateReadProperties(final String navigationPropertyName, + final boolean isFeed, + final List<ODataEntry> inlineEntries) { + Object entry = extractODataEntity(isFeed, inlineEntries); + readEntryResult.setContainsInlineEntry(true); + properties.put(navigationPropertyName, entry); + + } + + + + /** + * Get a list of {@link ODataEntry}, an empty list, a single {@link ODataEntry} or <code>NULL</code> based on + * <code>isFeed</code> value and <code>inlineEntries</code> content. + * + * @param isFeed + * @param inlineEntries + * @return + */ + private Object extractODataEntity(final boolean isFeed, final List<ODataEntry> inlineEntries) { + if (isFeed) { + return new ODataDeltaFeedImpl(inlineEntries, new FeedMetadataImpl()); + } else if (!inlineEntries.isEmpty()) { + return inlineEntries.get(0); + } + return null; + } + + + /** + * Create {@link EntityProviderReadProperties} which can be used for reading of inline properties/entrys of navigation + * links within + * this current read entry. + * + * @param readProperties + * @param navigationProperty + * @return + * @throws EntityProviderException + */ + private DeserializerProperties createInlineProperties + (final DeserializerProperties readProperties, + final EdmNavigationProperty navigationProperty) throws EntityProviderException { + final OnDeserializeInlineContent callback = readProperties.getCallback(); + + DeserializerProperties currentReadProperties = DeserializerProperties.initFrom(readProperties).build(); + if (callback == null) { + return currentReadProperties; + } else { + try { + return callback.receiveReadProperties(currentReadProperties, navigationProperty); + } catch (ODataApplicationException e) { + throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass() + .getSimpleName()), e); + } + } + } + + /** + * <p> + * Inline content was found and {@link XMLStreamReader} already points to <code><m:inline> tag</code>. + * <br/> + * <b>ATTENTION</b>: If {@link XMLStreamReader} does not point to the <code><m:inline> tag</code> an exception is + * thrown. + * </p> + * <p> + * Check whether it is an inline <code>Feed</code> or <code>Entry</code> and validate that... + * <ul> + * <li>...{@link FormatXml#M_INLINE} tag is correctly set.</li> + * <li>...based on {@link EdmMultiplicity} of {@link EdmNavigationProperty} all tags are correctly set.</li> + * <li>...{@link FormatXml#ATOM_TYPE} tag is correctly set and according to {@link FormatXml#ATOM_ENTRY} or + * {@link FormatXml#ATOM_FEED} to following tags are available.</li> + * </ul> + * + * For the case that one of above validations fail an {@link EntityProviderException} is thrown. + * If validation was successful <code>true</code> is returned for <code>Feed</code> and <code>false</code> for + * <code>Entry</code> + * multiplicity. + * </p> + * + * @param reader xml content reader which already points to <code><m:inline> tag</code> + * @param eia all necessary information about the entity + * @param type the atom type attribute value of the <code>link</code> tag + * @param navigationProperty the navigation property name of the entity + * @return <code>true</code> for <code>Feed</code> and <code>false</code> for <code>Entry</code> + * @throws EntityProviderException is thrown if at least one validation fails. + * @throws EdmException if edm access fails + */ + private boolean isInlineFeedValidated(final XMLStreamReader reader, + final String type, final EdmNavigationProperty navigationProperty) throws EntityProviderException, EdmException { + boolean isFeed = false; + try { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_INLINE); + // + ContentType cType = ContentType.parse(type); + if (cType == null) { + throw new EntityProviderException(EntityProviderException.INVALID_INLINE_CONTENT.addContent("xml data")); + } + EdmMultiplicity navigationMultiplicity = navigationProperty.getMultiplicity(); + + switch (navigationMultiplicity) { + case MANY: + validateFeedTags(reader, cType); + isFeed = true; + break; + case ONE: + case ZERO_TO_ONE: + validateEntryTags(reader, cType); + break; + } + } catch (XMLStreamException e) { + throw new EntityProviderException(EntityProviderException.INVALID_INLINE_CONTENT.addContent("xml data"), e); + } + return isFeed; + } + + private void validateEntryTags(final XMLStreamReader reader, final ContentType cType) throws XMLStreamException, + EntityProviderException { + if (FormatXml.ATOM_ENTRY.equals(cType.getParameters().get(FormatXml.ATOM_TYPE))) { + int next = reader.nextTag(); + if (XMLStreamConstants.START_ELEMENT == next) { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_ENTRY); + } else { + reader.require(XMLStreamConstants.END_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_INLINE); + } + } else { + throw new EntityProviderException(EntityProviderException.INVALID_INLINE_CONTENT.addContent("entry")); + } + } + + private void validateFeedTags(final XMLStreamReader reader, final ContentType cType) throws XMLStreamException, + EntityProviderException { + if (FormatXml.ATOM_FEED.equals(cType.getParameters().get(FormatXml.ATOM_TYPE))) { + int next = reader.nextTag(); + if (XMLStreamConstants.START_ELEMENT == next) { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_FEED); + } else { + reader.require(XMLStreamConstants.END_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_INLINE); + } + } else { + throw new EntityProviderException(EntityProviderException.INVALID_INLINE_CONTENT.addContent("feed")); + } + } + + private void readContent(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties readProperties) + throws EntityProviderException, XMLStreamException, EdmException { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_CONTENT); + + final String contentType = reader.getAttributeValue(null, FormatXml.ATOM_TYPE); + final String sourceLink = reader.getAttributeValue(null, FormatXml.ATOM_SRC); + + reader.nextTag(); + + if (reader.isStartElement() && reader.getLocalName().equals(FormatXml.M_PROPERTIES)) { + readProperties(reader, eia, readProperties); + } else if (reader.isEndElement()) { + reader.require(XMLStreamConstants.END_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_CONTENT); + } else { + throw new EntityProviderException(EntityProviderException.INVALID_STATE + .addContent("Expected closing 'content' or starting 'properties' but found '" + + reader.getLocalName() + "'.")); + } + + mediaMetadata.setContentType(contentType); + mediaMetadata.setSourceLink(sourceLink); + } + + private void readId(final XMLStreamReader reader) throws EntityProviderException, XMLStreamException { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_ID); + entryMetadata.setId(reader.getElementText()); + reader.require(XMLStreamConstants.END_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_ID); + } + + private void readProperties(final XMLStreamReader reader, final EntityInfoAggregator entitySet, //NOSONAR + final DeserializerProperties readProperties) + throws XMLStreamException, EdmException, EntityProviderException { + // validate namespace + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_PROPERTIES); + if (entitySet.getEntityType().hasStream()) { + // external properties + checkCurrentHandledStartTag(FormatXml.M_PROPERTIES); + } else { + // inline properties + checkCurrentHandledStartTag(FormatXml.ATOM_CONTENT); + } + + EntityPropertyInfo property; + XmlPropertyDeserializer xpc = new XmlPropertyDeserializer(); + + String closeTag = null; + boolean run = true; + reader.next(); + + while (run) { + if (reader.isStartElement() && closeTag == null) { + closeTag = reader.getLocalName(); + if (isEdmNamespaceProperty(reader)) { + if (properties.containsKey(closeTag)) { + throw new EntityProviderException(EntityProviderException.DOUBLE_PROPERTY.addContent(closeTag)); + } + property = getValidatedPropertyInfo(entitySet, closeTag); + final Object value = xpc.readStartedElement(reader, closeTag, property, typeMappings, readProperties); + properties.put(closeTag, value); + closeTag = null; + } + } else if (reader.isEndElement()) { + if (reader.getLocalName().equals(closeTag)) { + closeTag = null; + } else if (Edm.NAMESPACE_M_2007_08.equals(reader.getNamespaceURI()) + && FormatXml.M_PROPERTIES.equals(reader.getLocalName())) { + run = false; + } + } + reader.next(); + } + } + + /** + * Check if the {@link #currentHandledStartTagName} is the same as the <code>expectedTagName</code>. + * If tag name is not as expected or if {@link #currentHandledStartTagName} is not set an + * {@link EntityProviderException} is thrown. + * + * @param expectedTagName expected name for {@link #currentHandledStartTagName} + * @throws EntityProviderException if tag name is not as expected or if {@link #currentHandledStartTagName} is + * <code>NULL</code>. + */ + private void checkCurrentHandledStartTag(final String expectedTagName) throws EntityProviderException { + if (currentHandledStartTagName == null) { + throw new EntityProviderException(EntityProviderException.INVALID_STATE + .addContent("No current handled start tag name set.")); + } else if (!currentHandledStartTagName.equals(expectedTagName)) { + throw new EntityProviderException(EntityProviderException.INVALID_PARENT_TAG.addContent(expectedTagName) + .addContent(currentHandledStartTagName)); + } + } + + /** + * Checks if property of currently read tag in {@link XMLStreamReader} is defined in + * <code>edm properties namespace</code> {@value Edm#NAMESPACE_D_2007_08}. + * + * If no namespace uri definition is found for namespace prefix of property (<code>tag</code>) an exception is thrown. + * + * @param reader {@link XMLStreamReader} with position at to checked tag + * @return <code>true</code> if property is in <code>edm properties namespace</code>, otherwise <code>false</code>. + * @throws EntityProviderException If no namespace uri definition is found for namespace prefix of property + * (<code>tag</code>). + */ + private boolean isEdmNamespaceProperty(final XMLStreamReader reader) throws EntityProviderException { + final String nsUri = reader.getNamespaceURI(); + if (nsUri == null) { + throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(reader.getLocalName())); + } else { + return Edm.NAMESPACE_D_2007_08.equals(nsUri); + } + } + + /** + * Get validated {@link EntityPropertyInfo} for property with given <code>name</code>. + * If validation fails an {@link EntityProviderException} is thrown. + * + * Currently this is the case if no {@link EntityPropertyInfo} if found for given <code>name</code>. + * + * @param entitySet + * @param name + * @return valid {@link EntityPropertyInfo} (which is never <code>NULL</code>). + * @throws EntityProviderException + */ + private EntityPropertyInfo getValidatedPropertyInfo(final EntityInfoAggregator entitySet, final String name) + throws EntityProviderException { + EntityPropertyInfo info = entitySet.getPropertyInfo(name); + if (info == null) { + throw new EntityProviderException(EntityProviderException.INVALID_PROPERTY.addContent(name)); + } + return info; + } +}
http://git-wip-us.apache.org/repos/asf/olingo-odata2/blob/9e949e40/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlErrorDocumentDeserializer.java ---------------------------------------------------------------------- diff --git a/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlErrorDocumentDeserializer.java b/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlErrorDocumentDeserializer.java new file mode 100644 index 0000000..dd22aa5 --- /dev/null +++ b/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlErrorDocumentDeserializer.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.olingo.odata2.client.core.ep.deserializer; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.olingo.odata2.api.edm.Edm; +import org.apache.olingo.odata2.api.ep.EntityProviderException; +import org.apache.olingo.odata2.api.processor.ODataErrorContext; +import org.apache.olingo.odata2.core.commons.ContentType; +import org.apache.olingo.odata2.core.commons.XmlHelper; +import org.apache.olingo.odata2.core.ep.util.FormatXml; + +/** + * Consuming (read / deserialization) for OData error document in XML format. + */ +public class XmlErrorDocumentDeserializer { + /** + * Map containing language code (language - country) to Locale mapping + * based on Locale.getAvailableLocales() + * */ + private static final Map<String, Locale> AVAILABLE_LOCALES = new HashMap<String, Locale>(); + static { + Locale[] locales = Locale.getAvailableLocales(); + for (Locale l : locales) { + AVAILABLE_LOCALES.put(l.getLanguage() + "-" + l.getCountry(), l); + } + } + + /** + * Deserialize / read OData error document in ODataErrorContext. + * + * @param errorDocument OData error document in XML format + * @return created ODataErrorContext based on input stream content. + * @throws EntityProviderException if an exception during read / deserialization occurs. + */ + public ODataErrorContext readError(final InputStream errorDocument) throws EntityProviderException { + XMLStreamReader reader = null; + EntityProviderException cachedException = null; + + try { + reader = XmlHelper.createStreamReader(errorDocument); + return parserError(reader); + } catch (XMLStreamException e) { + cachedException = new EntityProviderException(EntityProviderException.INVALID_STATE.addContent( + e.getMessage()), e); + throw cachedException; + } catch (EntityProviderException e) { + cachedException = e; + throw cachedException; + } finally {//NOPMD - suppressed + if (reader != null) { + try { + reader.close(); + } catch (XMLStreamException e) { + if (cachedException != null) { + throw cachedException; //NOSONAR + } else { + throw new EntityProviderException( //NOSONAR + EntityProviderException.EXCEPTION_OCCURRED.addContent( + e.getClass().getSimpleName()), e); + } + } + } + } + } + + private ODataErrorContext parserError(final XMLStreamReader reader) + throws XMLStreamException, EntityProviderException { + // read xml tag + reader.require(XMLStreamConstants.START_DOCUMENT, null, null); + reader.nextTag(); + + // read error tag + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_ERROR); + + // read error data + boolean codeFound = false; + boolean messageFound = false; + ODataErrorContext errorContext = new ODataErrorContext(); + while (notFinished(reader)) { + reader.nextTag(); + if (reader.isStartElement()) { + String name = reader.getLocalName(); + if (FormatXml.M_CODE.equals(name)) { + codeFound = true; + handleCode(reader, errorContext); + } else if (FormatXml.M_MESSAGE.equals(name)) { + messageFound = true; + handleMessage(reader, errorContext); + } else if (FormatXml.M_INNER_ERROR.equals(name)) { + handleInnerError(reader, errorContext); + } else { + throw new EntityProviderException( + EntityProviderException.INVALID_CONTENT.addContent(name, FormatXml.M_ERROR)); + } + } + } + validate(codeFound, messageFound); + + errorContext.setContentType(ContentType.APPLICATION_XML.toContentTypeString()); + return errorContext; + } + + private void validate(final boolean codeFound, final boolean messageFound) throws EntityProviderException { + if (!codeFound) { + throw new EntityProviderException( + EntityProviderException.MISSING_PROPERTY.addContent("Mandatory 'code' property not found.'")); + } else if (!messageFound) { + throw new EntityProviderException( + EntityProviderException.MISSING_PROPERTY.addContent("Mandatory 'message' property not found.'")); + } + } + + private boolean notFinished(final XMLStreamReader reader) throws XMLStreamException { + return notFinished(reader, FormatXml.M_ERROR); + } + + private boolean notFinished(final XMLStreamReader reader, final String tagName) throws XMLStreamException { + boolean finished = reader.isEndElement() && tagName.equals(reader.getLocalName()); + return !finished && reader.hasNext(); + } + + private void handleInnerError(final XMLStreamReader reader, final ODataErrorContext errorContext) + throws XMLStreamException { + + StringBuilder sb = new StringBuilder(); + while (notFinished(reader, FormatXml.M_INNER_ERROR)) { + if (reader.hasName() && !FormatXml.M_INNER_ERROR.equals(reader.getLocalName())) { + sb.append("<"); + if (reader.isEndElement()) { + sb.append("/"); + } + sb.append(reader.getLocalName()).append(">"); + } else if (reader.isCharacters()) { + sb.append(reader.getText()); + } + reader.next(); + } + + errorContext.setInnerError(sb.toString()); + } + + private void handleMessage(final XMLStreamReader reader, final ODataErrorContext errorContext) + throws XMLStreamException { + String lang = reader.getAttributeValue(Edm.NAMESPACE_XML_1998, FormatXml.XML_LANG); + errorContext.setLocale(getLocale(lang)); + String message = reader.getElementText(); + errorContext.setMessage(message); + } + + private void handleCode(final XMLStreamReader reader, final ODataErrorContext errorContext) + throws XMLStreamException { + String code = reader.getElementText(); + errorContext.setErrorCode(code); + } + + private Locale getLocale(final String langValue) { + return AVAILABLE_LOCALES.get(langValue); + } +} http://git-wip-us.apache.org/repos/asf/olingo-odata2/blob/9e949e40/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlFeedDeserializer.java ---------------------------------------------------------------------- diff --git a/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlFeedDeserializer.java b/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlFeedDeserializer.java new file mode 100644 index 0000000..f47d303 --- /dev/null +++ b/odata2-lib/odata-client-core/src/main/java/org/apache/olingo/odata2/client/core/ep/deserializer/XmlFeedDeserializer.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.olingo.odata2.client.core.ep.deserializer; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.olingo.odata2.api.edm.Edm; +import org.apache.olingo.odata2.api.edm.EdmLiteralKind; +import org.apache.olingo.odata2.api.edm.EdmSimpleTypeException; +import org.apache.olingo.odata2.api.ep.EntityProviderException; +import org.apache.olingo.odata2.api.ep.entry.DeletedEntryMetadata; +import org.apache.olingo.odata2.api.ep.entry.ODataEntry; +import org.apache.olingo.odata2.api.ep.feed.ODataDeltaFeed; +import org.apache.olingo.odata2.client.api.ep.DeserializerProperties; +import org.apache.olingo.odata2.core.edm.EdmDateTimeOffset; +import org.apache.olingo.odata2.core.ep.aggregator.EntityInfoAggregator; +import org.apache.olingo.odata2.core.ep.entry.DeletedEntryMetadataImpl; +import org.apache.olingo.odata2.core.ep.feed.FeedMetadataImpl; +import org.apache.olingo.odata2.core.ep.feed.ODataDeltaFeedImpl; +import org.apache.olingo.odata2.core.ep.util.FormatXml; + +/** + * Atom/XML format reader/consumer for feeds. + * + * {@link XmlFeedDeserializer} instance use + * {@link XmlEntryDeserializer#readEntry(XMLStreamReader, EntityInfoAggregator, EntityProviderReadProperties)} for + * read/consume of several entries. + * + * + */ +public class XmlFeedDeserializer { + + /** + * + * @param reader + * @param eia + * @param readProperties + * @return {@link ODataDeltaFeed} object + * @throws EntityProviderException + */ + public ODataDeltaFeed readFeed(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties readProperties) throws EntityProviderException { + try { + // read xml tag + reader.require(XMLStreamConstants.START_DOCUMENT, null, null); + reader.nextTag(); + + // read feed tag + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_FEED); + Map<String, String> foundPrefix2NamespaceUri = extractNamespacesFromTag(reader); + foundPrefix2NamespaceUri.putAll(readProperties.getValidatedPrefixNamespaceUris()); + checkAllMandatoryNamespacesAvailable(foundPrefix2NamespaceUri); + DeserializerProperties entryReadProperties = + DeserializerProperties.initFrom(readProperties) + .addValidatedPrefixes(foundPrefix2NamespaceUri).build(); + + // read feed data (metadata and entries) + return readFeedData(reader, eia, entryReadProperties); + } catch (XMLStreamException e) { + throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass() + .getSimpleName()), e); + } + } + + /** + * Read all feed specific data (like <code>inline count</code> and <code>next link</code>) as well as all feed entries + * (<code>entry</code>) and delta feed extensions (tombstones). + * + * @param reader xml stream reader with xml content to be read + * @param eia entity infos for validation and mapping + * @param entryReadProperties properties which are used for read of feed. + * @return all feed specific data (like <code>inline count</code> and <code>next link</code>) as well as all feed + * entries (<code>entry</code>). + * @throws XMLStreamException if malformed xml is read in stream + * @throws EntityProviderException if xml contains invalid data (based on odata specification and edm definition) + */ + private ODataDeltaFeed readFeedData(final XMLStreamReader reader, final EntityInfoAggregator eia, + final DeserializerProperties entryReadProperties) throws XMLStreamException, EntityProviderException { + FeedMetadataImpl metadata = new FeedMetadataImpl(); + XmlEntryDeserializer xec = new XmlEntryDeserializer(); + List<ODataEntry> results = new ArrayList<ODataEntry>(); + List<DeletedEntryMetadata> deletedEntries = new ArrayList<DeletedEntryMetadata>(); + + while (reader.hasNext() && !isFeedEndTag(reader)) { + if (FormatXml.ATOM_ENTRY.equals(reader.getLocalName())) { + ODataEntry entry = xec.readEntry(reader, eia, entryReadProperties, true); + results.add(entry); + } else if (FormatXml.ATOM_TOMBSTONE_DELETED_ENTRY.equals(reader.getLocalName())) { + reader.require(XMLStreamConstants.START_ELEMENT, FormatXml.ATOM_TOMBSTONE_NAMESPACE, + FormatXml.ATOM_TOMBSTONE_DELETED_ENTRY); + + DeletedEntryMetadataImpl deletedEntryMetadata = readDeletedEntryMetadata(reader); + deletedEntries.add(deletedEntryMetadata); + reader.next(); + } else if (FormatXml.M_COUNT.equals(reader.getLocalName())) { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_COUNT); + readInlineCount(reader, metadata); + } else if (FormatXml.ATOM_LINK.equals(reader.getLocalName())) { + reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_LINK); + + final String rel = reader.getAttributeValue(null, FormatXml.ATOM_REL); + if (FormatXml.ATOM_NEXT_LINK.equals(rel)) { + final String uri = reader.getAttributeValue(null, FormatXml.ATOM_HREF); + metadata.setNextLink(uri); + } else if (FormatXml.ATOM_DELTA_LINK.equals(rel)) { + final String uri = reader.getAttributeValue(null, FormatXml.ATOM_HREF); + metadata.setDeltaLink(uri); + } + reader.next(); + } else { + reader.next(); + } + readTillNextStartTag(reader); + } + return new ODataDeltaFeedImpl(results, metadata, deletedEntries); + } + + private DeletedEntryMetadataImpl readDeletedEntryMetadata(final XMLStreamReader reader) + throws EntityProviderException { + try { + DeletedEntryMetadataImpl deletedEntryMetadata = new DeletedEntryMetadataImpl(); + + String uri = reader.getAttributeValue(null, FormatXml.ATOM_TOMBSTONE_REF); + String whenStr = reader.getAttributeValue(null, FormatXml.ATOM_TOMBSTONE_WHEN); + Date when; + when = EdmDateTimeOffset.getInstance().valueOfString(whenStr, EdmLiteralKind.DEFAULT, null, + Date.class); + + deletedEntryMetadata.setUri(uri); + deletedEntryMetadata.setWhen(when); + return deletedEntryMetadata; + } catch (EdmSimpleTypeException e) { + throw new EntityProviderException(EntityProviderException.INVALID_DELETED_ENTRY_METADATA, e); + } + } + + private void readInlineCount(final XMLStreamReader reader, final FeedMetadataImpl metadata) + throws XMLStreamException, + EntityProviderException { + String inlineCountString = reader.getElementText(); + if (inlineCountString != null) { + try { + int inlineCountNumber = Integer.parseInt(inlineCountString); + if (inlineCountNumber >= 0) { + metadata.setInlineCount(inlineCountNumber); + } else { + throw new EntityProviderException(EntityProviderException.INLINECOUNT_INVALID + .addContent(inlineCountNumber)); + } + } catch (NumberFormatException e) { + throw new EntityProviderException(EntityProviderException.INLINECOUNT_INVALID.addContent(""), e); + } + } + } + + private void readTillNextStartTag(final XMLStreamReader reader) throws XMLStreamException { + while (reader.hasNext() && !reader.isStartElement()) { + reader.next(); + } + } + + private boolean isFeedEndTag(final XMLStreamReader reader) { + return reader.isEndElement() + && Edm.NAMESPACE_ATOM_2005.equals(reader.getNamespaceURI()) + && FormatXml.ATOM_FEED.equals(reader.getLocalName()); + } + + /** + * Maps all all found namespaces of current xml tag into a map. + * + * @param reader xml reader with current position at a xml tag + * @return map with all found namespaces of current xml tag + */ + private Map<String, String> extractNamespacesFromTag(final XMLStreamReader reader) { + // collect namespaces + Map<String, String> foundPrefix2NamespaceUri = new HashMap<String, String>(); + int namespaceCount = reader.getNamespaceCount(); + for (int i = 0; i < namespaceCount; i++) { + String namespacePrefix = reader.getNamespacePrefix(i); + String namespaceUri = reader.getNamespaceURI(i); + + foundPrefix2NamespaceUri.put(namespacePrefix, namespaceUri); + } + return foundPrefix2NamespaceUri; + } + + /** + * + * @param foundPrefix2NamespaceUri + * @throws EntityProviderException + */ + private void checkAllMandatoryNamespacesAvailable(final Map<String, String> foundPrefix2NamespaceUri) + throws EntityProviderException { + if (!foundPrefix2NamespaceUri.containsValue(Edm.NAMESPACE_D_2007_08)) { + throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(Edm.NAMESPACE_D_2007_08)); + } else if (!foundPrefix2NamespaceUri.containsValue(Edm.NAMESPACE_M_2007_08)) { + throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(Edm.NAMESPACE_M_2007_08)); + } else if (!foundPrefix2NamespaceUri.containsValue(Edm.NAMESPACE_ATOM_2005)) { + throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(Edm.NAMESPACE_ATOM_2005)); + } + } +}