Commit f6a089c1 authored by gmantele's avatar gmantele
Browse files

[TAP] Add an optional parameter to a UDF property: the UDF description.

Although the Java code allowed the specification of a description of a User
Defined Function, it was not possible to set one in the UDFs listed in the
configuration file.
parent 8fe8e24d
Loading
Loading
Loading
Loading
+167 −221
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ package tap.config;
 * You should have received a copy of the GNU Lesser General Public License
 * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2016-2017 - Astronomisches Rechen Institut (ARI)
 * Copyright 2016-2018 - Astronomisches Rechen Institut (ARI)
 */

import static tap.config.TAPConfiguration.DEFAULT_ASYNC_FETCH_SIZE;
@@ -89,6 +89,8 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import adql.db.FunctionDef;
import adql.db.STCS;
@@ -127,7 +129,7 @@ import uws.service.log.UWSLog.LogLevel;
 * </p>
 *
 * @author Gr&eacute;gory Mantelet (ARI)
 * @version 2.1 (09/2017)
 * @version 2.1 (02/2018)
 * @since 2.0
 */
public final class ConfigurableServiceConnection implements ServiceConnection {
@@ -1118,6 +1120,20 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
		}
	}

	private final String REGEXP_SIGNATURE = "(\\([^()]*\\)|[^,])*";

	private final String REGEXP_CLASSPATH = "\\{[^{}]*\\}";

	private final String REGEXP_DESCRIPTION = "\"((\\\"|[^\"])*)\"";

	private final String REGEXP_UDF = "\\[\\s*(" + REGEXP_SIGNATURE + ")\\s*(,\\s*(" + REGEXP_CLASSPATH + ")?\\s*(,\\s*(" + REGEXP_DESCRIPTION + ")?\\s*)?)?\\]";

	private final String REGEXP_UDFS = "\\s*(" + REGEXP_UDF + ")\\s*(,(.*))?";
	private final int GROUP_SIGNATURE = 2;
	private final int GROUP_CLASSPATH = 5;
	private final int GROUP_DESCRIPTION = 8;
	private final int GROUP_NEXT_UDFs = 11;

	/**
	 * Initialize the list of all known and allowed User Defined Functions.
	 *
@@ -1130,7 +1146,7 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
		String propValue = getProperty(tapConfig, KEY_UDFS);

		// NO VALUE => NO UNKNOWN FCT ALLOWED!
		if (propValue == null)
		if (propValue == null || propValue.trim().length() == 0)
			udfs = new ArrayList<FunctionDef>(0);

		// "NONE" => NO UNKNOWN FCT ALLOWED (= none of the unknown functions are allowed)!
@@ -1144,85 +1160,31 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
		// 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 class name */
							within_classpath = true;
							buf.append(c);
							break;
						case ',': /* separation between the signature and the class name */
							// 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 class name) can be given within brackets. (position in the property " + KEY_UDFS + ": " + ind + ")");
							else{
								// end of the signature and start of the class name:
								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());
			Pattern patternUDFS = Pattern.compile(REGEXP_UDFS);
			String udfList = propValue;
			int udfOffset = 1;
			while(udfList != null){
				Matcher matcher = patternUDFS.matcher(udfList);
				if (matcher.matches()){

							// no signature...
					// Fetch the signature, classpath and description:
					String signature = matcher.group(GROUP_SIGNATURE),
							classpath = matcher.group(GROUP_CLASSPATH),
							description = matcher.group(GROUP_DESCRIPTION);

					// If no signature...
					boolean ignoreUdf = false;
					if (signature == null || signature.length() == 0){
						// ...BUT a class name => error
						if (classpath != null)
									throw new TAPException("Missing UDF declaration! (position in the property " + KEY_UDFS + ": " + posSignature[0] + "-" + posSignature[1] + ")");
							throw new TAPException("Missing UDF declaration! (position in the property " + KEY_UDFS + ": " + (udfOffset + matcher.start(GROUP_SIGNATURE)) + "-" + (udfOffset + matcher.end(GROUP_SIGNATURE)) + ")");
						// ... => ignore this item
						else
									continue;
							ignoreUdf = true;
					}

							// add the new UDF in the list:
					if (!ignoreUdf){
						// Add the new UDF in the list:
						try{
							// resolve the function signature:
							FunctionDef def = FunctionDef.parse(signature);
@@ -1236,47 +1198,31 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
										// set the class inside the UDF definition:
										def.setUDFClass(fctClass);
									}catch(TAPException te){
											throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": " + te.getMessage() + " (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")", te);
										throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": " + te.getMessage() + " (position in the property " + KEY_UDFS + ": " + (udfOffset + matcher.start(GROUP_CLASSPATH)) + "-" + (udfOffset + matcher.end(GROUP_CLASSPATH)) + ")", te);
									}catch(IllegalArgumentException iae){
											throw new TAPException("Invalid class name 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] + ")");
										throw new TAPException("Invalid class name 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 + ": " + (udfOffset + matcher.start(GROUP_CLASSPATH)) + "-" + (udfOffset + matcher.end(GROUP_CLASSPATH)) + ")");
									}
								}else
										throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": \"" + classpath + "\" is not a class name (or is not surrounding by {} as expected in this property file)! (position in the property " + KEY_UDFS + ": " + posClassPath[0] + "-" + posClassPath[1] + ")");
									throw new TAPException("Invalid class name for the UDF definition \"" + def + "\": \"" + classpath + "\" is not a class name (or is not surrounding by {} as expected in this property file)! (position in the property " + KEY_UDFS + ": " + (udfOffset + matcher.start(GROUP_CLASSPATH)) + "-" + (udfOffset + matcher.end(GROUP_CLASSPATH)) + ")");
							}
							// set the description if any:
							if (description != null)
								def.description = description;
							// 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,{className}]\".");
					}
							throw new TAPException("Wrong UDF declaration syntax: " + pe.getMessage() + " (position in the property " + KEY_UDFS + ": " + (udfOffset + matcher.start(GROUP_SIGNATURE)) + "-" + (udfOffset + matcher.end(GROUP_SIGNATURE)) + ")", pe);
						}
					}

			// 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() + "!");
					// Prepare the next iteration (i.e. the other UDFs):
					udfList = matcher.group(GROUP_NEXT_UDFs);
					if (udfList != null && udfList.trim().length() == 0)
						udfList = null;
					udfOffset += matcher.start(GROUP_NEXT_UDFs);
				}else
					throw new TAPException("Wrong UDF declaration syntax: \"" + udfList + "\"! (position in the property " + KEY_UDFS + ": " + udfOffset + "-" + (propValue.length() + 1) + ")");
			}
		}
	}

