Example #1
0
  /**
   * Returns a string built using a particular grammar. Single-quotes strings are counted as literal
   * strings, whereas all other strings appearing in the grammar require substitution with the
   * corresponding value from the extraparams hashmap.
   */
  private String buildGrammar(String grammar, Map<String, String> extraparams) {
    StringBuilder outboundstring = new StringBuilder();
    String[] fields = Pattern.compile("\\s+").split(grammar);
    for (int i = 0; i < fields.length; i++) {
      if (fields[i].substring(0, 1).equals("'")) {
        outboundstring.append(fields[i].substring(1, fields[i].length() - 1));
      } else {
        outboundstring.append(extraparams.get(fields[i]));
      }
    }

    return outboundstring.toString();
  }
Example #2
0
 /**
  * Returns the value of a specified fieldname from the specified hashmap and returns an integer
  * value or throws an exception if the value is not an integer
  */
 private int getIntValue(String fieldname, Map<String, String> extraparams) {
   Matcher checkint = Pattern.compile("^\\d+$").matcher(fieldname);
   int rv;
   if (checkint.matches()) {
     rv = Integer.parseInt(fieldname);
   } else {
     if (extraparams.containsKey(fieldname)) {
       rv = Integer.parseInt(extraparams.get(fieldname));
     } else {
       rv = -1;
       throw new TDTException(
           "No integer value for " + fieldname + " can be found - check extraparams");
     }
   }
   return rv;
 }
