public class ParameterValue {

    /**
     * the parameter key.
     * Since issue#15, it is a QName
     */
    private final QName key;
    /**
     * the parameter value.
     */
    private Object value;
    /**
     * This parameter datatype. If not specified, it's xs:string
     */
    private Datatype datatype;
    private boolean abstractParam;

    /**
     * Default constructor.
     *
     * @param key the parameter key
     * @param value the parameter value
     * @param datatype the parameter datatype
     */
    @SuppressWarnings("OverridableMethodCallInConstructor")
    public ParameterValue(QName key, Object value, Datatype datatype) {
        this.key = key;
        setValue(value);
        setDatatype(datatype);
    }

    /**
     * Constructs an abstract Parameter
     * @param key The param key
     * @param datatype The param datatype
     */
    @SuppressWarnings("OverridableMethodCallInConstructor")
    public ParameterValue(QName key, Datatype datatype) {
        this.key = key;
        setDatatype(datatype);
        abstractParam = true;
    }

    /**
     * @return the parameter key
     */
    public QName getKey() {
        return key;
    }

    /**
     * @return the parameter value
     */
    public Object getValue() {
        return value;
    }
    
    public void setValue(Object value) {
        if(value instanceof String) {
            this.value=value;
            abstractParam = false;
        } else if(value instanceof XdmValue) {
            this.value=value;
            abstractParam = false;
        } else if(value==null) {
            this.value=value;
            abstractParam = false;
        } else {
            throw new IllegalArgumentException("Only String or XdmValue are acceptable values for parameters");
        }
    }

    @Override
    public String toString() {
        return "[" + getKey() + "=" + (abstractParam ? "<abstract>" : getValue()) + "]";
    }

    public Datatype getDatatype() {
        return datatype;
    }

    public void setDatatype(Datatype datatype) {
        this.datatype = datatype;
    }
    
