diff --git a/src/adql/db/DBChecker.java b/src/adql/db/DBChecker.java index 834d45abc5cf55a0005ad4b178941ac28469b586..31f1b16d00781a558af4be025b71fb0e56aecb29 100644 --- a/src/adql/db/DBChecker.java +++ b/src/adql/db/DBChecker.java @@ -20,6 +20,8 @@ package adql.db; * Astronomisches Rechen Institut (ARI) */ +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -62,6 +64,7 @@ import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; import adql.search.ISearchHandler; import adql.search.SearchColumnHandler; +import adql.search.SimpleReplaceHandler; import adql.search.SimpleSearchHandler; /** @@ -780,7 +783,7 @@ public class DBChecker implements QueryChecker { * @since 1.3 */ protected void checkUDFs(final ADQLQuery query, final UnresolvedIdentifiersException errors){ - // Search all UDFs: + // 1. Search all UDFs: ISearchHandler sHandler = new SearchUDFHandler(); sHandler.search(query); @@ -789,7 +792,7 @@ public class DBChecker implements QueryChecker { for(ADQLObject result : sHandler) errors.addException(new UnresolvedFunction((UserDefinedFunction)result)); } - // Otherwise, try to resolve all of them: + // 2. Try to resolve all of them: else{ ArrayList<UserDefinedFunction> toResolveLater = new ArrayList<UserDefinedFunction>(); UserDefinedFunction udf; @@ -835,6 +838,9 @@ public class DBChecker implements QueryChecker { else if (udf instanceof DefaultUDF) ((DefaultUDF)udf).setDefinition(allowedUdfs[match]); } + + // 3. Replace all the resolved DefaultUDF by an instance of the class associated with the set signature: + (new ReplaceDefaultUDFHandler(errors)).searchAndReplace(query); } } @@ -1305,6 +1311,51 @@ public class DBChecker implements QueryChecker { } } + /** + * <p>Let replacing every {@link DefaultUDF}s whose a {@link FunctionDef} is set by their corresponding {@link UserDefinedFunction} class.</p> + * + * <p><i><b>Important note:</b> + * If the replacer can not be created using the class returned by {@link FunctionDef#getUDFClass()}, no replacement is performed. + * </i></p> + * + * @author Grégory Mantelet (ARI) + * @version 1.3 (02/2015) + * @since 1.3 + */ + private static class ReplaceDefaultUDFHandler extends SimpleReplaceHandler { + private final UnresolvedIdentifiersException errors; + + public ReplaceDefaultUDFHandler(final UnresolvedIdentifiersException errorsContainer){ + errors = errorsContainer; + } + + @Override + protected boolean match(ADQLObject obj){ + return (obj.getClass().getName().equals(DefaultUDF.class.getName())) && (((DefaultUDF)obj).getDefinition() != null) && (((DefaultUDF)obj).getDefinition().getUDFClass() != null); + /* Note: detection of DefaultUDF is done on the exact class name rather than using "instanceof" in order to have only direct instances of DefaultUDF, + * and not extensions of it. Indeed, DefaultUDFs are generally created automatically by the ADQLQueryFactory ; so, extensions of it can only be custom + * UserDefinedFunctions. */ + } + + @Override + protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{ + try{ + // get the associated UDF class: + Class<? extends UserDefinedFunction> udfClass = ((DefaultUDF)objToReplace).getDefinition().getUDFClass(); + // get the constructor with a single parameter of type ADQLOperand[]: + Constructor<? extends UserDefinedFunction> constructor = udfClass.getConstructor(ADQLOperand[].class); + // create a new instance of this UDF class with the operands stored in the object to replace: + return constructor.newInstance((Object)(((DefaultUDF)objToReplace).getParameters())); /* note: without this class, each item of the given array will be considered as a single parameter. */ + }catch(Exception ex){ + // IF NO INSTANCE CAN BE CREATED... + // ...keep the error for further report: + errors.addException(new UnresolvedFunction("Impossible to represent the function \"" + ((DefaultUDF)objToReplace).getName() + "\": the following error occured while creating this representation: \"" + ((ex instanceof InvocationTargetException) ? "[" + ex.getCause().getClass().getSimpleName() + "] " + ex.getCause().getMessage() : ex.getMessage()) + "\"", (DefaultUDF)objToReplace)); + // ...keep the same object (i.e. no replacement): + return objToReplace; + } + } + } + /** * Let searching geometrical functions. * diff --git a/src/adql/db/FunctionDef.java b/src/adql/db/FunctionDef.java index a8071eecb96545e846316d58c83a65de0e13eb43..82107d0a232a33d2e1d113c795304261f1bfda66 100644 --- a/src/adql/db/FunctionDef.java +++ b/src/adql/db/FunctionDef.java @@ -16,15 +16,19 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2014 - Astronomisches Rechen Institut (ARI) + * Copyright 2015 - Astronomisches Rechen Institut (ARI) */ +import java.lang.reflect.Constructor; import java.util.regex.Matcher; import java.util.regex.Pattern; import adql.db.DBType.DBDatatype; import adql.parser.ParseException; +import adql.query.operand.ADQLOperand; import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.DefaultUDF; +import adql.query.operand.function.UserDefinedFunction; /** * <p>Definition of any function that could be used in ADQL queries.</p> @@ -45,7 +49,7 @@ import adql.query.operand.function.ADQLFunction; * </p> * * @author Grégory Mantelet (ARI) - * @version 1.3 (10/2014) + * @version 1.3 (02/2015) * * @since 1.3 */ @@ -105,6 +109,13 @@ public class FunctionDef implements Comparable<FunctionDef> { * <pre>{fctName}([xxx, ...])</pre> */ private final String compareForm; + /** + * <p>Class of the {@link UserDefinedFunction} which must represent the UDF defined by this {@link FunctionDef} in the ADQL tree.</p> + * <p>This class MUST have a constructor with a single parameter of type {@link ADQLOperand}[].</p> + * <p>If this {@link FunctionDef} is defining an ordinary ADQL function, this attribute must be NULL. It is used only for user defined functions.</p> + */ + private Class<? extends UserDefinedFunction> udfClass = null; + /** * <p>Definition of a function parameter.</p> * @@ -258,6 +269,60 @@ public class FunctionDef implements Comparable<FunctionDef> { return params[indParam]; } + /** + * <p>Get the class of the {@link UserDefinedFunction} able to represent the function defined here in an ADQL tree.</p> + * + * <p><i>Note: + * This getter should return always NULL if the function defined here is not a user defined function. + * <br/> + * However, if this {@link FunctionDef} is defining a user defined function and this function returns NULL, + * the library will create on the fly a {@link DefaultUDF} corresponding to this definition when needed. + * Indeed this UDF class is useful only if the translation from ADQL (to SQL for instance) of the defined + * function has a different signature (e.g. a different name) in the target language (e.g. SQL). + * </i></p> + * + * @return The corresponding {@link UserDefinedFunction}. <i>MAY BE NULL</i> + */ + public final Class<? extends UserDefinedFunction> getUDFClass(){ + return udfClass; + } + + /** + * <p>Set the class of the {@link UserDefinedFunction} able to represent the function defined here in an ADQL tree.</p> + * + * <p><i>Note: + * If this {@link FunctionDef} defines an ordinary ADQL function - and not a user defined function - no class should be set here. + * <br/> + * However, if it defines a user defined function, there is no obligation to set a UDF class. It is useful only if the translation + * from ADQL (to SQL for instance) of the function has a different signature (e.g. a different name) in the target language (e.g. SQL). + * If the signature is the same, there is no need to set a UDF class ; a {@link DefaultUDF} will be created on the fly by the library + * when needed if it turns out that no UDF class is set. + * </i></p> + * + * @param udfClass Class to use to represent in an ADQL tree the User Defined Function defined in this {@link FunctionDef}. + * + * @throws IllegalArgumentException If the given class does not provide any constructor with a single parameter of type ADQLOperand[]. + */ + public final < T extends UserDefinedFunction > void setUDFClass(final Class<T> udfClass) throws IllegalArgumentException{ + try{ + + // Ensure that, if a class is provided, it contains a constructor with a single parameter of type ADQLOperand[]: + if (udfClass != null){ + Constructor<T> constructor = udfClass.getConstructor(ADQLOperand[].class); + if (constructor == null) + throw new IllegalArgumentException("The given class (" + udfClass.getName() + ") does not provide any constructor with a single parameter of type ADQLOperand[]!"); + } + + // Set the new UDF class: + this.udfClass = udfClass; + + }catch(SecurityException e){ + throw new IllegalArgumentException("A security problem occurred while trying to get constructor from the class " + udfClass.getName() + ": " + e.getMessage()); + }catch(NoSuchMethodException e){ + throw new IllegalArgumentException("The given class (" + udfClass.getName() + ") does not provide any constructor with a single parameter of type ADQLOperand[]!"); + } + } + /** * <p>Let parsing the serialized form of a function definition.</p> * diff --git a/src/adql/query/operand/function/DefaultUDF.java b/src/adql/query/operand/function/DefaultUDF.java index dd0c615843e0282855415d4c9b50c7fd4f9dda74..285eb4ef987cfa0e146ca1ad299af03460176cf8 100644 --- a/src/adql/query/operand/function/DefaultUDF.java +++ b/src/adql/query/operand/function/DefaultUDF.java @@ -16,7 +16,7 @@ package adql.query.operand.function; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ @@ -25,12 +25,14 @@ import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ClauseADQL; import adql.query.operand.ADQLOperand; +import adql.translator.ADQLTranslator; +import adql.translator.TranslationException; /** * It represents any function which is not managed by ADQL. * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (10/2014) + * @version 1.3 (02/2015) */ public final class DefaultUDF extends UserDefinedFunction { @@ -48,7 +50,7 @@ public final class DefaultUDF extends UserDefinedFunction { * Creates a user function. * @param params Parameters of the function. */ - public DefaultUDF(final String name, ADQLOperand[] params) throws NullPointerException{ + public DefaultUDF(final String name, final ADQLOperand[] params) throws NullPointerException{ functionName = name; parameters = new ClauseADQL<ADQLOperand>(); if (params != null){ @@ -64,7 +66,7 @@ public final class DefaultUDF extends UserDefinedFunction { * @throws Exception If there is an error during the copy. */ @SuppressWarnings("unchecked") - public DefaultUDF(DefaultUDF toCopy) throws Exception{ + public DefaultUDF(final DefaultUDF toCopy) throws Exception{ functionName = toCopy.functionName; parameters = (ADQLList<ADQLOperand>)(toCopy.parameters.getCopy()); } @@ -91,6 +93,8 @@ public final class DefaultUDF extends UserDefinedFunction { * @param def The definition applying to this parsed UDF, or NULL if none has been found. * * @throws IllegalArgumentException If the name in the given definition does not match the name of this parsed function. + * + * @since 1.3 */ public final void setDefinition(final FunctionDef def) throws IllegalArgumentException{ if (def != null && (def.name == null || !functionName.equalsIgnoreCase(def.name))) @@ -155,4 +159,17 @@ public final class DefaultUDF extends UserDefinedFunction { return parameters.set(index, replacer); } + @Override + public String translate(final ADQLTranslator caller) throws TranslationException{ + StringBuffer sql = new StringBuffer(functionName); + sql.append('('); + for(int i = 0; i < parameters.size(); i++){ + if (i > 0) + sql.append(',').append(' '); + sql.append(caller.translate(parameters.get(i))); + } + sql.append(')'); + return sql.toString(); + } + } diff --git a/src/adql/query/operand/function/UserDefinedFunction.java b/src/adql/query/operand/function/UserDefinedFunction.java index 78026e72d5429d4b6d76f07b42c3226ca241bbeb..f817832854b23f018e5a1af65756209481a61e61 100644 --- a/src/adql/query/operand/function/UserDefinedFunction.java +++ b/src/adql/query/operand/function/UserDefinedFunction.java @@ -1,7 +1,5 @@ package adql.query.operand.function; -import adql.query.operand.UnknownType; - /* * This file is part of ADQLLibrary. * @@ -18,15 +16,19 @@ import adql.query.operand.UnknownType; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ +import adql.query.operand.UnknownType; +import adql.translator.ADQLTranslator; +import adql.translator.TranslationException; + /** * Function defined by the user (i.e. PSQL functions). * * @author Grégory Mantelet (CDS;ARI) - * @version 1.3 (10/2014) + * @version 1.3 (02/2015) * * @see DefaultUDF */ @@ -46,4 +48,36 @@ public abstract class UserDefinedFunction extends ADQLFunction implements Unknow expectedType = c; } + /** + * <p>Translate this User Defined Function into the language supported by the given translator.</p> + * + * <p><b>VERY IMPORTANT:</b> This function <b>MUST NOT use</b> {@link ADQLTranslator#translate(UserDefinedFunction))} to translate itself. + * The given {@link ADQLTranslator} <b>must be used ONLY</b> to translate UDF's operands.</p> + * + * <p>Implementation example (extract of {@link DefaultUDF#translate(ADQLTranslator)}):</p> + * <pre> + * public String translate(final ADQLTranslator caller) throws TranslationException{ + * StringBuffer sql = new StringBuffer(functionName); + * sql.append('('); + * for(int i = 0; i < parameters.size(); i++){ + * if (i > 0) + * sql.append(',').append(' '); + * sql.append(caller.translate(parameters.get(i))); + * } + * sql.append(')'); + * return sql.toString(); + * } + * </pre> + * + * + * @param caller Translator to use in order to translate <b>ONLY</b> function parameters. + * + * @return The translation of this UDF into the language supported by the given translator. + * + * @throws TranslationException If one of the parameters can not be translated. + * + * @since 1.3 + */ + public abstract String translate(final ADQLTranslator caller) throws TranslationException; + } diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java index 362e67c9e8843a4c47490a2a3c5c4fd0ea9e85f8..4451dcc743a77674355b87738f1fc6de455710fb 100644 --- a/src/adql/translator/JDBCTranslator.java +++ b/src/adql/translator/JDBCTranslator.java @@ -16,7 +16,7 @@ package adql.translator; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2014 - Astronomisches Rechen Institut (ARI) + * Copyright 2015 - Astronomisches Rechen Institut (ARI) */ import java.util.ArrayList; @@ -167,7 +167,7 @@ import adql.query.operand.function.geometry.RegionFunction; * </p> * * @author Grégory Mantelet (ARI) - * @version 2.0 (02/2015) + * @version 1.3 (02/2015) * @since 1.3 * * @see PostgreSQLTranslator @@ -791,7 +791,7 @@ public abstract class JDBCTranslator implements ADQLTranslator { @Override public String translate(UserDefinedFunction fct) throws TranslationException{ - return getDefaultADQLFunction(fct); + return fct.translate(this); } /* *********************************** */ diff --git a/src/tap/config/DefaultServiceConnection.java b/src/tap/config/DefaultServiceConnection.java index c7baae8101feac5bb19f1620c83e07739673d842..f04733aacbcacc2a124658dcdf5d97011593d81b 100644 --- a/src/tap/config/DefaultServiceConnection.java +++ b/src/tap/config/DefaultServiceConnection.java @@ -25,9 +25,11 @@ import static tap.config.TAPConfiguration.KEY_METADATA_FILE; import static tap.config.TAPConfiguration.KEY_OUTPUT_FORMATS; import static tap.config.TAPConfiguration.KEY_PROVIDER_NAME; import static tap.config.TAPConfiguration.KEY_SERVICE_DESCRIPTION; +import static tap.config.TAPConfiguration.KEY_UDFS; import static tap.config.TAPConfiguration.KEY_UPLOAD_ENABLED; import static tap.config.TAPConfiguration.KEY_UPLOAD_MAX_FILE_SIZE; import static tap.config.TAPConfiguration.KEY_USER_IDENTIFIER; +import static tap.config.TAPConfiguration.VALUE_ANY; import static tap.config.TAPConfiguration.VALUE_CSV; import static tap.config.TAPConfiguration.VALUE_DB; import static tap.config.TAPConfiguration.VALUE_JSON; @@ -65,6 +67,8 @@ import uws.service.UserIdentifier; import uws.service.file.LocalUWSFileManager; import uws.service.file.UWSFileManager; import adql.db.FunctionDef; +import adql.parser.ParseException; +import adql.query.operand.function.UserDefinedFunction; public final class DefaultServiceConnection implements ServiceConnection { @@ -146,6 +150,7 @@ public final class DefaultServiceConnection implements ServiceConnection { // 9. CONFIGURE ADQL: initADQLGeometries(tapConfig); + initUDFs(tapConfig); // 10. MAKE THE SERVICE AVAILABLE: setAvailable(true, "TAP service available."); @@ -465,6 +470,10 @@ public final class DefaultServiceConnection implements ServiceConnection { else if (propValue.equalsIgnoreCase(VALUE_NONE)) geometries = new ArrayList<String>(0); + // "ANY" => ALL FCT ALLOWED (= all of these functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_ANY)) + geometries = null; + // OTHERWISE, JUST THE ALLOWED ONE ARE LISTED: else{ // split all the list items: @@ -482,6 +491,9 @@ public final class DefaultServiceConnection implements ServiceConnection { // "NONE" is not allowed inside a list => error! else if (item.toUpperCase().equals(VALUE_NONE)) throw new TAPException("The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed."); + // "ANY" is not allowed inside a list => error! + else if (item.toUpperCase().equals(VALUE_ANY)) + throw new TAPException("The special value \"" + VALUE_ANY + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that any value is allowed."); // unknown value => error! else throw new TAPException("Unknown ADQL geometrical function: \"" + item + "\"!"); @@ -494,6 +506,159 @@ public final class DefaultServiceConnection implements ServiceConnection { } } + private void initUDFs(final Properties tapConfig) throws TAPException{ + // Get the property value: + String propValue = getProperty(tapConfig, KEY_UDFS); + + // NO VALUE => NO UNKNOWN FCT ALLOWED! + if (propValue == null) + udfs = new ArrayList<FunctionDef>(0); + + // "NONE" => NO UNKNOWN FCT ALLOWED (= none of the unknown functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_NONE)) + udfs = new ArrayList<FunctionDef>(0); + + // "ANY" => ALL UNKNOWN FCT ALLOWED (= all of the unknown functions are allowed)! + else if (propValue.equalsIgnoreCase(VALUE_ANY)) + udfs = null; + + // OTHERWISE, JUST THE ALLOWED ONE ARE LISTED: + else{ + + char c; + int ind = 0; + short nbComma = 0; + boolean within_item = false, within_params = false, within_classpath = false; + StringBuffer buf = new StringBuffer(); + String signature, classpath; + int[] posSignature = new int[]{-1,-1}, posClassPath = new int[]{-1,-1}; + + signature = null; + classpath = null; + buf.delete(0, buf.length()); + + while(ind < propValue.length()){ + // Get the character: + c = propValue.charAt(ind++); + // If space => ignore + if (!within_params && Character.isWhitespace(c)) + continue; + // If inside a parameters list, keep all characters until the list end (')'): + if (within_params){ + if (c == ')') + within_params = false; + buf.append(c); + } + // If inside a classpath, keep all characters until the classpath end ('}'): + else if (within_classpath){ + if (c == '}') + within_classpath = false; + buf.append(c); + } + // If inside an UDF declaration: + else if (within_item){ + switch(c){ + case '(': /* start of a parameters list */ + within_params = true; + buf.append(c); + break; + case '{': /* start of a classpath */ + within_classpath = true; + buf.append(c); + break; + case ',': /* separation between the signature and the classpath */ + // count commas within this item: + if (++nbComma > 1) + // if more than 1, throw an error: + throw new TAPException("Wrong UDF declaration syntax: only two items (signature and classpath) can be given within brackets. (position in the property " + KEY_UDFS + ": " + ind + ")"); + else{ + // end of the signature and start of the class path: + signature = buf.toString(); + buf.delete(0, buf.length()); + posSignature[1] = ind; + posClassPath[0] = ind + 1; + } + break; + case ']': /* end of a UDF declaration */ + within_item = false; + if (nbComma == 0){ + signature = buf.toString(); + posSignature[1] = ind; + }else{ + classpath = (buf.length() == 0 ? null : buf.toString()); + if (classpath != null) + posClassPath[1] = ind; + } + buf.delete(0, buf.length()); + + // no signature... + if (signature == null || signature.length() == 0){ + // ...BUT a classpath => error + if (classpath != null) + throw new TAPException("Missing UDF declaration! (position in the property " + KEY_UDFS + ": " + posSignature[0] + "-" + posSignature[1] + ")"); + // ... => ignore this item + else + continue; + } + + // add the new UDF in the list: + try{ + // resolve the function signature: + FunctionDef def = FunctionDef.parse(signature); + // resolve the class path: + if (classpath != null){ + if (isClassPath(classpath)){ + Class<? extends UserDefinedFunction> fctClass = null; + try{ + // fetch the class: + fctClass = fetchClass(classpath, KEY_UDFS, UserDefinedFunction.class); + // set the class inside the UDF definition: + def.setUDFClass(fctClass); + }catch(TAPException te){ + throw new TAPException("Invalid class path for the UDF definition \"" + def + "\": " + te.getMessage() + " (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")", te); + }catch(IllegalArgumentException iae){ + throw new TAPException("Invalid class path for the UDF definition \"" + def + "\": missing a constructor with a single parameter of type ADQLOperand[] " + (fctClass != null ? "in the class \"" + fctClass.getName() + "\"" : "") + "! (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")"); + } + }else + throw new TAPException("Invalid class path for the UDF definition \"" + def + "\": \"" + classpath + "\" is not a class path (or is not surrounding by {} as expected in this property file)! (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")"); + } + // add the UDF: + udfs.add(def); + }catch(ParseException pe){ + throw new TAPException("Wrong UDF declaration syntax: " + pe.getMessage() + " (position in the property " + KEY_UDFS + ": " + posSignature[0] + "-" + posSignature[1] + ")", pe); + } + + // reset some variables: + nbComma = 0; + signature = null; + classpath = null; + break; + default: /* keep all other characters */ + buf.append(c); + break; + } + } + // If outside of everything, just starting a UDF declaration or separate each declaration is allowed: + else{ + switch(c){ + case '[': + within_item = true; + posSignature[0] = ind + 1; + break; + case ',': + break; + default: + throw new TAPException("Wrong UDF declaration syntax: unexpected character at position " + ind + " in the property " + KEY_UDFS + ": \"" + c + "\"! A UDF declaration must have one of the following syntaxes: \"[signature]\" or \"[signature,{classpath}]\"."); + } + } + } + + // If the parsing is not finished, throw an error: + if (within_item) + throw new TAPException("Wrong UDF declaration syntax: missing closing bracket at position " + propValue.length() + "!"); + } + } + @Override public String getProviderName(){ return providerName; @@ -722,7 +887,7 @@ public final class DefaultServiceConnection implements ServiceConnection { @Override public Collection<FunctionDef> getUDFs(){ - return udfs; // FORBID ANY UNKNOWN FUNCTION + return udfs; } } diff --git a/src/tap/config/TAPConfiguration.java b/src/tap/config/TAPConfiguration.java index 860873e11bd0ca5d4be6f2927bb221b7816d36a9..aa60398382ca60282f30baa61913f4fedc2cd834 100644 --- a/src/tap/config/TAPConfiguration.java +++ b/src/tap/config/TAPConfiguration.java @@ -89,6 +89,8 @@ public final class TAPConfiguration { /* ADQL RESTRICTIONS */ public final static String KEY_GEOMETRIES = "geometries"; public final static String VALUE_NONE = "NONE"; + public final static String KEY_UDFS = "udfs"; + public final static String VALUE_ANY = "ANY"; /** * <p>Read the asked property from the given Properties object.</p> diff --git a/src/tap/config/tap_configuration_file.html b/src/tap/config/tap_configuration_file.html index 7573f5ea01c59bd02899991a8db8869a27ae0e77..173dc066d2669fee902bf04ae63e147cef47f5f4 100644 --- a/src/tap/config/tap_configuration_file.html +++ b/src/tap/config/tap_configuration_file.html @@ -470,6 +470,27 @@ </td> <td><ul><li>ø <em>(default)</em></li><li>NONE</li><li>CONTAINS, intersects, Point, Box, CIRCLE</li></ul></td> </tr> + <tr> + <td class="done">udfs</td> + <td></td> + <td>text</td> + <td> + <p>Comma-separated list of all allowed UDFs (User Defined Functions).</p> + <p> + Each item of the list must have the following syntax: <code>[fct_signature]</code> or <code>[fct_signature, classpath]</code>. + <i>fct_function</i> is the function signature. Its syntax is the same as in <a href="http://www.ivoa.net/documents/TAPRegExt/20120827/REC-TAPRegExt-1.0.html#langs">TAPRegExt</a>. + <i>classpath</i> is the path of a class extending UserDefinedFunction. An instance of this class will replace any reference of a UDF + written in an ADQL function with the associated signature. A class path must be specified if the function to represent has a signature + (and more particularly a name) different in ADQL and in SQL. + </p> + <p> + If the list is empty (no item), all unknown functions are forbidden. And if the special value <em>ANY</em> is given, any unknown function is allowed ; + consequently the unknown ADQL functions will be translated into SQL as they are in ADQL. + </p> + <p><em>By default, no unknown function is allowed.</em></p> + </td> + <td><ul><li>ø <em>(default)</em></li><li>ANY</li><li>[trim(txt String) -> String], [random() -> DOUBLE]</li><li>[newFct(x double)->double, {apackage.MyNewFunction}]</li></ul></td> + </tr> </table> </body> diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java index c8e8b4b43dd1440f9371b6ae575837b560549084..3f9924a8616ad959deebbbbab8f7df1dac96b3f8 100644 --- a/test/adql/db/TestDBChecker.java +++ b/test/adql/db/TestDBChecker.java @@ -23,10 +23,16 @@ import adql.db.FunctionDef.FunctionParam; import adql.db.exception.UnresolvedIdentifiersException; import adql.parser.ADQLParser; import adql.parser.ParseException; +import adql.query.ADQLObject; import adql.query.ADQLQuery; import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; +import adql.query.operand.StringConstant; import adql.query.operand.function.DefaultUDF; +import adql.query.operand.function.UserDefinedFunction; +import adql.search.SimpleSearchHandler; +import adql.translator.ADQLTranslator; +import adql.translator.TranslationException; public class TestDBChecker { @@ -148,6 +154,52 @@ public class TestDBChecker { assertEquals(1, ex.getNbErrors()); assertEquals("Unresolved function: \"toto('blabla')\"! No UDF has been defined or found with the signature: toto(STRING).", ex.getErrors().next().getMessage()); } + + // Test with a UDF whose the class is specified ; the corresponding object in the ADQL tree must be replace by an instance of this class: + udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("txt", new DBType(DBDatatype.VARCHAR))})}; + udfs[0].setUDFClass(UDFToto.class); + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + try{ + ADQLQuery query = parser.parseQuery("SELECT toto('blabla') FROM foo;"); + assertNotNull(query); + Iterator<ADQLObject> it = query.search(new SimpleSearchHandler(){ + @Override + protected boolean match(ADQLObject obj){ + return (obj instanceof UserDefinedFunction) && ((UserDefinedFunction)obj).getName().equals("toto"); + } + }); + assertTrue(it.hasNext()); + assertEquals(UDFToto.class.getName(), it.next().getClass().getName()); + assertFalse(it.hasNext()); + }catch(Exception e){ + e.printStackTrace(); + fail("This query contains a DECLARED UDF with a valid UserDefinedFunction class: this test should have succeeded!"); + } + + // Test with a wrong parameter type: + try{ + parser.parseQuery("SELECT toto(123) FROM foo;"); + fail("This query contains an unknown UDF signature (the fct toto is declared with one parameter of type STRING...here it is a numeric): this test should have failed!"); + }catch(Exception e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Unresolved function: \"toto(123)\"! No UDF has been defined or found with the signature: toto(NUMERIC).", ex.getErrors().next().getMessage()); + } + + // Test with UDF class constructor throwing an exception: + udfs = new FunctionDef[]{new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{new FunctionParam("txt", new DBType(DBDatatype.VARCHAR))})}; + udfs[0].setUDFClass(WrongUDFToto.class); + parser = new ADQLParser(new DBChecker(tables, Arrays.asList(udfs))); + try{ + parser.parseQuery("SELECT toto('blabla') FROM foo;"); + fail("The set UDF class constructor has throw an error: this test should have failed!"); + }catch(Exception e){ + assertTrue(e instanceof UnresolvedIdentifiersException); + UnresolvedIdentifiersException ex = (UnresolvedIdentifiersException)e; + assertEquals(1, ex.getNbErrors()); + assertEquals("Impossible to represent the function \"toto\": the following error occured while creating this representation: \"[Exception] Systematic error!\"", ex.getErrors().next().getMessage()); + } } @Test @@ -606,4 +658,83 @@ public class TestDBChecker { } } + private static class WrongUDFToto extends UDFToto { + public WrongUDFToto(final ADQLOperand[] params) throws Exception{ + super(params); + throw new Exception("Systematic error!"); + } + } + + public static class UDFToto extends UserDefinedFunction { + protected StringConstant fakeParam; + + public UDFToto(final ADQLOperand[] params) throws Exception{ + if (params == null || params.length == 0) + throw new Exception("Missing parameter for the user defined function \"toto\"!"); + else if (params.length > 1) + throw new Exception("Too many parameters for the function \"toto\"! Only one is required."); + else if (!(params[0] instanceof StringConstant)) + throw new Exception("Wrong parameter type! The parameter of the UDF \"toto\" must be a string constant."); + fakeParam = (StringConstant)params[0]; + } + + @Override + public final boolean isNumeric(){ + return false; + } + + @Override + public final boolean isString(){ + return true; + } + + @Override + public final boolean isGeometry(){ + return false; + } + + @Override + public ADQLObject getCopy() throws Exception{ + ADQLOperand[] params = new ADQLOperand[]{(StringConstant)fakeParam.getCopy()}; + return new UDFToto(params); + } + + @Override + public final String getName(){ + return "toto"; + } + + @Override + public final ADQLOperand[] getParameters(){ + return new ADQLOperand[]{fakeParam}; + } + + @Override + public final int getNbParameters(){ + return 1; + } + + @Override + public final ADQLOperand getParameter(int index) throws ArrayIndexOutOfBoundsException{ + if (index != 0) + throw new ArrayIndexOutOfBoundsException("Incorrect parameter index: " + index + "! The function \"toto\" has only one parameter."); + return fakeParam; + } + + @Override + public ADQLOperand setParameter(int index, ADQLOperand replacer) throws ArrayIndexOutOfBoundsException, NullPointerException, Exception{ + if (index != 0) + throw new ArrayIndexOutOfBoundsException("Incorrect parameter index: " + index + "! The function \"toto\" has only one parameter."); + else if (!(replacer instanceof StringConstant)) + throw new Exception("Wrong parameter type! The parameter of the UDF \"toto\" must be a string constant."); + return (fakeParam = (StringConstant)replacer); + } + + @Override + public String translate(final ADQLTranslator caller) throws TranslationException{ + /* Note: Since this function is totally fake, this function will be replaced in SQL by its parameter (the string). */ + return caller.translate(fakeParam); + } + } + } diff --git a/test/tap/config/TestDefaultServiceConnection.java b/test/tap/config/TestDefaultServiceConnection.java index 4124ce7336ac6350276bef022fa9b730b550ffac..21e9692b11243379c328d9c8f0b58582e80e84c3 100644 --- a/test/tap/config/TestDefaultServiceConnection.java +++ b/test/tap/config/TestDefaultServiceConnection.java @@ -14,7 +14,9 @@ import static tap.config.TAPConfiguration.KEY_MAX_OUTPUT_LIMIT; import static tap.config.TAPConfiguration.KEY_METADATA; import static tap.config.TAPConfiguration.KEY_METADATA_FILE; import static tap.config.TAPConfiguration.KEY_OUTPUT_FORMATS; +import static tap.config.TAPConfiguration.KEY_UDFS; import static tap.config.TAPConfiguration.KEY_USER_IDENTIFIER; +import static tap.config.TAPConfiguration.VALUE_ANY; import static tap.config.TAPConfiguration.VALUE_CSV; import static tap.config.TAPConfiguration.VALUE_DB; import static tap.config.TAPConfiguration.VALUE_JSON; @@ -27,6 +29,7 @@ import static tap.config.TAPConfiguration.VALUE_XML; import java.io.File; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Iterator; import java.util.Map; import java.util.Properties; @@ -44,6 +47,8 @@ import uws.job.user.JobOwner; import uws.service.UWSUrl; import uws.service.UserIdentifier; import uws.service.file.LocalUWSFileManager; +import adql.db.FunctionDef; +import adql.db.TestDBChecker.UDFToto; public class TestDefaultServiceConnection { @@ -56,7 +61,12 @@ public class TestDefaultServiceConnection { negativeMaxAsyncProp, notIntMaxAsyncProp, defaultOutputLimitProp, maxOutputLimitProp, bothOutputLimitGoodProp, bothOutputLimitBadProp, userIdentProp, notClassPathUserIdentProp, - geometriesProp, noneGeomProp, noneInsideGeomProp, unknownGeomProp; + geometriesProp, noneGeomProp, anyGeomProp, noneInsideGeomProp, + unknownGeomProp, anyUdfsProp, noneUdfsProp, udfsProp, + udfsWithClassPathProp, udfsListWithNONEorANYProp, + udfsWithWrongParamLengthProp, udfsWithMissingBracketsProp, + udfsWithMissingDefProp1, udfsWithMissingDefProp2, + emptyUdfItemProp1, emptyUdfItemProp2, udfWithMissingEndBracketProp; @Before public void setUp() throws Exception{ @@ -137,11 +147,50 @@ public class TestDefaultServiceConnection { noneGeomProp = (Properties)validProp.clone(); noneGeomProp.setProperty(KEY_GEOMETRIES, VALUE_NONE); + anyGeomProp = (Properties)validProp.clone(); + anyGeomProp.setProperty(KEY_GEOMETRIES, VALUE_ANY); + noneInsideGeomProp = (Properties)validProp.clone(); noneInsideGeomProp.setProperty(KEY_GEOMETRIES, "POINT, Box, none, circle"); unknownGeomProp = (Properties)validProp.clone(); unknownGeomProp.setProperty(KEY_GEOMETRIES, "POINT, Contains, foo, circle,Polygon"); + + anyUdfsProp = (Properties)validProp.clone(); + anyUdfsProp.setProperty(KEY_UDFS, VALUE_ANY); + + noneUdfsProp = (Properties)validProp.clone(); + noneUdfsProp.setProperty(KEY_UDFS, VALUE_NONE); + + udfsProp = (Properties)validProp.clone(); + udfsProp.setProperty(KEY_UDFS, "[toto(a string)] , [ titi(b REAL) -> double ]"); + + udfsWithClassPathProp = (Properties)validProp.clone(); + udfsWithClassPathProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}]"); + + udfsListWithNONEorANYProp = (Properties)validProp.clone(); + udfsListWithNONEorANYProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR],ANY"); + + udfsWithWrongParamLengthProp = (Properties)validProp.clone(); + udfsWithWrongParamLengthProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, foo]"); + + udfsWithMissingBracketsProp = (Properties)validProp.clone(); + udfsWithMissingBracketsProp.setProperty(KEY_UDFS, "toto(a string)->VARCHAR"); + + udfsWithMissingDefProp1 = (Properties)validProp.clone(); + udfsWithMissingDefProp1.setProperty(KEY_UDFS, "[{adql.db.TestDBChecker$UDFToto}]"); + + udfsWithMissingDefProp2 = (Properties)validProp.clone(); + udfsWithMissingDefProp2.setProperty(KEY_UDFS, "[,{adql.db.TestDBChecker$UDFToto}]"); + + emptyUdfItemProp1 = (Properties)validProp.clone(); + emptyUdfItemProp1.setProperty(KEY_UDFS, "[ ]"); + + emptyUdfItemProp2 = (Properties)validProp.clone(); + emptyUdfItemProp2.setProperty(KEY_UDFS, "[ , ]"); + + udfWithMissingEndBracketProp = (Properties)validProp.clone(); + udfWithMissingEndBracketProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR"); } /** @@ -184,6 +233,7 @@ public class TestDefaultServiceConnection { assertTrue(connection.getExecutionDuration()[0] <= connection.getExecutionDuration()[1]); assertNull(connection.getUserIdentifier()); assertNull(connection.getGeometries()); + assertEquals(0, connection.getUDFs().size()); // finally, save metadata in an XML file for the other tests: writer = new PrintWriter(new File(XML_FILE)); @@ -213,8 +263,8 @@ public class TestDefaultServiceConnection { assertTrue(connection.getExecutionDuration()[0] <= connection.getExecutionDuration()[1]); assertNull(connection.getUserIdentifier()); assertNull(connection.getGeometries()); + assertEquals(0, connection.getUDFs().size()); }catch(Exception e){ - e.printStackTrace(); fail("This MUST have succeeded because the property file is valid! \nCaught exception: " + getPertinentMessage(e)); } @@ -223,8 +273,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(missingMetaProp); fail("This MUST have failed because the property 'metadata' is missing!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA)."); + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_METADATA + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA).", e.getMessage()); } // Missing metadata_file property: @@ -232,8 +282,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(missingMetaFileProp); fail("This MUST have failed because the property 'metadata_file' is missing!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "The property \"" + KEY_METADATA_FILE + "\" is missing! According to the property \"" + KEY_METADATA + "\", metadata must be fetched from an XML document. The local file path of it MUST be provided using the property \"" + KEY_METADATA_FILE + "\"."); + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_METADATA_FILE + "\" is missing! According to the property \"" + KEY_METADATA + "\", metadata must be fetched from an XML document. The local file path of it MUST be provided using the property \"" + KEY_METADATA_FILE + "\".", e.getMessage()); } // Wrong metadata property: @@ -241,8 +291,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(wrongMetaProp); fail("This MUST have failed because the property 'metadata' has a wrong value!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Unsupported value for the property \"" + KEY_METADATA + "\": \"foo\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA)."); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unsupported value for the property \"" + KEY_METADATA + "\": \"foo\"! Only two values are allowed: " + VALUE_XML + " (to get metadata from a TableSet XML document) or " + VALUE_DB + " (to fetch metadata from the database schema TAP_SCHEMA).", e.getMessage()); } // Wrong metadata_file property: @@ -250,8 +300,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(wrongMetaFileProp); fail("This MUST have failed because the property 'metadata_file' has a wrong value!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "A grave error occurred while reading/parsing the TableSet XML document: \"foo\"!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("A grave error occurred while reading/parsing the TableSet XML document: \"foo\"!", e.getMessage()); } // No File Manager: @@ -259,8 +309,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(noFmProp); fail("This MUST have failed because no File Manager is specified!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "The property \"" + KEY_FILE_MANAGER + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_LOCAL + " or a class path between {...}."); + assertEquals(TAPException.class, e.getClass()); + assertEquals("The property \"" + KEY_FILE_MANAGER + "\" is missing! It is required to create a TAP Service. Two possible values: " + VALUE_LOCAL + " or a class path between {...}.", e.getMessage()); } // File Manager = Class Path: @@ -288,8 +338,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(incorrectFmProp); fail("This MUST have failed because an incorrect File Manager value has been provided!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Unknown value for the property \"" + KEY_FILE_MANAGER + "\": \"foo\". Only two possible values: " + VALUE_LOCAL + " or a class path between {...}."); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unknown value for the property \"" + KEY_FILE_MANAGER + "\": \"foo\". Only two possible values: " + VALUE_LOCAL + " or a class path between {...}.", e.getMessage()); } // Valid output formats list: @@ -313,8 +363,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(badSVFormat1Prop); fail("This MUST have failed because an incorrect SV output format value has been provided!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Missing separator char/string for the SV output format: \"sv\"!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing separator char/string for the SV output format: \"sv\"!", e.getMessage()); } // Bad SV(...) format 2 = "sv()": @@ -322,8 +372,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(badSVFormat2Prop); fail("This MUST have failed because an incorrect SV output format value has been provided!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Missing separator char/string for the SV output format: \"sv()\"!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing separator char/string for the SV output format: \"sv()\"!", e.getMessage()); } // Unknown output format: @@ -331,8 +381,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(unknownFormatProp); fail("This MUST have failed because an incorrect output format value has been provided!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Unknown output format: foo"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unknown output format: foo", e.getMessage()); } // Valid value for max_async_jobs: @@ -356,8 +406,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(notIntMaxAsyncProp); fail("This MUST have failed because a not integer value has been provided for \"" + KEY_MAX_ASYNC_JOBS + "\"!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Integer expected for the property \"" + KEY_MAX_ASYNC_JOBS + "\", instead of: \"foo\"!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Integer expected for the property \"" + KEY_MAX_ASYNC_JOBS + "\", instead of: \"foo\"!", e.getMessage()); } // Test with no output limit specified: @@ -409,8 +459,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(bothOutputLimitBadProp); fail("This MUST have failed because the default output limit is greater than the maximum one!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "The default output limit (here: 1000) MUST be less or equal to the maximum output limit (here: 100)!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("The default output limit (here: 1000) MUST be less or equal to the maximum output limit (here: 100)!", e.getMessage()); } // Valid user identifier: @@ -428,8 +478,8 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(notClassPathUserIdentProp); fail("This MUST have failed because the user_identifier value is not a class path!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Class path expected for the property \"" + KEY_USER_IDENTIFIER + "\", instead of: \"foo\"!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Class path expected for the property \"" + KEY_USER_IDENTIFIER + "\", instead of: \"foo\"!", e.getMessage()); } // Valid geometry list: @@ -454,13 +504,21 @@ public class TestDefaultServiceConnection { fail("This MUST have succeeded because the given list of geometries is correct (reduced to only NONE)! \nCaught exception: " + getPertinentMessage(e)); } + // "ANY" as geometry list: + try{ + ServiceConnection connection = new DefaultServiceConnection(anyGeomProp); + assertNull(connection.getGeometries()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of geometries is correct (reduced to only ANY)! \nCaught exception: " + getPertinentMessage(e)); + } + // "NONE" inside a geometry list: try{ new DefaultServiceConnection(noneInsideGeomProp); fail("This MUST have failed because the given geometry list contains at least 2 items, whose one is NONE!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed."); + assertEquals(TAPException.class, e.getClass()); + assertEquals("The special value \"" + VALUE_NONE + "\" can not be used inside a list! It MUST be used in replacement of a whole list to specify that no value is allowed.", e.getMessage()); } // Unknown geometrical function: @@ -468,8 +526,121 @@ public class TestDefaultServiceConnection { new DefaultServiceConnection(unknownGeomProp); fail("This MUST have failed because the given geometry list contains at least 1 unknown ADQL geometrical function!"); }catch(Exception e){ - assertEquals(e.getClass(), TAPException.class); - assertEquals(e.getMessage(), "Unknown ADQL geometrical function: \"foo\"!"); + assertEquals(TAPException.class, e.getClass()); + assertEquals("Unknown ADQL geometrical function: \"foo\"!", e.getMessage()); + } + + // "ANY" as UDFs list: + try{ + ServiceConnection connection = new DefaultServiceConnection(anyUdfsProp); + assertNull(connection.getUDFs()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs is correct (reduced to only ANY)! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" as UDFs list: + try{ + ServiceConnection connection = new DefaultServiceConnection(noneUdfsProp); + assertNotNull(connection.getUDFs()); + assertEquals(0, connection.getUDFs().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs is correct (reduced to only NONE)! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid list of UDFs: + try{ + ServiceConnection connection = new DefaultServiceConnection(udfsProp); + assertNotNull(connection.getUDFs()); + assertEquals(2, connection.getUDFs().size()); + Iterator<FunctionDef> it = connection.getUDFs().iterator(); + assertEquals("toto(a VARCHAR)", it.next().toString()); + assertEquals("titi(b REAL) -> DOUBLE", it.next().toString()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains valid items! \nCaught exception: " + getPertinentMessage(e)); + } + + // Valid list of UDFs containing one UDF with a classpath: + try{ + ServiceConnection connection = new DefaultServiceConnection(udfsWithClassPathProp); + assertNotNull(connection.getUDFs()); + assertEquals(1, connection.getUDFs().size()); + FunctionDef def = connection.getUDFs().iterator().next(); + assertEquals("toto(a VARCHAR) -> VARCHAR", def.toString()); + assertEquals(UDFToto.class, def.getUDFClass()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains valid items! \nCaught exception: " + getPertinentMessage(e)); + } + + // "NONE" inside a UDFs list: + try{ + new DefaultServiceConnection(udfsListWithNONEorANYProp); + fail("This MUST have failed because the given UDFs list contains at least 2 items, whose one is ANY!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: unexpected character at position 27 in the property " + KEY_UDFS + ": \"A\"! A UDF declaration must have one of the following syntaxes: \"[signature]\" or \"[signature,{classpath}]\".", e.getMessage()); + } + + // UDF with no brackets: + try{ + new DefaultServiceConnection(udfsWithMissingBracketsProp); + fail("This MUST have failed because one UDFs list item has no brackets!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: unexpected character at position 1 in the property " + KEY_UDFS + ": \"t\"! A UDF declaration must have one of the following syntaxes: \"[signature]\" or \"[signature,{classpath}]\".", e.getMessage()); + } + + // UDFs whose one item have more parts than supported: + try{ + new DefaultServiceConnection(udfsWithWrongParamLengthProp); + fail("This MUST have failed because one UDFs list item has too many parameters!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: only two items (signature and classpath) can be given within brackets. (position in the property " + KEY_UDFS + ": 58)", e.getMessage()); + } + + // UDF with missing definition part (or wrong since there is no comma): + try{ + new DefaultServiceConnection(udfsWithMissingDefProp1); + fail("This MUST have failed because one UDFs list item has a wrong signature part (it has been forgotten)!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: Wrong function definition syntax! Expected syntax: \"<regular_identifier>(<parameters>?) <return_type>?\", where <regular_identifier>=\"[a-zA-Z]+[a-zA-Z0-9_]*\", <return_type>=\" -> <type_name>\", <parameters>=\"(<regular_identifier> <type_name> (, <regular_identifier> <type_name>)*)\", <type_name> should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"foo()\", \"foo() -> VARCHAR\", \"foo(param INTEGER)\", \"foo(param1 INTEGER, param2 DOUBLE) -> DOUBLE\" (position in the property " + KEY_UDFS + ": 2-33)", e.getMessage()); + } + + // UDF with missing definition part (or wrong since there is no comma): + try{ + new DefaultServiceConnection(udfsWithMissingDefProp2); + fail("This MUST have failed because one UDFs list item has no signature part!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Missing UDF declaration! (position in the property " + KEY_UDFS + ": 2-2)", e.getMessage()); + } + + // Empty UDF item (without comma): + try{ + ServiceConnection connection = new DefaultServiceConnection(emptyUdfItemProp1); + assertNotNull(connection.getUDFs()); + assertEquals(0, connection.getUDFs().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains one empty UDF (which should be merely ignored)! \nCaught exception: " + getPertinentMessage(e)); + } + + // Empty UDF item (with comma): + try{ + ServiceConnection connection = new DefaultServiceConnection(emptyUdfItemProp2); + assertNotNull(connection.getUDFs()); + assertEquals(0, connection.getUDFs().size()); + }catch(Exception e){ + fail("This MUST have succeeded because the given list of UDFs contains one empty UDF (which should be merely ignored)! \nCaught exception: " + getPertinentMessage(e)); + } + + // UDF item without its closing bracket: + try{ + new DefaultServiceConnection(udfWithMissingEndBracketProp); + fail("This MUST have failed because one UDFs list item has no closing bracket!"); + }catch(Exception e){ + assertEquals(TAPException.class, e.getClass()); + assertEquals("Wrong UDF declaration syntax: missing closing bracket at position 24!", e.getMessage()); } }