/*
 * 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.fit.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.ws.rs.NotFoundException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.olingo.fit.metadata.Metadata;
import org.apache.olingo.fit.metadata.NavigationProperty;

import static javax.xml.stream.XMLInputFactory.*;

public class XMLUtilities extends AbstractUtilities {

  private static final Pattern ENTITY_URI_PATTERN = Pattern.compile(".*\\/.*\\(.*\\)");

  protected static XMLInputFactory ifactory = null;

  protected static XMLOutputFactory ofactory = null;

  public XMLUtilities(final Metadata metadata) throws IOException {
    super(metadata);
  }

  @Override
  protected Accept getDefaultFormat() {
    return Accept.ATOM;
  }

  protected XMLEventReader getEventReader(final InputStream is) throws XMLStreamException {
    if (ifactory == null) {
      ifactory = XMLInputFactory.newInstance();
      ifactory.setProperty(SUPPORT_DTD, false);
      ifactory.setProperty(IS_SUPPORTING_EXTERNAL_ENTITIES, false);
    }

    return ifactory.createXMLEventReader(new InputStreamReader(is, Constants.DECODER));
  }

  protected static XMLEventWriter getEventWriter(final OutputStream os) throws XMLStreamException {
    if (ofactory == null) {
      ofactory = XMLOutputFactory.newInstance();
    }

    return ofactory.createXMLEventWriter(os, "UTF-8");
  }

  private void writeEvent(final XMLEvent event, final XMLEventWriter writer) {
    if (writer != null) {
      try {
        writer.add(event);
      } catch (XMLStreamException e) {
        LOG.error("Error writing event {}", event, e);
      }
    }
  }

  private void skipElement(
      final StartElement start,
      final XMLEventReader reader,
      final XMLEventWriter writer,
      final boolean excludeStart)
      throws Exception {

    if (!excludeStart) {
      writeEvent(start, writer);
    }

    int depth = 1;
    boolean found = false;

    while (reader.hasNext() && !found) {
      final XMLEvent event = reader.nextEvent();

      writeEvent(event, writer);

      if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
        depth++;
      } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
        depth--;
        found = depth == 0 && start.getName().equals(event.asEndElement().getName());
      }
    }
  }

  /**
   * {@inheritDoc }
   */
  @Override
  protected InputStream addLinks(
      final String entitySetName, final String entitykey, final InputStream is, final Set<String> links)
      throws Exception {

    // -----------------------------------------
    // 0. Build reader and writer
    // -----------------------------------------
    final XMLEventReader reader = getEventReader(is);
    final XMLEventFactory eventFactory = XMLEventFactory.newInstance();

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(bos);
    // -----------------------------------------
    final Map.Entry<Integer, XMLElement> entry =
        extractElement(reader, writer, Collections.singletonList("entry"), 0, 1, 1);

    writer.add(entry.getValue().getStart());

    final Map<String, NavigationProperty> navigationProperties = metadata.getNavigationProperties(entitySetName);

    // add for links
    for (String link : links) {
      final Set<Attribute> attributes = new HashSet<Attribute>();
      attributes.add(eventFactory.createAttribute(new QName("title"), link));
      attributes.add(eventFactory.createAttribute(new QName("href"),
          Commons.getLinksURI(entitySetName, entitykey, link)));
      attributes.add(eventFactory.createAttribute(new QName("rel"),
          Constants.get(ConstantKey.ATOM_LINK_REL) + link));
      attributes.add(eventFactory.createAttribute(new QName("type"),
          navigationProperties.get(link).isEntitySet()
              ? Constants.get(ConstantKey.ATOM_LINK_FEED)
              : Constants.get(ConstantKey.ATOM_LINK_ENTRY)));

      writer.add(eventFactory.createStartElement(
          new QName(Constants.get(ConstantKey.LINK)), attributes.iterator(), null));
      writer.add(eventFactory.createEndElement(new QName(Constants.get(ConstantKey.LINK)), null));
    }

    writer.add(entry.getValue().getContentReader());
    writer.add(entry.getValue().getEnd());
    writer.add(reader);
    IOUtils.closeQuietly(is);

    writer.flush();
    writer.close();
    reader.close();

    return new ByteArrayInputStream(bos.toByteArray());
  }

  /**
   * {@inheritDoc }
   */
  @Override
  protected Set<String> retrieveAllLinkNames(final InputStream is) throws Exception {
    final Set<String> links = new HashSet<String>();

    final XMLEventReader reader = getEventReader(is);

    try {

      int startDepth = 0;

      while (true) {
        final Map.Entry<Integer, XMLElement> linkInfo =
            extractElement(reader, null,
                Collections.<String> singletonList(Constants.get(ConstantKey.LINK)), startDepth, 2, 2);

        startDepth = linkInfo.getKey();

        links.add(linkInfo.getValue().getStart().getAttributeByName(new QName("title")).getValue());
      }
    } catch (Exception ignore) {
      // ignore
    } finally {
      reader.close();
      IOUtils.closeQuietly(is);
    }

    return links;
  }

  /**
   * {@inheritDoc }
   */
  @Override
  protected NavigationLinks retrieveNavigationInfo(
      final String entitySetName, final InputStream is)
      throws Exception {

    final NavigationLinks links = new NavigationLinks();

    final XMLEventReader reader = getEventReader(is);

    try {
      final List<Map.Entry<String, String>> filter = new ArrayList<Map.Entry<String, String>>();
      filter.add(new AbstractMap.SimpleEntry<String, String>("type", "application/atom+xml;type=entry"));
      filter.add(new AbstractMap.SimpleEntry<String, String>("type", "application/atom+xml;type=feed"));

      int startDepth = 0;

      while (true) {
        // a. search for link with type attribute equals to "application/atom+xml;type=entry/feed"
        final Map.Entry<Integer, XMLElement> linkInfo = extractElement(
            reader, null, Collections.<String> singletonList(Constants.get(ConstantKey.LINK)),
            filter, true, startDepth, 2, 2);
        final XMLElement link = linkInfo.getValue();
        startDepth = linkInfo.getKey();

        final String title = link.getStart().getAttributeByName(new QName("title")).getValue();

        final Attribute hrefAttr = link.getStart().getAttributeByName(new QName("href"));
        final String href = hrefAttr == null ? null : hrefAttr.getValue();

        try {
          final XMLElement inlineElement =
              extractElement(link.getContentReader(), null,
                  Collections.<String> singletonList(Constants.get(ConstantKey.INLINE)), 0, -1, -1).
                  getValue();
          final XMLEventReader inlineReader = inlineElement.getContentReader();

          try {
            while (true) {
              final XMLElement entry =
                  extractElement(inlineReader, null, Collections.<String> singletonList("entry"), 0, -1, -1).
                      getValue();
              links.addInlines(title, entry.toStream());
            }
          } catch (Exception e) {
            // Reached the end of document
          }

          inlineReader.close();
        } catch (Exception ignore) {
          // inline element not found (inlines are not mondatory).
          if (StringUtils.isNotBlank(href) && ENTITY_URI_PATTERN.matcher(href).matches()) {
            links.addLinks(title, href.substring(href.lastIndexOf('/') + 1));
          }
        }
      }
    } catch (Exception ignore) {
      // ignore
    } finally {
      reader.close();
    }

    return links;
  }

  /**
   * {@inheritDoc }
   */
  @Override
  protected InputStream normalizeLinks(
      final String entitySetName, final String entityKey, final InputStream is, final NavigationLinks links)
      throws Exception {

    // -----------------------------------------
    // 0. Build reader and writer
    // -----------------------------------------
    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    IOUtils.copy(is, bos);
    is.close();

    final ByteArrayOutputStream tmpBos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(tmpBos);

    final XMLEventReader reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));
    // -----------------------------------------

    // -----------------------------------------
    // 1. Normalize links
    // -----------------------------------------
    final Set<String> added = new HashSet<String>();

    try {
      final List<Map.Entry<String, String>> filter = new ArrayList<Map.Entry<String, String>>();
      filter.add(new AbstractMap.SimpleEntry<String, String>("type", "application/atom+xml;type=entry"));
      filter.add(new AbstractMap.SimpleEntry<String, String>("type", "application/atom+xml;type=feed"));

      Map.Entry<Integer, XMLElement> linkInfo = null;

      while (true) {
        // a. search for link with type attribute equals to "application/atom+xml;type=entry/feed"
        linkInfo = extractElement(
            reader, writer,
            Collections.<String> singletonList(Constants.get(ConstantKey.LINK)), filter, true,
            linkInfo == null ? 0 : linkInfo.getKey(), 2, 2);
        final XMLElement link = linkInfo.getValue();

        final String title = link.getStart().getAttributeByName(new QName("title")).getValue();

        if (!added.contains(title)) {
          added.add(title);

          final String normalizedLink = String.format(
              "<link href=\"%s(%s)/%s\" rel=\"%s\" title=\"%s\" type=\"%s\"/>",
              entitySetName,
              entityKey,
              title,
              link.getStart().getAttributeByName(new QName("rel")).getValue(),
              title,
              link.getStart().getAttributeByName(new QName("type")).getValue());

          addAtomElement(IOUtils.toInputStream(normalizedLink, Constants.ENCODING), writer);
        }
      }
    } catch (Exception ignore) {
      // ignore
    } finally {
      writer.close();
      reader.close();
    }
    // -----------------------------------------

    // -----------------------------------------
    // 2. Add/replace edit link
    // -----------------------------------------
    final InputStream content = addEditLink(
        new ByteArrayInputStream(tmpBos.toByteArray()),
        entitySetName,
        Constants.get(ConstantKey.DEFAULT_SERVICE_URL) + entitySetName + "(" + entityKey + ")");
    // -----------------------------------------

    // -----------------------------------------
    // 3. Add content element if missing
    // -----------------------------------------
    return addAtomContent(
        content,
        entitySetName,
        Constants.get(ConstantKey.DEFAULT_SERVICE_URL) + entitySetName + "(" + entityKey + ")");
    // -----------------------------------------

  }

  public XMLElement getXmlElement(
      final StartElement start,
      final XMLEventReader reader)
      throws Exception {

    final XMLElement res = new XMLElement();
    res.setStart(start);

    final Charset encoding = Charset.forName(org.apache.olingo.commons.api.Constants.UTF8);
    final ByteArrayOutputStream content = new ByteArrayOutputStream();
    final OutputStreamWriter writer = new OutputStreamWriter(content, encoding);

    int depth = 1;

    while (reader.hasNext() && depth > 0) {
      final XMLEvent event = reader.nextEvent();

      if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
        depth++;
      } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
        depth--;
      }

      if (depth == 0) {
        res.setEnd(event.asEndElement());
      } else {
        event.writeAsEncodedUnicode(writer);
      }
    }

    writer.flush();
    writer.close();

    res.setContent(new ByteArrayInputStream(content.toByteArray()));

    return res;
  }

  private void addAtomElement(
      final InputStream content,
      final XMLEventWriter writer)
      throws Exception {
    final XMLEventReader reader = getEventReader(content);

    final XMLEventFactory eventFactory = XMLEventFactory.newInstance();
    XMLEvent newLine = eventFactory.createSpace("\n");

    try {
      writer.add(newLine);

      while (reader.hasNext()) {
        final XMLEvent event = reader.nextEvent();

        if (event.getEventType() != XMLStreamConstants.START_DOCUMENT
            && event.getEventType() != XMLStreamConstants.END_DOCUMENT
            && event.getEventType() != XMLStreamConstants.COMMENT) {
          writer.add(event);
        }
      }
      writer.add(newLine);
    } finally {
      reader.close();
      IOUtils.closeQuietly(content);
    }
  }

  @Override
  public InputStream addEditLink(
      final InputStream content, final String title, final String href)
      throws Exception {

    final ByteArrayOutputStream copy = new ByteArrayOutputStream();
    IOUtils.copy(content, copy);
    IOUtils.closeQuietly(content);

    XMLEventReader reader = getEventReader(new ByteArrayInputStream(copy.toByteArray()));

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    XMLEventWriter writer = getEventWriter(bos);

    final String editLinkElement = String.format("<link rel=\"edit\" title=\"%s\" href=\"%s\" />", title, href);

    try {
      // check edit link existence
      extractElement(reader, writer, Collections.<String> singletonList(Constants.get(ConstantKey.LINK)),
          Collections.<Map.Entry<String, String>> singletonList(
              new AbstractMap.SimpleEntry<String, String>("rel", "edit")), false, 0, -1, -1);

      addAtomElement(IOUtils.toInputStream(editLinkElement, Constants.ENCODING), writer);
      writer.add(reader);

    } catch (Exception e) {
      reader.close();
      reader = getEventReader(new ByteArrayInputStream(copy.toByteArray()));

      bos = new ByteArrayOutputStream();
      writer = getEventWriter(bos);

      final XMLElement entryElement =
          extractElement(reader, writer, Collections.<String> singletonList("entry"), 0, 1, 1).getValue();

      writer.add(entryElement.getStart());

      addAtomElement(IOUtils.toInputStream(editLinkElement, Constants.ENCODING), writer);

      writer.add(entryElement.getContentReader());
      writer.add(entryElement.getEnd());

      writer.add(reader);

      writer.flush();
      writer.close();
    } finally {
      reader.close();
    }

    return new ByteArrayInputStream(bos.toByteArray());
  }

  @Override
  public InputStream addOperation(final InputStream content, final String name, final String metaAnchor,
      final String href) throws Exception {

    final ByteArrayOutputStream copy = new ByteArrayOutputStream();
    IOUtils.copy(content, copy);
    IOUtils.closeQuietly(content);

    final String action = String.format("<m:action metadata=\"%s%s\" title=\"%s\" target=\"%s\"/>",
        Constants.get(ConstantKey.DEFAULT_SERVICE_URL), metaAnchor, name, href);
    final String newContent = new String(copy.toByteArray(), "UTF-8").replaceAll("\\<content ", action + "\\<content ");

    return IOUtils.toInputStream(newContent, "UTF-8");
  }

  private InputStream addAtomContent(
      final InputStream content, final String title, final String href)
      throws Exception {

    final ByteArrayOutputStream copy = new ByteArrayOutputStream();
    IOUtils.copy(content, copy);

    IOUtils.closeQuietly(content);

    XMLEventReader reader = getEventReader(new ByteArrayInputStream(copy.toByteArray()));

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    XMLEventWriter writer = getEventWriter(bos);

    try {
      // check edit link existence
      XMLElement contentElement =
          extractElement(reader, writer, Collections.<String> singletonList("content"), 0, 2, 2).getValue();
      writer.add(contentElement.getStart());
      writer.add(contentElement.getContentReader());
      writer.add(contentElement.getEnd());
      writer.add(reader);
    } catch (Exception e) {
      reader.close();
      reader = getEventReader(new ByteArrayInputStream(copy.toByteArray()));

      bos = new ByteArrayOutputStream();
      writer = getEventWriter(bos);

      if (isMediaContent(title)) {
        final XMLElement entryElement =
            extractElement(reader, writer, Collections.<String> singletonList("entry"), 0, 1, 1).getValue();

        writer.add(entryElement.getStart());
        writer.add(entryElement.getContentReader());

        addAtomElement(
            IOUtils.toInputStream(String.format("<content type=\"*/*\" src=\"%s/$value\" />", href)),
            writer);

        writer.add(entryElement.getEnd());
      } else {
        try {
          final XMLElement entryElement =
              extractElement(reader, writer, Collections.<String> singletonList(
                  Constants.get(ConstantKey.PROPERTIES)), 0, 2, 3).getValue();

          addAtomElement(
              IOUtils.toInputStream("<content type=\"application/xml\">"),
              writer);

          writer.add(entryElement.getStart());
          writer.add(entryElement.getContentReader());
          writer.add(entryElement.getEnd());

          addAtomElement(
              IOUtils.toInputStream("</content>"),
              writer);
        } catch (Exception nf) {
          reader.close();
          reader = getEventReader(new ByteArrayInputStream(copy.toByteArray()));

          bos = new ByteArrayOutputStream();
          writer = getEventWriter(bos);

          final XMLElement entryElement =
              extractElement(reader, writer, Collections.<String> singletonList("entry"), 0, 1, 1).getValue();
          writer.add(entryElement.getStart());
          writer.add(entryElement.getContentReader());

          addAtomElement(
              IOUtils.toInputStream("<content type=\"application/xml\"/>"),
              writer);

          writer.add(entryElement.getEnd());
        }
      }

      writer.add(reader);

      writer.flush();
      writer.close();
    } finally {
      reader.close();
    }

    return new ByteArrayInputStream(bos.toByteArray());
  }

  public int countAllElements(final String entitySetName) throws Exception {
    final String basePath = entitySetName + File.separatorChar;
    int count = countFeedElements(fsManager.readFile(basePath + Constants.get(ConstantKey.FEED), Accept.XML),
        "entry");

    final String skipTokenDirPath =
        fsManager.getAbsolutePath(basePath + Constants.get(ConstantKey.SKIP_TOKEN),
            null);

    try {
      final FileObject skipToken = fsManager.resolve(skipTokenDirPath);
      final FileObject[] files = fsManager.find(skipToken, Accept.XML.getExtension().substring(1));

      for (FileObject file : files) {
        count += countFeedElements(fsManager.readFile(
            basePath + Constants.get(ConstantKey.SKIP_TOKEN) + File.separatorChar
                + file.getName().getBaseName(), null), "entry");
      }
    } catch (FileSystemException fse) {
      LOG.debug("Resource path '{}' not found", skipTokenDirPath);
    }

    return count;
  }

  private int countFeedElements(final InputStream is, final String elementName) throws XMLStreamException {
    final XMLEventReader reader = getEventReader(is);

    int count = 0;

    while (reader.hasNext()) {
      final XMLEvent event = reader.nextEvent();

      if (event.getEventType() == XMLStreamConstants.START_ELEMENT
          && elementName.equals(event.asStartElement().getName().getLocalPart())) {
        count++;
      }
    }

    reader.close();
    return count;
  }

  public Map.Entry<Integer, XMLElement> extractElement(
      final XMLEventReader reader, final XMLEventWriter writer, final List<String> path,
      final int startPathPos, final int minPathPos, final int maxPathPos)
      throws Exception {
    return extractElement(reader, writer, path, null, false, startPathPos, minPathPos, maxPathPos);
  }

  public Map.Entry<Integer, XMLElement> extractElement(
      final XMLEventReader reader, final XMLEventWriter writer, final List<String> path,
      final Collection<Map.Entry<String, String>> filter,
      final boolean filterInOr,
      final int startPathPos, final int minPathPos, final int maxPathPos)
      throws Exception {

    StartElement start = null;
    int searchFor = 0;
    int depth = startPathPos;

    // Current inspected element
    String current = null;

    // set defaults
    final List<String> pathElementNames = path == null ? Collections.<String> emptyList() : path;
    final Collection<Map.Entry<String, String>> filterAttrs =
        filter == null ? Collections.<Map.Entry<String, String>> emptySet() : filter;

    while (reader.hasNext() && start == null) {
      final XMLEvent event = reader.nextEvent();

      if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
        depth++;

        if (current != null || ((minPathPos < 0 || minPathPos <= depth) && (maxPathPos < 0 || depth <= maxPathPos))) {
          if (pathElementNames.isEmpty()
              || pathElementNames.get(searchFor).trim().equals(event.asStartElement().getName().getLocalPart())) {

            if (searchFor < pathElementNames.size() - 1) {
              // path exploring not completed
              writeEvent(event, writer);
              current = pathElementNames.get(searchFor).trim();
              searchFor++;
            } else {

              // path exploring completed ... evaluate filter about path element name attribute
              boolean match = filterAttrs.isEmpty() || !filterInOr;

              for (Map.Entry<String, String> filterAttr : filterAttrs) {
                final Attribute attr = event.asStartElement().getAttributeByName(new QName(filterAttr.getKey().trim()));

                if (attr == null || !filterAttr.getValue().trim().equals(attr.getValue())) {
                  match = filterInOr ? match : false;
                } else {
                  match = filterInOr ? true : match;
                }
              }

              if (match) {
                // found searched element
                start = event.asStartElement();
              } else {
                skipElement(event.asStartElement(), reader, writer, false);
                depth--;
              }
            }
          } else if (current == null) {
            writeEvent(event, writer);
          } else {
            // skip element
            skipElement(event.asStartElement(), reader, writer, false);
            depth--;
          }
        } else {
          writeEvent(event, writer);
        }

      } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
        depth--;

        writeEvent(event, writer);

        if (event.asEndElement().getName().getLocalPart().equals(current)) {
          // back step ....
          searchFor--;
          current = searchFor > 0 ? pathElementNames.get(searchFor - 1).trim() : null;
        }
      } else {
        writeEvent(event, writer);
      }
    }

    if (start == null) {
      throw new NotFoundException();
    }

    return new SimpleEntry<Integer, XMLElement>(Integer.valueOf(depth - 1), getXmlElement(start, reader));
  }

  public InputStream addAtomInlinecount(final InputStream feed, final int count) throws Exception {
    final XMLEventReader reader = getEventReader(feed);

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(bos);

    try {

      final XMLElement feedElement =
          extractElement(reader, writer, Collections.<String> singletonList("feed"), 0, 1, 1).getValue();

      writer.add(feedElement.getStart());
      addAtomElement(IOUtils.toInputStream(String.format("<m:count>%d</m:count>", count), Constants.ENCODING), writer);
      writer.add(feedElement.getContentReader());
      writer.add(feedElement.getEnd());

      while (reader.hasNext()) {
        writer.add(reader.nextEvent());
      }

    } finally {
      writer.flush();
      writer.close();
      reader.close();
      IOUtils.closeQuietly(feed);
    }

    return new ByteArrayInputStream(bos.toByteArray());
  }

  @Override
  public InputStream selectEntity(final InputStream entity, final String[] propertyNames) throws Exception {
    final XMLEventReader reader = getEventReader(entity);

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(bos);

    final List<String> found = new ArrayList<String>(Arrays.asList(propertyNames));

    boolean inProperties = false;
    boolean writeCurrent = true;
    Boolean writeNext = null;
    String currentName = null;

    final List<String> fieldToBeSaved = new ArrayList<String>(Arrays.asList(propertyNames));

    while (reader.hasNext()) {
      final XMLEvent event = reader.nextEvent();
      if (event.getEventType() == XMLStreamConstants.START_ELEMENT
          && Constants.get(ConstantKey.LINK).equals(event.asStartElement().getName().getLocalPart())
          && !fieldToBeSaved.contains(
              event.asStartElement().getAttributeByName(new QName("title")).getValue())
          && !"edit".equals(event.asStartElement().getAttributeByName(new QName("rel")).getValue())) {
        writeCurrent = false;
      } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT
          && Constants.get(ConstantKey.LINK).equals(event.asEndElement().getName().getLocalPart())) {
        writeNext = true;
      } else if (event.getEventType() == XMLStreamConstants.START_ELEMENT
          && (Constants.get(ConstantKey.PROPERTIES)).equals(
              event.asStartElement().getName().getLocalPart())) {
        writeCurrent = true;
        writeNext = false;
        inProperties = true;
      } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT
          && (Constants.get(ConstantKey.PROPERTIES)).equals(
              event.asEndElement().getName().getLocalPart())) {
        writeCurrent = true;
      } else if (inProperties) {
        if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
          final String elementName = event.asStartElement().getName().getLocalPart();

          for (String propertyName : propertyNames) {
            if ((Constants.get(ConstantKey.ATOM_PROPERTY_PREFIX) + propertyName.trim()).equals(elementName)) {
              writeCurrent = true;
              found.remove(propertyName);
              currentName = propertyName;
            }
          }

        } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT
            && StringUtils.isNotBlank(currentName)
            && (Constants.get(ConstantKey.ATOM_PROPERTY_PREFIX) + currentName.trim()).equals(
                event.asEndElement().getName().getLocalPart())) {
          writeNext = false;
          currentName = null;
        }

      }

      if (writeCurrent) {
        writer.add(event);
      }

      if (writeNext != null) {
        writeCurrent = writeNext;
        writeNext = null;
      }
    }

    writer.flush();
    writer.close();
    reader.close();
    IOUtils.closeQuietly(entity);

    // Do not raise any exception in order to support FC properties as well
    // if (!found.isEmpty()) {
    // throw new Exception(String.format("Could not find a properties '%s'", found));
    // }
    return new ByteArrayInputStream(bos.toByteArray());
  }

  @Override
  public InputStream readEntities(
      final List<String> links, final String linkName, final String next, final boolean forceFeed)
      throws Exception {

    if (links.isEmpty()) {
      throw new NotFoundException();
    }

    final Charset encoding = Charset.forName("UTF-8");

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final OutputStreamWriter writer = new OutputStreamWriter(bos, encoding);

    writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>".toCharArray());

    if (forceFeed || links.size() > 1) {
      // build a feed

      writer.write(("<feed xml:base=\"" + Constants.get(ConstantKey.DEFAULT_SERVICE_URL) + "\" "
          + "xmlns=\"http://www.w3.org/2005/Atom\" "
          + "xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" "
          + "xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\">")
          .toCharArray());

      writer.write(("<id>" + Constants.get(ConstantKey.DEFAULT_SERVICE_URL) + "entityset(entityid)/"
          + linkName
          + "</id>").toCharArray());

      writer.write(("<title type=\"text\">" + linkName + "</title>").toCharArray());
      writer.write("<updated>2014-03-03T13:40:49Z</updated>".toCharArray());
      writer.write(("<link rel=\"self\" title=\"" + linkName + "\" href=\"" + linkName + "\" />").toCharArray());
    }

    for (String link : links) {
      try {
        final Map.Entry<String, String> uri = Commons.parseEntityURI(link);

        final XMLElement entry =
            extractElement(
                getEventReader(readEntity(uri.getKey(), uri.getValue(), Accept.ATOM).getValue()),
                null,
                Collections.<String> singletonList("entry"),
                0, 1, 1).getValue();

        IOUtils.copy(entry.toStream(), writer, encoding);
      } catch (Exception e) {
        // log and ignore link
        LOG.warn("Error parsing uri {}", link, e);
      }
    }

    if (forceFeed || links.size() > 1) {

      if (StringUtils.isNotBlank(next)) {
        writer.write(String.format("<link rel=\"next\" href=\"%s\" />", next).toCharArray());
      }

      writer.write("</feed>".toCharArray());
    }

    writer.flush();
    writer.close();

    return new ByteArrayInputStream(bos.toByteArray());
  }

  @Override
  public Map<String, InputStream> getChanges(final InputStream src) throws Exception {
    final Map<String, InputStream> res = new HashMap<String, InputStream>();

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    IOUtils.copy(src, bos);
    IOUtils.closeQuietly(src);

    // retrieve properties ...
    XMLEventReader reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));

    final Map.Entry<Integer, XMLElement> propertyElement =
        extractElement(reader, null,
            Collections.<String> singletonList(Constants.get(ConstantKey.PROPERTIES)), 0, 2, 3);
    reader.close();

    reader = propertyElement.getValue().getContentReader();

    try {
      while (true) {
        final XMLElement property = extractElement(reader, null, null, 0, -1, -1).getValue();
        res.put(property.getStart().getName().getLocalPart(), property.toStream());
      }
    } catch (Exception ignore) {
      // end
    }

    reader.close();

    // retrieve links ...
    reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));

    try {
      int pos = 0;
      while (true) {
        final Map.Entry<Integer, XMLElement> linkElement =
            extractElement(reader, null,
                Collections.<String> singletonList(Constants.get(ConstantKey.LINK)), pos, 2, 2);

        res.put("[Constants.get(ConstantKey.LINK)]"
            + linkElement.getValue().getStart().getAttributeByName(new QName("title")).getValue(),
            linkElement.getValue().toStream());

        pos = linkElement.getKey();
      }
    } catch (Exception ignore) {
      // end
    }

    return res;
  }

  @Override
  protected InputStream replaceLink(
      final InputStream toBeChanged, final String linkName, final InputStream replacement)
      throws Exception {
    final XMLEventReader reader = getEventReader(toBeChanged);

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(bos);

    final XMLEventFactory eventFactory = XMLEventFactory.newInstance();
    XMLEvent newLine = eventFactory.createSpace("\n");

    try {
      final XMLElement linkElement =
          extractElement(reader, writer,
              Collections.<String> singletonList(Constants.get(ConstantKey.LINK)),
              Collections.<Map.Entry<String, String>> singletonList(
                  new SimpleEntry<String, String>("title", linkName)), false, 0, -1, -1).getValue();
      writer.add(linkElement.getStart());

      // ------------------------------------------
      // write inline ...
      // ------------------------------------------
      writer.add(newLine);
      writer.add(eventFactory.createStartElement("m", null, "inline"));

      addAtomElement(replacement, writer);

      writer.add(eventFactory.createEndElement("m", null, "inline"));
      writer.add(newLine);
      // ------------------------------------------

      writer.add(linkElement.getEnd());

      writer.add(reader);
      writer.flush();
      writer.close();
    } finally {
      reader.close();
      IOUtils.closeQuietly(toBeChanged);
    }

    return new ByteArrayInputStream(bos.toByteArray());
  }

  @Override
  public Map.Entry<String, List<String>> extractLinkURIs(
      final String entitySetName, final String entityId, final String linkName)
      throws Exception {

    final LinkInfo links = readLinks(entitySetName, entityId, linkName, Accept.XML);
    return extractLinkURIs(links.getLinks());
  }

  @Override
  public Map.Entry<String, List<String>> extractLinkURIs(final InputStream is)
      throws Exception {
    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    IOUtils.copy(is, bos);
    IOUtils.closeQuietly(is);

    XMLEventReader reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));
    final List<String> links = new ArrayList<String>();
    try {
      while (true) {
        links.add(IOUtils.toString(extractElement(reader, null, Collections.<String> singletonList("uri"), 0, -1, -1).
            getValue().getContent()));
      }
    } catch (Exception ignore) {
      // End document reached ...
    }
    reader.close();

    String next;

    reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));
    try {
      next = IOUtils.toString(extractElement(reader, null, Collections.<String> singletonList("next"), 0, -1, -1).
          getValue().getContent());
    } catch (Exception ignore) {
      // next link is not mandatory
      next = null;
    }
    reader.close();

    return new AbstractMap.SimpleEntry<String, List<String>>(next, links);
  }

  @Override
  public InputStream replaceProperty(
      final InputStream src, final InputStream replacement, final List<String> path, final boolean justValue)
      throws Exception {

    final List<String> pathElements = new ArrayList<String>();

    for (String element : path) {
      pathElements.add(Constants.get(ConstantKey.ATOM_PROPERTY_PREFIX) + element);
    }

    final XMLEventReader reader = getEventReader(src);

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(bos);

    final Map.Entry<Integer, XMLElement> element = extractElement(reader, writer, pathElements, 0, 3, 4);

    if (justValue) {
      writer.add(element.getValue().getStart());
    }

    final XMLEventReader changesReader = new XMLEventReaderWrapper(replacement);

    while (changesReader.hasNext()) {
      final XMLEvent event = changesReader.nextEvent();
      if (event.isStartElement() && event.asStartElement().getName().equals(element.getValue().getStart().getName())) {
        writer.add(element.getValue().getStart());
        writer.add(changesReader);
      } else {
        writer.add(event);
      }
    }

    changesReader.close();
    IOUtils.closeQuietly(replacement);

    if (justValue) {
      writer.add(element.getValue().getEnd());
    }

    writer.add(reader);

    reader.close();
    IOUtils.closeQuietly(src);

    writer.flush();
    writer.close();

    return new ByteArrayInputStream(bos.toByteArray());
  }

  @Override
  public InputStream deleteProperty(final InputStream src, final List<String> path) throws Exception {
    final XMLEventReader reader = getEventReader(src);

    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final XMLEventWriter writer = getEventWriter(bos);

    final XMLEventReader changesReader =
        new XMLEventReaderWrapper(IOUtils.toInputStream(
            String.format("<%s m:null=\"true\" />", path.get(path.size() - 1)), Constants.ENCODING));

    writer.add(changesReader);
    changesReader.close();

    writer.add(reader);

    reader.close();
    IOUtils.closeQuietly(src);

    writer.flush();
    writer.close();

    return new ByteArrayInputStream(bos.toByteArray());
  }
}
