Commit 5c5b4681 authored by Grégory Mantelet's avatar Grégory Mantelet
Browse files

[ADQL] Make UDF optional features. A special LanguageFeature constructor must be

used: `LanguageFeature(FunctionDef)`. This way, the syntax of the function
definition string is checked and function name and parameters can be correctly
extracted. These information are particularly important to determine at the
ADQLParser level whether a DefaultUDF is supported or not while not knowing the
parameters and function types.
parent 94c6d522
Loading
Loading
Loading
Loading
+31 −18
Original line number Diff line number Diff line
@@ -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;
@@ -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;
+24 −6
Original line number Diff line number Diff line
@@ -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)         *
+103 −12
Original line number Diff line number Diff line
@@ -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&eacute;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
		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
+43 −2
Original line number Diff line number Diff line
@@ -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
+5 −1
Original line number Diff line number Diff line
@@ -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;

Loading