+19 −6
Original line number Diff line number Diff line
@@ -718,11 +718,18 @@
				<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, className]</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>className</i> is the name 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 name must be specified if the function to represent has a signature
						(and more particularly a name) different in ADQL and in SQL.
						
						Each item of the list must have the following syntax: <code>[fct_signature]</code>,
						<code>[fct_signature, className]</code> or <code>[fct_signature, className, description]</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>className</i> is the name 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 name must be specified if
						the function to represent has a signature (and more particularly a name)
						different in ADQL and in SQL. <i>description</i> is the human description of the
						function to be displayed in the <i>/capabilities</i> of the TAP service. It must be
						written between double quotes.
					</p>
					<p>
						If the list is empty (no item), all unknown functions are forbidden. And if the special value <code>ANY</code> is given, any unknown function is allowed ;
@@ -730,7 +737,13 @@
					</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) -&gt; String], [random() -&gt; DOUBLE]</li><li>[newFct(x double)-&gt;double, {apackage.MyNewFunction}]</li></ul></td>
				<td><ul><li>ø <em>(default)</em></li>
						<li>ANY</li>
						<li>[trim(txt String) -&gt; String], [random() -&gt; DOUBLE]</li>
						<li>[newFct(x double)-&gt;double, {apackage.MyNewFunction}]</li>
						<li>[ivo_healpix_index(hpxOrder integer, ra double, dec double) -&gt; bigint, {adql.query.operand.function.healpix.HealpixIndex}, "Compute the index of the \"Healpix cell\" containing the specified position at the given Healpix order."]</li>
						<li>[random() -&gt; DOUBLE,,"Generate a random number."]</li>
				</ul></td>
			</tr>
			
			<tr><td colspan="5">Additional TAP Resources</td></tr>