Example #3
0
  /**
   * Adds additional entries to the extraparams hashmap by processing various rules defined in the
   * TDT definition files. Typically used for string processing functions, lookup in tables,
   * calculation of check digits etc.
   */
  private void processRules(Map<String, String> extraparams, Rule tdtrule) {
    String tdtfunction = tdtrule.getFunction();
    int openbracket = tdtfunction.indexOf("(");
    assert openbracket != -1;
    String params = tdtfunction.substring(openbracket + 1, tdtfunction.length() - 1);
    String rulename = tdtfunction.substring(0, openbracket);
    String[] parameter = params.split(",");
    String newfieldname = tdtrule.getNewFieldName();
    // System.out.println(tdtfunction + " " + parameter[0] + " " + extraparams.get(parameter[0]));
    /**
     * Stores in the hashmap extraparams the value obtained from a lookup in a specified XML table.
     *
     * <p>The first parameter is the given value already known. This is denoted as $1 in the
     * corresponding XPath expression
     *
     * <p>The second parameter is the string filename of the table which must be present in the
     * auxiliary subdirectory
     *
     * <p>The third parameter is the column in which the supplied input value should be sought
     *
     * <p>The fourth parameter is the column whose value should be read for the corresponding row,
     * in order to obtain the result of the lookup.
     *
     * <p>The rule in the definition file may contain an XPath expression and a URL where the table
     * may be obtained.
     */
    if (rulename.equals("TABLELOOKUP")) {
      // parameter[0] is given value
      // parameter[1] is table
      // parameter[2] is input column supplied
      // parameter[3] is output column required
      assert parameter.length == 4 : "incorrect number of parameters to tablelookup " + params;
      if (parameter[1].equals("tdt64bitcpi")) {
        String s = extraparams.get(parameter[0]);
        assert s != null : tdtfunction + " when " + parameter[0] + " is null";
        String t = gs1cpi.get(s);
        assert t != null : "gs1cpi[" + s + "] is null";
        assert newfieldname != null;
        extraparams.put(newfieldname, t);
        // extraparams.put(newfieldname, gs1cpi.get(extraparams.get(parameter[0])));
      } else { // JPB! the following is untested
        String tdtxpath = tdtrule.getTableXPath();
        String tdttableurl = tdtrule.getTableURL();
        String tdtxpathsub = tdtxpath.replaceAll("\\$1", extraparams.get(parameter[0]));
        extraparams.put(newfieldname, xpathlookup("ManagerTranslation.xml", tdtxpathsub));
      }
    }

    /**
     * Stores the length of the specified string under the new fieldname specified by the
     * corresponding rule of the definition file.
     */
    if (rulename.equals("LENGTH")) {
      assert extraparams.get(parameter[0]) != null
          : tdtfunction + " when " + parameter[0] + " is null";
      if (extraparams.get(parameter[0]) != null) {
        extraparams.put(newfieldname, Integer.toString(extraparams.get(parameter[0]).length()));
      }
    }

    /**
     * Stores a GS1 check digit in the extraparams hashmap, keyed under the new fieldname specified
     * by the corresponding rule of the definition file.
     */
    if (rulename.equals("GS1CHECKSUM")) {
      assert extraparams.get(parameter[0]) != null
          : tdtfunction + " when " + parameter[0] + " is null";
      if (extraparams.get(parameter[0]) != null) {
        extraparams.put(newfieldname, gs1checksum(extraparams.get(parameter[0])));
      }
    }

    /**
     * Obtains a substring of the string provided as the first parameter. If only a single second
     * parameter is specified, then this is considered as the start index and all characters from
     * the start index onwards are stored in the extraparams hashmap under the key named
     * 'newfieldname' in the corresponding rule of the definition file. If a second and third
     * parameter are specified, then the second parameter is the start index and the third is the
     * length of characters required. A substring consisting characters from the start index up to
     * the required length of characters is stored in the extraparams hashmap, keyed under the new
     * fieldname specified by the corresponding rule of the defintion file.
     */
    if (rulename.equals("SUBSTR")) {
      assert extraparams.get(parameter[0]) != null
          : tdtfunction + " when " + parameter[0] + " is null";
      if (parameter.length == 2) {
        if (extraparams.get(parameter[0]) != null) {
          int start = getIntValue(parameter[1], extraparams);
          if (start >= 0) {
            extraparams.put(newfieldname, extraparams.get(parameter[0]).substring(start));
          }
        }
      }
      if (parameter.length
          == 3) { // need to check that this variation is correct - c.f. Perl substr
        assert extraparams.get(parameter[0]) != null
            : tdtfunction + " when " + parameter[0] + " is null";
        if (extraparams.get(parameter[0]) != null) {
          int start = getIntValue(parameter[1], extraparams);
          int end = getIntValue(parameter[2], extraparams);
          if ((start >= 0) && (end >= 0)) {
            extraparams.put(
                newfieldname, extraparams.get(parameter[0]).substring(start, start + end));
          }
        }
      }
    }

    /**
     * Concatenates specified string parameters together. Literal values must be enclosed within
     * single or double quotes or consist of unquoted digits. Other unquoted strings are considered
     * as fieldnames and the corresponding value from the extraparams hashmap are inserted. The
     * result of the concatenation (and substitution) of the strings is stored as a new entry in the
     * extraparams hashmap, keyed under the new fieldname specified by the rule.
     */
    if (rulename.equals("CONCAT")) {
      StringBuilder buffer = new StringBuilder();
      for (int p1 = 0; p1 < parameter.length; p1++) {
        Matcher matcher = Pattern.compile("\"(.*?)\"|'(.*?)'|[0-9]").matcher(parameter[p1]);
        if (matcher.matches()) {
          buffer.append(parameter[p1]);
        } else {
          assert extraparams.get(parameter[p1]) != null
              : tdtfunction + " when " + parameter[p1] + " is null";
          if (extraparams.get(parameter[p1]) != null) {
            buffer.append(extraparams.get(parameter[p1]));
          }
        }
      }
      extraparams.put(newfieldname, buffer.toString());
    }
  }
