package tap.parameters; /* * 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 2014 - Astronomisches Rechen Institut (ARI) */ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import tap.TAPException; import tap.TAPJob; import uws.UWSException; import uws.service.file.UWSFileManager; import uws.service.file.UnsupportedURIProtocolException; import uws.service.request.RequestParser; import uws.service.request.UploadFile; /** * <p>Description of an uploaded content specified using the DALI/TAP syntax.</p> * * <h3>How to access the upload content?</h3> * * <p> * This parameter is either a reference to a distant content and is then specified by a URI, * or a pointer to the stored version of a file submitted inline in a HTTP request. In both cases, * this class lets access the upload content with the function {@link #open()}. * </p> * * <h3>How to get {@link DALIUpload} objects from HTTP request parameters?</h3> * * <p> * The static function {@link #getDALIUploads(Map, boolean, UWSFileManager)} should be used in order to * extract the {@link DALIUpload} items specified in a list of request parameters. * </p> * <p><i>Note: * It is recommended to provide these parameters as a map generated by a {@link RequestParser}. * If not, you should ensure that values of the map associated to the "UPLOAD" parameter(s) are {@link String}s, {@link String}[]s, * {@link DALIUpload}s, {@link DALIUpload}[]s or {@link Object}[] containing {@link String}s and/or {@link DALIUpload}s. * Besides, the request parameters referenced using the syntax "param:{param-name}" must be instances of only {@link UploadFile} * or an array of {@link Object}s containing at least one {@link UploadFile} instance (if several are found, just the last one will be used). * </i></p> * <p> * Calling this function will also modify a little the given list of parameters by rewriting the "UPLOAD" parameter and * removing unreferenced uploaded files (from the list and from the file-system). * </p> * * <h3>Reminder about the "UPLOAD" parameter</h3> * * <p> * The IVOA standards DAL and TAP define both the same special parameter: "UPLOAD" (not case-sensitive). * </p> * * <p> * This parameter lists all upload items. A such item can be either an inline file or a reference to a distant file. * In both cases, it is specified as a URI. The parameter "UPLOAD" sets also a label/name to this item. * The syntax to use for a single item is the following: "{label},{URI}". Several items can be provided, but there is * a slight difference between DALI and TAP in the way to do it. DALI says that multiple uploads MUST be done * by several submit of a single "UPLOAD" parameter with the syntax described above. TAP says that multiple uploads CAN * be done in one "UPLOAD" parameter by separating each item with a semicolon (;). For instance: * </p> * <ul> * <li><b>In TAP:</b> "UPLOAD=tableA,param:foo;tableB,http://..." => only 1 parameter for 2 uploads</li> * <li><b>In DALI:</b> "UPLOAD=tableA,param:foo" and "UPLOAD=tableB,http://..." => 2 parameters, one for each upload</li> * </ul> * * <p><i>Note: * The drawback of the TAP method is: what happens when a URI contains a semicolon? URI can indeed contain a such character * and in this case the parsing becomes more tricky, or even impossible in some cases. In such cases, it is strongly * recommended to either encode the URI (so the ";" becomes "%3B") or to forbid the TAP syntax. This latter can be * done by setting the second parameter of {@link #getDALIUploads(Map, boolean, UWSFileManager)} to <i>false</i>. * </i></p> * * @author Grégory Mantelet (ARI) * @version 2.0 (12/2014) * @since 2.0 * * @see RequestParser */ public class DALIUpload { /** <p>Pointer to the stored version of the file submitted inline in a HTTP request.</p> * <p><i>Note: * If NULL, this {@link DALIUpload} is then a "byReference" upload, meaning that its content is distant * and can be accessed only with the URI {@link #uri}. * </i></p> */ public final UploadFile file; /** <p>URI toward a distant resource.</p> * <p><i>Note: * If NULL, this {@link DALIUpload} corresponds to a file submitted inline in a HTTP request. * Its content has then been stored by this service and can be accessed using the pointer {@link #file}. * </i></p>*/ public final URI uri; /** <p>Name to use in the service to label this upload.</p> * <p><i>Note: * In a TAP service, this label is the name of the table to create in the database * when creating the corresponding table inside it. * </i></p> */ public final String label; /** The file manager to use when a stream will be opened toward the given URI. * It should know how to access it, because the URI can use a URL scheme (http, https, ftp) but also another scheme * unknown by the library (e.g. ivo, vos). */ protected final UWSFileManager fileManager; /** * <p>Build a {@link DALIUpload} whose the content has been submitted inline in an HTTP request.</p> * * <p> * A such upload has been specified by referencing another HTTP request parameter containing an inline file. * The used syntax was then: "{label},param:{param-name}". * </p> * * @param label Label of the DALIUpload (i.e. {label} inside an "UPLOAD" parameter value "{label},{URI}"). * <i>Note: If NULL, the file name will be used as label.</i> * @param file Pointer to the uploaded file. */ public DALIUpload(final String label, final UploadFile file){ if (file == null) throw new NullPointerException("Missing UploadFile! => Can not build a DaliUpload instance."); this.label = (label == null) ? file.paramName : label; this.file = file; this.uri = null; this.fileManager = null; } /** * <p>Build a {@link DALIUpload} whose the content is distant and specified by a URI.</p> * * <p> * A such upload has been specified by referencing a URI (whose the scheme is different from "param"). * The used syntax was then: "{label},{URI}". * </p> * * @param label Label of the DALIUpload (i.e. {label} inside an "UPLOAD" parameter value "{label},{URI}"). <i>Note: If NULL, the URI will be used as label.</i> * @param uri URI toward a distant file. <i><b>The scheme of this URI must be different from "param".</b> This scheme is indeed reserved by the DALI syntax to reference a HTTP request parameter containing an inline file.</i> * @param fileManager The file manager to use when a stream will be opened toward the given URI. This file manager should know how to access it, * because the URI can use a URL scheme (http, https, ftp) but also another scheme unknown by the library (e.g. ivo, vos). */ public DALIUpload(final String label, final URI uri, final UWSFileManager fileManager){ if (uri == null) throw new NullPointerException("Missing URI! => Can not build a DaliUpload instance."); else if (uri.getScheme() != null && uri.getScheme().equalsIgnoreCase("param")) throw new IllegalArgumentException("Wrong URI scheme: \"param\" is reserved to reference a HTTP request parameter! If used, the content of this parameter must be stored in a file, then the parameter must be represented by an UploadFile and integrated into a DALIUpload with the other constructor."); else if (uri.getScheme() != null && uri.getScheme().equalsIgnoreCase("file")) throw new IllegalArgumentException("Wrong URI scheme: \"file\" is forbidden!"); else if (fileManager == null) throw new NullPointerException("Missing File Manager! => Can not build a DaliUpload instance."); this.label = (label == null) ? uri.toString() : label; this.uri = uri; this.file = null; this.fileManager = fileManager; } /** * Tell whether this upload is actually a reference toward a distant resource. * * @return <i>true</i> if this upload is referenced by a URI, * <i>false</i> if the upload has been submitted inline in the HTTP request. */ public boolean isByReference(){ return (file == null); } /** * Open a stream to the content of this upload. * * @return An InputStream. * * @throws UnsupportedURIProtocolException If the URI of this upload item is using a protocol not supported by this service implementation. * @throws IOException If the stream can not be opened. */ public InputStream open() throws UnsupportedURIProtocolException, IOException{ if (file == null) return fileManager.openURI(uri); else return file.open(); } @Override public String toString(){ return label + "," + (file != null ? "param:" + file.paramName : uri.toString()); } /* ****************************** */ /* EXTRACTION OF DALI/TAP UPLOADS */ /* ****************************** */ /** <p>Regular expression of an UPLOAD parameter as defined by DALI (REC-DALI-1.0-20131129).</p> * <p><i>Note: * In DALI, multiple uploads must be done by posting several UPLOAD parameters. * It is not possible to provide directly a list of parameters as in TAP. * However, the advantage of the DALI method is to allow ; in URI (while ; is the * parameter separator in TAP). * </i></p> */ protected static final String DALI_UPLOAD_REGEXP = "[^,]+,\\s*(param:.+|.+)"; /** <p>Regular expression of an UPLOAD parameter as defined by TAP (REC-TAP-1.0).</p> * <p><i>Note: * In TAP, multiple uploads may be done by POSTing only one UPLOAD parameter * whose the value is a list of DALI UPLOAD parameters, separated by a ; * </i></p> */ protected static final String TAP_UPLOAD_REGEXP = DALI_UPLOAD_REGEXP + "(\\s*;\\s*" + DALI_UPLOAD_REGEXP + ")*"; /** * <p>Get all uploads specified in the DALI parameter "UPLOAD" from the given request parameters.</p> * * <p><i>Note: * This function is case INsensitive for the "UPLOAD" parameter. * </i></p> * <p><b>WARNING:</b> * Calling this function modifies the given map ONLY IF the "UPLOAD" parameter (whatever is its case) is found. * In such case, the following modifications are applied: * </p> * <ul> * <li> * All "UPLOAD" parameters will be removed and then added again in the map with their corresponding {@link DALIUpload} item (not any more a String). * </li> * <li> * If <i>allowTAPSyntax</i> is <i>true</i>, several uploads may be specified in the same "UPLOAD" parameter value. * For more clarity for the user (once the parameters listed), this list of uploads will be split in the same number of "UPLOAD" parameters. * That's to say, there will be only one "UPLOAD" item in the Map, but its value will be an array containing every specified uploads: * <i>an array of {@link DALIUpload} objects</i>. * </li> * <li> * If there is at least one "UPLOAD" parameter, all uploaded files (parameters associated with instances of {@link UploadFile}) will be removed * from the map (and also from the file system). They are indeed not useful for a DALI service since all interesting uploads have already been * listed. * </li> * </ul> * * <p><i>Note: * This function can be called several times on the same map. After a first call, this function will just gathers into a List * all found {@link DALIUpload} objects. Of course, only uploads specified in the "UPLOAD" parameter(s) will be returned and others will be removed * as explained above. * </i></p> * * <h3>DALI and TAP syntax</h3> * <p> * The "UPLOAD" parameter lists all files to consider as uploaded. * The syntax for one item is the following: "{name},{uri}", where {uri} is "param:{param-ref}" when the file is provided * inline in the parameter named {param-ref}, otherwise, it can be any valid URI (http:..., ftp:..., vos:..., ivo:..., etc...). * </p> * * <p> * The parameter <i>allowTAPSyntax</i> lets switch between the DALI and TAP syntax. * The only difference between them, is in the way to list multiple uploads. In TAP, they can be given as a semicolon separated * list in a single parameter, whereas in DALI, there must be submitted as several individual parameters. For instance: * </p> * <ul> * <li><b>In TAP:</b> "UPLOAD=tableA,param:foo;tableB,http://..." => only 1 parameter</li> * <li><b>In DALI:</b> "UPLOAD=tableA,param:foo" and "UPLOAD=tableB,http://..." => 2 parameters</li> * </ul> * * <p><i>Note: * Because of the possible presence of a semicolon in a URI (which is also used as separator of uploads in the TAP syntax), * there could be a problem while splitting the uploads specified in "UPLOAD". In that case, it is strongly recommended to * either encode the URI (in UTF-8) (i.e. ";" becomes "%3B") or to merely restrict the syntax to the DALI one. In this last case, * the parameter "allowTAPSyntax" should be set to <i>false</i> and then all parameters should be submitted individually. * </i></p> * * @param requestParams All parameters extracted from an HTTP request by a {@link RequestParser}. * @param allowTAPSyntax <i>true</i> to allow a list of several upload items in one "UPLOAD" parameter value (each item separated by a semicolon), * <i>false</i> to forbid it (and so, multiple upload items shall be submitted individually). * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. * <i>(a link to the file manager will be set in the {@link DALIUpload} object in order to open it * whenever it will asked after its creation)</i> * * @return List of all uploads specified with the DALI or TAP syntax. * * @throws TAPException If the syntax of an "UPLOAD" parameter is wrong. * * @see RequestParser#parse(javax.servlet.http.HttpServletRequest) */ public final static List<DALIUpload> getDALIUploads(final Map<String,Object> requestParams, final boolean allowTAPSyntax, final UWSFileManager fileManager) throws TAPException{ // 1. Get all "UPLOAD" parameters and build/get their corresponding DALIUpload(s): ArrayList<DALIUpload> uploads = new ArrayList<DALIUpload>(3); ArrayList<String> usedFiles = new ArrayList<String>(3); Iterator<Map.Entry<String,Object>> it = requestParams.entrySet().iterator(); Map.Entry<String,Object> entry; Object value; while(it.hasNext()){ entry = it.next(); // If the parameter is an "UPLOAD" one: if (entry.getKey() != null && entry.getKey().toLowerCase().equals(TAPJob.PARAM_UPLOAD)){ // get its value: value = entry.getValue(); if (value != null){ // CASE DALIUpload: just add the upload item inside the list: if (value instanceof DALIUpload){ DALIUpload upl = (DALIUpload)value; uploads.add(upl); if (!upl.isByReference()) usedFiles.add(upl.file.paramName); } // CASE String: it must be parsed and transformed into a DALIUpload item which will be then added inside the list: else if (value instanceof String) fetchDALIUploads(uploads, usedFiles, (String)value, requestParams, allowTAPSyntax, fileManager); // CASE Array: else if (value.getClass().isArray()){ Object[] objects = (Object[])value; for(Object o : objects){ if (o != null){ if (o instanceof DALIUpload) uploads.add((DALIUpload)o); else if (o instanceof String) fetchDALIUploads(uploads, usedFiles, (String)o, requestParams, allowTAPSyntax, fileManager); } } } } // remove this "UPLOAD" parameter ; if it was not NULL, it will be added again in the map but as DALIUpload item(s) after this loop: it.remove(); } } // 2. Remove all other files of the request parameters ONLY IF there was a not-NULL "UPLOAD" parameter: if (uploads.size() > 0){ it = requestParams.entrySet().iterator(); while(it.hasNext()){ entry = it.next(); value = entry.getValue(); if (value == null) it.remove(); else if (value instanceof UploadFile && !usedFiles.contains(entry.getKey())){ try{ ((UploadFile)value).deleteFile(); }catch(IOException ioe){} it.remove(); }else if (value.getClass().isArray()){ Object[] objects = (Object[])value; int cnt = objects.length; for(int i = 0; i < objects.length; i++){ if (objects[i] == null){ objects[i] = null; cnt--; }else if (objects[i] instanceof UploadFile && !usedFiles.contains(entry.getKey())){ try{ ((UploadFile)objects[i]).deleteFile(); }catch(IOException ioe){} objects[i] = null; cnt--; } } if (cnt == 0) it.remove(); } } } // 3. Re-add a new "UPLOAD" parameter gathering all extracted DALI Uploads: if (uploads.size() > 0) requestParams.put("UPLOAD", uploads.toArray(new DALIUpload[uploads.size()])); return uploads; } /** * <p>Fetch all uploads specified in the DALI/TAP "UPLOAD" parameter. * The fetched {@link DALIUpload}s are added in the given {@link ArrayList}.</p> * * <p><i>Note: A DALI upload can be either a URI or an inline file (specified as "param:{param-ref}").</i></p> * * @param uploads List of {@link DALIUpload}s. <b>to update</b>. * @param usedFiles List of the the names of the referenced file parameters. <b>to update</b>. * @param uploadParam Value of the "UPLOAD" parameter. * @param parameters List of all extracted parameters (including {@link UploadFile}(s)). * @param allowTAPSyntax <i>true</i> to allow a list of several upload items in one "UPLOAD" parameter value (each item separated by a semicolon), * <i>false</i> to forbid it (and so, multiple upload items shall be submitted individually). * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. * <i>(a link to the file manager will be set in the {@link DALIUpload} object in order to open it * whenever it will asked after its creation)</i> * * @throws TAPException If the syntax of the given "UPLOAD" parameter is incorrect. */ protected static void fetchDALIUploads(final ArrayList<DALIUpload> uploads, final ArrayList<String> usedFiles, String uploadParam, final Map<String,Object> parameters, final boolean allowTAPSyntax, final UWSFileManager fileManager) throws TAPException{ if (uploadParam == null || uploadParam.trim().length() <= 0) return; // TAP SYNTAX (list of DALI UPLOAD items, separated by a semicolon): if (allowTAPSyntax && uploadParam.matches("([^,]+,.+);([^,]+,.+)")){ Pattern p = Pattern.compile("([^,]+,.+);([^,]+,.+)"); Matcher m = p.matcher(uploadParam); while(m != null && m.matches()){ // Fetch the last UPLOAD item: DALIUpload upl = fetchDALIUpload(m.group(2), parameters, fileManager); uploads.add(upl); if (!upl.isByReference()) usedFiles.add(upl.file.paramName); // Prepare the fetching of the other DALI parameters: if (m.group(1) != null) m = p.matcher(uploadParam = m.group(1)); } } // DALI SYNTAX (only one UPLOAD item): if (uploadParam.matches("[^,]+,.+")){ // Fetch the single UPLOAD item: DALIUpload upl = fetchDALIUpload(uploadParam, parameters, fileManager); uploads.add(upl); if (!upl.isByReference()) usedFiles.add(upl.file.paramName); } // /!\ INCORRECT SYNTAX /!\ else throw new TAPException("Wrong DALI syntax for the parameter UPLOAD \"" + uploadParam + "\"!", UWSException.BAD_REQUEST); } /** * Fetch the single upload item (a pair with the syntax: "{label},{URI}". * * @param uploadParam Value of the "UPLOAD" parameter. <i>A single upload item is expected ; that's to say something like "{label},{URI}".</i> * @param parameters List of extracted parameters. The fetched LOB must be added as a new parameter in this map. <b>MUST not be NULL</b> * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. * <i>(a link to the file manager will be set in the {@link DALIUpload} object in order to open it * whenever it will asked after its creation)</i> * * @return The corresponding {@link DALIUpload} object. * * @throws TAPException If the syntax of the given "UPLOAD" parameter is incorrect. * * @see #parseDALIParam(String) * @see #buildDALIUpload(String, String, Map, UWSFileManager) */ protected static DALIUpload fetchDALIUpload(final String uploadParam, final Map<String,Object> parameters, final UWSFileManager fileManager) throws TAPException{ if (uploadParam.matches("[^,]+,.+")){ // Check and extract the pair parts ([0]=label, [1]=URI): String[] parts = parseDALIParam(uploadParam); // Build the corresponding DALIUpload: return buildDALIUpload(parts[0], parts[1], parameters, fileManager); }else throw new TAPException("Wrong DALI syntax for the parameter UPLOAD \"" + uploadParam + "\"!", UWSException.BAD_REQUEST); } /** * <p>Extract the two parts (label and URI) of the given DALI parameter, and then, check their syntax.</p> * * <p><i><b>Important note:</b> * It MUST be ensured before calling this function that the given DALI parameter is not NULL * and contains at least one comma (,). * </i></p> * * <p> * The first comma found in the given string will be the separator of the two parts * of the given DALI parameter: {label},{URI} * </p> * * <p> * The label part - {label} - must start with one letter and may be followed by a letter, * a digit or an underscore. The corresponding regular expression is: [a-zA-Z][a-zA-Z0-9_]* * </p> * * <p> * The URI part - {URI} - must start with a scheme, followed by a colon (:) and then by several characters * (no restriction). A scheme must start with one letter and may be followed by a letter, * a digit, a plus (+), a dot (.) or an hyphen/minus (-). The corresponding regular expression is: * [a-zA-Z][a-zA-Z0-9\+\.-]* * </p> * * @param definition MUST BE A PAIR label,value * * @return An array of exactly 2 items: [0]=upload label/name, [1]=an URI. <i>(note: the special DALI syntax "param:..." is also a valid URI)</i> * * @throws TAPException If the given upload definition is not following the valid DALI syntax. */ protected static String[] parseDALIParam(final String definition) throws TAPException{ // Locate the separator: int sep = definition.indexOf(','); if (sep <= 0) throw new TAPException("A DALI parameter must be a pair whose the items are separated by a colon!", UWSException.INTERNAL_SERVER_ERROR); // Extract the two parts: {label},{uri} String[] parts = new String[]{definition.substring(0, sep),definition.substring(sep + 1)}; // Check the label: if (!parts[0].matches("[a-zA-Z][a-zA-Z0-9_]*")) throw new TAPException("Wrong uploaded item name syntax: \"" + parts[0] + "\"! An uploaded item must have a label respecting the 'regular_identifier' production of ADQL 2.0 (regular expression: [a-zA-Z][a-zA-Z0-9_]*).", UWSException.BAD_REQUEST); // Check the URI: else if (!parts[1].matches("[a-zA-Z][a-zA-Z0-9\\+\\.\\-]*:.+")) throw new TAPException("Bad URI syntax: \"" + parts[1] + "\"! A URI must start with: \"<scheme>:\", where <scheme>=\"[a-zA-Z][a-zA-Z0-9+.-]*\".", UWSException.BAD_REQUEST); return parts; } /** * <p>Build a {@link DALIUpload} corresponding to the specified URI.</p> * * <p> * If the URI starts, case-insensitively, with "param:", it is then a reference to another request parameter containing a file content. * In this case, the file content has been already stored inside a local file and represented by an {@link UploadFile} instance in the map. * </p> * * <p> * If the URI does not start with "param:", the DALI upload is considered as a reference to a distant file which can be accessed using this URI. * Any URI scheme is allowed here, but the given file manager should be able to interpret it and open a stream toward the referenced resource * whenever it will be asked. * </p> * * <p><i>Note: * If the URI is not a parameter reference (i.e. started by "param:"), it will be decoded using {@link URLDecoder#decode(String, String)} * (character encoding: UTF-8). * </i></p> * * @param label Label of the {@link DALIUpload} to build. * @param uri URI of the LOB. <b>MUST be NOT-NULL</b> * @param parameters All parameters extracted from an HTTP request by a {@link RequestParser}. * @param fileManager The file manager to use in order to build a {@link DALIUpload} objects from a URI. * <i>(a link to the file manager will be set in the {@link DALIUpload} object in order to open it * whenever it will asked after its creation)</i> * * @return The corresponding {@link DALIUpload} object. * * @throws TAPException If the parameter reference is broken or if the given URI has a wrong syntax. */ protected final static DALIUpload buildDALIUpload(final String label, String uri, final Map<String,Object> parameters, final UWSFileManager fileManager) throws TAPException{ // FILE case: if (uri.toLowerCase().startsWith("param:")){ // get the specified parameter name: uri = uri.substring(6); // get the corresponding file: Object obj = parameters.get(uri); /* a map value can be an array of objects in case several parameters have the same name ; * in this case, we just keep the last instance of UploadFile: */ if (obj != null && obj.getClass().isArray()){ Object[] objects = (Object[])obj; obj = null; for(Object o : objects){ if (o != null && o instanceof UploadFile) obj = o; } } // ensure the type of the retrieved parameter is correct: if (obj == null) throw new TAPException("Missing file parameter to upload: \"" + uri + "\"!", UWSException.BAD_REQUEST); else if (!(obj instanceof UploadFile)) throw new TAPException("Incorrect parameter type \"" + uri + "\": a file was expected!", UWSException.BAD_REQUEST); // build the LOB: return new DALIUpload(label, (UploadFile)obj); } // URI case: else{ // extract the URI as it is given: uri = uri.trim(); if (uri.toLowerCase().startsWith("file:")) throw new TAPException("Wrong URI scheme in the upload specification labeled \"" + label + "\": \"file\" is forbidden!", UWSException.BAD_REQUEST); // decode it in case there is any illegal character: try{ uri = URLDecoder.decode(uri, "UTF-8"); }catch(UnsupportedEncodingException uee){} try{ // build the LOB: return new DALIUpload(label, new URI(uri), fileManager); }catch(URISyntaxException e){ throw new TAPException("Incorrect URI syntax: \"" + uri + "\"!", UWSException.BAD_REQUEST); } } } }