content: /**
   * Copyright 2000-2006 DFKI GmbH.
   * All Rights Reserved.  Use is subject to license terms.
   * <p/>
   * This file is part of MARY TTS.
   * <p/>
   * MARY TTS is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, version 3 of the License.
   * <p/>
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   * <p/>
   * You should have received a copy of the GNU Lesser General Public License
   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   */
  package marytts.server;
  
  import android.util.Log;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.OutputStream;
  import java.io.Reader;
  import java.sql.SQLException;
  import java.util.HashMap;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Set;
  import java.util.Timer;
  import java.util.TimerTask;
  
  import lib.sound.sampled.AudioFileFormat;
  import lib.sound.sampled.AudioInputStream;
  import lib.sound.sampled.AudioSystem;
  import lib.sound.sampled.UnsupportedAudioFileException;
  import marytts.datatypes.MaryData;
  import marytts.datatypes.MaryDataType;
  import marytts.datatypes.MaryXML;
  import marytts.modules.MaryModule;
  import marytts.modules.ModuleRegistry;
  import marytts.modules.synthesis.Voice;
  import marytts.util.MaryCache;
  import marytts.util.MaryRuntimeUtils;
  import marytts.util.MaryUtils;
  import marytts.util.data.audio.AppendableSequenceAudioInputStream;
  import marytts.util.dom.DomUtils;
  import marytts.util.dom.NameNodeFilter;
  import marytts.util.io.FileUtils;
  import mf.javax.xml.transform.TransformerException;
  import mf.org.w3c.dom.Document;
  import mf.org.w3c.dom.Element;
  import mf.org.w3c.dom.Node;
  import mf.org.w3c.dom.NodeList;
  import mf.org.w3c.dom.Text;
  import mf.org.w3c.dom.traversal.DocumentTraversal;
  import mf.org.w3c.dom.traversal.NodeFilter;
  import mf.org.w3c.dom.traversal.TreeWalker;
  
  //import org.apache.log4j.Level;
  //import org.apache.log4j.Logger;
  
  
  /**
   * A request consists of input data, a desired output data type and the means
   * to process the input data into the data of the output type.<br> <br> A
   * request is used as follows. First, its basic properties are set in the
   * constructor, such as its input and output types. Second, the input data is
   * provided to the request either by directly setting it
   * (<code>setInputData()</code>) or by reading it from a Reader
   * (<code>readInputData()</code>). Third, the request is processed
   * (<code>process()</code>). Finally, the output data is either accessed
   * directly (<code>getOutputData()</code>) or written to an output stream
   * (<code>writeOutputData</code>).
   */
  public class Request {
      protected MaryDataType inputType;
      protected MaryDataType outputType;
      protected String outputTypeParams;
      protected AudioFileFormat audioFileFormat;
      protected AppendableSequenceAudioInputStream appendableAudioStream;
      protected Locale defaultLocale;
      protected Voice defaultVoice;
      protected String defaultStyle;
      protected String defaultEffects;
  
      protected int id;
      protected MaryData inputData;
      protected MaryData outputData;
      protected boolean streamAudio = false;
      protected boolean abortRequested = false;
  
      // Keep track of timing info for each module
      // (map MaryModule onto Long)
      protected Set<MaryModule> usedModules;
      protected Map<MaryModule, Long> timingInfo;
  
      public Request(MaryDataType inputType, MaryDataType outputType, Locale defaultLocale,
                     Voice defaultVoice, String defaultEffects, String defaultStyle,
                     int id, AudioFileFormat audioFileFormat) {
          this(inputType, outputType, defaultLocale, defaultVoice, defaultEffects, defaultStyle, id, audioFileFormat, false, null);
      }
  
      public Request(MaryDataType inputType, MaryDataType outputType, Locale defaultLocale,
                     Voice defaultVoice, String defaultEffects, String defaultStyle,
                     int id, AudioFileFormat audioFileFormat, boolean streamAudio,
                     String outputTypeParams) {
          if (!inputType.isInputType())
              throw new IllegalArgumentException("not an input type: " + inputType.name());
          if (!outputType.isOutputType())
              throw new IllegalArgumentException("not an output type: " + outputType.name());
          this.inputType = inputType;
          this.outputType = outputType;
          this.defaultLocale = defaultLocale;
          this.defaultVoice = defaultVoice;
          this.defaultEffects = defaultEffects;
          this.defaultStyle = defaultStyle;
          this.id = id;
          this.audioFileFormat = audioFileFormat;
          this.streamAudio = streamAudio;
          if (outputType == MaryDataType.get("AUDIO")) {
              if (audioFileFormat == null)
                  throw new NullPointerException("audio file format is needed for output type AUDIO");
              this.appendableAudioStream = new AppendableSequenceAudioInputStream(audioFileFormat.getFormat(), null);
          } else {
              this.appendableAudioStream = null;
          }
          //this.logger = MaryUtils.getLogger("R " + id);
          this.outputTypeParams = outputTypeParams;
          this.inputData = null;
          this.outputData = null;
          StringBuilder info =
                  new StringBuilder(
                          "New request (input type \"" + inputType.name() + "\", output type \"" + outputType.name());
          if (this.defaultVoice != null)
              info.append("\", voice \"" + this.defaultVoice.getName());
          if (this.defaultEffects != null && this.defaultEffects != "")
              info.append("\", effect \"" + this.defaultEffects);
          if (this.defaultStyle != null && this.defaultStyle != "")
              info.append("\", style \"" + this.defaultStyle);
          if (audioFileFormat != null)
              info.append("\", audio \"" + audioFileFormat.getType().toString() + "\"");
          if (streamAudio)
              info.append(", streaming");
          info.append(")");
          Log.i(Mary.LOG, info.toString());
  
          // Keep track of timing info for each module
          // (map MaryModule onto Long)
          usedModules = new LinkedHashSet<MaryModule>();
          timingInfo = new HashMap<MaryModule, Long>();
      }
  
      /**
       * Move all the boundary elements outside of paragraphs into paragraphs.
       *
       * @param rawmaryxml
       */
      private static void moveBoundariesIntoParagraphs(Document rawmaryxml) {
          if (rawmaryxml == null) {
              throw new NullPointerException("Received null rawmaryxml");
          }
          TreeWalker paraTW = ((DocumentTraversal) rawmaryxml).createTreeWalker
                  (rawmaryxml.getDocumentElement(), NodeFilter.SHOW_ELEMENT,
                          new NameNodeFilter(MaryXML.PARAGRAPH), true);
          TreeWalker tw = ((DocumentTraversal) rawmaryxml).createTreeWalker
                  (rawmaryxml.getDocumentElement(), NodeFilter.SHOW_ELEMENT,
                          new NameNodeFilter(MaryXML.PARAGRAPH, MaryXML.BOUNDARY), true);
          // Find first paragraph, to see if there are any leading boundaries
          Element firstParagraph = (Element) paraTW.nextNode();
          if (firstParagraph == null) {
              throw new NullPointerException("Document does not have a paragraph");
          }
          tw.setCurrentNode(firstParagraph);
          // Now move any leading boundaries to the left of into the first paragraph
          Element boundary = null;
          while ((boundary = (Element) tw.previousNode()) != null) {
              assert boundary.getTagName().equals(MaryXML.BOUNDARY);
              firstParagraph.insertBefore(boundary, firstParagraph.getFirstChild());
              tw.setCurrentNode(firstParagraph);
          }
          // Now, for each paragraph, all the boundaries following it are shifted inside it
          tw.setCurrentNode(firstParagraph);
          Element paragraph = firstParagraph;
          Element current = null;
          while ((current = (Element) tw.nextNode()) != null) {
              if (current.getTagName().equals(MaryXML.PARAGRAPH)) {
                  paragraph = current;
              } else { // current is a boundary
                  if (!DomUtils.hasAncestor(current, MaryXML.PARAGRAPH)) {
                      // current is a boundary outside a paragraph
                      paragraph.appendChild(current);
                  }
              }
          }
      }
  
      /**
       * For a given maryxml document, extract one paragraph element as a separate
       * document, including any parent nodes around the paragraph element.
       */
      private static MaryData extractParagraphAsMaryData(MaryData maryxml,
                                                         Element paragraph) {
          if (!maryxml.getType().isMaryXML()) {
              throw new IllegalArgumentException("Expected MaryXML data");
          }
          String rootLanguage = maryxml.getDocument().getDocumentElement().getAttribute("xml:lang");
          Document newDoc = MaryXML.newDocument();
          Element newRoot = newDoc.getDocumentElement();
          // Now import not only the paragraph itself (with substructure,
          // but also all the nodes above it (without substructure).
          Element importedInner = (Element) newDoc.importNode(paragraph, true);
          Element toImport = (Element) paragraph.getParentNode();
          while (!toImport.getTagName().equals(MaryXML.MARYXML)) {
              Element imported = (Element) newDoc.importNode(toImport, false);
              imported.appendChild(importedInner);
              importedInner = imported;
              toImport = (Element) toImport.getParentNode();
          }
          newRoot.appendChild(importedInner);
          // Determine the language of the new document.
          String language = rootLanguage;
          Element voice = (Element) DomUtils.getAncestor(paragraph, MaryXML.VOICE);
          if (voice != null) {
              if (voice.hasAttribute("xml:lang")) {
                  language = voice.getAttribute("xml:lang");
              } else if (voice.hasAttribute("name")) {
                  String name = voice.getAttribute("name");
                  Voice v = Voice.getVoice(name);
                  if (v != null && v.getLocale() != null)
                      language = MaryUtils.locale2xmllang(v.getLocale());
              }
          }
          newRoot.setAttribute("xml:lang", language);
          MaryData md = new MaryData(maryxml.getType(), MaryUtils.string2locale(language));
          Voice dVoice = maryxml.getDefaultVoice();
          if (dVoice != null) {
              md.setDefaultVoice(dVoice);
          }
          //md.setAudioEffects(maryxml.getAudioEffects());
          md.setDocument(newDoc);
          return md;
      }
  
      public MaryDataType getInputType() {
          return inputType;
      }
  
      public MaryDataType getOutputType() {
          return outputType;
      }
  
      public Locale getDefaultLocale() {
          return defaultLocale;
      }
  
      public Voice getDefaultVoice() {
          return defaultVoice;
      }
  
      public String getDefaultStyle() {
          return defaultStyle;
      }
  
      public String getDefaultEffects() {
          return defaultEffects;
      }
  
      public int getId() {
          return id;
      }
  
      public AudioFileFormat getAudioFileFormat() {
          return audioFileFormat;
      }
  
      public AppendableSequenceAudioInputStream getAudio() {
          return appendableAudioStream;
      }
  
      public boolean getStreamAudio() {
          return streamAudio;
      }
  
      /**
       * Inform this request that any further processing does not make sense.
       */
      public void abort() {
          Log.i(Mary.LOG, "Requesting abort.");
          abortRequested = true;
      }
  
      /**
       * Set the input data directly, in case it is already in the form
       * of a MaryData object.
       */
      public void setInputData(MaryData inputData) {
          if (inputData != null && inputData.getType() != inputType) {
              throw new IllegalArgumentException(
                      "Input data has wrong data type (expected "
                              + inputType.toString()
                              + ", got "
                              + inputData.getType().toString());
          }
          if (defaultVoice == null) {
              defaultVoice = Voice.getSuitableVoice(inputData);
          }
          //assert defaultVoice != null;
  
          if (inputData.getDefaultVoice() == null) {
              inputData.setDefaultVoice(defaultVoice);
          }
          inputData.setDefaultStyle(defaultStyle);
          inputData.setDefaultEffects(defaultEffects);
  
          this.inputData = inputData;
      }
  
      /**
       * Read the input data from a Reader.
       */
      public void readInputData(Reader inputReader) throws Exception {
          String inputText = FileUtils.getReaderAsString(inputReader);
          setInputData(inputText);
      }
  
      public void setInputData(String inputText) throws Exception {
          inputData = new MaryData(inputType, defaultLocale);
          inputData.setWarnClient(true); // log warnings to client
          // For RAWMARYXML, a validating parse is not possible
          // because RAWMARYXML is not tokenised, so it does not yet
          // fulfill the MaryXML Schema constraints.
          if (inputType == MaryDataType.get("RAWMARYXML")) {
              inputData.setValidating(false);
          } else if (inputType.isMaryXML()) {
              inputData.setValidating(MaryProperties.getBoolean("maryxml.validate.input"));
          }
          inputData.setData(inputText);
          if (defaultVoice == null) {
              defaultVoice = Voice.getSuitableVoice(inputData);
          }
          //assert defaultVoice != null;
          inputData.setDefaultVoice(defaultVoice);
          inputData.setDefaultStyle(defaultStyle);
          inputData.setDefaultEffects(defaultEffects);
      }
  
      /**
       * Process the input data to produce the output data.
       *
       * @see #getOutputData for direct access to the resulting output data
       * @see #writeOutputData for writing the output data to a stream
       */
      public void process() throws Exception {
          assert Mary.currentState() == Mary.STATE_RUNNING;
          long startTime = System.currentTimeMillis();
          if (inputData == null)
              throw new NullPointerException("Input data is not set.");
          if (inputType.isXMLType() && inputData.getDocument() == null)
              throw new NullPointerException("Input data contains no XML document.");
          if (inputType.isMaryXML() && !inputData.getDocument().getDocumentElement().hasAttribute("xml:lang"))
              throw new IllegalArgumentException("Mandatory attribute xml:lang is missing from maryxml document element.");
  
          NodeList inputDataList;
          MaryData rawmaryxml;
          // Is inputdata of a type that must be converted to RAWMARYXML?
          if (outputType.name().equals("PRAAT_TEXTGRID")) { // never chunk for PRAAT_TEXTGRID
              outputData = processOrLookupOneChunk(inputData, outputType, outputTypeParams);
              return;
          } else if (inputType.isTextType() && inputType.name().startsWith("TEXT")
                  || inputType.isXMLType() && !inputType.isMaryXML()) {
              // Convert to RAWMARYXML
              rawmaryxml = processOrLookupOneChunk(inputData, MaryDataType.get("RAWMARYXML"), null);
  
              //assert rawmaryxml.getDefaultVoice() != null;
              inputDataList = splitIntoChunks(rawmaryxml);
          } else if (inputType.equals(MaryDataType.get("RAWMARYXML"))) {
              rawmaryxml = inputData;
              inputDataList = splitIntoChunks(inputData);
          } else {
              // other input data types are processed as a whole
              outputData = processOrLookupOneChunk(inputData, outputType, outputTypeParams);
              //assert outputData.getDefaultVoice() != null;
              if (outputType == MaryDataType.AUDIO) {
                  assert appendableAudioStream != null;
                  appendableAudioStream.append(outputData.getAudio());
                  appendableAudioStream.doneAppending();
              }
              return;
          }
          assert rawmaryxml != null && rawmaryxml.getType().equals(MaryDataType.get("RAWMARYXML"))
                  && rawmaryxml.getDocument() != null;
          moveBoundariesIntoParagraphs(rawmaryxml.getDocument());
  
          // Now the beyond-RAWMARYXML processing:
          outputData = new MaryData(outputType, defaultLocale);
          outputData.setDefaultVoice(defaultVoice);
          outputData.setDefaultStyle(defaultStyle);
          outputData.setDefaultEffects(defaultEffects);
          if (outputType.isMaryXML()) {
              // use the input or intermediate MaryXML document
              // as the starting point for MaryXML output types,
              // in order to gradually enrich them:
              outputData.setDocument(rawmaryxml.getDocument());
          } else if (outputType.equals(MaryDataType.get("AUDIO"))) {
              outputData.setAudio(appendableAudioStream);
              outputData.setAudioFileFormat(audioFileFormat);
          }
          int len = inputDataList.getLength();
          for (int i = 0; i < len && !abortRequested; i++) {
              Element currentInputParagraph = (Element) inputDataList.item(i);
              assert currentInputParagraph.getTagName().equals(MaryXML.PARAGRAPH);
              NodeList outputNodeList = null;
              // Only process paragraph if there is any text below it:
              if (DomUtils.getPlainTextBelow(currentInputParagraph).trim().equals("")) {
                  outputNodeList = currentInputParagraph.getChildNodes();
              } else { // process "real" data:
                  MaryData oneInputData = extractParagraphAsMaryData(rawmaryxml, currentInputParagraph);
                  //assert oneInputData.getDefaultVoice() != null;
                  MaryData oneOutputData = processOrLookupOneChunk(oneInputData, outputType, outputTypeParams);
                  //assert oneOutputData.getDefaultVoice() != null;
                  if (outputType.isMaryXML()) {
                      NodeList outParagraphList = oneOutputData.getDocument().getDocumentElement().getElementsByTagName(MaryXML.PARAGRAPH);
                      // This does not hold for Tibetan:
                      //assert outParagraphList.getLength() == 1;
                      outputNodeList = outParagraphList;
                  } else { // output is not MaryXML, e.g. text or audio
                      assert outputData != null;
                      outputData.append(oneOutputData);
                  }
              }
              if (outputType.isMaryXML()) {
                  assert outputNodeList != null;
                  // And now replace the paragraph in-place:
                  DomUtils.replaceElement(currentInputParagraph, outputNodeList);
              }
          }
          long stopTime = System.currentTimeMillis();
          Log.i(Mary.LOG, "Request processed in " + (stopTime - startTime) + " ms.");
          for (MaryModule m : usedModules) {
              Log.i(Mary.LOG, "   " + m.name() + " took " + timingInfo.get(m) + " ms");
          }
          if (appendableAudioStream != null) appendableAudioStream.doneAppending();
      }
  
      /**
       * Convert the given data into the requested output type, either by looking it up in the cache
       * or by actually processing it.
       *
       * @param oneInputData the input data to convert
       * @param oneOuputType the output type to convert to
       * @param usedModules  for the record, the modules used (will be added to)
       * @param timingInfo   for the record, the processing time used for each module (will be added to)
       */
      private MaryData processOrLookupOneChunk(MaryData oneInputData, MaryDataType oneOutputType, String outputParams)
              throws Exception {
  
          Locale locale = determineLocale(oneInputData);
          assert locale != null;
  
          MaryCache cache = null;
          if (MaryProperties.getBoolean("cache")) {
              cache = MaryCache.getCache();
          }
  
          if (cache == null) {
              return processOneChunk(oneInputData, oneOutputType, outputParams, locale);
          }
  
          String inputtype = null;
          String outputtype = null;
          String localeString = null;
          String voice = null;
          String inputtext = null;
  
          // try to look up the requested result in the cache:
          inputtype = oneInputData.getType().name();
          outputtype = oneOutputType.name();
          ByteArrayOutputStream sw = new ByteArrayOutputStream();
          oneInputData.writeTo(sw);
          inputtext = new String(sw.toByteArray(), "UTF-8");
          voice = defaultVoice != null ? defaultVoice.getName() : null;
          localeString = locale.toString();
  
          if (oneOutputType.isTextType()) {
              try {
                  String outputtext = cache.lookupText(inputtype, outputtype, localeString, voice, outputParams, defaultStyle, defaultEffects, inputtext);
                  if (outputtext != null) {
                      MaryData outData = new MaryData(oneOutputType, locale);
                      ByteArrayInputStream sr = new ByteArrayInputStream(outputtext.getBytes());
                      outData.readFrom(sr);
                      sr.close();
                      outData.setDefaultVoice(defaultVoice);
                      outData.setDefaultStyle(defaultStyle);
                      outData.setDefaultEffects(defaultEffects);
                      return outData;
                  }
              } catch (Exception e) {
                  Log.w(Mary.LOG, "Problem looking up text in cache", e);
              }
          } else if (outputtype.equals("AUDIO")) {
              try {
                  byte[] wavFileData = cache.lookupAudio(inputtype, localeString, voice, outputParams, defaultStyle, defaultEffects, inputtext);
                  if (wavFileData != null) {
                      AudioInputStream ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(wavFileData));
                      MaryData outData = new MaryData(oneOutputType, locale);
                      outData.setAudio(ais);
                      outData.setAudioFileFormat(audioFileFormat);
                      return outData;
                  }
              } catch (Exception e) {
                  Log.w(Mary.LOG, "Problem looking up audio in cache", e);
              }
          } else {
              //logger.debug("Don't know how to cache data of type '"+outputtype+"'");
          }
  
          // Couldn't get it from cache, need to process
          if (oneOutputType.equals(MaryDataType.AUDIO)
                  || oneOutputType.equals(MaryDataType.REALISED_ACOUSTPARAMS)
                  || oneOutputType.equals(MaryDataType.REALISED_DURATIONS)) {
              // Special case: when we generate AUDIO, we also remember REALISED_ACOUSTPARAMS and REALISED_DURATIONS formats and vice versa
              MaryData audioData = processOneChunk(oneInputData, MaryDataType.AUDIO, outputParams, locale);
              MaryData realisedAcoustparams = processOneChunk(audioData, MaryDataType.REALISED_ACOUSTPARAMS, outputParams, locale);
              MaryData realisedDurations = processOneChunk(audioData, MaryDataType.REALISED_DURATIONS, outputParams, locale);
              insertAudioIntoCache(cache, inputtype, localeString, voice, outputParams, inputtext, audioData);
              insertTextIntoCache(cache, inputtype, MaryDataType.REALISED_ACOUSTPARAMS.name(), localeString, voice, outputParams, inputtext, realisedAcoustparams);
              insertTextIntoCache(cache, inputtype, MaryDataType.REALISED_DURATIONS.name(), localeString, voice, outputParams, inputtext, realisedDurations);
              if (oneOutputType.equals(MaryDataType.AUDIO)) return audioData;
              else if (oneOutputType.equals(MaryDataType.REALISED_ACOUSTPARAMS))
                  return realisedAcoustparams;
              return realisedDurations;
          } else { // simple, straightforward processing of output
              MaryData oneOutputData = processOneChunk(oneInputData, oneOutputType, outputParams, locale);
              // Remember the processing result in the cache
              if (oneOutputType.isTextType()) {
                  insertTextIntoCache(cache, inputtype, outputtype, localeString,
                          voice, outputParams, inputtext, oneOutputData);
              } else {
                  Log.d(Mary.LOG, "Don't know how to cache data of type '" + outputtype + "'");
              }
              return oneOutputData;
          }
      }
  
      private void insertAudioIntoCache(MaryCache cache, String inputtype,
                                        String localeString, String voice, String outputParams,
                                        String inputtext, MaryData currentData) throws IOException,
              SQLException, UnsupportedAudioFileException {
          AppendableSequenceAudioInputStream as = (AppendableSequenceAudioInputStream) currentData.getAudio();
          assert as != appendableAudioStream;
          as.doneAppending();
          ByteArrayOutputStream baos = new ByteArrayOutputStream(2 * (int) as.getFrameLength() + 100);
          AudioSystem.write(as, AudioFileFormat.Type.WAVE, baos);
          byte[] wavFileData = baos.toByteArray();
          cache.insertAudio(inputtype, localeString, voice, outputParams, defaultStyle, defaultEffects, inputtext, wavFileData);
          AudioInputStream ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(wavFileData));
          currentData.setAudio(ais);
      }
  
      private void insertTextIntoCache(MaryCache cache, String inputtype,
                                       String outputtype, String localeString, String voice,
                                       String outputParams, String inputtext, MaryData currentData) {
          try {
              ByteArrayOutputStream sw = new ByteArrayOutputStream();
              currentData.writeTo(sw);
              String outputtext = new String(sw.toByteArray(), "UTF-8");
              cache.insertText(inputtype, outputtype, localeString, voice, outputParams, defaultStyle, defaultEffects, inputtext, outputtext);
          } catch (Exception e) {
              Log.w(Mary.LOG, "Problem inserting text into cache", e);
          }
      }
  
      private MaryData processOneChunk(MaryData oneInputData,
                                       MaryDataType oneOutputType, String outputParams, Locale locale)
              throws Exception,
              TransformerException, IOException {
          Log.d(Mary.LOG, "Determining which modules to usedddd");
          List<MaryModule> neededModules = ModuleRegistry.modulesRequiredForProcessing(oneInputData.getType(), oneOutputType, locale, oneInputData.getDefaultVoice());
          // Now neededModules contains references to the needed modules,
          // in the order in which they are to process the data.
          if (neededModules == null) {
              // The modules we have cannot be combined such that
              // the outputType can be generated from the inputData type.
              String message = "No known way of generating output from input -- " + "no processing path through modules.";
              throw new UnsupportedOperationException(message);
          }
          usedModules.addAll(neededModules);
          Log.i(Mary.LOG, "Handling request using the following modules:");
          for (MaryModule m : neededModules) {
              Log.i(Mary.LOG, "- " + m.name() + " (" + m.getClass().getName() + ")");
          }
          MaryData currentData = oneInputData;
  
          for (MaryModule m : neededModules) {
              if (abortRequested)
                  break;
              if (m.getState() == MaryModule.MODULE_OFFLINE) {
                  // This should happen only in command line mode:
                  assert MaryProperties.needProperty("server").compareTo("commandline") == 0;
                  Log.i(Mary.LOG, "Starting module " + m.name());
                  m.startup();
                  assert m.getState() == MaryModule.MODULE_RUNNING;
              }
              long moduleStartTime = System.currentTimeMillis();
              // Let synthesis know which audio format to produce:
              // (this isn't nice -- instead, we could add a reference
              // to the Request to each MaryData, and look up request-specific
              // settings such as default voice and audio file format type
              // from where it is required.)
              if (m.outputType() == MaryDataType.get("AUDIO")) {
                  currentData.setAudioFileFormat(audioFileFormat);
                  currentData.setAudio(new AppendableSequenceAudioInputStream(audioFileFormat.getFormat(), null));
              }
              // TODO: The following hack makes sure that the Synthesis module gets outputParams. Make this more general and robust.
              if (m.outputType() == oneOutputType || m.outputType() == MaryDataType.AUDIO) {
                  currentData.setOutputParams(outputParams);
              }
  //            if (logger.getEffectiveLevel().equals(Level.DEBUG)
  //                && (currentData.getType().isTextType() || currentData.getType().isXMLType())) {
  //                Log.d(Mary.LOG,"Handing the following data to the next module:");
  //                ByteArrayOutputStream dummy = new ByteArrayOutputStream();
  //                currentData.writeTo(dummy);
  //                // side effect: writeTo() writes to log if debug
  //            }
              Log.i(Mary.LOG, "Next module: " + m.name());
              MaryData outData = null;
              try {
                  outData = m.process(currentData);
              } catch (Exception e) {
                  Log.d(Mary.LOG, e.toString());
                  throw new Exception("Module " + m.name() + ": Problem processing the data.", e);
              }
  
              if (outData == null) {
                  throw new NullPointerException("Module " + m.name() + " returned null. This should not happen.");
              }
              outData.setDefaultVoice(defaultVoice);
              outData.setDefaultStyle(defaultStyle);
              outData.setDefaultEffects(defaultEffects);
  
              currentData = outData;
              long moduleStopTime = System.currentTimeMillis();
              long delta = moduleStopTime - moduleStartTime;
              Long soFar = timingInfo.get(m);
              if (soFar != null)
                  timingInfo.put(m, new Long(soFar.longValue() + delta));
              else
                  timingInfo.put(m, new Long(delta));
              if (MaryRuntimeUtils.veryLowMemoryCondition()) {
                  Log.i(Mary.LOG, "Very low memory condition detected (only " + MaryUtils.availableMemory() + " bytes left). Triggering garbage collection.");
                  Runtime.getRuntime().gc();
                  Log.i(Mary.LOG, "After garbage collection: " + MaryUtils.availableMemory() + " bytes available.");
              }
          }
          if (currentData.getType() == MaryDataType.AUDIO) {
              AudioInputStream ais = currentData.getAudio();
              assert ais != null;
              assert ais instanceof AppendableSequenceAudioInputStream;
              ((AppendableSequenceAudioInputStream) ais).doneAppending();
          }
          return currentData;
      }
  
      /**
       * Split the entire rawmaryxml document into individual paragraph elements.
       * Any text not enclosed by a paragraph in the input will be enclosed
       * by a new paragraph element, which is then included in the return.
       *
       * @param rawmaryxml the maryxml document to split into paragraph chunks;
       *                   this input document will be modified!
       * @return a nodelist containing the paragraph elements in the modified rawmaryxml document.
       */
      private NodeList splitIntoChunks(MaryData rawmaryxml) {
          // TODO: replace this with code that combines this and the splitting functionality of Synthesis: one chunk is one locale and one voice
          if (rawmaryxml == null)
              throw new NullPointerException("Received null data");
          if (rawmaryxml.getType() != MaryDataType.get("RAWMARYXML"))
              throw new IllegalArgumentException("Expected data of type RAWMARYXML, got " + rawmaryxml.getType());
  //        if (logger.getEffectiveLevel().equals(Level.DEBUG)) {
  //                Log.d(Mary.LOG,"Now splitting the following RAWMARYXML data into chunks:");
  //                ByteArrayOutputStream dummy = new ByteArrayOutputStream();
  //                try {
  //                    rawmaryxml.writeTo(dummy);
  //                } catch (Exception ex) {
  //                    Log.d(Mary.LOG,ex);
  //                }
  //                // side effect: writeTo() writes to log if debug
  //            }
  
          Document doc = rawmaryxml.getDocument();
          Element root = doc.getDocumentElement();
          // First, make sure there are no "dangling" text nodes
          // or empty elements
          // that are not enclosed by any paragraph elements.
          TreeWalker tw = ((DocumentTraversal) doc).
                  createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
          // TODO: clean up the following code wrt checks for non-TEXT nodes
          Node firstNode = null;
          Node lastNode = null;
          Node currentNode = null;
          while ((currentNode = tw.nextNode()) != null) {
              // Ignore whitespace-only nodes:
              if (currentNode.getNodeType() == Node.TEXT_NODE) {
                  Text currentTextNode = (Text) currentNode;
                  if (currentTextNode.getData().trim().length() == 0)
                      continue;
              }
              if (!DomUtils.hasAncestor(currentNode, MaryXML.PARAGRAPH)) {
                  // Outside paragraphs:
                  if (firstNode == null)
                      firstNode = currentNode;
                  lastNode = currentNode;
              } else {
                  // a node below a paragraph -- enclose any previous text
                  if (firstNode != null) { // have something to enclose
                      String first;
                      if (firstNode.getNodeType() == Node.TEXT_NODE) {
                          first = ((Text) firstNode).getData();
                      } else {
                          first = firstNode.getNodeName();
                      }
                      String last;
                      if (lastNode.getNodeType() == Node.TEXT_NODE) {
                          last = ((Text) lastNode).getData();
                      } else {
                          last = lastNode.getNodeName();
                      }
                      Log.d(Mary.LOG, "Found text node below paragraph; enclosing from '"
                              + first + "' to '" + last + "'");
                      DomUtils.encloseNodesWithNewElement(firstNode, lastNode, MaryXML.PARAGRAPH);
                      firstNode = null;
                      lastNode = null;
                  }
              }
          }
          // any leftovers to enclose?
          if (firstNode != null) { // have something to enclose
              String first;
              if (firstNode.getNodeType() == Node.TEXT_NODE) {
                  first = ((Text) firstNode).getData();
              } else {
                  first = firstNode.getNodeName();
              }
              String last;
              if (lastNode.getNodeType() == Node.TEXT_NODE) {
                  last = ((Text) lastNode).getData();
              } else {
                  last = lastNode.getNodeName();
              }
              Log.d(Mary.LOG, "Found text node below paragraph; enclosing from '"
                      + first + "' to '" + last + "'");
              DomUtils.encloseNodesWithNewElement(firstNode, lastNode, MaryXML.PARAGRAPH);
          }
          return doc.getElementsByTagName(MaryXML.PARAGRAPH);
  /*
          // Second, for each paragraph, create a separate MaryData.
          tw = ((DocumentTraversal)doc).createTreeWalker(root, NodeFilter.SHOW_ELEMENT,
                  new NameNodeFilter(MaryXML.PARAGRAPH), false);
          Element paragraph = null;
          while ((paragraph = (Element) tw.nextNode()) != null) {
              MaryData md = extractParagraphAsMaryData(rawmaryxml, paragraph);
              result.add(md);
  
              if (logger.getEffectiveLevel().equals(Level.DEBUG)) {
                  logger.debug("Created chunk:");
                  ByteArrayOutputStream dummy = new ByteArrayOutputStream();
                  try {
                      md.writeTo(dummy);
                  } catch (Exception ex) {
                      logger.debug(ex);
                  }
                  // side effect: writeTo() writes to log if debug
              }
  
          }
          return result;
  */
      }
  
      /**
       * For a given instance of MaryData, determine the locale -- either from the
       * data type, or, if it is not specified there, from the XML document root
       * element's attribute "xml:lang".
       */
      private Locale determineLocale(MaryData data) {
          Locale locale = null;
          if (data.getType().isXMLType()) {
              // We can determine the locale if the document element
              // has an xml:lang attribute.
              Document doc = data.getDocument();
              if (doc != null) {
                  Element docEl = doc.getDocumentElement();
                  if (docEl != null) {
                      String langCode = docEl.getAttribute("xml:lang");
                      if (!langCode.equals("")) {
                          locale = MaryUtils.string2locale(langCode);
                      }
                  }
              }
          }
          assert defaultLocale != null;
          return locale != null ? locale : defaultLocale;
      }
  
      /**
       * Direct access to the output data.
       */
      public MaryData getOutputData() {
          return outputData;
      }
  
      /**
       * Write the output data to the specified OutputStream.
       */
      public void writeOutputData(OutputStream outputStream) throws Exception {
          if (outputData == null) {
              throw new NullPointerException("No output data -- did process() succeed?");
          }
          if (outputStream == null)
              throw new NullPointerException("cannot write to null output stream");
          // Safety net: if the output is not written within a certain amount of
          // time, give up. This prevents our thread from being locked forever if an
          // output deadlock occurs (happened very rarely on Java 1.4.2beta).
          final OutputStream os = outputStream;
          Timer timer = new Timer();
          TimerTask timerTask = new TimerTask() {
              @Override
              public void run() {
                  Log.w(Mary.LOG, "Timeout occurred while writing output. Forcefully closing output stream.");
                  try {
                      os.close();
                  } catch (IOException ioe) {
                      Log.w(Mary.LOG, ioe);
                  }
              }
          };
          int timeout = MaryProperties.getInteger("modules.timeout", 10000);
          if (outputType.equals(MaryDataType.get("AUDIO"))) {
              // This means either a lot of data (for WAVE etc.) or a lot of processing
              // effort (for MP3), so allow for a lot of time:
              timeout *= 5;
          }
          timer.schedule(timerTask, timeout);
          try {
              outputData.writeTo(os);
          } catch (Exception e) {
              timer.cancel();
              throw e;
          }
          timer.cancel();
      }
  
  
  }
  
  