/** * 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(); }
/** * 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; }
/** * 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()); } }
/** 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; }
@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; } }