    public boolean isAbstract() {
        return abstractParam;
    }

}
 public class CfgFile implements Verifiable {
    static final QName QNAME = new QName(Config.NS, "file");
    static final QName QN_FOLDER = new QName(Config.NS, "folder");
    static final QName ATTR_HREF = new QName("href");
    private final File source;
    private final HashMap<QName, ParameterValue> params;
    
    public CfgFile(File source) {
        super();
        this.source=source;
        params = new HashMap<>();
    }
    public void addParameter(ParameterValue param) {
        if(param==null) return;
        params.put(param.getKey(), param);
    }
    public File getSource() { return source; }
    public HashMap<QName,ParameterValue> getParams() {
        return params;
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        if(!getSource().exists() || !getSource().isFile()) throw new InvalidSyntaxException(getSource().getAbsolutePath()+" does no exists or is not a regular file.");
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(source.getAbsolutePath());
        if(params.size()>0) {
            sb.append("[");
            for(ParameterValue pv:params.values()) {
                sb.append("(").append(pv.getKey()).append(",").append(pv.getValue()).append("),");
            }
            sb.deleteCharAt(sb.length()-1);
            sb.append("]");
        }
        return sb.toString();
    }
    
    
} public class ChooseStep implements ParametrableStep {
    private final List<WhenEntry> conditions;
    public static final QName QNAME = new QName(Config.NS, "choose");
    
    public ChooseStep() {
        super();
        conditions = new ArrayList<>();
    }

    /**
     * Always return null, a step may not have parameters.
     * @return {@code null}
     */
    @Override
    public Collection<ParameterValue> getParams() {
        return null;
    }
    
    public List<WhenEntry> getConditions() {
        return conditions;
    }
    public void addWhen(WhenEntry when) {
        conditions.add(when);
    }

    /**
     * Does nothing, a ChooseStep may not have parameters
     * @param param Ignored
     */
    @Override
    public void addParameter(ParameterValue param) { }

    @Override
    public String toString(String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("choose\n");
        String _p=prefix+"  ";
        for(WhenEntry when:conditions) {
            sb.append(when.toString(_p));
        }
        return sb.toString();
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        if(conditions.isEmpty()) {
            throw new InvalidSyntaxException("a choose can not be empty.");
        }
        for(WhenEntry when: conditions)
            when.verify();
    }
} public class Config implements Verifiable {
    public static final int MAX_DOCUMENT_CACHE_SIZE = 1;
    static final QName ATTR_DOCUMENT_CACHE_SIZE = new QName("documentCacheSize");
    public static final String NS = "http://efl.fr/chaine/saxon-pipe/config";
    static final QName PARAMS_CHILD = new QName(NS, "params");
    private Pipe pipe;
    private final HashMap<QName,ParameterValue> params;
    private Sources sources;
    private int maxDocumentCacheSize = MAX_DOCUMENT_CACHE_SIZE;
    private static final Logger LOGGER = LoggerFactory.getLogger(Config.class);
    private boolean logFileSize;
    private boolean skipSchemaValidation;
    public Namespaces namespaces;
    private File currentDir;
    // issue #40
    private List<String> schemaLocations;
    /**
     * This has nothing to do in Configuration, but for implementation reason,
     * it's here...
     */
    public transient String __instanceName;
    

    public Config(XdmNode node) {
        super();
        params = new HashMap<>();
        String val = node.getAttributeValue(ATTR_DOCUMENT_CACHE_SIZE);
        if(val!=null) {
            try {
                maxDocumentCacheSize = Integer.parseInt(val);
            } catch(Exception ex) {
                LOGGER.warn(val+" n'est pas une valeur acceptable pour la taille maximale du cache de documents. "+MAX_DOCUMENT_CACHE_SIZE+" sera utilisé");
            }
        }
    }
    /**
     * Pour compatibilité avec l'ancienne version
     */
    public Config() {
        super();
        params = new HashMap<>();
        currentDir = new File(System.getProperty("user.dir"));
    }
    public Config(String currentDir) {
        super();
        params = new HashMap<>();
        this.currentDir = new File(currentDir);
    }
    public Pipe getPipe() {
        return pipe;
    }

    void setPipe(Pipe pipe) {
        this.pipe = pipe;
    }

    public HashMap<QName,ParameterValue> getParams() {
        return params;
    }

    public void addParameter(ParameterValue p) {
        if(p!=null) {
            params.put(p.getKey(),p);
        }
    }

    public Sources getSources() {
        return sources;
    }

    void setSources(Sources sources) {
        this.sources = sources;
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        if(pipe==null) throw new InvalidSyntaxException("No pipe defined");
        pipe.verify();
        if(sources==null) throw new InvalidSyntaxException("No input file defined");
        sources.verify();
        if(namespaces!=null) {
            namespaces.verify();
        }
        for(ParameterValue pv: params.values()) {
            LOGGER.debug(pv.toString());
            if(pv.isAbstract()) {
                throw new InvalidSyntaxException("Parameter "+pv.getKey().getEQName()+" is declared abstract and no value is provided for it.");
            }
        }
    }
    /**
     * Returns <tt>true</tt> if input files with size over multi-thread limit exist.
     * @return <tt>true</tt> if at least one file exceeds the limit size
     */
    public boolean hasFilesOverMultiThreadLimit() {
        return getSources().hasFileOverLimit(getPipe().getMultithreadMaxSourceSize());
    }

    public int getMaxDocumentCacheSize() {
        return maxDocumentCacheSize;
    }

    public void setMaxDocumentCacheSize(int maxDocumentCacheSize) {
        this.maxDocumentCacheSize = maxDocumentCacheSize;
    }

    public boolean isLogFileSize() {
        return logFileSize;
    }

    public void setLogFileSize(boolean logFileSize) {
        this.logFileSize = logFileSize;
    }
    
    public void skipSchemaValidation(final boolean skipSchemaValidation) {
        this.skipSchemaValidation=skipSchemaValidation;
    }

    public Namespaces getNamespaces() {
        return namespaces;
    }

    public void setNamespaces(Namespaces namespaces) {
        this.namespaces = namespaces;
    }

    // issue #40
    public List<String> getSchemaLocations() {
        return schemaLocations;
    }

    public void setSchemaLocations(List<String> schemaLocations) {
        this.schemaLocations = schemaLocations;
    }
    
} public class ConfigUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigUtil.class);
    
    private static final QName CONFIG_EL = new QName(Config.NS, "config");
    private static final QName PARAM_NAME = new QName("name");
    private static final QName PARAM_VALUE = new QName("value");
    private static final QName PARAM_ABSTRACT = new QName("abstract");
    private static final QName PARAM_AS = new QName("as");
    private static final QName QN_PATTERN = new QName("pattern");
    private static final QName QN_RECURSE = new QName("recurse");
    private static final QName QN_PARAM = new QName(Config.NS,"param");
    private static final QName QN_NULL = new QName(Config.NS, "null");
    private static final QName AS_NAME = new QName("as");
    private static final QName QN_GRAMMARS = new QName(Config.NS, "grammars");
    private final File currentDir;
    private final Configuration saxonConfig;
    private final URIResolver uriResolver;
    private final String configUri;
    private final boolean skipSchemaValidation;
    private boolean __isConfigUriTrueURI = false;
    private DatatypeFactory factory;
    
    public ConfigUtil(Configuration saxonConfig, URIResolver uriResolver, String configUri) throws InvalidSyntaxException {
        this(saxonConfig, uriResolver, configUri, false, System.getProperty("user.dir"));
    }
    public ConfigUtil(Configuration saxonConfig, URIResolver uriResolver, String configUri, final boolean skipSchemaValidation, final String currentDir) throws InvalidSyntaxException {
        super();
        this.saxonConfig = saxonConfig;
        this.uriResolver=uriResolver;
        this.skipSchemaValidation=skipSchemaValidation;
        Pattern pattern = Pattern.compile("[a-z].+:.+");
        if(pattern.matcher(configUri).matches()) {
            try {            	
                URL url = new URL(configUri); 
                try (InputStream is = url.openStream()) {
                    if(is==null) {
                        throw new InvalidSyntaxException(configUri+" not found or can not be open");
                    }
                    __isConfigUriTrueURI = true;
                }
            } catch (IOException ex) {
                throw new InvalidSyntaxException(configUri+" not found or can not be open",ex);
            }
        } else {
            File file = new File(configUri);
            if(!file.exists() && !file.isFile()) {
                throw new InvalidSyntaxException(configUri+" not found or not a regular file");
            }
        }
        this.configUri=configUri;
        this.currentDir = new File(currentDir);
        try {
            factory = DatatypeFactory.getInstance(saxonConfig);
        } catch(ValidationException ex) {
            throw new InvalidSyntaxException(ex);
        }
    }
    
    public Config buildConfig(HashMap<QName,ParameterValue> inputParameters) throws SaxonApiException, InvalidSyntaxException {
        try {
            Processor processor = new Processor(saxonConfig);
            if(!skipSchemaValidation) {
                try {
                    System.setProperty("javax.xml.validation.SchemaFactory:http://www.w3.org/2001/XMLSchema/v1.1","org.apache.xerces.jaxp.validation.XMLSchema11Factory");
                    SchemaFactory schemaFactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema/v1.1");
                    Source schemaSource = saxonConfig.getURIResolver().resolve("cp:/fr/efl/chaine/xslt/schemas/gaulois-pipe_config.xsd", null);
                    Schema schema = schemaFactory.newSchema(schemaSource);
                    SchemaValidationErrorListener errListener = new SchemaValidationErrorListener();
                    Validator validator = schema.newValidator();
                    validator.setErrorHandler(errListener);
                    SAXSource saxSource = new SAXSource(
                            __isConfigUriTrueURI ? new InputSource(new URL(configUri).openStream()) :
                                    new InputSource(new FileInputStream(new File(configUri)))
                    );
                    validator.validate(saxSource);
                    if(errListener.hasErrors()) {
                        throw new InvalidSyntaxException(configUri+" does not respect gaulois-pipe schema");
                    }
                } catch(SAXException | TransformerException | IOException ex) {
                    LOGGER.error("while verifying schema conformity",ex);
                } catch(Error er) {
                    LOGGER.error("java.protocol.handler.pkgs="+System.getProperty("java.protocol.handler.pkgs"));
                    LOGGER.error("while parsing config",er);
                    throw er;
                }
            }
            if(LOGGER.isDebugEnabled() && __isConfigUriTrueURI) {
                // check if resolver can resolve URI
                LOGGER.debug("trying to resolve "+configUri);
                Source source = uriResolver.resolve(configUri, null);
                if(source==null) {
                    LOGGER.error(configUri+" can not be resolved. Please try again, setting org.xmlresolver logger to DEBUG");
                }
            }
            XdmNode configRoot = 
                    __isConfigUriTrueURI ? 
                    processor.newDocumentBuilder().build(uriResolver.resolve(configUri,null)) :
                    processor.newDocumentBuilder().build(new File(configUri));
            XPathCompiler xc = processor.newXPathCompiler();
            xc.declareNamespace("cfg", Config.NS);
            XPathSelector xs = xc.compile("/*").load();
            xs.setContextItem(configRoot);
            XdmNode root = (XdmNode)xs.evaluateSingle();
            if(CONFIG_EL.equals(root.getNodeName())) {
                Config config = new Config(root);
                // namespaces
                XdmSequenceIterator it = root.axisIterator(Axis.CHILD, Namespaces.QNAME);
                Namespaces namespaces = new Namespaces();
                while(it.hasNext()) {
                    XdmNode ns = (XdmNode)it.next();
                    XdmSequenceIterator itm = ns.axisIterator(Axis.CHILD, Namespaces.QN_MAPPING);
                    while(itm.hasNext()) {
                        XdmNode node = (XdmNode)itm.next();
                        String prefix = node.getAttributeValue(Namespaces.ATTR_PREFIX);
                        String uri = node.getAttributeValue(Namespaces.ATTR_URI);
                        namespaces.getMappings().put(prefix, uri);
                    }
                }
                config.setNamespaces(namespaces);
                it.close();
                // grammars issue #40
                XPathSelector gs = xc.compile("cfg:grammars/cfg:schema/@href").load();
                gs.setContextItem(root);
                it=gs.evaluate().iterator();
                List<String> grammars = new ArrayList<>();
                while(it.hasNext()) {
                    String href = it.next().getStringValue();
                    grammars.add(href);
                }
                it.close();
                config.setSchemaLocations(grammars);
                // params
//                config.getParams().putAll(inputParameters);
                HashMap<QName, ParameterValue> configParameters = new HashMap<>();
                it = root.axisIterator(Axis.CHILD, Config.PARAMS_CHILD);
                while(it.hasNext()) {
                    XdmNode params = (XdmNode)it.next();
                    XdmSequenceIterator itp = params.axisIterator(Axis.CHILD, new QName(Config.NS, "param"));
                    while(itp.hasNext()) {
                        //config.addParameter(buildParameter((XdmNode)itp.next(),inputParameters));
                        ParameterValue pv = buildParameter((XdmNode)itp.next(),inputParameters);
                        configParameters.put(pv.getKey(), pv);
                    }
                }
                config.getParams().putAll(ParametersMerger.merge(inputParameters, configParameters));
                // pipe
                config.setPipe(buildPipe((XdmNode)(root.axisIterator(Axis.CHILD, Pipe.QNAME).next()),config.getParams()));
                // sources
                XdmSequenceIterator sourceIterator = root.axisIterator(Axis.CHILD, Sources.QNAME);
                if(sourceIterator.hasNext()) {
                    config.setSources(buildSources((XdmNode)(sourceIterator.next()), config.getParams()));
                }
                // output
                // pour compatibilité ascendante, si on a un output sous la config, on essaie de le mettre sur le pipe
                // possible uniquement si le pipe est rectiligne
                XdmSequenceIterator outputIt = root.axisIterator(Axis.CHILD, Output.QNAME);
                if(outputIt.hasNext()) {
                    LOGGER.warn("Defining <output/> in config is now deprecated - but still works. You should define it in <pipe/>");
                    if(config.getPipe().getOutput()==null) {
                        config.getPipe().setOutput(buildOutput((XdmNode)(outputIt.next()), config.getParams()));
                    } else {
                        throw new InvalidSyntaxException("Using output outside of pipe is deprecated but supported, only if pipe has no output defined");
                    }
                }
                return config;
            } else {
                throw new InvalidSyntaxException("The file "+configUri+" does not respect schema saxon-pipe_config.xsd");
            }
        } catch(TransformerException ex) {
            throw new InvalidSyntaxException(configUri+" can not be read.");
        }
    }
    
    private Pipe buildPipe(XdmNode pipeNode, HashMap<QName,ParameterValue> parameters) throws IllegalStateException, InvalidSyntaxException {
        return buildPipe(pipeNode, parameters, null);
    }
    private Pipe buildPipe(XdmNode pipeNode, HashMap<QName,ParameterValue> parameters, Tee parentTee) throws IllegalStateException, InvalidSyntaxException {
        LOGGER.trace("buildPipe on "+pipeNode.getNodeName());
        Pipe pipe = new Pipe(parentTee);
        try {
            int nbThreads = Integer.parseInt(resolveEscapes(pipeNode.getAttributeValue(new QName(Pipe.ATTR_NB_THREADS)),parameters));
            pipe.setNbThreads(nbThreads);
        } catch(Throwable t) {}
        try {
            int max = Integer.parseInt(resolveEscapes(pipeNode.getAttributeValue(new QName(Pipe.ATTR_MAX)),parameters));
            pipe.setMultithreadMaxSourceSize(max);
        } catch(Throwable t) {}
        pipe.setTraceOutput(resolveEscapes(pipeNode.getAttributeValue(new QName(Pipe.ATTR_TRACE)),parameters));
        XdmSequenceIterator it = pipeNode.axisIterator(Axis.CHILD);
        while(it.hasNext()) {
            XdmItem item = it.next();
            if(item instanceof XdmNode) {
                // c'est un élément
                XdmNode node = (XdmNode)item;
                if(Xslt.QNAME.equals(node.getNodeName())) {
                    pipe.addXslt(buildXslt(node,parameters));
                } else if(JavaStep.QNAME.equals(node.getNodeName())) {
                    pipe.addXslt(buildJavaStep(node, parameters));
                } else if(ChooseStep.QNAME.equals(node.getNodeName())) {
                    pipe.addXslt(buildChooseStep(node, parameters));
                } else if(Tee.QNAME.equals(node.getNodeName())) {
                    pipe.setTee(buildTee(node, parameters));
                } else if(Output.QNAME.equals(node.getNodeName())) {
                    pipe.setOutput(buildOutput(node, parameters));
                } else if(node.getNodeKind()==XdmNodeKind.TEXT) {
                    // on ignore
                } else if(node.getNodeKind()==XdmNodeKind.COMMENT) {
                    // on ignore
                } else {
                    throw new InvalidSyntaxException(node.getNodeKind().toString()+" - "+ node.getNodeName()+": unexpected element in "+Pipe.QNAME);
                }
            }
        }
        return pipe;
    }
    
    private Tee buildTee(XdmNode teeNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildTee on "+teeNode.getNodeName());
        Tee tee = new Tee();
        XdmSequenceIterator seq = teeNode.axisIterator(Axis.CHILD,Pipe.QNAME);
        while(seq.hasNext()) {
            XdmNode node = (XdmNode)seq.next();
            Pipe pipe = buildPipe(node, parameters, tee);
            tee.addPipe(pipe);
        }
        return tee;
    }
    private Sources buildSources(XdmNode sourcesNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        String orderBy = sourcesNode.getAttributeValue(Sources.ATTR_ORDERBY);
        String sort = sourcesNode.getAttributeValue(Sources.ATTR_SORT);
        LOGGER.trace("buildSources from {} with orderBy={} and sort={}", new Object[]{sourcesNode.getNodeName(), orderBy, sort});
        Sources sources = new Sources(orderBy, sort);
        XdmSequenceIterator it = sourcesNode.axisIterator(Axis.CHILD);
        while(it.hasNext()) {
            XdmNode node = (XdmNode)it.next();
            LOGGER.trace("buildSource from {}", node.getNodeName());
            if(CfgFile.QNAME.equals(node.getNodeName())) {
                try {
                    CfgFile localFile = buildFile(node, parameters);
                    LOGGER.trace("add source {}", localFile);
                    sources.addFile(localFile);
                } catch (URISyntaxException ex) {
                    LOGGER.error(ex.getMessage(),ex);
                    throw new InvalidSyntaxException(ex);
                }
            } else if(CfgFile.QN_FOLDER.equals(node.getNodeName())) {
                Collection<CfgFile> files = buildFolderContent(node, parameters);
                LOGGER.trace("buildSources from folder contains {} files", files.size());
                sources.addFiles(files);
            } else if(Listener.QName.equals(node.getNodeName())) {
                sources.setListener(buildListener(node, parameters));
            }
        }
        return sources;
    }
    private Listener buildListener(XdmNode listenerNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        String tmp = resolveEscapes(listenerNode.getAttributeValue(Listener.ATTR_PORT),parameters);
        int port = Listener.DEFAULT_PORT;
        try {
            port = Integer.parseInt(tmp);
        } catch(NumberFormatException ex) {}
        String stopKeyword = resolveEscapes(listenerNode.getAttributeValue(Listener.ATTR_STOP),parameters);
        Listener list = new Listener(port, stopKeyword);
        XdmSequenceIterator it = listenerNode.axisIterator(Axis.CHILD, JavaStep.QNAME);
        if(it.hasNext()) {
            list.setJavastep(buildJavaStep((XdmNode)it.next(), parameters));
        }
        return list;
    }
    private Xslt buildXslt(XdmNode xsltNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildXslt on {}", xsltNode.getNodeName());
        Xslt ret = new Xslt(resolveEscapes(xsltNode.getAttributeValue(Xslt.ATTR_HREF),parameters));
        if("true".equals(xsltNode.getAttributeValue(Xslt.ATTR_TRACE_ACTIVE))) {
            ret.setTraceToAdd(true);
        }
        if("true".equals(xsltNode.getAttributeValue(Xslt.ATTR_DEBUG))) {
            ret.setDebug(true);
            ret.setId(xsltNode.getAttributeValue(Xslt.ATTR_ID));
        }
        XdmSequenceIterator it = xsltNode.axisIterator(Axis.CHILD, QN_PARAM);
        while(it.hasNext()) {
            ret.addParameter(buildParameter((XdmNode)it.next(),parameters));
        }
        return ret;
    }
    private JavaStep buildJavaStep(XdmNode javaNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildJavaStep on {}", javaNode.getNodeName());
        JavaStep ret = new JavaStep(resolveEscapes(javaNode.getAttributeValue(JavaStep.ATTR_CLASS), parameters));
        XdmSequenceIterator it = javaNode.axisIterator(Axis.CHILD, QN_PARAM);
        while(it.hasNext()) {
            ret.addParameter(buildParameter((XdmNode)it.next(),parameters));
        }
        return ret;
    }
    private ChooseStep buildChooseStep(XdmNode chooseNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildChooseStep on {}", chooseNode.getNodeName());
        ChooseStep chooseStep = new ChooseStep();
        XdmSequenceIterator it = chooseNode.axisIterator(Axis.CHILD, WhenEntry.QNAME);
        while(it.hasNext()) {
            XdmNode whenNode = (XdmNode)it.next();
            chooseStep.addWhen(buildWhen(whenNode, parameters));
        }
        it = chooseNode.axisIterator(Axis.CHILD, WhenEntry.QN_OTHERWISE);
        while(it.hasNext()) {
            XdmNode whenNode = (XdmNode)it.next();
            chooseStep.addWhen(buildWhen(whenNode, parameters));
        }
        return chooseStep;
    }
    private WhenEntry buildWhen(XdmNode whenNode, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        String test = whenNode.getAttributeValue(WhenEntry.ATTR_TEST);
        // particular case of the otherwise, which is implemented as a when[@test='true()']
        if((test==null || test.length()==0) && WhenEntry.QN_OTHERWISE.equals(whenNode.getNodeName()) ) {
            test="true()";
        }
        WhenEntry when = new WhenEntry(test);
        XdmSequenceIterator it = whenNode.axisIterator(Axis.CHILD);
        while(it.hasNext()) {
            XdmItem item = it.next();
            if(item instanceof XdmNode) {
                XdmNode node = (XdmNode)item;
                if(Xslt.QNAME.equals(node.getNodeName())) {
                    when.addStep(buildXslt(node,parameters));
                } else if(JavaStep.QNAME.equals(node.getNodeName())) {
                    when.addStep(buildJavaStep(node, parameters));
                } else if(ChooseStep.QNAME.equals(node.getNodeName())) {
                    when.addStep(buildChooseStep(node, parameters));
                } else if(node.getNodeKind()==XdmNodeKind.TEXT) {
                    // on ignore
                } else if(node.getNodeKind()==XdmNodeKind.COMMENT) {
                    // on ignore
                } else {
                    throw new InvalidSyntaxException(node.getNodeKind().toString()+" - "+ node.getNodeName()+": unexpected element in "+Pipe.QNAME);
                }
            }
        }
        return when;
    }
    private ParameterValue buildParameter(XdmNode param, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildParameter on "+param.getNodeName());
        // attributes already presents will no be added, so return the existing parameter
        // issue#15 : parameter names are not anymore resolved, only QNamed
        try {
            String sDatatype = param.getAttributeValue(PARAM_AS);
            Datatype dt ;
            if(sDatatype!=null) {
                QName datatypeName = null;
                int doubleDotPosition = sDatatype.indexOf(":");
                if(doubleDotPosition>0) {
                    String prefix = sDatatype.substring(0, doubleDotPosition);
                    String uri = new InscopeNamespaceResolver(param.getUnderlyingNode()).getURIForPrefix(prefix, true);
                    if(uri==null) throw new InvalidSyntaxException("prefix "+prefix+" is not bound to any namespace URI ("+param.getLineNumber()+","+param.getColumnNumber()+")");
                    datatypeName=new QName(prefix, uri, sDatatype.substring(doubleDotPosition+1));
                } else {
                    datatypeName = new QName(sDatatype);
                }
                dt = factory.getDatatype(datatypeName);
            } else {
                dt = factory.XS_STRING;
            }
            ParameterValue pv;
            if("true".equals(param.getAttributeValue(PARAM_ABSTRACT))) {
                pv = new ParameterValue(resolveQName(param.getAttributeValue(PARAM_NAME)), dt);
                ParameterValue fcli = parameters.get(pv.getKey());
                if(fcli!=null) {
                    XdmValue value = pv.getDatatype().convert(
                            resolveEscapes(fcli.getValue().toString(),parameters), 
                            saxonConfig);
                    pv.setValue(value);
                }
                return pv;
            } else {
                pv = new ParameterValue(
                        resolveQName(param.getAttributeValue(PARAM_NAME)), 
                        dt.convert(resolveEscapes(param.getAttributeValue(PARAM_VALUE),parameters),saxonConfig),
                        dt);
                if(parameters.containsKey(pv.getKey())) return parameters.get(pv.getKey());
                else return pv;
            }
        } catch(ValidationException ex) {
            throw new InvalidSyntaxException(ex);
        }
    }
    public static QName resolveQName(String name) {
        if(name==null) return null;
        if(name.length()==0) return null;
        else if(name.startsWith("Q{")) return QName.fromEQName(name);
        else if(name.startsWith("{")) return QName.fromClarkName(name);
        else return new QName(name);
    }
    private CfgFile buildFile(XdmNode node, HashMap<QName,ParameterValue> parameters) throws URISyntaxException, InvalidSyntaxException {
        String href = node.getAttributeValue(CfgFile.ATTR_HREF);
        LOGGER.trace("buildFile from {} with href={}", node.getNodeName(), href);
        File f;
        href = resolveEscapes(href, parameters);
        if(href.startsWith("file:")) {
            f = new File(new URI(href));
        } else {
            f = new File(href);
            if(!f.exists()) {
                f = new File(currentDir, href);
            }
        }
        LOGGER.trace("buildFile on {}", f.getName());
        CfgFile ret = new CfgFile(f);
        XdmSequenceIterator it = node.axisIterator(Axis.CHILD, QN_PARAM);
        while(it.hasNext()) {
            ret.addParameter(buildParameter((XdmNode)it.next(),parameters));
        }
        ParameterValue relativeFile = new ParameterValue(ParametersMerger.INPUT_RELATIVE_FILE, f.toURI().toString(), factory.XS_STRING);
        ret.addParameter(relativeFile);
        ParameterValue relativeDir = new ParameterValue(ParametersMerger.INPUT_RELATIVE_DIR, f.getParentFile().toURI().toString(), factory.XS_STRING);
        ret.addParameter(relativeDir);
        return ret;
    }
    Object resolveEscapes(Object input, HashMap<QName,ParameterValue> params) {
        LOGGER.debug("resolveEscapes in "+input+" with "+params);
        if(input==null) return input;
        if(input instanceof String) {
            String ret = input.toString();
            return ParametersMerger.processParametersReplacement(ret, params);
        } else {
            return input;
        }
    }
    String resolveEscapes(String input, HashMap<QName,ParameterValue> params) {
        if(input==null) return input;
        String ret = input;
        ret = (String)ParametersMerger.processParametersReplacement(ret, params);
        if(LOGGER.isDebugEnabled()) {
            LOGGER.debug("resolveEscapes in {} -> {}", input, ret);
        }
        return ret;
    }
    private Collection<CfgFile> buildFolderContent(XdmNode node, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildFolderContent on "+node.getNodeName());
        String pattern = (String)resolveEscapes(node.getAttributeValue(QN_PATTERN), parameters);
        final boolean recurse = getBooleanValue(node.getAttributeValue(QN_RECURSE));
        HashMap<QName, ParameterValue> params = new HashMap<>();
        XdmSequenceIterator it = node.axisIterator(Axis.CHILD, QN_PARAM);
        while(it.hasNext()) {
            ParameterValue p = buildParameter((XdmNode)it.next(), parameters);
            params.put(p.getKey(),p);
        }
        if(pattern==null) pattern="$.*^";
        final Pattern regex = Pattern.compile(pattern);
        LOGGER.trace("checking filenames to "+regex.toString());
        FilenameFilter filter;
        filter = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String filename) {
                boolean match = regex.matcher(filename).matches();
                if(!match) {
                    LOGGER.trace(filename+" not matched");
                }
                return match;
            }
        };
        String fileName = (String)resolveEscapes(node.getAttributeValue(CfgFile.ATTR_HREF),parameters);
        File dir;
        try {
            dir = new File(new URI(fileName));
        } catch (URISyntaxException | IllegalArgumentException ex) {
            dir = new File(currentDir, fileName);
        }
        
        if(!dir.isDirectory()) {
            LOGGER.warn(dir.getAbsolutePath()+" is not a valid directory");
            return Collections.EMPTY_LIST;
        }
        LOGGER.trace("dir="+dir+", filter="+filter+", recurse="+recurse);
        List<CfgFile> files = getFilesFromDirectory(dir,filter,recurse);
        Path dirPath= dir.toPath();
        for(CfgFile sourceFile:files) {
            for(ParameterValue p:params.values()) {
                sourceFile.addParameter(p);
            }
            ParameterValue relativeFile = new ParameterValue(ParametersMerger.INPUT_RELATIVE_FILE, makeAsUri(dirPath.relativize(sourceFile.getSource().toPath())), factory.XS_STRING);
            sourceFile.addParameter(relativeFile);
            ParameterValue relativeDir = new ParameterValue(ParametersMerger.INPUT_RELATIVE_DIR, makeAsUri(dirPath.relativize(sourceFile.getSource().getParentFile().toPath())), factory.XS_STRING);
            sourceFile.addParameter(relativeDir);

        }
        return files;
    }
    private String makeAsUri(final Path p) {
        return p.toString().replaceAll("\\\\", "/");
    }
    private Output buildOutput(XdmNode node, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException {
        LOGGER.trace("buildOutput from {}", node.getNodeName());
        Output ret = new Output();
        // searching for output parameters
        XdmSequenceIterator attributes = node.axisIterator(Axis.ATTRIBUTE);
        while(attributes.hasNext()) {
            XdmNode attr = (XdmNode)attributes.next();
            ret.setOutputProperty(attr.getNodeName().getLocalName(), (String)resolveEscapes(attr.getStringValue(),parameters));
        }
        boolean nullOutput = node.axisIterator(Axis.CHILD, QN_NULL).hasNext();
        if(nullOutput) {
            ret.setNull(true);
        } else {
            XdmSequenceIterator consoleIterator = node.axisIterator(Axis.CHILD, Output.QN_CONSOLE);
            if(consoleIterator.hasNext()) {
                XdmNode console = (XdmNode)consoleIterator.next();
                String sConsole = console.getAttributeValue(Output.ATTR_CONSOLE_WHICH);
                if(sConsole==null) sConsole = "out";
                ret.setConsole(sConsole);
            } else {
                XdmNode folder = (XdmNode)node.axisIterator(Axis.CHILD, CfgFile.QN_FOLDER).next();
                String relative = resolveEscapes(folder.getAttributeValue(new QName("relative")),parameters);
                String temp = folder.getAttributeValue(new QName("to"));
                String to = resolveEscapes(temp, parameters);
                String absolute = resolveEscapes(folder.getAttributeValue(new QName("absolute")), parameters);
                if(relative!=null) ret.setRelativePath(relative);
                if(to!=null) ret.setRelativeTo(to);
                if(absolute!=null) ret.setAbsolute(absolute);
                XdmNode filename = (XdmNode)node.axisIterator(Axis.CHILD, new QName(Config.NS,"fileName")).next();
                String prefix = resolveEscapes(filename.getAttributeValue(new QName("prefix")),parameters);
                String name = resolveEscapes(filename.getAttributeValue(new QName("name")),parameters);
                String suffix = resolveEscapes(filename.getAttributeValue(new QName("suffix")), parameters);
                if(prefix!=null) ret.setPrefix(prefix);
                if(name!=null) ret.setName(name);
                if(suffix!=null) ret.setSuffix(suffix);
            }
        }
        return ret;
    }
    /**
     * Visibility package private only for unit-tests
     * @param directory
     * @param filter
     * @param recurse
     * @return Found files
     */
    List<CfgFile> getFilesFromDirectory(File directory, FilenameFilter filter, boolean recurse) {
        File [] files = directory.listFiles();
        List<CfgFile> ret = new ArrayList<>();
        for(File f:files) {
            if(f.isDirectory() && recurse) {
                ret.addAll(getFilesFromDirectory(f, filter, recurse));
            } else if(filter.accept(f.getParentFile(), f.getName())) {
                ret.add(new CfgFile(f));
            }
        }
        return ret;
    }
    private boolean getBooleanValue(String v) {
        return "true".equals(v) || "1".equals(v) || "yes".equals(v);
    }
    
    /**
     * Adds a parameter to the specified config.
     * @param config The config to modify
     * @param parameterPattern The prameter definition
     * @param factory The datatype factory to use
     * @throws InvalidSyntaxException If the parameter definition is incorrect
     */
    public static void addConfigParameter(Config config, String parameterPattern, DatatypeFactory factory) throws InvalidSyntaxException {
        config.addParameter(parseParameterPattern(parameterPattern, factory));
    }
    public static ParameterValue parseParameterPattern(String parameterPattern, DatatypeFactory factory) throws InvalidSyntaxException {
        String[] dec = parameterPattern.split("=");
        if(dec.length!=2) {
            throw new InvalidSyntaxException(parameterPattern+" is not a valid parameter declaration");
        }
        return new ParameterValue(resolveQName(dec[0]), dec[1], factory.XS_STRING);
    }
    /**
     * Defines the thread number to use in this config
     * @param config The config to modify
     * @param argument The number of threads
     * @throws InvalidSyntaxException It should never throws anything... but if argument is not an integer...
     */
    public static void setNbThreads(Config config, String argument) throws InvalidSyntaxException {
        try {
            if(config.getPipe()==null) {
                Pipe pipe = new Pipe();
                pipe.setNbThreads(Integer.parseInt(argument));
                config.setPipe(pipe);
            }
        } catch(Exception ex) {
            throw new InvalidSyntaxException(ex);
        }
    }
    /**
     * Adds an input file to the config
     * @param config The config to modify
     * @param argument The file to add
     * @param factory The datatypeFactory to use
     * @throws InvalidSyntaxException If input file definition is incorrect
     */
    public static void addInputFile(Config config, String argument, DatatypeFactory factory) throws InvalidSyntaxException {
        Sources sources = config.getSources();
        if(sources==null) {
            sources = new Sources();
            config.setSources(sources);
        }
        sources.addFile(resolveInputFile(argument, factory));
    }
    /**
     * Adds a template (XSLT) to the specified config
     * @param config the config to modify
     * @param argument The Xsl to add
     * @param factory The datatype factory to use
     * @throws fr.efl.chaine.xslt.InvalidSyntaxException If xsl definition is incorrect
     */
    public static void addTemplate(Config config, String argument, DatatypeFactory factory) throws InvalidSyntaxException {
        Pipe pipe = config.getPipe();
        if(pipe==null) {
            pipe = new Pipe();
            config.setPipe(pipe);
        }
        pipe.addXslt(resolveTemplate(argument, factory));
    }
    /**
     * Defines the output of this config
     * @param config The config used
     * @param argument The absolute dir to put files in
     * @throws fr.efl.chaine.xslt.InvalidSyntaxException  If output definition is incorrect
     */
    public static void setOutput(Config config, String argument) throws InvalidSyntaxException {
        if(config.getPipe()==null) {
            Output output = new Output();
            output.setAbsolute(argument);
            output.setName("${name}");
            config.getPipe().setOutput(output);
        }
    }

    private static Xslt resolveTemplate(String path, DatatypeFactory factory) {
        int index = path.indexOf("(");
        String filePath = index>0 ? path.substring(0, index) : path;
        String sParams = index>0 ? path.substring(index+1, path.length()-1).trim() : "";
        Xslt ret = new Xslt(filePath);
        for(ParameterValue p:getParametersOfTemplate(sParams, factory)) {
            ret.addParameter(p);
        }
        return ret;
    }
    private static CfgFile resolveInputFile(String path, DatatypeFactory factory) {
        int index = path.indexOf("(");
        String filePath = index>0 ? path.substring(0, index) : path;
        String sParams = index>0 ? path.substring(index+1, path.length()-1).trim() : "";
        CfgFile ret = new CfgFile(new File(filePath));
        for(ParameterValue p:getParametersOfTemplate(sParams, factory)) {
            ret.addParameter(p);
        }
        ParameterValue relativeFile = new ParameterValue(ParametersMerger.INPUT_RELATIVE_FILE, ret.getSource().toURI().toString(), factory.XS_STRING);
        ret.addParameter(relativeFile);
        ParameterValue relativeDir = new ParameterValue(ParametersMerger.INPUT_RELATIVE_DIR, ret.getSource().getParentFile().toURI().toString(), factory.XS_STRING);
        ret.addParameter(relativeDir);
        return ret;
    }
    private static List<ParameterValue> getParametersOfTemplate(final String s, DatatypeFactory factory) {
        ArrayList<ParameterValue> ret = new ArrayList<>();
        String[] entries = s.split(",");
        for(String entry:entries) {
            String[] ps = entry.split("=");
            if(ps.length==2) {
                ParameterValue p = new ParameterValue(resolveQName(ps[0]), ps[1], factory.XS_STRING);
                ret.add(p);
            }
        }
        return ret;
    }
    
    private class SchemaValidationErrorListener implements ErrorHandler {
        private boolean errors = false;
        public boolean hasErrors() { return errors; }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
            ConfigUtil.LOGGER.warn("validating configFile: "+exception.getMessage());
        }

        @Override
        public void error(SAXParseException exception) throws SAXException {
            ConfigUtil.LOGGER.warn("validating configFile: "+exception.getMessage());
            errors = true;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
            ConfigUtil.LOGGER.warn("validating configFile: "+exception.getMessage());
            errors = true;
        }
    }

} public class JavaStep implements ParametrableStep {
    static final QName QNAME = new QName(Config.NS, "java");
    static final QName ATTR_CLASS = new QName("class");
    private String className;
    private HashMap<QName,ParameterValue> params;
    private Class clazz;
    
    public JavaStep() {
        super();
        params = new HashMap<>();
    }
    public JavaStep(String className) {
        this();
        this.className=className;
    }

    @Override
    public Collection<ParameterValue> getParams() {
        return params.values();
    }

    @Override
    public void addParameter(ParameterValue param) {
        if(param!=null) {
            params.put(param.getKey(), param);
        }
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        try {
            clazz = Class.forName(className);
            if(!isDerivedFrom(clazz, StepJava.class)) {
                throw new InvalidSyntaxException("Class "+className+" do not extends "+StepJava.class.getName());
            }
        } catch(ClassNotFoundException | InvalidSyntaxException ex) {
            throw new InvalidSyntaxException(ex);
        }
    }
    
    public Class<StepJava> getStepClass() {
        return clazz;
    }
    
    /**
     * Returns true if {@link toTest} is or extends {@link isA}
     * @param toTest
     * @param isA
     * @return <tt>true</tt> if the <tt>toTest</tt> class extends the <tt>isA</tt> class.
     */
    private static boolean isDerivedFrom(Class toTest, Class isA) {
        Class hierarch = toTest;
        while(hierarch!=null && !hierarch.equals(isA)) {
            hierarch = hierarch.getSuperclass();
        }
        return hierarch!=null;
    }

    @Override
    public String toString(String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("java class=").append(className).append("\n");
        return sb.toString();
    }

    @Override
    public String toString() {
        return toString("");
    }
    
    
} public class Listener implements Verifiable, Serializable {
    public static final int DEFAULT_PORT=8888;
    public static final transient QName QName = new QName(Config.NS, "listener");
    public static final transient QName ATTR_PORT = new QName("port");
    public static final transient QName ATTR_STOP = new QName("stopKeyword");
    private final int port;
    private final String stopKeyword;
    private JavaStep javastep;
    
    public Listener(final int port, final String stopKeyword) {
        super();
        this.port=port;
        this.stopKeyword=stopKeyword;
    }
    public Listener(final String stopKeyword) {
        this(DEFAULT_PORT, stopKeyword);
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        if(stopKeyword==null || stopKeyword.length()<3) {
            throw new InvalidSyntaxException("STOP keyword must be at least 3 chars length");
        }
        if(javastep!=null) {
            javastep.verify();
        }
    }

    public int getPort() {
        return port;
    }

    public String getStopKeyword() {
        return stopKeyword;
    }

    public JavaStep getJavastep() {
        return javastep;
    }

    public void setJavastep(JavaStep javastep) {
        this.javastep = javastep;
    }
    
    
} public class Output implements Verifiable {
    final static QName QNAME = new QName(Config.NS, "output");
    static final QName QN_CONSOLE = new QName(Config.NS, "console");
    static final QName ATTR_CONSOLE_WHICH = new QName("which");
    public static final HashMap<String,OutputPropertyEntry> VALID_OUTPUT_PROPERTIES = new HashMap<>();
    private static final Logger LOGGER = LoggerFactory.getLogger(Output.class);
    private String relativeTo, relativePath;
    private String absolute;
    private String prefix, suffix, name;
    private String console = null;
    private String id;
    private final OutputProperties outputProperties;
    private boolean nullOutput = false;
    
    static {
        VALID_OUTPUT_PROPERTIES.put("byte-order-mark", new OutputPropertyEntry(Serializer.Property.BYTE_ORDER_MARK, "yes", "no"));
        VALID_OUTPUT_PROPERTIES.put("cdata-section-elements", new OutputPropertyEntry(Serializer.Property.CDATA_SECTION_ELEMENTS));
        VALID_OUTPUT_PROPERTIES.put("doctype-public", new OutputPropertyEntry(Serializer.Property.DOCTYPE_PUBLIC));
        VALID_OUTPUT_PROPERTIES.put("doctype-system", new OutputPropertyEntry(Serializer.Property.DOCTYPE_SYSTEM));
        VALID_OUTPUT_PROPERTIES.put("encoding", new OutputPropertyEntry(Serializer.Property.ENCODING));
        VALID_OUTPUT_PROPERTIES.put("escape-uri-attributes", new OutputPropertyEntry(Serializer.Property.ESCAPE_URI_ATTRIBUTES, "yes", "no"));
//        VALID_OUTPUT_PROPERTIES.put("html-version", new OutputPropertyEntry(Serializer.Property.HTML_VERSION));
        VALID_OUTPUT_PROPERTIES.put("include-content-type", new OutputPropertyEntry(Serializer.Property.INCLUDE_CONTENT_TYPE, "yes", "no"));
        VALID_OUTPUT_PROPERTIES.put("indent", new OutputPropertyEntry(Serializer.Property.INDENT, "yes", "no"));
        VALID_OUTPUT_PROPERTIES.put("media-type", new OutputPropertyEntry(Serializer.Property.MEDIA_TYPE));
        VALID_OUTPUT_PROPERTIES.put("method", new OutputPropertyEntry(Serializer.Property.METHOD, "xml", "html", "xhtml", "text"));
        VALID_OUTPUT_PROPERTIES.put("normalization-form", new OutputPropertyEntry(Serializer.Property.NORMALIZATION_FORM, "NFC", "NFD", "NFKC", "NFKD", "none"));
        VALID_OUTPUT_PROPERTIES.put("omit-xml-declaration", new OutputPropertyEntry(Serializer.Property.OMIT_XML_DECLARATION, "yes", "no"));
        VALID_OUTPUT_PROPERTIES.put("saxon-attribute-order", new OutputPropertyEntry(Serializer.Property.SAXON_ATTRIBUTE_ORDER));
        VALID_OUTPUT_PROPERTIES.put("saxon-character-representation", new OutputPropertyEntry(Serializer.Property.SAXON_CHARACTER_REPRESENTATION));
        VALID_OUTPUT_PROPERTIES.put("saxon-double-space", new OutputPropertyEntry(Serializer.Property.SAXON_DOUBLE_SPACE));
        VALID_OUTPUT_PROPERTIES.put("saxon-implicit-result-document", new OutputPropertyEntry(Serializer.Property.SAXON_IMPLICIT_RESULT_DOCUMENT));
        VALID_OUTPUT_PROPERTIES.put("saxon-indent-spaces", new OutputPropertyEntry(Serializer.Property.SAXON_INDENT_SPACES));
        VALID_OUTPUT_PROPERTIES.put("saxon-line-length", new OutputPropertyEntry(Serializer.Property.SAXON_LINE_LENGTH));
//        VALID_OUTPUT_PROPERTIES.put("saxon-new-line", new OutputPropertyEntry(Serializer.Property.SAXON_NEWLINE));
        VALID_OUTPUT_PROPERTIES.put("saxon-recognize-binary", new OutputPropertyEntry(Serializer.Property.SAXON_RECOGNIZE_BINARY, "yes", "no"));
        VALID_OUTPUT_PROPERTIES.put("saxon-suppress-inndentation", new OutputPropertyEntry(Serializer.Property.SAXON_SUPPRESS_INDENTATION));
        VALID_OUTPUT_PROPERTIES.put("standalone", new OutputPropertyEntry(Serializer.Property.STANDALONE, "yes", "no"));
        VALID_OUTPUT_PROPERTIES.put("undeclare-prefixes", new OutputPropertyEntry(Serializer.Property.UNDECLARE_PREFIXES));
        VALID_OUTPUT_PROPERTIES.put("use-character-maps", new OutputPropertyEntry(Serializer.Property.USE_CHARACTER_MAPS));
        VALID_OUTPUT_PROPERTIES.put("version", new OutputPropertyEntry(Serializer.Property.VERSION, "1.0", "1.1"));
    }
    public Output() {
        super();
        outputProperties = new OutputProperties () {
            @Override
            public Object defineProperty(String key, String value) throws InvalidSyntaxException {
                // ignore @id attribute
                if("id".equals(key)) {
                    setId(value);
                    return value;
                }
                OutputPropertyEntry ope = VALID_OUTPUT_PROPERTIES.get(key);
                if(ope!=null) {
                    if(ope.isValueValid(value)) {
                        return super.setProperty(key, value); 
                    } else {
                        throw new InvalidSyntaxException(value+" is no a valid value for "+key);
                    }
                } else throw new InvalidSyntaxException(key+" is not a valid output property");
            }
        };
    }

    public void setRelativeTo(String relativeTo) {
        this.relativeTo = relativeTo;
        absolute = null;
    }
    public void setRelativePath(String relativePath) {
        this.relativePath = relativePath;
        absolute = null;
    }
    public void setAbsolute(String absolute) {
        this.absolute = absolute;
        relativePath = null;
        relativeTo = null;
    }
    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }
    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
    public void setName(String name) {
        this.name = name;
    }
    private boolean isAbsolute() {
        return absolute!=null;
    }
    public void setNull(final boolean nullOutput) {
        this.nullOutput = nullOutput;
    }
    /**
     * Defines outputProperties, based on <tt>props</tt> values.
     * It does not modify {@link #outputProperties} <tt>Properties</tt> object, it defines the values in.
     * It call {@link #setOutputProperty(java.lang.String, java.lang.String)} for each property
     * All existing properties are removed before setting new properties.
     * @param props The properties to set
     * @throws fr.efl.chaine.xslt.InvalidSyntaxException  If a property or a value is invalid
     */
    public void setOutputProperties(final Properties props) throws InvalidSyntaxException {
        outputProperties.clear();
        for(Object key: props.keySet()) {
            outputProperties.defineProperty((String)key, (String)props.get(key));
        }
    }
    /**
     * Sets a new output property value. If this property was allready defined,
     * it is overriden
     * @param key The property name
     * @param value The property value
     * @throws fr.efl.chaine.xslt.InvalidSyntaxException If the property is invalid, or if the value is not allowed for the property
     */
    public void setOutputProperty(final String key, final String value) throws InvalidSyntaxException {
        outputProperties.defineProperty(key, value);
    }
    public void unsetOutputProperty(final String key) {
        outputProperties.remove(key);
    }
    /**
     * Returns <tt>true</tt> if the output should not be written anywhere
     * @return {@code true} if this output should produce nothing
     */
    public boolean isNullOutput() {
        return nullOutput;
    }
    /**
     * Returns a copy of outputProperties
     * @return A new copy of output properties
     */
    public Properties getOutputProperties() {
        Properties ret = new Properties();
        for(Object key: outputProperties.keySet()) {
            String sKey = key.toString();
            ret.setProperty(sKey,outputProperties.getProperty(sKey));
        }
        return ret;
    }
    public String getOutputProperty(final String key) {
        return outputProperties.getProperty(key);
    }
    
    /**
     * Renvoie le fichier destination
     * @param sourceFile Le fichier source traité
     * @param parameters Parameters to give. Should be used only to parse the escapes
     * @return The file to create
     * @throws InvalidSyntaxException If this output has no been correctly defined
     * @throws java.net.URISyntaxException If the constructed URI is no valid
     */
    public File getDestinationFile(File sourceFile, HashMap<QName,ParameterValue> parameters) throws InvalidSyntaxException, URISyntaxException {
        File ret;
        if(isAbsolute()) {
            String __abs = absolute;
            int pos = __abs.indexOf("${");
            while(pos>=0) {
                int closingPos = __abs.indexOf("}", pos);
                String propertyName = __abs.substring(pos+2, closingPos);
                String propertyValue = System.getProperty(propertyName);
                if(propertyValue!=null) {
                    LOGGER.debug("Replace system property {} with value {} in {} produces {}", new String[]{propertyName, propertyValue, __abs, __abs.replaceAll("\\$\\{"+propertyName+"\\}", Matcher.quoteReplacement(propertyValue))});
                    __abs = __abs.replaceAll("\\$\\{"+propertyName+"\\}", Matcher.quoteReplacement(propertyValue));
                } else {
                    LOGGER.warn("System property "+propertyName+" is not defined");
                }
                pos = __abs.indexOf("${", pos+1);
            }
            for(ParameterValue pv:parameters.values()) {
                LOGGER.debug("replacing $["+pv.getKey()+"]");
                if(pv.getValue() instanceof String) {
                    __abs = __abs.replaceAll("\\$\\["+pv.getKey()+"\\]", pv.getValue().toString());
                }
            }
            File directory = __abs.startsWith("file:") ? new File(new URI(__abs)) : new File(__abs);
            ret = new File(directory, getFileName(sourceFile, parameters));
        } else {
            File directory = null;
            if("source-file".equals(relativeTo)) directory = sourceFile.getParentFile();
            else if(relativeTo.startsWith("${")) {
                directory = new File(System.getProperty(relativeTo.substring(2).substring(0,relativeTo.length()-3)));
            } else {
                throw new InvalidSyntaxException("folder/@to must be either source-file or ${xxx} where xxx is a system-property name. "+relativeTo+" is not a valid value");
            }
            directory = new File(directory, relativePath);
            ret = new File(directory, getFileName(sourceFile, parameters));
        }
        return ret;
    }
    private String getFileName(File sourceFile, HashMap<QName,ParameterValue> parameters) {
        String filename = (prefix!=null?prefix:"") + name + (suffix!=null?suffix:"");
        String sourceName = sourceFile.getName();
        int ix = sourceName.lastIndexOf(".");
        // FIXME: this shouldn't be supported, it has been replaced by input-* pseudo-variables
        String extension = sourceName.substring(ix);
        String basename = sourceName.substring(0, ix);
        String ret = filename.replaceAll("\\$\\{name\\}", sourceName).replaceAll("\\$\\{basename\\}", basename).replaceAll("\\$\\{extension\\}", extension);
        for(ParameterValue pv:parameters.values()) {
            if(pv.getValue() instanceof String || pv.getValue() instanceof XdmAtomicValue) {
                ret = ret.replaceAll("\\$\\["+pv.getKey()+"\\]", pv.getValue().toString());
            }
        }
        return ret;
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        if(nullOutput) return;
        if(isConsoleOutput()) return;
        if(!isAbsolute() && (relativePath==null || relativeTo==null)) throw new InvalidSyntaxException("output is neither absolute nor relative");
        if(name==null) throw new InvalidSyntaxException("no strategy to calculate output filename is defined");
    }

    @Override
    public String toString() {
        return "Output{" + "relativeTo=" + relativeTo + ", relativePath=" + relativePath + ", absolute=" + absolute + ", prefix=" + prefix + ", suffix=" + suffix + ", name=" + name + '}';
    }
    
    public static class OutputPropertyEntry {
        Serializer.Property saxonProp;
        List<String> validValuesList;
        public OutputPropertyEntry(Serializer.Property saxonProp, String... values) {
            super();
            this.saxonProp=saxonProp;
            if(values!=null && values.length>0) {
                Arrays.sort(values);
                validValuesList = Arrays.asList(values);
            }
        }
        public Serializer.Property getSaxonProperty() { return saxonProp; }
        public boolean isValueValid(String value) {
            if(validValuesList==null || validValuesList.isEmpty()) return true;
            if(value==null) return false;
            return validValuesList.contains(value);
        }
    }
    public abstract class OutputProperties extends Properties {
        public abstract Object defineProperty(String key, String value) throws InvalidSyntaxException;
    }
    public String toString(final String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append(toString());
        return sb.toString();
    }

    /**
     * Returns the console to use.
     * @return "out", "err" or null
     */
    public String getConsole() {
        return console;
    }

    /**
     * Defines which console to use.
     * @param console Only "out", "err" and null are valid values
     * @throws InvalidSyntaxException If console is not a valid value.
     */
    public void setConsole(String console) throws InvalidSyntaxException {
        if(!"out".equals(console) && !"err".equals(console) && console!=null) {
            throw new InvalidSyntaxException("Only out, err and null are valid values for console");
        }
        this.console = console;
    }
    public boolean isConsoleOutput() { return getConsole()!=null; }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    
} public interface ParametrableStep extends Verifiable {
    
    /**
     * Returns the parameters 
     * @return The parameters
     */
    Collection<ParameterValue> getParams();
    
    /**
     * Adds a parameter
     * @param param the parameter to add. <tt>param</tt> may be null.
     */
    void addParameter(ParameterValue param);
    
    /**
     * A tostring() with a prefix on each line
     * @param prefix The prefix to use on each line
     * @return The formal description, with a prefix
     */
    public String toString(final String prefix);
} public class Pipe implements Verifiable {
    static final QName QNAME = new QName(Config.NS, "pipe");
    static final String ATTR_NB_THREADS = "nbThreads";
    static final String ATTR_MAX = "mutiThreadMaxSourceSize";
    static final String ATTR_TRACE = "traceOutput";
    // par défaut, 10Mo
    private int multithreadMaxSourceSize = 10*1024*1024;
    private int nbThreads = 1;
    private String traceOutput;
    private final List<ParametrableStep> steps;
    private Output output;
    private Tee tee;
    private Tee parentTee = null;
    
    public Pipe() {
        super();
        steps = new ArrayList<>();
    }
    public Pipe(Tee parent) {
        this();
        this.parentTee=parent;
    }

    public int getMultithreadMaxSourceSize() {
        return multithreadMaxSourceSize;
    }

    public void setMultithreadMaxSourceSize(int multithreadMaxSourceSize) {
        this.multithreadMaxSourceSize = multithreadMaxSourceSize;
    }

    public int getNbThreads() {
        return nbThreads;
    }

    public void setNbThreads(int nbThreads) {
        this.nbThreads = nbThreads;
    }

    public Iterator<ParametrableStep> getXslts() {
        return steps.iterator();
    }
    
    /**
     * Adds an xslt to the pipe
     * @param xsl The xsl to add
     * @throws fr.efl.chaine.xslt.InvalidSyntaxException If this xsl is added in a invalid location
     * @throws IllegalStateException If a <tt>&lt;tee&gt;</tt> or a <tt>&lt;output&gt;</tt> has already been added
     */
    public void addXslt(ParametrableStep xsl) throws InvalidSyntaxException {
        if(output!=null || tee!=null) {
            throw new InvalidSyntaxException("xsl|javaStep elements must not be added after an output or a tee element");
        }
        steps.add(xsl);
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        for(ParametrableStep x:steps) x.verify();
        
        boolean shouldHaveXsltAsFirstStep = !steps.isEmpty() || tee != null;
        boolean hasXsltAsFirstStep = !steps.isEmpty() && steps.get(0) instanceof Xslt;
        if(shouldHaveXsltAsFirstStep && !hasXsltAsFirstStep) {
            throw new InvalidSyntaxException("The first step of a pipe must be an XSLT. Please add an identity XSL to start pipe.");      
        }
        
        if(tee==null && output==null) {
            throw new InvalidSyntaxException("a pipe must be terminated either by a tee or by an output");
        }
        

        if(tee!=null) {
            tee.verify();
        }
        if(output!=null) {
            output.verify();
        }
    }

    public Output getOutput() {
        return output;
    }

    public void setOutput(Output output) throws InvalidSyntaxException {
        if(tee!=null) {
            throw new InvalidSyntaxException("a pipe must be terminated either by a tee or by an output");
        }
        this.output = output;
    }

    public Tee getTee() {
        return tee;
    }

    public void setTee(Tee tee) throws InvalidSyntaxException {
        if(output!=null) {
            throw new InvalidSyntaxException("a pipe must be terminated either by a tee or by an output");
        }
        this.tee = tee;
    }
    
    public boolean isFinal() {
        return output!=null;
    }
    /**
     * Return <tt>true</tt> if this pipe does not contains any <tt>tee</tt>.
     * @return <tt>true</tt> if this pipe does not contains a tee
     */
    public boolean isStraight() {
        return tee==null;
    }

    /**
     * Retuns the trace output. Valid values are #default, #logger, or an URI
     * @return Where to output traces
     */
    public String getTraceOutput() {
        return traceOutput;
    }

    void setTraceOutput(String traceOutput) {
        this.traceOutput = traceOutput;
    }
    
    @Override
    public String toString() {
        return toString("");
    }
    public String toString(final String prefix) {
        String _p = prefix+"  ";
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("pipe\n");
        for(ParametrableStep ps:steps) {
            sb.append(ps.toString(_p));
        }
        if(output!=null) {
            sb.append(output.toString(_p));
        }
        if(tee!=null) {
            sb.append(tee.toString(_p));
        }
        return sb.toString();
    }
    

} public class Tee implements Verifiable {
    static final QName QNAME = new QName(Config.NS, "tee");
    static final QName PIPE = new QName(Config.NS, "pipe");
    private List<Pipe> pipes = new ArrayList<>();
    
    public Tee() {
        super();
    }

    public List<Pipe> getPipes() {
        return pipes;
    }

    public void addPipe(Pipe pipe) {
        pipes.add(pipe);
    }
    @Override
    public void verify() throws InvalidSyntaxException {
        if(pipes.size()<2) throw new InvalidSyntaxException("At least 2 sub-pipes are required");
        for(Pipe pipe:pipes) pipe.verify();
    }
    public String toString(String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("tee").append("\n");
        String _p = prefix.concat("  ");
        for(Pipe pipe:pipes) {
            sb.append(pipe.toString(_p));
        }
        return sb.toString();
    }
    
} public class WhenEntry implements ParametrableStep {
    public static final QName QNAME = new QName(Config.NS, "when");
    public static final QName QN_OTHERWISE = new QName(Config.NS, "otherwise");
    public static final QName ATTR_TEST = new QName("test");
    private String test;
    private final List<ParametrableStep> steps;
    
    public WhenEntry() {
        steps = new ArrayList<>();
    }
    
    public WhenEntry(final String test) {
        this();
        this.test=test;
    }

    public String getTest() {
        return test;
    }

    public List<ParametrableStep> getSteps() {
        return steps;
    }
    
    public void addStep(ParametrableStep step) {
        steps.add(step);
    }

    /**
     * Always return null, a When can not have parameters.
     * @return {@code null}
     */
    @Override
    public Collection<ParameterValue> getParams() {
        return null;
    }

    /**
     * Does nothing, a When  can not have parameters.
     * @param param Ignored
     */
    @Override
    public void addParameter(ParameterValue param) { }

    @Override
    public String toString(String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("when test=\"").append(test).append("\"\n");
        String _p = prefix+"  ";
        for(ParametrableStep step:steps) {
            step.toString(_p);
        }
        return sb.toString();
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        if(test==null || test.length()==0) {
            throw new InvalidSyntaxException("when/@test must be a XPath expression that returns null");
        }
        for(ParametrableStep step:steps) {
            step.verify();
        }
    }
} public class Xslt implements ParametrableStep {
    static final QName QNAME = new QName(Config.NS, "xslt");
    static final QName ATTR_HREF = new QName("href");
    static final QName ATTR_TRACE_ACTIVE = new QName("traceActive");
    static final QName ATTR_DEBUG= new QName("debug");
    static final QName ATTR_ID = new QName("id");
    private String href;
    private HashMap<QName,ParameterValue> params;
    private boolean traceToAdd = false;
    // pour usage interne
    private File file;
    private boolean debug;
    private String id;
    
    public Xslt() {
        super();
        params = new HashMap<>();
    }
    
    public Xslt(String href) {
        this();
        this.href = href;
    }

    public String getHref() {
        return href;
    }

    public void setHref(String href) {
        this.href = href;
    }

    @Override
    public Collection<ParameterValue> getParams() {
        return params.values();
    }
    
    @Override
    public void addParameter(ParameterValue param) {
        if(param!=null) {
            params.put(param.getKey(), param);
        }
    }
    
    // cette méthode ne peut être appelée, il faut passer par la substitution dans le href
    private File getFile() throws URISyntaxException {
        if(file==null) {
            if(href.startsWith("jar:")) {
                file = new File(new URI(href));
            } else if(href.startsWith("cp:")) {
                // nope
            } else {
                // issue 38
                try {
                    URI uri = new URI(href);
                    file = new File(uri);
                } catch(Exception ex) {
                    file = new File(href);
                }
            }
        }
        return file;
    }

    @Override
    public void verify() throws InvalidSyntaxException {
        try {
            if(!href.contains("$[")) {
                if(href.startsWith("jar:")) {
                    try (InputStream is = new URL(href).openStream()){
                    } catch(Exception ex) {
                        throw new InvalidSyntaxException("Unable to load "+href+". Either jar file does not exist, or entry does not exist in.");
                    }
                } else if(href.startsWith("cp:")) {
                    // FIXME : create a URL
                    // URL url = GauloisPipe.class.getResource(href.substring(3));
                    try {
                        URL url = new URL(href);
                    } catch(MalformedURLException ex) {
                        throw new InvalidSyntaxException("Unable to load "+href+" from classpath. Did you start with a '/' ?");
                    }
                } else  {
                    File xslFile = getFile();
                    if(!xslFile.exists() || !xslFile.isFile()) {
                        throw new InvalidSyntaxException(getFile().getAbsolutePath()+" does not exists or is not a regular file");
                    }
                }
            } else {
                // on ne peut pas effectuer cette vérification.
                // on ne peut pas vérifier que les paramètres existent
                // on ne fait pas de contrôle
            }
        } catch(URISyntaxException ex) {
            throw new InvalidSyntaxException(ex);
        }
    }

    /**
     * Return true if a TraceListener should be added to this Xslt
     * @return true if a TraceListener should be added to this Xslt
     */
    public boolean isTraceToAdd() {
        return traceToAdd;
    }

    void setTraceToAdd(boolean traceToAdd) {
        this.traceToAdd = traceToAdd;
    }

    public boolean isDebug() {
        return debug;
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString(String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append("xslt href=").append(href);
        if(id!=null) {
            sb.append(" id=").append(id);
        }
        sb.append("\n");
        return sb.toString();
    }

    @Override
    public String toString() {
        return toString("");
    }
    
    
} public class HttpListener {
    static final transient String CONTEXT_ENTRY = "CTX_KEY";
    static final transient String STOP_ENTRY = "STOP_KEY";
    static final transient String LISTENER = "__XX__LISTENER__XX__";
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpListener.class);
    private final int port;
    private final String stopKeyWord;
    private final ExecutionContext execCtx;
    private Server jettyServer;

    public HttpListener(int port, String stopKeyWord, ExecutionContext context) {
        super();
        this.port = port;
        this.stopKeyWord = stopKeyWord;
        this.execCtx = context;
    }
    
    public void run() {
        jettyServer = new Server(port);
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        context.getServletContext().setAttribute(STOP_ENTRY, stopKeyWord);
        context.getServletContext().setAttribute(CONTEXT_ENTRY, execCtx);
        context.getServletContext().setAttribute(LISTENER, this);
        context.setContextPath("");
        context.addServlet(new ServletHolder(ListenerServlet.class), "/*");
        
        
        jettyServer.setHandler(context);
        try {
            jettyServer.start();
            jettyServer.join();
        } catch(Exception ex) {
            LOGGER.error("While starting", ex);
            try {
                jettyServer.stop();
            } catch(Exception ex2) {
            } finally {
                jettyServer.destroy();
            }
        }
    }
    
    /**
     * This method stop the Listener
     * and returns when the server is actually stopped.
     * <b>Warning</b>: if the stop keyword has not been
     * received before, the service will not be terminated.
     */
    void stop() {
//        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
//        final ExecutionContext ctx = context;
        Runnable runner = new Runnable() {
            @Override
            public void run() {
                try {
                    jettyServer.stop();
                    try {
                        execCtx.getService().awaitTermination(5, TimeUnit.HOURS);
                    } catch (InterruptedException ex) {}
//                    Map<Thread,StackTraceElement[]> threads = Thread.getAllStackTraces();
//                    for(Thread t:threads.keySet()) {
//                        LOGGER.debug(t.getName());
//                        for(StackTraceElement s:threads.get(t)) {
//                            LOGGER.debug("\t"+s.toString());
//                        }
//                    }
                    LOGGER.info("asynchronous processing terminated, JVM is going to terminate");
                } catch(Exception ex) {
                    LOGGER.error("while stopping", ex);
                }
            }
        };
        new Thread(runner).start();
    }
} public class DoubleDestination {
    private Object start;
    private Destination end;
    
    public DoubleDestination() {
        super();
    }
    public DoubleDestination(final Object start, final Destination end) {
        this();
        this.start=start;
        this.end=end;
    }

    public Object getStart() {
        return start;
    }

    public void setStart(XsltTransformer start) {
        this.start = start;
    }

    public Destination getEnd() {
        return end;
    }

    public void setEnd(Destination end) {
        this.end = end;
    }
    
    
} public class ParametersMerger {
    private static final Logger LOGGER = LoggerFactory.getLogger(ParametersMerger.class);
    
    public static final QName INPUT_BASENAME = new QName("input-basename");
    public static final QName INPUT_NAME = new QName("input-name");
    public static final QName INPUT_EXTENSION = new QName("input-extension");
    public static final QName INPUT_ABSOLUTE = new QName("input-absolute");
    public static final QName INPUT_RELATIVE_FILE = new QName("input-relative-file");
    public static final QName INPUT_RELATIVE_DIR = new QName("input-relative-dir");
    public static final QName GP_STATIC_BASE_URI = new QName(Config.NS, "static-base-uri");


    /**
     * Merges the two lists into a new list, and let the two original lists unchanged.
     * @param highPriority First list of parameters
     * @param lowPriority Second list of parameters
     * @return A new list that contains all elements from <tt>l1</tt> and <tt>l2</tt>
     */
    public static HashMap<QName,ParameterValue> merge(final HashMap<QName,ParameterValue> highPriority, final HashMap<QName,ParameterValue> lowPriority) {
//        LOGGER.debug("merging : "+highPriority+" with "+lowPriority);
        HashMap<QName,ParameterValue> ret = new HashMap<>();
        ret.putAll(highPriority);
        for(ParameterValue pv: lowPriority.values()) {
            if(!ret.containsKey(pv.getKey()))
                ret.put(pv.getKey(), pv);
        }
        // on fait les éventuelles substitutions, pour ne les faire qu'une fois.
        for(ParameterValue pv:ret.values()) {
            if(!pv.isAbstract()) {
                pv.setValue(processParametersReplacement(pv.getValue(), ret));
            }
        }
        return ret;
    }
    
    /**
     * Replaces the parameters in string
     * @param initialValue The String to change parameters in
     * @param parameters The parameters values
     * @return The initialValue with all parameters replaced
     */
    public static Object processParametersReplacement(Object initialValue, final HashMap<QName,ParameterValue> parameters) {
        if(initialValue instanceof String) {
            String ret = initialValue.toString();
            if(ret.contains("$[")) {
                for(ParameterValue pv: parameters.values()) {
                    if(pv.getValue() instanceof String) {
                        try {
                            // issue #14
                            ret = ret.replaceAll("\\$\\["+pv.getKey()+"\\]", Matcher.quoteReplacement(pv.getValue().toString()));
                        } catch(java.lang.IllegalArgumentException ex) {
                            LOGGER.error("while replacing "+pv.getKey()+" -> "+pv.getValue(),ex);
                            throw ex;
                        }
                    }
                    if(!ret.contains("$[")) break;
                }
            }
            return ret;
        } else {
            return initialValue;
        }
    }
    /**
     * Replaces the parameters in string
     * @param initialValue The String to change parameters in
     * @param parameters The parameters values
     * @return The initialValue with all parameters replaced
     */
    public static String processParametersReplacement(String initialValue, final HashMap<QName,ParameterValue> parameters) {
        String ret = initialValue;
        if(ret.contains("$[")) {
            for(ParameterValue pv: parameters.values()) {
                if(pv.getValue() instanceof String) {
                    try {
                        // issue #14
                        ret = ret.replaceAll("\\$\\["+pv.getKey()+"\\]", Matcher.quoteReplacement(pv.getValue().toString()));
                    } catch(java.lang.IllegalArgumentException ex) {
                        LOGGER.error("while replacing "+pv.getKey()+" -> "+pv.getValue(),ex);
                        throw ex;
                    }
                } else if(pv.getValue() instanceof XdmAtomicValue) {
                    String replacer = Matcher.quoteReplacement(((XdmAtomicValue)pv.getValue()).toString());
                    ret = ret.replaceAll("\\$\\["+pv.getKey()+"\\]", replacer);
                }
                if(!ret.contains("$[")) break;
            }
        }
        return ret;
    }
    
    /**
     * Add input pseudo-variables to parameters
     * @param parameters The parameters values
     * @param inputFile The input file actually processed. input-basename, input-name, input-absolute and input-extension are added, so can be used.
     * @param factory The DatatypeFactory to use
     * 
     * @return The parameters whith input-name, input-basename, input-absolute and input-extension added
     */
    public static HashMap<QName,ParameterValue> addInputInParameters(final HashMap<QName,ParameterValue> parameters, final File inputFile, final DatatypeFactory factory) {
        HashMap<QName,ParameterValue> fileParams = new HashMap<>();
        String name = inputFile.getName();
        String basename = name.substring(0, name.lastIndexOf("."));
        String extension = name.substring(basename.length()+1);
        fileParams.put(INPUT_BASENAME, new ParameterValue(INPUT_BASENAME, basename, factory.XS_STRING));
        fileParams.put(INPUT_NAME, new ParameterValue(INPUT_NAME, name, factory.XS_STRING));
        fileParams.put(INPUT_EXTENSION, new ParameterValue(INPUT_EXTENSION, extension, factory.XS_STRING));
        fileParams.put(INPUT_ABSOLUTE, new ParameterValue(INPUT_ABSOLUTE, inputFile.getAbsolutePath(), factory.XS_STRING));
        return merge(parameters, fileParams);
    }
} public class ParametrableFile {
    private final HashMap<QName,ParameterValue> parameters;
    private final File file;
    private boolean avoidCache;
    
    public ParametrableFile(final File file) {
        super();
        this.file=file;
        parameters = new HashMap<>();
    }

    public HashMap<QName,ParameterValue> getParameters() {
        return parameters;
    }

    public File getFile() {
        return file;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        if(file!=null) {
            sb.append(file.getAbsolutePath());
        } else {
            sb.append("<null>");
        }
        if(!parameters.isEmpty()) {
            sb.append("(");
            for(ParameterValue pv: parameters.values()) {
                sb.append(pv).append(",");
            }
            sb.deleteCharAt(sb.length()-1);
            sb.append(")");
        }
        return sb.toString();
    }
    public void setAvoidCache(boolean avoidCache) {
        this.avoidCache=avoidCache;
    }
    public boolean getAvoidCache() {
        return avoidCache;
    }
} public class TeeDebugDestination implements Destination {
//    private static final Logger LOGGER = LoggerFactory.getLogger(TeeDebugDestination.class);
    private TeeDestination tee;
    private final Destination debugDestination;

    public TeeDebugDestination(Destination debugDestination) {
//        LOGGER.debug("<new>(debugDestination)");
        this.debugDestination = debugDestination;
    }
    
    public void setDestination(Destination mainDestination) {
//        LOGGER.debug("setDestination(mainDestination)");
        tee = new TeeDestination(debugDestination, mainDestination);
    }

    @Override
    public Receiver getReceiver(Configuration c) throws SaxonApiException {
//        LOGGER.debug("getReceiver(Configuration)");
        return tee.getReceiver(c);
    }

    @Override
    public void close() throws SaxonApiException {
        tee.close();
    }
    
} public class DatatypeFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(DatatypeFactory.class);
    public static final String NS_XSD = "http://www.w3.org/2001/XMLSchema";
    public final Datatype XS_STRING;
    private HashMap<QName,Datatype> constructed = new HashMap<>();
    private final BuiltInAtomicType[] ATOMIC_TYPES = {
        BuiltInAtomicType.ANY_ATOMIC, BuiltInAtomicType.ANY_URI,
        BuiltInAtomicType.BASE64_BINARY, BuiltInAtomicType.BOOLEAN,
        BuiltInAtomicType.BYTE, BuiltInAtomicType.DATE,
        BuiltInAtomicType.DATE_TIME, BuiltInAtomicType.DATE_TIME_STAMP,
        BuiltInAtomicType.DAY_TIME_DURATION, BuiltInAtomicType.DECIMAL,
        BuiltInAtomicType.DOUBLE, BuiltInAtomicType.DURATION,
        BuiltInAtomicType.ENTITY, BuiltInAtomicType.FLOAT,
        BuiltInAtomicType.G_DAY, BuiltInAtomicType.G_MONTH,
        BuiltInAtomicType.G_MONTH_DAY, BuiltInAtomicType.G_YEAR,
        BuiltInAtomicType.G_YEAR_MONTH, BuiltInAtomicType.HEX_BINARY,
        BuiltInAtomicType.ID, BuiltInAtomicType.IDREF,
        BuiltInAtomicType.INT, BuiltInAtomicType.INTEGER,
        BuiltInAtomicType.LANGUAGE, BuiltInAtomicType.LONG,
        BuiltInAtomicType.NAME, BuiltInAtomicType.NCNAME,
        BuiltInAtomicType.NEGATIVE_INTEGER, BuiltInAtomicType.NMTOKEN,
        BuiltInAtomicType.NON_NEGATIVE_INTEGER, BuiltInAtomicType.NON_POSITIVE_INTEGER,
        BuiltInAtomicType.NORMALIZED_STRING, BuiltInAtomicType.NOTATION,
        BuiltInAtomicType.POSITIVE_INTEGER, BuiltInAtomicType.QNAME,
        BuiltInAtomicType.SHORT, BuiltInAtomicType.STRING,
        BuiltInAtomicType.TIME, BuiltInAtomicType.TOKEN,
        BuiltInAtomicType.UNSIGNED_BYTE, BuiltInAtomicType.UNSIGNED_INT,
        BuiltInAtomicType.UNSIGNED_LONG, BuiltInAtomicType.UNSIGNED_SHORT,
        BuiltInAtomicType.UNTYPED_ATOMIC, BuiltInAtomicType.YEAR_MONTH_DURATION
    };
    private final ConversionRules conversionRules;
    
    @SuppressWarnings("OverridableMethodCallInConstructor")
    private DatatypeFactory(ConversionRules conversionRules) throws ValidationException {
        super();
        this.constructed = new HashMap<>();
        this.conversionRules = conversionRules;
        XS_STRING = getDatatype(new QName(NS_XSD,"string"));
    }
    public Datatype getDatatype(QName qn) throws ValidationException {
        Datatype ret = constructed.get(qn);
        if(ret==null) {
            ret = constructDatatype(qn);
            constructed.put(qn, ret);
        }
        return ret;
    }
    
    protected Datatype constructDatatype(final QName qn) throws ValidationException {
        String localName = qn.getLocalName();
        final boolean isAtomic = NS_XSD.equals(qn.getNamespaceURI());
        final boolean allowsMultiple = localName.endsWith("+") | localName.endsWith("*");
        final boolean allowsEmpty = localName.endsWith("?") | localName.endsWith("*");
        if(isAtomic) {
            return constructAtomicDatatype(qn, allowsEmpty, allowsMultiple);
        } else {
            return constructNodeDatatype( qn, allowsEmpty, allowsMultiple);
        }
    }
    private Datatype constructAtomicDatatype(final QName qn, final boolean allowsEmpty, final boolean allowsMultiple) {
        String localName = qn.getLocalName();
        StructuredQName baseType = new StructuredQName(qn.getPrefix(), qn.getNamespaceURI(), (allowsMultiple|allowsEmpty) ? localName.substring(0, localName.length()-1) : localName);
        BuiltInAtomicType theType = null;
        for(BuiltInAtomicType type:ATOMIC_TYPES) {
            if(type.getStructuredQName().equals(baseType)) {
                theType = type;
                break;
            }
        }
        if(theType==null) {
            throw new IllegalArgumentException("Unable to determine datatype of "+qn.getEQName());
        }
        final StringConverter converter = theType.getStringConverter(conversionRules);
        return new Datatype() {
            @Override
            public boolean isAtomic() { return true; }
            @Override
            public boolean allowsMultiple() { return allowsMultiple; }
            @Override
            public boolean allowsEmpty() { return allowsEmpty; }
            @Override
            public XdmValue convert(String input, Configuration configuration) throws ValidationException {
                if(input==null && allowsEmpty()) return XdmValue.wrap(null);
                else if(input==null) {
                    throw new ValidationException(qn.toString()+" does not allow empty sequence", new Exception());
                }
                if(allowsMultiple()) {
                    String sValue = input.trim();
                    if(sValue.startsWith("(") && sValue.endsWith(")")) {
                        sValue = sValue.substring(1, sValue.length()-1);
                    }
                    String[] values = sValue.split("[ ]*,[ ]*");
                    if(values.length>1) {
                        XdmValue xValue = null;
                        // certainly could be optimized, but I do not expect having 1000 values in a parameter definition...
                        for(String value:values) {
                            XdmValue ret = XdmValue.wrap(converter.convertString(value).asAtomic());
                            if(xValue==null) {
                                xValue = ret;
                            } else {
                                xValue= xValue.append(ret);
                            }
                        }
                        return xValue;
                    } else if(values.length==1) {
                        return XdmValue.wrap(converter.convertString(values[0]).asAtomic());
                    } else {
                        throw new ValidationException("can not cast "+input+" to "+qn, new Exception());
                    }
                } else {
                    return XdmValue.wrap(converter.convertString(input).asAtomic());
                }
            }
        };
    }
    
    /**
     * Here, only <tt>element()</tt> and <tt>document</tt> are supported.
     * @param qn
     * @param allowsEmpty
     * @param allowsMultiple
     * @return A document fragment
     */
    private Datatype constructNodeDatatype(final QName qn, final boolean allowsEmpty, final boolean allowsMultiple) throws ValidationException {
        String localName = qn.getLocalName();
        String typeName = (allowsEmpty | allowsMultiple) ? localName.substring(0, localName.length()) : localName;
        typeName = typeName.trim();
        typeName= typeName.replaceAll("[ ]*\\(", "(");
        if(typeName.startsWith("element(")) {
            return constructElementParserDatatype(qn, allowsEmpty, allowsMultiple);
        } else if(typeName.startsWith("document(")) {
            return constructDocumentParserDatatype(qn, allowsEmpty, allowsMultiple);
        } else {
            throw new ValidationException("Only document() and element() are supported for node types", new Exception());
        }
    }
    private Datatype constructElementParserDatatype(final QName qn, final boolean allowsEmpty, final boolean allowsMultiple) throws ValidationException {
        return new Datatype() {
            @Override
            public boolean isAtomic() { return false; }
            @Override
            public boolean allowsMultiple() { return allowsMultiple; }
            @Override
            public boolean allowsEmpty() { return allowsEmpty; }
            @Override
            public XdmValue convert(String input, Configuration configuration) throws ValidationException {
                Processor proc = new Processor(configuration);
                DocumentBuilder builder = proc.newDocumentBuilder();
                String wrappedInput="<fake:document xmlns:fake=\"top:marchand:xml:gaulois:wrapper\">".concat(input).concat("</fake:document>");
                InputStream is = new ByteArrayInputStream(wrappedInput.getBytes(Charset.forName("UTF-8")));
                try {
                    XdmNode documentNode = builder.build(new StreamSource(is));
                    XPathCompiler compiler = proc.newXPathCompiler();
                    compiler.declareNamespace("fake", "top:marchand:xml:gaulois:wrapper");
                    XPathSelector selector = compiler.compile("/fake:document/node()").load();
                    selector.setContextItem(documentNode);
                    XdmValue ret = selector.evaluate();
                    if(ret.size()==0 && !allowsEmpty()) throw new ValidationException(qn.toString()+" does not allow empty sequence", new Exception());
                    if(ret.size()>1 && !allowsMultiple()) throw new ValidationException(qn.toString()+" does not allow sequence with more than one element", new Exception());
                    return ret;
                } catch(SaxonApiException ex) {
                    throw new ValidationException(input+" can not be casted to "+qn.toString(), new Exception());
                }
            }
        };
    }
    private Datatype constructDocumentParserDatatype(final QName qn, final boolean allowsEmpty, final boolean allowsMultiple) throws ValidationException {
        String localName= qn.getLocalName();
        if(localName.endsWith("*") || localName.endsWith("+")) {
            throw new ValidationException("Multiple documents are not allowed", new Exception());
        }
        return new Datatype() {
            @Override
            public boolean isAtomic() { return false; }
            @Override
            public boolean allowsMultiple() { return allowsMultiple; }
            @Override
            public boolean allowsEmpty() { return allowsEmpty; }
            @Override
            public XdmValue convert(String input, Configuration configuration) throws ValidationException {
                Source source = null;
                try {
                    configuration.getURIResolver().resolve(input, null);
                } catch(TransformerException ex0) {
                    InputStream is = null;
                    try {
                        URI uri = new URI(input);
                        is = uri.toURL().openStream();
                    } catch (URISyntaxException | IOException ex1) {
                        File f = new File(input);
                        if(f.exists() && f.isFile()) {
                            try {
                                is= new FileInputStream(f);
                            } catch (FileNotFoundException ex2) {
                                // can not happens, checked before
                            }
                        }
                    }
                    if(is==null) {
                        throw new ValidationException("Unable to load document "+input, new Exception());
                    }
                    source = new StreamSource(is);
                }
                DocumentBuilder builder = new Processor(configuration).newDocumentBuilder();
                try {
                    return builder.build(source);
                } catch (SaxonApiException ex) {
                    throw new ValidationException("Unable to load document "+input,ex);
                }
            }
        };
    }
    public static DatatypeFactory getInstance(Configuration configuration) throws ValidationException {
        return new DatatypeFactory(configuration.getConversionRules());
    }
    
} public class DefaultSaxonConfigurationFactory extends SaxonConfigurationFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSaxonConfigurationFactory.class);
    protected Configuration configuration;
    
    public DefaultSaxonConfigurationFactory() {
        super();
        __initConfiguration();
    }
    
    /**
     * To be overriden, if required
     */
    protected void createConfigurationObject() {
        configuration = Configuration.newConfiguration();
    }

    @Override
    public Configuration getConfiguration() {
        return configuration;
    }
    
    private void __initConfiguration() {
        // default construct
        createConfigurationObject();
        LOGGER.debug("configuration is a "+configuration.getClass().getName());
        // issue 39
        configuration.setSourceParserClass(GauloisSAXParserFactory.class.getName());
        ClassLoader cl = getClass().getClassLoader();
        if(cl instanceof URLClassLoader) {
            Processor proc = new Processor(configuration);
            DocumentBuilder builder = proc.newDocumentBuilder();
            XPathCompiler xpCompiler = proc.newXPathCompiler();
            URLClassLoader ucl = (URLClassLoader)cl;
            try {
                for(Enumeration<URL> enumer = ucl.findResources("META-INF/services/top.marchand.xml.gaulois.xml"); enumer.hasMoreElements();) {
                    URL url = enumer.nextElement();
                    LOGGER.debug("loading service "+url.toExternalForm());
                    XdmNode document = builder.build(new StreamSource(url.openStream()));
                    XPathSelector selector = xpCompiler.compile("/gaulois-services/saxon/extensions/function").load();
                    selector.setContextItem(document);
                    XdmSequenceIterator it = selector.evaluate().iterator();
                    while(it.hasNext()) {
                        String className = it.next().getStringValue();
                        try {
                            Class clazz = Class.forName(className);
                            if(extendsClass(clazz, ExtensionFunctionDefinition.class)) {
                                Class<ExtensionFunctionDefinition> cle = (Class<ExtensionFunctionDefinition>)clazz;
                                configuration.registerExtensionFunction(cle.newInstance());
                                LOGGER.debug(className+"registered as Saxon extension function");
                            } else {
                                LOGGER.warn(className+" does not extends "+ExtensionFunctionDefinition.class.getName());
                            }
                        } catch(ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
                            LOGGER.warn("unable to load extension function "+className);
                        }
                    }
                }
            } catch(IOException | SaxonApiException ex) {
                LOGGER.error("while looking for resources in /META-INF/services/top.marchand.xml.gaulois/", ex);
            }
        }
    }
    
    private boolean extendsClass(Class toCheck, Class inheritor) {
        if(toCheck.equals(inheritor)) return true;
        if(toCheck.equals(Object.class)) return false;
        return extendsClass(toCheck.getSuperclass(), inheritor);
    }
    
}
import fr.efl.chaine.xslt.utils.ParameterValue;
import fr.efl.chaine.xslt.config.CfgFile;
import fr.efl.chaine.xslt.config.ChooseStep;
import fr.efl.chaine.xslt.config.Config;
import fr.efl.chaine.xslt.config.ConfigUtil;
import fr.efl.chaine.xslt.config.JavaStep;
import fr.efl.chaine.xslt.config.Listener;
import fr.efl.chaine.xslt.config.Output;
import fr.efl.chaine.xslt.config.ParametrableStep;
import fr.efl.chaine.xslt.config.Pipe;
import fr.efl.chaine.xslt.config.Tee;
import fr.efl.chaine.xslt.config.WhenEntry;
import fr.efl.chaine.xslt.config.Xslt;
import fr.efl.chaine.xslt.listener.HttpListener;
import fr.efl.chaine.xslt.utils.DoubleDestination;
import fr.efl.chaine.xslt.utils.ParametersMerger;
import fr.efl.chaine.xslt.utils.ParametrableFile;
import fr.efl.chaine.xslt.utils.TeeDebugDestination;
import net.sf.saxon.s9api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.transform.URIResolver;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.io.OutputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.Duration;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import net.sf.saxon.Configuration;
import net.sf.saxon.event.ProxyReceiver;
import net.sf.saxon.event.Receiver;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.lib.StandardErrorListener;
import net.sf.saxon.lib.StandardLogger;
import net.sf.saxon.trace.XSLTTraceListener;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.SchemaException;
import net.sf.saxon.type.ValidationException;
import org.apache.commons.io.output.NullOutputStream;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.EntityResolver2;
import org.xmlresolver.Resolver;
import top.marchand.xml.gaulois.config.typing.DatatypeFactory;
import top.marchand.xml.gaulois.impl.DefaultSaxonConfigurationFactory;
import top.marchand.xml.protocols.ProtocolInstaller;

                pipe, 
                input.getFile(), 
                input.getFile().toURI().toURL().toExternalForm(), 
                parameters,
                listener, source, false, outputs.length>0 ? outputs[0] : null);
        LOGGER.debug("["+instanceName+"] transformer build");
        XsltTransformer t = (XsltTransformer)(dd.getStart());
        t.setInitialContextNode(source);
        t.transform();
        long duration = System.currentTimeMillis() - start;
        String distinctName = input.toString();
        try {
            Duration duree = xmlDatatypeFactory.newDuration(duration);
            LOGGER.info("["+instanceName+"] - "+distinctName+" - transform terminated: "+duree.toString());
        } catch(Exception ex) {
            LOGGER.info("["+instanceName+"] - "+distinctName+" - transform terminated");
        }
    }
    
    /**
     * Execute the pipe for the specified input stream to the specified
     * serializer.<br>
     * <b>Warning</b>: this method is public for implementation reasons,
     * and <b>must not</b> be called outside of GauloisPipe
     *
     * @param pipe the pipe to run
     * @param input the specified input stream
     * @param listener the message listener to use
     * @param isFake An optional parameter to indicate that we build a pipe not to run it, only to pre-compile XSLs
     * @throws SaxonApiException when a problem occurs
     * @throws java.net.MalformedURLException When an URL is not correctly formed
     * @throws fr.efl.chaine.xslt.InvalidSyntaxException When config file is invalid
     * @throws java.net.URISyntaxException When URI is invalid
     * @throws java.io.FileNotFoundException And when the file can not be found !
     */
    private DoubleDestination buildTransformer(Pipe pipe, File inputFile, String inputFileUri, HashMap<QName,ParameterValue> parameters, MessageListener listener, XdmNode documentTree, boolean... isFake) 
            throws InvalidSyntaxException, URISyntaxException, MalformedURLException, SaxonApiException, FileNotFoundException, IOException {
        return buildTransformer(pipe, inputFile, inputFileUri, parameters, listener, documentTree, false, new HashMap<String,OutputStream>());
    }
    private DoubleDestination buildTransformer(Pipe pipe, File inputFile, String inputFileUri, HashMap<QName,ParameterValue> parameters, MessageListener listener, XdmNode documentTree, boolean isFake, Map<String, OutputStream> ... outputs) 
            throws InvalidSyntaxException, URISyntaxException, MalformedURLException, SaxonApiException, FileNotFoundException, IOException {
        LOGGER.trace("in buildTransformer(Pipe,...)");
        XsltTransformer first = null;
        Iterator<ParametrableStep> it = pipe.getXslts();
        Object previousTransformer = null;
        Configuration _config = null;
        _config = configurationFactory.getConfiguration();
        while(it.hasNext()) {
            LOGGER.trace("...in buildTransformer.tee.while");
            ParametrableStep step = it.next();
            if(step instanceof Xslt) {
                Xslt xsl = (Xslt)step;
                XsltTransformer currentTransformer = getXsltTransformer(xsl.getHref(), parameters);
                Destination currentDestination = currentTransformer;
                if(xsl.isTraceToAdd()) {
                    currentTransformer.setTraceListener(traceListener);
                }
                if(listener!=null) {
                    LOGGER.trace(xsl.getHref()+" setting messageListener "+listener);
                    currentTransformer.setMessageListener(listener);
                }
                for(ParameterValue pv:xsl.getParams()) {
                    // on substitue les paramètres globaux dans ceux de la XSL
                    if(pv.getValue() instanceof String) {
                        String value = (String)ParametersMerger.processParametersReplacement(pv.getValue(), parameters);
                        LOGGER.trace("Setting parameter ("+pv.getKey()+","+value+")");
                        currentTransformer.setParameter(pv.getKey(), new XdmAtomicValue(value));
                    } else if(pv.getValue() instanceof XdmAtomicValue) {
                        String sValue = ((XdmAtomicValue)pv.getValue()).getStringValue();
                        sValue = (String)ParametersMerger.processParametersReplacement(sValue, parameters);
                        XdmValue newValue;
                        try {
                            newValue = pv.getDatatype().convert(sValue, _config);
                            currentTransformer.setParameter(pv.getKey(), newValue);
                        } catch (ValidationException ex) {
                            throw new SaxonApiException(ex);
                        }
                    } else if(pv.getValue() instanceof XdmValue){
                        currentTransformer.setParameter(pv.getKey(), (XdmValue)pv.getValue());
                    }
                }
                for(ParameterValue pv:parameters.values()) {
                    // substitution has been before, in merge, but there is input-file relative parameters...
                    if(pv.getValue() instanceof String) {
                        currentTransformer.setParameter(pv.getKey(), new XdmAtomicValue(
                                (String)ParametersMerger.processParametersReplacement(pv.getValue(), parameters)
                        ));
                    } else if(pv.getValue() instanceof XdmAtomicValue) {
                        String sValue = ((XdmAtomicValue)pv.getValue()).getStringValue();
                        sValue = (String)ParametersMerger.processParametersReplacement(sValue, parameters);
                        XdmValue newValue;
                        try {
                            newValue = pv.getDatatype().convert(sValue, _config);
                            currentTransformer.setParameter(pv.getKey(), newValue);
                        } catch (ValidationException ex) {
                            throw new SaxonApiException(ex);
                        }
                    } else {
                        currentTransformer.setParameter(pv.getKey(), (XdmValue)pv.getValue());
                    }
                }
                if(xsl.isDebug()) {
                    File debugFile;
                    if(debugDirectory!=null) {
                        debugFile = new File(debugDirectory, xsl.getId()+"-"+inputFile.getName());
                    } else {
                        debugFile = new File(xsl.getId()+"-"+inputFile.getName());
                    }
                    Serializer debug = processor.newSerializer(debugFile);
                    // here currentTransformer==currentDestination
                    currentTransformer.setDestination(currentDestination=new TeeDebugDestination(debug));
                }
                if(first==null) {
                    first = currentTransformer;
                }
                if(previousTransformer!=null) {
                    assignStepToDestination(previousTransformer, currentDestination);
                }
                previousTransformer = currentDestination;
                LOGGER.trace(xsl.getHref()+" constructed and added to pipe");

            } else if(step instanceof ChooseStep) {
