From f6a089c162a282279c40d79471075aba4b06b5c0 Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Mon, 26 Feb 2018 11:43:12 +0100
Subject: [PATCH] [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.
---
 .../config/ConfigurableServiceConnection.java | 388 ++++++++----------
 src/tap/config/tap_configuration_file.html    |  25 +-
 src/tap/config/tap_full.properties            |  27 +-
 .../TestConfigurableServiceConnection.java    |  93 +++--
 4 files changed, 275 insertions(+), 258 deletions(-)

diff --git a/src/tap/config/ConfigurableServiceConnection.java b/src/tap/config/ConfigurableServiceConnection.java
index 2071290..f624992 100644
--- a/src/tap/config/ConfigurableServiceConnection.java
+++ b/src/tap/config/ConfigurableServiceConnection.java
@@ -2,21 +2,21 @@ package tap.config;
 
 /*
  * This file is part of TAPLibrary.
- * 
+ *
  * TAPLibrary is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published by
  * the Free Software Foundation, either version 3 of the License, or
  * (at your option) any later version.
- * 
+ *
  * TAPLibrary is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU Lesser General Public License for more details.
- * 
+ *
  * 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;
@@ -120,14 +122,14 @@ import uws.service.log.UWSLog.LogLevel;
 
 /**
  * <p>Concrete implementation of {@link ServiceConnection}, fully parameterized with a TAP configuration file.</p>
- * 
+ *
  * <p>
  * 	Every aspects of the TAP service are configured here. This instance is also creating the {@link TAPFactory} using the
  * 	TAP configuration file thanks to the implementation {@link ConfigurableTAPFactory}.
  * </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 {
@@ -206,9 +208,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Create a TAP service description thanks to the given TAP configuration file.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws NullPointerException	If the given properties set is NULL.
 	 * @throws TAPException			If a property is wrong or missing.
 	 */
@@ -218,12 +220,12 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Create a TAP service description thanks to the given TAP configuration file.
-	 * 
+	 *
 	 * @param tapConfig		The content of the TAP configuration file.
 	 * @param webAppRootDir	The directory of the Web Application running this TAP service.
 	 *                     	<em>In this directory another directory may be created in order to store all TAP service files
 	 *                     	if none is specified in the given TAP configuration file.</em>
-	 * 
+	 *
 	 * @throws NullPointerException	If the given properties set is NULL.
 	 * @throws TAPException			If a property is wrong or missing.
 	 */
@@ -279,12 +281,12 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the management of TAP service files using the given TAP configuration file.
-	 * 
+	 *
 	 * @param tapConfig		The content of the TAP configuration file.
 	 * @param webAppRootDir	The directory of the Web Application running this TAP service.
 	 *                     	<em>This directory may be used only to search the root TAP directory
 	 *                     	if specified with a relative path in the TAP configuration file.</em>
-	 * 
+	 *
 	 * @throws TAPException	If a property is wrong or missing, or if an error occurs while creating the file manager.
 	 */
 	private void initFileManager(final Properties tapConfig, final String webAppRootDir) throws TAPException{
@@ -325,16 +327,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Resolve the given file name/path.</p>
-	 * 
+	 *
 	 * <p>
 	 * 	If not an absolute path, the given path may be either relative or absolute. A relative path is always considered
 	 * 	as relative from the Web Application directory (supposed to be given in 2nd parameter).
 	 * </p>
-	 * 
+	 *
 	 * @param filePath			Path/Name of the file to get.
 	 * @param webAppRootPath	Web Application directory local path.
 	 * @param propertyName		Name of the property which gives the given file path.
-	 * 
+	 *
 	 * @return	The specified File instance.
 	 *
 	 * @throws ParseException	If the given file path is a URI/URL.
@@ -354,9 +356,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the TAP logger with the given TAP configuration file.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If no instance of the specified custom logger can
 	 *                     	be created.
 	 */
@@ -394,16 +396,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Initialize the {@link TAPFactory} to use.</p>
-	 * 
+	 *
 	 * <p>
 	 * 	The built factory is either a {@link ConfigurableTAPFactory} instance (by default) or
 	 * 	an instance of the class specified in the TAP configuration file.
 	 * </p>
-	 * 
+	 *
 	 * @param tapConfig		The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If an error occurs while building the specified {@link TAPFactory}.
-	 * 
+	 *
 	 * @see ConfigurableTAPFactory
 	 */
 	private void initFactory(final Properties tapConfig) throws TAPException{
@@ -418,16 +420,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the TAP metadata (i.e. database schemas, tables and columns and their attached metadata).
-	 * 
+	 *
 	 * @param tapConfig		The content of the TAP configuration file.
 	 * @param webAppRootDir	Web Application directory local path.
 	 *                     	<em>This directory may be used if a relative path is given for an XML metadata file.</em>
-	 * 
+	 *
 	 * @return	The extracted TAP metadata.
-	 * 
+	 *
 	 * @throws TAPException	If some TAP configuration file properties are wrong or missing,
 	 *                     	or if an error has occurred while extracting the metadata from the database or the XML file.
-	 * 
+	 *
 	 * @see DBConnection#getTAPSchema()
 	 * @see TableSetParser
 	 */
@@ -592,9 +594,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the maximum number of asynchronous jobs.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration property is wrong.
 	 */
 	private void initMaxAsyncJobs(final Properties tapConfig) throws TAPException{
@@ -610,9 +612,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the default and maximum retention period.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initRetentionPeriod(final Properties tapConfig) throws TAPException{
@@ -642,9 +644,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the default and maximum execution duration.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initExecutionDuration(final Properties tapConfig) throws TAPException{
@@ -674,15 +676,15 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Initialize the list of all output format that the TAP service must support.</p>
-	 * 
+	 *
 	 * <p>
 	 * 	This function ensures that at least one VOTable format is part of the returned list,
 	 * 	even if none has been specified in the TAP configuration file. Indeed, the VOTable format is the only
 	 * 	format required for a TAP service.
 	 * </p>
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void addOutputFormats(final Properties tapConfig) throws TAPException{
@@ -805,13 +807,13 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Parse the given VOTable format specification.</p>
-	 * 
+	 *
 	 * <p>This specification is expected to be an item of the property {@link TAPConfiguration#KEY_OUTPUT_FORMATS}.</p>
-	 * 
+	 *
 	 * @param propValue	A single VOTable format specification.
-	 * 
+	 *
 	 * @return	The corresponding configured {@link VOTableFormat} instance.
-	 * 
+	 *
 	 * @throws TAPException	If the syntax of the given specification is incorrect,
 	 *                     	or if the specified VOTable version or serialization does not exist.
 	 */
@@ -893,9 +895,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the default and maximum output limits.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initOutputLimits(final Properties tapConfig) throws TAPException{
@@ -910,9 +912,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the fetch size for the synchronous and for the asynchronous resources.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initFetchSize(final Properties tapConfig) throws TAPException{
@@ -949,9 +951,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the default and maximum upload limits.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initUploadLimits(final Properties tapConfig) throws TAPException{
@@ -969,9 +971,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the maximum size (in bytes) of a VOTable files set upload.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration property is wrong.
 	 */
 	private void initMaxUploadSize(final Properties tapConfig) throws TAPException{
@@ -993,9 +995,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the TAP user identification method.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration property is wrong.
 	 */
 	private void initUserIdentifier(final Properties tapConfig) throws TAPException{
@@ -1007,9 +1009,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the list of all allowed coordinate systems.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initCoordSys(final Properties tapConfig) throws TAPException{
@@ -1065,9 +1067,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Initialize the list of all allowed ADQL geometrical functions.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initADQLGeometries(final Properties tapConfig) throws TAPException{
@@ -1118,11 +1120,25 @@ 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.
-	 * 
+	 *
 	 * @param tapConfig	The content of the TAP configuration file.
-	 * 
+	 *
 	 * @throws TAPException	If the corresponding TAP configuration properties are wrong.
 	 */
 	private void initUDFs(final Properties tapConfig) throws TAPException{
@@ -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,139 +1160,69 @@ 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());
-
-							// no signature...
-							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] + ")");
-								// ... => ignore this item
-								else
-									continue;
-							}
+			Pattern patternUDFS = Pattern.compile(REGEXP_UDFS);
+			String udfList = propValue;
+			int udfOffset = 1;
+			while(udfList != null){
+				Matcher matcher = patternUDFS.matcher(udfList);
+				if (matcher.matches()){
+
+					// 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 + ": " + (udfOffset + matcher.start(GROUP_SIGNATURE)) + "-" + (udfOffset + matcher.end(GROUP_SIGNATURE)) + ")");
+						// ... => ignore this item
+						else
+							ignoreUdf = true;
+					}
 
-							// add the new UDF in the list:
-							try{
-								// resolve the function signature:
-								FunctionDef def = FunctionDef.parse(signature);
-								// resolve the class name:
-								if (classpath != null){
-									if (isClassName(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 name 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 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] + ")");
-										}
-									}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] + ")");
-								}
-								// 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);
+					if (!ignoreUdf){
+						// Add the new UDF in the list:
+						try{
+							// resolve the function signature:
+							FunctionDef def = FunctionDef.parse(signature);
+							// resolve the class name:
+							if (classpath != null){
+								if (isClassName(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 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 + ": " + (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 + ": " + (udfOffset + matcher.start(GROUP_CLASSPATH)) + "-" + (udfOffset + matcher.end(GROUP_CLASSPATH)) + ")");
 							}
-
-							// 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}]\".");
+							// 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 + ": " + (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) + ")");
+			}
 		}
 	}
 
@@ -1313,16 +1259,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the default retention period.</p>
-	 * 
+	 *
 	 * <p>This period is set by default if the user did not specify one before the execution of his query.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function will apply the given retention period only if legal compared to the currently set maximum value.
 	 * 	In other words, if the given value is less or equals to the current maximum retention period.
 	 * </em></p>
-	 * 
+	 *
 	 * @param period	New default retention period (in seconds).
-	 * 
+	 *
 	 * @return	<i>true</i> if the given retention period has been successfully set, <i>false</i> otherwise.
 	 */
 	public boolean setDefaultRetentionPeriod(final int period){
@@ -1335,15 +1281,15 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the maximum retention period.</p>
-	 * 
+	 *
 	 * <p>This period limits the default retention period and the retention period specified by a user.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function may reduce the default retention period if the current default retention period is bigger
 	 * 	to the new maximum retention period. In a such case, the default retention period is set to the
 	 * 	new maximum retention period.
 	 * </em></p>
-	 * 
+	 *
 	 * @param period	New maximum retention period (in seconds).
 	 */
 	public void setMaxRetentionPeriod(final int period){
@@ -1361,16 +1307,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the default execution duration.</p>
-	 * 
+	 *
 	 * <p>This duration is set by default if the user did not specify one before the execution of his query.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function will apply the given execution duration only if legal compared to the currently set maximum value.
 	 * 	In other words, if the given value is less or equals to the current maximum execution duration.
 	 * </em></p>
-	 * 
+	 *
 	 * @param duration	New default execution duration (in milliseconds).
-	 * 
+	 *
 	 * @return	<i>true</i> if the given execution duration has been successfully set, <i>false</i> otherwise.
 	 */
 	public boolean setDefaultExecutionDuration(final int duration){
@@ -1383,15 +1329,15 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the maximum execution duration.</p>
-	 * 
+	 *
 	 * <p>This duration limits the default execution duration and the execution duration specified by a user.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function may reduce the default execution duration if the current default execution duration is bigger
 	 * 	to the new maximum execution duration. In a such case, the default execution duration is set to the
 	 * 	new maximum execution duration.
 	 * </em></p>
-	 * 
+	 *
 	 * @param duration	New maximum execution duration (in milliseconds).
 	 */
 	public void setMaxExecutionDuration(final int duration){
@@ -1421,12 +1367,12 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Add the given {@link OutputFormat} in the list of output formats supported by the TAP service.</p>
-	 * 
+	 *
 	 * <p><b>Warning:
 	 * 	No verification is done in order to avoid duplicated output formats in the list.
 	 * 	NULL objects are merely ignored silently.
 	 * </b></p>
-	 * 
+	 *
 	 * @param newOutputFormat	New output format.
 	 */
 	public void addOutputFormat(final OutputFormat newOutputFormat){
@@ -1436,9 +1382,9 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Remove the specified output format.
-	 * 
+	 *
 	 * @param mimeOrAlias	Full or short MIME type of the output format to remove.
-	 * 
+	 *
 	 * @return	<i>true</i> if the specified format has been found and successfully removed from the list,
 	 *        	<i>false</i> otherwise.
 	 */
@@ -1457,16 +1403,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the default output limit.</p>
-	 * 
+	 *
 	 * <p>This limit is set by default if the user did not specify one before the execution of his query.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function will apply the given output limit only if legal compared to the currently set maximum value.
 	 * 	In other words, if the given value is less or equals to the current maximum output limit.
 	 * </em></p>
-	 * 
+	 *
 	 * @param limit	New default output limit (in number of rows).
-	 * 
+	 *
 	 * @return	<i>true</i> if the given output limit has been successfully set, <i>false</i> otherwise.
 	 */
 	public boolean setDefaultOutputLimit(final int limit){
@@ -1479,15 +1425,15 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the maximum output limit.</p>
-	 * 
+	 *
 	 * <p>This output limit limits the default output limit and the output limit specified by a user.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function may reduce the default output limit if the current default output limit is bigger
 	 * 	to the new maximum output limit. In a such case, the default output limit is set to the
 	 * 	new maximum output limit.
 	 * </em></p>
-	 * 
+	 *
 	 * @param limit	New maximum output limit (in number of rows).
 	 */
 	public void setMaxOutputLimit(final int limit){
@@ -1544,7 +1490,7 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * Set the unit of the upload limit.
-	 * 
+	 *
 	 * @param type	Unit of upload limit (rows or bytes).
 	 */
 	public void setUploadLimitType(final LimitUnit type){
@@ -1554,14 +1500,14 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the default upload limit.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function will apply the given upload limit only if legal compared to the currently set maximum value.
 	 * 	In other words, if the given value is less or equals to the current maximum upload limit.
 	 * </em></p>
-	 * 
+	 *
 	 * @param limit	New default upload limit.
-	 * 
+	 *
 	 * @return	<i>true</i> if the given upload limit has been successfully set, <i>false</i> otherwise.
 	 */
 	public boolean setDefaultUploadLimit(final int limit){
@@ -1576,15 +1522,15 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the maximum upload limit.</p>
-	 * 
+	 *
 	 * <p>This upload limit limits the default upload limit.</p>
-	 * 
+	 *
 	 * <p><em><b>Important note:</b>
 	 * 	This function may reduce the default upload limit if the current default upload limit is bigger
 	 * 	to the new maximum upload limit. In a such case, the default upload limit is set to the
 	 * 	new maximum upload limit.
 	 * </em></p>
-	 * 
+	 *
 	 * @param limit	New maximum upload limit.
 	 */
 	public void setMaxUploadLimit(final int limit){
@@ -1604,16 +1550,16 @@ public final class ConfigurableServiceConnection implements ServiceConnection {
 
 	/**
 	 * <p>Set the maximum size of a VOTable files set that can be uploaded in once.</p>
-	 * 
+	 *
 	 * <p><b>Warning:
 	 * 	This size can not be negative or 0. If the given value is in this case, nothing will be done
 	 * 	and <i>false</i> will be returned.
 	 * 	On the contrary to the other limits, no "unlimited" limit is possible here ; only the
 	 * 	maximum value can be set (i.e. maximum positive integer value).
 	 * </b></p>
-	 * 
+	 *
 	 * @param maxSize	New maximum size (in bytes).
-	 * 
+	 *
 	 * @return	<i>true</i> if the size has been successfully set, <i>false</i> otherwise.
 	 */
 	public boolean setMaxUploadSize(final int maxSize){
diff --git a/src/tap/config/tap_configuration_file.html b/src/tap/config/tap_configuration_file.html
index 122097d..2345960 100644
--- a/src/tap/config/tap_configuration_file.html
+++ b/src/tap/config/tap_configuration_file.html
@@ -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>
diff --git a/src/tap/config/tap_full.properties b/src/tap/config/tap_full.properties
index 0fbf1de..dce6ffc 100644
--- a/src/tap/config/tap_full.properties
+++ b/src/tap/config/tap_full.properties
@@ -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
diff --git a/test/tap/config/TestConfigurableServiceConnection.java b/test/tap/config/TestConfigurableServiceConnection.java
index e0e3be4..45a2390 100644
--- a/test/tap/config/TestConfigurableServiceConnection.java
+++ b/test/tap/config/TestConfigurableServiceConnection.java
@@ -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");
@@ -348,18 +359,18 @@ public class TestConfigurableServiceConnection {
 	 * CONSTRUCTOR TESTS
 	 *  * In general:
 	 * 		- A valid configuration file builds successfully a fully functional ServiceConnection object.
-	 * 
+	 *
 	 * 	* Over the file manager:
 	 * 		- If no TAPFileManager is provided, an exception must be thrown.
 	 * 		- If a class name toward a valid TAPFileManager is provided, a functional DefaultServiceConnection must be successfully built.
 	 * 		- An incorrect file manager value in the configuration file must generate an exception.
-	 * 
+	 *
 	 *  * Over the output format:
 	 *  	- If a SV format is badly expressed (test with "sv" and "sv()"), an exception must be thrown.
 	 *  	- If an unknown output format is provided an exception must be thrown.
-	 * 
+	 *
 	 * Note: the good configuration of the TAPFactory built by the DefaultServiceConnection is tested in {@link TestConfigurableTAPFactory}.
-	 * 
+	 *
 	 * @see ConfigurableServiceConnection#DefaultServiceConnection(Properties)
 	 */
 	@Test
@@ -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:
@@ -1198,7 +1245,7 @@ public class TestConfigurableServiceConnection {
 
 	/**
 	 * A UWSFileManager to test the load of a UWSFileManager from the configuration file with a class path.
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 01/2015
 	 * @see TestConfigurableServiceConnection#testDefaultServiceConnectionProperties()
@@ -1212,7 +1259,7 @@ public class TestConfigurableServiceConnection {
 	/**
 	 * A UserIdentifier which always return the same user...that's to say, all users are in a way still anonymous :-)
 	 * This class is only for test purpose.
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 02/2015
 	 */
@@ -1235,7 +1282,7 @@ public class TestConfigurableServiceConnection {
 
 	/**
 	 * TAPFactory just to test whether the property tap_factory works well.
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 03/2017
 	 */
@@ -1268,7 +1315,7 @@ public class TestConfigurableServiceConnection {
 	/**
 	 * ConfigurableTAPFactory just to test whether the property tap_factory allows TAPFactory
 	 * with a constructor (ServiceConnection, Properties).
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 03/2017
 	 */
@@ -1300,7 +1347,7 @@ public class TestConfigurableServiceConnection {
 
 	/**
 	 * TAPFactory just to test whether the property tap_factory is rejected when no constructor with a single parameter of type ServiceConnection exists.
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 02/2015
 	 */
@@ -1326,7 +1373,7 @@ public class TestConfigurableServiceConnection {
 	/**
 	 * TAPMetadata extension just to test whether it is possible to customize the output class of ConfigurableServiceConnection with the
 	 * metadata fetching methods "db" and "xml".
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 08/2015
 	 */
@@ -1341,9 +1388,9 @@ public class TestConfigurableServiceConnection {
 	/**
 	 * TAPMetadata extension just to test whether it is possible to customize the output class of ConfigurableServiceConnection with the
 	 * metadata fetching methods "db" and "xml".
-	 * 
+	 *
 	 * <strong>This extension is however bad because it does not have any of the required constructor.</strong>
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 08/2015
 	 */
@@ -1355,12 +1402,12 @@ public class TestConfigurableServiceConnection {
 
 	/**
 	 * Custom TAPLog implementation.
-	 * 
+	 *
 	 * <p><i>
 	 * 	Actually, for quick implementation, this class just extends
 	 * 	{@link DefaultTAPLog} (and so, implements TAPLog).
 	 * </i></p>
-	 * 
+	 *
 	 * @author Gr&eacute;gory Mantelet (ARI)
 	 * @version 09/2017
 	 */
-- 
GitLab