<commit_before>/*
 * Copyright 2007-2014 Wyona
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.wyona.org/licenses/APACHE-LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.wyona.yanel.impl.resources;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamSource;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationUtil;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.xml.resolver.tools.CatalogResolver;
import org.apache.xml.serializer.Serializer;
import org.apache.xml.utils.ListingErrorHandler;
import org.w3c.dom.Document;
import org.wyona.commons.io.MimeTypeUtil;
import org.wyona.security.core.api.Identity;
import org.wyona.yanel.core.Resource;
import org.wyona.yanel.core.api.attributes.ViewableV2;
import org.wyona.yanel.core.attributes.viewable.View;
import org.wyona.yanel.core.attributes.viewable.ViewDescriptor;
import org.wyona.yanel.core.serialization.SerializerFactory;
import org.wyona.yanel.core.source.SourceResolver;
import org.wyona.yanel.core.transformation.I18nTransformer3;
import org.wyona.yanel.core.transformation.XIncludeTransformer;
import org.wyona.yanel.core.util.PathUtil;
import org.wyona.yanel.impl.resources.xml.ConfigurableViewDescriptor;
import org.wyona.yarep.core.Repository;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * It is a base class for resources that generate XML. Subclasses must override getContentXML 
 * in order to pass XML for a view.
 * <p>
 * The class has its configuration for views ('default' and 'source' are built in). It resides in *.yanel-rc file, e.g.
 * <pre>
 * &lt;yanel:custom-config>
 * &lt;views>
 *  &lt;!-- No parameter needed for getting this view -->
 *  &lt;view id="default">
 *      &lt;mime-type>text/html&lt;/mime-type>
 *      &lt;xslt>/xslt/global.xsl&lt;/xslt>
 *      &lt;serializer key="HTML_TRANSITIONAL">
 *      &lt;/serializer>
 *  &lt;/view>
 *  
 *  &lt;view id="beautiful">
 *      &lt;mime-type>application/xhtml+xml&lt;/mime-type>
 *      &lt;xslt>/xslt/global.xsl&lt;/xslt>
 *      &lt;serializer key="XHTML_STRICT">
 *      &lt;/serializer>
 *  &lt;/view>
 *  
 *   &lt;view id="atom">
 *      &lt;mime-type>application/atom+xml&lt;/mime-type>
 *      &lt;serializer key="XML">
 *      &lt;/serializer>
 *   &lt;/view>
 *   
 *   &lt;view id="json">
 *      &lt;mime-type>application/json&lt;/mime-type>
 *      &lt;serializer key="TEXT">
 *      &lt;/serializer>
 *   &lt;/view>
 *   
 *   &lt;!-- Skips any provided XSLT-->
 *   &lt;view id="source">
 *      &lt;mime-type>application/xhtml+xml&lt;/mime-type>
 *      &lt;serializer key="XML">
 *      &lt;/serializer>
 *   &lt;/view>
 *&lt;/views>
 *&lt;/yanel:custom-config>
 * </pre>
 * <p>
 * A view is accessed through a request parameter <b>yanel.resource.viewid</b>
 * <p>
 * If no serializer is specified for a view, the serializer will be chosen depending
 * on the mime-type, whereas the default serializer is XHTML_STRICT.
 * </p>
 */
public class BasicXMLResource extends Resource implements ViewableV2 {

    private static final Logger log = LogManager.getLogger(BasicXMLResource.class);

    protected static String DEFAULT_VIEW_ID = "default";
    protected static String SOURCE_VIEW_ID = "source";
    protected static String MOBILE_VIEW_ID = "mobile";

    protected HashMap<String, ViewDescriptor> viewDescriptors;

    private static final String VIEW_ID_PARAM_NAME = "yanel.resource.viewid";

