/*
 * 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.client.core.serialization;

import java.io.Writer;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.olingo.client.api.data.ResWrap;
import org.apache.olingo.client.api.serialization.ODataSerializer;
import org.apache.olingo.client.api.serialization.ODataSerializerException;
import org.apache.olingo.commons.api.Constants;
import org.apache.olingo.commons.api.data.AbstractODataObject;
import org.apache.olingo.commons.api.data.Annotation;
import org.apache.olingo.commons.api.data.ComplexValue;
import org.apache.olingo.commons.api.data.ContextURL;
import org.apache.olingo.commons.api.data.Entity;
import org.apache.olingo.commons.api.data.EntityCollection;
import org.apache.olingo.commons.api.data.Link;
import org.apache.olingo.commons.api.data.Operation;
import org.apache.olingo.commons.api.data.Property;
import org.apache.olingo.commons.api.data.ValueType;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.geo.Geospatial;
import org.apache.olingo.commons.api.ex.ODataRuntimeException;
import org.apache.olingo.commons.api.format.ContentType;
import org.apache.olingo.commons.core.edm.EdmTypeInfo;
import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory;

import com.fasterxml.aalto.stax.OutputFactoryImpl;

public class AtomSerializer implements ODataSerializer {

  private static final String TYPE_TEXT = "text";

  private static final XMLOutputFactory FACTORY = new OutputFactoryImpl();

  private final AtomGeoValueSerializer geoSerializer;
  private final boolean serverMode;

  public AtomSerializer() {
    this(false);
  }

  public AtomSerializer(final boolean serverMode) {
    geoSerializer = new AtomGeoValueSerializer();
    this.serverMode = serverMode;
  }

  protected void namespaces(XMLStreamWriter writer) throws XMLStreamException {
    writer.writeNamespace("", Constants.NS_ATOM);
    writer.writeNamespace(XMLConstants.XML_NS_PREFIX, XMLConstants.XML_NS_URI);
    writer.writeNamespace(Constants.PREFIX_METADATA, Constants.NS_METADATA);
    writer.writeNamespace(Constants.PREFIX_DATASERVICES, Constants.NS_DATASERVICES);
    writer.writeNamespace(Constants.PREFIX_GML, Constants.NS_GML);
    writer.writeNamespace(Constants.PREFIX_GEORSS, Constants.NS_GEORSS);
  }

  private void collection(final XMLStreamWriter writer,
      final ValueType valueType, final EdmPrimitiveTypeKind kind, final List<?> value)
          throws XMLStreamException, EdmPrimitiveTypeException {
    for (Object item : value) {
      writer.writeStartElement(Constants.PREFIX_METADATA, Constants.ELEM_ELEMENT, Constants.NS_METADATA);
      value(writer, valueType, kind, item);
      writer.writeEndElement();
    }
  }

  private void value(final XMLStreamWriter writer,
      final ValueType valueType, final EdmPrimitiveTypeKind kind, final Object value)
          throws XMLStreamException, EdmPrimitiveTypeException {
    if (value == null || (valueType == ValueType.COMPLEX && ((ComplexValue) value).getValue().isEmpty())) {
      writer.writeAttribute(Constants.PREFIX_METADATA, Constants.NS_METADATA,
          Constants.ATTR_NULL, Boolean.TRUE.toString());
      return;
    }
    switch (valueType) {
    case PRIMITIVE:
      final EdmPrimitiveTypeKind valueKind = kind == null ? EdmTypeInfo.determineTypeKind(value) : kind;
      if (valueKind == null) {
        if (serverMode) {
          throw new EdmPrimitiveTypeException("The primitive type could not be determined.");
        } else {
          writer.writeCharacters(value.toString()); // This might not be valid OData.
        }
      } else {
        writer.writeCharacters(EdmPrimitiveTypeFactory.getInstance(valueKind) // TODO: add facets
            .valueToString(value, null, null, Constants.DEFAULT_PRECISION, Constants.DEFAULT_SCALE, null));
      }
      break;
    case ENUM:
      writer.writeCharacters(value.toString());
      break;
    case GEOSPATIAL:
      geoSerializer.serialize(writer, (Geospatial) value);
      break;
    case COLLECTION_PRIMITIVE:
    case COLLECTION_GEOSPATIAL:
    case COLLECTION_ENUM:
    case COLLECTION_COMPLEX:
      collection(writer, valueType.getBaseType(), kind, (List<?>) value);
      break;
    case COMPLEX:
      if (((ComplexValue) value).getTypeName() != null) {
        EdmTypeInfo typeInfo = new EdmTypeInfo.Builder().
            setTypeExpression(((ComplexValue) value).getTypeName()).build();
        writer.writeAttribute(Constants.PREFIX_METADATA, Constants.NS_METADATA,
            Constants.ATTR_TYPE, typeInfo.external());
      }
      for (Property property : ((ComplexValue) value).getValue()) {
        property(writer, property, false);
      }
      break;
    case ENTITY:
    case COLLECTION_ENTITY:
      throw new ODataRuntimeException("Entities cannot appear in this payload");
    }
  }

  public void property(final XMLStreamWriter writer, final Property property, final boolean standalone)
      throws XMLStreamException, EdmPrimitiveTypeException {

    if (standalone) {
      writer.writeStartElement(Constants.PREFIX_METADATA, Constants.VALUE, Constants.NS_DATASERVICES);
      namespaces(writer);
    } else {
      writer.writeStartElement(Constants.PREFIX_DATASERVICES, property.getName(), Constants.NS_DATASERVICES);
    }

    EdmTypeInfo typeInfo = null;
    if (property.getType() != null && !"COMPLEX".equalsIgnoreCase(property.getValueType().name())) {
      typeInfo = new EdmTypeInfo.Builder().setTypeExpression(property.getType()).build();
      if (!EdmPrimitiveTypeKind.String.getFullQualifiedName().toString().equals(typeInfo.internal())) {
        writer.writeAttribute(Constants.PREFIX_METADATA, Constants.NS_METADATA,
            Constants.ATTR_TYPE, typeInfo.external());
      }
    }

    value(writer, property.getValueType(),
        typeInfo == null ? null : typeInfo.getPrimitiveTypeKind(),
        property.getValue());
    if (!property.isNull() && property.isComplex() && !property.isCollection()) {
      links(writer, property.asComplex().getAssociationLinks());
      if (serverMode) {
        links(writer, property.asComplex().getNavigationLinks());
      } else {
        writeNavigationLinks(writer, property.asComplex().getNavigationLinks());
      }
    }

    writer.writeEndElement();

    for (Annotation annotation : property.getAnnotations()) {
      annotation(writer, annotation, property.getName());
    }
  }

  private void property(final XMLStreamWriter writer, final Property property)
      throws XMLStreamException, EdmPrimitiveTypeException {
    property(writer, property, true);
  }

  private void startDocument(final XMLStreamWriter writer, final String rootElement) throws XMLStreamException {
    writer.writeStartDocument();
    writer.setDefaultNamespace(Constants.NS_ATOM);
    writer.writeStartElement(rootElement);

    namespaces(writer);
  }

  private void property(final Writer outWriter, final Property property)
      throws XMLStreamException, EdmPrimitiveTypeException {
    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    writer.writeStartDocument();

    property(writer, property);

    writer.writeEndDocument();
    writer.flush();
  }
  
  private boolean isEntitySetNavigation(final Link link) {
    return Constants.ENTITY_SET_NAVIGATION_LINK_TYPE.equals(link.getType());
  }
  
  private void writeNavigationLinks(final XMLStreamWriter writer, final List<Link> links)
      throws XMLStreamException, EdmPrimitiveTypeException {
    final Map<String, List<String>> entitySetLinks = new HashMap<>();

    for (Link link : links) {
    
      if (link.getInlineEntity() != null || link.getInlineEntitySet() != null) {
        writeLink(writer, link, new ExtraContent() {
          @Override
          public void write(XMLStreamWriter writer, Link link) throws XMLStreamException, EdmPrimitiveTypeException {
            writer.writeStartElement(Constants.PREFIX_METADATA, Constants.ATOM_ELEM_INLINE, Constants.NS_METADATA);
            if (link.getInlineEntity() != null) {
              writer.writeStartElement(Constants.NS_ATOM, Constants.ATOM_ELEM_ENTRY);
              entity(writer, link.getInlineEntity());
              writer.writeEndElement();
            }
            if (link.getInlineEntitySet() != null) {
              writer.writeStartElement(Constants.NS_ATOM, Constants.ATOM_ELEM_FEED);
              entitySet(writer, link.getInlineEntitySet());
              writer.writeEndElement();
            }
            writer.writeEndElement(); // inline    
          }
        });
        
      } else if (link.getBindingLink() != null) {
        writeLink(writer, link, new ExtraContent() {
          @Override
          public void write(XMLStreamWriter writer, Link link) throws XMLStreamException, EdmPrimitiveTypeException {
            writer.writeAttribute(Constants.ATTR_HREF, link.getBindingLink());
          }
        });
      } else if (link.getBindingLinks() != null && !link.getBindingLinks().isEmpty()) {
        writeLink(writer, link, new ExtraContent() {
          @Override
          public void write(XMLStreamWriter writer, Link link) throws XMLStreamException, EdmPrimitiveTypeException {
            writer.writeStartElement(Constants.PREFIX_METADATA, Constants.ATOM_ELEM_INLINE, Constants.NS_METADATA);
            writer.writeStartElement(Constants.NS_ATOM, Constants.ATOM_ELEM_FEED);
            for (String binding:link.getBindingLinks()) {            
              Entity entity = new Entity();
              entity.setId(URI.create(binding));
              inlineEntityRef(writer, entity);                      
            }
            writer.writeEndElement(); //feed            
            writer.writeEndElement(); //inline
          }
        });
      } else {
        if (isEntitySetNavigation(link)) {
          final List<String> uris;
          if (entitySetLinks.containsKey(link.getTitle())) {
            uris = entitySetLinks.get(link.getTitle());
          } else {
            uris = new ArrayList<>();
            entitySetLinks.put(link.getTitle(), uris);
          }
          if (link.getHref() != null) {
            uris.add(link.getHref());
          }
        } else {
          writeLink(writer, link, new ExtraContent() {
            @Override
            public void write(XMLStreamWriter writer, Link link) 
                throws XMLStreamException, EdmPrimitiveTypeException {
            }
          });
        }
      }
    }
    for (Entry<String, List<String>> entry : entitySetLinks.entrySet()) {
      final List<String>entitySetLink = entry.getValue();
      if (!entitySetLink.isEmpty()) {
        Link link = new Link();
        link.setTitle(entry.getKey());
        link.setType(Constants.ENTITY_SET_NAVIGATION_LINK_TYPE);
        link.setRel(Constants.NS_NAVIGATION_LINK_REL+entry.getKey());

        writeLink(writer, link, new ExtraContent() {
          @Override
          public void write(XMLStreamWriter writer, Link link) throws XMLStreamException, EdmPrimitiveTypeException {
            writer.writeStartElement(Constants.PREFIX_METADATA, Constants.ATOM_ELEM_INLINE, Constants.NS_METADATA);
            writer.writeStartElement(Constants.NS_ATOM, Constants.ATOM_ELEM_FEED);
            for (String binding:entitySetLink) {            
              Entity entity = new Entity();
              entity.setId(URI.create(binding));
              inlineEntityRef(writer, entity);                      
            }
            writer.writeEndElement();    
            writer.writeEndElement();
          }
        });                
      }
    }   
  }

  private void links(final XMLStreamWriter writer, final List<Link> links)
      throws XMLStreamException, EdmPrimitiveTypeException {
    for (Link link : links) {
      writeLink(writer, link, new ExtraContent() {
        @Override
        public void write(XMLStreamWriter writer, Link link) 
            throws XMLStreamException, EdmPrimitiveTypeException {
        }
      });
    }
  }
  
  interface ExtraContent {
    void write(final XMLStreamWriter writer, final Link link) 
        throws XMLStreamException, EdmPrimitiveTypeException;
  }
  
  private void writeLink(final XMLStreamWriter writer, final Link link, final ExtraContent content)
      throws XMLStreamException, EdmPrimitiveTypeException {
    writer.writeStartElement(Constants.ATOM_ELEM_LINK);

    if (link.getRel() != null) {
      writer.writeAttribute(Constants.ATTR_REL, link.getRel());
    }
    if (link.getTitle() != null) {
      writer.writeAttribute(Constants.ATTR_TITLE, link.getTitle());
    }
    if (link.getHref() != null) {
      writer.writeAttribute(Constants.ATTR_HREF, link.getHref());
    }
    if (link.getType() != null) {
      writer.writeAttribute(Constants.ATTR_TYPE, link.getType());
    }

    content.write(writer, link);

    for (Annotation annotation : link.getAnnotations()) {
      annotation(writer, annotation, null);
    }
    writer.writeEndElement();    
  }  

  private void common(final XMLStreamWriter writer, final AbstractODataObject object) throws XMLStreamException {
    if (object.getTitle() != null) {
      writer.writeStartElement(Constants.ATOM_ELEM_TITLE);
      writer.writeAttribute(Constants.ATTR_TYPE, TYPE_TEXT);
      writer.writeCharacters(object.getTitle());
      writer.writeEndElement();
    }
  }

  private void properties(final XMLStreamWriter writer, final List<Property> properties)
      throws XMLStreamException, EdmPrimitiveTypeException {
    for (Property property : properties) {
      property(writer, property, false);
    }
  }

  private void annotation(final XMLStreamWriter writer, final Annotation annotation, final String target)
      throws XMLStreamException, EdmPrimitiveTypeException {

    writer.writeStartElement(Constants.PREFIX_METADATA, Constants.ANNOTATION, Constants.NS_METADATA);

    writer.writeAttribute(Constants.ATOM_ATTR_TERM, annotation.getTerm());

    if (target != null) {
      writer.writeAttribute(Constants.ATTR_TARGET, target);
    }

    EdmTypeInfo typeInfo = null;
    if (annotation.getType() != null) {
      typeInfo = new EdmTypeInfo.Builder().setTypeExpression(annotation.getType()).build();
      if (!EdmPrimitiveTypeKind.String.getFullQualifiedName().toString().equals(typeInfo.internal())) {
        writer.writeAttribute(Constants.PREFIX_METADATA, Constants.NS_METADATA,
            Constants.ATTR_TYPE, typeInfo.external());
      }
    }

    value(writer, annotation.getValueType(),
        typeInfo == null ? EdmTypeInfo.determineTypeKind(annotation.getValue()) : typeInfo.getPrimitiveTypeKind(),
        annotation.getValue());

    writer.writeEndElement();
  }

  private void entity(final XMLStreamWriter writer, final Entity entity)
      throws XMLStreamException, EdmPrimitiveTypeException {
    if (entity.getBaseURI() != null) {
      writer.writeAttribute(XMLConstants.XML_NS_URI, Constants.ATTR_XML_BASE, entity.getBaseURI().toASCIIString());
    }

    if (serverMode && entity.getETag() != null) {
      writer.writeAttribute(Constants.NS_METADATA, Constants.ATOM_ATTR_ETAG, entity.getETag());
    }

    if (entity.getId() != null) {
      writer.writeStartElement(Constants.ATOM_ELEM_ID);
      writer.writeCharacters(entity.getId().toASCIIString());
      writer.writeEndElement();
    }

    writer.writeStartElement(Constants.ATOM_ELEM_CATEGORY);
    writer.writeAttribute(Constants.ATOM_ATTR_SCHEME, Constants.NS_SCHEME);
    if (entity.getType() != null) {
      writer.writeAttribute(Constants.ATOM_ATTR_TERM,
          new EdmTypeInfo.Builder().setTypeExpression(entity.getType()).build().external());
    }
    writer.writeEndElement();

    common(writer, entity);

    if (serverMode) {
      if (entity.getEditLink() != null) {
        links(writer, Collections.singletonList(entity.getEditLink()));
      }

      if (entity.getSelfLink() != null) {
        links(writer, Collections.singletonList(entity.getSelfLink()));
      }
    }

    links(writer, entity.getAssociationLinks());
    if (serverMode) {
      links(writer, entity.getNavigationLinks());
    } else {
      writeNavigationLinks(writer, entity.getNavigationLinks());
      writeNavigationLinks(writer, entity.getNavigationBindings());
    }
    links(writer, entity.getMediaEditLinks());

    if (serverMode) {
      for (Operation operation : entity.getOperations()) {
        writer.writeStartElement(Constants.NS_METADATA, Constants.ATOM_ELEM_ACTION);
        writer.writeAttribute(Constants.ATTR_METADATA, operation.getMetadataAnchor());
        writer.writeAttribute(Constants.ATTR_TITLE, operation.getTitle());
        writer.writeAttribute(Constants.ATTR_TARGET, operation.getTarget().toASCIIString());
        writer.writeEndElement();
      }
    }

    writer.writeStartElement(Constants.ATOM_ELEM_CONTENT);
    if (entity.isMediaEntity()) {
      if (entity.getMediaContentType() != null) {
        writer.writeAttribute(Constants.ATTR_TYPE, entity.getMediaContentType());
      }
      if (entity.getMediaContentSource() != null) {
        writer.writeAttribute(Constants.ATOM_ATTR_SRC, entity.getMediaContentSource().toASCIIString());
      }
      writer.writeEndElement();

      writer.writeStartElement(Constants.NS_METADATA, Constants.PROPERTIES);
      properties(writer, entity.getProperties());
    } else {
      writer.writeAttribute(Constants.ATTR_TYPE, ContentType.APPLICATION_XML.toContentTypeString());
      writer.writeStartElement(Constants.NS_METADATA, Constants.PROPERTIES);
      properties(writer, entity.getProperties());
      writer.writeEndElement();
    }
    writer.writeEndElement();

    for (Annotation annotation : entity.getAnnotations()) {
      annotation(writer, annotation, null);
    }
  }

  private void inlineEntityRef(final XMLStreamWriter writer, final Entity entity) throws XMLStreamException {
    writer.writeStartElement(Constants.NS_METADATA, Constants.ATOM_ELEM_ENTRY_REF);
    writer.writeAttribute(Constants.ATOM_ATTR_ID, entity.getId().toASCIIString());
    writer.writeEndElement();
  }
  
  private void entityRef(final XMLStreamWriter writer, final Entity entity) throws XMLStreamException {
    writer.writeStartElement(Constants.ATOM_ELEM_ENTRY_REF);
    writer.writeDefaultNamespace(Constants.NS_METADATA);
    writer.writeAttribute(Constants.ATOM_ATTR_ID, entity.getId().toASCIIString());
    writer.writeEndElement();
  }

  private void entityRef(final XMLStreamWriter writer, final ResWrap<Entity> container) throws XMLStreamException {
    writer.writeStartElement(Constants.ATOM_ELEM_ENTRY_REF);
    writer.writeDefaultNamespace(Constants.NS_METADATA);
    addContextInfo(writer, container);
    writer.writeAttribute(Constants.ATOM_ATTR_ID, container.getPayload().getId().toASCIIString());
  }

  private void entity(final Writer outWriter, final Entity entity)
      throws XMLStreamException, EdmPrimitiveTypeException {
    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    if (entity.getType() == null && entity.getProperties().isEmpty()) {
      writer.writeStartDocument();
      writer.setDefaultNamespace(Constants.NS_METADATA);
      entityRef(writer, entity);
    } else {
      startDocument(writer, Constants.ATOM_ELEM_ENTRY);
      entity(writer, entity);
    }
    writer.writeEndDocument();
    writer.flush();
  }

  private void entity(final Writer outWriter, final ResWrap<Entity> container)
      throws XMLStreamException, EdmPrimitiveTypeException {
    final Entity entity = container.getPayload();

    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    if (entity.getType() == null && entity.getProperties().isEmpty()) {
      writer.writeStartDocument();
      writer.setDefaultNamespace(Constants.NS_METADATA);

      entityRef(writer, container);
    } else {
      startDocument(writer, Constants.ATOM_ELEM_ENTRY);

      addContextInfo(writer, container);

      entity(writer, entity);
    }

    writer.writeEndElement();
    writer.writeEndDocument();
    writer.flush();
  }

  private void entitySet(final XMLStreamWriter writer, final EntityCollection entitySet)
      throws XMLStreamException, EdmPrimitiveTypeException {
    if (entitySet.getBaseURI() != null) {
      writer.writeAttribute(XMLConstants.XML_NS_URI, Constants.ATTR_XML_BASE, entitySet.getBaseURI().toASCIIString());
    }

    if (entitySet.getCount() != null) {
      writer.writeStartElement(Constants.NS_METADATA, Constants.ATOM_ELEM_COUNT);
      writer.writeCharacters(Integer.toString(entitySet.getCount()));
      writer.writeEndElement();
    }

    if (entitySet.getId() != null) {
      writer.writeStartElement(Constants.ATOM_ELEM_ID);
      writer.writeCharacters(entitySet.getId().toASCIIString());
      writer.writeEndElement();
    }

    common(writer, entitySet);

    for (Entity entity : entitySet) {
      if (entity.getType() == null && entity.getProperties().isEmpty()) {
        entityRef(writer, entity);
      } else {
        writer.writeStartElement(Constants.ATOM_ELEM_ENTRY);
        entity(writer, entity);
        writer.writeEndElement();
      }
    }

    if (serverMode) {
      if (entitySet.getNext() != null) {
        final Link next = new Link();
        next.setRel(Constants.NEXT_LINK_REL);
        next.setHref(entitySet.getNext().toASCIIString());

        links(writer, Collections.<Link> singletonList(next));
      }
      if (entitySet.getDeltaLink() != null) {
        final Link next = new Link();
        next.setRel(Constants.NS_DELTA_LINK_REL);
        next.setHref(entitySet.getDeltaLink().toASCIIString());

        links(writer, Collections.<Link> singletonList(next));
      }
    }
  }

  private void entitySet(final Writer outWriter, final EntityCollection entitySet)
      throws XMLStreamException, EdmPrimitiveTypeException {
    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    startDocument(writer, Constants.ATOM_ELEM_FEED);

    entitySet(writer, entitySet);

    writer.writeEndElement();
    writer.writeEndDocument();
    writer.flush();
  }

  private void entitySet(final Writer outWriter, final ResWrap<EntityCollection> entitySet)
      throws XMLStreamException, EdmPrimitiveTypeException {
    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    startDocument(writer, Constants.ATOM_ELEM_FEED);

    addContextInfo(writer, entitySet);

    entitySet(writer, entitySet.getPayload());

    writer.writeEndElement();
    writer.writeEndDocument();
    writer.flush();
  }

  private void link(final Writer outWriter, final Link link) throws XMLStreamException {
    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    writer.writeStartDocument();

    writer.writeStartElement(Constants.ELEM_LINKS);
    writer.writeDefaultNamespace(Constants.NS_DATASERVICES);

    writer.writeStartElement(Constants.ELEM_URI);
    writer.writeCharacters(link.getHref());
    writer.writeEndElement();

    writer.writeEndElement();

    writer.writeEndDocument();
    writer.flush();
  }

  @Override
  public <T> void write(final Writer writer, final T obj) throws ODataSerializerException {
    try {
      if (obj instanceof EntityCollection) {
        entitySet(writer, (EntityCollection) obj);
      } else if (obj instanceof Entity) {
        entity(writer, (Entity) obj);
      } else if (obj instanceof Property) {
        property(writer, (Property) obj);
      } else if (obj instanceof Link) {
        link(writer, (Link) obj);
      }
    } catch (final XMLStreamException | EdmPrimitiveTypeException e) {
      throw new ODataSerializerException(e);
    }
  }

  private void reference(final Writer outWriter, final ResWrap<URI> container) throws XMLStreamException {
    final XMLStreamWriter writer = FACTORY.createXMLStreamWriter(outWriter);

    writer.writeStartDocument();

    writer.writeStartElement(Constants.ATTR_METADATA, Constants.ATTR_REF, Constants.NS_METADATA);
    writer.writeNamespace(Constants.ATTR_METADATA, Constants.NS_METADATA);
    writer.writeAttribute(Constants.ATTR_METADATA, Constants.NS_METADATA, Constants.CONTEXT, 
        container.getContextURL().toASCIIString());
    writer.writeAttribute(Constants.ATOM_ATTR_ID, container.getPayload().toASCIIString());
    writer.writeEndElement();

    writer.writeEndDocument();
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> void write(final Writer writer, final ResWrap<T> container) throws ODataSerializerException {
    final T obj = container == null ? null : container.getPayload();

    try {
      if (obj instanceof EntityCollection) {
        this.entitySet(writer, (ResWrap<EntityCollection>) container);
      } else if (obj instanceof Entity) {
        entity(writer, (ResWrap<Entity>) container);
      } else if (obj instanceof Property) {
        property(writer, (Property) obj);
      } else if (obj instanceof Link) {
        link(writer, (Link) obj);
      } else if (obj instanceof URI) {
        reference(writer, (ResWrap<URI>) container);
      }
    } catch (final XMLStreamException | EdmPrimitiveTypeException e) {
      throw new ODataSerializerException(e);
    }
  }

  private <T> void addContextInfo(
      final XMLStreamWriter writer, final ResWrap<T> container) throws XMLStreamException {

    if (container.getContextURL() != null) {
      final ContextURL contextURL = ContextURLParser.parse(container.getContextURL());
      final URI base = contextURL.getServiceRoot();
      if (container.getPayload() instanceof EntityCollection) {
        ((EntityCollection) container.getPayload()).setBaseURI(base);
      }
      if (container.getPayload() instanceof Entity) {
        ((Entity) container.getPayload()).setBaseURI(base);
      }

      writer.writeAttribute(Constants.NS_METADATA, Constants.CONTEXT,
          container.getContextURL().toASCIIString());
    }

    if (container.getMetadataETag() != null) {
      writer.writeAttribute(Constants.NS_METADATA, Constants.ATOM_ATTR_METADATAETAG,
          container.getMetadataETag());
    }
  }
}