Example #4
0
  /** convert from a particular scheme / level */
  private String convertLevel(
      Scheme tdtscheme,
      Level tdtlevel,
      String input,
      Map<String, String> inputParameters,
      LevelTypeList outboundlevel) {

    String outboundstring;
    Map<String, String> extraparams =
        //	    new NoisyMap
        (new HashMap<String, String>(inputParameters));

    // get the scheme's option key, which is the name of a
    // parameter whose value is matched to the option key of the
    // level.

    String optionkey = tdtscheme.getOptionKey();
    String optionValue = extraparams.get(optionkey);
    // the name of a parameter which allows the appropriate option
    // to be selected

    // now consider the various options within the scheme and
    // level for each option element inside the level, check
    // whether the pattern attribute matches as a regular
    // expression

    String matchingOptionKey = null;
    Option matchingOption = null;
    Matcher prefixMatcher = null;
    for (Enumeration e = tdtlevel.enumerateOption(); e.hasMoreElements(); ) {
      Option opt = (Option) e.nextElement();
      if (optionValue == null || optionValue.equals(opt.getOptionKey())) {
        // possible match

        Matcher matcher = Pattern.compile(opt.getPattern()).matcher(input);
        if (matcher.matches()) {
          if (prefixMatcher != null) throw new TDTException("Multiple patterns matched");
          prefixMatcher = matcher;
          matchingOptionKey = opt.getOptionKey();
          matchingOption = opt;
        }
      }
    }
    if (prefixMatcher == null) throw new TDTException("No patterns matched");

    optionValue = matchingOptionKey;

    for (Enumeration e = matchingOption.enumerateField(); e.hasMoreElements(); ) {
      Field field = (Field) e.nextElement();
      int seq = field.getSeq();

      String strfieldname = field.getName();
      int fieldlength = field.getLength();
      String strfieldvalue = prefixMatcher.group(seq);
      // System.out.println("   processing field " + strfieldname + " = '" + strfieldvalue + "'");

      if (field.getCompaction() == null) {
        // if compaction is null, treat field as an integer

        if (field.getCharacterSet() != null) { // if the character set is specified
          Matcher charsetmatcher =
              Pattern.compile("^" + field.getCharacterSet() + "$").matcher(strfieldvalue);
          if (!charsetmatcher.matches()) {
            throw new TDTException(
                "field "
                    + strfieldname
                    + " ("
                    + strfieldvalue
                    + ") does not conform to the allowed character set ("
                    + field.getCharacterSet()
                    + ") ");
          }
        }

        BigInteger bigvalue = null;

        if (tdtlevel.getType() == LevelTypeList.BINARY) { // if the input was BINARY
          bigvalue = new BigInteger(strfieldvalue, 2);
          extraparams.put(strfieldname, bigvalue.toString());
        } else {
          if (field.getDecimalMinimum() != null || field.getDecimalMaximum() != null)
            bigvalue = new BigInteger(strfieldvalue);
          extraparams.put(strfieldname, strfieldvalue);
        }

        if (field.getDecimalMinimum() != null) { // if the decimal minimum is specified
          BigInteger bigmin = new BigInteger(field.getDecimalMinimum());

          if (bigvalue.compareTo(bigmin)
              == -1) { // throw an exception if the field value is less than the decimal minimum
            throw new TDTException(
                "field "
                    + strfieldname
                    + " ("
                    + bigvalue
                    + ") is less than DecimalMinimum ("
                    + field.getDecimalMinimum()
                    + ") allowed");
          }
        }

        if (field.getDecimalMaximum() != null) { // if the decimal maximum is specified
          BigInteger bigmax = new BigInteger(field.getDecimalMaximum());

          if (bigvalue.compareTo(bigmax)
              == 1) { // throw an excpetion if the field value is greater than the decimal maximum
            throw new TDTException(
                "field "
                    + strfieldname
                    + " ("
                    + bigvalue
                    + ") is greater than DecimalMaximum ("
                    + field.getDecimalMaximum()
                    + ") allowed");
          }
        }

        // after extracting the field, it may be necessary to pad it.

        padField(extraparams, field);

      } else {
        // compaction is specified - interpret binary as a string value using a truncated byte per
        // character

        CompactionMethodList compaction = field.getCompaction();
        PadDirectionList padDir = field.getPadDir();
        String padchar = field.getPadChar();
        String s;
        if (compaction == CompactionMethodList.VALUE_5)
          // "5-bit"
          s = bin2uppercasefive(strfieldvalue);
        else if (compaction == CompactionMethodList.VALUE_4)
          // 6-bit
          s = bin2alphanumsix(strfieldvalue);
        else if (compaction == CompactionMethodList.VALUE_3)
          // 7-bit
          s = bin2asciiseven(strfieldvalue);
        else if (compaction == CompactionMethodList.VALUE_2)
          // 8-bit
          s = bin2bytestring(strfieldvalue);
        else throw new Error("unsupported compaction method " + compaction);
        extraparams.put(strfieldname, stripPadChar(s, padDir, padchar));
      }
    } // for each field;

    /**
     * the EXTRACT rules are performed after parsing the input, in order to determine additional
     * fields that are to be derived from the fields obtained by the pattern match process
     */
    int seq = 0;
    for (Enumeration e = tdtlevel.enumerateRule(); e.hasMoreElements(); ) {
      Rule tdtrule = (Rule) e.nextElement();
      if (tdtrule.getType() == ModeList.EXTRACT) {
        assert seq < tdtrule.getSeq() : "Rule out of sequence order";
        seq = tdtrule.getSeq();
        processRules(extraparams, tdtrule);
      }
    }

    /**
     * Now we need to consider the corresponding output level and output option. The scheme must
     * remain the same, as must the value of optionkey (to select the corresponding option element
     * nested within the required outbound level)
     */
    Level tdtoutlevel = findLevel(tdtscheme, outboundlevel);
    Option tdtoutoption = findOption(tdtoutlevel, optionValue);

    /**
     * the FORMAT rules are performed before formatting the output, in order to determine additional
     * fields that are required for preparation of the outbound format
     */
    seq = 0;
    for (Enumeration e = tdtoutlevel.enumerateRule(); e.hasMoreElements(); ) {
      Rule tdtrule = (Rule) e.nextElement();
      if (tdtrule.getType() == ModeList.FORMAT) {
        assert seq < tdtrule.getSeq() : "Rule out of sequence order";
        seq = tdtrule.getSeq();
        processRules(extraparams, tdtrule);
      }
    }

    /**
     * Now we need to ensure that all fields required for the outbound grammar are suitably padded
     * etc. processPadding takes care of firstly padding the non-binary fields if padChar and
     * padDir, length are specified then (if necessary) converting to binary and padding the binary
     * representation to the left with zeros if the bit string is has fewer bits than the bitLength
     * attribute specifies. N.B. TDTv1.1 will be more specific about bit-level padding rather than
     * assuming that it is always to the left with the zero bit.
     */

    // System.out.println(" prior to processPadding, " + extraparams);
    for (Enumeration e = tdtoutoption.enumerateField(); e.hasMoreElements(); ) {
      Field field = (Field) e.nextElement();
      // processPadding(extraparams, field, outboundlevel, tdtoutoption);

      padField(extraparams, field);
      if (outboundlevel == LevelTypeList.BINARY) binaryPadding(extraparams, field);
    }

    /**
     * Construct the output from the specified grammar (in ABNF format) together with the field
     * values stored in inputparams
     */
    outboundstring = buildGrammar(tdtoutoption.getGrammar(), extraparams);

    // System.out.println("final extraparams = " + extraparams);
    // System.out.println("returned " + outboundstring);
    return outboundstring;
  }
