diff --git a/src/adql/db/STCS.java b/src/adql/db/STCS.java index c4a34e9eb4e3a64dfb08e62a10db0a123e742789..2f1f4f3f20a1f3c63124736e18ee584714690cee 100644 --- a/src/adql/db/STCS.java +++ b/src/adql/db/STCS.java @@ -1236,7 +1236,7 @@ public final class STCS { * Let parse any STC-S expression. * * @author Grégory Mantelet (ARI) - * @version 1.3 (10/2014) + * @version 1.3 (11/2014) * @since 1.3 */ private static class STCSParser { @@ -1318,7 +1318,7 @@ public final class STCS { * @param newStcs New STC-S expression to parse from now. */ private void init(final String newStcs){ - stcs = (newStcs == null) ? "" : newStcs.replaceAll("\\s", " "); + stcs = (newStcs == null) ? "" : newStcs; token = null; buffer = new StringBuffer(); pos = 0; @@ -1351,7 +1351,7 @@ public final class STCS { * Tool function which skip all next space characters until the next meaningful characters. */ private void skipSpaces(){ - while(pos < stcs.length() && stcs.charAt(pos) == ' ') + while(pos < stcs.length() && Character.isWhitespace(stcs.charAt(pos))) pos++; } @@ -1371,7 +1371,7 @@ public final class STCS { skipSpaces(); // Fetch all characters until word separator (a space or a open/close parenthesis): - while(pos < stcs.length() && stcs.charAt(pos) != ' ' && stcs.charAt(pos) != '(' && stcs.charAt(pos) != ')') + while(pos < stcs.length() && !Character.isWhitespace(stcs.charAt(pos)) && stcs.charAt(pos) != '(' && stcs.charAt(pos) != ')') buffer.append(stcs.charAt(pos++)); // If no character has been fetched while at least one was expected, throw an exception: diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java index 346d0278956c6625d685805a813521c4bac94bd0..6ea5ae34cf0004bff883ad533d3e344de516f3ff 100644 --- a/src/adql/translator/JDBCTranslator.java +++ b/src/adql/translator/JDBCTranslator.java @@ -23,9 +23,13 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import tap.data.DataReadException; import adql.db.DBColumn; import adql.db.DBTable; +import adql.db.DBType; +import adql.db.STCS.Region; import adql.db.exception.UnresolvedJoin; +import adql.parser.ParseException; import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ADQLOrder; @@ -93,8 +97,9 @@ import adql.query.operand.function.geometry.RegionFunction; * <h3>PostgreSQLTranslator and PgSphereTranslator</h3> * * <p> - * {@link PgSphereTranslator} extends {@link PostgreSQLTranslator} and is just translating geometrical - * functions according to the syntax given by PgSphere. + * {@link PgSphereTranslator} extends {@link PostgreSQLTranslator} and is able to translate geometrical + * functions according to the syntax given by PgSphere. But it can also convert geometrical types + * (from and toward the database), translate PgSphere regions into STC expression and vice-versa. * </p> * * <p> @@ -150,8 +155,8 @@ import adql.query.operand.function.geometry.RegionFunction; * </p> * * <p><i>Note: - * Geometrical function have not been translated here. They stay abstract because it is obviously impossible to have a generic - * translation ; it totally depends from the database system. + * Geometrical regions and types have not been managed here. They stay abstract because it is obviously impossible to have a generic + * translation and conversion ; it totally depends from the database system. * </i></p> * * <h3>Translation of "FROM" with JOINs</h3> @@ -162,8 +167,8 @@ import adql.query.operand.function.geometry.RegionFunction; * </p> * * @author Grégory Mantelet (ARI) - * @version 1.3 (09/2014) - * @since 2.0 + * @version 1.3 (11/2014) + * @since 1.3 * * @see PostgreSQLTranslator * @see PgSphereTranslator @@ -802,4 +807,77 @@ public abstract class JDBCTranslator implements ADQLTranslator { return translate(geomValue.getValue()); } + /** + * Convert any type provided by a JDBC driver into a type understandable by the ADQL/TAP library. + * + * @param dbmsType Type returned by a JDBC driver. <i>Note: this value is returned by ResultSetMetadata.getColumnType(int) and correspond to a type of java.sql.Types</i> + * @param rawDbmsTypeName Full name of the type returned by a JDBC driver. <i>Note: this name is returned by ResultSetMetadata.getColumnTypeName(int) ; this name may contain parameters</i> + * @param dbmsTypeName Name of type, without the eventual parameters. <i>Note: this name is extracted from rawDbmsTypeName.</i> + * @param typeParams The eventual type parameters (e.g. char string length). <i>Note: these parameters are extracted from rawDbmsTypeName.</i> + * + * @return The corresponding ADQL/TAP type or NULL if the specified type is unknown. + */ + public abstract DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, final String dbmsTypeName, final String[] typeParams); + + /** + * <p>Convert any type provided by the ADQL/TAP library into a type understandable by a JDBC driver.</p> + * + * <p><i>Note: + * The returned DBMS type may contain some parameters between brackets. + * </i></p> + * + * @param type The ADQL/TAP library's type to convert. + * + * @return The corresponding DBMS type or NULL if the specified type is unknown. + */ + public abstract String convertTypeToDB(final DBType type); + + /** + * <p>Parse the given JDBC column value as a geometry object and convert it into a {@link Region}.</p> + * + * <p><i>Note: + * Generally the returned object will be used to get its STC-S expression. + * </i></p> + * + * <p><i>Note: + * If the given column value is NULL, NULL will be returned. + * </i></p> + * + * <p><i><b>Important note:</b> + * This function is called ONLY for value of columns flagged as geometries by + * {@link #convertTypeFromDB(int, String, String, String[])}. So the value should always + * be of the expected type and format. However, if it turns out that the type is wrong + * and that the conversion is finally impossible, this function SHOULD throw a + * {@link DataReadException}. + * </i></p> + * + * @param jdbcColValue A JDBC column value (returned by ResultSet.getObject(int)). + * + * @return The corresponding {@link Region} if the given value is a geometry. + * + * @throws ParseException If the given object is not a geometrical object + * or can not be transformed into a {@link Region} object. + */ + public abstract Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException; + + /** + * <p>Convert the given STC region into a DB column value.</p> + * + * <p><i>Note: + * This function is used only by the UPLOAD feature, to import geometries provided as STC-S expression in + * a VOTable document inside a DB column. + * </i></p> + * + * <p><i>Note: + * If the given region is NULL, NULL will be returned. + * </i></p> + * + * @param stcs The region to store in the DB. + * + * @return The corresponding DB column object. + * + * @throws ParseException If the given STC Region can not be converted into a DB object. + */ + public abstract Object translateGeometryToDB(final Region region) throws ParseException; + } diff --git a/src/adql/translator/PgSphereTranslator.java b/src/adql/translator/PgSphereTranslator.java index 8a6a197c5179f01d836705573bb9ee2bfeea6d9b..96c509c52744219b63c7560a82988cd06a72d720 100644 --- a/src/adql/translator/PgSphereTranslator.java +++ b/src/adql/translator/PgSphereTranslator.java @@ -20,6 +20,16 @@ package adql.translator; * Astronomisches Rechen Institut (ARI) */ +import java.sql.SQLException; +import java.util.ArrayList; + +import org.postgresql.util.PGobject; + +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; +import adql.query.TextPosition; import adql.query.constraint.Comparison; import adql.query.constraint.ComparisonOperator; import adql.query.operand.function.geometry.AreaFunction; @@ -38,10 +48,16 @@ import adql.query.operand.function.geometry.PolygonFunction; * The other functions are managed by {@link PostgreSQLTranslator}.</p> * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (08/2014) + * @version 1.3 (11/2014) */ public class PgSphereTranslator extends PostgreSQLTranslator { + /** Angle between two points generated while transforming a circle into a polygon. + * This angle is computed by default to get at the end a polygon of 32 points. + * @see #circleToPolygon(double[], double) + * @since 1.3 */ + protected static double ANGLE_CIRCLE_TO_POLYGON = 2 * Math.PI / 32; + /** * Builds a PgSphereTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ; * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation. @@ -99,11 +115,12 @@ public class PgSphereTranslator extends PostgreSQLTranslator { public String translate(BoxFunction box) throws TranslationException{ StringBuffer str = new StringBuffer("sbox("); + str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("-(").append(translate(box.getWidth())).append("/2.0)),"); + str.append("radians(").append(translate(box.getCoord2())).append("-(").append(translate(box.getHeight())).append("/2.0))),"); + str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("+(").append(translate(box.getWidth())).append("/2.0)),"); - str.append("radians(").append(translate(box.getCoord2())).append("+(").append(translate(box.getHeight())).append("/2.0))),"); + str.append("radians(").append(translate(box.getCoord2())).append("+(").append(translate(box.getHeight())).append("/2.0))))"); - str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("-(").append(translate(box.getWidth())).append("/2.0)),"); - str.append("radians(").append(translate(box.getCoord2())).append("-(").append(translate(box.getHeight())).append("/2.0))))"); return str.toString(); } @@ -152,8 +169,8 @@ public class PgSphereTranslator extends PostgreSQLTranslator { @Override public String translate(AreaFunction areaFunction) throws TranslationException{ - StringBuffer str = new StringBuffer("degrees(area("); - str.append(translate(areaFunction.getParameter())).append("))"); + StringBuffer str = new StringBuffer("degrees(degrees(area("); + str.append(translate(areaFunction.getParameter())).append(")))"); return str.toString(); } @@ -181,4 +198,534 @@ public class PgSphereTranslator extends PostgreSQLTranslator { return super.translate(comp); } + @Override + public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + + // Put the dbmsTypeName in lower case for the following comparisons: + dbmsTypeName = dbmsTypeName.toLowerCase(); + + if (dbmsTypeName.equals("spoint")) + return new DBType(DBDatatype.POINT); + else if (dbmsTypeName.equals("scircle") || dbmsTypeName.equals("sbox") || dbmsTypeName.equals("spoly")) + return new DBType(DBDatatype.REGION); + else + return super.convertTypeFromDB(dbmsType, rawDbmsTypeName, dbmsTypeName, params); + } + + @Override + public String convertTypeToDB(final DBType type){ + if (type != null){ + if (type.type == DBDatatype.POINT) + return "spoint"; + else if (type.type == DBDatatype.REGION) + return "spoly"; + } + return super.convertTypeToDB(type); + } + + @Override + public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{ + // A NULL value stays NULL: + if (jdbcColValue == null) + return null; + // Only a special object is expected: + else if (!(jdbcColValue instanceof PGobject)) + throw new ParseException("Incompatible type! The column value \"" + jdbcColValue.toString() + "\" was supposed to be a geometrical object."); + + PGobject pgo = (PGobject)jdbcColValue; + + // In case one or both of the fields of the given object are NULL: + if (pgo == null || pgo.getType() == null || pgo.getValue() == null || pgo.getValue().length() == 0) + return null; + + // Extract the object type and its value: + String objType = pgo.getType().toLowerCase(); + String geomStr = pgo.getValue(); + + /* Only spoint, scircle, sbox and spoly are supported ; + * these geometries are parsed and transformed in Region instances:*/ + if (objType.equals("spoint")) + return (new PgSphereGeometryParser()).parsePoint(geomStr); + else if (objType.equals("scircle")) + return (new PgSphereGeometryParser()).parseCircle(geomStr); + else if (objType.equals("sbox")) + return (new PgSphereGeometryParser()).parseBox(geomStr); + else if (objType.equals("spoly")) + return (new PgSphereGeometryParser()).parsePolygon(geomStr); + else + throw new ParseException("Unsupported PgSphere type: \"" + objType + "\"! Impossible to convert the column value \"" + geomStr + "\" into a Region."); + } + + @Override + public Object translateGeometryToDB(final Region region) throws ParseException{ + // A NULL value stays NULL: + if (region == null) + return null; + + try{ + PGobject dbRegion = new PGobject(); + StringBuffer buf; + + // Build the PgSphere expression from the given geometry in function of its type: + switch(region.type){ + + case POSITION: + dbRegion.setType("spoint"); + dbRegion.setValue("(" + region.coordinates[0][0] + "d," + region.coordinates[0][1] + "d)"); + break; + + case POLYGON: + dbRegion.setType("spoly"); + buf = new StringBuffer("{"); + for(int i = 0; i < region.coordinates.length; i++){ + if (i > 0) + buf.append(','); + buf.append('(').append(region.coordinates[i][0]).append("d,").append(region.coordinates[i][1]).append("d)"); + } + buf.append('}'); + dbRegion.setValue(buf.toString()); + break; + + case BOX: + dbRegion.setType("spoly"); + buf = new StringBuffer("{"); + // south west + buf.append('(').append(region.coordinates[0][0] - region.width / 2).append("d,").append(region.coordinates[0][1] - region.height / 2).append("d),"); + // north west + buf.append('(').append(region.coordinates[0][0] - region.width / 2).append("d,").append(region.coordinates[0][1] + region.height / 2).append("d),"); + // north east + buf.append('(').append(region.coordinates[0][0] + region.width / 2).append("d,").append(region.coordinates[0][1] + region.height / 2).append("d),"); + // south east + buf.append('(').append(region.coordinates[0][0] + region.width / 2).append("d,").append(region.coordinates[0][1] - region.height / 2).append("d)"); + buf.append('}'); + dbRegion.setValue(buf.toString()); + break; + + case CIRCLE: + dbRegion.setType("spoly"); + dbRegion.setValue(circleToPolygon(region.coordinates[0], region.radius)); + break; + + default: + throw new ParseException("Unsupported geometrical region: \"" + region.type + "\"!"); + } + return dbRegion; + }catch(SQLException e){ + /* This error could never happen! */ + return null; + } + } + + /** + * <p>Convert the specified circle into a polygon. + * The generated polygon is formatted using the PgSphere syntax.</p> + * + * <p><i>Note: + * The center coordinates and the radius are expected in degrees. + * </i></p> + * + * @param center Center of the circle ([0]=ra and [1]=dec). + * @param radius Radius of the circle. + * + * @return The PgSphere serialization of the corresponding polygon. + * + * @since 1.3 + */ + protected String circleToPolygon(final double[] center, final double radius){ + double angle = 0, x, y; + StringBuffer buf = new StringBuffer(); + while(angle < 2 * Math.PI){ + x = center[0] + radius * Math.cos(angle); + y = center[1] + radius * Math.sin(angle); + if (buf.length() > 0) + buf.append(','); + buf.append('(').append(x).append("d,").append(y).append("d)"); + angle += ANGLE_CIRCLE_TO_POLYGON; + } + return "{" + buf + "}"; + } + + /** + * <p>Let parse a geometry serialized with the PgSphere syntax.</p> + * + * <p> + * There is one function parseXxx(String) for each supported geometry. + * These functions always return a {@link Region} object, + * which is the object representation of an STC region. + * </p> + * + * <p>Only the following geometries are supported:</p> + * <ul> + * <li>spoint => Position</li> + * <li>scircle => Circle</li> + * <li>sbox => Box</li> + * <li>spoly => Polygon</li> + * </ul> + * + * <p> + * This parser supports all the known PgSphere representations of an angle. + * However, it always returns angle (coordinates, radius, width and height) in degrees. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (11/2014) + * @since 1.3 + */ + protected static class PgSphereGeometryParser { + /** Position of the next characters to read in the PgSphere expression to parse. */ + private int pos; + /** Full PgSphere expression to parse. */ + private String expr; + /** Last read token (either a string/numeric or a separator). */ + private String token; + /** Buffer used to read tokens. */ + private StringBuffer buffer; + + private static final char OPEN_PAR = '('; + private static final char CLOSE_PAR = ')'; + private static final char COMMA = ','; + private static final char LESS_THAN = '<'; + private static final char GREATER_THAN = '>'; + private static final char OPEN_BRACE = '{'; + private static final char CLOSE_BRACE = '}'; + private static final char DEGREE = 'd'; + private static final char HOUR = 'h'; + private static final char MINUTE = 'm'; + private static final char SECOND = 's'; + + /** + * Exception sent when the end of the expression + * (EOE = End Of Expression) is reached. + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (11/2014) + * @since 1.3 + */ + private static class EOEException extends ParseException { + private static final long serialVersionUID = 1L; + + /** Build a simple EOEException. */ + public EOEException(){ + super("Unexpected End Of PgSphere Expression!"); + } + } + + /** + * Build the PgSphere parser. + */ + public PgSphereGeometryParser(){} + + /** + * Prepare the parser in order to read the given PgSphere expression. + * + * @param newStcs New PgSphere expression to parse from now. + */ + private void init(final String newExpr){ + expr = (newExpr == null) ? "" : newExpr; + token = null; + buffer = new StringBuffer(); + pos = 0; + } + + /** + * Finalize the parsing. + * No more characters (except eventually some space characters) should remain in the PgSphere expression to parse. + * + * @throws ParseException If other non-space characters remains. + */ + private void end() throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // If there is still some characters, they are not expected, and so throw an exception: + if (expr.length() > 0 && pos < expr.length()) + throw new ParseException("Unexpected end of PgSphere region expression: \"" + expr.substring(pos) + "\" was unexpected!", new TextPosition(1, pos, 1, expr.length())); + + // Reset the buffer, token and the PgSphere expression to parse: + buffer = null; + expr = null; + token = null; + } + + /** + * Tool function which skips all next space characters until the next meaningful characters. + */ + private void skipSpaces(){ + while(pos < expr.length() && Character.isWhitespace(expr.charAt(pos))) + pos++; + } + + /** + * <p>Get the next meaningful word. This word can be a numeric, any string constant or a separator. + * This function returns this token but also stores it in the class attribute {@link #token}.</p> + * + * <p> + * In case the end of the expression is reached before getting any meaningful character, + * an {@link EOEException} is thrown. + * </p> + * + * @return The full read word/token, or NULL if the end has been reached. + */ + private String nextToken() throws EOEException{ + // Skip all spaces: + skipSpaces(); + + if (pos >= expr.length()) + throw new EOEException(); + + // Fetch all characters until word separator (a space or a open/close parenthesis): + buffer.append(expr.charAt(pos++)); + if (!isSyntaxSeparator(buffer.charAt(0))){ + while(pos < expr.length() && !isSyntaxSeparator(expr.charAt(pos))){ + // skip eventual white-spaces: + if (!Character.isWhitespace(expr.charAt(pos))) + buffer.append(expr.charAt(pos)); + pos++; + } + } + + // Save the read token and reset the buffer: + token = buffer.toString(); + buffer.delete(0, token.length()); + + return token; + } + + /** + * <p>Tell whether the given character is a separator defined in the syntax.</p> + * + * <p>Here, the following characters are considered as separators/specials: + * ',', 'd', 'h', 'm', 's', '(', ')', '<', '>', '{' and '}'.</p> + * + * @param c Character to test. + * + * @return <i>true</i> if the given character must be considered as a separator, <i>false</i> otherwise. + */ + private static boolean isSyntaxSeparator(final char c){ + return (c == COMMA || c == DEGREE || c == HOUR || c == MINUTE || c == SECOND || c == OPEN_PAR || c == CLOSE_PAR || c == LESS_THAN || c == GREATER_THAN || c == OPEN_BRACE || c == CLOSE_BRACE); + } + + /** + * Get the next character and ensure it is the same as the character given in parameter. + * If the read character is not matching the expected one, a {@link ParseException} is thrown. + * + * @param expected Expected character. + * + * @throws ParseException If the next character is not matching the given one. + */ + private void nextToken(final char expected) throws ParseException{ + // Skip all spaces: + skipSpaces(); + + // Test whether the end is reached: + if (pos >= expr.length()) + throw new EOEException(); + + // Fetch the next character: + char t = expr.charAt(pos++); + token = new String(new char[]{t}); + + /* Test the the fetched character with the expected one + * and throw an error if they don't match: */ + if (t != expected) + throw new ParseException("Incorrect syntax for \"" + expr + "\"! \"" + expected + "\" was expected instead of \"" + t + "\".", new TextPosition(1, pos - 1, 1, pos)); + } + + /** + * Parse the given PgSphere geometry as a point. + * + * @param pgsphereExpr The PgSphere expression to parse as a point. + * + * @return A {@link Region} implementing a STC Position region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a point. + */ + public Region parsePoint(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + // Parse the expression: + double[] coord = parsePoint(); + // No more character should remain after that: + end(); + // Build the STC Position region: + return new Region(null, coord); + } + + /** + * Internal spoint parsing function. It parses the PgSphere expression stored in this parser as a point. + * + * @return The ra and dec coordinates (in degrees) of the parsed point. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a point. + * + * @see #parseAngle() + * @see #parsePoint(String) + */ + private double[] parsePoint() throws ParseException{ + nextToken(OPEN_PAR); + double x = parseAngle(); + nextToken(COMMA); + double y = parseAngle(); + nextToken(CLOSE_PAR); + return new double[]{x,y}; + } + + /** + * Parse the given PgSphere geometry as a circle. + * + * @param pgsphereExpr The PgSphere expression to parse as a circle. + * + * @return A {@link Region} implementing a STC Circle region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a circle. + */ + public Region parseCircle(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + + // Parse the expression: + nextToken(LESS_THAN); + double[] center = parsePoint(); + nextToken(COMMA); + double radius = parseAngle(); + nextToken(GREATER_THAN); + + // No more character should remain after that: + end(); + + // Build the STC Circle region: + return new Region(null, center, radius); + } + + /** + * Parse the given PgSphere geometry as a box. + * + * @param pgsphereExpr The PgSphere expression to parse as a box. + * + * @return A {@link Region} implementing a STC Box region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a box. + */ + public Region parseBox(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + + // Parse the expression: + nextToken(OPEN_PAR); + double[] southwest = parsePoint(); + nextToken(COMMA); + double[] northeast = parsePoint(); + nextToken(CLOSE_PAR); + + // No more character should remain after that: + end(); + + // Build the STC Box region: + double width = Math.abs(northeast[0] - southwest[0]), height = Math.abs(northeast[1] - southwest[1]); + double[] center = new double[]{northeast[0] - width / 2,northeast[1] - height / 2}; + return new Region(null, center, width, height); + } + + /** + * Parse the given PgSphere geometry as a point. + * + * @param pgsphereExpr The PgSphere expression to parse as a point. + * + * @return A {@link Region} implementing a STC Position region. + * + * @throws ParseException If the PgSphere syntax of the given expression is wrong or does not correspond to a point. + */ + public Region parsePolygon(final String pgsphereExpr) throws ParseException{ + // Init the parser: + init(pgsphereExpr); + + // Parse the expression: + nextToken(OPEN_BRACE); + ArrayList<double[]> points = new ArrayList<double[]>(3); + // at least 3 points are expected: + points.add(parsePoint()); + nextToken(COMMA); + points.add(parsePoint()); + nextToken(COMMA); + points.add(parsePoint()); + // but if there are more points, parse and keep them: + while(nextToken().length() == 1 && token.charAt(0) == COMMA) + points.add(parsePoint()); + // the expression must end with a } : + if (token.length() != 1 || token.charAt(0) != CLOSE_BRACE) + throw new ParseException("Incorrect syntax for \"" + expr + "\"! \"}\" was expected instead of \"" + token + "\".", new TextPosition(1, pos - token.length(), 1, pos)); + + // No more character should remain after that: + end(); + + // Build the STC Polygon region: + return new Region(null, points.toArray(new double[points.size()][2])); + } + + /** + * <p>Read the next tokens as an angle expression and returns the corresponding angle in <b>degrees</b>.</p> + * + * <p>This function supports the 4 following syntaxes:</p> + * <ul> + * <li><b>RAD:</b> {number}</li> + * <li><b>DEG:</b> {number}d</li> + * <li><b>DMS:</b> {number}d {number}m {number}s</li> + * <li><b>HMS:</b> {number}h {number}m {number}s</li> + * </ul> + * + * @return The corresponding angle in degrees. + * + * @throws ParseException If the angle syntax is wrong or not supported. + */ + private double parseAngle() throws ParseException{ + int oldPos = pos; + String number = nextToken(); + try{ + double degrees = Double.parseDouble(number); + int sign = (degrees < 0) ? -1 : 1; + degrees = Math.abs(degrees); + + oldPos = pos; + try{ + if (nextToken().length() == 1 && token.charAt(0) == HOUR) + sign *= 15; + else if (token.length() != 1 || token.charAt(0) != DEGREE){ + degrees = degrees * 180 / Math.PI; + pos -= token.length(); + return degrees * sign; + } + + oldPos = pos; + number = nextToken(); + if (nextToken().length() == 1 && token.charAt(0) == MINUTE) + degrees += Double.parseDouble(number) / 60; + else if (token.length() == 1 && token.charAt(0) == SECOND){ + degrees += Double.parseDouble(number) / 3600; + return degrees * sign; + }else{ + pos = oldPos; + return degrees * sign; + } + + oldPos = pos; + number = nextToken(); + if (nextToken().length() == 1 && token.charAt(0) == SECOND) + degrees += Double.parseDouble(number) / 3600; + else + pos = oldPos; + }catch(EOEException ex){ + pos = oldPos; + } + + return degrees * sign; + + }catch(NumberFormatException nfe){ + throw new ParseException("Incorrect numeric syntax: \"" + number + "\"!", new TextPosition(1, pos - token.length(), 1, pos)); + } + } + } + } diff --git a/src/adql/translator/PostgreSQLTranslator.java b/src/adql/translator/PostgreSQLTranslator.java index 4a5b5e9d142349ace142bda4d6b0ab70f424e5f8..4b2d01395a3e049ff05a4901a0ce8b05624c76e9 100644 --- a/src/adql/translator/PostgreSQLTranslator.java +++ b/src/adql/translator/PostgreSQLTranslator.java @@ -20,6 +20,10 @@ package adql.translator; * Astronomisches Rechen Institut (ARI) */ +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; import adql.query.IdentifierField; import adql.query.operand.StringConstant; import adql.query.operand.function.MathFunction; @@ -46,7 +50,7 @@ import adql.query.operand.function.geometry.RegionFunction; * </i></p> * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (10/2014) + * @version 1.3 (11/2014) * * @see PgSphereTranslator */ @@ -188,4 +192,108 @@ public class PostgreSQLTranslator extends JDBCTranslator { return getDefaultADQLFunction(region); } + @Override + public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + + // Put the dbmsTypeName in lower case for the following comparisons: + dbmsTypeName = dbmsTypeName.toLowerCase(); + + // Extract the length parameter (always the first one): + int lengthParam = DBType.NO_LENGTH; + if (params != null && params.length > 0){ + try{ + lengthParam = Integer.parseInt(params[0]); + }catch(NumberFormatException nfe){} + } + + // SMALLINT + if (dbmsTypeName.equals("smallint") || dbmsTypeName.equals("int2") || dbmsTypeName.equals("smallserial") || dbmsTypeName.equals("serial2") || dbmsTypeName.equals("boolean") || dbmsTypeName.equals("bool")) + return new DBType(DBDatatype.SMALLINT); + // INTEGER + else if (dbmsTypeName.equals("integer") || dbmsTypeName.equals("int") || dbmsTypeName.equals("int4") || dbmsTypeName.equals("serial") || dbmsTypeName.equals("serial4")) + return new DBType(DBDatatype.INTEGER); + // BIGINT + else if (dbmsTypeName.equals("bigint") || dbmsTypeName.equals("int8") || dbmsTypeName.equals("bigserial") || dbmsTypeName.equals("bigserial8")) + return new DBType(DBDatatype.BIGINT); + // REAL + else if (dbmsTypeName.equals("real") || dbmsTypeName.equals("float4")) + return new DBType(DBDatatype.REAL); + // DOUBLE + else if (dbmsTypeName.equals("double precision") || dbmsTypeName.equals("float8")) + return new DBType(DBDatatype.DOUBLE); + // BINARY + else if (dbmsTypeName.equals("bit")) + return new DBType(DBDatatype.BINARY, lengthParam); + // VARBINARY + else if (dbmsTypeName.equals("bit varying") || dbmsTypeName.equals("varbit")) + return new DBType(DBDatatype.VARBINARY, lengthParam); + // CHAR + else if (dbmsTypeName.equals("char") || dbmsTypeName.equals("character")) + return new DBType(DBDatatype.CHAR, lengthParam); + // VARCHAR + else if (dbmsTypeName.equals("varchar") || dbmsTypeName.equals("character varying")) + return new DBType(DBDatatype.VARCHAR, lengthParam); + // BLOB + else if (dbmsTypeName.equals("bytea")) + return new DBType(DBDatatype.BLOB); + // CLOB + else if (dbmsTypeName.equals("text")) + return new DBType(DBDatatype.CLOB); + // TIMESTAMP + else if (dbmsTypeName.equals("timestamp") || dbmsTypeName.equals("timestamptz") || dbmsTypeName.equals("time") || dbmsTypeName.equals("timetz") || dbmsTypeName.equals("date")) + return new DBType(DBDatatype.TIMESTAMP); + // Default: + else + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + } + + @Override + public String convertTypeToDB(final DBType type){ + if (type == null) + return "VARCHAR"; + + switch(type.type){ + + case SMALLINT: + case INTEGER: + case REAL: + case BIGINT: + case CHAR: + case VARCHAR: + case TIMESTAMP: + return type.type.toString(); + + case DOUBLE: + return "DOUBLE PRECISION"; + + case BINARY: + case VARBINARY: + return "bytea"; + + case BLOB: + return "bytea"; + + case CLOB: + return "TEXT"; + + case POINT: + case REGION: + default: + return "VARCHAR"; + } + } + + @Override + public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{ + throw new ParseException("Unsupported geometrical value! The value \"" + jdbcColValue + "\" can not be parsed as a region."); + } + + @Override + public Object translateGeometryToDB(final Region region) throws ParseException{ + throw new ParseException("Geometries can not be uploaded in the database in this implementation!"); + } + } diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java index a580c87fbfec937fb3008eb44504d10bc2a0d1d3..eb51075327e8fbd9d2c18f53fbe2652124f6dd30 100644 --- a/src/tap/data/ResultSetTableIterator.java +++ b/src/tap/data/ResultSetTableIterator.java @@ -30,6 +30,9 @@ import uws.ISO8601Format; import adql.db.DBColumn; import adql.db.DBType; import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; +import adql.translator.JDBCTranslator; /** * <p>{@link TableIterator} which lets iterate over a SQL {@link ResultSet}.</p> @@ -39,7 +42,7 @@ import adql.db.DBType.DBDatatype; * </i></p> * * @author Grégory Mantelet (ARI) - * @version 2.0 (08/2014) + * @version 2.0 (11/2014) * @since 2.0 */ public class ResultSetTableIterator implements TableIterator { @@ -47,6 +50,11 @@ public class ResultSetTableIterator implements TableIterator { /** ResultSet/Dataset to read. */ private final ResultSet data; + /** Object which has the knowledge of the specific JDBC column types + * and which knows how to deal with geometrical values between the + * library and the database. */ + private final JDBCTranslator translator; + /** Number of columns to read. */ private final int nbColumns; /** Metadata of all columns identified before the iteration. */ @@ -71,18 +79,11 @@ public class ResultSetTableIterator implements TableIterator { * * <p> * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} - * which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * which deals with the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by a translator. That's why it is recommended + * to use one of the constructor having a {@link JDBCTranslator} in parameter. * </p> * - * <p><i><b>Important</b>: - * To guess the TAP type from a DBMS type, {@link #convertType(String, String)} may not need to know the DBMS, - * except for SQLite. Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know - * it is the DBMS from which the ResultSet is coming. Without this information, type guessing will be unpredictable! - * - * <b>So, if your ResultSet is coming from a SQLite connection, you SHOULD really use one of the 2 other constructors</b> - * and provide "sqlite" as value for the second parameter. - * </i></p> - * * @param dataSet Dataset over which this iterator must iterate. * * @throws NullPointerException If NULL is given in parameter. @@ -92,7 +93,7 @@ public class ResultSetTableIterator implements TableIterator { * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet) throws NullPointerException, DataReadException{ - this(dataSet, null, null); + this(dataSet, null, null, null); } /** @@ -107,14 +108,16 @@ public class ResultSetTableIterator implements TableIterator { * * <p> * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} - * which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * which deals with the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by a translator. That's why it is recommended + * to use one of the constructor having a {@link JDBCTranslator} in parameter. * </p> * * <p><i><b>Important</b>: * The second parameter of this constructor is given as second parameter of {@link #convertType(String, String)}. - * <b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> Indeed, SQLite has so many datatype - * restrictions that it is absolutely needed to know it is the DBMS from which the ResultSet is coming. Without this - * information, type guessing will be unpredictable! + * <b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! * </i></p> * * @param dataSet Dataset over which this iterator must iterate. @@ -124,10 +127,82 @@ public class ResultSetTableIterator implements TableIterator { * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. * * @see #convertType(String, String) - * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) */ public ResultSetTableIterator(final ResultSet dataSet, final String dbms) throws NullPointerException, DataReadException{ - this(dataSet, dbms, null); + this(dataSet, null, dbms, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + * </p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. <i>note: MAY be NULL</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + */ + public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator) throws NullPointerException, DataReadException{ + this(dataSet, translator, null, null); + } + + /** + * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p> + * + * <p> + * In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype + * from the DBMS column datatype (using {@link #convertType(String, String)}). + * </p> + * + * <h3>Type guessing</h3> + * + * <p> + * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. + * </p> + * + * <p><i><b>Important</b>: + * The third parameter of this constructor is given as second parameter of {@link #convertType(String, String)}. + * <b>This parameter is really used ONLY when the translator conversion failed and when the DBMS is SQLite ("sqlite").</b> + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! + * </i></p> + * + * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. <i>note: MAY be NULL</i> + * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> + * + * @throws NullPointerException If NULL is given in parameter. + * @throws DataReadException If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched. + * + * @see #convertType(String, String) + * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, JDBCTranslator, String, DBColumn[]) + */ + public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator, final String dbms) throws NullPointerException, DataReadException{ + this(dataSet, translator, dbms, null); } /** @@ -158,17 +233,22 @@ public class ResultSetTableIterator implements TableIterator { * * <p> * In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)} - * which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * which will ask to the given translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}) + * if not NULL. However if no translator is provided, this function will proceed to a default conversion + * using the most common standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby. + * This conversion is therefore not as precise as the one expected by the translator. * </p> * * <p><i><b>Important</b>: - * The second parameter of this constructor is given as second parameter of {@link #convertType(String, String)}. - * <b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> Indeed, SQLite has so many datatype - * restrictions that it is absolutely needed to know it is the DBMS from which the ResultSet is coming. Without this - * information, type guessing will be unpredictable! + * The third parameter of this constructor is given as second parameter of {@link #convertType(String, String)}. + * <b>This parameter is really used ONLY when the translator conversion failed and when the DBMS is SQLite ("sqlite").</b> + * Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know it is the DBMS from which the + * ResultSet is coming. Without this information, type guessing will be unpredictable! * </i></p> * * @param dataSet Dataset over which this iterator must iterate. + * @param translator The {@link JDBCTranslator} used to transform the ADQL query into SQL query. This translator is also able to convert + * JDBC types and to parse geometrical values. <i>note: MAY be NULL</i> * @param dbms Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i> * @param resultMeta List of expected columns. <i>note: these metadata are expected to be really {@link TAPColumn} objects ; MAY be NULL.</i> * @@ -177,7 +257,7 @@ public class ResultSetTableIterator implements TableIterator { * * @see #convertType(String, String) */ - public ResultSetTableIterator(final ResultSet dataSet, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{ + public ResultSetTableIterator(final ResultSet dataSet, final JDBCTranslator translator, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{ // A dataset MUST BE provided: if (dataSet == null) throw new NullPointerException("Missing ResultSet object over which to iterate!"); @@ -185,6 +265,9 @@ public class ResultSetTableIterator implements TableIterator { // Keep a reference to the ResultSet: data = dataSet; + // Set the translator to use (if needed): + this.translator = translator; + // Count columns and determine their type: try{ // get the metadata: @@ -198,11 +281,11 @@ public class ResultSetTableIterator implements TableIterator { try{ colMeta[i - 1] = (TAPColumn)resultMeta[i - 1]; }catch(ClassCastException cce){ - DBType datatype = convertType(metadata.getColumnTypeName(i), dbms); + DBType datatype = convertType(metadata.getColumnType(i), metadata.getColumnTypeName(i), dbms); colMeta[i - 1] = new TAPColumn(resultMeta[i - 1].getADQLName(), datatype); } }else{ - DBType datatype = convertType(metadata.getColumnTypeName(i), dbms); + DBType datatype = convertType(metadata.getColumnType(i), metadata.getColumnTypeName(i), dbms); colMeta[i - 1] = new TAPColumn(metadata.getColumnLabel(i), datatype); } } @@ -274,12 +357,23 @@ public class ResultSetTableIterator implements TableIterator { // Get the column value: try{ Object o = data.getObject(++colIndex); - // if the column value is a Timestamp object, format it in ISO8601: - if (o != null && o instanceof Timestamp) - o = ISO8601Format.format(((Timestamp)o).getTime()); + if (o != null){ + DBType colType = getColType(); + // if the column value is a Timestamp object, format it in ISO8601: + if (o instanceof Timestamp) + o = ISO8601Format.format(((Timestamp)o).getTime()); + // if the column value is a geometrical object, it must be serialized in STC-S: + else if (translator != null && colType.isGeometry()){ + Region region = translator.translateGeometryFromDB(o); + if (region != null) + o = region.toSTCS(); + } + } return o; }catch(SQLException se){ throw new DataReadException("Can not read the value of the " + colIndex + "-th column!", se); + }catch(ParseException pe){ + throw new DataReadException(pe.getMessage()); } } @@ -298,62 +392,106 @@ public class ResultSetTableIterator implements TableIterator { return colMeta[colIndex - 1].getDatatype(); } + /** + * <p>Convert the given DBMS type into the corresponding {@link DBType} instance.</p> + * + * <p> + * This function first tries the conversion using the translator ({@link JDBCTranslator#convertTypeFromDB(int, String, String, String[])}). + * If the translator fails, a default conversion is done. + * </p> + * + * <p><b>Warning: + * It is not recommended to rely on the default conversion. + * This conversion is just a matter of guessing the better matching {@link DBType} + * considering the types of the following DBMS: PostgreSQL, SQLite, MySQL, Oracle and Java/DB/Derby. + * </b></p> + * + * @param dbmsType DBMS column data-type name. + * @param dbms Lower-case string which indicates which DBMS the ResultSet is coming from. <i>note: MAY be NULL.</i> + * + * @return The best suited {@link DBType} object. + * + * @see JDBCTranslator#convertTypeFromDB(int, String, String, String[]) + * @see #defaultTypeConversion(String, String[], String) + */ + protected DBType convertType(final int dbmsType, String dbmsTypeName, final String dbms) throws DataReadException{ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); + + // Extract the type prefix and lower-case it: + int startParamIndex = dbmsTypeName.indexOf('('), endParamIndex = dbmsTypeName.indexOf(')'); + String dbmsTypePrefix = (startParamIndex <= 0) ? dbmsTypeName : dbmsTypeName.substring(0, endParamIndex); + dbmsTypePrefix = dbmsTypePrefix.trim().toLowerCase(); + String[] typeParams = (startParamIndex <= 0) ? null : dbmsTypeName.substring(startParamIndex + 1, endParamIndex).split(","); + + // Ask first to the translator: + DBType dbType = null; + if (translator != null) + dbType = translator.convertTypeFromDB(dbmsType, dbmsTypeName, dbmsTypePrefix, typeParams); + + // And if unsuccessful, apply a default conversion: + if (dbType == null) + dbType = defaultTypeConversion(dbmsTypePrefix, typeParams, dbms); + + return dbType; + } + /** * <p>Convert the given DBMS type into the better matching {@link DBType} instance. - * This function is used to guess the TAP type of a column when it is not provided in the constructor. + * This function is used to <b>guess</b> the TAP type of a column when it is not provided in the constructor. * It aims not to be exhaustive, but just to provide a type when the given TAP metadata are incomplete.</p> * * <p><i>Note: - * Any unknown DBMS datatype will be considered and translated as a VARCHAR. - * The same type will be returned if the given parameter is an empty string or NULL. + * Any unknown DBMS data-type will be considered and translated as a VARCHAR. + * This latter will be also returned if the given parameter is an empty string or NULL. * </i></p> * * <p><i>Note: - * This type conversion function has been designed to work with all standard datatypes of the following DBMS: + * This type conversion function has been designed to work with all standard data-types of the following DBMS: * PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby. * </i></p> * * <p><i><b>Important</b>: - * <b>The second parameter is REALLY NEEDED when the DBMS is SQLite ("sqlite")!</b> - * Indeed, SQLite has a so restrictive list of datatypes that this function can reliably convert its types + * <b>The third parameter is REALLY NEEDED when the DBMS is SQLite ("sqlite")!</b> + * Indeed, SQLite has a so restrictive list of data-types that this function can reliably convert * only if it knows the DBMS is SQLite. Otherwise, the conversion result would be unpredictable. * </i>In this default implementation of this function, all other DBMS values are ignored.<i> * </i></p> * * <p><b>Warning</b>: - * This function is not translating the geometrical datatypes. If a such datatype is encountered, + * This function is not translating the geometrical data-types. If a such data-type is encountered, * it will considered as unknown and so, a VARCHAR TAP type will be returned. * </p> * - * @param dbmsType DBMS column datatype name. - * @param dbms Lower-case string which indicates which DBMS the ResultSet is coming from. <i>note: MAY be NULL.</i> + * @param dbmsTypeName Name of type, without the eventual parameters. + * @param params The eventual type parameters (e.g. char string length). + * @param dbms The targeted DBMS. * - * @return The best suited {@link DBType} object. + * @return The corresponding ADQL/TAP type. <i>NEVER NULL</i> */ - protected DBType convertType(String dbmsType, final String dbms){ - // If no type is provided return VARCHAR: - if (dbmsType == null || dbmsType.trim().length() == 0) - return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH); - - // Extract the type prefix and lower-case it: - dbmsType = dbmsType.toLowerCase(); - int paramIndex = dbmsType.indexOf('('); - String dbmsTypePrefix = (paramIndex <= 0) ? dbmsType : dbmsType.substring(0, paramIndex); - int firstParam = getLengthParam(dbmsTypePrefix, paramIndex); + protected final DBType defaultTypeConversion(final String dbmsTypeName, final String[] params, final String dbms){ + // Get the length parameter (always in first position): + int lengthParam = DBType.NO_LENGTH; + if (params != null && params.length > 0){ + try{ + lengthParam = Integer.parseInt(params[0]); + }catch(NumberFormatException nfe){} + } // CASE: SQLITE if (dbms != null && dbms.equals("sqlite")){ // INTEGER -> SMALLINT, INTEGER, BIGINT - if (dbmsTypePrefix.equals("integer")) + if (dbmsTypeName.equals("integer")) return new DBType(DBDatatype.BIGINT); // REAL -> REAL, DOUBLE - else if (dbmsTypePrefix.equals("real")) + else if (dbmsTypeName.equals("real")) return new DBType(DBDatatype.DOUBLE); // TEXT -> CHAR, VARCHAR, CLOB, TIMESTAMP - else if (dbmsTypePrefix.equals("text")) + else if (dbmsTypeName.equals("text")) return new DBType(DBDatatype.VARCHAR); // BLOB -> BINARY, VARBINARY, BLOB - else if (dbmsTypePrefix.equals("blob")) + else if (dbmsTypeName.equals("blob")) return new DBType(DBDatatype.BLOB); // Default: else @@ -362,40 +500,40 @@ public class ResultSetTableIterator implements TableIterator { // CASE: OTHER DBMS else{ // SMALLINT - if (dbmsTypePrefix.equals("smallint") || dbmsTypePrefix.equals("int2")) + if (dbmsTypeName.equals("smallint") || dbmsTypeName.equals("int2") || dbmsTypeName.equals("smallserial") || dbmsTypeName.equals("serial2") || dbmsTypeName.equals("boolean") || dbmsTypeName.equals("bool")) return new DBType(DBDatatype.SMALLINT); // INTEGER - else if (dbmsTypePrefix.equals("integer") || dbmsTypePrefix.equals("int") || dbmsTypePrefix.equals("int4")) + else if (dbmsTypeName.equals("integer") || dbmsTypeName.equals("int") || dbmsTypeName.equals("int4") || dbmsTypeName.equals("serial") || dbmsTypeName.equals("serial4")) return new DBType(DBDatatype.INTEGER); // BIGINT - else if (dbmsTypePrefix.equals("bigint") || dbmsTypePrefix.equals("int8") || dbmsTypePrefix.equals("number")) + else if (dbmsTypeName.equals("bigint") || dbmsTypeName.equals("int8") || dbmsTypeName.equals("bigserial") || dbmsTypeName.equals("bigserial8") || dbmsTypeName.equals("number")) return new DBType(DBDatatype.BIGINT); // REAL - else if (dbmsTypePrefix.equals("float4") || (dbmsTypePrefix.equals("float") && firstParam <= 63)) + else if (dbmsTypeName.equals("real") || dbmsTypeName.equals("float4") || (dbmsTypeName.equals("float") && lengthParam <= 63)) return new DBType(DBDatatype.REAL); // DOUBLE - else if (dbmsTypePrefix.equals("double") || dbmsTypePrefix.equals("double precision") || dbmsTypePrefix.equals("float8") || (dbmsTypePrefix.equals("float") && firstParam > 63)) + else if (dbmsTypeName.equals("double") || dbmsTypeName.equals("double precision") || dbmsTypeName.equals("float8") || (dbmsTypeName.equals("float") && lengthParam > 63)) return new DBType(DBDatatype.DOUBLE); // BINARY - else if (dbmsTypePrefix.equals("binary") || dbmsTypePrefix.equals("raw") || ((dbmsTypePrefix.equals("char") || dbmsTypePrefix.equals("character")) && dbmsType.endsWith(" for bit data"))) - return new DBType(DBDatatype.BINARY, firstParam); + else if (dbmsTypeName.equals("bit") || dbmsTypeName.equals("binary") || dbmsTypeName.equals("raw") || ((dbmsTypeName.equals("char") || dbmsTypeName.equals("character")) && dbmsTypeName.endsWith(" for bit data"))) + return new DBType(DBDatatype.BINARY, lengthParam); // VARBINARY - else if (dbmsTypePrefix.equals("varbinary") || dbmsTypePrefix.equals("long raw") || ((dbmsTypePrefix.equals("varchar") || dbmsTypePrefix.equals("character varying")) && dbmsType.endsWith(" for bit data"))) - return new DBType(DBDatatype.VARBINARY, firstParam); + else if (dbmsTypeName.equals("bit varying") || dbmsTypeName.equals("varbit") || dbmsTypeName.equals("varbinary") || dbmsTypeName.equals("long raw") || ((dbmsTypeName.equals("varchar") || dbmsTypeName.equals("character varying")) && dbmsTypeName.endsWith(" for bit data"))) + return new DBType(DBDatatype.VARBINARY, lengthParam); // CHAR - else if (dbmsTypePrefix.equals("char") || dbmsTypePrefix.equals("character")) - return new DBType(DBDatatype.CHAR, firstParam); + else if (dbmsTypeName.equals("char") || dbmsTypeName.equals("character")) + return new DBType(DBDatatype.CHAR, lengthParam); // VARCHAR - else if (dbmsTypePrefix.equals("varchar") || dbmsTypePrefix.equals("varchar2") || dbmsTypePrefix.equals("character varying")) - return new DBType(DBDatatype.VARBINARY, firstParam); + else if (dbmsTypeName.equals("varchar") || dbmsTypeName.equals("varchar2") || dbmsTypeName.equals("character varying")) + return new DBType(DBDatatype.VARCHAR, lengthParam); // BLOB - else if (dbmsTypePrefix.equals("bytea") || dbmsTypePrefix.equals("blob") || dbmsTypePrefix.equals("binary large object")) + else if (dbmsTypeName.equals("bytea") || dbmsTypeName.equals("blob") || dbmsTypeName.equals("binary large object")) return new DBType(DBDatatype.BLOB); // CLOB - else if (dbmsTypePrefix.equals("text") || dbmsTypePrefix.equals("clob") || dbmsTypePrefix.equals("character large object")) + else if (dbmsTypeName.equals("text") || dbmsTypeName.equals("clob") || dbmsTypeName.equals("character large object")) return new DBType(DBDatatype.CLOB); // TIMESTAMP - else if (dbmsTypePrefix.equals("timestamp")) + else if (dbmsTypeName.equals("timestamp") || dbmsTypeName.equals("timestamptz") || dbmsTypeName.equals("time") || dbmsTypeName.equals("timetz") || dbmsTypeName.equals("date")) return new DBType(DBDatatype.TIMESTAMP); // Default: else @@ -403,46 +541,4 @@ public class ResultSetTableIterator implements TableIterator { } } - /** - * <p>Extract the 'length' parameter of a DBMS type string.</p> - * - * <p> - * If the given type string does not contain any parameter - * OR if the first parameter can not be casted into an integer, - * {@link DBType#NO_LENGTH} will be returned. - * </p> - * - * @param dbmsType DBMS type string (containing the datatype and the 'length' parameter). - * @param paramIndex Index of the open bracket. - * - * @return The 'length' parameter value if found, {@link DBType#NO_LENGTH} otherwise. - */ - protected final int getLengthParam(final String dbmsType, final int paramIndex){ - // If no parameter has been previously detected, no length parameter: - if (paramIndex <= 0) - return DBType.NO_LENGTH; - - // If there is one and that at least ONE parameter is provided.... - else{ - int lengthParam = DBType.NO_LENGTH; - String paramsStr = dbmsType.substring(paramIndex + 1); - - // ...extract the 'length' parameter: - /* note: we suppose here that no other parameter is possible ; - * but if there are, they are ignored and we try to consider the first parameter - * as the length */ - int paramEndIndex = paramsStr.indexOf(','); - if (paramEndIndex <= 0) - paramEndIndex = paramsStr.indexOf(')'); - - // ...cast it into an integer: - try{ - lengthParam = Integer.parseInt(paramsStr.substring(0, paramEndIndex)); - }catch(Exception ex){} - - // ...and finally return it: - return lengthParam; - } - } - } diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index 1e1a836f92d422fbf8ed4f89247d203993947a56..437ffdba5399b1b29136ea6263dd668e68630f11 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -49,8 +49,11 @@ import tap.metadata.TAPTable; import tap.metadata.TAPTable.TableType; import uws.ISO8601Format; import uws.service.log.UWSLog.LogLevel; +import adql.db.DBColumn; import adql.db.DBType; import adql.db.DBType.DBDatatype; +import adql.db.STCS; +import adql.db.STCS.Region; import adql.query.ADQLQuery; import adql.query.IdentifierField; import adql.translator.ADQLTranslator; @@ -97,25 +100,26 @@ import adql.translator.TranslationException; * * <h3>Datatypes</h3> * - * <p>Column types are converted from DBMS to TAP types with {@link #getTAPType(String)} and from TAP to DBMS types with {@link #getDBMSDatatype(DBType)}.</p> + * <p> + * All datatype conversions done while fetching a query result (via a {@link ResultSet}) + * are done exclusively by the returned {@link TableIterator} (so, here {@link ResultSetTableIterator}). + * </p> * * <p> - * All typical DBMS datatypes are taken into account, <b>EXCEPT the geometrical types</b> (POINT and REGION). For these types, the only object having this - * information is the translator thanks to {@link JDBCTranslator#isPointType(String)}, {@link JDBCTranslator#isRegionType(String)}, - * {@link JDBCTranslator#getPointType()} and {@link JDBCTranslator#getRegionType()}. The two first functions are used to identify a DBMS type as a point or - * a region (note: several DBMS datatypes may be identified as a geometry type). The two others provide the DBMS type corresponding the best to the TAP types - * POINT and REGION. + * However, datatype conversions done while uploading a table are done here by the function + * {@link #convertTypeToDB(DBType)}. This function uses first the conversion function of the translator + * ({@link JDBCTranslator#convertTypeToDB(DBType)}), and then {@link #defaultTypeConversion(DBType)} + * if it fails. * </p> * - * <p><i><b>Warning:</b> - * The TAP type REGION can be either a circle, a box or a polygon. Since several DBMS types correspond to one TAP type, {@link JDBCTranslator#getRegionType()} - * MUST return a type covering all these region datatypes. Generally, it will be a VARCHAR whose the values would be STC-S expressions. - * Note that this function is used ONLY WHEN tables with a geometrical value is uploaded. On the contrary, {@link JDBCTranslator#isRegionType(String)} - * is used much more often: in order to write the metadata part of a query result. - * </i></p> + * <p> + * In this default conversion, all typical DBMS datatypes are taken into account, <b>EXCEPT the geometrical types</b> + * (POINT and REGION). That's why it is recommended to use a translator in which the geometrical types are supported + * and managed. + * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (09/2014) + * @version 2.0 (11/2014) * @since 2.0 */ public class JDBCConnection implements DBConnection { @@ -331,33 +335,54 @@ public class JDBCConnection implements DBConnection { ResultSet result = null; try{ // 1. Translate the ADQL query into SQL: - logger.logDB(LogLevel.INFO, this, "TRANSLATE", "Translating ADQL: " + adqlQuery.toADQL().replaceAll("(\t|\r?\n)+", " "), null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "TRANSLATE", "Translating ADQL: " + adqlQuery.toADQL().replaceAll("(\t|\r?\n)+", " "), null); sql = translator.translate(adqlQuery); // 2. Execute the SQL query: Statement stmt = connection.createStatement(); - logger.logDB(LogLevel.INFO, this, "EXECUTE", "Executing translated query: " + sql.replaceAll("(\t|\r?\n)+", " "), null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "EXECUTE", "Executing translated query: " + sql.replaceAll("(\t|\r?\n)+", " "), null); result = stmt.executeQuery(sql); // 3. Return the result through a TableIterator object: - logger.logDB(LogLevel.INFO, this, "RESULT", "Returning result", null); - return new ResultSetTableIterator(result, dbms, adqlQuery.getResultingColumns()); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "RESULT", "Returning result", null); + return createTableIterator(result, adqlQuery.getResultingColumns()); }catch(SQLException se){ close(result); - logger.logDB(LogLevel.ERROR, this, "EXECUTE", "Unexpected error while EXECUTING SQL query!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXECUTE", "Unexpected error while EXECUTING SQL query!", se); throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se); }catch(TranslationException te){ close(result); - logger.logDB(LogLevel.ERROR, this, "TRANSLATE", "Unexpected error while TRANSLATING ADQL into SQL!", te); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "TRANSLATE", "Unexpected error while TRANSLATING ADQL into SQL!", te); throw new DBException("Unexpected error while translating ADQL into SQL: " + te.getMessage(), te); }catch(DataReadException dre){ close(result); - logger.logDB(LogLevel.ERROR, this, "RESULT", "Unexpected error while reading the query result!", dre); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "RESULT", "Unexpected error while reading the query result!", dre); throw new DBException("Impossible to read the query result, because: " + dre.getMessage(), dre); } } + /** + * Create a {@link TableIterator} instance which lets reading the given result table. + * + * @param rs Result of an SQL query. + * @param resultingColumns Metadata corresponding to each columns of the result. + * + * @return A {@link TableIterator} instance. + * + * @throws DataReadException If the metadata (columns count and types) can not be fetched + * or if any other error occurs. + */ + protected TableIterator createTableIterator(final ResultSet rs, final DBColumn[] resultingColumns) throws DataReadException{ + return new ResultSetTableIterator(rs, translator, dbms, resultingColumns); + } + /* *********************** */ /* TAP_SCHEMA MANIPULATION */ /* *********************** */ @@ -434,23 +459,28 @@ public class JDBCConnection implements DBConnection { stmt = connection.createStatement(); // load all schemas from TAP_SCHEMA.schemas: - logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.schemas.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.schemas.", null); loadSchemas(tap_schema.getTable(STDTable.SCHEMAS.label), metadata, stmt); // load all tables from TAP_SCHEMA.tables: - logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.tables.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.tables.", null); List<TAPTable> lstTables = loadTables(tap_schema.getTable(STDTable.TABLES.label), metadata, stmt); // load all columns from TAP_SCHEMA.columns: - logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.columns.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.columns.", null); loadColumns(tap_schema.getTable(STDTable.COLUMNS.label), lstTables, stmt); // load all foreign keys from TAP_SCHEMA.keys and TAP_SCHEMA.key_columns: - logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.keys and TAP_SCHEMA.key_columns.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "LOAD_TAP_SCHEMA", "Loading TAP_SCHEMA.keys and TAP_SCHEMA.key_columns.", null); loadKeys(tap_schema.getTable(STDTable.KEYS.label), tap_schema.getTable(STDTable.KEY_COLUMNS.label), lstTables, stmt); }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create a Statement!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create a Statement!", se); throw new DBException("Can not create a Statement!", se); }finally{ close(stmt); @@ -500,7 +530,8 @@ public class JDBCConnection implements DBConnection { metadata.addSchema(newSchema); } }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load schemas from TAP_SCHEMA.schemas!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load schemas from TAP_SCHEMA.schemas!", se); throw new DBException("Impossible to load schemas from TAP_SCHEMA.schemas!", se); }finally{ close(rs); @@ -553,7 +584,8 @@ public class JDBCConnection implements DBConnection { // get the schema: TAPSchema schema = metadata.getSchema(schemaName); if (schema == null){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!", null); throw new DBException("Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!"); } @@ -579,7 +611,8 @@ public class JDBCConnection implements DBConnection { return lstTables; }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load tables from TAP_SCHEMA.tables!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load tables from TAP_SCHEMA.tables!", se); throw new DBException("Impossible to load tables from TAP_SCHEMA.tables!", se); }finally{ close(rs); @@ -630,7 +663,8 @@ public class JDBCConnection implements DBConnection { // get the table: TAPTable table = searchTable(tableName, lstTables.iterator()); if (table == null){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!", null); throw new DBException("Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!"); } @@ -659,7 +693,8 @@ public class JDBCConnection implements DBConnection { table.addColumn(newColumn); } }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); }finally{ close(rs); @@ -713,12 +748,14 @@ public class JDBCConnection implements DBConnection { // get the two tables (source and target): TAPTable sourceTable = searchTable(from_table, lstTables.iterator()); if (sourceTable == null){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!", null); throw new DBException("Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!"); } TAPTable targetTable = searchTable(target_table, lstTables.iterator()); if (targetTable == null){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!", null); throw new DBException("Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!"); } @@ -731,7 +768,8 @@ public class JDBCConnection implements DBConnection { while(rsKeyCols.next()) columns.put(rsKeyCols.getString(1), rsKeyCols.getString(2)); }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); throw new DBException("Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se); }finally{ close(rsKeyCols); @@ -741,12 +779,14 @@ public class JDBCConnection implements DBConnection { try{ sourceTable.addForeignKey(key_id, targetTable, columns, nullifyIfNeeded(description), nullifyIfNeeded(utype)); }catch(Exception ex){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); throw new DBException("Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex); } } }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "LOAD_TAP_SCHEMA", "Impossible to load columns from TAP_SCHEMA.columns!", se); throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se); }finally{ close(rs); @@ -792,26 +832,31 @@ public class JDBCConnection implements DBConnection { stmt = connection.createStatement(); // 1. Ensure TAP_SCHEMA exists and drop all its standard TAP tables: - logger.logDB(LogLevel.INFO, this, "CLEAN_TAP_SCHEMA", "Cleaning TAP_SCHEMA.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CLEAN_TAP_SCHEMA", "Cleaning TAP_SCHEMA.", null); resetTAPSchema(stmt, stdTables); // 2. Create all standard TAP tables: - logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Creating TAP_SCHEMA tables.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Creating TAP_SCHEMA tables.", null); for(TAPTable table : stdTables) createTAPSchemaTable(table, stmt); // C. FILL THE NEW TABLE USING THE GIVEN DATA ITERATOR: - logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Filling TAP_SCHEMA tables.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Filling TAP_SCHEMA tables.", null); fillTAPSchema(metadata); // D. CREATE THE INDEXES OF ALL STANDARD TAP TABLES: - logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Creating TAP_SCHEMA tables' indexes.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "CREATE_TAP_SCHEMA", "Creating TAP_SCHEMA tables' indexes.", null); for(TAPTable table : stdTables) createTAPTableIndexes(table, stmt); commit(); }catch(SQLException se){ - logger.logDB(LogLevel.ERROR, this, "CREATE_TAP_SCHEMA", "Impossible to SET TAP_SCHEMA in DB!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "CREATE_TAP_SCHEMA", "Impossible to SET TAP_SCHEMA in DB!", se); rollback(); throw new DBException("Impossible to SET TAP_SCHEMA in DB!", se); }finally{ @@ -1040,7 +1085,7 @@ public class JDBCConnection implements DBConnection { sql.append(translator.getColumnName(col)); // column type: - sql.append(' ').append(getDBMSDatatype(col.getDatatype())); + sql.append(' ').append(convertTypeToDB(col.getDatatype())); // last column ? if (it.hasNext()) @@ -1486,12 +1531,14 @@ public class JDBCConnection implements DBConnection { // 1. Create the upload schema, if it does not already exist: if (!isSchemaExisting(tableDef.getDBSchemaName(), dbMeta)){ stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(tableDef) + ";"); - logger.logDB(LogLevel.INFO, this, "SCHEMA_CREATED", "Schema \"" + tableDef.getADQLSchemaName() + "\" (in DB: " + translator.getQualifiedSchemaName(tableDef) + ") created.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "SCHEMA_CREATED", "Schema \"" + tableDef.getADQLSchemaName() + "\" (in DB: " + translator.getQualifiedSchemaName(tableDef) + ") created.", null); } // 1bis. Ensure the table does not already exist and if it is the case, throw an understandable exception: else if (isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), dbMeta)){ DBException de = new DBException("Impossible to create the user uploaded table in the database: " + translator.getQualifiedTableName(tableDef) + "! This table already exists."); - logger.logDB(LogLevel.ERROR, this, "ADD_UPLOAD_TABLE", de.getMessage(), de); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "ADD_UPLOAD_TABLE", de.getMessage(), de); throw de; } @@ -1505,7 +1552,7 @@ public class JDBCConnection implements DBConnection { // column name: sqlBuf.append(translator.getColumnName(col)); // column type: - sqlBuf.append(' ').append(getDBMSDatatype(col.getDatatype())); + sqlBuf.append(' ').append(convertTypeToDB(col.getDatatype())); // last column ? if (it.hasNext()) sqlBuf.append(','); @@ -1521,13 +1568,15 @@ public class JDBCConnection implements DBConnection { commit(); // Log the end: - logger.logDB(LogLevel.INFO, this, "TABLE_CREATED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") created.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "TABLE_CREATED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") created.", null); return true; }catch(SQLException se){ rollback(); - logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); throw new DBException("Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); }catch(DBException de){ rollback(); @@ -1549,6 +1598,10 @@ public class JDBCConnection implements DBConnection { * In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed. * </i></p> * + * <p><i>Note: + * This function proceeds to a formatting of TIMESTAMP and GEOMETRY (point, circle, box, polygon) values. + * </i></p> + * * @param metaTable Description of the updated table. * @param data Iterator over the rows to insert. * @@ -1589,14 +1642,30 @@ public class JDBCConnection implements DBConnection { int c = 1; while(data.hasNextCol()){ Object val = data.nextCol(); - /* If the value is supposed to be a Timestamp, parse it - * and build an appropriate SQL object: */ - if (val != null && cols[c - 1].getDatatype().type == DBDatatype.TIMESTAMP){ - try{ - val = new Timestamp(ISO8601Format.parse(val.toString())); - }catch(ParseException pe){ - logger.logDB(LogLevel.ERROR, this, "UPLOAD", "Unexpected date format for the " + c + "-th column (" + val + ")! A date formatted in ISO8601 was expected.", pe); - throw new DBException("Unexpected date format for the " + c + "-th column (" + val + ")! A date formatted in ISO8601 was expected.", pe); + if (val != null && cols[c - 1] != null){ + /* TIMESTAMP FORMATTING */ + if (cols[c - 1].getDatatype().type == DBDatatype.TIMESTAMP){ + try{ + val = new Timestamp(ISO8601Format.parse(val.toString())); + }catch(ParseException pe){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "UPLOAD", "Unexpected date format for the " + c + "-th column (" + val + ")! A date formatted in ISO8601 was expected.", pe); + throw new DBException("Unexpected date format for the " + c + "-th column (" + val + ")! A date formatted in ISO8601 was expected.", pe); + } + } + /* GEOMETRY FORMATTING */ + else if (cols[c - 1].getDatatype().type == DBDatatype.POINT || cols[c - 1].getDatatype().type == DBDatatype.REGION){ + Region region; + try{ + // parse the region as an STC-S expression: + region = STCS.parseRegion(val.toString()); + // translate this STC region into the corresponding column value: + val = translator.translateGeometryToDB(region); + }catch(adql.parser.ParseException e){ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "UPLOAD", "Incorrect STC-S syntax for the geometrical value \"" + val + "\"!", e); + throw new DataReadException("Incorrect STC-S syntax for the geometrical value \"" + val + "\"!", e); + } } } stmt.setObject(c++, val); @@ -1653,16 +1722,19 @@ public class JDBCConnection implements DBConnection { int cnt = stmt.executeUpdate("DROP TABLE " + translator.getQualifiedTableName(tableDef) + ";"); // Log the end: - if (cnt == 0) - logger.logDB(LogLevel.INFO, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") dropped.", null); - else - logger.logDB(LogLevel.ERROR, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") NOT dropped.", null); + if (logger != null){ + if (cnt == 0) + logger.logDB(LogLevel.INFO, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") dropped.", null); + else + logger.logDB(LogLevel.ERROR, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") NOT dropped.", null); + } // Ensure the update is successful: return (cnt == 0); }catch(SQLException se){ - logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); throw new DBException("Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se); }finally{ close(stmt); @@ -1707,7 +1779,28 @@ public class JDBCConnection implements DBConnection { /* ************** */ /** - * <p>Get the DBMS compatible datatype corresponding to the given column TAPType.</p> + * <p>Convert the given TAP type into the corresponding DBMS column type.</p> + * + * <p> + * This function tries first the type conversion using the translator ({@link JDBCTranslator#convertTypeToDB(DBType)}). + * If it fails, a default conversion is done considering all the known types of the following DBMS: + * PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby. + * </p> + * + * @param type TAP type to convert. + * + * @return The corresponding DBMS type. + * + * @see JDBCTranslator#convertTypeToDB(DBType) + * @see #defaultTypeConversion(DBType) + */ + protected String convertTypeToDB(final DBType type){ + String dbmsType = translator.convertTypeToDB(type); + return (dbmsType == null) ? defaultTypeConversion(type) : dbmsType; + } + + /** + * <p>Get the DBMS compatible datatype corresponding to the given column {@link DBType}.</p> * * <p><i>Note 1: * This function is able to generate a DB datatype compatible with the currently used DBMS. @@ -1728,7 +1821,7 @@ public class JDBCConnection implements DBConnection { * * @return The corresponding DB type, or NULL if the given type is not managed or is NULL. */ - protected String getDBMSDatatype(DBType datatype){ + protected String defaultTypeConversion(DBType datatype){ if (datatype == null) datatype = new DBType(DBDatatype.VARCHAR); @@ -1838,11 +1931,13 @@ public class JDBCConnection implements DBConnection { try{ if (supportsTransaction){ connection.setAutoCommit(false); - logger.logDB(LogLevel.INFO, this, "START_TRANSACTION", "Transaction STARTED.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "START_TRANSACTION", "Transaction STARTED.", null); } }catch(SQLException se){ supportsTransaction = false; - logger.logDB(LogLevel.ERROR, this, "START_TRANSACTION", "Transaction STARTing impossible!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "START_TRANSACTION", "Transaction STARTing impossible!", se); throw new DBException("Transaction STARTing impossible!", se); } } @@ -1869,11 +1964,13 @@ public class JDBCConnection implements DBConnection { try{ if (supportsTransaction){ connection.commit(); - logger.logDB(LogLevel.INFO, this, "COMMIT", "Transaction COMMITED.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "COMMIT", "Transaction COMMITED.", null); } }catch(SQLException se){ supportsTransaction = false; - logger.logDB(LogLevel.ERROR, this, "COMMIT", "Transaction COMMIT impossible!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "COMMIT", "Transaction COMMIT impossible!", se); throw new DBException("Transaction COMMIT impossible!", se); } } @@ -1900,11 +1997,13 @@ public class JDBCConnection implements DBConnection { try{ if (supportsTransaction){ connection.rollback(); - logger.logDB(LogLevel.INFO, this, "ROLLBACK", "Transaction ROLLBACKED.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "ROLLBACK", "Transaction ROLLBACKED.", null); } }catch(SQLException se){ supportsTransaction = false; - logger.logDB(LogLevel.ERROR, this, "ROLLBACK", "Transaction ROLLBACK impossible!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "ROLLBACK", "Transaction ROLLBACK impossible!", se); } } @@ -1929,11 +2028,13 @@ public class JDBCConnection implements DBConnection { try{ if (supportsTransaction){ connection.setAutoCommit(true); - logger.logDB(LogLevel.INFO, this, "END_TRANSACTION", "Transaction ENDED.", null); + if (logger != null) + logger.logDB(LogLevel.INFO, this, "END_TRANSACTION", "Transaction ENDED.", null); } }catch(SQLException se){ supportsTransaction = false; - logger.logDB(LogLevel.ERROR, this, "END_TRANSACTION", "Transaction ENDing impossible!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "END_TRANSACTION", "Transaction ENDing impossible!", se); } } @@ -1954,7 +2055,8 @@ public class JDBCConnection implements DBConnection { if (rs != null) rs.close(); }catch(SQLException se){ - logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a ResultSet!", null); + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a ResultSet!", null); } } @@ -1975,7 +2077,8 @@ public class JDBCConnection implements DBConnection { if (stmt != null) stmt.close(); }catch(SQLException se){ - logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a Statement!", null); + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "CLOSE", "Can not close a Statement!", null); } } @@ -2180,7 +2283,8 @@ public class JDBCConnection implements DBConnection { } if (cnt > 1){ - logger.logDB(LogLevel.ERROR, this, "TABLE_EXIST", "More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "TABLE_EXIST", "More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!", null); throw new DBException("More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!"); } @@ -2291,10 +2395,12 @@ public class JDBCConnection implements DBConnection { * Otherwise, it is impossible to insert the previous batched rows ; an exception must be thrown * and must stop the whole TAP_SCHEMA initialization. */ - if (indRow == 1) - logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "BATCH query impossible => TRYING AGAIN IN A NORMAL EXECUTION (executeUpdate())!", se); - else{ - logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH query impossible!", se); + if (indRow == 1){ + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "BATCH query impossible => TRYING AGAIN IN A NORMAL EXECUTION (executeUpdate())!", se); + }else{ + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH query impossible!", se); throw new DBException("BATCH query impossible!", se); } } @@ -2308,7 +2414,8 @@ public class JDBCConnection implements DBConnection { // Check the row has been inserted with success: if (nbRowsWritten != 1){ - logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "ROW " + indRow + " not inserted!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "ROW " + indRow + " not inserted!", null); throw new DBException("ROW " + indRow + " not inserted!"); } } @@ -2346,7 +2453,8 @@ public class JDBCConnection implements DBConnection { rows = stmt.executeBatch(); }catch(SQLException se){ supportsBatchUpdates = false; - logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH execution impossible!", se); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "BATCH execution impossible!", se); throw new DBException("BATCH execution impossible!", se); } @@ -2354,7 +2462,8 @@ public class JDBCConnection implements DBConnection { try{ stmt.clearBatch(); }catch(SQLException se){ - logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "CLEAR BATCH impossible!", se); + if (logger != null) + logger.logDB(LogLevel.WARNING, this, "EXEC_UPDATE", "CLEAR BATCH impossible!", se); } // Count the updated rows: @@ -2364,7 +2473,8 @@ public class JDBCConnection implements DBConnection { // Check all given rows have been inserted with success: if (nbRowsUpdated != nbRows){ - logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "ROWS not all update (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!", null); + if (logger != null) + logger.logDB(LogLevel.ERROR, this, "EXEC_UPDATE", "ROWS not all update (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!", null); throw new DBException("ROWS not all updated (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!"); } } diff --git a/src/tap/formatter/VOTableFormat.java b/src/tap/formatter/VOTableFormat.java index 7ce1f63abe5fc1bed42e3ffe77db86dad6000c97..d039e630d3e3a2b3bb0d48d98ab414c99cb2ca48 100644 --- a/src/tap/formatter/VOTableFormat.java +++ b/src/tap/formatter/VOTableFormat.java @@ -384,7 +384,10 @@ public class VOTableFormat implements OutputFormat { if (logFormatReport) service.getLogger().logTAP(LogLevel.INFO, execReport, "FORMAT", "Result formatted (in VOTable ; " + table.getNbReadRows() + " rows ; " + table.getColumnCount() + " columns) in " + (System.currentTimeMillis() - start) + "ms!", null); }catch(IOException ioe){ - throw new TAPException("Error while writing a query result in VOTable!", ioe); + if (ioe.getCause() != null && ioe.getCause() instanceof DataReadException) + throw (DataReadException)ioe.getCause(); + else + throw new TAPException("Error while writing a query result in VOTable!", ioe); } } @@ -432,7 +435,7 @@ public class VOTableFormat implements OutputFormat { * 2/ a GROUP item with the STC expression of the coordinate system. */ - out.flush(); + //out.flush(); // TODO DEBUG flush() => commit } /** diff --git a/test/adql/translator/TestPgSphereTranslator.java b/test/adql/translator/TestPgSphereTranslator.java new file mode 100644 index 0000000000000000000000000000000000000000..2f34471fc7be78303a7945fee526c63fb425e71f --- /dev/null +++ b/test/adql/translator/TestPgSphereTranslator.java @@ -0,0 +1,332 @@ +package adql.translator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Types; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.postgresql.util.PGobject; + +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.parser.ParseException; + +public class TestPgSphereTranslator { + + @BeforeClass + public static void setUpBeforeClass() throws Exception{} + + @AfterClass + public static void tearDownAfterClass() throws Exception{} + + @Before + public void setUp() throws Exception{} + + @After + public void tearDown() throws Exception{} + + @Test + public void testConvertTypeFromDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + + // POINT + DBType type = translator.convertTypeFromDB(Types.OTHER, "spoint", "spoint", null); + assertNotNull(type); + assertEquals(DBDatatype.POINT, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + + // CIRCLE + type = translator.convertTypeFromDB(Types.OTHER, "scircle", "scircle", null); + assertNotNull(type); + assertEquals(DBDatatype.REGION, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + + // BOX + type = translator.convertTypeFromDB(Types.OTHER, "sbox", "sbox", null); + assertNotNull(type); + assertEquals(DBDatatype.REGION, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + + // POLYGON + type = translator.convertTypeFromDB(Types.OTHER, "spoly", "spoly", null); + assertNotNull(type); + assertEquals(DBDatatype.REGION, type.type); + assertEquals(DBType.NO_LENGTH, type.length); + } + + @Test + public void testConvertTypeToDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + + // NULL + assertEquals("VARCHAR", translator.convertTypeToDB(null)); + + // POINT + assertEquals("spoint", translator.convertTypeToDB(new DBType(DBDatatype.POINT))); + + // REGION (any other region is transformed into a polygon) + assertEquals("spoly", translator.convertTypeToDB(new DBType(DBDatatype.REGION))); + } + + @Test + public void testTranslateGeometryFromDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + PGobject pgo = new PGobject(); + + // NULL + try{ + assertNull(translator.translateGeometryFromDB(null)); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SPOINT + try{ + pgo.setType("spoint"); + pgo.setValue("(0.1 , 0.2)"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + + pgo.setValue("(5.72957d , 11.45915d)"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + + pgo.setValue("( 5d 43m 46.480625s , +11d 27m 32.961249s)"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + + pgo.setValue("( 0h 22m 55.098708s , +11d 27m 32.961249s)"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(11.45915, r.coordinates[0][1], 1e-5); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SCIRCLE + try{ + pgo.setType("scircle"); + pgo.setValue("<(0.1,-0.2),1>"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + + pgo.setValue("<(5.72957d , -11.45915d) , 57.29577d>"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + + pgo.setValue("<( 5d 43m 46.452s , -11d 27m 32.94s) , 57d 17m 44.772s>"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + + pgo.setValue("<( 0h 22m 55.0968s , -11d 27m 32.94s) , 57d 17m 44.772s>"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(5.72957, r.coordinates[0][0], 1e-5); + assertEquals(-11.45915, r.coordinates[0][1], 1e-5); + assertEquals(57.29577, r.radius, 1e-5); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SBOX + try{ + pgo.setType("sbox"); + pgo.setValue("((0.1,0.2),(0.5,0.5))"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + + pgo.setValue("((5.72957795130823d , 11.4591559026165d), (28.6478897565412d , 28.6478897565412d))"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + + pgo.setValue("(( 5d 43m 46.480625s , +11d 27m 32.961249s), ( 28d 38m 52.403124s , +28d 38m 52.403124s))"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + + pgo.setValue("(( 0h 22m 55.098708s , +11d 27m 32.961249s), ( 1h 54m 35.493542s , +28d 38m 52.403124s))"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(17.18873, r.coordinates[0][0], 1e-5); + assertEquals(20.05352, r.coordinates[0][1], 1e-5); + assertEquals(22.91831, r.width, 1e-5); + assertEquals(17.18873, r.height, 1e-5); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // SPOLY + try{ + pgo.setType("spoly"); + pgo.setValue("{(0.789761486527434 , 0.00436332312998582),(0.789761486527434 , 0.00872664625997165),(0.785398163397448 , 0.00872664625997165),(0.785398163397448 , 0.00436332312998582),(0.781034840267463 , 0.00436332312998582),(0.781034840267463 , 0),(0.785398163397448 , 0)}"); + Region r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + + pgo.setValue("{(45.25d , 0.25d), (45.25d , 0.5d), (45d , 0.5d), (45d , 0.25d), (44.75d , 0.25d), (44.75d , 0d), (45d , 0d)}"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + + pgo.setValue("{( 45d 15m 0s , + 0d 15m 0s),( 45d 15m 0s , + 0d 30m 0s),( 45d 0m 0s , + 0d 30m 0s),( 45d 0m 0s , + 0d 15m 0s),( 44d 45m 0s , + 0d 15m 0s),( 44d 45m 0s , + 0d 0m 0s),( 45d 0m 0s , + 0d 0m 0s)}"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + + pgo.setValue("{( 3h 1m 0s , + 0d 15m 0s),( 3h 1m 0s , + 0d 30m 0s),( 3h 0m 0s , + 0d 30m 0s),( 3h 0m 0s , + 0d 15m 0s),( 2h 59m 0s , + 0d 15m 0s),( 2h 59m 0s , + 0d 0m 0s),( 3h 0m 0s , + 0d 0m 0s)}"); + r = translator.translateGeometryFromDB(pgo); + assertEquals(45.25, r.coordinates[0][0], 1e-2); + assertEquals(0.25, r.coordinates[0][1], 1e-2); + assertEquals(45.25, r.coordinates[1][0], 1e-2); + assertEquals(0.5, r.coordinates[1][1], 1e-2); + assertEquals(45, r.coordinates[2][0], 1e-2); + assertEquals(0.5, r.coordinates[2][1], 1e-2); + assertEquals(45, r.coordinates[3][0], 1e-2); + assertEquals(0.25, r.coordinates[3][1], 1e-2); + assertEquals(44.75, r.coordinates[4][0], 1e-2); + assertEquals(0.25, r.coordinates[4][1], 1e-2); + assertEquals(44.75, r.coordinates[5][0], 1e-2); + assertEquals(0, r.coordinates[5][1], 1e-2); + assertEquals(45, r.coordinates[6][0], 1e-2); + assertEquals(0, r.coordinates[6][1], 1e-2); + }catch(Throwable t){ + t.printStackTrace(); + fail(t.getMessage()); + } + + // OTHER + try{ + translator.translateGeometryFromDB(new Double(12.3)); + fail("The translation of a Double as a geometry is not supported!"); + }catch(Throwable t){ + assertTrue(t instanceof ParseException); + assertEquals("Incompatible type! The column value \"12.3\" was supposed to be a geometrical object.", t.getMessage()); + } + try{ + pgo.setType("sline"); + pgo.setValue("( -90d, -20d, 200d, XYZ ), 30d "); + translator.translateGeometryFromDB(pgo); + fail("The translation of a sline is not supported!"); + }catch(Throwable t){ + assertTrue(t instanceof ParseException); + assertEquals("Unsupported PgSphere type: \"sline\"! Impossible to convert the column value \"( -90d, -20d, 200d, XYZ ), 30d \" into a Region.", t.getMessage()); + } + } + + @Test + public void testTranslateGeometryToDB(){ + PgSphereTranslator translator = new PgSphereTranslator(); + + try{ + // NULL + assertNull(translator.translateGeometryToDB(null)); + + // POSITION + Region r = new Region(null, new double[]{45,0}); + PGobject pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoint", pgo.getType()); + assertEquals("(45.0d,0.0d)", pgo.getValue()); + + // CIRCLE + r = new Region(null, new double[]{45,0}, 1.2); + pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoly", pgo.getType()); + assertEquals("{(46.2d,0.0d),(46.176942336483876d,0.2341083864193539d),(46.108655439013546d,0.4592201188381077d),(45.99776353476305d,0.6666842796235226d),(45.848528137423855d,0.8485281374238569d),(45.666684279623524d,0.9977635347630542d),(45.45922011883811d,1.1086554390135441d),(45.23410838641935d,1.1769423364838765d),(45.0d,1.2d),(44.76589161358065d,1.1769423364838765d),(44.54077988116189d,1.1086554390135441d),(44.333315720376476d,0.9977635347630543d),(44.151471862576145d,0.848528137423857d),(44.00223646523695d,0.6666842796235226d),(43.891344560986454d,0.4592201188381073d),(43.823057663516124d,0.23410838641935325d),(43.8d,-9.188564877424678E-16d),(43.823057663516124d,-0.23410838641935505d),(43.891344560986454d,-0.45922011883810904d),(44.00223646523695d,-0.6666842796235241d),(44.151471862576145d,-0.8485281374238584d),(44.333315720376476d,-0.9977635347630555d),(44.540779881161896d,-1.108655439013545d),(44.76589161358065d,-1.176942336483877d),(45.0d,-1.2d),(45.23410838641936d,-1.1769423364838758d),(45.45922011883811d,-1.1086554390135428d),(45.666684279623524d,-0.9977635347630521d),(45.84852813742386d,-0.8485281374238541d),(45.99776353476306d,-0.6666842796235192d),(46.108655439013546d,-0.45922011883810354d),(46.176942336483876d,-0.23410838641934922d)}", pgo.getValue()); + + // BOX + r = new Region(null, new double[]{45,0}, 1.2, 5); + pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoly", pgo.getType()); + assertEquals("{(44.4d,-2.5d),(44.4d,2.5d),(45.6d,2.5d),(45.6d,-2.5d)}", pgo.getValue()); + + // POLYGON + r = new Region(null, new double[][]{new double[]{45.25,0.25},new double[]{45.25,0.5},new double[]{45,0.5},new double[]{45,0.25},new double[]{44.75,0.25},new double[]{44.75,0},new double[]{45,0}}); + pgo = (PGobject)translator.translateGeometryToDB(r); + assertNotNull(pgo); + assertEquals("spoly", pgo.getType()); + assertEquals("{(45.25d,0.25d),(45.25d,0.5d),(45.0d,0.5d),(45.0d,0.25d),(44.75d,0.25d),(44.75d,0.0d),(45.0d,0.0d)}", pgo.getValue()); + + // OTHER + try{ + r = new Region(new Region(null, new double[]{45,0})); + translator.translateGeometryToDB(r); + fail("The translation of a STC Not region is not supported!"); + }catch(Throwable ex){ + assertTrue(ex instanceof ParseException); + assertEquals("Unsupported geometrical region: \"" + r.type + "\"!", ex.getMessage()); + } + + }catch(ParseException t){ + t.printStackTrace(); + fail(t.getMessage()); + } + } + +} diff --git a/test/tap/db/JDBCConnectionTest.java b/test/tap/db/JDBCConnectionTest.java index b645e85800bee3eb0f71d0ad592bce7cec49df76..bd43211f3588f1efc4d473db3a4e6413f9aa2a2d 100644 --- a/test/tap/db/JDBCConnectionTest.java +++ b/test/tap/db/JDBCConnectionTest.java @@ -176,11 +176,11 @@ public class JDBCConnectionTest { @Test public void testGetDBMSDatatype(){ - assertEquals("VARCHAR", pgJDBCConnection.getDBMSDatatype(null)); - assertEquals("TEXT", sqliteJDBCConnection.getDBMSDatatype(null)); + assertEquals("VARCHAR", pgJDBCConnection.defaultTypeConversion(null)); + assertEquals("TEXT", sqliteJDBCConnection.defaultTypeConversion(null)); - assertEquals("bytea", pgJDBCConnection.getDBMSDatatype(new DBType(DBDatatype.VARBINARY))); - assertEquals("BLOB", sqliteJDBCConnection.getDBMSDatatype(new DBType(DBDatatype.VARBINARY))); + assertEquals("bytea", pgJDBCConnection.defaultTypeConversion(new DBType(DBDatatype.VARBINARY))); + assertEquals("BLOB", sqliteJDBCConnection.defaultTypeConversion(new DBType(DBDatatype.VARBINARY))); } @Test @@ -379,6 +379,19 @@ public class JDBCConnectionTest { for(TAPColumn c : cols) tableDef.addColumn(c); + // Test with no schema set: + try{ + conn.addUploadedTable(tableDef, it); + fail("The table is not inside a TAPSchema, so this test should have failed!"); + }catch(Exception ex){ + assertTrue(ex instanceof DBException); + assertEquals("Missing upload schema! An uploaded table must be inside a schema whose the ADQL name is strictly equals to \"" + STDSchema.UPLOADSCHEMA.label + "\" (but the DB name may be different).", ex.getMessage()); + } + + // Specify the UPLOAD schema for the table to upload: + TAPSchema schema = new TAPSchema(STDSchema.UPLOADSCHEMA.label); + schema.addTable(tableDef); + // Prepare the test: no TAP_UPLOAD schema and no table TAP_UPLOAD.UploadExample: dropSchema(STDSchema.UPLOADSCHEMA.label, conn); // Test: diff --git a/test/tap/db/TestTAPDb.db b/test/tap/db/TestTAPDb.db index 227549965a7b445848f09562dc48441528f00dfd..d722315944c5da861381aede0e50b8951ae85fb2 100644 Binary files a/test/tap/db/TestTAPDb.db and b/test/tap/db/TestTAPDb.db differ