    /**
     * Get view descriptor for a particular view id
     * @param viewId View id
     */
    protected ViewDescriptor getViewDescriptor(String viewId) {
        if (viewId == null) {
            log.debug("View ID is null (" + getPath() + ")");
            // TODO: Setting the view ID to default would actually make sense, but check for backwards compatibility first!
            //viewId = DEFAULT_VIEW_ID;
            return null;
        }
        ViewDescriptor[] viewDescriptors = getViewDescriptors();
        if (viewDescriptors != null) {
            for (int i = 0; i < viewDescriptors.length; i++) {
                if (viewDescriptors[i].getId().equals(viewId)) {
                    return viewDescriptors[i];
                }
            }
            log.warn("No view descriptor found for view id '" + viewId + "' (Resource path: " + getPath() + ")");
            return null;
        } else {
            log.warn("No view descriptors set inside resource configuration (Resource path: " + getPath() + ")!");
            return null;
        }
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#getViewDescriptors()
     */
    public ViewDescriptor[] getViewDescriptors() {
        if (this.viewDescriptors != null) {
            return this.viewDescriptors.values().toArray(new ViewDescriptor[this.viewDescriptors.size()]);
        }
        try {
            this.viewDescriptors = new HashMap<String, ViewDescriptor>();
            // reads views from configuration:
            if (getConfiguration() != null && getConfiguration().getCustomConfiguration() != null) {
                Document customConfigDoc = getConfiguration().getCustomConfiguration();
                Configuration config = ConfigurationUtil.toConfiguration(customConfigDoc.getDocumentElement());
                Configuration viewsConfig = config.getChild("views");
                Configuration[] viewConfigs = viewsConfig.getChildren("view");
                for (int i = 0; i < viewConfigs.length; i++) {
                    String id = viewConfigs[i].getAttribute("id");
                    ConfigurableViewDescriptor viewDescriptor = new ConfigurableViewDescriptor(id);
                    viewDescriptor.configure(viewConfigs[i]);
                    this.viewDescriptors.put(id, viewDescriptor);
                }
                return this.viewDescriptors.values().toArray(new ViewDescriptor[this.viewDescriptors.size()]);
            }

            log.warn("No custom view descriptors set inside resource configuration (Resource path: " + getPath() + "), hence use default ones: " + DEFAULT_VIEW_ID + ", " + SOURCE_VIEW_ID);
            // no custom config
            ConfigurableViewDescriptor[] vd = new ConfigurableViewDescriptor[2];
            vd[0] = new ConfigurableViewDescriptor(DEFAULT_VIEW_ID);
            String mimeType = getResourceConfigProperty("mime-type");
            vd[0].setMimeType(mimeType);
            this.viewDescriptors.put(DEFAULT_VIEW_ID, vd[0]);

            vd[1] = new ConfigurableViewDescriptor(SOURCE_VIEW_ID);
            mimeType = getResourceConfigProperty("source-view-mime-type");
            vd[1].setMimeType(mimeType);
            this.viewDescriptors.put(SOURCE_VIEW_ID, vd[1]);
            return vd;
        } catch (Exception e) {
            String errorMsg = "Error configuring resource: " + getPath() + ": " + e.toString();
            log.error(errorMsg, e);
            // TODO: throw exception
            return null;
        }
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#getView(java.lang.String)
     */
    public View getView(String viewId) throws Exception {
        return getXMLView(viewId, getContentXML(viewId));
    }

    /**
     * Get mime type for a specific view ID
     */
    public String getMimeType(String viewId) throws Exception {
        if (viewId == null) {
            log.debug("View ID is null (" + getPath() + "), hence use default view ID: " + DEFAULT_VIEW_ID);
            // TODO: Setting this to default would make sense, but check backwards compatibility first!
            //viewId = DEFAULT_VIEW_ID;
        }
        String mimeType = null;
        ViewDescriptor viewDescriptor = getViewDescriptor(viewId);
        if (viewDescriptor != null) {
            mimeType = viewDescriptor.getMimeType();
        } else {
            log.debug("No view descriptor for view ID '" + viewId + "' and path '" + getPath() + "'.");
        }
        if (mimeType == null) {
            mimeType = this.getResourceConfigProperty("mime-type");
        }
        if (mimeType == null) {
            mimeType = "application/xhtml+xml";
            log.warn("No mime type configured, hence will return '" + mimeType + "' as mime type!");
        }
        //log.debug("Mime type: " + mimeType + ", " + viewId);
        return mimeType;
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#exists()
     */
    public boolean exists() throws Exception {
        return getRealm().getRepository().existsNode(getPath());
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#getSize()
     */
    public long getSize() throws Exception {
        return -1;
    }

    /**
     * Apply view ID specific transformers to source XML
     * @param viewId View ID
     * @param xmlInputStream Source XML
     */
    public View getXMLView(String viewId, InputStream xmlInputStream) throws Exception {
        if (xmlInputStream == null) {
            log.error("No source XML!");
        }
        View view = new View();
        if (viewId == null) {
            if (isMobileView()) {
                if (getViewDescriptor(MOBILE_VIEW_ID) != null) {
                    //viewId = DEFAULT_VIEW_ID;
                    viewId = MOBILE_VIEW_ID;
                } else {
                    log.warn("Seems to be a mobile device, but no mobile view configured, hence use default view!");
                    viewId = DEFAULT_VIEW_ID;
                }
            } else {
                viewId = DEFAULT_VIEW_ID;
            }
        } else {
            log.info("View already set (probably via query string or session attribute): " + viewId);
        }

        String mimeType = getMimeType(viewId);
        view.setMimeType(mimeType);

        StringWriter errorWriter = new StringWriter();

        try {
            if (viewId != null && viewId.equals(SOURCE_VIEW_ID)) {
                view.setInputStream(xmlInputStream);
                return view;
            }

            // write result into view:
            ConfigurableViewDescriptor viewDescriptor = (ConfigurableViewDescriptor)getViewDescriptor(viewId);
            view.setInputStream(getTransformedInputStream(xmlInputStream, viewDescriptor, errorWriter));
            return view;
        } catch(Exception e) {
            log.error(e, e);
            log.error(e + " (" + getPath() + ", " + getRealm() + ")", e);
            String errorMsg;
            String transformationError = errorWriter.toString();
            if (transformationError != null) {
                errorMsg = "Transformation error:\n" + transformationError; 
                log.error(errorMsg);
            } else {
                errorMsg = e.getMessage();
            }
            throw new Exception(errorMsg);
        }
    }

    /**
     * @param xmlInputStream XML as input stream
     * @param viewDescriptor View descriptor
     */
    private InputStream getTransformedInputStream(InputStream xmlInputStream, ConfigurableViewDescriptor viewDescriptor, StringWriter errorWriter) throws Exception {
        //log.debug("View descriptor: " + viewDescriptor.getId());

        // create reader:
        XMLReader xmlReader = XMLReaderFactory.createXMLReader();
        CatalogResolver catalogResolver = new CatalogResolver(); // TODO: Make CatalogResolver configurable...
        xmlReader.setEntityResolver(catalogResolver);
        xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);

            SourceResolver uriResolver = new SourceResolver(this);
            ListingErrorHandler errorListener = new ListingErrorHandler(new PrintWriter(errorWriter));

            // create xslt transformer:
            SAXTransformerFactory tf = (SAXTransformerFactory)TransformerFactory.newInstance();
            tf.setURIResolver(uriResolver);
            tf.setErrorListener(errorListener);

            String[] xsltPaths = null;
            if (viewDescriptor != null) {
                xsltPaths = viewDescriptor.getXSLTPaths();
            } else {
                log.warn("View descriptor is null!");
            }
            if (xsltPaths == null || xsltPaths.length == 0) {
                xsltPaths = getXSLTPath(getPath());
            }
            
            Repository repo = getRealm().getRepository();<commit_msg>note about templates added<commit_after>/*
 * Copyright 2007-2014 Wyona
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.wyona.org/licenses/APACHE-LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.wyona.yanel.impl.resources;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamSource;

import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationUtil;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import org.apache.xml.resolver.tools.CatalogResolver;
import org.apache.xml.serializer.Serializer;
import org.apache.xml.utils.ListingErrorHandler;
import org.w3c.dom.Document;
import org.wyona.commons.io.MimeTypeUtil;
import org.wyona.security.core.api.Identity;
import org.wyona.yanel.core.Resource;
import org.wyona.yanel.core.api.attributes.ViewableV2;
import org.wyona.yanel.core.attributes.viewable.View;
import org.wyona.yanel.core.attributes.viewable.ViewDescriptor;
import org.wyona.yanel.core.serialization.SerializerFactory;
import org.wyona.yanel.core.source.SourceResolver;
import org.wyona.yanel.core.transformation.I18nTransformer3;
import org.wyona.yanel.core.transformation.XIncludeTransformer;
import org.wyona.yanel.core.util.PathUtil;
import org.wyona.yanel.impl.resources.xml.ConfigurableViewDescriptor;
import org.wyona.yarep.core.Repository;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * It is a base class for resources that generate XML. Subclasses must override getContentXML 
 * in order to pass XML for a view.
 * <p>
 * The class has its configuration for views ('default' and 'source' are built in). It resides in *.yanel-rc file, e.g.
 * <pre>
 * &lt;yanel:custom-config>
 * &lt;views>
 *  &lt;!-- No parameter needed for getting this view -->
 *  &lt;view id="default">
 *      &lt;mime-type>text/html&lt;/mime-type>
 *      &lt;xslt>/xslt/global.xsl&lt;/xslt>
 *      &lt;serializer key="HTML_TRANSITIONAL">
 *      &lt;/serializer>
 *  &lt;/view>
 *  
 *  &lt;view id="beautiful">
 *      &lt;mime-type>application/xhtml+xml&lt;/mime-type>
 *      &lt;xslt>/xslt/global.xsl&lt;/xslt>
 *      &lt;serializer key="XHTML_STRICT">
 *      &lt;/serializer>
 *  &lt;/view>
 *  
 *   &lt;view id="atom">
 *      &lt;mime-type>application/atom+xml&lt;/mime-type>
 *      &lt;serializer key="XML">
 *      &lt;/serializer>
 *   &lt;/view>
 *   
 *   &lt;view id="json">
 *      &lt;mime-type>application/json&lt;/mime-type>
 *      &lt;serializer key="TEXT">
 *      &lt;/serializer>
 *   &lt;/view>
 *   
 *   &lt;!-- Skips any provided XSLT-->
 *   &lt;view id="source">
 *      &lt;mime-type>application/xhtml+xml&lt;/mime-type>
 *      &lt;serializer key="XML">
 *      &lt;/serializer>
 *   &lt;/view>
 *&lt;/views>
 *&lt;/yanel:custom-config>
 * </pre>
 * <p>
 * A view is accessed through a request parameter <b>yanel.resource.viewid</b>
 * <p>
 * If no serializer is specified for a view, the serializer will be chosen depending
 * on the mime-type, whereas the default serializer is XHTML_STRICT.
 * </p>
 */
public class BasicXMLResource extends Resource implements ViewableV2 {

    private static final Logger log = LogManager.getLogger(BasicXMLResource.class);

    protected static String DEFAULT_VIEW_ID = "default";
    protected static String SOURCE_VIEW_ID = "source";
    protected static String MOBILE_VIEW_ID = "mobile";

    protected HashMap<String, ViewDescriptor> viewDescriptors;

    private static final String VIEW_ID_PARAM_NAME = "yanel.resource.viewid";

    /**
     * Get view descriptor for a particular view id
     * @param viewId View id
     */
    protected ViewDescriptor getViewDescriptor(String viewId) {
        if (viewId == null) {
            log.debug("View ID is null (" + getPath() + ")");
            // TODO: Setting the view ID to default would actually make sense, but check for backwards compatibility first!
            //viewId = DEFAULT_VIEW_ID;
            return null;
        }
        ViewDescriptor[] viewDescriptors = getViewDescriptors();
        if (viewDescriptors != null) {
            for (int i = 0; i < viewDescriptors.length; i++) {
                if (viewDescriptors[i].getId().equals(viewId)) {
                    return viewDescriptors[i];
                }
            }
            log.warn("No view descriptor found for view id '" + viewId + "' (Resource path: " + getPath() + ")");
            return null;
        } else {
            log.warn("No view descriptors set inside resource configuration (Resource path: " + getPath() + ")!");
            return null;
        }
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#getViewDescriptors()
     */
    public ViewDescriptor[] getViewDescriptors() {
        if (this.viewDescriptors != null) {
            return this.viewDescriptors.values().toArray(new ViewDescriptor[this.viewDescriptors.size()]);
        }
        try {
            this.viewDescriptors = new HashMap<String, ViewDescriptor>();
            // reads views from configuration:
            if (getConfiguration() != null && getConfiguration().getCustomConfiguration() != null) {
                Document customConfigDoc = getConfiguration().getCustomConfiguration();
                Configuration config = ConfigurationUtil.toConfiguration(customConfigDoc.getDocumentElement());
                Configuration viewsConfig = config.getChild("views");
                Configuration[] viewConfigs = viewsConfig.getChildren("view");
                for (int i = 0; i < viewConfigs.length; i++) {
                    String id = viewConfigs[i].getAttribute("id");
                    ConfigurableViewDescriptor viewDescriptor = new ConfigurableViewDescriptor(id);
                    viewDescriptor.configure(viewConfigs[i]);
                    this.viewDescriptors.put(id, viewDescriptor);
                }
                return this.viewDescriptors.values().toArray(new ViewDescriptor[this.viewDescriptors.size()]);
            }

            log.warn("No custom view descriptors set inside resource configuration (Resource path: " + getPath() + "), hence use default ones: " + DEFAULT_VIEW_ID + ", " + SOURCE_VIEW_ID);
            // no custom config
            ConfigurableViewDescriptor[] vd = new ConfigurableViewDescriptor[2];
            vd[0] = new ConfigurableViewDescriptor(DEFAULT_VIEW_ID);
            String mimeType = getResourceConfigProperty("mime-type");
            vd[0].setMimeType(mimeType);
            this.viewDescriptors.put(DEFAULT_VIEW_ID, vd[0]);

            vd[1] = new ConfigurableViewDescriptor(SOURCE_VIEW_ID);
            mimeType = getResourceConfigProperty("source-view-mime-type");
            vd[1].setMimeType(mimeType);
            this.viewDescriptors.put(SOURCE_VIEW_ID, vd[1]);
            return vd;
        } catch (Exception e) {
            String errorMsg = "Error configuring resource: " + getPath() + ": " + e.toString();
            log.error(errorMsg, e);
            // TODO: throw exception
            return null;
        }
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#getView(java.lang.String)
     */
    public View getView(String viewId) throws Exception {
        return getXMLView(viewId, getContentXML(viewId));
    }

    /**
     * Get mime type for a specific view ID
     */
    public String getMimeType(String viewId) throws Exception {
        if (viewId == null) {
            log.debug("View ID is null (" + getPath() + "), hence use default view ID: " + DEFAULT_VIEW_ID);
            // TODO: Setting this to default would make sense, but check backwards compatibility first!
            //viewId = DEFAULT_VIEW_ID;
        }
        String mimeType = null;
        ViewDescriptor viewDescriptor = getViewDescriptor(viewId);
        if (viewDescriptor != null) {
            mimeType = viewDescriptor.getMimeType();
        } else {
            log.debug("No view descriptor for view ID '" + viewId + "' and path '" + getPath() + "'.");
        }
        if (mimeType == null) {
            mimeType = this.getResourceConfigProperty("mime-type");
        }
        if (mimeType == null) {
            mimeType = "application/xhtml+xml";
            log.warn("No mime type configured, hence will return '" + mimeType + "' as mime type!");
        }
        //log.debug("Mime type: " + mimeType + ", " + viewId);
        return mimeType;
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#exists()
     */
    public boolean exists() throws Exception {
        return getRealm().getRepository().existsNode(getPath());
    }

    /**
     * @see org.wyona.yanel.core.api.attributes.ViewableV2#getSize()
     */
    public long getSize() throws Exception {
        return -1;
    }

    /**
     * Apply view ID specific transformers to source XML
     * @param viewId View ID
     * @param xmlInputStream Source XML
     */
    public View getXMLView(String viewId, InputStream xmlInputStream) throws Exception {
        if (xmlInputStream == null) {
            log.error("No source XML!");
        }
        View view = new View();
        if (viewId == null) {
            if (isMobileView()) {
                if (getViewDescriptor(MOBILE_VIEW_ID) != null) {
                    //viewId = DEFAULT_VIEW_ID;
                    viewId = MOBILE_VIEW_ID;
                } else {
                    log.warn("Seems to be a mobile device, but no mobile view configured, hence use default view!");
                    viewId = DEFAULT_VIEW_ID;
                }
            } else {
                viewId = DEFAULT_VIEW_ID;
            }
        } else {
            log.info("View already set (probably via query string or session attribute): " + viewId);
        }

        String mimeType = getMimeType(viewId);
        view.setMimeType(mimeType);

        StringWriter errorWriter = new StringWriter();

        try {
            if (viewId != null && viewId.equals(SOURCE_VIEW_ID)) {
                view.setInputStream(xmlInputStream);
                return view;
            }

            // write result into view:
            ConfigurableViewDescriptor viewDescriptor = (ConfigurableViewDescriptor)getViewDescriptor(viewId);
            view.setInputStream(getTransformedInputStream(xmlInputStream, viewDescriptor, errorWriter));
            return view;
        } catch(Exception e) {
            log.error(e, e);
            log.error(e + " (" + getPath() + ", " + getRealm() + ")", e);
            String errorMsg;
            String transformationError = errorWriter.toString();
            if (transformationError != null) {
                errorMsg = "Transformation error:\n" + transformationError; 
                log.error(errorMsg);
            } else {
                errorMsg = e.getMessage();
            }
            throw new Exception(errorMsg);
        }
    }

    /**
     * @param xmlInputStream XML as input stream
     * @param viewDescriptor View descriptor
     */
    private InputStream getTransformedInputStream(InputStream xmlInputStream, ConfigurableViewDescriptor viewDescriptor, StringWriter errorWriter) throws Exception {
        //log.debug("View descriptor: " + viewDescriptor.getId());

        // create reader:
        XMLReader xmlReader = XMLReaderFactory.createXMLReader();
        CatalogResolver catalogResolver = new CatalogResolver(); // TODO: Make CatalogResolver configurable...
        xmlReader.setEntityResolver(catalogResolver);
        xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);

            SourceResolver uriResolver = new SourceResolver(this);
            ListingErrorHandler errorListener = new ListingErrorHandler(new PrintWriter(errorWriter));

            // create xslt transformer:
            SAXTransformerFactory tf = (SAXTransformerFactory)TransformerFactory.newInstance();
            tf.setURIResolver(uriResolver);
            tf.setErrorListener(errorListener);

            String[] xsltPaths = null;
            if (viewDescriptor != null) {
                xsltPaths = viewDescriptor.getXSLTPaths();
            } else {
                log.warn("View descriptor is null!");
            }
            if (xsltPaths == null || xsltPaths.length == 0) {
                xsltPaths = getXSLTPath(getPath());
            }
            
            Repository repo = getRealm().getRepository();
            // TBD: Introduce javax.xml.transform.Templates in order to cache transformers (see for example http://www.javaworld.com/article/2073394/java-xml/transparently-cache-xsl-transformations-with-jaxp.html)
{'fraction_non_alphanumeric': 0.11341693485739651, 'fraction_numerical': 0.002848370510117264, 'mean_word_length': 2.5660203139427518, 'pattern_counts': {'":': 2, '<': 27, '<?xml version=': 0, '>': 103, 'https://': 0, 'lorem ipsum': 0, 'www.': 3, 'xml': 58}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5451838', 'n_tokens_mistral': 7638, 'n_tokens_neox': 7459, 'n_words': 1758}