Example #5
0
@WebService(
    endpointInterface = "uk.ac.open.lts.webmaths.image.MathsImagePort",
    targetNamespace = "http://ns.open.ac.uk/lts/vle/filter_maths/",
    serviceName = "MathsImage",
    portName = "MathsImagePort")
public class WebMathsImage extends WebMathsService implements MathsImagePort {
  private static boolean SHOWPERFORMANCE = false;

  private Graphics2D context;

  /** Initialises the graphics context (first time). */
  private void initContext() {
    if (context == null) {
      // Create graphics context used for laying out equation
      BufferedImage silly = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
      context = silly.createGraphics();
    }
  }

  private static final Pattern REGEX_RGB =
      Pattern.compile("^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$");

  private static final byte[] EMPTY = new byte[0];

  @Override
  public MathsImageReturn getImage(MathsImageParams params) {
    long start = System.currentTimeMillis();
    MathsImageReturn result = new MathsImageReturn();
    result.setOk(false);
    result.setImage(EMPTY);
    result.setError("");

    try {
      // Parse XML
      Document mathml = parseMathml(params, result, start);
      if (mathml == null) {
        return result;
      }
      return getImage(params, mathml, result, start);
    } catch (Throwable t) {
      result.setError("MathML unexpected error - " + t.getMessage());
      t.printStackTrace();
      return result;
    }
  }

  @Override
  public MathsEpsReturn getEps(MathsEpsParams params) {
    long start = System.currentTimeMillis();
    MathsEpsReturn result = new MathsEpsReturn();
    result.setOk(false);
    result.setEps(EMPTY);
    result.setError("");

    try {
      // Parse XML
      Document mathml = parseMathml(params, result, start);
      if (mathml == null) {
        return result;
      }
      return getEps(params, mathml, result, start);
    } catch (Throwable t) {
      result.setError("MathML unexpected error - " + t.getMessage());
      t.printStackTrace();
      return result;
    }
  }

  /**
   * Parses mathml from the input into a DOM document. Split out so that subclass can call.
   *
   * @param params Parameters including MathML
   * @param result Blank result object
   * @param start Request start time (milliseconds since epoch)
   * @return Document or null if failed (in which case should return result)
   */
  protected Document parseMathml(MathsImageParams params, MathsImageReturn result, long start)
      throws Exception {
    try {
      Document mathml = parseMathml(params.getMathml());
      if (SHOWPERFORMANCE) {
        System.err.println("Parse DOM: " + (System.currentTimeMillis() - start));
      }
      return mathml;
    } catch (SAXParseException e) {
      int line = e.getLineNumber(), col = e.getColumnNumber();
      result.setError("MathML parse error at " + line + ":" + col + " - " + e.getMessage());
      return null;
    }
  }

