Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/AbstractClarinFCSRecordDataParser.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/AbstractClarinFCSRecordDataParser.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/AbstractClarinFCSRecordDataParser.java	(revision 6945)
@@ -0,0 +1,198 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRURecordData;
+import eu.clarin.sru.client.SRURecordDataParser;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+
+
+/**
+ * An abstract base class for CLARIN-FCS record data parsers.
+ */
+abstract class AbstractClarinFCSRecordDataParser implements SRURecordDataParser {
+    protected static final Logger logger =
+            LoggerFactory.getLogger(AbstractClarinFCSRecordDataParser.class);
+    protected final List<DataViewParser> parsers;
+
+
+    /**
+     * Constructor.
+     *
+     * @param parsers
+     *            the list of data view parsers to be used by this record data
+     *            parser. This list should contain one
+     *            {@link DataViewParserGenericDOM} or
+     *            {@link DataViewParserGenericString} instance.
+     * @throws NullPointerException
+     *             if parsers is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if parsers is empty or contains duplicate entries
+     */
+    protected AbstractClarinFCSRecordDataParser(List<DataViewParser> parsers) {
+        if (parsers == null) {
+            throw new NullPointerException("parsers == null");
+        }
+        if (parsers.isEmpty()) {
+            throw new IllegalArgumentException("parsers is empty");
+        }
+
+        ArrayList<DataViewParser> list = new ArrayList<DataViewParser>();
+        boolean foundGeneric = false;
+        for (DataViewParser parser : parsers) {
+            if ((parser instanceof DataViewParserGenericDOM) ||
+                    (parser instanceof DataViewParserGenericString)) {
+                if (foundGeneric) {
+                    throw new IllegalAccessError("parser list should contain " +
+                            "only one instance of DataViewParserGenericDOM " +
+                            "or DataViewParserGenericString");
+                }
+                foundGeneric = true;
+            }
+            if (list.contains(parser)) {
+                throw new IllegalArgumentException("parser list should not " +
+                        "contain douplicates: " + parser.getClass().getName());
+            }
+            list.add(parser);
+        }
+
+        if (!foundGeneric) {
+            logger.warn("No generic type data view parser found. You should " +
+                    "make sure that the parser list contains one " +
+                    "DataViewParserGenericDOM or DataViewParserGenericString " +
+                    "instance");
+        }
+
+        this.parsers = Collections.unmodifiableList(list);
+    }
+
+
+    protected SRURecordData parse(XMLStreamReader reader, String ns)
+            throws XMLStreamException, SRUClientException {
+        // Resource
+        XmlStreamReaderUtils.readStart(reader, ns, "Resource", true, true);
+        final String pid =
+                XmlStreamReaderUtils.readAttributeValue(reader, null, "pid");
+        final String ref =
+                XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+        XmlStreamReaderUtils.consumeStart(reader);
+
+        // Resource/Resource (optional)
+        if (XmlStreamReaderUtils.readStart(reader, ns, "Resource", false)) {
+            logger.info("skipping nested <Resource> element");
+            XmlStreamReaderUtils.readEnd(reader, ns, "Resource", true);
+        }
+
+        // Resource/DataView
+        final List<DataView> dataviews = parseDataViews(reader, ns);
+
+        // Resource/ResourceFragment
+        final List<Resource.ResourceFragment> resourceFragments =
+                parseResourceFragments(reader, ns);
+
+        XmlStreamReaderUtils.readEnd(reader, ns, "Resource", true);
+
+        return new ClarinFCSRecordData(pid, ref, dataviews, resourceFragments);
+    }
+
+
+    private List<DataView> parseDataViews(XMLStreamReader reader, String ns)
+            throws XMLStreamException, SRUClientException {
+        List<DataView> dataviews = null;
+
+        while (XmlStreamReaderUtils.readStart(reader, ns, "DataView", false, true)) {
+            final String pid =
+                    XmlStreamReaderUtils.readAttributeValue(reader, null, "pid");
+            final String ref =
+                    XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+            final String type =
+                    XmlStreamReaderUtils.readAttributeValue(reader, null, "type");
+            if ((type == null) || type.isEmpty()) {
+                throw new SRUClientException("element <DataView> needs a "
+                        + "non-empty 'type' attribute");
+            }
+
+            // consume start element and get rid of any whitespace
+            XmlStreamReaderUtils.consumeStart(reader);
+            XmlStreamReaderUtils.consumeWhitespace(reader);
+
+            logger.debug("processing <DataView> of type = {}", type);
+
+            DataViewParser selectedParser = null;
+            for (DataViewParser parser : parsers) {
+                if (parser.acceptType(type) &&
+                        ((selectedParser == null) ||
+                         (selectedParser.getPriority() < parser.getPriority()))) {
+                    selectedParser = parser;
+                }
+            }
+
+            DataView dataview = null;
+            if (selectedParser != null) {
+                dataview = selectedParser.parse(reader, type, pid, ref);
+            } else {
+                logger.warn("no parser found for <DataView> of type = {}", type);
+            }
+
+            XmlStreamReaderUtils.readEnd(reader, ns, "DataView", true);
+
+            if (dataview != null) {
+                if (dataviews == null) {
+                    dataviews = new LinkedList<DataView>();
+                }
+                dataviews.add(dataview);
+            } else {
+                logger.warn("skipped <DataView> of type = {}", type);
+            }
+        } // while
+        return dataviews;
+    }
+
+
+    private List<Resource.ResourceFragment> parseResourceFragments(
+            XMLStreamReader reader, String ns) throws XMLStreamException,
+            SRUClientException {
+        List<Resource.ResourceFragment> resourceFragments = null;
+        while (XmlStreamReaderUtils.readStart(reader, ns, "ResourceFragment", false, true)) {
+            logger.debug("found ResourceFragment");
+            String pid = XmlStreamReaderUtils.readAttributeValue(reader, null, "pid");
+            String ref = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+            XmlStreamReaderUtils.consumeStart(reader);
+            final List<DataView> dataviews = parseDataViews(reader, ns);
+            XmlStreamReaderUtils.readEnd(reader, ns, "ResourceFragment", true);
+
+            if (resourceFragments == null) {
+                resourceFragments = new LinkedList<Resource.ResourceFragment>();
+            }
+            resourceFragments.add(new Resource.ResourceFragment(pid, ref, dataviews));
+        } // while
+        return resourceFragments;
+    }
+
+} // class AbstractClarinFCSRecordDataParser
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSClientBuilder.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSClientBuilder.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSClientBuilder.java	(revision 6945)
@@ -0,0 +1,341 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.clarin.sru.client.SRUClient;
+import eu.clarin.sru.client.SRUClientConfig;
+import eu.clarin.sru.client.SRUExtraResponseDataParser;
+import eu.clarin.sru.client.SRUSimpleClient;
+import eu.clarin.sru.client.SRUThreadedClient;
+import eu.clarin.sru.client.SRUVersion;
+
+
+/**
+ * A class that implements the builder pattern for creating SRU client instances
+ * that are configured to be used for CLARIN-FCS.
+ *
+ */
+public class ClarinFCSClientBuilder {
+    private static final boolean DEFAULT_UNKNOWN_AS_DOM =
+            false;
+    private static final SRUVersion DEFAULT_SRU_VERSION =
+            SRUVersion.VERSION_1_2;
+    private List<DataViewParser> parsers = new ArrayList<DataViewParser>();
+    private List<SRUExtraResponseDataParser> extraDataParsers =
+            new ArrayList<SRUExtraResponseDataParser>();
+    private SRUVersion defaultVersion = DEFAULT_SRU_VERSION;
+    private boolean unknownAsDom      = DEFAULT_UNKNOWN_AS_DOM;
+    private boolean legacySupport     = false;
+    private int connectTimeout        = SRUClientConfig.DEFAULT_CONNECT_TIMEOUT;
+    private int socketTimeout         = SRUClientConfig.DEFAULT_SOCKET_TIMEOUT;
+
+
+    /**
+     * Constructor.
+     *
+     * @param unknownAsDom
+     *            if <code>true</code> unknown data views are parsed into a DOM
+     *
+     */
+    public ClarinFCSClientBuilder(boolean unknownAsDom) {
+        this.unknownAsDom = unknownAsDom;
+    }
+
+
+    /**
+     * Constructor.
+     */
+    public ClarinFCSClientBuilder() {
+        this(DEFAULT_UNKNOWN_AS_DOM);
+    }
+
+
+    /**
+     * Add the recommended default set of data record view parsers.
+     *
+     * @return this {@link ClarinFCSClientBuilder} instance
+     */
+    public ClarinFCSClientBuilder addDefaultDataViewParsers() {
+        doRegisterDataViewParser(parsers, new DataViewParserHits());
+        doRegisterDataViewParser(parsers, new DataViewParserAdvanced());
+        return this;
+    }
+
+
+    /**
+     * Configure client to parse unknown Data Views into a DOM representation.
+     *
+     * @return this {@link ClarinFCSClientBuilder} instance
+     * @see DataViewParserGenericDOM
+     * @see DataViewGenericDOM
+     */
+    public ClarinFCSClientBuilder unknownDataViewAsDOM() {
+        unknownAsDom = true;
+        return this;
+    }
+
+
+    /**
+     * Configure client to parse unknown Data Views into a String
+     * representation.
+     *
+     * @return this {@link ClarinFCSClientBuilder} instance
+     * @see DataViewParserGenericString
+     * @see DataViewGenericString
+     */
+    public ClarinFCSClientBuilder unknownDataViewAsString() {
+        unknownAsDom = false;
+        return this;
+    }
+
+
+    /**
+     * Set default SRU version to be used.
+     *
+     * @param defaultVersion
+     *            the default SRU version to be used
+     *
+     * @return this {@link ClarinFCSClientBuilder} instance
+     */
+    public ClarinFCSClientBuilder setDefaultSRUVersion(
+            final SRUVersion defaultVersion) {
+        if (defaultVersion == null) {
+            throw new NullPointerException("defaultVersion == null");
+        }
+        this.defaultVersion = defaultVersion;
+        return this;
+    }
+
+
+    /**
+     * Configure client to enable support for legacy CLARIN-FCS endpoints.
+     *
+     * @return this {@link ClarinFCSClientBuilder} instance
+     */
+    public ClarinFCSClientBuilder enableLegacySupport() {
+        legacySupport = true;
+        return this;
+    }
+
+
+    /**
+     * Configure client to disable support for legacy CLARIN-FCS endpoints.
+     *
+     * @return this {@link ClarinFCSClientBuilder} instance
+     */
+    public ClarinFCSClientBuilder disableLegacySupport() {
+        legacySupport = false;
+        return this;
+    }
+
+
+    /**
+     * Get the timeout in milliseconds until a connection is established.
+     *
+     * @return this connect timeout in milliseconds
+     */
+    public int getConnectTimeout() {
+        return connectTimeout;
+    }
+
+
+    /**
+     * Set the timeout in milliseconds until a connection is established.
+     * <p>
+     * A timeout value of <code>0</code> is interpreted as an infinite timeout;
+     * <code>-1</code> is interpreted as system default.
+     * </p>
+     *
+     * @param connectTimeout
+     *            the connect timeout in milliseconds
+     * @return this {@link ClarinFCSClientBuilder} instance
+     */
+    public ClarinFCSClientBuilder setConnectTimeout(int connectTimeout) {
+        if (connectTimeout < -1) {
+            throw new IllegalArgumentException("connectTimeout < -1");
+        }
+        this.connectTimeout = connectTimeout;
+        return this;
+    }
+
+
+    /**
+     * Get the socket timeout (<code>SO_TIMEOUT</code>) in milliseconds,
+     * which is the timeout for waiting for data.
+     *
+     * @return socketTimeout
+     *            the socket timeout in milliseconds
+     */
+    public int getSocketTimeout() {
+        return socketTimeout;
+    }
+
+
+    /**
+     * Set the socket timeout (<code>SO_TIMEOUT</code>) in milliseconds, which
+     * is the timeout for waiting for data.
+     * <p>
+     * A timeout value of <code>0</code> is interpreted as an infinite timeout;
+     * <code>-1</code> is interpreted as system default.
+     * </p>
+     *
+     * @param socketTimeout
+     *            the socket timeout in milliseconds
+     * @return this {@link ClarinFCSClientBuilder} instance
+     */
+    public ClarinFCSClientBuilder setSocketTimeout(int socketTimeout) {
+        if (socketTimeout < -1) {
+            throw new IllegalArgumentException("socketTimeout < -1");
+        }
+        this.socketTimeout = socketTimeout;
+        return this;
+    }
+
+
+    /**
+     * Register a Data View parser.
+     *
+     * @param parser
+     *            the data view parser to be registered
+     * @return this {@link ClarinFCSClientBuilder} instance
+     * @throws IllegalArgumentException
+     *             if an error occurred while registering the data view parser
+     * @see DataViewParser
+     */
+    public ClarinFCSClientBuilder registerDataViewParser(DataViewParser parser) {
+        if (parser == null) {
+            throw new NullPointerException("parser == null");
+        }
+        if ((parser instanceof DataViewParserGenericDOM) ||
+                (parser instanceof DataViewParserGenericString)) {
+            throw new IllegalArgumentException("parsers of type '" +
+                    parser.getClass().getName() +
+                    "' cannot be added manually");
+        }
+
+        if (!doRegisterDataViewParser(parsers, parser)) {
+            throw new IllegalArgumentException("parser of type '" +
+                    parser.getClass().getName() + "' was already registered");
+        }
+        return this;
+    }
+
+
+    /**
+     * Register an extra response data parser.
+     *
+     * @param parser
+     *            the extra response data parser to be registered
+     * @return this {@link ClarinFCSClientBuilder} instance
+     * @throws IllegalArgumentException
+     *             if an error occurred while registering the extra response
+     *             data parser
+     * @see SRUExtraResponseDataParser
+     */
+    public ClarinFCSClientBuilder registerExtraResponseDataParser(
+            SRUExtraResponseDataParser parser) {
+        if (parser == null) {
+            throw new NullPointerException("parser == null");
+        }
+        extraDataParsers.add(parser);
+        return this;
+    }
+
+
+    /**
+     * Create a {@link SRUSimpleClient} instance.
+     *
+     * @return a configured {@link SRUSimpleClient} instance
+     */
+    public SRUSimpleClient buildSimpleClient() {
+        return new SRUSimpleClient(makeClientConfig());
+    }
+
+
+    /**
+     * Create a {@link SRUClient} instance.
+     *
+     * @return a configured {@link SRUClient} instance
+     */
+    public SRUClient buildClient() {
+        return new SRUClient(makeClientConfig());
+    }
+
+
+    /**
+     * Create a {@link SRUThreadedClient} instance.
+     *
+     * @return a configured {@link SRUThreadedClient} instance
+     */
+    public SRUThreadedClient buildThreadedClient() {
+        return new SRUThreadedClient(makeClientConfig());
+    }
+
+
+    @SuppressWarnings("deprecation")
+    private SRUClientConfig makeClientConfig() {
+        final SRUClientConfig.Builder builder = new SRUClientConfig.Builder();
+        builder
+            .setDefaultVersion(defaultVersion)
+            .setConnectTimeout(connectTimeout)
+            .setSocketTimeout(socketTimeout);
+        final List<DataViewParser> p = finalizeDataViewParsers();
+        builder.addRecordDataParser(new ClarinFCSRecordDataParser(p));
+        if (legacySupport) {
+            builder.addRecordDataParser(new LegacyClarinFCSRecordDataParser(p));
+        }
+        if ((extraDataParsers != null) && !extraDataParsers.isEmpty()) {
+            for (SRUExtraResponseDataParser parser : extraDataParsers) {
+                builder.addExtraResponseDataParser(parser);
+            }
+        }
+        return builder.build();
+    }
+
+
+    @SuppressWarnings("deprecation")
+    private List<DataViewParser> finalizeDataViewParsers() {
+        final List<DataViewParser> result =
+                new ArrayList<DataViewParser>(parsers.size() +
+                        (legacySupport ? 2 : 1));
+        result.addAll(parsers);
+        if (unknownAsDom) {
+            result.add(new DataViewParserGenericDOM());
+        } else {
+            result.add(new DataViewParserGenericString());
+        }
+        if (legacySupport) {
+            result.add(new DataViewParserKWIC());
+        }
+        return result;
+    }
+
+
+    private static boolean doRegisterDataViewParser(
+            List<DataViewParser> parsers, DataViewParser parser) {
+        if (parsers.contains(parser)) {
+            return false;
+        } else {
+            parsers.add(parser);
+            return true;
+        }
+    }
+
+} // class ClarinFCSClientBuilder
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescription.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescription.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescription.java	(revision 6945)
@@ -0,0 +1,601 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+
+import eu.clarin.sru.client.SRUExtraResponseData;
+
+
+/**
+ * CLARIN-FCS endpoint description holder class.
+ */
+public class ClarinFCSEndpointDescription implements Serializable,
+        SRUExtraResponseData {
+    private static final long serialVersionUID = 9061228238942949036L;
+    private static final QName ROOT_ELEMENT =
+            new QName("http://clarin.eu/fcs/endpoint-description",
+                      "EndpointDescription");
+    private final int version;
+    private final List<URI> capabilites;
+    private final List<DataView> supportedDataViews;
+    private final List<Layer> supportedLayers;
+    private final List<ResourceInfo> resources;
+
+
+    ClarinFCSEndpointDescription(int version, List<URI> capabilites,
+            List<DataView> supportedDataViews, List<Layer> supportedLayers,
+            List<ResourceInfo> resources) {
+        this.version = version;
+        if ((capabilites != null) && !capabilites.isEmpty()) {
+            this.capabilites = Collections.unmodifiableList(capabilites);
+        } else {
+            this.capabilites = Collections.emptyList();
+        }
+        if ((supportedDataViews != null) && !supportedDataViews.isEmpty()) {
+            this.supportedDataViews =
+                    Collections.unmodifiableList(supportedDataViews);
+        } else {
+            this.supportedDataViews = Collections.emptyList();
+        }
+        if ((supportedLayers != null) && !supportedLayers.isEmpty()) {
+            this.supportedLayers =
+                    Collections.unmodifiableList(supportedLayers);
+        } else {
+            this.supportedLayers = Collections.emptyList();
+        }
+        if ((resources != null) && !resources.isEmpty()) {
+            this.resources = Collections.unmodifiableList(resources);
+        } else {
+            this.resources = Collections.emptyList();
+        }
+    }
+
+
+    @Override
+    public QName getRootElement() {
+        return ROOT_ELEMENT;
+    }
+
+
+    /**
+     * Get the version of this endpoint description.
+     *
+     * @return the version of the endpoint description
+     */
+    public int getVersion() {
+        return version;
+    }
+
+
+    /**
+     * Get the list of capabilities supported by this endpoint. The list
+     * contains the appropriate URIs defined by the CLARIN-FCS specification to
+     * indicate support for certain capabilities.
+     *
+     * @return the list of capabilities supported by this endpoint
+     */
+    public List<URI> getCapabilities() {
+        return capabilites;
+    }
+
+
+    /**
+     * Get the list of data views supported by this endpoint.
+     *
+     * @return the list of data views supported by this endpoint
+     */
+    public List<DataView> getSupportedDataViews() {
+        return supportedDataViews;
+    }
+
+
+    /**
+     * Get the list of data views supported by this endpoint.
+     *
+     * @return the list of data views supported by this endpoint
+     */
+    public List<Layer> getSupportedLayers() {
+        return supportedLayers;
+    }
+
+
+    /**
+     * Get the list of top-level resources of this endpoint.
+     *
+     * @return the list of top-level resources of this endpoint
+     */
+    public List<ResourceInfo> getResources() {
+        return resources;
+    }
+
+
+    /**
+     * This class implements a description of a data view supported by the endpoint.
+     */
+    public static final class DataView implements Serializable {
+        /**
+         * Enumeration to indicate the delivery policy of a data view.
+         */
+        public enum DeliveryPolicy {
+            /**
+             * The data view is sent automatically  by the endpoint.
+             */
+            SEND_BY_DEFAULT,
+            /**
+             * A client must explicitly request the endpoint.
+             */
+            NEED_TO_REQUEST;
+        } // enum PayloadDelivery
+        private static final long serialVersionUID = -5628565233032672627L;
+        private final String identifier;
+        private final String mimeType;
+        private final DeliveryPolicy deliveryPolicy;
+
+
+        /**
+         * Constructor. <em>Internal use only!</em>
+         */
+        DataView(String identifier, String mimeType,
+                DeliveryPolicy deliveryPolicy) {
+            if (identifier == null) {
+                throw new NullPointerException("identifier == null");
+            }
+            if (identifier.isEmpty()) {
+                throw new IllegalArgumentException("identifier is empty");
+            }
+            this.identifier = identifier;
+
+            if (mimeType == null) {
+                throw new NullPointerException("mimeType == null");
+            }
+            if (mimeType.isEmpty()) {
+                throw new IllegalArgumentException("mimeType is empty");
+            }
+            this.mimeType = mimeType;
+
+            if (deliveryPolicy == null) {
+                throw new NullPointerException("deliveryPolicy == null");
+            }
+            this.deliveryPolicy = deliveryPolicy;
+        }
+
+
+        /**
+         * Get the identifier of this data view.
+         *
+         * @return the identifier of the data view
+         */
+        public String getIdentifier() {
+            return identifier;
+        }
+
+
+        /**
+         * Get the MIME type of this data view.
+         *
+         * @return the MIME type of this data view
+         */
+        public String getMimeType() {
+            return mimeType;
+        }
+
+
+        /**
+         * Get the delivery policy for this data view.
+         *
+         * @return the delivery policy of this data view
+         * @see DeliveryPolicy
+         */
+        public DeliveryPolicy getDeliveryPolicy() {
+            return deliveryPolicy;
+        }
+
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(getClass().getSimpleName());
+            sb.append("[");
+            sb.append("identifier=").append(identifier);
+            sb.append(", mimeType=").append(mimeType);
+            sb.append("]");
+            return sb.toString();
+        }
+    } // class DataView
+
+
+    public static final class Layer implements Serializable {
+        /**
+         * Enumeration to indicate the content encoding of a layer.
+         */
+        public enum ContentEncoding {
+            /**
+             * The layer is encoded as content values.
+             */
+            VALUE,
+            /**
+             * The layer is encoded just as segment units.
+             */
+            EMPTY
+        }
+        private static final long serialVersionUID = 6641490182609459912L;
+        private final String identifier;
+        private final URI resultId;
+        private final String layerType;
+        private final ContentEncoding encoding;
+        private final String qualifier;
+        private final String altValueInfo;
+        private final URI altValueInfoURI;
+
+
+        /**
+         * Constructor. <em>Internal use only!</em>
+         */
+        Layer(String identifier, URI resultId, String layerType,
+                ContentEncoding encoding, String qualifier,
+                String altValueInfo, URI altValueInfoURI) {
+            if (identifier == null) {
+                throw new NullPointerException("identifier == null");
+            }
+            if (identifier.isEmpty()) {
+                throw new IllegalArgumentException("identifier is empty");
+            }
+            this.identifier = identifier;
+
+            if (resultId == null) {
+                throw new NullPointerException("resultId == null");
+            }
+            this.resultId = resultId;
+
+            if (layerType == null) {
+                throw new NullPointerException("layerType == null");
+            }
+            if (layerType.isEmpty()) {
+                throw new IllegalArgumentException("layerType is empty");
+            }
+            this.layerType = layerType;
+            this.encoding = (encoding != null)
+                    ? encoding
+                    : ContentEncoding.VALUE;
+            this.qualifier = qualifier;
+            this.altValueInfo = altValueInfo;
+            this.altValueInfoURI = altValueInfoURI;
+        }
+
+
+        /**
+         * Get the identifier of this layer
+         *
+         * @return the identifier of the layer
+         */
+        public String getIdentifier() {
+            return identifier;
+        }
+
+
+        /**
+         * Get the result URI of this layer
+         *
+         * @return the result URI of the layer
+         */
+        public URI getResultId() {
+            return resultId;
+        }
+
+
+        /**
+         * Get the layer type of this layer
+         *
+         * @return the layer type of the layer
+         */
+        public String getLayerType() {
+            return layerType;
+        }
+
+
+        /**
+         * Get the content encoding of this layer
+         *
+         * @return the content encoding of the layer
+         */
+        public ContentEncoding getEncoding() {
+            return encoding;
+        }
+
+
+        /**
+         * Get the qualifier of this layer
+         *
+         * @return the qualifier of the layer or <code>null</code> if none
+         */
+        public String getQualifier() {
+            return qualifier;
+        }
+
+
+        /**
+         * Get the alternative value information of this layer
+         *
+         * @return the alternative value information of the layer or
+         *         <code>null</code> if none
+         */
+        public String getAltValueInfo() {
+            return altValueInfo;
+        }
+
+
+        /**
+         * Get the alternative value information URI of this layer
+         *
+         * @return the alternative value information URI of the layer or
+         *         <code>null</code> if none
+         */
+        public URI getAltValueInfoURI() {
+            return altValueInfoURI;
+        }
+
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(getClass().getSimpleName());
+            sb.append("[");
+            sb.append("identifier=").append(identifier);
+            sb.append(", result-id=").append(resultId);
+            sb.append(", layer-type=").append(layerType);
+            if (encoding != null) {
+                sb.append(", encoding=").append(encoding);
+            }
+            if (qualifier != null) {
+                sb.append(", qualifier=").append(qualifier);
+            }
+            if (altValueInfo != null) {
+                sb.append(", alt-value-info=").append(altValueInfo);
+            }
+            if (altValueInfoURI != null) {
+                sb.append(", alt-value-info-uri=").append(altValueInfoURI);
+            }
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+
+
+    /**
+     * This class implements a description of a resource available at an
+     * endpoint.
+     */
+    public static final class ResourceInfo implements Serializable {
+        private static final long serialVersionUID = 1046130188435071544L;
+        private final String pid;
+        private final Map<String, String> title;
+        private final Map<String, String> description;
+        private final String landingPageURI;
+        private final List<String> languages;
+        private final List<DataView> availableDataViews;
+        private final List<Layer> availableLayers;
+        private final List<ResourceInfo> subResources;
+
+
+        /**
+         * Constructor. <em>Internal use only!</em>
+         */
+        ResourceInfo(String pid, Map<String, String> title,
+                Map<String, String> description, String landingPageURI,
+                List<String> languages, List<DataView> availableDataViews,
+                List<Layer> availableLayers,
+                List<ResourceInfo> subResources) {
+            if (pid == null) {
+                throw new NullPointerException("pid == null");
+            }
+            this.pid = pid;
+
+            if (title == null) {
+                throw new NullPointerException("title == null");
+            }
+            if (title.isEmpty()) {
+                throw new IllegalArgumentException("title is empty");
+            }
+            this.title = Collections.unmodifiableMap(title);
+            if ((description != null) && !description.isEmpty()) {
+                this.description = Collections.unmodifiableMap(description);
+            } else {
+                this.description = null;
+            }
+
+            this.landingPageURI = landingPageURI;
+            if (languages == null) {
+                throw new NullPointerException("languages == null");
+            }
+            if (languages.isEmpty()) {
+                throw new IllegalArgumentException("languages is empty");
+            }
+            this.languages = languages;
+
+            if (availableDataViews == null) {
+                throw new IllegalArgumentException("availableDataViews == null");
+            }
+            this.availableDataViews =
+                    Collections.unmodifiableList(availableDataViews);
+
+            if ((availableLayers != null) && !availableLayers.isEmpty()) {
+                this.availableLayers =
+                        Collections.unmodifiableList(availableLayers);
+            } else {
+                this.availableLayers = Collections.emptyList();
+            }
+
+            if ((subResources != null) && !subResources.isEmpty()) {
+                this.subResources = Collections.unmodifiableList(subResources);
+            } else {
+                this.subResources = null;
+            }
+        }
+
+
+        /**
+         * Get the persistent identifier of this resource.
+         *
+         * @return a string representing the persistent identifier of this resource
+         */
+        public String getPid() {
+            return pid;
+        }
+
+
+        /**
+         * Determine, if this resource has sub-resources.
+         *
+         * @return <code>true</code> if the resource has sub-resources,
+         *         <code>false</code> otherwise
+         */
+        public boolean hasSubResources() {
+            return subResources != null;
+        }
+
+
+        /**
+         * Get the title of this resource.
+         *
+         * @return a Map of titles keyed by language code
+         */
+        public Map<String, String> getTitle() {
+            return title;
+        }
+
+
+        /**
+         * Get the title of the resource for a specific language code.
+         *
+         * @param language
+         *            the language code
+         * @return the title for the language code or <code>null</code> if no title
+         *         for this language code exists
+         */
+        public String getTitle(String language) {
+            return title.get(language);
+        }
+
+
+        /**
+         * Get the description of this resource.
+         *
+         * @return a Map of descriptions keyed by language code
+         */
+        public Map<String, String> getDescription() {
+            return description;
+        }
+
+
+        /**
+         * Get the description of the resource for a specific language code.
+         *
+         * @param language
+         *            the language code
+         * @return the description for the language code or <code>null</code> if no
+         *         title for this language code exists
+         */
+        public String getDescription(String language) {
+            return (description != null) ? description.get(language) : null;
+        }
+
+
+        /**
+         * Get the landing page of this resource.
+         *
+         * @return the landing page of this resource or <code>null</code> if not
+         *         applicable
+         */
+        public String getLandingPageURI() {
+            return landingPageURI;
+        }
+
+
+        /**
+         * Get the list of languages in this resource represented as ISO-632-3 three
+         * letter language code.
+         *
+         * @return the list of languages in this resource as a list of ISO-632-3
+         *         three letter language codes.
+         */
+        public List<String> getLanguages() {
+            return languages;
+        }
+
+
+        /**
+         * Check, if this resource supports a certain language.
+         *
+         * @param language
+         *            a language encoded as a ISO-632-3 three letter language
+         *            code
+         * @return <code>true</code> if the language is supported by this
+         *         resource, <code>false</code> otherwise
+         */
+        public boolean supportsLanguage(String language) {
+            if (language == null) {
+                throw new NullPointerException("language == null");
+            }
+            for (String l : languages) {
+                if (language.equals(l)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+
+        /**
+         * Get the direct sub-ordinate resources of this resource.
+         *
+         * @return a list of resources or <code>null</code> if this resource has
+         *         no sub-ordinate resources
+         */
+        public List<ResourceInfo> getSubResources() {
+            return subResources;
+        }
+
+
+        /**
+         * Get the list of data views that are available for this resource.
+         *
+         * @return the list of data views available for this resource
+         */
+        public List<DataView> getAvailableDataViews() {
+            return availableDataViews;
+        }
+
+
+        /**
+         * (ADV-FCS) Get the list of layers that are available for this
+         * resource.
+         *
+         * @return the list of layers views available for this resource
+         */
+        public List<Layer> getAvailableLayers() {
+            return availableLayers;
+        }
+    } // class ResourceInfo
+
+} // class ClarinFCSEndpointDescription
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescriptionParser.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescriptionParser.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSEndpointDescriptionParser.java	(revision 6945)
@@ -0,0 +1,546 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRUExtraResponseData;
+import eu.clarin.sru.client.SRUExtraResponseDataParser;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.DataView;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.DataView.DeliveryPolicy;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.Layer;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.Layer.ContentEncoding;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.ResourceInfo;
+
+
+/**
+ * An extra response data parser for parsing CLARIN-FCS endpoint descriptions.
+ */
+public class ClarinFCSEndpointDescriptionParser implements
+        SRUExtraResponseDataParser {
+    /**
+     * constant for infinite resource enumeration parsing depth
+     */
+    public static final int INFINITE_MAX_DEPTH = -1;
+    /**
+     * constant for default parsing resource enumeration parsing depth
+     */
+    public static final int DEFAULT_MAX_DEPTH  = INFINITE_MAX_DEPTH;
+    private static final Logger logger =
+            LoggerFactory.getLogger(ClarinFCSClientBuilder.class);
+    private static final String ED_NS_URI =
+            "http://clarin.eu/fcs/endpoint-description";
+    private static final QName ED_ROOT_ELEMENT =
+            new QName(ED_NS_URI, "EndpointDescription");
+    private static final int VERSION_1 = 1;
+    private static final int VERSION_2 = 2;
+    private static final String CAPABILITY_PREFIX =
+            "http://clarin.eu/fcs/capability/";
+    private static final URI CAPABILITY_BASIC_SEARCH =
+            URI.create("http://clarin.eu/fcs/capability/basic-search");
+    private static final URI CAPABILITY_ADVANCED_SEARCH =
+            URI.create("http://clarin.eu/fcs/capability/advanced-search");
+    private static final String MIMETYPE_HITS_DATAVIEW =
+            "application/x-clarin-fcs-hits+xml";
+    private final int maxDepth;
+
+
+    /**
+     * Constructor. By default, the parser will parse the endpoint resource
+     * enumeration to an infinite depth.
+     */
+    public ClarinFCSEndpointDescriptionParser() {
+        this(DEFAULT_MAX_DEPTH);
+    }
+
+
+    /**
+     * Constructor.
+     *
+     * @param maxDepth
+     *            maximum depth for parsing the endpoint resource enumeration.
+     * @throws IllegalArgumentException
+     *             if an argument is illegal
+     */
+    public ClarinFCSEndpointDescriptionParser(int maxDepth) {
+        if (maxDepth < -1) {
+            throw new IllegalArgumentException("maxDepth < -1");
+        }
+        this.maxDepth = maxDepth;
+    }
+
+
+    @Override
+    public boolean supports(QName name) {
+        return ED_ROOT_ELEMENT.equals(name);
+    }
+
+
+    @Override
+    public SRUExtraResponseData parse(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+        final int version = parseVersion(reader);
+        if ((version != VERSION_1) && (version != VERSION_2)) {
+            throw new SRUClientException("Attribute 'version' of " +
+                    "element '<EndpointDescription>' must be of value '1' or '2'");
+        }
+        reader.next(); // consume start tag
+
+        // Capabilities
+        List<URI> capabilities = null;
+        XmlStreamReaderUtils.readStart(reader, ED_NS_URI, "Capabilities", true);
+        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                "Capability", (capabilities == null))) {
+            final String s = XmlStreamReaderUtils.readString(reader, true);
+            try {
+                if (!s.startsWith(CAPABILITY_PREFIX)) {
+                    throw new XMLStreamException("Capabilites must start " +
+                            "with prefix '" + CAPABILITY_PREFIX +
+                            "' (offending value = '" + s +"')",
+                            reader.getLocation());
+                }
+                final URI uri = new URI(s);
+                if (capabilities == null) {
+                    capabilities = new ArrayList<URI>();
+                }
+                capabilities.add(uri);
+            } catch (URISyntaxException e) {
+                throw new XMLStreamException("Capabilities must be encoded " +
+                        "as URIs (offending value = '" + s + "')",
+                        reader.getLocation(), e);
+            }
+            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Capability");
+        } // while
+        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Capabilities");
+
+        if (capabilities == null) {
+            throw new SRUClientException("Endpoint must support at " +
+                    "least one capability!");
+        }
+        final boolean hasBasicSearch =
+                (capabilities.indexOf(CAPABILITY_BASIC_SEARCH) != -1);
+        final boolean hasAdvancedSearch =
+                (capabilities.indexOf(CAPABILITY_ADVANCED_SEARCH) != -1);
+        if (!hasBasicSearch) {
+            throw new SRUClientException("Endpoint must support " +
+                    "'basic-search' (" + CAPABILITY_BASIC_SEARCH +
+                    ") to conform to CLARIN-FCS specification");
+        }
+
+        // SupportedDataViews
+        List<DataView> supportedDataViews = null;
+        XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                "SupportedDataViews", true);
+        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                "SupportedDataView", (supportedDataViews == null), true)) {
+            final String id = XmlStreamReaderUtils.readAttributeValue(
+                    reader, null, "id", true);
+            if ((id.indexOf(' ') != -1) || (id.indexOf(',') != -1) ||
+                    (id.indexOf(';') != -1)) {
+                throw new XMLStreamException("Value of attribute 'id' on " +
+                        "element '<SupportedDataView>' may not contain the " +
+                        "characters ',' (comma) or ';' (semicolon) " +
+                        "or ' ' (space)", reader.getLocation());
+            }
+            final DeliveryPolicy policy = parsePolicy(reader);
+            reader.next(); // consume start tag
+
+            final String type = XmlStreamReaderUtils.readString(reader, true);
+            // do some sanity checks ...
+            if (supportedDataViews != null) {
+                for (DataView dataView : supportedDataViews) {
+                    if (dataView.getIdentifier().equals(id)) {
+                        throw new XMLStreamException("Supported data view " +
+                                "with identifier '" + id +
+                                "' was already declared", reader.getLocation());
+                    }
+                    if (dataView.getMimeType().equals(type)) {
+                        throw new XMLStreamException("Supported data view " +
+                                "with MIME type '" + type +
+                                "' was already declared", reader.getLocation());
+                    }
+                }
+            } else {
+                supportedDataViews = new ArrayList<DataView>();
+            }
+            supportedDataViews.add(new DataView(id, type, policy));
+            XmlStreamReaderUtils.readEnd(reader,
+                    ED_NS_URI, "SupportedDataView");
+        } // while
+        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI,
+                "SupportedDataViews", true);
+        boolean found = false;
+        if (supportedDataViews != null) {
+            for (DataView dataView : supportedDataViews) {
+                if (MIMETYPE_HITS_DATAVIEW.equals(dataView.getMimeType())) {
+                    found = true;
+                    break;
+                }
+            }
+        }
+        if (!found) {
+            throw new SRUClientException("Endpoint must support " +
+                    "generic hits dataview (expected MIME type '" +
+                    MIMETYPE_HITS_DATAVIEW +
+                    "') to conform to CLARIN-FCS specification");
+        }
+
+        // SupportedLayers
+        List<Layer> supportedLayers = null;
+        if (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                "SupportedLayers", false)) {
+            while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                    "SupportedLayer", (supportedLayers == null), true)) {
+                final String id = XmlStreamReaderUtils.readAttributeValue(reader, null, "id");
+                if ((id.indexOf(' ') != -1) || (id.indexOf(',') != -1) ||
+                        (id.indexOf(';') != -1)) {
+                    throw new XMLStreamException("Value of attribute 'id' on " +
+                            "element '<SupportedLayer>' may not contain the " +
+                            "characters ',' (comma) or ';' (semicolon) " +
+                            "or ' ' (space)", reader.getLocation());
+                }
+                URI resultId = null;
+                final String s1 =
+                        XmlStreamReaderUtils.readAttributeValue(reader,
+                                null, "result-id");
+                try {
+                    resultId = new URI(s1);
+                } catch (URISyntaxException e) {
+                    throw new XMLStreamException("'result-id' must be encoded " +
+                            "as URIs (offending value = '" + s1 + "')",
+                            reader.getLocation(), e);
+                }
+                final ContentEncoding encoding = parseContentEncoding(reader);
+                String qualifier = XmlStreamReaderUtils.readAttributeValue(reader, null, "qualifier", false);
+                String altValueInfo = XmlStreamReaderUtils.readAttributeValue(reader, null, "alt-value-info", false);
+                URI altValueInfoURI = null;
+                final String s2 =
+                        XmlStreamReaderUtils.readAttributeValue(reader, null,
+                                "alt-value-info-uri", false);
+                if (s2 != null) {
+                    try {
+                        altValueInfoURI = new URI(s2);
+                    } catch (URISyntaxException e) {
+                        throw new XMLStreamException("'alt-value-info-uri' must be encoded " +
+                                "as URIs (offending value = '" + s2 + "')",
+                                reader.getLocation(), e);
+                    }
+                }
+                reader.next(); // consume element
+                final String layer = XmlStreamReaderUtils.readString(reader, true);
+                logger.debug("layer: id={}, resultId={}, layer={}, encoding={}, qualifier={}, alt-value-info={}, alt-value-info-uri={}",
+                        id, resultId, layer, encoding, qualifier, altValueInfo, altValueInfoURI);
+
+                XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "SupportedLayer");
+                if (supportedLayers == null) {
+                    supportedLayers = new ArrayList<Layer>();
+                }
+                supportedLayers.add(new Layer(id, resultId, layer,
+                        encoding, qualifier, altValueInfo,
+                        altValueInfoURI));
+            } // while
+            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI,
+                    "SupportedLayers", true);
+        }
+        if (hasAdvancedSearch && (supportedLayers == null)) {
+            throw new SRUClientException("Endpoint must declare " +
+                    "all supported layers (<SupportedLayers>) if they " +
+                    "provide the 'advanced-search' (" +
+                    CAPABILITY_ADVANCED_SEARCH + ") capability");
+        }
+        if (!hasAdvancedSearch && (supportedLayers != null)) {
+            // XXX: hard error?!
+            logger.warn("Endpoint superflously declared supported " +
+                    "layers (<SupportedLayers> without providing the " +
+                    "'advanced-search' (" + CAPABILITY_ADVANCED_SEARCH + ") capability");
+        }
+
+        // Resources
+        final List<ResourceInfo> resources =
+                parseResources(reader, 0, maxDepth,
+                        supportedDataViews, supportedLayers);
+
+        // skip over extensions
+        while (!XmlStreamReaderUtils.peekEnd(reader,
+                ED_NS_URI, "EndpointDescription")) {
+            if (reader.isStartElement()) {
+                final String namespaceURI = reader.getNamespaceURI();
+                final String localName    = reader.getLocalName();
+                logger.debug("skipping over extension with element {{}}{}",
+                        namespaceURI, localName);
+                XmlStreamReaderUtils.skipTag(reader, namespaceURI, localName);
+            }
+        }
+
+        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "EndpointDescription");
+
+        return new ClarinFCSEndpointDescription(version, capabilities,
+                supportedDataViews, supportedLayers, resources);
+    }
+
+
+    /**
+     * Get the maximum resource enumeration parsing depth. The first level is
+     * indicate by the value <code>0</code>.
+     *
+     * @return the default resource parsing depth or <code>-1</code> for
+     *         infinite.
+     */
+    public int getMaximumResourcePArsingDepth() {
+        return maxDepth;
+    }
+
+
+    private static List<ResourceInfo> parseResources(XMLStreamReader reader,
+            int depth, int maxDepth, List<DataView> supportedDataviews,
+            List<Layer> supportedLayers) throws XMLStreamException {
+        List<ResourceInfo> resources = null;
+
+        XmlStreamReaderUtils.readStart(reader, ED_NS_URI, "Resources", true);
+        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                "Resource", (resources == null), true)) {
+            final String pid = XmlStreamReaderUtils.readAttributeValue(reader,
+                    null, "pid", true);
+            reader.next(); // consume start tag
+
+            logger.debug("pid = {}", pid);
+
+            final Map<String, String> title =
+                    parseI18String(reader, "Title", true);
+            logger.debug("title: {}", title);
+
+            final Map<String, String> description =
+                    parseI18String(reader, "Description", false);
+            logger.debug("description: {}", description);
+
+            final String landingPageURI =
+                    XmlStreamReaderUtils.readContent(reader, ED_NS_URI,
+                            "LandingPageURI", false);
+            logger.debug("landingPageURI: {}", landingPageURI);
+
+            List<String> languages = null;
+            XmlStreamReaderUtils.readStart(reader,
+                    ED_NS_URI, "Languages", true);
+            while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                    "Language", (languages == null))) {
+                final String language =
+                        XmlStreamReaderUtils.readString(reader, true);
+                XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Language");
+                if (languages == null) {
+                    languages = new ArrayList<String>();
+                } else {
+                    for (String l : languages) {
+                        if (l.equals(language)) {
+                            throw new XMLStreamException("language '" +
+                                    language + "' was already defined " +
+                                    "in '<Language>'", reader.getLocation());
+                        }
+                    } // for
+                }
+                languages.add(language);
+            } // while
+            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Languages", true);
+            logger.debug("languages: {}", languages);
+
+            // AvailableDataViews
+            XmlStreamReaderUtils.readStart(reader, ED_NS_URI, "AvailableDataViews", true, true);
+            final String dvs = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref", true);
+            reader.next(); // consume start tag
+            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "AvailableDataViews");
+            List<DataView> dataviews = null;
+            for (String dv : dvs.split("\\s+")) {
+                boolean found = false;
+                for (DataView dataview : supportedDataviews) {
+                    if (dataview.getIdentifier().equals(dv)) {
+                        found = true;
+                        if (dataviews == null) {
+                            dataviews = new ArrayList<DataView>();
+                        }
+                        dataviews.add(dataview);
+                        break;
+                    }
+                } // for
+                if (!found) {
+                    throw new XMLStreamException("DataView with id '" + dv +
+                            "' was not declared in <SupportedDataViews>",
+                            reader.getLocation());
+                }
+            } // for
+            logger.debug("DataViews: {}", dataviews);
+
+            // AvailableLayers
+            List<Layer> layers = null;
+            if (XmlStreamReaderUtils.readStart(reader, ED_NS_URI,
+                    "AvailableLayers", false, true)) {
+                final String ls =
+                        XmlStreamReaderUtils.readAttributeValue(reader,
+                                null, "ref", true);
+                reader.next(); // consume start tag
+                XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "AvailableLayers");
+                for (String l : ls.split("\\s+")) {
+                    boolean found = false;
+                    for (Layer layer : supportedLayers) {
+                        if (layer.getIdentifier().equals(l)) {
+                            found = true;
+                            if (layers == null) {
+                                layers = new ArrayList<Layer>();
+                            }
+                            layers.add(layer);
+                            break;
+                        }
+                    } // for
+                    if (!found) {
+                        throw new XMLStreamException("Layer with id '" + l +
+                                "' was not declared in <SupportedLayers>",
+                                reader.getLocation());
+                    }
+                } // for
+                logger.debug("Layers: {}", layers);
+            }
+
+
+            List<ResourceInfo> subResources = null;
+            if (XmlStreamReaderUtils.peekStart(reader,
+                    ED_NS_URI, "Resources")) {
+                final int nextDepth = depth + 1;
+                if ((maxDepth == INFINITE_MAX_DEPTH) ||
+                        (nextDepth < maxDepth)) {
+                    subResources = parseResources(reader, nextDepth,
+                            maxDepth, supportedDataviews, supportedLayers);
+                } else {
+                    XmlStreamReaderUtils.skipTag(reader, ED_NS_URI,
+                            "Resources", true);
+                }
+            }
+
+            while (!XmlStreamReaderUtils.peekEnd(reader,
+                    ED_NS_URI, "Resource")) {
+                if (reader.isStartElement()) {
+                    final String namespaceURI = reader.getNamespaceURI();
+                    final String localName    = reader.getLocalName();
+                    logger.debug("skipping over extension with element " +
+                            "{{}}{} (resource)", namespaceURI, localName);
+                    XmlStreamReaderUtils.skipTag(reader,
+                            namespaceURI, localName);
+                }
+            } // while
+
+            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Resource");
+
+            if (resources == null) {
+                resources = new ArrayList<ResourceInfo>();
+            }
+            resources.add(new ResourceInfo(pid, title, description,
+                    landingPageURI, languages, dataviews, layers,
+                    subResources));
+        } // while
+        XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, "Resources");
+
+        return resources;
+    }
+
+
+    private static Map<String, String> parseI18String(XMLStreamReader reader,
+            String localName, boolean required) throws XMLStreamException {
+        Map<String, String> result = null;
+        while (XmlStreamReaderUtils.readStart(reader, ED_NS_URI, localName,
+                ((result == null) && required), true)) {
+            final String lang = XmlStreamReaderUtils.readAttributeValue(reader,
+                    XMLConstants.XML_NS_URI, "lang", true);
+            reader.next(); // skip start tag
+            final String content = XmlStreamReaderUtils.readString(reader, true);
+            if (result == null) {
+                result = new HashMap<String, String>();
+            }
+            if (result.containsKey(lang)) {
+                throw new XMLStreamException("language '" + lang +
+                        "' already defined for element '<" + localName + ">'",
+                        reader.getLocation());
+            } else {
+                result.put(lang, content);
+            }
+            XmlStreamReaderUtils.readEnd(reader, ED_NS_URI, localName);
+        } // while
+        return result;
+    }
+
+
+    private static int parseVersion(XMLStreamReader reader)
+            throws XMLStreamException {
+        try {
+            final String s = XmlStreamReaderUtils.readAttributeValue(
+                    reader, null, "version", true);
+            return Integer.parseInt(s);
+        } catch (NumberFormatException e) {
+            throw new XMLStreamException("Attribute 'version' is not a number",
+                    reader.getLocation(), e);
+        }
+    }
+
+
+    private static DeliveryPolicy parsePolicy(XMLStreamReader reader)
+            throws XMLStreamException {
+        final String s = XmlStreamReaderUtils.readAttributeValue(reader,
+                null, "delivery-policy", true);
+        if ("send-by-default".equals(s)) {
+            return DeliveryPolicy.SEND_BY_DEFAULT;
+        } else if ("need-to-request".equals(s)) {
+            return DeliveryPolicy.NEED_TO_REQUEST;
+        } else {
+            throw new XMLStreamException("Unexpected value '" + s +
+                    "' for attribute 'delivery-policy' on " +
+                    "element '<SupportedDataView>'", reader.getLocation());
+        }
+    }
+
+
+    private static ContentEncoding parseContentEncoding(XMLStreamReader reader)
+            throws XMLStreamException {
+        final String s = XmlStreamReaderUtils.readAttributeValue(reader,
+                null, "encoding", false);
+        if (s != null) {
+            if ("value".equals(s)) {
+                return ContentEncoding.VALUE;
+            } else if ("need-to-request".equals(s)) {
+                return ContentEncoding.EMPTY;
+            } else {
+                throw new XMLStreamException("Unexpected value '" + s +
+                                "' for attribute 'encoding' on " +
+                                "element '<SupportedLayer>'",
+                        reader.getLocation());
+            }
+        } else {
+            return null;
+        }
+    }
+
+} // class ClarinFCSEndpointDescriptionParser
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordData.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordData.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordData.java	(revision 6945)
@@ -0,0 +1,72 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.List;
+
+import eu.clarin.sru.client.SRURecordData;
+
+
+/**
+ * A record data implementation for CLARIN-FCS.
+ */
+public final class ClarinFCSRecordData implements
+        SRURecordData {
+    /**
+     * The record schema for CLARIN-FCS records.
+     */
+    public static final String RECORD_SCHEMA =
+            "http://clarin.eu/fcs/resource";
+    /**
+     * The legacy record schema for CLARIN-FCS records.
+     *
+     * @deprecated Only use to talk to legacy clients
+     */
+    @Deprecated
+    public static final String LEGACY_RECORD_SCHEMA =
+            "http://clarin.eu/fcs/1.0";
+    private final Resource resource;
+
+
+    ClarinFCSRecordData(String pid, String ref,
+            List<DataView> dataviews,
+            List<Resource.ResourceFragment> resourceFragments) {
+        this.resource = new Resource(pid, ref, dataviews, resourceFragments);
+    }
+
+
+    @Override
+    public boolean isTransient() {
+        return false;
+    }
+
+
+    @Override
+    public String getRecordSchema() {
+        return RECORD_SCHEMA;
+    }
+
+    /**
+     * Get the CLARIN-FCS record resource.
+     *
+     * @return a {@link Resource} object
+     */
+    public Resource getResource() {
+        return resource;
+    }
+
+} // class ClarinFederatedContentSearchRecordData
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordDataParser.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordDataParser.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/ClarinFCSRecordDataParser.java	(revision 6945)
@@ -0,0 +1,68 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRURecordData;
+
+
+/**
+ * A record data parse to parse records conforming to CLARIN-FCS specification.
+ */
+public class ClarinFCSRecordDataParser extends
+        AbstractClarinFCSRecordDataParser {
+
+
+    /**
+     * Constructor.
+     *
+     * @param parsers
+     *            the list of data view parsers to be used by this record data
+     *            parser. This list should contain one
+     *            {@link DataViewParserGenericDOM} or
+     *            {@link DataViewParserGenericString} instance.
+     * @throws NullPointerException
+     *             if parsers is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if parsers is empty or contains duplicate entries
+     */
+    public ClarinFCSRecordDataParser(List<DataViewParser> parsers) {
+        super(parsers);
+    }
+
+
+
+    @Override
+    public String getRecordSchema() {
+        return ClarinFCSRecordData.RECORD_SCHEMA;
+    }
+
+
+    @Override
+    public SRURecordData parse(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+        logger.debug("parsing CLARIN-FCS record");
+
+        return parse(reader, ClarinFCSRecordData.RECORD_SCHEMA);
+    }
+
+} // class ClarinFCSRecordParser
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataView.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataView.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataView.java	(revision 6945)
@@ -0,0 +1,100 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+/**
+ * Base class for Data View implementations according to the CLARIN-FCS record
+ * schema.
+ */
+public abstract class DataView {
+    private final String type;
+    private final String pid;
+    private final String ref;
+
+
+    /**
+     * Constructor.
+     *
+     * @param type
+     *            the MIME type of this DataView
+     * @param pid
+     *            a persistent identifier or <code>null</code>
+     * @param ref
+     *            a reference URI or <code>null</code>
+     * @throws NullPointerException
+     *             if a mandatory argument was not supplied
+     *
+     */
+    protected DataView(String type, String pid, String ref) {
+        if (type == null) {
+            throw new NullPointerException("type == null");
+        }
+        this.type = type;
+        this.pid  = ((pid != null) && !pid.isEmpty()) ? pid : null;
+        this.ref  = ((ref != null) && !ref.isEmpty()) ? ref : null;
+    }
+
+
+    /**
+     * Get the MIME type of this DataView.
+     *
+     * @return the MIME type of this DataView
+     */
+    public String getMimeType() {
+        return type;
+    }
+
+
+    /**
+     * Convenience method to check if this DataView is of a certain MIME type.
+     *
+     * @param type
+     *            the MIME type to test against
+     * @return <code>true</code> if the DataView is in the supplied MIME type,
+     *         <code>false</code> otherwise
+     * @throws NullPointerException
+     *             if any required arguments are not supplied
+     */
+    public boolean isMimeType(String type) {
+        if (type == null) {
+            throw new NullPointerException("mimetype == null");
+        }
+        return (this.type.equals(type));
+    }
+
+    /**
+     * Get the persistent identifier for this DataView.
+     *
+     * @return a persistent identifier or <code>null</code> of this DataView has
+     *         none
+     */
+    public String getPid() {
+        return pid;
+    }
+
+
+    /**
+     * Get the reference URI for this DataView.
+     *
+     * @return a reference URI or <code>null</code> of this DataView has
+     *         none
+     */
+    public String getRef() {
+        return ref;
+    }
+
+} // abstract class DataView
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewAdvanced.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewAdvanced.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewAdvanced.java	(revision 6945)
@@ -0,0 +1,206 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A Data View implementation that stores the content of a Advanced Data View.
+ */
+public class DataViewAdvanced extends DataView {
+    /**
+     * The MIME type for CLARIN-FCS Advanced data views.
+     */
+    public static final String TYPE = "application/x-clarin-fcs-adv+xml";
+    private final List<Layer> layers;
+    private final Unit unit;
+
+
+    /**
+     * Constructor.
+     *
+     * @param pid
+     *            a persistent identifier or <code>null</code>
+     * @param ref
+     *            a reference URI or <code>null</code>
+     * @param unit
+     *            the unit for offsets in this Data View
+     * @param layers
+     *            the list of layers in this Data View
+     * @throws NullPointerException
+     *             if any mandatory argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is illegal
+     */
+    protected DataViewAdvanced(String pid, String ref, Unit unit, List<Layer> layers) {
+        super(TYPE, pid, ref);
+        if (unit == null) {
+            throw new NullPointerException("unit == null");
+        }
+        this.unit = unit;
+        if (layers == null) {
+            throw new NullPointerException("layers == null");
+        }
+        if (layers.isEmpty()) {
+            throw new IllegalArgumentException("layers is empty");
+        }
+        this.layers = Collections.unmodifiableList(layers);
+    }
+
+
+    public Unit getUnit() {
+        return unit;
+    }
+
+
+    public List<Layer> getLayers() {
+        return layers;
+    }
+
+    public enum Unit {
+        ITEM, TIMESTAMP
+    }
+
+
+    public static final class Segment {
+        private final String id;
+        private final long start;
+        private final long end;
+        private URI reference;
+
+        protected Segment(String id, long start, long end, URI reference) {
+            if (id == null) {
+                throw new NullPointerException("id == null");
+            }
+            this.id = id;
+            this.start = start;
+            this.end = end;
+            this.reference = reference;
+        }
+
+
+        public String getId() {
+            return id;
+        }
+
+
+        public long getStartOffset() {
+            return start;
+        }
+
+
+        public long getEndOffset() {
+            return end;
+        }
+
+
+        public URI getRefernce() {
+            return reference;
+        }
+    }
+
+    public static final class Layer {
+        private final String id;
+        private final List<Span> spans;
+
+        protected Layer(String id, List<Span> spans) {
+            if (id == null) {
+                throw new NullPointerException("id == null");
+            }
+            this.id = id;
+            if (spans == null) {
+                throw new NullPointerException("span == null");
+            }
+            if (spans.isEmpty()) {
+                throw new IllegalArgumentException("spans is empty");
+            }
+            this.spans = Collections.unmodifiableList(spans);
+        }
+
+
+        public String getId() {
+            return id;
+        }
+
+
+        public List<Span> getSpans() {
+            return this.spans;
+        }
+    }
+
+    public static final class Span {
+        private final Segment segment;
+        private final String highlight;
+        private final String altValue;
+        private final String content;
+
+
+        protected Span(Segment segment, String highlight, String altValue,
+                String content) {
+            if (segment == null) {
+                throw new NullPointerException("segment == null");
+            }
+            this.segment = segment;
+            this.highlight = highlight;
+            this.altValue = altValue;
+            this.content = content;
+        }
+
+
+        public Segment getSegment() {
+            return segment;
+        }
+
+
+        public String getHighlight() {
+            return highlight;
+        }
+
+
+        public String getAltValue() {
+            return altValue;
+        }
+
+
+        public String getContent() {
+            return content;
+        }
+
+
+        public String getSegmentId() {
+            return segment.getId();
+        }
+
+
+        public long getStartOffset() {
+            return segment.getStartOffset();
+        }
+
+
+        public long getEndOffset() {
+            return segment.getEndOffset();
+        }
+
+
+        public URI getReference() {
+            return segment.getRefernce();
+        }
+    }
+
+} // class DataViewAdvanced
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericDOM.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericDOM.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericDOM.java	(revision 6945)
@@ -0,0 +1,46 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import org.w3c.dom.Document;
+
+
+/**
+ * A generic Data View implementation that stores the content of the Data View
+ * as a DOM document.
+ */
+public class DataViewGenericDOM extends DataView {
+    private final Document document;
+
+
+    protected DataViewGenericDOM(String type, String pid, String ref,
+            Document document) {
+        super(type, pid, ref);
+        this.document = document;
+    }
+
+
+    /**
+     * Get the DataView content.
+     *
+     * @return the DataView content as DOM document.
+     */
+    public Document getDocument() {
+        return document;
+    }
+
+} // class GenericDataView
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericString.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericString.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewGenericString.java	(revision 6945)
@@ -0,0 +1,43 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+/**
+ * A generic Data View implementation that stores the content of a Data View as
+ * a String.
+ */
+public class DataViewGenericString extends DataView {
+    private final String content;
+
+
+    protected DataViewGenericString(String type, String pid, String ref,
+            String content) {
+        super(type, pid, ref);
+        this.content = content;
+    }
+
+
+    /**
+     * Get DataView content.
+     *
+     * @return the DataView content as String.
+     */
+    public String getContent() {
+        return content;
+    }
+
+} // class DataViewGenericString
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewHits.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewHits.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewHits.java	(revision 6945)
@@ -0,0 +1,122 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+/**
+ * A Data View implementation that stores the content of a HITS Data View.
+ */
+public class DataViewHits extends DataView {
+    /**
+     * The MIME type for CLARIN-FCS KWIC data views.
+     */
+    public static final String TYPE = "application/x-clarin-fcs-hits+xml";
+    private final String text;
+    private final int[] offsets;
+    private final int max_offset;
+
+
+    /**
+     * Constructor.
+     *
+     * @param pid
+     *            a persistent identifier or <code>null</code>
+     * @param ref
+     *            a reference URI or <code>null</code>
+     * @param text
+     *            the textual content of the hits
+     * @param offsets
+     *            an array of (start, end) offset pairs that indicate the part
+     *            of the text, that is considered a hit
+     * @param offsets_idx
+     *            the largest index (= hit_count * 2) within the offsets array
+     *            plus one
+     * @throws NullPointerException
+     *             if any mandatory argument is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if any argument is illegal
+     */
+    protected DataViewHits(String pid, String ref, String text,
+            int[] offsets, int offsets_idx) {
+        super(TYPE, pid, ref);
+        if (text == null) {
+            throw new NullPointerException("text == null");
+        }
+        if (text.isEmpty()) {
+            throw new IllegalArgumentException("text is empty");
+        }
+        this.text = text;
+        if (offsets == null) {
+            throw new NullPointerException("offsets == null");
+        }
+        this.offsets = offsets;
+        if (offsets_idx < 0) {
+            throw new IllegalArgumentException("offset_idx < 0");
+        }
+        if (offsets_idx > offsets.length) {
+            throw new IllegalArgumentException("offset_idx > offsets.length");
+        }
+        this.max_offset = (offsets_idx / 2);
+    }
+
+
+    /**
+     * Get the total number of hits in the result.
+     * @return the number of hits
+     */
+    public int getHitCount() {
+        return max_offset;
+    }
+
+
+    /**
+     * Get the text content of the hit. Usually this is complete sentence.
+     *
+     * @return the text content of the hit
+     */
+    public String getText() {
+        return text;
+    }
+
+
+    /**
+     * Get the offsets pointing to range in the text content that yield the hit.
+     *
+     * @param idx
+     *            the hit to retrieve. Must be larger than <code>0</code> and
+     *            smaller than the result of {@link #getHitCount()}.
+     * @return An array of two elements. The first array element is the start
+     *         offset, the second array element is the end offset of the hit
+     *         range.
+     * @throws ArrayIndexOutOfBoundsException
+     *             of the <code>idx</code> argument is out of bounds.
+     */
+    public int[] getHitOffsets(int idx) {
+        if (idx < 0) {
+            throw new ArrayIndexOutOfBoundsException("idx < 0");
+        }
+        if (idx < max_offset) {
+            int[] result = new int[2];
+            result[0] = offsets[(2 *idx)];
+            result[1] = offsets[(2 * idx) + 1];
+            return result;
+        } else {
+            throw new ArrayIndexOutOfBoundsException("idx > " +
+                    (max_offset - 1));
+        }
+    }
+
+} // class DataViewHits
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParser.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParser.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParser.java	(revision 6945)
@@ -0,0 +1,76 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import eu.clarin.sru.client.SRUClientException;
+
+
+/**
+ * Base class for implementing parsers for parsing a specific Data View embedded
+ * in a CLARIN-FCS record.
+ * <p>
+ * If multiple record parsers support a certain type, the one with the highest
+ * priority is selected.
+ * </p>
+ */
+public interface DataViewParser {
+
+    /**
+     * Check, if parser accepts a certain DataView type
+     *
+     * @param type
+     *            the type to be checked
+     * @return <code>true</code> if the parser supports this type,
+     *         <code>false</code> otherwise
+     */
+    public boolean acceptType(String type);
+
+
+    /**
+     * Get the priority for this parser
+     *
+     * @return the priority for this parser
+     */
+    public int getPriority();
+
+
+    /**
+     * Parse a DataView. Implementations of this methods are required to be
+     * thread-safe!
+     *
+     * @param reader
+     *            the {@link XMLStreamReader} to read from
+     * @param type
+     *            the type of the DataView
+     * @param pid
+     *            the pid of this DataView
+     * @param ref
+     *            the reference of this DataView
+     * @return the parsed {@link DataView} object
+     * @throws XMLStreamException
+     *             an error occurred while parsing the DataView
+     * @throws SRUClientException
+     *             any SRU exception, possibly wrapping another exception
+     * @see DataView
+     */
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException;
+
+} // interface DataViewParser
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserAdvanced.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserAdvanced.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserAdvanced.java	(revision 6945)
@@ -0,0 +1,208 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+
+
+/**
+ * An implementation of a Data View parser that parses Advanced Data Views. This
+ * parser expects input that conforms to the CLARIN-FCS specification for the
+ * Advanced Data View.
+ *
+ * @see DataViewAdvanced
+ */
+public final class DataViewParserAdvanced implements DataViewParser {
+    private static final String FCS_ADV_NS =
+            "http://clarin.eu/fcs/dataview/advanced";
+    private static final String UNIT_ITEM      = "item";
+    private static final String UNIT_TIMESTAMP = "timestamp";
+    private static final Logger logger =
+            LoggerFactory.getLogger(DataViewParserAdvanced.class);
+
+
+    @Override
+    public boolean acceptType(String type) {
+        return DataViewAdvanced.TYPE.equals(type);
+    }
+
+
+    @Override
+    public int getPriority() {
+        return 1000;
+    }
+
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        XmlStreamReaderUtils.readStart(reader, FCS_ADV_NS, "Advanced", true, true);
+        final DataViewAdvanced.Unit unit = readUnit(reader);
+        logger.debug("Advanced: unit={}", unit);
+        reader.next(); // skip start tag
+
+        // Segments
+        final Map<String, DataViewAdvanced.Segment> segments =
+                new HashMap<String, DataViewAdvanced.Segment>();
+        XmlStreamReaderUtils.readStart(reader, FCS_ADV_NS, "Segments", true);
+        while (XmlStreamReaderUtils.readStart(reader, FCS_ADV_NS, "Segment",
+                segments.isEmpty(), true)) {
+            final String id =
+                    XmlStreamReaderUtils.readAttributeValue(reader, null, "id");
+            final long start = readOffset(reader, "start", unit);
+            final long end = readOffset(reader, "end", unit);
+            final URI reference = readAttributeURI(reader, null, "ref", false);
+            if (start > end) {
+                throw new SRUClientException("invalid offsets: start > end");
+            }
+            reader.next(); // skip start element
+            XmlStreamReaderUtils.readEnd(reader, FCS_ADV_NS, "Segment");
+
+            logger.debug("segment: id={}, start={}, end={}, ref={}",
+                    id, start, end, reference);
+            DataViewAdvanced.Segment segment =
+                    new DataViewAdvanced.Segment(id, start, end, reference);
+            segments.put(id, segment);
+        } // while
+        XmlStreamReaderUtils.readEnd(reader, FCS_ADV_NS, "Segments");
+
+        // Layers
+        List<DataViewAdvanced.Layer> layers =
+                new ArrayList<DataViewAdvanced.Layer>();
+
+        XmlStreamReaderUtils.readStart(reader, FCS_ADV_NS, "Layers", true);
+        while (XmlStreamReaderUtils.readStart(reader, FCS_ADV_NS, "Layer",
+                layers.isEmpty(), true)) {
+            String id = XmlStreamReaderUtils.readAttributeValue(reader, null, "id");
+            reader.next(); // skip start element
+            logger.debug("layer: id={}", id);
+
+            final List<DataViewAdvanced.Span> spans =
+                    new ArrayList<DataViewAdvanced.Span>();
+            while (XmlStreamReaderUtils.readStart(reader, FCS_ADV_NS, "Span",
+                    spans.isEmpty(), true)) {
+                String segment_ref = XmlStreamReaderUtils.readAttributeValue(reader, null, "ref");
+                String highlight = XmlStreamReaderUtils.readAttributeValue(reader, null, "highlight", false);
+                String altValue = XmlStreamReaderUtils.readAttributeValue(reader, null, "alt-value", false);
+                reader.next(); // skip start element
+                String content = XmlStreamReaderUtils.readString(reader, false);
+                XmlStreamReaderUtils.readEnd(reader, FCS_ADV_NS, "Span");
+
+                logger.debug("span: ref={}, highlight={}, alt-value={}, content={}",
+                        segment_ref, highlight, altValue, content);
+                DataViewAdvanced.Segment segment = segments.get(segment_ref);
+                if (segment == null) {
+                    throw new XMLStreamException("No segment with id '" +
+                            segment_ref + "' found", reader.getLocation());
+                }
+                DataViewAdvanced.Span span =
+                        new DataViewAdvanced.Span(segment, highlight, altValue, content);
+                spans.add(span);
+            } // while
+            XmlStreamReaderUtils.readEnd(reader, FCS_ADV_NS, "Layer");
+
+            DataViewAdvanced.Layer layer =
+                    new DataViewAdvanced.Layer(id, spans);
+            layers.add(layer);
+        } // while
+        XmlStreamReaderUtils.readEnd(reader, FCS_ADV_NS, "Layers");
+
+        XmlStreamReaderUtils.readEnd(reader, FCS_ADV_NS, "Advanced");
+        return new DataViewAdvanced(pid, ref, unit, layers);
+    }
+
+
+    private static final DataViewAdvanced.Unit readUnit(XMLStreamReader reader)
+            throws XMLStreamException {
+
+        final String s = XmlStreamReaderUtils.readAttributeValue(reader, null,
+                "unit", true);
+        if (UNIT_ITEM.equals(s)) {
+            return DataViewAdvanced.Unit.ITEM;
+        } else if (UNIT_TIMESTAMP.equals(s)) {
+            return DataViewAdvanced.Unit.TIMESTAMP;
+        } else {
+            throw new XMLStreamException(
+                    "Attribute 'unit' may only have values '" + UNIT_ITEM +
+                            "' or '" + UNIT_TIMESTAMP + "'",
+                    reader.getLocation());
+        }
+    }
+
+
+    private static final URI readAttributeURI(XMLStreamReader reader,
+            String namespaceURI, String localName, boolean required)
+                    throws XMLStreamException, SRUClientException {
+        final String s = XmlStreamReaderUtils.readAttributeValue(reader,
+                namespaceURI, localName, required);
+        if (s != null) {
+            try {
+                return new URI(s);
+            } catch (URISyntaxException e) {
+                throw new XMLStreamException("malformed URI in attribute '" +
+                        new QName(namespaceURI, localName) + "'",
+                        reader.getLocation(), e);
+            }
+        } else {
+            return null;
+        }
+    }
+
+
+    private static final long readOffset(XMLStreamReader reader,
+            String localName, DataViewAdvanced.Unit unit)
+                    throws XMLStreamException, SRUClientException {
+        String s = XmlStreamReaderUtils.readAttributeValue(reader,
+                null, localName, true);
+        switch (unit) {
+        case ITEM:
+            try {
+                long num = Long.parseLong(s);
+                if (num < 0) {
+                    throw new XMLStreamException("offset is smaller than '0'",
+                            reader.getLocation());
+                }
+                return num;
+            } catch (NumberFormatException e) {
+                throw new XMLStreamException(
+                        "invalid number in attribute '" + localName + "'",
+                        reader.getLocation(), e);
+            }
+        case TIMESTAMP:
+            throw new SRUClientException("no support for 'timestamp' offsets, yet!");
+        default:
+            throw new SRUClientException("internal error: invalid unit (" +
+                    unit + ")");
+        }
+    }
+
+} // class DataViewParserAdvanced
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericDOM.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericDOM.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericDOM.java	(revision 6945)
@@ -0,0 +1,134 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.transform.stax.StAXSource;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+import eu.clarin.sru.client.SRUClientException;
+
+
+/**
+ * An implementation of a Data View parser that stores the content of a Data
+ * View in DOM representation.
+ *
+ * @see DataViewGenericDOM
+ */
+public class DataViewParserGenericDOM implements DataViewParser {
+    private static class TransformHelper {
+        private final DocumentBuilder builder;
+        private final Transformer transformer;
+
+
+        private TransformHelper(DocumentBuilder builder,
+                Transformer transformer) {
+            if (builder == null) {
+                throw new NullPointerException("builder == null");
+            }
+            this.builder = builder;
+            if (transformer == null) {
+                throw new NullPointerException("transformer == null");
+            }
+            this.transformer = transformer;
+        }
+
+
+        private Document transform(XMLStreamReader reader)
+                throws XMLStreamException, TransformerException {
+            // parse STAX to DOM fragment
+            Document document = builder.newDocument();
+            DOMResult result = new DOMResult(document);
+            transformer.transform(new StAXSource(reader), result);
+            return document;
+        }
+
+
+        private void reset() {
+            builder.reset();
+            transformer.reset();
+        }
+    } // private class TransformHelper
+    private final ThreadLocal<TransformHelper> transformHelper;
+
+    public DataViewParserGenericDOM() {
+        final DocumentBuilderFactory builderFactory =
+                DocumentBuilderFactory.newInstance();
+        final TransformerFactory transformerFactory =
+                TransformerFactory.newInstance();
+        this.transformHelper = new ThreadLocal<TransformHelper>() {
+            @Override
+            protected TransformHelper initialValue() {
+                try {
+                    return new TransformHelper(builderFactory.newDocumentBuilder(),
+                                   transformerFactory.newTransformer());
+                } catch (TransformerConfigurationException e) {
+                    throw new InternalError("unexpected error creating new transformer");
+                } catch (ParserConfigurationException e) {
+                    throw new InternalError("unexpected error creating new document builder");
+                }
+            }
+        };
+
+
+    }
+
+
+    @Override
+    public boolean acceptType(String type) {
+        return true;
+    }
+
+
+    @Override
+    public int getPriority() {
+        return Integer.MIN_VALUE;
+    }
+
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        final TransformHelper helper = transformHelper.get();
+        try {
+            final Document document = helper.transform(reader);
+            final NodeList children = document.getChildNodes();
+            if ((children != null) && (children.getLength() > 0)) {
+                return new DataViewGenericDOM(type, pid, ref, document);
+            } else {
+                throw new SRUClientException("element <DataView> does not "
+                        + "contain any nested elements");
+            }
+        } catch (TransformerException e) {
+            throw new SRUClientException("error while parsing dataview", e);
+        } finally {
+            helper.reset();
+        }
+    }
+
+} // class DataViewParserGenericDOM
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericString.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericString.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserGenericString.java	(revision 6945)
@@ -0,0 +1,190 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.io.StringWriter;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import eu.clarin.sru.client.SRUClientException;
+
+
+/**
+ * An implementation of a DataView parser that stores the content of a Data
+ * Views in String representation.
+ *
+ * @see DataViewGenericString
+ */
+public class DataViewParserGenericString implements DataViewParser {
+    private static final XMLOutputFactory factory =
+            XMLOutputFactory.newInstance();
+
+    @Override
+    public boolean acceptType(String type) {
+        return true;
+    }
+
+
+    @Override
+    public int getPriority() {
+        return Integer.MIN_VALUE;
+    }
+
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        XMLStreamWriter to = null;
+        try {
+            final StringWriter writer = new StringWriter();
+            to = factory.createXMLStreamWriter(writer);
+            copy(reader, to);
+            to.close();
+            return new DataViewGenericString(type, pid, ref, writer.toString());
+        } finally {
+            if (to != null) {
+                try {
+                    to.close();
+                } catch (XMLStreamException e) {
+                    /* IGNORE */
+                }
+            }
+        }
+    }
+
+
+    public static void copy(XMLStreamReader from, XMLStreamWriter to)
+            throws XMLStreamException {
+        int depth = 0;
+        while (from.hasNext()) {
+            depth = copyEvent(from, to, depth);
+            from.next();
+            if (depth <= 0) {
+                break;
+            }
+        } // while
+    }
+
+
+    private static int copyEvent(XMLStreamReader from, XMLStreamWriter to,
+            int depth) throws XMLStreamException {
+        switch (from.getEventType()) {
+        case XMLStreamConstants.START_DOCUMENT:
+            {
+                final String version = from.getVersion();
+                if ((version == null) || version.isEmpty()) {
+                    to.writeStartDocument();
+                } else {
+                    to.writeStartDocument(from.getCharacterEncodingScheme(),
+                            from.getVersion());
+                }
+                to.writeCharacters("\n");
+            }
+            return depth + 1;
+
+        case XMLStreamConstants.END_DOCUMENT:
+            to.writeCharacters("\n");
+            to.writeEndDocument();
+            return depth - 1;
+
+        case XMLStreamConstants.START_ELEMENT:
+            copyStartElement(from, to);
+            return depth + 1;
+
+        case XMLStreamConstants.END_ELEMENT:
+            to.writeEndElement();
+            return depth - 1;
+
+        case XMLStreamConstants.SPACE:
+            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
+                    from.getTextLength());
+            return depth;
+
+        case XMLStreamConstants.CDATA:
+            to.writeCData(from.getText());
+            return depth;
+
+        case XMLStreamConstants.CHARACTERS:
+            to.writeCharacters(from.getTextCharacters(), from.getTextStart(),
+                    from.getTextLength());
+            return depth;
+
+        case XMLStreamConstants.COMMENT:
+            to.writeComment(from.getText());
+            return depth;
+
+        case XMLStreamConstants.PROCESSING_INSTRUCTION:
+            to.writeProcessingInstruction(from.getPITarget(), from.getPIData());
+            return depth;
+
+        case XMLStreamConstants.DTD:
+        case XMLStreamConstants.ENTITY_REFERENCE:
+        case XMLStreamConstants.ATTRIBUTE:
+        case XMLStreamConstants.NAMESPACE:
+        case XMLStreamConstants.ENTITY_DECLARATION:
+        case XMLStreamConstants.NOTATION_DECLARATION:
+            /* FALL_TROUGH */
+        } // switch
+        throw new XMLStreamException("unsupported event type: " +
+                from.getEventType());
+    }
+
+    private static void copyStartElement(XMLStreamReader from,
+            XMLStreamWriter to) throws XMLStreamException {
+        final int nsCount = from.getNamespaceCount();
+        if (nsCount > 0) { // yup, got some...
+            for (int i = 0; i < nsCount; ++i) {
+                final String prefix = from.getNamespacePrefix(i);
+                final String uri = from.getNamespaceURI(i);
+                if ((prefix == null) || prefix.isEmpty()) { // default NS
+                    to.setDefaultNamespace(uri);
+                } else {
+                    to.setPrefix(prefix, uri);
+                }
+            }
+        }
+        to.writeStartElement(from.getPrefix(), from.getLocalName(),
+                from.getNamespaceURI());
+
+        if (nsCount > 0) {
+            // write namespace declarations
+            for (int i = 0; i < nsCount; ++i) {
+                final String prefix = from.getNamespacePrefix(i);
+                final String uri = from.getNamespaceURI(i);
+
+                if ((prefix == null) || prefix.isEmpty()) { // default NS
+                    to.writeDefaultNamespace(uri);
+                } else {
+                    to.writeNamespace(prefix, uri);
+                }
+            }
+        }
+        final int attrCount = from.getAttributeCount();
+        if (attrCount > 0) {
+            for (int i = 0; i < attrCount; ++i) {
+                to.writeAttribute(from.getAttributePrefix(i),
+                        from.getAttributeNamespace(i),
+                        from.getAttributeLocalName(i),
+                        from.getAttributeValue(i));
+            }
+        }
+    }
+
+} // class DataViewParserGenericString
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserHits.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserHits.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserHits.java	(revision 6945)
@@ -0,0 +1,101 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.Arrays;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+
+
+/**
+ * An implementation of a Data View parser that parses HITS Data Views. This
+ * parser expects input that conforms to the CLARIN-FCS specification for the
+ * HITS Data View.
+ *
+ * @see DataViewHits
+ */
+public final class DataViewParserHits implements DataViewParser {
+    private static final int OFFSET_CHUNK_SIZE = 8;
+    private static final String FCS_HITS_NS =
+            "http://clarin.eu/fcs/dataview/hits";
+    private static final Logger logger =
+            LoggerFactory.getLogger(DataViewParserHits.class);
+
+
+    @Override
+    public boolean acceptType(String type) {
+        return DataViewHits.TYPE.equals(type);
+    }
+
+
+    @Override
+    public int getPriority() {
+        return 1000;
+    }
+
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        int offsets[] = new int[OFFSET_CHUNK_SIZE];
+        int offsets_idx = 0;
+        StringBuilder buffer = new StringBuilder();
+        XmlStreamReaderUtils.readStart(reader, FCS_HITS_NS, "Result", true);
+
+        int idx = 0;
+        while (!XmlStreamReaderUtils.peekEnd(reader, FCS_HITS_NS, "Result")) {
+            if (buffer.length() > 0) {
+                if (!Character.isWhitespace(buffer.charAt(buffer.length() - 1))) {
+                    buffer.append(' ');
+                }
+                idx = buffer.length();
+            }
+
+            if (XmlStreamReaderUtils.readStart(reader, FCS_HITS_NS, "Hit", false)) {
+                String hit = XmlStreamReaderUtils.readString(reader, false);
+                XmlStreamReaderUtils.readEnd(reader, FCS_HITS_NS, "Hit");
+                if (hit.length() > 0) {
+                    buffer.append(hit);
+                    if (offsets_idx == offsets.length) {
+                        offsets = Arrays.copyOf(offsets, offsets.length + 8);
+                    }
+                    /*
+                     * add pair of offsets and simultaneously increase index
+                     */
+                    offsets[offsets_idx++] = idx;
+                    offsets[offsets_idx++] = idx + hit.length();
+                } else {
+                    logger.warn("skipping empty <Hit> element within <Result> element");
+                }
+            } else {
+                buffer.append(XmlStreamReaderUtils.readString(reader, false));
+            }
+        } // while
+        XmlStreamReaderUtils.readEnd(reader, FCS_HITS_NS, "Result");
+
+        final String text = buffer.toString();
+        return new DataViewHits(pid, ref, text, offsets, offsets_idx);
+    }
+
+} // class DataViewParserHits
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserKWIC.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserKWIC.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/DataViewParserKWIC.java	(revision 6945)
@@ -0,0 +1,107 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.XmlStreamReaderUtils;
+
+
+/**
+ * An implementation of a Data View parser that parses legacy KWIC Data Views.
+ * The input will automatically be upgraded to a HITS Data View and an instance
+ * of {@link DataViewHits} will be returned.
+ *
+ * @see DataViewHits
+ * @deprecated Use only to talk to legacy clients. Endpoints should upgrade to
+ *             recent CLARIN-FCS specification.
+ */
+@Deprecated
+public class DataViewParserKWIC implements DataViewParser {
+    private static final String FCS_KWIC_NS = "http://clarin.eu/fcs/1.0/kwic";
+    private static final String KWIC_LEGACY_TYPE = "kwic";
+    private static final String KWIC_TYPE =
+            "application/x-clarin-fcs-kwic+xml";
+    private static final Logger logger =
+            LoggerFactory.getLogger(DataViewParserKWIC.class);
+
+    @Override
+    public boolean acceptType(String type) {
+        return KWIC_TYPE.equals(type) || KWIC_LEGACY_TYPE.equals(type);
+    }
+
+    @Override
+    public int getPriority() {
+        return 1000;
+    }
+
+    @Override
+    public DataView parse(XMLStreamReader reader, String type, String pid,
+            String ref) throws XMLStreamException, SRUClientException {
+        if (KWIC_LEGACY_TYPE.equals(type)) {
+            logger.warn("type '" + KWIC_LEGACY_TYPE + "' is deprecated " +
+                    "for a KWIC <DataView>, please use '" +
+                    KWIC_TYPE + "' instead");
+        }
+        String left = null;
+        String keyword = null;
+        String right = null;
+
+        XmlStreamReaderUtils.readStart(reader, FCS_KWIC_NS, "kwic", true);
+        if (XmlStreamReaderUtils.readStart(reader, FCS_KWIC_NS, "c", false)) {
+            left = XmlStreamReaderUtils.readString(reader, false);
+            XmlStreamReaderUtils.readEnd(reader, FCS_KWIC_NS, "c");
+        }
+        keyword = XmlStreamReaderUtils.readContent(reader, FCS_KWIC_NS, "kw", true);
+        if (XmlStreamReaderUtils.readStart(reader, FCS_KWIC_NS, "c", false)) {
+            right = XmlStreamReaderUtils.readString(reader, false);
+            XmlStreamReaderUtils.readEnd(reader, FCS_KWIC_NS, "c");
+        }
+        XmlStreamReaderUtils.readEnd(reader, FCS_KWIC_NS, "kwic");
+
+        logger.debug("left='{}' keyword='{}', right='{}'",
+                left, keyword, right);
+
+        logger.warn("Upgraded deprecated KWIC dataview to HITS dataview. " +
+                "Please upgrade to the new CLARIN-FCS specification " +
+                "as soon as possible.");
+        final int[] offsets    = new int[3];
+        final StringBuilder sb = new StringBuilder();
+        if (left != null) {
+            sb.append(left);
+            if (!Character.isWhitespace(sb.charAt(sb.length() - 1))) {
+                sb.append(" ");
+            }
+        }
+        offsets[0] = sb.length();
+        sb.append(keyword);
+        offsets[1] = sb.length();
+        if (right != null) {
+            if (!Character.isWhitespace(sb.charAt(sb.length() - 1))) {
+                sb.append(" ");
+            }
+            sb.append(right);
+        }
+        return new DataViewHits(pid, ref, sb.toString(), offsets, 3);
+    }
+
+} // class DataViewParserKWIC
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/LegacyClarinFCSRecordDataParser.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/LegacyClarinFCSRecordDataParser.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/LegacyClarinFCSRecordDataParser.java	(revision 6945)
@@ -0,0 +1,72 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.List;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRURecordData;
+
+
+/**
+ * A record data parse to parse legacy records.
+ *
+ * @deprecated Use only to talk to legacy clients. Endpoints should upgrade to
+ *             recent CLARIN-FCS specification.
+ */
+@Deprecated
+public class LegacyClarinFCSRecordDataParser extends
+        AbstractClarinFCSRecordDataParser {
+
+
+    /**
+     * Constructor.
+     *
+     * @param parsers
+     *            the list of data view parsers to be used by this record data
+     *            parser. This list should contain one
+     *            {@link DataViewParserGenericDOM} or
+     *            {@link DataViewParserGenericString} instance.
+     * @throws NullPointerException
+     *             if parsers is <code>null</code>
+     * @throws IllegalArgumentException
+     *             if parsers is empty or contains duplicate entries
+     */
+    public LegacyClarinFCSRecordDataParser(List<DataViewParser> parsers) {
+        super(parsers);
+    }
+
+
+    @Override
+    public String getRecordSchema() {
+        return ClarinFCSRecordData.LEGACY_RECORD_SCHEMA;
+    }
+
+
+    @Override
+    public SRURecordData parse(XMLStreamReader reader)
+            throws XMLStreamException, SRUClientException {
+        logger.warn("The endpoint supplied data in the deprecated CLARIN-FCS " +
+            "record data format. Please upgrade to the new CLARIN-FCS " +
+                "specification as soon as possible.");
+        return parse(reader, ClarinFCSRecordData.LEGACY_RECORD_SCHEMA);
+    }
+
+} // class LegacyClarinFCSRecordDataParser
Index: FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/Resource.java
===================================================================
--- FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/Resource.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/main/java/eu/clarin/sru/client/fcs/Resource.java	(revision 6945)
@@ -0,0 +1,180 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A CLARIN-FCS resource
+ */
+public class Resource {
+    /**
+     * A CLARIN-FCS resource fragment
+     */
+    public final static class ResourceFragment {
+        private final String pid;
+        private final String ref;
+        private final List<DataView> dataviews;
+
+
+        ResourceFragment(String pid, String ref, List<DataView> dataviews) {
+            this.pid = ((pid != null) && !pid.isEmpty()) ? pid : null;
+            this.ref = ((ref != null) && !ref.isEmpty()) ? ref : null;
+            if ((dataviews != null) && !dataviews.isEmpty()) {
+                this.dataviews = Collections.unmodifiableList(dataviews);
+            } else {
+                this.dataviews = null;
+            }
+        }
+
+
+        /**
+         * Get the persistent identifier for this resource fragment.
+         *
+         * @return a persistent identifier or <code>null</code> of this resource
+         *         fragment has none
+         */
+        public String getPid() {
+            return pid;
+        }
+
+
+        /**
+         * Get the reference URI for this resource fragment.
+         *
+         * @return a reference URI or <code>null</code> of this resource
+         *         fragment has none
+         */
+        public String getRef() {
+            return ref;
+        }
+
+
+        /**
+         * Convenience method to check if this resource fragment has any
+         * data views.
+         *
+         * @return <code>true</code> if this resource fragment has data views,
+         *         <code>false</code> otherwise
+         */
+        public boolean hasDataViews() {
+            return (dataviews != null);
+        }
+
+
+        /**
+         * Get the list of data view objects for this this resource fragment.
+         *
+         * @return a list of {@link DataView} objects or <code>null</code>,
+         *         or <code>null</code> if this resource fragment has
+         *         none
+         */
+        public List<DataView> getDataViews() {
+            return dataviews;
+        }
+    } // inner class ResourceFragment
+    private final String pid;
+    private final String ref;
+    private final List<DataView> dataviews;
+    private final List<ResourceFragment> resourceFragments;
+
+
+    Resource(String pid, String ref, List<DataView> dataviews,
+            List<ResourceFragment> resourceFragments) {
+        this.pid = ((pid != null) && !pid.isEmpty()) ? pid : null;
+        this.ref = ((ref != null) && !ref.isEmpty()) ? ref : null;
+        if ((dataviews != null) && !dataviews.isEmpty()) {
+            this.dataviews = Collections.unmodifiableList(dataviews);
+        } else {
+            this.dataviews = null;
+        }
+        if ((resourceFragments != null) && !resourceFragments.isEmpty()) {
+            this.resourceFragments =
+                    Collections.unmodifiableList(resourceFragments);
+        } else {
+            this.resourceFragments = null;
+        }
+    }
+
+
+    /**
+     * Get the persistent identifier for this resource.
+     *
+     * @return a persistent identifier or <code>null</code> of this resource has
+     *         none
+     */
+    public String getPid() {
+        return pid;
+    }
+
+
+    /**
+     * Get the reference URI for this resource.
+     *
+     * @return a reference URI or <code>null</code> of this resource has
+     *         none
+     */
+    public String getRef() {
+        return ref;
+    }
+
+
+    /**
+     * Convenience method to check if this resource has any dataviews.
+     *
+     * @return <code>true</code> if this resource has dataviews,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasDataViews() {
+        return (dataviews != null);
+    }
+
+
+    /**
+     * Get the list of dataview objects for this this resource.
+     *
+     * @return a list of {@link DataView} objects or <code>null</code>, or
+     *         <code>null</code> if this resource has none
+     */
+    public List<DataView> getDataViews() {
+        return dataviews;
+    }
+
+
+    /**
+     * Convenience method to check if this resource has any resource fragments.
+     *
+     * @return <code>true</code> if this resource has resource fragments,
+     *         <code>false</code> otherwise
+     */
+    public boolean hasResourceFragments() {
+        return (resourceFragments != null);
+    }
+
+
+    /**
+     * Get the list of resource fragment objects for this this resource.
+     *
+     * @return a list of {@link ResourceFragment} objects or <code>null</code>,
+     *         or <code>null</code> if this resource has none
+     */
+    public List<ResourceFragment> getResourceFragments() {
+        return resourceFragments;
+    }
+
+} // class Resource
Index: FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestClient.java
===================================================================
--- FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestClient.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestClient.java	(revision 6945)
@@ -0,0 +1,103 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClient;
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRUExplainRequest;
+import eu.clarin.sru.client.SRUExplainResponse;
+import eu.clarin.sru.client.SRUScanRequest;
+import eu.clarin.sru.client.SRUScanResponse;
+import eu.clarin.sru.client.SRUSearchRetrieveRequest;
+import eu.clarin.sru.client.SRUSearchRetrieveResponse;
+import eu.clarin.sru.client.SRUVersion;
+import eu.clarin.sru.client.fcs.ClarinFCSClientBuilder;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescriptionParser;
+
+
+public class TestClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestClient.class);
+
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+
+            SRUClient client = new ClarinFCSClientBuilder()
+                    .addDefaultDataViewParsers()
+                    .setDefaultSRUVersion(SRUVersion.VERSION_2_0)
+                    .unknownDataViewAsString()
+                    .enableLegacySupport()
+                    .registerExtraResponseDataParser(
+                            new ClarinFCSEndpointDescriptionParser())
+                    .buildClient();
+
+            // explain
+            try {
+                logger.info("performing 'explain' request ...");
+                SRUExplainRequest request = TestUtils.makeExplainRequest(args[0]);
+                SRUExplainResponse response = client.explain(request);
+                TestUtils.printExplainResponse(response);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'explain' request", e);
+            }
+
+            // scan
+            try {
+                logger.info("performing 'scan' request ...");
+                SRUScanRequest request = TestUtils.makeScanRequest(args[0]);
+                SRUScanResponse response = client.scan(request);
+                TestUtils.printScanResponse(response);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'scan' request", e);
+            }
+
+            // searchRetrieve
+            try {
+                logger.info("performing 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request = TestUtils.makeSearchRequest(args[0], null);
+                SRUSearchRetrieveResponse response = client.searchRetrieve(request);
+                TestUtils.printSearchResponse(response);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'searchRetrieve' request", e);
+            }
+
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+    static {
+        org.apache.log4j.BasicConfigurator
+                .configure(new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger = org.apache.log4j.Logger
+                .getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin")
+                .setLevel(org.apache.log4j.Level.DEBUG);
+    }
+
+}
Index: FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestSimpleClient.java
===================================================================
--- FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestSimpleClient.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestSimpleClient.java	(revision 6945)
@@ -0,0 +1,174 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRUDefaultHandlerAdapter;
+import eu.clarin.sru.client.SRUDiagnostic;
+import eu.clarin.sru.client.SRUExplainRecordData;
+import eu.clarin.sru.client.SRUExplainRequest;
+import eu.clarin.sru.client.SRURecordData;
+import eu.clarin.sru.client.SRUScanRequest;
+import eu.clarin.sru.client.SRUSearchRetrieveRequest;
+import eu.clarin.sru.client.SRUSimpleClient;
+import eu.clarin.sru.client.SRUWhereInList;
+import eu.clarin.sru.client.fcs.ClarinFCSClientBuilder;
+import eu.clarin.sru.client.fcs.ClarinFCSRecordData;
+
+
+public class TestSimpleClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestSimpleClient.class);
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+
+            SRUSimpleClient client = new ClarinFCSClientBuilder()
+                        .addDefaultDataViewParsers()
+                        .unknownDataViewAsString()
+                        .buildSimpleClient();
+
+            /*
+             * just use one dump handler for each request.
+             * A real application should be smarter here and use the
+             * appropriate handler
+             */
+            SRUDefaultHandlerAdapter handler = new SRUDefaultHandlerAdapter() {
+                @Override
+                public void onDiagnostics(List<SRUDiagnostic> diagnostics)
+                        throws SRUClientException {
+                    for (SRUDiagnostic diagnostic : diagnostics) {
+                        logger.info("onDiagnostics: uri={}, detail={}, message={}",
+                                diagnostic.getURI(), diagnostic.getDetails(),
+                                diagnostic.getMessage());
+                    }
+                }
+
+                @Override
+                public void onRequestStatistics(int bytes, long millisTotal,
+                        long millisNetwork, long millisParsing) {
+                    logger.info("onRequestStatistics(): {} bytes in {} millis",
+                            bytes, millisTotal);
+                }
+
+                @Override
+                public void onStartTerms() throws SRUClientException {
+                    logger.info("onStartTerms()");
+                }
+
+                @Override
+                public void onFinishTerms() throws SRUClientException {
+                    logger.info("onFinishTerms()");
+                }
+
+                @Override
+                public void onTerm(String value, int numberOfRecords,
+                        String displayTerm, SRUWhereInList whereInList)
+                        throws SRUClientException {
+                    logger.info("onTerm(): value = {}, numberOfRecords = {}, " +
+                            "displayTerm = {}, whereInList = {}",
+                            value, numberOfRecords, displayTerm, whereInList);
+                }
+
+                @Override
+                public void onStartRecords(int numberOfRecords,
+                        String resultSetId, int resultSetIdleTime)
+                        throws SRUClientException {
+                    logger.info("onStartRecords(): numberOfRecords = {}",
+                            numberOfRecords);
+                }
+
+                @Override
+                public void onFinishRecords(int nextRecordPosition)
+                        throws SRUClientException {
+                    logger.info("onFinishRecords(): nextRecordPosition = {}",
+                            nextRecordPosition);
+                }
+
+                @Override
+                public void onRecord(String identifier, int position,
+                        SRURecordData data) throws SRUClientException {
+                    logger.info("onRecord(): identifier = {}, position = {}, schema = {}",
+                                identifier, position, data.getRecordSchema());
+                    if (ClarinFCSRecordData.RECORD_SCHEMA.equals(data.getRecordSchema())) {
+                        ClarinFCSRecordData record =
+                                (ClarinFCSRecordData) data;
+                        TestUtils.dumpResource(record.getResource());
+                    } else if (SRUExplainRecordData.RECORD_SCHEMA.equals(data.getRecordSchema())) {
+                        TestUtils.dumpExplainRecordData(data);
+                    }
+                }
+
+                @Override
+                public void onSurrogateRecord(String identifier, int position,
+                        SRUDiagnostic data) throws SRUClientException {
+                    logger.info("onSurrogateRecord: identifier = {}, position = {}, uri={}, detail={}, message={}",
+                            identifier, position, data.getURI(), data.getDetails(), data.getMessage());
+                }
+            };
+
+            try {
+                logger.info("performing 'explain' request ...");
+                SRUExplainRequest request = TestUtils.makeExplainRequest(args[0]);
+                client.explain(request, handler);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'explain' request", e);
+            }
+
+            try {
+                logger.info("performing 'scan' request ...");
+                SRUScanRequest request = TestUtils.makeScanRequest(args[0]);
+                client.scan(request, handler);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'scan' request", e);
+            }
+
+            try {
+                logger.info("performing 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request = TestUtils.makeSearchRequest(args[0], null);
+                client.searchRetrieve(request, handler);
+            } catch (SRUClientException e) {
+                logger.error("a fatal error occured while performing 'searchRetrieve' request", e);
+            }
+
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+    static {
+        org.apache.log4j.BasicConfigurator.configure(
+                new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin").setLevel(
+                org.apache.log4j.Level.DEBUG);
+    }
+
+} // class TestSimpleClient
Index: FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestThreadedClient.java
===================================================================
--- FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestThreadedClient.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestThreadedClient.java	(revision 6945)
@@ -0,0 +1,126 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUExplainRequest;
+import eu.clarin.sru.client.SRUExplainResponse;
+import eu.clarin.sru.client.SRUScanRequest;
+import eu.clarin.sru.client.SRUScanResponse;
+import eu.clarin.sru.client.SRUSearchRetrieveRequest;
+import eu.clarin.sru.client.SRUSearchRetrieveResponse;
+import eu.clarin.sru.client.SRUThreadedClient;
+import eu.clarin.sru.client.fcs.ClarinFCSClientBuilder;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescriptionParser;
+
+@Deprecated
+public class TestThreadedClient {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestThreadedClient.class);
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+
+            SRUThreadedClient client = new ClarinFCSClientBuilder()
+                    .addDefaultDataViewParsers()
+                    .unknownDataViewAsString()
+                    .enableLegacySupport()
+                    .registerExtraResponseDataParser(
+                            new ClarinFCSEndpointDescriptionParser())
+                    .buildThreadedClient();
+
+            try {
+                /*
+                 * the following requests will be run asynchronously and
+                 * concurrently
+                 */
+                logger.info("submitting 'explain' request ...");
+                SRUExplainRequest request1 = TestUtils.makeExplainRequest(args[0]);
+                Future<SRUExplainResponse> result1 = client.explain(request1);
+
+                logger.info("submitting 'scan' request ...");
+                SRUScanRequest request2 = TestUtils.makeScanRequest(args[0]);
+                Future<SRUScanResponse> result2 = client.scan(request2);
+
+                logger.info("submitting 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request3 = TestUtils.makeSearchRequest(args[0], null);
+                Future<SRUSearchRetrieveResponse> result3 =
+                        client.searchRetrieve(request3);
+
+                /*
+                 * HACK: the following code is quick and dirty. Don't every busy
+                 * wait on responses like this in a real-world application!
+                 */
+                while (!(result1.isDone() && result2.isDone() &&
+                        result3.isDone())) {
+                    logger.debug("waiting for results ...");
+                    try {
+                        Thread.sleep(125);
+                    } catch (InterruptedException e) {
+                        /* IGNORE */
+                    }
+                }
+
+                try {
+                    TestUtils.printExplainResponse(result1.get());
+                } catch (ExecutionException e) {
+                    logger.error("some error occured while performing 'explain' request", e);
+                }
+                try {
+                    TestUtils.printScanResponse(result2.get());
+                } catch (ExecutionException e) {
+                    logger.error("some error occured while performing 'scan' request", e);
+                }
+                try {
+                    TestUtils.printSearchResponse(result3.get());
+                } catch (ExecutionException e) {
+                    logger.error("some error occured while performing 'searchRetrieve' request", e);
+                }
+            } catch (Exception e) {
+                logger.error("a fatal error occured while performing request", e);
+            }
+
+            client.shutdown();
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+
+
+    static {
+        org.apache.log4j.BasicConfigurator.configure(
+                new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin").setLevel(
+                org.apache.log4j.Level.DEBUG);
+    }
+
+}
Index: FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestThreadedClientCallback.java
===================================================================
--- FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestThreadedClientCallback.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestThreadedClientCallback.java	(revision 6945)
@@ -0,0 +1,144 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import eu.clarin.sru.client.SRUCallback;
+import eu.clarin.sru.client.SRUClientException;
+import eu.clarin.sru.client.SRUExplainRequest;
+import eu.clarin.sru.client.SRUExplainResponse;
+import eu.clarin.sru.client.SRUScanRequest;
+import eu.clarin.sru.client.SRUScanResponse;
+import eu.clarin.sru.client.SRUSearchRetrieveRequest;
+import eu.clarin.sru.client.SRUSearchRetrieveResponse;
+import eu.clarin.sru.client.SRUThreadedClient;
+import eu.clarin.sru.client.fcs.ClarinFCSClientBuilder;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescriptionParser;
+
+
+public class TestThreadedClientCallback {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestThreadedClientCallback.class);
+
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            logger.info("initializing client ...");
+
+            SRUThreadedClient client = new ClarinFCSClientBuilder()
+                    .addDefaultDataViewParsers()
+                    .unknownDataViewAsString()
+                    .enableLegacySupport()
+                    .registerExtraResponseDataParser(
+                            new ClarinFCSEndpointDescriptionParser())
+                    .buildThreadedClient();
+
+            try {
+                final CountDownLatch latch = new CountDownLatch(3);
+
+                /*
+                 * the following requests will be run asynchronously and
+                 * concurrently
+                 * Invoke requests and supply a callback, that
+                 *  a) prints the results
+                 *  b) downs a latch (which is used to make the main-thread wait
+                 *     on the requests to be completed)
+                 */
+                logger.info("submitting 'explain' request ...");
+                SRUExplainRequest request1 =
+                        TestUtils.makeExplainRequest(args[0]);
+                client.explain(request1, new SRUCallback<SRUExplainRequest, SRUExplainResponse>() {
+                    @Override
+                    public void onSuccess(SRUExplainResponse response) {
+                        TestUtils.printExplainResponse(response);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(SRUExplainRequest request,
+                            SRUClientException error) {
+                        logger.error("error while performing request", error);
+                        latch.countDown();
+                    }
+                });
+
+                logger.info("submitting 'scan' request ...");
+                SRUScanRequest request2 =
+                        TestUtils.makeScanRequest(args[0]);
+                client.scan(request2, new SRUCallback<SRUScanRequest, SRUScanResponse>() {
+                    @Override
+                    public void onSuccess(SRUScanResponse response) {
+                        TestUtils.printScanResponse(response);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(SRUScanRequest request,
+                            SRUClientException error) {
+                        logger.error("error while performing request", error);
+                        latch.countDown();
+                    }
+                });
+
+                logger.info("submitting 'searchRetrieve' request ...");
+                SRUSearchRetrieveRequest request3 =
+                        TestUtils.makeSearchRequest(args[0], null);
+                client.searchRetrieve(request3, new SRUCallback<SRUSearchRetrieveRequest, SRUSearchRetrieveResponse>() {
+                    @Override
+                    public void onSuccess(SRUSearchRetrieveResponse response) {
+                        TestUtils.printSearchResponse(response);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onError(SRUSearchRetrieveRequest request,
+                            SRUClientException error) {
+                        logger.error("error while performing request", error);
+                        latch.countDown();
+                    }
+                });
+
+                latch.await();
+            } catch (Exception e) {
+                logger.error("a fatal error occured while performing request", e);
+            }
+
+            client.shutdown();
+            logger.info("done");
+        } else {
+            System.err.println("missing args");
+            System.exit(64);
+        }
+    }
+
+
+    static {
+        org.apache.log4j.BasicConfigurator.configure(
+                new org.apache.log4j.ConsoleAppender(
+                        new org.apache.log4j.PatternLayout("%-5p [%t] %m%n"),
+                        org.apache.log4j.ConsoleAppender.SYSTEM_ERR));
+        org.apache.log4j.Logger logger =
+                org.apache.log4j.Logger.getRootLogger();
+        logger.setLevel(org.apache.log4j.Level.INFO);
+        logger.getLoggerRepository().getLogger("eu.clarin").setLevel(
+                org.apache.log4j.Level.DEBUG);
+    }
+
+}
Index: FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestUtils.java
===================================================================
--- FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestUtils.java	(revision 6945)
+++ FCSSimpleClient/trunk/src/test/java/eu/clarin/sru/client/fcs/TestUtils.java	(revision 6945)
@@ -0,0 +1,360 @@
+/**
+ * This software is copyright (c) 2012-2016 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.client.fcs;
+
+import java.net.URI;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Node;
+
+import eu.clarin.sru.client.SRUDiagnostic;
+import eu.clarin.sru.client.SRUExplainRecordData;
+import eu.clarin.sru.client.SRUExplainRequest;
+import eu.clarin.sru.client.SRUExplainResponse;
+import eu.clarin.sru.client.SRUExtraResponseData;
+import eu.clarin.sru.client.SRURecord;
+import eu.clarin.sru.client.SRURecordData;
+import eu.clarin.sru.client.SRURecordPacking;
+import eu.clarin.sru.client.SRURecordXmlEscaping;
+import eu.clarin.sru.client.SRUScanRequest;
+import eu.clarin.sru.client.SRUScanResponse;
+import eu.clarin.sru.client.SRUSearchRetrieveRequest;
+import eu.clarin.sru.client.SRUSearchRetrieveResponse;
+import eu.clarin.sru.client.SRUSurrogateRecordData;
+import eu.clarin.sru.client.SRUTerm;
+import eu.clarin.sru.client.SRUExplainRecordData.ConfigInfo;
+import eu.clarin.sru.client.SRUExplainRecordData.Schema;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription;
+import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription.ResourceInfo;
+import eu.clarin.sru.client.fcs.ClarinFCSRecordData;
+import eu.clarin.sru.client.fcs.DataView;
+import eu.clarin.sru.client.fcs.DataViewAdvanced;
+import eu.clarin.sru.client.fcs.DataViewGenericDOM;
+import eu.clarin.sru.client.fcs.DataViewGenericString;
+import eu.clarin.sru.client.fcs.DataViewHits;
+import eu.clarin.sru.client.fcs.Resource;
+
+class TestUtils {
+    private static final Logger logger =
+            LoggerFactory.getLogger(TestUtils.class);
+
+    public static SRUExplainRequest makeExplainRequest(String baseURI) {
+        SRUExplainRequest request = new SRUExplainRequest(baseURI);
+        request.setExtraRequestData("x-indent-response", "4");
+        request.setExtraRequestData("x-fcs-endpoint-description", "true");
+        request.setParseRecordDataEnabled(true);
+        return request;
+    }
+
+
+    public static SRUScanRequest makeScanRequest(String baseURI) {
+        SRUScanRequest request = new SRUScanRequest(baseURI);
+        request.setScanClause("fcs.resource = root");
+        request.setExtraRequestData("x-clarin-resource-info", "true");
+        request.setExtraRequestData("x-indent-response", "4");
+        return request;
+    }
+
+
+    public static SRUSearchRetrieveRequest makeSearchRequest(String baseURI, String query) {
+        if ((query == null) || query.isEmpty()) {
+            query = "'Faustus'";
+        }
+        SRUSearchRetrieveRequest request = new SRUSearchRetrieveRequest(baseURI);
+        request.setQuery("fcs" /*SRUClientConstants.QUERY_TYPE_CQL*/, query);
+//        request.setRecordSchema(ClarinFCSRecordData.LEGACY_RECORD_SCHEMA);
+        request.setMaximumRecords(5);
+        request.setRecordXmlEscaping(SRURecordXmlEscaping.XML);
+        request.setRecordPacking(SRURecordPacking.PACKED);
+        request.setExtraRequestData("x-indent-response", "4");
+        return request;
+    }
+
+
+    public static void printExplainResponse(SRUExplainResponse response) {
+        logger.info("displaying results of 'explain' request ...");
+        if (response.hasDiagnostics()) {
+            for (SRUDiagnostic diagnostic : response.getDiagnostics()) {
+                logger.info("uri={}, message={}, detail={}",
+                        diagnostic.getURI(),
+                        diagnostic.getMessage(),
+                        diagnostic.getDetails());
+            }
+        }
+        if (response.hasRecord()) {
+            SRURecord record = response.getRecord();
+            logger.info("schema = {}", record.getRecordSchema());
+            if (record.isRecordSchema(SRUExplainRecordData.RECORD_SCHEMA)) {
+                dumpExplainRecordData(record.getRecordData());
+            }
+            if (record.hasExtraRecordData()) {
+                logger.info("extraRecordInfo = {}",
+                        record.getExtraRecordData());
+            }
+        }
+        if (response.hasExtraResponseData()) {
+            for (SRUExtraResponseData data : response.getExtraResponseData()) {
+                if (data instanceof ClarinFCSEndpointDescription) {
+                    dumpEndpointDescription(
+                            (ClarinFCSEndpointDescription) data);
+                } else {
+                    logger.info("extraResponseData = {} (class={})",
+                            data.getRootElement(), data.getClass().getName());
+                }
+
+            }
+        }
+    }
+
+
+    public static void printScanResponse(SRUScanResponse response) {
+        logger.info("displaying results of 'scan' request ...");
+        if (response.hasDiagnostics()) {
+            for (SRUDiagnostic diagnostic : response.getDiagnostics()) {
+                logger.info("uri={}, message={}, detail={}",
+                        diagnostic.getURI(),
+                        diagnostic.getMessage(),
+                        diagnostic.getDetails());
+            }
+        }
+        if (response.hasTerms()) {
+            for (SRUTerm term : response.getTerms()) {
+                logger.info("value={}, numberOfRecords={}, displayTerm={}",
+                            term.getValue(),
+                            term.getNumberOfRecords(),
+                            term.getDisplayTerm());
+            }
+        } else {
+            logger.info("no terms");
+        }
+    }
+
+
+    public static void printSearchResponse(SRUSearchRetrieveResponse response) {
+        logger.info("displaying results of 'searchRetrieve' request ...");
+        logger.info("numberOfRecords = {}, nextResultPosition = {}",
+                response.getNumberOfRecords(),
+                response.getNextRecordPosition());
+        if (response.hasDiagnostics()) {
+            for (SRUDiagnostic diagnostic : response.getDiagnostics()) {
+                logger.info("uri={}, message={}, detail={}",
+                        diagnostic.getURI(),
+                        diagnostic.getMessage(),
+                        diagnostic.getDetails());
+            }
+        }
+        if (response.hasRecords()) {
+            for (SRURecord record : response.getRecords()) {
+                logger.info("schema = {}, identifier = {}, position = {}",
+                        record.getRecordSchema(),
+                        record.getRecordIdentifier(),
+                        record.getRecordPosition());
+                if (record.isRecordSchema(ClarinFCSRecordData.RECORD_SCHEMA)) {
+                    ClarinFCSRecordData rd =
+                            (ClarinFCSRecordData) record.getRecordData();
+                    dumpResource(rd.getResource());
+                } else if (record.isRecordSchema(SRUSurrogateRecordData.RECORD_SCHEMA)) {
+                    SRUSurrogateRecordData r =
+                            (SRUSurrogateRecordData) record.getRecordData();
+                    logger.info("SURROGATE DIAGNOSTIC: uri={}, message={}, detail={}",
+                                r.getURI(), r.getMessage(), r.getDetails());
+                } else {
+                    logger.info("UNSUPPORTED SCHEMA: {}",
+                            record.getRecordSchema());
+                }
+            }
+        } else {
+            logger.info("no results");
+        }
+    }
+
+
+    public static void dumpExplainRecordData(SRURecordData recordData) {
+        if (SRUExplainRecordData.RECORD_SCHEMA.equals(recordData.getRecordSchema())) {
+            SRUExplainRecordData data = (SRUExplainRecordData) recordData;
+            logger.info("host={}, port={}, database={}",
+                    data.getServerInfo().getHost(),
+                    data.getServerInfo().getPort(),
+                    data.getServerInfo().getDatabase());
+            List<Schema> schemaInfo = data.getSchemaInfo();
+            if (schemaInfo != null) {
+                for (Schema schema : schemaInfo) {
+                    logger.debug("schema: identifier={}, name={}, " +
+                            "location={}, sort={}, retrieve={}",
+                            schema.getIdentifier(),
+                            schema.getName(),
+                            schema.getLocation(),
+                            schema.getSort(),
+                            schema.getRetrieve());
+                }
+            }
+            ConfigInfo configInfo = data.getConfigInfo();
+            if (configInfo != null) {
+                if (configInfo.getDefaults() != null) {
+                    logger.debug("configInfo/default = {}",
+                            configInfo.getDefaults());
+                }
+                if (configInfo.getSettings() != null) {
+                    logger.debug("configInfo/setting = {}",
+                            configInfo.getSettings());
+                }
+                if (configInfo.getSupports() != null) {
+                    logger.debug("configInfo/supports = {}",
+                            configInfo.getSupports());
+                }
+            }
+        }
+    }
+
+
+    public static void dumpResource(Resource resource) {
+        logger.info("CLARIN-FCS: pid={}, ref={}",
+                resource.getPid(), resource.getRef());
+        if (resource.hasDataViews()) {
+            dumpDataView("CLARIN-FCS: ", resource.getDataViews());
+        }
+        if (resource.hasResourceFragments()) {
+            for (Resource.ResourceFragment fragment : resource.getResourceFragments()) {
+                logger.debug("CLARIN-FCS: ResourceFragment: pid={}, ref={}",
+                        fragment.getPid(), fragment.getRef());
+                if (fragment.hasDataViews()) {
+                    dumpDataView("CLARIN-FCS: ResourceFragment/", fragment.getDataViews());
+                }
+            }
+        }
+    }
+
+
+    private static void dumpEndpointDescription(ClarinFCSEndpointDescription ed) {
+        logger.info("dumping <EndpointDescription> (version={})",
+                ed.getVersion());
+        for (URI capability : ed.getCapabilities()) {
+            logger.info("  capability: {}", capability);
+        } // for
+        for (ClarinFCSEndpointDescription.DataView dataView :
+            ed.getSupportedDataViews()) {
+            logger.info("  supportedDataView: id={}, type={}, policy={}",
+                    dataView.getIdentifier(),
+                    dataView.getMimeType(),
+                    dataView.getDeliveryPolicy());
+        } // for
+        for (ClarinFCSEndpointDescription.Layer layer :
+            ed.getSupportedLayers()) {
+            logger.info("  supportedLayer: id={}, result-id={}, " +
+                    "layer-type={}, encoding={}, qualifier={}, " +
+                    "alt-value-info={}, alt-value-info-uri={}",
+                    layer.getIdentifier(),
+                    layer.getResultId(),
+                    layer.getLayerType(),
+                    layer.getEncoding(),
+                    layer.getQualifier(),
+                    layer.getAltValueInfo(),
+                    layer.getAltValueInfoURI());
+        }
+        dumpResourceInfo(ed.getResources(), 1, "  ");
+    }
+
+
+    private static void dumpResourceInfo(List<ResourceInfo> ris, int depth,
+            String indent) {
+        for (ResourceInfo ri : ris) {
+            logger.info("{}[depth={}] <ResourceInfo>", indent, depth);
+            logger.info("{}    pid={}", indent, ri.getPid());
+            logger.info("{}    title: {}", indent, ri.getTitle());
+            if (ri.getDescription() != null) {
+                logger.info("{}    description: {}",
+                        indent, ri.getDescription());
+            }
+            if (ri.getLandingPageURI() != null) {
+                logger.info("{}    landingPageURI: {}",
+                        indent, ri.getLandingPageURI());
+            }
+            for (ClarinFCSEndpointDescription.DataView dv :
+                ri.getAvailableDataViews()) {
+                logger.info("{}    available dataviews: type={}, policy={}",
+                        indent, dv.getMimeType(), dv.getDeliveryPolicy());
+            }
+            for (ClarinFCSEndpointDescription.Layer l :
+                ri.getAvailableLayers()) {
+                logger.info("{}    available layers: result-id={}, layer-type={}",
+                        indent, l.getResultId(), l.getLayerType());
+            }
+            if (ri.hasSubResources()) {
+                dumpResourceInfo(ri.getSubResources(),
+                        depth + 1, indent + "  ");
+            }
+        }
+    }
+
+
+    private static void dumpDataView(String s, List<DataView> dataviews) {
+        for (DataView dataview : dataviews) {
+            logger.info("{}DataView: type={}, pid={}, ref={}",
+                    s, dataview.getMimeType(), dataview.getPid(),
+                    dataview.getRef());
+            if (dataview instanceof DataViewGenericDOM) {
+                final DataViewGenericDOM view = (DataViewGenericDOM) dataview;
+                final Node root = view.getDocument().getFirstChild();
+                logger.info("{}DataView (generic dom): root element <{}> / {}",
+                        s, root.getNodeName(),
+                        root.getOwnerDocument().hashCode());
+            } else if (dataview instanceof DataViewGenericString) {
+                final DataViewGenericString view =
+                        (DataViewGenericString) dataview;
+                logger.info("{}DataView (generic string): data = {}",
+                        s, view.getContent());
+            } else if (dataview instanceof DataViewHits) {
+                final DataViewHits hits = (DataViewHits) dataview;
+                logger.info("{}DataView: {}",
+                        s, addHitHighlights(hits));
+            } else if (dataview instanceof DataViewAdvanced) {
+                final DataViewAdvanced adv = (DataViewAdvanced) dataview;
+                logger.info("{}DataView: unit={}",
+                        s, adv.getUnit());
+                for (DataViewAdvanced.Layer layer : adv.getLayers()) {
+                    logger.info("{}DataView: Layer: id={}",
+                            s, layer.getId());
+                    for (DataViewAdvanced.Span span : layer.getSpans()) {
+                        logger.info("{}DataView:   Span: start={}, end={}, content={}",
+                                s, span.getStartOffset(), span.getEndOffset(), span.getContent());
+                    }
+                }
+            } else {
+                logger.info("{}DataView: cannot display " +
+                        "contents of unexpected class '{}'",
+                        s, dataview.getClass().getName());
+            }
+        }
+    }
+
+
+    private static String addHitHighlights(DataViewHits hits) {
+        StringBuilder sb = new StringBuilder(hits.getText());
+        int corr = 0;
+        for (int i = 0; i < hits.getHitCount(); i++) {
+            int[] offsets = hits.getHitOffsets(i);
+            sb.insert(offsets[0] + corr, "[");
+            corr += 1;
+            sb.insert(offsets[1] + corr, "]");
+            corr += 1;
+        }
+        return sb.toString();
+    }
+
+} // class TestUtils