+19 −8
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
#                        FULL TAP CONFIGURATION FILE                           #
#                                                                              #
# TAP Version: 2.1                                                             #
# Date: 11 Feb. 2018                                                           #
# Date: 26 Feb. 2018                                                           #
# Author: Gregory Mantelet (ARI)                                               #
#                                                                              #
################################################################################ 
@@ -663,13 +663,24 @@ geometries =
# [OPTIONAL]
# Comma-separated list of all allowed UDFs (User Defined Functions).
# 
# Each item of the list must have the following syntax: [fct_signature] or
# [fct_signature, className]. fct_function is the function signature. Its syntax
# is the same as in TAPRegExt. className is the name 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 name
# must be specified if the function to represent has a signature (and more
# particularly a name) different in ADQL and in SQL.
# Each item of the list must have the following syntax: [fct_signature],
# [fct_signature, className] or [fct_signature, className, description].
# fct_function is the function signature. Its syntax is the same as in
# TAPRegExt. className is the name 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 name must be specified if
# the function to represent has a signature (and more particularly a name)
# different in ADQL and in SQL. description is the human description of the
# function to be displayed in the /capabilities of the TAP service. It must be
# written between double quotes.
# 
# Example: udfs = [ivo_healpix_index(hpxOrder integer, ra double, dec double)
#                  -> bigint, {adql.query.operand.function.healpix.HealpixIndex}
#                  , "Compute the index of the \"Healpix cell\" containing the
#                     specified position at the given Healpix order."],
#                 [trim(txt String) -> String],
#                 [newFct(x double)-&gt;double, {apackage.MyNewFunction}],
#                 [random() -> DOUBLE,,"Generate a random number."]
# 
# If the list is empty (no item), all unknown functions are forbidden. And if
# the special value ANY is given, any unknown function is allowed ; consequently
+70 −23
Original line number Diff line number Diff line
@@ -108,10 +108,12 @@ public class TestConfigurableServiceConnection {
			anyCoordSysProp, noneInsideCoordSysProp, unknownCoordSysProp,
			geometriesProp, noneGeomProp, anyGeomProp, noneInsideGeomProp,
			unknownGeomProp, anyUdfsProp, noneUdfsProp, udfsProp,
			udfsWithClassNameProp, udfsListWithNONEorANYProp,
			udfsWithWrongParamLengthProp, udfsWithMissingBracketsProp,
			udfsWithMissingDefProp1, udfsWithMissingDefProp2, emptyUdfItemProp1,
			emptyUdfItemProp2, udfWithMissingEndBracketProp, customFactoryProp,
			udfsWithClassNameProp, udfsWithClassNameAndDescriptionProp,
			udfsWithEmptyOptParamsProp, udfsListWithNONEorANYProp,
			udfsWithWrongDescriptionFormatProp, udfsWithWrongParamLengthProp,
			udfsWithMissingBracketsProp, udfsWithMissingDefProp1,
			udfsWithMissingDefProp2, emptyUdfItemProp1, emptyUdfItemProp2,
			udfWithMissingEndBracketProp, customFactoryProp,
			customConfigurableFactoryProp, badCustomFactoryProp;

	@BeforeClass
@@ -305,11 +307,20 @@ public class TestConfigurableServiceConnection {
		udfsWithClassNameProp = (Properties)validProp.clone();
		udfsWithClassNameProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}]");

		udfsWithClassNameAndDescriptionProp = (Properties)validProp.clone();
		udfsWithClassNameAndDescriptionProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, \"Bla \"bla\".\"]");

		udfsWithEmptyOptParamsProp = (Properties)validProp.clone();
		udfsWithEmptyOptParamsProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR,,  	 ]");

		udfsListWithNONEorANYProp = (Properties)validProp.clone();
		udfsListWithNONEorANYProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR],ANY");

		udfsWithWrongDescriptionFormatProp = (Properties)validProp.clone();
		udfsWithWrongDescriptionFormatProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, Blabla]");

		udfsWithWrongParamLengthProp = (Properties)validProp.clone();
		udfsWithWrongParamLengthProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, foo]");
		udfsWithWrongParamLengthProp.setProperty(KEY_UDFS, "[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, \"Blabla\", foo]");

		udfsWithMissingBracketsProp = (Properties)validProp.clone();
		udfsWithMissingBracketsProp.setProperty(KEY_UDFS, "toto(a string)->VARCHAR");