  /**
   * Converts a colour from #rrggbb to Java Color object.
   *
   * @param rgb String e.g. #ff00ee
   * @return Colour
   * @throws IllegalArgumentException If colour string is not valid
   */
  protected Color convertRgb(String rgb) throws IllegalArgumentException {
    // Get colour parameter
    Matcher m = REGEX_RGB.matcher(rgb);
    if (!m.matches()) {
      throw new IllegalArgumentException(
          "MathML invalid colour '" + rgb + "'; expected #rrggbb (lower-case)");
    }
    return new Color(
        Integer.parseInt(m.group(1), 16),
        Integer.parseInt(m.group(2), 16),
        Integer.parseInt(m.group(3), 16));
  }

  /**
   * Does real work; separated out so it can be efficiently called from subclass.
   *
   * @param params Request parameters
   * @param doc MathML as DOM document
   * @param result Initialised result object with blank fields
   * @param start Start time of request (milliseconds since epoch)
   * @return Return result
   * @throws IOException Any error creating image file
   */
  protected MathsImageReturn getImage(
      MathsImageParams params, Document doc, MathsImageReturn result, long start)
      throws IOException {
    initContext();

    Color fg;
    try {
      fg = convertRgb(params.getRgb());
    } catch (IllegalArgumentException e) {
      result.setError(e.getMessage());
      return result;
    }

    if (SHOWPERFORMANCE) {
      System.err.println("Setup: " + (System.currentTimeMillis() - start));
    }

    // Parse XML to JEuclid document
    DocumentElement document;
    preprocessForJEuclid(doc);
    document = DOMBuilder.getInstance().createJeuclidDom(doc);
    if (SHOWPERFORMANCE) {
      System.err.println("Parse: " + (System.currentTimeMillis() - start));
    }

    // Set layout options
    LayoutContextImpl layout = new LayoutContextImpl(LayoutContextImpl.getDefaultLayoutContext());
    layout.setParameter(Parameter.ANTIALIAS, Boolean.TRUE);
    // This size is hardcoded to go well with our default text size
    // and be one of the sizes that doesn't look too horrible.
    layout.setParameter(Parameter.MATHSIZE, params.getSize() * 15f);
    layout.setParameter(Parameter.SCRIPTSIZEMULTIPLIER, 0.86667f);
    layout.setParameter(Parameter.MATHCOLOR, fg);

    // These fonts are included with the JEuclid build so ought to work
    layout.setParameter(
        Parameter.FONTS_SERIF, Arrays.asList(new String[] {"DejaVu Serif", "Quivira"}));
    layout.setParameter(Parameter.FONTS_SCRIPT, Arrays.asList(new String[] {"Allura"}));
    layout.setParameter(Parameter.FONTS_SANSSERIF, "DejaVu Sans");
    layout.setParameter(Parameter.FONTS_MONOSPACED, "DejaVu Sans Mono");

    if (SHOWPERFORMANCE) {
      System.err.println("Layout: " + (System.currentTimeMillis() - start));
    }

    // Layout equation
    JEuclidView view = new JEuclidView(document, layout, context);
    float ascent = view.getAscentHeight();
    float descent = view.getDescentHeight();
    float width = view.getWidth();
    if (SHOWPERFORMANCE) {
      System.err.println("View: " + (System.currentTimeMillis() - start));
    }

    // Create new image to hold it
    int pixelWidth = Math.max(1, (int) Math.ceil(width)),
        pixelHeight = Math.max(1, (int) Math.ceil(ascent + descent));

    BufferedImage image = new BufferedImage(pixelWidth, pixelHeight, BufferedImage.TYPE_INT_ARGB);
    if (SHOWPERFORMANCE) {
      System.err.println("Image: " + (System.currentTimeMillis() - start));
    }
    view.draw(image.createGraphics(), 0, ascent);
    if (SHOWPERFORMANCE) {
      System.err.println("Draw: " + (System.currentTimeMillis() - start));
    }
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    ImageIO.write(image, "png", output);
    if (SHOWPERFORMANCE) {
      System.err.println("PNG: " + (System.currentTimeMillis() - start));
    }

    // Save results
    result.setImage(output.toByteArray());
    result.setBaseline(BigInteger.valueOf(image.getHeight() - (int) Math.round(ascent)));
    result.setOk(true);

    if (SHOWPERFORMANCE) {
      System.err.println("End: " + (System.currentTimeMillis() - start));
    }
    return result;
  }

