diff --git a/src/main/java/cz/cvut/kbss/termit/exception/InvalidIdentifierException.java b/src/main/java/cz/cvut/kbss/termit/exception/InvalidIdentifierException.java index 2ef873889..2ba55c265 100644 --- a/src/main/java/cz/cvut/kbss/termit/exception/InvalidIdentifierException.java +++ b/src/main/java/cz/cvut/kbss/termit/exception/InvalidIdentifierException.java @@ -5,4 +5,8 @@ public class InvalidIdentifierException extends TermItException { public InvalidIdentifierException(String message, String messageId) { super(message, messageId); } + + public InvalidIdentifierException(String message, Throwable cause, String messageId) { + super(message, cause, messageId); + } } diff --git a/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java b/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java index 1b9d689ff..0ea71c47c 100644 --- a/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java +++ b/src/main/java/cz/cvut/kbss/termit/rest/handler/RestExceptionHandler.java @@ -40,6 +40,7 @@ import cz.cvut.kbss.termit.exception.WebServiceIntegrationException; import cz.cvut.kbss.termit.exception.importing.UnsupportedImportMediaTypeException; import cz.cvut.kbss.termit.exception.importing.VocabularyImportException; +import cz.cvut.kbss.termit.util.ExceptionUtils; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +53,10 @@ import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.multipart.MaxUploadSizeExceededException; -import static cz.cvut.kbss.termit.util.ExceptionUtils.isCausedBy; +import java.net.URISyntaxException; +import java.util.Optional; + +import static cz.cvut.kbss.termit.util.ExceptionUtils.findCause; /** * Exception handlers for REST controllers. @@ -85,7 +89,7 @@ private static void logException(Throwable ex, HttpServletRequest request) { private static void logException(String message, Throwable ex) { // Prevents exceptions caused by broken connection with a client from logging - if (!isCausedBy(ex, AsyncRequestNotUsableException.class)) { + if (findCause(ex, AsyncRequestNotUsableException.class).isEmpty()) { LOG.error(message, ex); } } @@ -175,6 +179,10 @@ public ResponseEntity termItException(HttpServletRequest request, Ter @ExceptionHandler(JsonLdException.class) public ResponseEntity jsonLdException(HttpServletRequest request, JsonLdException e) { logException(e, request); + Optional uriSyntaxException = ExceptionUtils.findCause(e, URISyntaxException.class); + if (uriSyntaxException.isPresent()) { + return uriSyntaxException(request, uriSyntaxException.get()); + } return new ResponseEntity<>( ErrorInfo.createWithMessage("Error when processing JSON-LD.", request.getRequestURI()), HttpStatus.INTERNAL_SERVER_ERROR); @@ -268,4 +276,18 @@ public ResponseEntity invalidIdentifierException(HttpServletRequest r logException(e, request); return new ResponseEntity<>(errorInfo(request, e), HttpStatus.CONFLICT); } + + @ExceptionHandler + public ResponseEntity uriSyntaxException(HttpServletRequest request, URISyntaxException e) { + logException(e, request); + // when the index is less than zero, its unknown, and we will use more general message + final String messageId = e.getIndex() < 0 ? "error.invalidIdentifier" : "error.invalidUriCharacter"; + TermItException exception = new InvalidIdentifierException(e.getMessage(), e, messageId) + .addParameter("uri", e.getInput()) + .addParameter("reason", e.getReason()) + .addParameter("message", e.getMessage()) + .addParameter("index", Integer.toString(e.getIndex())) + .addParameter("char", Character.toString(e.getInput().charAt(e.getIndex()))); + return new ResponseEntity<>(errorInfo(request, exception), HttpStatus.CONFLICT); + } } diff --git a/src/main/java/cz/cvut/kbss/termit/service/IdentifierResolver.java b/src/main/java/cz/cvut/kbss/termit/service/IdentifierResolver.java index fda72edb0..8da277b76 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/IdentifierResolver.java +++ b/src/main/java/cz/cvut/kbss/termit/service/IdentifierResolver.java @@ -152,11 +152,17 @@ public URI generateIdentifier(String namespace, String... components) { } catch (IllegalArgumentException e) { throw new InvalidIdentifierException( "Generated identifier " + namespace + localPart + " is not a valid URI.", + e, "error.identifier.invalidCharacters"); } } - private static boolean isUri(String value) { + /** + * @param value the URI to check + * @return {@code true} when the URI is prefixed with protocol ({@code http/s, ftp or file} + * and it is a valid {@link URI}. {@code false} otherwise + */ + public static boolean isUri(String value) { try { if (!value.matches("^(https?|ftp|file)://.+")) { return false; diff --git a/src/main/java/cz/cvut/kbss/termit/service/repository/BaseRepositoryService.java b/src/main/java/cz/cvut/kbss/termit/service/repository/BaseRepositoryService.java index 413235e4f..b55061c83 100644 --- a/src/main/java/cz/cvut/kbss/termit/service/repository/BaseRepositoryService.java +++ b/src/main/java/cz/cvut/kbss/termit/service/repository/BaseRepositoryService.java @@ -19,10 +19,12 @@ import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.TypeResolver; +import cz.cvut.kbss.termit.exception.InvalidIdentifierException; import cz.cvut.kbss.termit.exception.NotFoundException; import cz.cvut.kbss.termit.exception.ValidationException; import cz.cvut.kbss.termit.model.util.HasIdentifier; import cz.cvut.kbss.termit.persistence.dao.GenericDao; +import cz.cvut.kbss.termit.service.IdentifierResolver; import cz.cvut.kbss.termit.validation.ValidationResult; import jakarta.annotation.Nonnull; import jakarta.validation.Validator; @@ -173,6 +175,7 @@ public void persist(@Nonnull T instance) { */ protected void prePersist(@Nonnull T instance) { validate(instance); + validateUri(instance.getUri()); } /** @@ -208,6 +211,7 @@ public T update(T instance) { * @param instance The instance to be updated, not {@code null} */ protected void preUpdate(@Nonnull T instance) { + validateUri(instance.getUri()); if (!exists(instance.getUri())) { throw NotFoundException.create(instance.getClass().getSimpleName(), instance.getUri()); } @@ -282,4 +286,17 @@ protected void validate(T instance) { throw new ValidationException(validationResult); } } + + /** + * Validates the specified uri. + * + * @param uri the uri to validate + * @throws cz.cvut.kbss.termit.exception.InvalidIdentifierException when the URI is invalid + * @see cz.cvut.kbss.termit.service.IdentifierResolver#isUri(String) + */ + protected void validateUri(URI uri) throws InvalidIdentifierException { + if (uri != null && !IdentifierResolver.isUri(uri.toString())) { + throw new InvalidIdentifierException("Invalid URI: '" + uri + "'", "error.invalidIdentifier").addParameter("uri", uri.toString()); + } + } } diff --git a/src/main/java/cz/cvut/kbss/termit/util/ExceptionUtils.java b/src/main/java/cz/cvut/kbss/termit/util/ExceptionUtils.java index 4f837d609..fbac2ccba 100644 --- a/src/main/java/cz/cvut/kbss/termit/util/ExceptionUtils.java +++ b/src/main/java/cz/cvut/kbss/termit/util/ExceptionUtils.java @@ -3,6 +3,7 @@ import jakarta.annotation.Nonnull; import java.util.HashSet; +import java.util.Optional; import java.util.Set; public class ExceptionUtils { @@ -11,21 +12,22 @@ private ExceptionUtils() { } /** - * Resolves all nested causes of the {@code throwable} and returns true if any is matching the {@code cause} + * Resolves all nested causes of the {@code throwable} + * @return any cause of the {@code throwable} matching the {@code cause} class, or empty when not found */ - public static boolean isCausedBy(final Throwable throwable, @Nonnull final Class cause) { + public static Optional findCause(final Throwable throwable, @Nonnull final Class cause) { Throwable t = throwable; final Set visited = new HashSet<>(); while (t != null) { if(visited.add(t)) { if (cause.isInstance(t)){ - return true; + return Optional.of((T) t); } t = t.getCause(); continue; } break; } - return false; + return Optional.empty(); } } diff --git a/src/main/java/cz/cvut/kbss/termit/websocket/handler/WebSocketExceptionHandler.java b/src/main/java/cz/cvut/kbss/termit/websocket/handler/WebSocketExceptionHandler.java index a65d61a48..c5869701b 100644 --- a/src/main/java/cz/cvut/kbss/termit/websocket/handler/WebSocketExceptionHandler.java +++ b/src/main/java/cz/cvut/kbss/termit/websocket/handler/WebSocketExceptionHandler.java @@ -38,7 +38,9 @@ import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.multipart.MaxUploadSizeExceededException; -import static cz.cvut.kbss.termit.util.ExceptionUtils.isCausedBy; +import java.net.URISyntaxException; + +import static cz.cvut.kbss.termit.util.ExceptionUtils.findCause; /** * @implSpec Should reflect {@link cz.cvut.kbss.termit.rest.handler.RestExceptionHandler} @@ -74,8 +76,8 @@ private static void logException(Throwable ex, Message message) { } private static void logException(String message, Throwable ex) { - // prevents from logging exceptions caused be broken connection with a client - if (!isCausedBy(ex, AsyncRequestNotUsableException.class)) { + // Prevents exceptions caused by broken connection with a client from logging + if (findCause(ex, AsyncRequestNotUsableException.class).isEmpty()) { LOG.error(message, ex); } } @@ -263,4 +265,10 @@ public ErrorInfo invalidIdentifierException(Message message, InvalidIdentifie logException(e, message); return errorInfo(message, e); } + + @MessageExceptionHandler + public ErrorInfo uriSyntaxException(Message message, URISyntaxException e) { + logException(e, message); + return errorInfo(message, e); + } }