/* * The JTS Topology Suite is a collection of Java classes that * implement the fundamental operations required to validate a given * geo-spatial data set to a known topological specification. * * Copyright (C) 2001 Vivid Solutions * * This library 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; either * version 2.1 of the License, or (at your option) any later version. * * This library 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. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.revolsys.geometry.test.testrunner; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Vector; import org.jdom.Attribute; import org.jdom.DataConversionException; import org.jdom.Document; import org.jdom.Element; import org.jdom.input.SAXBuilder; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.test.function.GeometryFunctionRegistry; import com.revolsys.geometry.test.function.TestCaseGeometryFunctions; import com.revolsys.geometry.test.geomop.GeometryFunctionOperation; import com.revolsys.geometry.test.geomop.GeometryOperation; import com.revolsys.geometry.test.util.LineNumberElement; import com.revolsys.geometry.test.util.LineNumberSAXBuilder; import com.revolsys.geometry.test.util.TestFileUtil; import com.revolsys.geometry.test.util.WKTOrWKBReader; import com.revolsys.util.Exceptions; /** * @version 1.7 */ public class TestReader { public static final EqualityResultMatcher EQUALITY_RESULT_MATCHER = new EqualityResultMatcher(); public static final GeometryFunctionRegistry GEOMETRY_FUNCTION_REGISTRY = new GeometryFunctionRegistry( TestCaseGeometryFunctions.class); public static final GeometryFunctionOperation GEOMETRY_FUNCTION_OPERATION = new GeometryFunctionOperation( GEOMETRY_FUNCTION_REGISTRY); private GeometryFactory geometryFactory; private GeometryOperation geomOp = GEOMETRY_FUNCTION_OPERATION; private ResultMatcher resultMatcher = EQUALITY_RESULT_MATCHER; private double tolerance = 0.0; private WKTOrWKBReader wktorbReader; public TestReader() { } private File absoluteWktFile(final File wktFile, final TestFile testRun) { if (wktFile == null) { return null; } File absoluteWktFile = wktFile; if (!absoluteWktFile.isAbsolute()) { final File directory = testRun.getWorkspace() != null ? testRun.getWorkspace() : testRun.getFile().getParentFile(); absoluteWktFile = new File(directory + File.separator + absoluteWktFile.getName()); } return absoluteWktFile; } public GeometryOperation getGeometryOperation() { return this.geomOp; } /** * Gets an instance of a class with the given name, * and ensures that the class is assignable to a specified baseClass. * * @return an instance of the class, if it is assignment-compatible, or * null if the requested class is not assigment-compatible */ private Object getInstance(final String classname, final Class<?> baseClass) { Object o = null; try { final Class<?> goClass = Class.forName(classname); if (!baseClass.isAssignableFrom(goClass)) { return null; } o = goClass.newInstance(); } catch (final Exception ex) { return null; } return o; } public boolean isBooleanFunction(final String name) { return getGeometryOperation().getReturnType(name) == boolean.class; } public boolean isDoubleFunction(final String name) { return getGeometryOperation().getReturnType(name) == double.class; } public boolean isGeometryFunction(final String name) { final Class<?> returnType = getGeometryOperation().getReturnType(name); if (returnType == null) { return false; } return Geometry.class.isAssignableFrom(returnType); } public boolean isIntegerFunction(final String name) { return getGeometryOperation().getReturnType(name) == int.class; } private double newPrecisionModel(final Element precisionModelElement) throws TestParseException { final Attribute scaleAttribute = precisionModelElement.getAttribute("scale"); if (scaleAttribute == null) { throw new TestParseException("Missing scale attribute in <precisionModel>"); } double scale; try { scale = scaleAttribute.getDoubleValue(); } catch (final DataConversionException e) { throw new TestParseException( "Could not convert scale attribute to double: " + scaleAttribute.getValue()); } return scale; } public TestFile newTestRun(final TestDirectory parent, final File testFile, final int runIndex) throws Throwable { try { final SAXBuilder builder = new LineNumberSAXBuilder(); final Document document = builder.build(new FileInputStream(testFile)); final Element runElement = document.getRootElement(); if (!runElement.getName().equalsIgnoreCase("run")) { throw new TestParseException( "Expected <run> but encountered <" + runElement.getName() + ">"); } return parseTestRun(parent, runElement, testFile, runIndex); } catch (final IllegalArgumentException e) { throw e; } catch (final Exception e) { throw new IllegalArgumentException("Error parsing " + testFile, e); } } /** * Parses an optional <tt>geometryOperation</tt> element. * The default is to leave this unspecified . * * @param runElement * @return an instance of the GeometryOperation class, if specified, or * null if no geometry operation was specified * @throws TestParseException if a parsing error was encountered */ private GeometryOperation parseGeometryOperation(final Element runElement) throws TestParseException { final Element goElement = runElement.getChild("geometryOperation"); if (goElement == null) { return GEOMETRY_FUNCTION_OPERATION; } final String goClass = goElement.getTextTrim(); final GeometryOperation geomOp = (GeometryOperation)getInstance(goClass, GeometryOperation.class); if (geomOp == null) { throw new TestParseException( "Could not create instance of GeometryOperation from class " + goClass); } return geomOp; } /** * Parses an optional <tt>precisionModel</tt> element. * The default is to use a FLOATING model. * * @param runElement * @return a PrecisionModel instance (default if not specified) * @throws TestParseException */ private double parsePrecisionModel(final Element runElement) throws TestParseException { final Element precisionModelElement = runElement.getChild("precisionModel"); if (precisionModelElement == null) { return 0; } final Attribute typeAttribute = precisionModelElement.getAttribute("type"); final Attribute scaleAttribute = precisionModelElement.getAttribute("scale"); if (typeAttribute == null && scaleAttribute == null) { throw new TestParseException("Missing type attribute in <precisionModel>"); } if (scaleAttribute != null || typeAttribute != null && typeAttribute.getValue().trim().equalsIgnoreCase("FIXED")) { if (typeAttribute != null && typeAttribute.getValue().trim().equalsIgnoreCase("FLOATING")) { throw new TestParseException("scale attribute not allowed in floating <precisionModel>"); } return newPrecisionModel(precisionModelElement); } return 0; } /** * Parses an optional <tt>resultMatcher</tt> element. * The default is to leave this unspecified . * * @param runElement * @return an instance of the ResultMatcher class, if specified, or * null if no result matcher was specified * @throws TestParseException if a parsing error was encountered */ private ResultMatcher parseResultMatcher(final Element runElement) throws TestParseException { final Element goElement = runElement.getChild("resultMatcher"); if (goElement == null) { return EQUALITY_RESULT_MATCHER; } final String goClass = goElement.getTextTrim(); final ResultMatcher resultMatcher = (ResultMatcher)getInstance(goClass, ResultMatcher.class); if (resultMatcher == null) { throw new TestParseException( "Could not create instance of ResultMatcher from class " + goClass); } return resultMatcher; } /** * Creates a List of TestCase's from the given <case> Element's. */ private List<TestCase> parseTestCases(final List caseElements, final File testFile, final TestFile testRun, final double tolerance) throws Throwable { this.wktorbReader = new WKTOrWKBReader(this.geometryFactory); final Vector<TestCase> testCases = new Vector<>(); int caseIndex = 0; for (final Iterator i = caseElements.iterator(); i.hasNext();) { final Element caseElement = (Element)i.next(); // System.out.println("Line: " + // ((LineNumberElement)caseElement).getStartLine()); caseIndex++; try { final Element descElement = caseElement.getChild("desc"); final Element aElement = caseElement.getChild("a"); final Element bElement = caseElement.getChild("b"); final File aWktFile = wktFile(aElement, testRun); final File bWktFile = wktFile(bElement, testRun); final String description = descElement != null ? descElement.getTextTrim() : ""; final Geometry a = readGeometry(aElement, absoluteWktFile(aWktFile, testRun)); final Geometry b = readGeometry(bElement, absoluteWktFile(bWktFile, testRun)); final TestCase testCase = new TestCase(description, a, b, aWktFile, bWktFile, testRun, caseIndex, ((LineNumberElement)caseElement).getStartLine()); final List testElements = caseElement.getChildren("test"); // if (testElements.size() == 0) { // throw new TestParseException("Missing <test> in <case>"); // } final List<GeometryOperationTest> tests = parseTests(testElements, caseIndex, testFile, testCase, tolerance); for (final GeometryOperationTest test : tests) { testCase.add(test); } testCases.add(testCase); } catch (final Exception e) { throw new IllegalArgumentException( "An exception occurred while parsing <case> " + caseIndex + " in " + testFile, e); } } return testCases; } /** * Creates a TestRun from the <run> Element. * @param parent */ private TestFile parseTestRun(final TestDirectory parent, final Element runElement, final File testFile, final int runIndex) throws Throwable { // ----------- <workspace> (optional) ------------------ File workspace = null; if (runElement.getChild("workspace") != null) { if (runElement.getChild("workspace").getAttribute("dir") == null) { throw new TestParseException("Missing <dir> in <workspace>"); } workspace = new File(runElement.getChild("workspace").getAttribute("dir").getValue().trim()); if (!workspace.exists()) { throw new TestParseException("<workspace> does not exist: " + workspace); } if (!workspace.isDirectory()) { throw new TestParseException("<workspace> is not a directory: " + workspace); } } // ----------- <tolerance> (optional) ------------------ this.tolerance = parseTolerance(runElement); final Element descElement = runElement.getChild("desc"); // ----------- <geometryOperation> (optional) ------------------ this.geomOp = parseGeometryOperation(runElement); // ----------- <geometryMatcher> (optional) ------------------ this.resultMatcher = parseResultMatcher(runElement); // ----------- <precisionModel> (optional) ---------------- final double scale = parsePrecisionModel(runElement); this.geometryFactory = GeometryFactory.fixed(0, scale, scale); // --------------- build TestRun --------------------- final String description = descElement != null ? descElement.getTextTrim() : ""; final TestFile testRun = new TestFile(parent, description, runIndex, this.geometryFactory, this.geomOp, this.resultMatcher, testFile); testRun.setWorkspace(workspace); final List caseElements = runElement.getChildren("case"); if (caseElements.size() == 0) { throw new TestParseException("Missing <case> in <run>"); } for (final TestCase testCase : parseTestCases(caseElements, testFile, testRun, this.tolerance)) { testRun.addTest(testCase); } return testRun; } /** * Creates a List of Test's from the given <test> Element's. */ private List<GeometryOperationTest> parseTests(final List testElements, final int caseIndex, final File testFile, final TestCase testCase, final double tolerance) throws Throwable { final List<GeometryOperationTest> tests = new ArrayList<>(); int testIndex = 0; for (final Iterator i = testElements.iterator(); i.hasNext();) { final Element testElement = (Element)i.next(); testIndex++; try { final Element descElement = testElement.getChild("desc"); if (testElement.getChildren("op").size() > 1) { throw new TestParseException("Multiple <op>s in <test>"); } final Element opElement = testElement.getChild("op"); if (opElement == null) { throw new TestParseException("Missing <op> in <test>"); } final Attribute nameAttribute = opElement.getAttribute("name"); if (nameAttribute == null) { throw new TestParseException("Missing name attribute in <op>"); } final String arg1 = opElement.getAttribute("arg1") == null ? "A" : opElement.getAttribute("arg1").getValue().trim(); final String arg2 = opElement.getAttribute("arg2") == null ? null : opElement.getAttribute("arg2").getValue().trim(); String arg3 = opElement.getAttribute("arg3") == null ? null : opElement.getAttribute("arg3").getValue().trim(); if (arg3 == null && nameAttribute.getValue().trim().equalsIgnoreCase("relate")) { arg3 = opElement.getAttribute("pattern") == null ? null : opElement.getAttribute("pattern").getValue().trim(); } final ArrayList arguments = new ArrayList(); if (arg2 != null) { arguments.add(arg2); } if (arg3 != null) { arguments.add(arg3); } final Result result = toResult(opElement.getTextTrim(), nameAttribute.getValue().trim(), testCase.getTestRun()); final GeometryOperationTest test = new GeometryOperationTest(testCase, testIndex, descElement != null ? descElement.getTextTrim() : "", nameAttribute.getValue().trim(), arg1, arguments, result, tolerance); tests.add(test); } catch (final Exception e) { throw new IllegalArgumentException("An exception occurred while parsing <test> " + testIndex + " in <case> " + caseIndex + " in " + testFile, e); } } return tests; } private double parseTolerance(final Element runElement) throws TestParseException { double tolerance = 0.0; // Note: the tolerance element applies to the coordinate-by-coordinate // comparisons of spatial functions. It does not apply to binary predicates. // [Jon Aquino] final Element toleranceElement = runElement.getChild("tolerance"); if (toleranceElement != null) { try { tolerance = Double.parseDouble(toleranceElement.getTextTrim()); } catch (final NumberFormatException e) { throw new TestParseException( "Could not parse tolerance from string: " + toleranceElement.getTextTrim()); } } return tolerance; } private Geometry readGeometry(final Element geometryElement, final File wktFile) throws FileNotFoundException, com.revolsys.geometry.wkb.ParseException, IOException { String geomText = null; if (wktFile != null) { final List wktList = TestFileUtil.getContents(wktFile.getPath()); geomText = toString(wktList); } else { if (geometryElement == null) { return null; } geomText = geometryElement.getTextTrim(); } try { return this.wktorbReader.read(geomText); } catch (final Exception e) { throw Exceptions.wrap(geomText, e); } } private BooleanResult toBooleanResult(final String value) throws TestParseException { if (value.equalsIgnoreCase("true")) { return new BooleanResult(true); } else if (value.equalsIgnoreCase("false")) { return new BooleanResult(false); } else { throw new TestParseException("Expected 'true' or 'false' but encountered '" + value + "'"); } } private DoubleResult toDoubleResult(final String value) throws TestParseException { try { return new DoubleResult(Double.valueOf(value)); } catch (final NumberFormatException e) { throw new TestParseException("Expected double but encountered '" + value + "'"); } } /* * private GeometryOperation getGeometryOperationInstance(String classname) { GeometryOperation op * = null; try { Class goClass = Class.forName(classname); if * (!(GeometryOperation.class.isAssignableFrom(goClass))) return null; op = (GeometryOperation) * goClass.newInstance(); } catch (Exception ex) { return null; } return op; } */ private GeometryResult toGeometryResult(final String value, final TestFile testRun) throws com.revolsys.geometry.wkb.ParseException { final GeometryFactory geometryFactory = GeometryFactory.floating(0, 2); final WKTOrWKBReader wktorbReader = new WKTOrWKBReader(geometryFactory); return new GeometryResult(wktorbReader.read(value)); } private IntegerResult toIntegerResult(final String value) throws TestParseException { try { return new IntegerResult(Integer.valueOf(value)); } catch (final NumberFormatException e) { throw new TestParseException("Expected integer but encountered '" + value + "'"); } } private Result toResult(final String value, final String name, final TestFile testRun) throws TestParseException, com.revolsys.geometry.wkb.ParseException { if (isBooleanFunction(name)) { return toBooleanResult(value); } if (isIntegerFunction(name)) { return toIntegerResult(value); } if (isDoubleFunction(name)) { return toDoubleResult(value); } if (isGeometryFunction(name)) { return toGeometryResult(value, testRun); } throw new TestParseException("Unknown operation name '" + name + "'"); // return null; } private String toString(final List<String> stringList) { String string = ""; for (final String line : stringList) { string += line + "\n"; } return string; } private File wktFile(final Element geometryElement, final TestFile testRun) throws TestParseException { if (geometryElement == null) { return null; } if (geometryElement.getAttribute("file") == null) { return null; } if (!geometryElement.getTextTrim().equals("")) { throw new TestParseException("WKT specified both in-line and in external file"); } final File wktFile = new File(geometryElement.getAttribute("file").getValue().trim()); final File absoluteWktFile = absoluteWktFile(wktFile, testRun); if (!absoluteWktFile.exists()) { throw new TestParseException("WKT file does not exist: " + absoluteWktFile); } if (absoluteWktFile.isDirectory()) { throw new TestParseException("WKT file is a directory: " + absoluteWktFile); } return wktFile; } }