  /**
   * Carries out preprocessing that makes JEuclid handle the document better.
   *
   * @param doc Document
   */
  static void preprocessForJEuclid(Document doc) {
    // underbrace and overbrace
    NodeList list = doc.getElementsByTagName("mo");
    for (int i = 0; i < list.getLength(); i++) {
      Element mo = (Element) list.item(i);
      String parentName = ((Element) mo.getParentNode()).getTagName();
      if (parentName == null) {
        continue;
      }
      if (parentName.equals("munder") && isTextChild(mo, "\ufe38")) {
        mo.setAttribute("stretchy", "true");
        mo.removeChild(mo.getFirstChild());
        mo.appendChild(doc.createTextNode("\u23df"));
      } else if (parentName.equals("mover") && isTextChild(mo, "\ufe37")) {
        mo.setAttribute("stretchy", "true");
        mo.removeChild(mo.getFirstChild());
        mo.appendChild(doc.createTextNode("\u23de"));
      }
    }

    // menclose for long division doesn't allow enough top padding. Oh, and
    // <mpadded> isn't implemented. And there isn't enough padding to left of
    // the bar either. Solve by adding an <mover> with just an <mspace> over#
    // the longdiv, contained within an mrow that adds a <mspace> before it.
    list = doc.getElementsByTagName("menclose");
    for (int i = 0; i < list.getLength(); i++) {
      Element menclose = (Element) list.item(i);
      // Only for longdiv
      if (!"longdiv".equals(menclose.getAttribute("notation"))) {
        continue;
      }
      Element mrow = doc.createElementNS(WebMathsService.NS, "mrow");
      Element mover = doc.createElementNS(WebMathsService.NS, "mover");
      Element mspace = doc.createElementNS(WebMathsService.NS, "mspace");
      Element mspaceW = doc.createElementNS(WebMathsService.NS, "mspace");
      boolean previousElement = false;
      for (Node previous = menclose.getPreviousSibling();
          previous != null;
          previous = previous.getPreviousSibling()) {
        if (previous.getNodeType() == Node.ELEMENT_NODE) {
          previousElement = true;
          break;
        }
      }
      if (previousElement) {
        mspaceW.setAttribute("width", "4px");
      }
      menclose.getParentNode().insertBefore(mrow, menclose);
      menclose.getParentNode().removeChild(menclose);
      mrow.appendChild(mspaceW);
      mrow.appendChild(mover);
      mover.appendChild(menclose);
      mover.appendChild(mspace);
    }
  }

  private static boolean isTextChild(Node parent, String text) {
    NodeList list = parent.getChildNodes();
    if (list.getLength() != 1) {
      return false;
    }
    Node child = list.item(0);
    if (child.getNodeType() != Node.TEXT_NODE) {
      return false;
    }
    return child.getNodeValue().equals(text);
  }

  /**
   * Parses mathml from the input into a DOM document. Split out so that subclass can call.
   *
   * @param params Parameters including MathML
   * @param result Blank result object
   * @param start Request start time (milliseconds since epoch)
   * @return Document or null if failed (in which case should return result)
   */
  protected Document parseMathml(MathsEpsParams params, MathsEpsReturn result, long start)
      throws Exception {
    try {
      Document mathml = parseMathml(params.getMathml());
      if (SHOWPERFORMANCE) {
        System.err.println("Parse DOM: " + (System.currentTimeMillis() - start));
      }
      return mathml;
    } catch (SAXParseException e) {
      int line = e.getLineNumber(), col = e.getColumnNumber();
      result.setError("MathML parse error at " + line + ":" + col + " - " + e.getMessage());
      return null;
    }
  }

  /**
   * Does real work; separated out so it can be efficiently called from subclass.
   *
   * @param params Request parameters
   * @param doc MathML as DOM document
   * @param result Initialised result object with blank fields
   * @param start Start time of request (milliseconds since epoch)
   * @return Return result
   * @throws IOException Any error creating image file
   */
  protected MathsEpsReturn getEps(
      MathsEpsParams params, Document doc, MathsEpsReturn result, long start) throws IOException {
    result.setError("EPS output from JEuclid not supported");
    return result;
  }
}