Skip to content

Commit f3aafa7

Browse files
committed
rest: Report warnings when parsing invalid queries
Fix the structure of error in a WellComposedErrorBody to comply with the Google JSON style-guide. Add location and locationType to help identify what the error (or warning) is relevant to.
1 parent b00dd55 commit f3aafa7

File tree

15 files changed

+232
-160
lines changed

15 files changed

+232
-160
lines changed

gemma-core/src/main/java/ubic/gemma/core/search/lucene/LuceneQueryUtils.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.HashSet;
2525
import java.util.LinkedHashSet;
2626
import java.util.Set;
27+
import java.util.function.Consumer;
2728
import java.util.regex.Pattern;
2829

2930
import static java.util.Objects.requireNonNull;
@@ -68,19 +69,27 @@ private static QueryParser createQueryParser() {
6869
* escaped if necessary.
6970
*/
7071
public static Query parseSafely( SearchSettings settings, QueryParser queryParser ) throws SearchException {
71-
return parseSafely( settings.getQuery(), queryParser );
72+
return parseSafely( settings.getQuery(), queryParser, settings.getIssueReporter() );
7273
}
7374

7475
/**
7576
* Safely parse the given search query into a Lucene query, falling back on a query with special characters
7677
* escaped if necessary.
78+
* @param report a consumer for potential {@link ParseException} when attempting to parse the query, ignored if null
7779
*/
78-
public static Query parseSafely( String query, QueryParser queryParser ) throws SearchException {
80+
public static Query parseSafely( String query, QueryParser queryParser, @Nullable Consumer<? super ParseException> report ) throws SearchException {
7981
try {
8082
return queryParser.parse( query );
8183
} catch ( ParseException e ) {
8284
String strippedQuery = escape( query );
83-
log.debug( String.format( "Failed to parse '%s': %s.", query, ExceptionUtils.getRootCauseMessage( e ) ), e );
85+
String m = String.format( "Failed to parse '%s': %s, it will be reattempted stripped from Lucene special characters as '%s'.",
86+
query, ExceptionUtils.getRootCauseMessage( e ), strippedQuery );
87+
if ( report != null ) {
88+
log.debug( m, e ); // no need to warn if it is consumed
89+
report.accept( e );
90+
} else {
91+
log.warn( m, e );
92+
}
8493
try {
8594
return queryParser.parse( strippedQuery );
8695
} catch ( ParseException e2 ) {
@@ -241,7 +250,7 @@ public static String prepareDatabaseQuery( SearchSettings settings, boolean allo
241250

242251
@Nullable
243252
public static String prepareDatabaseQuery( String query, boolean allowWildcards ) throws SearchException {
244-
return prepareDatabaseQueryInternal( parseSafely( query, createQueryParser() ), allowWildcards );
253+
return prepareDatabaseQueryInternal( parseSafely( query, createQueryParser(), null ), allowWildcards );
245254
}
246255

247256
@Nullable
@@ -274,7 +283,7 @@ private static String prepareDatabaseQueryInternal( Query query, boolean allowWi
274283

275284
@Nullable
276285
public static URI prepareTermUriQuery( String s ) throws SearchException {
277-
Query query = parseSafely( s, createQueryParser() );
286+
Query query = parseSafely( s, createQueryParser(), null );
278287
if ( query instanceof TermQuery ) {
279288
Term term = ( ( TermQuery ) query ).getTerm();
280289
return tryParseUri( term );

gemma-core/src/main/java/ubic/gemma/model/common/search/SearchSettings.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.io.Serializable;
4040
import java.util.Map;
4141
import java.util.Set;
42+
import java.util.function.Consumer;
4243
import java.util.stream.Collectors;
4344

4445
/**
@@ -207,6 +208,14 @@ public static SearchSettings geneSearch( String query, @Nullable Taxon taxon ) {
207208
@Nullable
208209
private transient Highlighter highlighter;
209210

211+
/**
212+
* A consumer that accepts various query issues.
213+
* <p>
214+
* This is meant to report non-critical issues.
215+
*/
216+
@Nullable
217+
private transient Consumer<Throwable> issueReporter;
218+
210219
/**
211220
* Check if this is configured to search a given result type.
212221
*/

gemma-rest/src/main/java/ubic/gemma/rest/DatasetsWebService.java

Lines changed: 47 additions & 43 deletions
Large diffs are not rendered by default.

gemma-rest/src/main/java/ubic/gemma/rest/providers/AbstractExceptionMapper.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
import ubic.gemma.core.util.BuildInfo;
99
import ubic.gemma.rest.util.BuildInfoValueObject;
1010
import ubic.gemma.rest.util.ResponseErrorObject;
11+
import ubic.gemma.rest.util.WellComposedError;
1112
import ubic.gemma.rest.util.WellComposedErrorBody;
1213

1314
import javax.ws.rs.container.ResourceContext;
1415
import javax.ws.rs.core.Context;
1516
import javax.ws.rs.core.MediaType;
1617
import javax.ws.rs.core.Response;
1718
import javax.ws.rs.ext.ExceptionMapper;
19+
import java.util.List;
1820

1921
public abstract class AbstractExceptionMapper<E extends Throwable> implements ExceptionMapper<E> {
2022

@@ -43,7 +45,7 @@ protected AbstractExceptionMapper( String hostUrl, OpenAPI spec, BuildInfo build
4345
*/
4446
protected WellComposedErrorBody getWellComposedErrorBody( E exception ) {
4547
// for security reasons, we don't include the error object in the response entity
46-
return new WellComposedErrorBody( getStatus( exception ), exception.getMessage() );
48+
return new WellComposedErrorBody( getStatus( exception ).getStatusCode(), exception.getMessage() );
4749
}
4850

4951
/**
@@ -91,23 +93,28 @@ public final Response toResponse( E exception ) {
9193
version = null;
9294
}
9395
Response.ResponseBuilder responseBuilder = getResponseBuilder( request, exception );
96+
WellComposedErrorBody body = getWellComposedErrorBody( exception );
9497
if ( request == null || acceptsJson( request ) ) {
9598
return responseBuilder
9699
.type( MediaType.APPLICATION_JSON_TYPE )
97-
.entity( new ResponseErrorObject( getWellComposedErrorBody( exception ), version, new BuildInfoValueObject( buildInfo ) ) )
100+
.entity( new ResponseErrorObject( version, new BuildInfoValueObject( buildInfo ), body ) )
98101
.build();
99102
} else {
100-
WellComposedErrorBody body = getWellComposedErrorBody( exception );
101103
StringBuilder builder = new StringBuilder();
102104
builder.append( "Request method: " ).append( requestMethod != null ? requestMethod : "?" ).append( '\n' );
103105
builder.append( "Request URI: " ).append( requestUri != null ? requestUri : "?" ).append( '\n' );
104106
builder.append( "Version: " ).append( version != null ? version : "?" ).append( '\n' );
105107
builder.append( "Build info: " ).append( buildInfo ).append( '\n' );
106108
builder.append( "Message: " ).append( body.getMessage() );
107109
if ( body.getErrors() != null ) {
108-
body.getErrors().forEach( ( k, v ) -> {
109-
builder.append( '\n' ).append( k ).append( ": " ).append( v );
110-
} );
110+
List<WellComposedError> errors = body.getErrors();
111+
for ( int i = 0; i < errors.size(); i++ ) {
112+
WellComposedError e = errors.get( i );
113+
builder.append( '\n' ).append( '\n' )
114+
.append( "Error #" ).append( i + 1 ).append( '\n' )
115+
.append( "Reason: " ).append( e.getReason() ).append( '\n' )
116+
.append( "Message: " ).append( e.getMessage() );
117+
}
111118
}
112119
return responseBuilder
113120
.type( MediaType.TEXT_PLAIN_TYPE )

gemma-rest/src/main/java/ubic/gemma/rest/providers/MalformedArgExceptionMapper.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package ubic.gemma.rest.providers;
22

33
import io.swagger.v3.oas.models.OpenAPI;
4+
import org.apache.commons.lang3.exception.ExceptionUtils;
45
import org.springframework.beans.factory.annotation.Autowired;
56
import org.springframework.beans.factory.annotation.Value;
67
import org.springframework.stereotype.Component;
78
import ubic.gemma.core.util.BuildInfo;
9+
import ubic.gemma.persistence.util.EntityUtils;
10+
import ubic.gemma.rest.util.LocationType;
811
import ubic.gemma.rest.util.MalformedArgException;
912
import ubic.gemma.rest.util.WellComposedErrorBody;
1013

@@ -27,9 +30,10 @@ protected Response.Status getStatus( MalformedArgException exception ) {
2730

2831
@Override
2932
protected WellComposedErrorBody getWellComposedErrorBody( MalformedArgException exception ) {
30-
WellComposedErrorBody body = new WellComposedErrorBody( Response.Status.BAD_REQUEST, exception.getMessage() );
33+
WellComposedErrorBody body = new WellComposedErrorBody( Response.Status.BAD_REQUEST.getStatusCode(), exception.getMessage() );
3134
if ( exception.getCause() != null ) {
32-
WellComposedErrorBody.addExceptionFields( body, exception.getCause() );
35+
// TODO: report the exact request parameter that caused this
36+
body.addError( ExceptionUtils.getRootCause( exception ), null, null );
3337
}
3438
return body;
3539
}

gemma-rest/src/main/java/ubic/gemma/rest/providers/NotFoundExceptionMapper.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ubic.gemma.rest.providers;
22

33
import io.swagger.v3.oas.models.OpenAPI;
4+
import org.apache.commons.lang3.exception.ExceptionUtils;
45
import org.springframework.beans.factory.annotation.Autowired;
56
import org.springframework.beans.factory.annotation.Value;
67
import org.springframework.stereotype.Component;
@@ -37,9 +38,9 @@ protected Response.Status getStatus( NotFoundException exception ) {
3738

3839
@Override
3940
protected WellComposedErrorBody getWellComposedErrorBody( NotFoundException exception ) {
40-
WellComposedErrorBody errorBody = new WellComposedErrorBody( Response.Status.NOT_FOUND, exception.getMessage() );
41+
WellComposedErrorBody errorBody = new WellComposedErrorBody( Response.Status.NOT_FOUND.getStatusCode(), exception.getMessage() );
4142
if ( exception.getCause() != null ) {
42-
WellComposedErrorBody.addExceptionFields( errorBody, exception.getCause() );
43+
errorBody.addError( ExceptionUtils.getRootCause( exception ), null, null );
4344
}
4445
return errorBody;
4546
}

gemma-rest/src/main/java/ubic/gemma/rest/security/RestAuthEntryPoint.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ public void commence( final HttpServletRequest request, final HttpServletRespons
6666
realm = "Gemma RESTful API";
6767
version = null;
6868
}
69-
WellComposedErrorBody errorBody = new WellComposedErrorBody( Response.Status.UNAUTHORIZED, MESSAGE_401 );
70-
ResponseErrorObject errorObject = new ResponseErrorObject( errorBody, version, new BuildInfoValueObject( buildInfo ) );
69+
WellComposedErrorBody errorBody = new WellComposedErrorBody( Response.Status.UNAUTHORIZED.getStatusCode(), MESSAGE_401 );
70+
ResponseErrorObject errorObject = new ResponseErrorObject( version, new BuildInfoValueObject( buildInfo ), errorBody );
7171
response.setContentType( MediaType.APPLICATION_JSON );
7272
// using 'xBasic' instead of 'basic' to prevent default browser login popup
7373
response.addHeader( "WWW-Authenticate", "xBasic realm=" + realm );
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package ubic.gemma.rest.util;
2+
3+
public enum LocationType {
4+
HEADERS,
5+
QUERY,
6+
BODY
7+
}

gemma-rest/src/main/java/ubic/gemma/rest/util/ResponseDataObject.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@
1414
*/
1515
package ubic.gemma.rest.util;
1616

17+
import com.fasterxml.jackson.annotation.JsonInclude;
1718
import lombok.Getter;
1819

20+
import java.util.ArrayList;
21+
import java.util.LinkedHashSet;
22+
import java.util.List;
23+
1924
/**
2025
* Wrapper for a non-error response payload compliant with the
21-
* <a href="https://google.github.io/styleguide/jsoncstyleguide.xml?showone=error#error">Google JSON style-guide</a>
26+
* <a href="https://google.github.io/styleguide/jsoncstyleguide.xml?#data">Google JSON style-guide</a>
2227
*
2328
* @author tesarst
2429
*/
@@ -27,10 +32,31 @@ public class ResponseDataObject<T> {
2732

2833
private final T data;
2934

35+
/**
36+
* A list of warnings applicable to the request.
37+
* <p>
38+
* This is an extension to the Google JSON style-guide.
39+
*/
40+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
41+
private final List<WellComposedWarning> warnings = new ArrayList<>();
42+
3043
/**
3144
* @param payload the data to be serialised and returned as the response payload.
3245
*/
3346
public ResponseDataObject( T payload ) {
3447
this.data = payload;
3548
}
49+
50+
/**
51+
* Add a bunch of warnings to {@link #getWarnings()}.
52+
*/
53+
public <S extends ResponseDataObject<T>> S addWarnings( Iterable<Throwable> throwables, String location, LocationType locationType ) {
54+
LinkedHashSet<WellComposedWarning> distinctWarnings = new LinkedHashSet<>();
55+
for ( Throwable throwable : throwables ) {
56+
distinctWarnings.add( new WellComposedWarning( throwable.getClass().getName(), throwable.getMessage(), location, locationType ) );
57+
}
58+
warnings.addAll( distinctWarnings );
59+
//noinspection unchecked
60+
return ( S ) this;
61+
}
3662
}

gemma-rest/src/main/java/ubic/gemma/rest/util/ResponseErrorObject.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,27 @@
1717
import lombok.Value;
1818

1919
/**
20-
* Wrapper for an error response payload compliant with the
21-
* <a href="https://google.github.io/styleguide/jsoncstyleguide.xml?showone=error#error">Google JSON style-guide</a>
20+
* Wrapper for an error response payload compliant with the <a href="https://google.github.io/styleguide/jsoncstyleguide.xml#error">Google JSON style-guide</a>.
2221
*
2322
* @author tesarst
2423
*/
2524
@Value
2625
public class ResponseErrorObject {
2726

28-
WellComposedErrorBody error;
27+
/**
28+
* API version.
29+
*/
2930
String apiVersion;
31+
32+
/**
33+
* Build information.
34+
* <p>
35+
* This is an extension to the Google JSON style-guide.
36+
*/
3037
BuildInfoValueObject buildInfo;
38+
39+
/**
40+
* Error beign reported.
41+
*/
42+
WellComposedErrorBody error;
3143
}

0 commit comments

Comments
 (0)