@@ -1040,6 +1051,33 @@ public class TestConfigurableServiceConnection {
			FunctionDef def = connection.getUDFs().iterator().next();
			assertEquals("toto(a VARCHAR) -> VARCHAR", def.toString());
			assertEquals(UDFToto.class, def.getUDFClass());
			assertNull(def.description);
		}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 class name AND a description:
		try{
			ServiceConnection connection = new ConfigurableServiceConnection(udfsWithClassNameAndDescriptionProp);
			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());
			assertEquals("Bla \"bla\".", def.description);
		}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 empty optional parameters:
		try{
			ServiceConnection connection = new ConfigurableServiceConnection(udfsWithEmptyOptParamsProp);
			assertNotNull(connection.getUDFs());
			assertEquals(1, connection.getUDFs().size());
			FunctionDef def = connection.getUDFs().iterator().next();
			assertEquals("toto(a VARCHAR) -> VARCHAR", def.toString());
			assertNull(def.getUDFClass());
			assertNull(def.description);
		}catch(Exception e){
			fail("This MUST have succeeded because the given list of UDFs contains valid items! \nCaught exception: " + getPertinentMessage(e));
		}
@@ -1050,7 +1088,7 @@ public class TestConfigurableServiceConnection {
			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,{className}]\".", e.getMessage());
			assertEquals("Wrong UDF declaration syntax: \"ANY\"! (position in the property " + KEY_UDFS + ": 27-30)", e.getMessage());
		}

		// UDF with no brackets:
@@ -1059,7 +1097,16 @@ public class TestConfigurableServiceConnection {
			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,{className}]\".", e.getMessage());
			assertEquals("Wrong UDF declaration syntax: \"toto(a string)->VARCHAR\"! (position in the property " + KEY_UDFS + ": 1-24)", e.getMessage());
		}

		// UDF with a badly formatted description:
		try{
			new ConfigurableServiceConnection(udfsWithWrongDescriptionFormatProp);
			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: \"[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, Blabla]\"! (position in the property " + KEY_UDFS + ": 1-67)", e.getMessage());
		}

		// UDFs whose one item have more parts than supported:
@@ -1068,7 +1115,7 @@ public class TestConfigurableServiceConnection {
			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 class name) can be given within brackets. (position in the property " + KEY_UDFS + ": 58)", e.getMessage());
			assertEquals("Wrong UDF declaration syntax: \"[toto(a string)->VARCHAR, {adql.db.TestDBChecker$UDFToto}, \"Blabla\", foo]\"! (position in the property " + KEY_UDFS + ": 1-74)", e.getMessage());
		}

		// UDF with missing definition part (or wrong since there is no comma):
@@ -1113,7 +1160,7 @@ public class TestConfigurableServiceConnection {
			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());
			assertEquals("Wrong UDF declaration syntax: \"[toto(a string)->VARCHAR\"! (position in the property " + KEY_UDFS + ": 1-25)", e.getMessage());
		}

		// Valid custom TAPFactory: