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

[ADQL,TAP] Allow to set a simple string as translation pattern for UDFs.

This aims to prevent extending UserDefinedFunction for UDFs whose translation
is very simple (e.g. different name in SQL, different argument order, etc).

Fixes #115
parent 25a783a8
Loading
Loading
Loading
Loading
+441 −192

File changed.

Preview size limit exceeded, changes collapsed.

+93 −46
Original line number Diff line number Diff line
package adql.query.operand.function;

import java.util.regex.Matcher;

/*
 * This file is part of ADQLLibrary.
 *
@@ -16,7 +18,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-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 * Copyright 2012-2020 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

@@ -33,12 +35,13 @@ import adql.translator.TranslationException;
 * It represents any function which is not managed by ADQL.
 *
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 1.4 (06/2015)
 * @version 2.0 (08/2020)
 */
public final class DefaultUDF extends UserDefinedFunction {

	/** Define/Describe this user defined function.
	 * This object gives the return type and the number and type of all parameters. */
	 * This object gives the return type and the number and type of all
	 * parameters. */
	protected FunctionDef definition = null;

	/** Its parsed parameters. */
@@ -70,12 +73,13 @@ public final class DefaultUDF extends UserDefinedFunction {
	public DefaultUDF(final DefaultUDF toCopy) throws Exception {
		functionName = toCopy.functionName;
		parameters = (ADQLList<ADQLOperand>)(toCopy.parameters.getCopy());
		setPosition((toCopy.getPosition() == null) ? null : new TextPosition(toCopy.getPosition()));;
		setPosition((toCopy.getPosition() == null) ? null : new TextPosition(toCopy.getPosition()));
	}

	/**
	 * Get the signature/definition/description of this user defined function.
	 * The returned object provides information on the return type and the number and type of parameters. 
	 * The returned object provides information on the return type and the
	 * number and type of parameters.
	 *
	 * @return	Definition of this function. (MAY be NULL)
	 */
@@ -84,17 +88,21 @@ public final class DefaultUDF extends UserDefinedFunction {
	}

	/**
	 * <p>Let set the signature/definition/description of this user defined function.</p>
	 * Let set the signature/definition/description of this user defined
	 * function.
	 *
	 * <p><i><b>IMPORTANT:</b>
	 * 	No particular checks are done here except on the function name which MUST
	 * 	be the same (case insensitive) as the name of the given definition.
	 * 	No particular checks are done here except on the function name which
	 * 	MUST be the same (case insensitive) as the name of the given definition.
	 * 	Advanced checks must have been done before calling this setter.
	 * </i></p>
	 *
	 * @param def	The definition applying to this parsed UDF, or NULL if none has been found.
	 * @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.
	 * @throws IllegalArgumentException	If the name in the given definition does
	 *                                 	not match the name of this parsed
	 *                                 	function.
	 *
	 * @since 1.3
	 */
@@ -152,7 +160,8 @@ public final class DefaultUDF extends UserDefinedFunction {
	}

	/**
	 * Function to override if you want to check the parameters of this user defined function.
	 * Function to override if you want to check the parameters of this user
	 * defined function.
	 *
	 * @see adql.query.operand.function.ADQLFunction#setParameter(int, adql.query.operand.ADQLOperand)
	 */
@@ -165,6 +174,43 @@ public final class DefaultUDF extends UserDefinedFunction {

	@Override
	public String translate(final ADQLTranslator caller) throws TranslationException {
		// Use the translation pattern if any is specified:
		if (definition != null && definition.getTranslationPattern() != null) {
			StringBuffer sql = new StringBuffer();

			final String escapedDollarRegex = "\\$\\$";
			final String dollarReplacement = Matcher.quoteReplacement("$");
			final String transPattern = definition.getTranslationPattern();

			Matcher m = FunctionDef.argRefPattern.matcher(transPattern);
			int startIndex = 0;
			ADQLOperand arg;

			// Search for all argument references:
			while(m.find()) {
				// append the string before the argument to insert:
				sql.append(transPattern.substring(startIndex, m.start()).replaceAll(escapedDollarRegex, dollarReplacement));
				startIndex = m.end();
				// get the argument:
				arg = getParameter(Integer.parseInt(m.group().substring(1)) - 1);
				// translate and insert the argument:
				sql.append(caller.translate(arg));
			}

			// If no argument found, take the whole string as such
			// (and just replace escaped $):
			if (startIndex == 0)
				sql.append(transPattern.replaceAll(escapedDollarRegex, dollarReplacement));

			// Otherwise, take the whole end of the string as such
			// (and again replace escaped $):
			else if (startIndex < transPattern.length())
				sql.append(transPattern.substring(startIndex).replaceAll(escapedDollarRegex, dollarReplacement));

			return sql.toString();
		}
		// Otherwise, no translation needed (use the ADQL as translation result):
		else {
			StringBuffer sql = new StringBuffer(functionName);
			sql.append('(');
			for(int i = 0; i < parameters.size(); i++) {
@@ -175,5 +221,6 @@ public final class DefaultUDF extends UserDefinedFunction {
			sql.append(')');
			return sql.toString();
		}
	}

}
+196 −184
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-2019 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 * Copyright 2016-2020 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

@@ -138,7 +138,7 @@ import uws.service.log.UWSLog.LogLevel;
 * </p>
 *
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 2.3 (03/2019)
 * @version 2.4 (08/2020)
 * @since 2.0
 */
public final class ConfigurableServiceConnection implements ServiceConnection {
@@ -1198,17 +1198,20 @@ public final class ConfigurableServiceConnection implements ServiceConnection {

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

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

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

	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_UDF = "\\[\\s*(" + REGEXP_SIGNATURE + ")\\s*(,\\s*(" + REGEXP_CLASSPATH + "|" + REGEXP_TRANSLATION + ")?\\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;
	private final int GROUP_CLASSPATH = 6;
	private final int GROUP_TRANSLATION = 7;
	private final int GROUP_DESCRIPTION = 11;
	private final int GROUP_NEXT_UDFs = 14;

	/**
	 * Initialize the list of all known and allowed User Defined Functions.
@@ -1246,6 +1249,7 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
					// Fetch the signature, classpath and description:
					String signature = matcher.group(GROUP_SIGNATURE),
							classpath = matcher.group(GROUP_CLASSPATH),
							translation = matcher.group(GROUP_TRANSLATION),
							description = matcher.group(GROUP_DESCRIPTION);

					// If no signature...
@@ -1264,7 +1268,7 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
						try {
							// resolve the function signature:
							FunctionDef def = FunctionDef.parse(signature);
							// resolve the class name:
							// resolve the class name...
							if (classpath != null) {
								if (isClassName(classpath)) {
									Class<? extends UserDefinedFunction> fctClass = null;
@@ -1281,6 +1285,14 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
								} 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 + ": " + (udfOffset + matcher.start(GROUP_CLASSPATH)) + "-" + (udfOffset + matcher.end(GROUP_CLASSPATH)) + ")");
							}
							// ...or the given translation:
							else if (translation != null) {
								try {
									def.setTranslationPattern(translation);
								} catch(IllegalArgumentException iae) {
									throw new TAPException("Invalid argument reference in the translation pattern for the UDF \"" + def + "\"! Cause: " + iae.getMessage());
								}
							}
							// set the description if any:
							if (description != null)
								def.description = description;
+134 −72
Original line number Diff line number Diff line
@@ -369,4 +369,66 @@ public class TestFunctionDef {
		assertEquals(-1, def0.compareTo(new DefaultUDF("fct0", new ADQLOperand[]{ col })));
	}

	@Test
	public void testSetTranslationPattern() {
		FunctionDef fct = null;
		try {
			fct = FunctionDef.parse("foo(a VARCHAR, b INTEGER) -> VARCHAR");
		} catch(ParseException e) {
			e.printStackTrace();
			fail("Impossible anymore to parse this correct UDF definition! (see console for more details)");
		}

		try {
			// TEST: NULL => OK
			fct.setTranslationPattern(null);

			// TEST: empty string => OK
			fct.setTranslationPattern("");
			fct.setTranslationPattern(" 	 ");

			// TEST: no argument reference => OK
			fct.setTranslationPattern("foobar()");

			// TEST: no REAL argument reference => OK
			fct.setTranslationPattern("foobar('$$1', $$2, $$, $-1, $a, $)");

			// TEST: with valid argument reference => OK
			fct.setTranslationPattern("$1");
			fct.setTranslationPattern("foobar($1)");
			fct.setTranslationPattern("foobar($2)");
			fct.setTranslationPattern("foobar($1, $2)");
		} catch(Exception ex) {
			ex.printStackTrace();
			fail("All of these pattern should be valid! (see console for more details)");
		}

		// TEST: argument reference to 0 => ERROR
		try {
			fct.setTranslationPattern("foobar($0)");
			fail("Should have failed => the argument $0 is forbidden (indices start from 1)");
		} catch(Exception ex) {
			assertEquals(IllegalArgumentException.class, ex.getClass());
			assertEquals("'$0' is not a valid ; an argument reference should be an integer starting from 1.", ex.getMessage());
		}

		// TEST: argument reference > nbArguments => ERROR
		try {
			fct.setTranslationPattern("foobar($3)");
			fail("Should have failed => the argument $3 is forbidden (must be <= nbParams (here, 2))");
		} catch(Exception ex) {
			assertEquals(IllegalArgumentException.class, ex.getClass());
			assertEquals("'$3' is not valid ; the argument index is bigger than the actual number of arguments (" + fct.getNbParams() + ") according to this UDF definition.", ex.getMessage());
		}

		// TEST: invalid index prefixed with 0 => ERROR
		try {
			fct.setTranslationPattern("foobar($01)");
			fail("Should have failed => the argument $01 is parsed as $0 and so, is forbidden");
		} catch(Exception ex) {
			assertEquals(IllegalArgumentException.class, ex.getClass());
			assertEquals("'$0' is not a valid ; an argument reference should be an integer starting from 1.", ex.getMessage());
		}
	}

}
+77 −21
Original line number Diff line number Diff line
@@ -7,10 +7,14 @@ import org.junit.Before;
import org.junit.Test;

import adql.db.DBType;
import adql.db.FunctionDef;
import adql.db.STCS.Region;
import adql.parser.ParseException;
import adql.query.IdentifierField;
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.geometry.AreaFunction;
import adql.query.operand.function.geometry.BoxFunction;
import adql.query.operand.function.geometry.CentroidFunction;
@@ -27,7 +31,8 @@ import adql.query.operand.function.geometry.RegionFunction;
public class TestJDBCTranslator {

	@Before
	public void setUp() throws Exception{}
	public void setUp() throws Exception {
	}

	@Test
	public void testTranslateStringConstant() {
@@ -44,6 +49,57 @@ public class TestJDBCTranslator {
		}
	}

	@Test
	public void testTranslateUserDefinedFunction() {
		JDBCTranslator tr = new AJDBCTranslator();

		DefaultUDF udf = new DefaultUDF("split", new ADQLOperand[]{ new ADQLColumn("values"), new StringConstant(";") });

		// TEST: no FunctionDef, so no translation pattern => just return the ADQL
		try {
			assertEquals("split(values, ';')", tr.translate(udf));
		} catch(TranslationException e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem to translate this UDF as in ADQL.");
		}

		// TEST: a FunctionDef with no translation pattern => just return the ADQL
		try {
			udf.setDefinition(FunctionDef.parse("split(str VARCHAR, sep VARCHAR) -> VARCHAR"));
			assertEquals("split(values, ';')", tr.translate(udf));
		} catch(TranslationException e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem to translate this UDF as in ADQL.");
		} catch(Exception e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem preparing this test.");
		}

		// TEST: a FunctionDef with a translation pattern but no argument reference => the pattern should be returned as such
		try {
			udf.getDefinition().setTranslationPattern("foobar");
			assertEquals("foobar", tr.translate(udf));
		} catch(TranslationException e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem to translate this UDF as in ADQL.");
		} catch(Exception e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem preparing this test.");
		}

		// TEST: a FunctionDef with a translation pattern and with argument reference => the pattern should be applied
		try {
			udf.getDefinition().setTranslationPattern("splitWith($2, $1)");
			assertEquals("splitWith(" + tr.translate(udf.getParameter(1)) + ", " + tr.translate(udf.getParameter(0)) + ")", tr.translate(udf));
		} catch(TranslationException e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem to translate this UDF as in ADQL.");
		} catch(Exception e) {
			e.printStackTrace(System.err);
			fail("There should have been no problem preparing this test.");
		}
	}

	public final static class AJDBCTranslator extends JDBCTranslator {

		@Override
Loading