diff --git a/src/adql/db/FunctionDef.java b/src/adql/db/FunctionDef.java index 55d9ef4a6f993dc3c2650f513279fcb316e7b388..b1e27b1ca9bdd1c39ba781b79385ea19e693afa1 100644 --- a/src/adql/db/FunctionDef.java +++ b/src/adql/db/FunctionDef.java @@ -15,7 +15,7 @@ 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 2015-2020 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ @@ -26,6 +26,7 @@ import java.util.regex.Pattern; import adql.db.DBType.DBDatatype; import adql.parser.ParseException; +import adql.parser.feature.LanguageFeature; import adql.query.operand.ADQLOperand; import adql.query.operand.function.ADQLFunction; import adql.query.operand.function.DefaultUDF; @@ -51,10 +52,10 @@ import adql.query.operand.function.UserDefinedFunction; * A description of this function may be set thanks to the public class * attribute {@link #description}. * </p> - * + * * @author Grégory Mantelet (CDS;ARI) * @version 2.0 (08/2020) - * + * * @since 1.3 */ public class FunctionDef implements Comparable<FunctionDef> { @@ -194,12 +195,12 @@ public class FunctionDef implements Comparable<FunctionDef> { /** * Translation to apply for this User Defined Function. - * + * * <p> * It can be any string. Any <code>$i</code> substring (i being an integer * >0) will be replaced by the corresponding function argument. * </p> - * + * * <p>For instance, for the UDF signature</p> * <pre>foo(p1 VARCHAR, p2 DOUBLE)</pre> * <p>, the translation pattern</p> @@ -287,13 +288,13 @@ public class FunctionDef implements Comparable<FunctionDef> { /** * Create a function definition. - * + * * <p> * The created function will have <b>no return type</b> and some parameters * (except if the given array is NULL or empty). * </p> - * - * @param fctName Name of the function. + * + * @param fctName Name of the function. * @param params Parameters of this function. * <i>If NULL or empty, this function will have no * parameter.</i> @@ -367,14 +368,14 @@ public class FunctionDef implements Comparable<FunctionDef> { /** * Tell whether this function returns an unknown type. - * + * * <p> * If this function returns <code>true</code>, {@link #isNumeric()}, * {@link #isString()} and {@link #isGeometry()} <b>MUST ALL</b> return * <code>false</code>. Otherwise, one of these 3 last functions MUST * return <code>true</code>. - * </p> - * + * </p> + * * @return <code>true</code> if this function returns an unknown/unresolved * /unsupported type, * <code>false</code> otherwise. @@ -414,7 +415,7 @@ public class FunctionDef implements Comparable<FunctionDef> { /** * Get the class of the {@link UserDefinedFunction} able to represent the * function defined here in an ADQL tree. - * + * * <p><i><b>Note:</b> * This getter should return always NULL if the function defined here is * not a user defined function. @@ -426,7 +427,7 @@ public class FunctionDef implements Comparable<FunctionDef> { * 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> */ @@ -437,7 +438,7 @@ public class FunctionDef implements Comparable<FunctionDef> { /** * Set the class of the {@link UserDefinedFunction} able to represent the * function defined here in an ADQL tree. - * + * * <p><i><b>Note:</b> * If this {@link FunctionDef} defines an ordinary ADQL function - and not * a user defined function - no class should be set here. @@ -450,12 +451,12 @@ public class FunctionDef implements Comparable<FunctionDef> { * will be created on the fly by the library when needed if it turns out * that no UDF class is set. * </i></p> - * + * * <p><i><b>WARNING:</b> * If successful, this operation will reset to NULL any translation pattern * already set with {@link #setTranslationPattern(String)}. * </i></p> - * + * * @param udfClass Class to use to represent in an ADQL tree the User * Defined Function defined in this {@link FunctionDef}. * @@ -479,7 +480,7 @@ public class FunctionDef implements Comparable<FunctionDef> { // Set to NULL the translation pattern (if any): this.translationPattern = null; - } catch(SecurityException e) { + }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[]!"); @@ -489,7 +490,7 @@ public class FunctionDef implements Comparable<FunctionDef> { /** * Get the translation pattern to apply on any ADQL function implementing * this UDF definition. - * + * * @return The corresponding {@link UserDefinedFunction}. * <i>NULL if no translation pattern is defined.</i> * @@ -724,6 +725,18 @@ public class FunctionDef implements Comparable<FunctionDef> { } } + /** + * Create a {@link LanguageFeature} corresponding and linked to this + * {@link FunctionDef}. + * + * @return The corresponding LanguageFeature. + * + * @since 2.0 + */ + public final LanguageFeature toLanguageFeature() { + return new LanguageFeature(this); + } + @Override public String toString() { return serializedForm; diff --git a/src/adql/parser/feature/FeatureSet.java b/src/adql/parser/feature/FeatureSet.java index dafea9a89c703a413374e3747692ba150cc5cd54..04b7a61544de50c57fe929e6b692b1ead2477e85 100644 --- a/src/adql/parser/feature/FeatureSet.java +++ b/src/adql/parser/feature/FeatureSet.java @@ -19,6 +19,7 @@ package adql.parser.feature; * Copyright 2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) */ +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -26,6 +27,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import adql.db.FunctionDef; import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CentroidFunction; @@ -550,6 +552,22 @@ public class FeatureSet implements Iterable<LanguageFeature> { return getSupportedFeatures(); } + /** + * Get the list of the definition of all declared UDFs. + * + * @return List of all supported UDFs. + */ + public final Collection<FunctionDef> getSupportedUDFList() { + Set<LanguageFeature> supportedUDFs = supportedFeatures.get(LanguageFeature.TYPE_UDF); + if (supportedUDFs != null) { + Set<FunctionDef> definitions = new HashSet<FunctionDef>(supportedUDFs.size()); + for(LanguageFeature feature : supportedUDFs) + definitions.add(feature.udfDefinition); + return definitions; + } else + return new HashSet<FunctionDef>(0); + } + /* ********************************************************************** * * * ALL AVAILABLE FEATURES (according to the ADQL Language) * @@ -557,22 +575,22 @@ public class FeatureSet implements Iterable<LanguageFeature> { ********************************************************************** */ /*public static final LanguageFeature ILIKE = new LanguageFeature(FeatureType.ADQL_STRING, "ILIKE"); // TODO ILIKE - + public static final LanguageFeature UNION = new LanguageFeature(FeatureType.ADQL_SETS, "UNION"); // TODO UNION public static final LanguageFeature EXCEPT = new LanguageFeature(FeatureType.ADQL_SETS, "EXCEPT"); // TODO EXCEPT public static final LanguageFeature INTERSECT = new LanguageFeature(FeatureType.ADQL_SETS, "INTERSECT"); // TODO INTERSECT - + public static final LanguageFeature WITH = new LanguageFeature(FeatureType.ADQL_COMMON_TABLE, "WITH"); // TODO WITH - + public static final LanguageFeature CAST = new LanguageFeature(FeatureType.ADQL_TYPE, "CAST"); // TODO CAST - + public static final LanguageFeature IN_UNIT = new LanguageFeature(FeatureType.ADQL_UNIT, "IN_UNIT"); // TODO IN_UNIT - + public static final LanguageFeature BIT_AND = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_AND"); // TODO BIT_AND public static final LanguageFeature BIT_OR = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_OR"); // TODO BIT_OR public static final LanguageFeature BIT_XOR = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_XOR"); // TODO BIT_XOR public static final LanguageFeature BIT_NOT = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_NOT"); // TODO BIT_NOT - + public static final LanguageFeature OFFSET = new LanguageFeature(FeatureType.ADQL_OFFSET, "OFFSET"); // TODO OFFSET*/ /** All standard features available. diff --git a/src/adql/parser/feature/LanguageFeature.java b/src/adql/parser/feature/LanguageFeature.java index f4c0a470810fff22453f82e3e1d8bc887488cb2a..abca273c8114c1ff5ad5721dd52e960f32b1fcd0 100644 --- a/src/adql/parser/feature/LanguageFeature.java +++ b/src/adql/parser/feature/LanguageFeature.java @@ -21,6 +21,8 @@ package adql.parser.feature; import java.util.Objects; +import adql.db.FunctionDef; + /** * Description of an ADQL's language feature. * @@ -42,13 +44,20 @@ import java.util.Objects; * set to NULL. * </i></p> * + * <p><i><b>IMPORTANT note about UDF:</b> + * To create a UDF feature (i.e. a {@link LanguageFeature} with the type + * {@link #TYPE_UDF}), ONLY ONE constructor can be used: + * {@link #LanguageFeature(FunctionDef, String)}. Any attempt with another + * public constructor will fail. + * </i></p> + * * @author Grégory Mantelet (CDS) * @version 2.0 (07/2019) * @since 2.0 * * @see FeatureSet */ -public class LanguageFeature { +public final class LanguageFeature { /** Unique identifier of this language feature. * <p><i><b>MANDATORY</b></i></p> @@ -96,6 +105,10 @@ public class LanguageFeature { * </ul> */ public final String form; + /** Definition of the UDF represented by this {@link LanguageFeature}. + * <p><i><b>OPTIONAL</b></i></p> */ + public final FunctionDef udfDefinition; + /** Is this feature optional in the ADQL grammar? * <p><i><b>MANDATORY</b></i></p> * <p> @@ -113,17 +126,18 @@ public class LanguageFeature { public final boolean optional; /** Description of this feature. - * <p><i><b>OPTIONAL</b></i></p> - * <p><i><b>Note:</b> - * This field is generally set when declaring a User Defined Function - * (UDF). - * </i></p> */ - public final String description; + * <p><i><b>OPTIONAL</b></i></p> */ + public String description; /** * Create a <em>de-facto supported</em> (i.e. non-optional) language * feature. * + * <p><i><b>IMPORTANT note:</b> + * To create a UDF feature, DO NOT use this constructor. + * You MUST use instead {@link #LanguageFeature(FunctionDef, String)}. + * </i></p> + * * @param type [OPTIONAL] Category of the language feature. * <em>(see all static attributes starting with * <code>TYPE_</code>)</em> @@ -139,6 +153,11 @@ public class LanguageFeature { /** * Create a language feature. * + * <p><i><b>IMPORTANT note:</b> + * To create a UDF feature, DO NOT use this constructor. + * You MUST use instead {@link #LanguageFeature(FunctionDef, String)}. + * </i></p> + * * @param type [OPTIONAL] Category of the language feature. * <em>(see all static attributes starting with * <code>TYPE_</code>)</em> @@ -158,6 +177,11 @@ public class LanguageFeature { /** * Create a language feature. * + * <p><i><b>IMPORTANT note:</b> + * To create a UDF feature, DO NOT use this constructor. + * You MUST use instead {@link #LanguageFeature(FunctionDef, String)}. + * </i></p> + * * @param type [OPTIONAL] Category of the language feature. * <em>(see all static attributes starting with * <code>TYPE_</code>)</em> @@ -172,12 +196,69 @@ public class LanguageFeature { * @throws NullPointerException If given form is missing. */ public LanguageFeature(final String type, final String form, final boolean optional, final String description) throws NullPointerException { + this(type, form, null, optional, description); + } + + /** + * Create a UDF feature. + * + * @param udfDef [REQUIRED] Detailed definition of the UDF feature. + * + * @throws NullPointerException If given {@link FunctionDef} is missing. + */ + public LanguageFeature(final FunctionDef udfDef) throws NullPointerException { + this(udfDef, null); + } + + /** + * Create a UDF feature. + * + * @param udfDef [REQUIRED] Detailed definition of the UDF feature. + * @param description [OPTIONAL] Description overwriting the description + * provided in the given {@link FunctionDef}. + * <em>If NULL, the description of the + * {@link FunctionDef} will be used. If empty string, + * no description will be set.</em> + * + * @throws NullPointerException If given {@link FunctionDef} is missing. + */ + public LanguageFeature(final FunctionDef udfDef, final String description) throws NullPointerException { + this(LanguageFeature.TYPE_UDF, udfDef.toString(), udfDef, true, (description == null ? udfDef.description : (description.trim().isEmpty() ? null : description))); + } + + /** + * Create a language feature. + * + * <p><i><b>IMPORTANT note:</b> + * To create a UDF feature, the parameter udfDef MUST be NON-NULL. + * </i></p> + * + * @param type [OPTIONAL] Category of the language feature. + * <em>(see all static attributes starting with + * <code>TYPE_</code>)</em> + * @param form [REQUIRED] Name (or function signature) of the + * language feature. + * @param udfDef [REQUIRED if type=UDF] Detailed definition of the + * UDF feature. + * @param optional [REQUIRED] <code>true</code> if the feature is by + * default supported in the ADQL standard, + * <code>false</code> if the ADQL client must declare + * it as supported in order to use it. + * @param description [OPTIONAL] Description of this feature. + * + * @throws NullPointerException If given form or udfDef is missing. + */ + private LanguageFeature(final String type, final String form, final FunctionDef udfDef, final boolean optional, final String description) throws NullPointerException { this.type = (type == null || type.trim().isEmpty()) ? null : type.trim(); if (form == null || form.trim().isEmpty()) throw new NullPointerException("Missing form/name of the language feature to create!"); this.form = form.trim(); + if (TYPE_UDF.equals(this.type) && udfDef == null) + throw new NullPointerException("Missing UDF definition! To declare a UDF feature, you MUST use the constructor LanguageFeature(FunctionDef, ...) with a non-NULL FunctionDef instance."); + this.udfDefinition = udfDef; + this.id = (this.type == null ? "" : this.type) + "!" + this.form; this.optional = optional; @@ -187,15 +268,25 @@ public class LanguageFeature { @Override public boolean equals(final Object obj) { - if ((obj != null) && (obj instanceof LanguageFeature)) - return id.equals(((LanguageFeature)obj).id); - else - return false; + if ((obj != null) && (obj instanceof LanguageFeature)) { + // Equals IF SAME ID: + if (id.equals(((LanguageFeature)obj).id)) + return true; + // If UDF, equals IF SAME NAME and SAME NB PARAMETERS: + else if (TYPE_UDF.equals(type) && type.equals(((LanguageFeature)obj).type)) { + FunctionDef udfDefinition2 = ((LanguageFeature)obj).udfDefinition; + return udfDefinition.name.equalsIgnoreCase(udfDefinition2.name) && (udfDefinition.nbParams == udfDefinition2.nbParams); + } + } + return false; } @Override public int hashCode() { - return Objects.hash(id, form); + if (udfDefinition != null) + return Objects.hash(type, udfDefinition.name.toLowerCase(), udfDefinition.nbParams); + else + return Objects.hash(type, form, -1); } @Override diff --git a/src/adql/query/operand/function/DefaultUDF.java b/src/adql/query/operand/function/DefaultUDF.java index 01e2bec5f61635fecd81c5eb1751bd720ecf8cd5..b67b40d2bbdad6ea3b48cac93c7d0be58909320c 100644 --- a/src/adql/query/operand/function/DefaultUDF.java +++ b/src/adql/query/operand/function/DefaultUDF.java @@ -22,7 +22,10 @@ import java.util.regex.Matcher; * Astronomisches Rechen Institut (ARI) */ +import adql.db.DBType; +import adql.db.DBType.DBDatatype; import adql.db.FunctionDef; +import adql.db.FunctionDef.FunctionParam; import adql.parser.feature.LanguageFeature; import adql.query.ADQLList; import adql.query.ADQLObject; @@ -67,7 +70,7 @@ public final class DefaultUDF extends UserDefinedFunction { for(ADQLOperand p : params) parameters.add(p); } - languageFeature = new LanguageFeature(LanguageFeature.TYPE_UDF, functionName + "(...)", true, null); + generateLanguageFeature(); } /** @@ -122,11 +125,49 @@ public final class DefaultUDF extends UserDefinedFunction { * @since 1.3 */ public final void setDefinition(final FunctionDef def) throws IllegalArgumentException { + // Ensure the definition is compatible with this ADQL function: if (def != null && (def.name == null || !functionName.equalsIgnoreCase(def.name))) throw new IllegalArgumentException("The parsed function name (" + functionName + ") does not match to the name of the given UDF definition (" + def.name + ")."); + // Set the new definition (may be NULL): this.definition = def; - languageFeature = new LanguageFeature(LanguageFeature.TYPE_UDF, this.definition.toString(), true, this.definition.description); + + // Update the Language Feature of this ADQL function: + // ...if no definition, generate a default LanguageFeature: + if (this.definition == null) + generateLanguageFeature(); + // ...otherwise, use the definition to set the LanguageFeature: + else + languageFeature = new LanguageFeature(this.definition); + } + + /** + * Generate and set a default {@link LanguageFeature} for this ADQL + * function. + * + * <p><i><b>Note:</b> + * Knowing neither the parameters name nor their type, the generated + * LanguageFeature will just set an unknown type and set a default + * parameter name (index prefixed with `$`). + * </i></p> + * + * @since 2.0 + */ + private void generateLanguageFeature() { + // Create an unknown DBType: + DBType unknownType = new DBType(DBDatatype.UNKNOWN); + unknownType.type.setCustomType("type"); + + // Create the list of input parameters: + FunctionParam[] inputParams = new FunctionParam[parameters.size()]; + for(int i = 1; i <= parameters.size(); i++) + inputParams[i - 1] = new FunctionParam("param" + i, unknownType); + + // Create the Function Definition: + FunctionDef fctDef = new FunctionDef(functionName, unknownType, inputParams); + + // Finally create the LanguageFeature: + languageFeature = new LanguageFeature(fctDef); } @Override diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java index cb602976ef94ef22bef51634f5fa430ee3e9b437..72ca3dde2ecf9969fce90bb1520fbfbd68fb596c 100644 --- a/test/adql/db/TestDBChecker.java +++ b/test/adql/db/TestDBChecker.java @@ -267,6 +267,7 @@ public class TestDBChecker { public void testUDFManagement() { // UNKNOWN FUNCTIONS ARE NOT ALLOWED: ADQLParser parser = parserFactory.createParser(); + parser.getSupportedFeatures().allowAnyUdf(true); parser.setQueryChecker(new DBChecker(tables, new ArrayList<FunctionDef>(0))); // Test with a simple ADQL query without unknown or user defined function: @@ -291,6 +292,7 @@ public class TestDBChecker { // DECLARE THE UDFs: FunctionDef[] udfs = new FunctionDef[]{ new FunctionDef("toto", new DBType(DBDatatype.VARCHAR)), new FunctionDef("tata", new DBType(DBDatatype.INTEGER)) }; parser = parserFactory.createParser(); + parser.getSupportedFeatures().allowAnyUdf(true); parser.setQueryChecker(new DBChecker(tables, Arrays.asList(udfs))); // Test again: @@ -341,6 +343,7 @@ public class TestDBChecker { 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 = parserFactory.createParser(); + parser.getSupportedFeatures().allowAnyUdf(true); parser.setQueryChecker(new DBChecker(tables, Arrays.asList(udfs))); try { ADQLQuery query = parser.parseQuery("SELECT toto('blabla') FROM foo;"); @@ -374,6 +377,7 @@ public class TestDBChecker { 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 = parserFactory.createParser(); + parser.getSupportedFeatures().allowAnyUdf(true); parser.setQueryChecker(new DBChecker(tables, Arrays.asList(udfs))); try { parser.parseQuery("SELECT toto('blabla') FROM foo;"); @@ -893,7 +897,7 @@ public class TestDBChecker { } public static class UDFToto extends UserDefinedFunction { - private LanguageFeature FEATURE = new LanguageFeature(LanguageFeature.TYPE_UDF, getName() + "(VARCHAR) -> VARCHAR"); + private LanguageFeature FEATURE = (new FunctionDef(getName(), new DBType(DBDatatype.VARCHAR), new FunctionParam[]{ new FunctionParam("txt", new DBType(DBDatatype.VARCHAR)) })).toLanguageFeature(); protected StringConstant fakeParam; diff --git a/test/adql/parser/feature/TestFeatureSet.java b/test/adql/parser/feature/TestFeatureSet.java index e58fb7575ae2dab97dc5a63f9adbd8f51b993d3d..3d256aabac445d26aeb3545a4d0188e3b703447c 100644 --- a/test/adql/parser/feature/TestFeatureSet.java +++ b/test/adql/parser/feature/TestFeatureSet.java @@ -11,6 +11,9 @@ import java.util.Set; import org.junit.Test; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.FunctionDef; import adql.query.ColumnReference; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.PolygonFunction; @@ -217,7 +220,7 @@ public class TestFeatureSet { /* here is a custom Language Feature (i.e. not part of the * availableFeatures list): */ - set.support(new LanguageFeature(LanguageFeature.TYPE_UDF, "foo(VARCHAR) -> BOOLEAN", true)); + set.support(new LanguageFeature(new FunctionDef("foo", new DBType(DBDatatype.SMALLINT), new FunctionDef.FunctionParam[]{ new FunctionDef.FunctionParam("", new DBType(DBDatatype.VARCHAR)) }))); // unsupport all currently supported features: set.unsupportAll();