com.msopentech.odatajclient.testservice.utils.XMLUtilities.java Source code

Java tutorial

Introduction

Here is the source code for com.msopentech.odatajclient.testservice.utils.XMLUtilities.java

Source

/**
 * 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 com.msopentech.odatajclient.testservice.utils;

import static com.msopentech.odatajclient.testservice.utils.Constants.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
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 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.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;

public class XMLUtilities extends AbstractUtilities {

    protected static XMLInputFactory factory = null;

    public XMLUtilities(final ODataVersion version) throws Exception {
        super(version);
    }

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

    protected static XMLEventReader getEventReader(final InputStream is) throws XMLStreamException {
        if (factory == null) {
            factory = XMLInputFactory.newInstance();
        }
        factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, false);
        return factory.createXMLEventReader(is);
    }

    /**
     * {@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 XMLOutputFactory xof = XMLOutputFactory.newInstance();
        final XMLEventWriter writer = xof.createXMLEventWriter(bos);
        // -----------------------------------------

        final XmlElement entry = getAtomElement(reader, writer, "entry");
        writer.add(entry.getStart());

        // 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(version, entitySetName, entitykey, link)));
            attributes.add(eventFactory.createAttribute(new QName("rel"), Constants.ATOM_LINK_REL + link));
            attributes.add(eventFactory.createAttribute(new QName("type"),
                    Commons.linkInfo.get(version).isFeed(entitySetName, link) ? Constants.ATOM_LINK_FEED
                            : Constants.ATOM_LINK_ENTRY));

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

        writer.add(entry.getContentReader());
        writer.add(entry.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 = getAtomElement(reader, null, LINK, null, startDepth,
                        2, 2, true);

                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 = getAtomElement(reader, null, LINK, filter,
                        startDepth, 2, 2, true);
                final XmlElement link = linkInfo.getValue();
                startDepth = linkInfo.getKey();

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

                try {
                    final XmlElement inlineElement = getAtomElement(link.getContentReader(), null, INLINE);
                    final XMLEventReader inlineReader = inlineElement.getContentReader();

                    try {
                        while (true) {
                            final XmlElement entry = getAtomElement(inlineReader, null, "entry");
                            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 (entityUriPattern.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 XMLOutputFactory xof = XMLOutputFactory.newInstance();
        final XMLEventWriter writer = xof.createXMLEventWriter(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 = getAtomElement(reader, writer, LINK, filter, linkInfo == null ? 0 : linkInfo.getKey(), 2,
                        2, true);
                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), writer);
                }
            }
        } catch (Exception ignore) {
            // ignore
        } finally {
            writer.close();
            reader.close();
        }
        // -----------------------------------------

        // -----------------------------------------
        // 2. Add edit link if missing
        // -----------------------------------------
        final InputStream content = addAtomEditLink(new ByteArrayInputStream(tmpBos.toByteArray()), entitySetName,
                Constants.DEFAULT_SERVICE_URL + entitySetName + "(" + entityKey + ")");
        // -----------------------------------------

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

    }

    public XmlElement getAtomElement(final InputStream is, final String name) throws Exception {
        return getAtomElement(is, name, -1, -1);
    }

    public static XmlElement getAtomElement(final InputStream is, final String name, final int minDepth,
            final int maxDepth) throws Exception {
        final XMLEventReader reader = getEventReader(is);
        final XmlElement res = getAtomElement(reader, null, name, null, 0, minDepth, maxDepth, false).getValue();
        reader.close();

        return res;
    }

    public static XmlElement getAtomElement(final XMLEventReader reader, final XMLEventWriter discarded,
            final String name) throws Exception {
        return getAtomElement(reader, discarded, name, null, 0, -1, -1, false).getValue();
    }

    public XmlElement getAtomElement(final XMLEventReader reader, final XMLEventWriter discarded, final String name,
            final Collection<Map.Entry<String, String>> filterAttrs) throws Exception {
        return getAtomElement(reader, discarded, name, filterAttrs, 0, -1, -1, false).getValue();
    }

    public static Map.Entry<Integer, XmlElement> getAtomElement(final XMLEventReader reader,
            final XMLEventWriter discarded, final String name,
            final Collection<Map.Entry<String, String>> filterAttrs, final int initialDepth, final int minDepth,
            final int maxDepth, final boolean filterInOr) throws Exception {

        int depth = initialDepth;
        StartElement start = null;

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

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

                if ((StringUtils.isBlank(name)
                        || name.trim().equals(event.asStartElement().getName().getLocalPart()))
                        && (minDepth < 0 || minDepth <= depth) && (maxDepth < 0 || maxDepth >= depth)) {

                    boolean match = filterAttrs == null || filterAttrs.isEmpty() || !filterInOr;

                    for (Map.Entry<String, String> filterAttr : filterAttrs == null
                            ? Collections.<Map.Entry<String, String>>emptySet()
                            : 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) {
                        start = event.asStartElement();
                    }
                }

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

            if (start == null) {
                if (discarded != null) {
                    discarded.add(event);
                }
            }
        }

        if (start == null) {
            throw new Exception(String.format("Could not find an element named '%s'", name));
        }

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

    public static XmlElement getAtomElement(final StartElement start, final XMLEventReader reader)
            throws Exception {

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

        StringWriter content = new StringWriter();

        int depth = 1;

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

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

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

        content.flush();
        content.close();

        res.setContent(new ByteArrayInputStream(content.toString().getBytes()));

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

    private InputStream addAtomEditLink(final InputStream content, final String title, final String href)
            throws Exception {
        final XMLOutputFactory xof = XMLOutputFactory.newInstance();

        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 = xof.createXMLEventWriter(bos);

        try {
            // check edit link existence
            final XmlElement editLink = getAtomElement(reader, writer, LINK,
                    Collections.<Map.Entry<String, String>>singletonList(
                            new AbstractMap.SimpleEntry<String, String>("rel", "edit")));
            writer.add(editLink.getStart());
            writer.add(editLink.getContentReader());
            writer.add(editLink.getEnd());
            writer.add(reader);
        } catch (Exception e) {
            reader.close();
            reader = getEventReader(new ByteArrayInputStream(copy.toByteArray()));

            bos = new ByteArrayOutputStream();
            writer = xof.createXMLEventWriter(bos);

            final XmlElement entryElement = getAtomElement(reader, writer, "entry");

            writer.add(entryElement.getStart());
            addAtomElement(IOUtils.toInputStream(
                    String.format("<link rel=\"edit\" title=\"%s\" href=\"%s\" />", title, href)), writer);
            writer.add(entryElement.getContentReader());
            writer.add(entryElement.getEnd());

            writer.add(reader);

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

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

    public InputStream addAtomContent(final InputStream content, final String title, final String href)
            throws Exception {
        final XMLOutputFactory xof = XMLOutputFactory.newInstance();

        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 = xof.createXMLEventWriter(bos);

        try {
            // check edit link existence
            XmlElement contentElement = getAtomElement(reader, writer, "content");
            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 = xof.createXMLEventWriter(bos);

            if (isMediaContent(title)) {
                final XmlElement entryElement = getAtomElement(reader, writer, "entry");

                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 = getAtomElement(reader, writer, PROPERTIES);

                    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 = xof.createXMLEventWriter(bos);

                    final XmlElement entryElement = getAtomElement(reader, writer, "entry");
                    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 + FEED, Accept.XML), "entry");

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

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

            for (FileObject file : files) {
                count += countFeedElements(
                        fsManager.readFile(
                                basePath + 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 static StartElement getPropertyStartTag(final XMLEventReader propReader, final String[] path)
            throws Exception {
        int pos = 0;

        StartElement property = null;

        while (propReader.hasNext() && pos < path.length) {
            final XMLEvent event = propReader.nextEvent();

            if (event.getEventType() == XMLStreamConstants.START_ELEMENT
                    && (ATOM_PROPERTY_PREFIX + path[pos].trim())
                            .equals(event.asStartElement().getName().getLocalPart())) {
                pos++;
                if (path.length == pos) {
                    property = event.asStartElement();
                }
            }
        }

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

        return property;
    }

    public String getEdmTypeFromXML(final InputStream is, final String[] path) throws Exception {
        final XMLEventReader reader = getEventReader(is);

        final Attribute type = getPropertyStartTag(reader, path).getAttributeByName(new QName(TYPE));

        reader.close();

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

        return type.getValue();
    }

    public InputStream getAtomProperty(final InputStream is, final String[] path) throws Exception {
        final XMLEventReader reader = getEventReader(is);

        final XmlElement props = getAtomElement(reader, null, PROPERTIES);
        final XMLEventReader propsReader = props.getContentReader();

        reader.close();

        final InputStream propertyStream = writeFromStartToEndElement(getPropertyStartTag(propsReader, path),
                propsReader, true);

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

        return propertyStream;
    }

    private InputStream writeFromStartToEndElement(final StartElement element, final XMLEventReader reader,
            final boolean document) throws XMLStreamException {
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final XMLOutputFactory xof = XMLOutputFactory.newInstance();
        final XMLEventWriter writer = xof.createXMLEventWriter(bos);

        final QName name = element.getName();

        if (document) {
            final XMLEventFactory eventFactory = XMLEventFactory.newInstance();
            writer.add(eventFactory.createStartDocument("UTF-8", "1.0"));
            writer.add(element);

            if (element.getAttributeByName(new QName(ATOM_DATASERVICE_NS)) == null) {
                writer.add(eventFactory.createNamespace(ATOM_PROPERTY_PREFIX.substring(0, 1), DATASERVICES_NS));
            }
            if (element.getAttributeByName(new QName(ATOM_METADATA_NS)) == null) {
                writer.add(eventFactory.createNamespace(ATOM_METADATA_PREFIX.substring(0, 1), METADATA_NS));
            }
        } else {
            writer.add(element);
        }

        XMLEvent event = element;

        while (reader.hasNext() && !(event.isEndElement() && name.equals(event.asEndElement().getName()))) {
            event = reader.nextEvent();
            writer.add(event);
        }

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

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

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

        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final XMLOutputFactory xof = XMLOutputFactory.newInstance();
        final XMLEventWriter writer = xof.createXMLEventWriter(bos);

        try {

            final XmlElement feedElement = getAtomElement(reader, writer, "feed");

            writer.add(feedElement.getStart());
            addAtomElement(IOUtils.toInputStream(String.format("<m:count>%d</m:count>", count)), 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());
    }

    public static InputStream getAtomPropertyValue(final InputStream is, final String[] path) throws Exception {
        final XmlElement props = getAtomElement(is, PROPERTIES, 2, 3);
        final XMLEventReader propsReader = props.getContentReader();

        // search for property start element
        getPropertyStartTag(propsReader, ArrayUtils.subarray(path, 0, path.length - 1));

        final InputStream res;

        XMLEvent event = propsReader.nextEvent();

        // expected text node
        if (event.isCharacters()) {
            res = new ByteArrayInputStream(event.asCharacters().getData().getBytes());
        } else if (event.isEndElement()) {
            throw new NotFoundException();
        } else {
            throw new Exception("The method or operation is not implemented.");
        }

        return res;
    }

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

        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final XMLOutputFactory xof = XMLOutputFactory.newInstance();
        final XMLEventWriter writer = xof.createXMLEventWriter(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
                    && 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
                    && LINK.equals(event.asEndElement().getName().getLocalPart())) {
                writeNext = true;
            } else if (event.getEventType() == XMLStreamConstants.START_ELEMENT
                    && (PROPERTIES).equals(event.asStartElement().getName().getLocalPart())) {
                writeCurrent = true;
                writeNext = false;
                inProperties = true;
            } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT
                    && (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 ((ATOM_PROPERTY_PREFIX + propertyName.trim()).equals(elementName)) {
                            writeCurrent = true;
                            found.remove(propertyName);
                            currentName = propertyName;
                        }
                    }

                } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT
                        && StringUtils.isNotBlank(currentName) && (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 ByteArrayOutputStream bos = new ByteArrayOutputStream();

        if (forceFeed || links.size() > 1) {
            // build a feed
            bos.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>".getBytes());

            bos.write(("<feed xml:base=\"" + 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\">").getBytes());

            bos.write(("<id>" + DEFAULT_SERVICE_URL + "entityset(entityid)/" + linkName + "</id>").getBytes());

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

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

                final XmlElement entry = getAtomElement(
                        readEntity(uri.getKey(), uri.getValue(), Accept.ATOM).getValue(), "entry");

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

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

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

            bos.write("</feed>".getBytes());
        }

        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 = getAtomElement(reader, null, PROPERTIES, null, 0, 2,
                3, false);
        reader.close();

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

        try {
            while (true) {
                final XmlElement property = getAtomElement(reader, null, null);
                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 = getAtomElement(reader, null, LINK, null, pos, 2,
                        2, false);

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

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

        return res;
    }

    @Override
    public InputStream setChanges(final InputStream toBeChanged, final Map<String, InputStream> properties)
            throws Exception {
        XMLEventReader reader = getEventReader(toBeChanged);

        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final XMLOutputFactory xof = XMLOutputFactory.newInstance();
        XMLEventWriter writer = xof.createXMLEventWriter(bos);

        // ---------------------------------
        // add property changes
        // ---------------------------------
        Map.Entry<Integer, XmlElement> propertyElement = getAtomElement(reader, writer, PROPERTIES, null, 0, 2, 3,
                false);

        writer.flush();

        ByteArrayOutputStream pbos = new ByteArrayOutputStream();
        OutputStreamWriter pwriter = new OutputStreamWriter(pbos);

        final XMLEventReader propertyReader = propertyElement.getValue().getContentReader();

        try {
            while (true) {
                final XmlElement property = getAtomElement(propertyReader, null, null);
                final String name = property.getStart().getName().getLocalPart();

                if (properties.containsKey(name)) {
                    // replace
                    final InputStream replacement = properties.get(name);
                    properties.remove(property.getStart().getName().getLocalPart());
                    pwriter.append(IOUtils.toString(replacement));
                    IOUtils.closeQuietly(replacement);
                } else {
                    pwriter.append(IOUtils.toString(property.toStream()));
                }
            }
        } catch (Exception ignore) {
            // end
        }

        for (Map.Entry<String, InputStream> remains : properties.entrySet()) {
            if (!remains.getKey().startsWith("[LINK]")) {
                pwriter.append(IOUtils.toString(remains.getValue()));
                IOUtils.closeQuietly(remains.getValue());
            }
        }

        pwriter.flush();
        pwriter.close();

        writer.add(propertyElement.getValue().getStart());
        writer.add(new XMLEventReaderWrapper(new ByteArrayInputStream(pbos.toByteArray())));
        writer.add(propertyElement.getValue().getEnd());

        IOUtils.closeQuietly(pbos);

        writer.add(reader);
        reader.close();
        writer.flush();
        writer.close();
        // ---------------------------------

        // ---------------------------------
        // add navigationm changes
        // ---------------------------------

        // remove existent links
        for (Map.Entry<String, InputStream> remains : properties.entrySet()) {

            if (remains.getKey().startsWith("[LINK]")) {
                reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));

                bos.reset();
                writer = xof.createXMLEventWriter(bos);

                try {
                    final String linkName = remains.getKey().substring(remains.getKey().indexOf("]") + 1);

                    getAtomElement(reader, writer, LINK, Collections.<Map.Entry<String, String>>singleton(
                            new SimpleEntry<String, String>("title", linkName)), 0, 2, 2, false);

                    writer.add(reader);

                } catch (Exception ignore) {
                    // ignore
                }

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

        reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));

        bos.reset();
        writer = xof.createXMLEventWriter(bos);

        propertyElement = getAtomElement(reader, writer, CONTENT, null, 0, 2, 2, false);
        writer.flush();

        pbos.reset();
        pwriter = new OutputStreamWriter(pbos);

        for (Map.Entry<String, InputStream> remains : properties.entrySet()) {
            if (remains.getKey().startsWith("[LINK]")) {
                pwriter.append(IOUtils.toString(remains.getValue()));
                IOUtils.closeQuietly(remains.getValue());
            }
        }

        pwriter.flush();
        pwriter.close();

        writer.add(new XMLEventReaderWrapper(new ByteArrayInputStream(pbos.toByteArray())));
        IOUtils.closeQuietly(pbos);

        writer.add(propertyElement.getValue().getStart());
        writer.add(propertyElement.getValue().getContentReader());
        writer.add(propertyElement.getValue().getEnd());

        writer.add(reader);
        reader.close();
        writer.flush();
        writer.close();
        // ---------------------------------

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

    @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 XMLOutputFactory xof = XMLOutputFactory.newInstance();
        final XMLEventWriter writer = xof.createXMLEventWriter(bos);

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

        try {
            final XmlElement linkElement = getAtomElement(reader, writer, LINK,
                    Collections.<Map.Entry<String, String>>singletonList(
                            new SimpleEntry<String, String>("title", linkName)));
            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());
    }

    public static 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(getAtomElement(reader, null, "uri").getContent()));
            }
        } catch (Exception ignore) {
            // End document reached ...
        }
        reader.close();

        String next;

        reader = getEventReader(new ByteArrayInputStream(bos.toByteArray()));
        try {
            next = IOUtils.toString(getAtomElement(reader, null, "next").getContent());
        } catch (Exception ignore) {
            // next link is not mandatory
            next = null;
        }
        reader.close();

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