hawkBit rest docs (management & DDI API) (#688)
* hawkBit REST docs. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fiy gitignore. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Add to website. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Switch to generated docs. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Fix typos. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Review findings. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Otimizations. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Revert accidental checkin. Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com> * Add security link.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.hawkbit.rest.exception.ResponseExceptionHandler;
|
||||
import org.eclipse.hawkbit.rest.filter.ExcludePathAwareShallowETagFilter;
|
||||
import org.eclipse.hawkbit.rest.util.FilterHttpResponse;
|
||||
import org.eclipse.hawkbit.rest.util.HttpResponseFactoryBean;
|
||||
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Configuration for Rest api.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableHypermediaSupport(type = { HypermediaType.HAL })
|
||||
public class RestConfiguration {
|
||||
|
||||
/**
|
||||
* Create filter for {@link HttpServletResponse}.
|
||||
*/
|
||||
@Bean
|
||||
FilterHttpResponse filterHttpResponse() {
|
||||
return new FilterHttpResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create factory bean for {@link HttpServletResponse}.
|
||||
*/
|
||||
@Bean
|
||||
FactoryBean<HttpServletResponse> httpResponseFactoryBean() {
|
||||
return new HttpResponseFactoryBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create factory bean for {@link HttpServletResponse}.
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
|
||||
RequestResponseContextHolder requestResponseContextHolder() {
|
||||
return new RequestResponseContextHolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ControllerAdvice} for mapping {@link RuntimeException}s from the
|
||||
* repository to {@link HttpStatus} codes.
|
||||
*/
|
||||
@Bean
|
||||
ResponseExceptionHandler responseExceptionHandler() {
|
||||
return new ResponseExceptionHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter registration bean for spring etag filter.
|
||||
*
|
||||
* @return the spring filter registration bean for registering an etag
|
||||
* filter in the filter chain
|
||||
*/
|
||||
@Bean
|
||||
FilterRegistrationBean eTagFilter() {
|
||||
|
||||
final FilterRegistrationBean filterRegBean = new FilterRegistrationBean();
|
||||
// Exclude the URLs for downloading artifacts, so no eTag is generated
|
||||
// in the ShallowEtagHeaderFilter, just using the SH1 hash of the
|
||||
// artifact itself as 'ETag', because otherwise the file will be copied
|
||||
// in memory!
|
||||
filterRegBean.setFilter(new ExcludePathAwareShallowETagFilter("/UI/**",
|
||||
"/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download",
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**",
|
||||
"/api/v1/downloadserver/**"));
|
||||
|
||||
return filterRegBean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.data;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
|
||||
/**
|
||||
* List that extends ResourceSupport to ensure that links in content are in HAL
|
||||
* format.
|
||||
*
|
||||
* @param <T>
|
||||
* of the response content
|
||||
*/
|
||||
public class ResponseList<T> extends ResourceSupport implements List<T> {
|
||||
|
||||
private final List<T> content;
|
||||
|
||||
/**
|
||||
* @param content
|
||||
* to delegate
|
||||
*/
|
||||
public ResponseList(final List<T> content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return content.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return content.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(final Object o) {
|
||||
return content.contains(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return content.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
return content.toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(final T[] a) {
|
||||
return content.toArray(a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(final T e) {
|
||||
return content.add(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(final Object o) {
|
||||
return content.remove(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(final Collection<?> c) {
|
||||
return content.containsAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final Collection<? extends T> c) {
|
||||
return content.addAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(final int index, final Collection<? extends T> c) {
|
||||
return content.addAll(index, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(final Collection<?> c) {
|
||||
return content.removeAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(final Collection<?> c) {
|
||||
return content.retainAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
content.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
return content.equals(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return content.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(final int index) {
|
||||
return content.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T set(final int index, final T element) {
|
||||
return content.set(index, element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(final int index, final T element) {
|
||||
content.add(index, element);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T remove(final int index) {
|
||||
return content.remove(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(final Object o) {
|
||||
return content.indexOf(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOf(final Object o) {
|
||||
return content.lastIndexOf(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListIterator<T> listIterator() {
|
||||
return content.listIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListIterator<T> listIterator(final int index) {
|
||||
return content.listIterator(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> subList(final int fromIndex, final int toIndex) {
|
||||
return content.subList(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.data;
|
||||
|
||||
import org.eclipse.hawkbit.rest.exception.SortParameterUnsupportedDirectionException;
|
||||
|
||||
/**
|
||||
* A definition of possible sorting direction.
|
||||
*/
|
||||
public enum SortDirection {
|
||||
/**
|
||||
* Ascending.
|
||||
*/
|
||||
ASC,
|
||||
/**
|
||||
* Descending.
|
||||
*/
|
||||
DESC;
|
||||
|
||||
/**
|
||||
* Returns the sort direction for the given name.
|
||||
*
|
||||
* @param name
|
||||
* the name of the enum
|
||||
* @return the corresponding enum
|
||||
* @throws SortParameterUnsupportedDirectionException
|
||||
* if there is no matching enum for the specified name
|
||||
*/
|
||||
public static SortDirection getByName(final String name) {
|
||||
try {
|
||||
return valueOf(name.toUpperCase());
|
||||
} catch (final IllegalArgumentException ex) {// NOSONAR
|
||||
throw new SortParameterUnsupportedDirectionException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception which is thrown in case an request body is not well formaned and
|
||||
* cannot be parsed.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class MessageNotReadableException extends AbstractServerRtException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new MessageNotReadableException with
|
||||
* {@link SpServerError#SP_REST_BODY_NOT_READABLE} error.
|
||||
*/
|
||||
public MessageNotReadableException() {
|
||||
super(SpServerError.SP_REST_BODY_NOT_READABLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Thrown if a multi part exception occurred.
|
||||
*
|
||||
*/
|
||||
public final class MultiPartFileUploadException extends AbstractServerRtException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @param cause
|
||||
* for the exception
|
||||
*/
|
||||
public MultiPartFileUploadException(final Throwable cause) {
|
||||
super(cause.getMessage(), SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.multipart.MultipartException;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
/**
|
||||
* General controller advice for exception handling.
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class ResponseExceptionHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResponseExceptionHandler.class);
|
||||
private static final Map<SpServerError, HttpStatus> ERROR_TO_HTTP_STATUS = new EnumMap<>(SpServerError.class);
|
||||
private static final HttpStatus DEFAULT_RESPONSE_STATUS = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
private static final String MESSAGE_FORMATTER_SEPARATOR = " ";
|
||||
|
||||
static {
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_NOT_EXISTS, HttpStatus.NOT_FOUND);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS, HttpStatus.CONFLICT);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_READ_ONLY, HttpStatus.FORBIDDEN);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_SORT_PARAM_INVALID_DIRECTION, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_SORT_PARAM_INVALID_FIELD, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_SORT_PARAM_SYNTAX, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_RSQL_PARAM_INVALID_FIELD, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_INSUFFICIENT_PERMISSION, HttpStatus.FORBIDDEN);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_DELETE_FAILED, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_LOAD_FAILED, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_QUOTA_EXCEEDED, HttpStatus.FORBIDDEN);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ACTION_NOT_CANCELABLE, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ACTION_NOT_FORCE_QUITABLE, HttpStatus.METHOD_NOT_ALLOWED);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_CREATION_FAILED_MISSING_MODULE, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_MODULE_UNSUPPORTED, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_TYPE_UNDEFINED, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_TENANT_NOT_EXISTS, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ENTITY_LOCKED, HttpStatus.LOCKED);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ROLLOUT_ILLEGAL_STATE, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_KEY_INVALID, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_INVALID_TARGET_ADDRESS, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_CONSTRAINT_VIOLATION, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_OPERATION_NOT_SUPPORTED, HttpStatus.GONE);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_CONCURRENT_MODIFICATION, HttpStatus.CONFLICT);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
|
||||
}
|
||||
|
||||
private static HttpStatus getStatusOrDefault(final SpServerError error) {
|
||||
return ERROR_TO_HTTP_STATUS.getOrDefault(error, DEFAULT_RESPONSE_STATUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* method for handling exception of type AbstractServerRtException. Called
|
||||
* by the Spring-Framework for exception handling.
|
||||
*
|
||||
* @param request
|
||||
* the Http request
|
||||
* @param ex
|
||||
* the exception which occurred
|
||||
*
|
||||
* @return the entity to be responded containing the exception information
|
||||
* as entity.
|
||||
*/
|
||||
@ExceptionHandler(AbstractServerRtException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleSpServerRtExceptions(final HttpServletRequest request,
|
||||
final Exception ex) {
|
||||
logRequest(request, ex);
|
||||
final ExceptionInfo response = createExceptionInfo(ex);
|
||||
final HttpStatus responseStatus;
|
||||
if (ex instanceof AbstractServerRtException) {
|
||||
responseStatus = getStatusOrDefault(((AbstractServerRtException) ex).getError());
|
||||
} else {
|
||||
responseStatus = DEFAULT_RESPONSE_STATUS;
|
||||
}
|
||||
return new ResponseEntity<>(response, responseStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling exception of type HttpMessageNotReadableException
|
||||
* which is thrown in case the request body is not well formed and cannot be
|
||||
* deserialized. Called by the Spring-Framework for exception handling.
|
||||
*
|
||||
* @param request
|
||||
* the Http request
|
||||
* @param ex
|
||||
* the exception which occurred
|
||||
* @return the entity to be responded containing the exception information
|
||||
* as entity.
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleHttpMessageNotReadableException(final HttpServletRequest request,
|
||||
final Exception ex) {
|
||||
logRequest(request, ex);
|
||||
final ExceptionInfo response = createExceptionInfo(new MessageNotReadableException());
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling exception of type ConstraintViolationException which
|
||||
* is thrown in case the request is rejected due to a constraint violation.
|
||||
* Called by the Spring-Framework for exception handling.
|
||||
*
|
||||
* @param request
|
||||
* the Http request
|
||||
* @param ex
|
||||
* the exception which occurred
|
||||
* @return the entity to be responded containing the exception information
|
||||
* as entity.
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleConstraintViolationException(final HttpServletRequest request,
|
||||
final ConstraintViolationException ex) {
|
||||
logRequest(request, ex);
|
||||
|
||||
final ExceptionInfo response = new ExceptionInfo();
|
||||
response.setMessage(ex.getConstraintViolations().stream().map(
|
||||
violation -> violation.getPropertyPath() + MESSAGE_FORMATTER_SEPARATOR + violation.getMessage() + ".")
|
||||
.collect(Collectors.joining(MESSAGE_FORMATTER_SEPARATOR)));
|
||||
response.setExceptionClass(ex.getClass().getName());
|
||||
response.setErrorCode(SpServerError.SP_REPO_CONSTRAINT_VIOLATION.getKey());
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling exception of type ValidationException which is thrown
|
||||
* in case the request is rejected due to invalid requests. Called by the
|
||||
* Spring-Framework for exception handling.
|
||||
*
|
||||
* @param request
|
||||
* the Http request
|
||||
* @param ex
|
||||
* the exception which occurred
|
||||
* @return the entity to be responded containing the exception information
|
||||
* as entity.
|
||||
*/
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleValidationException(final HttpServletRequest request,
|
||||
final ValidationException ex) {
|
||||
logRequest(request, ex);
|
||||
|
||||
final ExceptionInfo response = new ExceptionInfo();
|
||||
response.setMessage(ex.getMessage());
|
||||
response.setExceptionClass(ex.getClass().getName());
|
||||
response.setErrorCode(SpServerError.SP_REPO_CONSTRAINT_VIOLATION.getKey());
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling exception of type {@link MultipartException} which is
|
||||
* thrown in case the request body is not well formed and cannot be
|
||||
* deserialized. Called by the Spring-Framework for exception handling.
|
||||
*
|
||||
* @param request
|
||||
* the Http request
|
||||
* @param ex
|
||||
* the exception which occurred
|
||||
* @return the entity to be responded containing the exception information
|
||||
* as entity.
|
||||
*/
|
||||
@ExceptionHandler(MultipartException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleMultipartException(final HttpServletRequest request,
|
||||
final Exception ex) {
|
||||
|
||||
logRequest(request, ex);
|
||||
|
||||
final List<Throwable> throwables = ExceptionUtils.getThrowableList(ex);
|
||||
final Throwable responseCause = Iterables.getLast(throwables);
|
||||
final ExceptionInfo response = createExceptionInfo(new MultiPartFileUploadException(responseCause));
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
private void logRequest(final HttpServletRequest request, final Exception ex) {
|
||||
LOG.debug("Handling exception {} of request {}", ex.getClass().getName(), request.getRequestURL());
|
||||
}
|
||||
|
||||
private ExceptionInfo createExceptionInfo(final Exception ex) {
|
||||
final ExceptionInfo response = new ExceptionInfo();
|
||||
response.setMessage(ex.getMessage());
|
||||
response.setExceptionClass(ex.getClass().getName());
|
||||
if (ex instanceof AbstractServerRtException) {
|
||||
response.setErrorCode(((AbstractServerRtException) ex).getError().getKey());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception used by the REST API in case of invalid sort parameter syntax.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class SortParameterSyntaxErrorException extends AbstractServerRtException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new SortParameterSyntaxErrorException with
|
||||
* {@link SpServerError#SP_REST_SORT_PARAM_SYNTAX} error.
|
||||
*/
|
||||
public SortParameterSyntaxErrorException() {
|
||||
super(SpServerError.SP_REST_SORT_PARAM_SYNTAX);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception used by the REST API in case of invalid sort parameter direction
|
||||
* name.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class SortParameterUnsupportedDirectionException extends AbstractServerRtException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new SortParameterSyntaxErrorException.
|
||||
*/
|
||||
public SortParameterUnsupportedDirectionException() {
|
||||
super(SpServerError.SP_REST_SORT_PARAM_INVALID_DIRECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SortParameterSyntaxErrorException with
|
||||
* {@link SpServerError#SP_REST_SORT_PARAM_INVALID_DIRECTION} error.
|
||||
*
|
||||
* @param cause
|
||||
* the cause (which is saved for later retrieval by the
|
||||
* getCause() method). (A null value is permitted, and indicates
|
||||
* that the cause is nonexistent or unknown.)
|
||||
*/
|
||||
public SortParameterUnsupportedDirectionException(final Throwable cause) {
|
||||
super(SpServerError.SP_REST_SORT_PARAM_INVALID_DIRECTION, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception used by the REST API in case of invalid field name in the sort
|
||||
* parameter.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class SortParameterUnsupportedFieldException extends AbstractServerRtException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new SortParameterSyntaxErrorException with
|
||||
* {@link SpServerError#SP_REST_SORT_PARAM_INVALID_FIELD} error.
|
||||
*/
|
||||
public SortParameterUnsupportedFieldException() {
|
||||
super(SpServerError.SP_REST_SORT_PARAM_INVALID_FIELD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SortParameterSyntaxErrorException with
|
||||
* {@link SpServerError#SP_REST_SORT_PARAM_INVALID_FIELD} error.
|
||||
*
|
||||
* @param cause
|
||||
* the cause (which is saved for later retrieval by the
|
||||
* getCause() method). (A null value is permitted, and indicates
|
||||
* that the cause is nonexistent or unknown.)
|
||||
*/
|
||||
public SortParameterUnsupportedFieldException(final Throwable cause) {
|
||||
super(SpServerError.SP_REST_SORT_PARAM_INVALID_FIELD, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||
|
||||
/**
|
||||
* An {@link ShallowEtagHeaderFilter} with exclusion paths to exclude some paths
|
||||
* where no ETag header should be generated due that calculating the ETag is an
|
||||
* expensive operation and the response output need to be copied in memory which
|
||||
* should be excluded in case of artifact downloads which could be big of size.
|
||||
*/
|
||||
public class ExcludePathAwareShallowETagFilter extends ShallowEtagHeaderFilter {
|
||||
|
||||
private final String[] excludeAntPaths;
|
||||
private final AntPathMatcher antMatcher = new AntPathMatcher();
|
||||
|
||||
/**
|
||||
* @param excludeAntPaths
|
||||
*/
|
||||
public ExcludePathAwareShallowETagFilter(final String... excludeAntPaths) {
|
||||
this.excludeAntPaths = excludeAntPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
|
||||
final FilterChain filterChain) throws ServletException, IOException {
|
||||
final boolean shouldExclude = shouldExclude(request);
|
||||
if (shouldExclude) {
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
super.doFilterInternal(request, response, filterChain);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldExclude(final HttpServletRequest request) {
|
||||
for (final String pattern : excludeAntPaths) {
|
||||
if (antMatcher.match(request.getContextPath() + pattern, request.getRequestURI())) {
|
||||
// exclude this request from eTag filter
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.json.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
|
||||
/**
|
||||
* A exception model rest representation with JSON annotations for response
|
||||
* bodies in case of RESTful exception occurrence.
|
||||
*
|
||||
*/
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
public class ExceptionInfo {
|
||||
|
||||
private String exceptionClass;
|
||||
private String errorCode;
|
||||
private String message;
|
||||
private List<String> parameters;
|
||||
|
||||
/**
|
||||
* @return the exceptionClass
|
||||
*/
|
||||
public String getExceptionClass() {
|
||||
return exceptionClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param exceptionClass
|
||||
* the exceptionClass to set
|
||||
*/
|
||||
public void setExceptionClass(final String exceptionClass) {
|
||||
this.exceptionClass = exceptionClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the parameters
|
||||
*/
|
||||
public List<String> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parameters
|
||||
* the parameters to set
|
||||
*/
|
||||
public void setParameters(final List<String> parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the errorCode
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode
|
||||
* the errorCode to set
|
||||
*/
|
||||
public void setErrorCode(final String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the message
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message
|
||||
* the message to set
|
||||
*/
|
||||
public void setMessage(final String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Thrown if artifact content streaming to client failed.
|
||||
*/
|
||||
public final class FileStreamingFailedException extends AbstractServerRtException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new FileUploadFailedException with
|
||||
* {@link SpServerError#SP_REST_BODY_NOT_READABLE} error.
|
||||
*/
|
||||
public FileStreamingFailedException() {
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with Throwable.
|
||||
*
|
||||
* @param cause
|
||||
* for the exception
|
||||
*/
|
||||
public FileStreamingFailedException(final Throwable cause) {
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with error string.
|
||||
*
|
||||
* @param message
|
||||
* of the error
|
||||
*/
|
||||
public FileStreamingFailedException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with error string and cause.
|
||||
*
|
||||
* @param message
|
||||
* of the error
|
||||
* @param cause
|
||||
* for the exception
|
||||
*/
|
||||
public FileStreamingFailedException(final String message, final Throwable cause) {
|
||||
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
/**
|
||||
* Listener for progress on artifact file streaming.
|
||||
*
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FileStreamingProgressListener {
|
||||
|
||||
/**
|
||||
* Called multiple times during streaming.
|
||||
*
|
||||
* @param requestedBytes
|
||||
* requested bytes of the request
|
||||
* @param shippedBytesSinceLast
|
||||
* since the last report
|
||||
* @param shippedBytesOverall
|
||||
* during the request
|
||||
*/
|
||||
void progress(long requestedBytes, long shippedBytesSinceLast, long shippedBytesOverall);
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.math.DoubleMath;
|
||||
|
||||
/**
|
||||
* Utility class for artifact file streaming.
|
||||
*/
|
||||
public final class FileStreamingUtil {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileStreamingUtil.class);
|
||||
|
||||
/**
|
||||
* File suffix for MDH hash download (see Linux md5sum).
|
||||
*/
|
||||
public static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM";
|
||||
|
||||
private static final int BUFFER_SIZE = 0x2000; // 8k
|
||||
|
||||
private FileStreamingUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a md5 file response.
|
||||
*
|
||||
* @param response
|
||||
* the response
|
||||
* @param md5Hash
|
||||
* of the artifact
|
||||
* @param filename
|
||||
* as provided by the client
|
||||
* @return the response
|
||||
* @throws IOException
|
||||
* cannot write output stream
|
||||
*/
|
||||
public static ResponseEntity<Void> writeMD5FileResponse(final HttpServletResponse response, final String md5Hash,
|
||||
final String filename) throws IOException {
|
||||
|
||||
if (md5Hash == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append(md5Hash);
|
||||
builder.append(" ");
|
||||
builder.append(filename);
|
||||
final byte[] content = builder.toString().getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
final StringBuilder header = new StringBuilder().append("attachment;filename=").append(filename)
|
||||
.append(ARTIFACT_MD5_DWNL_SUFFIX);
|
||||
|
||||
response.setContentLength(content.length);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header.toString());
|
||||
|
||||
response.getOutputStream().write(content);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Write response with target relation and publishes events concerning the
|
||||
* download progress based on given update action status.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The request supports RFC7233 range requests.
|
||||
* </p>
|
||||
*
|
||||
* @param artifact
|
||||
* the artifact
|
||||
* @param filename
|
||||
* to be written to the client response
|
||||
* @param lastModified
|
||||
* unix timestamp of the artifact
|
||||
* @param response
|
||||
* to be sent back to the requesting client
|
||||
* @param request
|
||||
* from the client
|
||||
* @param progressListener
|
||||
* to write progress updates to
|
||||
*
|
||||
* @return http response
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
|
||||
* /html/rfc7233</a>
|
||||
*
|
||||
* @throws FileStreamingFailedException
|
||||
* if streaming fails
|
||||
*/
|
||||
public static ResponseEntity<InputStream> writeFileResponse(final AbstractDbArtifact artifact,
|
||||
final String filename, final long lastModified, final HttpServletResponse response,
|
||||
final HttpServletRequest request, final FileStreamingProgressListener progressListener) {
|
||||
|
||||
ResponseEntity<InputStream> result;
|
||||
|
||||
final String etag = artifact.getHashes().getSha1();
|
||||
final long length = artifact.getSize();
|
||||
|
||||
response.reset();
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
|
||||
response.setHeader(HttpHeaders.ETAG, etag);
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
if (lastModified > 0) {
|
||||
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
|
||||
}
|
||||
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setBufferSize(BUFFER_SIZE);
|
||||
|
||||
final ByteRange full = new ByteRange(0, length - 1, length);
|
||||
final List<ByteRange> ranges = new ArrayList<>();
|
||||
|
||||
// Validate and process Range and If-Range headers.
|
||||
final String range = request.getHeader("Range");
|
||||
if (lastModified > 0 && range != null) {
|
||||
LOG.debug("range header for filename ({}) is: {}", filename, range);
|
||||
|
||||
// Range header matches"bytes=n-n,n-n,n-n..."
|
||||
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
|
||||
LOG.debug("range header for filename ({}) is not satisfiable: ", filename);
|
||||
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
// RFC: if the representation is unchanged, send me the part(s) that
|
||||
// I am requesting in
|
||||
// Range; otherwise, send me the entire representation.
|
||||
checkForShortcut(request, etag, lastModified, full, ranges);
|
||||
|
||||
// it seems there are valid ranges
|
||||
result = extractRange(response, length, ranges, range);
|
||||
// return if range extraction turned out to be invalid
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// full request - no range
|
||||
if (ranges.isEmpty() || ranges.get(0).equals(full)) {
|
||||
LOG.debug("filename ({}) results into a full request: ", filename);
|
||||
result = handleFullFileRequest(artifact, filename, response, progressListener, full);
|
||||
}
|
||||
// standard range request
|
||||
else if (ranges.size() == 1) {
|
||||
LOG.debug("filename ({}) results into a standard range request: ", filename);
|
||||
result = handleStandardRangeRequest(artifact, filename, response, progressListener, ranges);
|
||||
}
|
||||
// multipart range request
|
||||
else {
|
||||
LOG.debug("filename ({}) results into a multipart range request: ", filename);
|
||||
result = handleMultipartRangeRequest(artifact, filename, response, progressListener, ranges);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleFullFileRequest(final AbstractDbArtifact artifact,
|
||||
final String filename, final HttpServletResponse response,
|
||||
final FileStreamingProgressListener progressListener, final ByteRange full) {
|
||||
final ByteRange r = full;
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
|
||||
response.setContentLengthLong(r.getLength());
|
||||
|
||||
try (InputStream from = artifact.getFileInputStream()) {
|
||||
final ServletOutputStream to = response.getOutputStream();
|
||||
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
|
||||
} catch (final IOException e) {
|
||||
throw new FileStreamingFailedException("fullfileRequest " + filename, e);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> extractRange(final HttpServletResponse response, final long length,
|
||||
final List<ByteRange> ranges, final String range) {
|
||||
|
||||
if (ranges.isEmpty()) {
|
||||
for (final String part : range.substring(6).split(",")) {
|
||||
long start = sublong(part, 0, part.indexOf('-'));
|
||||
long end = sublong(part, part.indexOf('-') + 1, part.length());
|
||||
|
||||
if (start == -1) {
|
||||
start = length - end;
|
||||
end = length - 1;
|
||||
} else if (end == -1 || end > length - 1) {
|
||||
end = length - 1;
|
||||
}
|
||||
|
||||
// Check if Range is syntactically valid. If not, then return
|
||||
// 416.
|
||||
if (start > end) {
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
|
||||
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
// Add range.
|
||||
ranges.add(new ByteRange(start, end, length));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long sublong(final String value, final int beginIndex, final int endIndex) {
|
||||
final String substring = value.substring(beginIndex, endIndex);
|
||||
return substring.length() > 0 ? Long.parseLong(substring) : -1;
|
||||
}
|
||||
|
||||
private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified,
|
||||
final ByteRange full, final List<ByteRange> ranges) {
|
||||
final String ifRange = request.getHeader(HttpHeaders.IF_RANGE);
|
||||
if (ifRange != null && !ifRange.equals(etag)) {
|
||||
try {
|
||||
final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE);
|
||||
if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
|
||||
ranges.add(full);
|
||||
}
|
||||
} catch (final IllegalArgumentException ignore) {
|
||||
LOG.info("Invalid if-range header field", ignore);
|
||||
ranges.add(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleMultipartRangeRequest(final AbstractDbArtifact artifact,
|
||||
final String filename, final HttpServletResponse response,
|
||||
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
|
||||
|
||||
response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY);
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
|
||||
try {
|
||||
final ServletOutputStream to = response.getOutputStream();
|
||||
|
||||
for (final ByteRange r : ranges) {
|
||||
try (InputStream from = artifact.getFileInputStream()) {
|
||||
|
||||
// Add multipart boundary and header fields for every range.
|
||||
to.println();
|
||||
to.println("--" + ByteRange.MULTIPART_BOUNDARY);
|
||||
to.println(HttpHeaders.CONTENT_RANGE + ": bytes " + r.getStart() + "-" + r.getEnd() + "/"
|
||||
+ r.getTotal());
|
||||
|
||||
// Copy single part range of multi part range.
|
||||
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
|
||||
}
|
||||
}
|
||||
|
||||
// End with final multipart boundary.
|
||||
to.println();
|
||||
to.print("--" + ByteRange.MULTIPART_BOUNDARY + "--");
|
||||
} catch (final IOException e) {
|
||||
throw new FileStreamingFailedException("multipartRangeRequest " + filename, e);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleStandardRangeRequest(final AbstractDbArtifact artifact,
|
||||
final String filename, final HttpServletResponse response,
|
||||
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
|
||||
final ByteRange r = ranges.get(0);
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
|
||||
response.setContentLengthLong(r.getLength());
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
|
||||
try (InputStream from = artifact.getFileInputStream()) {
|
||||
final ServletOutputStream to = response.getOutputStream();
|
||||
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("standardRangeRequest of file ({}) failed!", filename, e);
|
||||
throw new FileStreamingFailedException(filename);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
|
||||
}
|
||||
|
||||
private static long copyStreams(final InputStream from, final OutputStream to,
|
||||
final FileStreamingProgressListener progressListener, final long start, final long length,
|
||||
final String filename) throws IOException {
|
||||
|
||||
final long startMillis = System.currentTimeMillis();
|
||||
LOG.trace("Start of copy-streams of file {} from {} to {}", filename, start, length);
|
||||
|
||||
Preconditions.checkNotNull(from);
|
||||
Preconditions.checkNotNull(to);
|
||||
final byte[] buf = new byte[BUFFER_SIZE];
|
||||
long total = 0;
|
||||
int progressPercent = 1;
|
||||
|
||||
ByteStreams.skipFully(from, start);
|
||||
|
||||
long toRead = length;
|
||||
boolean toContinue = true;
|
||||
long shippedSinceLastEvent = 0;
|
||||
|
||||
while (toContinue) {
|
||||
final int r = from.read(buf);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
toRead -= r;
|
||||
if (toRead > 0) {
|
||||
to.write(buf, 0, r);
|
||||
total += r;
|
||||
shippedSinceLastEvent += r;
|
||||
} else {
|
||||
to.write(buf, 0, (int) toRead + r);
|
||||
total += toRead + r;
|
||||
shippedSinceLastEvent += toRead + r;
|
||||
toContinue = false;
|
||||
}
|
||||
|
||||
if (progressListener != null) {
|
||||
final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN);
|
||||
|
||||
// every 10 percent an event
|
||||
if (newPercent == 100 || newPercent > progressPercent + 10) {
|
||||
progressPercent = newPercent;
|
||||
progressListener.progress(length, shippedSinceLastEvent, total);
|
||||
shippedSinceLastEvent = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final long totalTime = System.currentTimeMillis() - startMillis;
|
||||
|
||||
if (total < length) {
|
||||
throw new FileStreamingFailedException(filename + ": " + (length - total)
|
||||
+ " bytes could not be written to client, total time on write: !" + totalTime + " ms");
|
||||
}
|
||||
|
||||
LOG.trace("Finished copy-stream of file {} with length {} in {} ms", filename, length, totalTime);
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static final class ByteRange {
|
||||
private static final String MULTIPART_BOUNDARY = "THIS_STRING_SEPARATES_MULTIPART";
|
||||
|
||||
private final long start;
|
||||
private final long end;
|
||||
private final long length;
|
||||
private final long total;
|
||||
|
||||
private ByteRange(final long start, final long end, final long total) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
length = end - start + 1;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
private long getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
private long getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
private long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
private long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Generated code
|
||||
@SuppressWarnings("squid:S864")
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (end ^ (end >>> 32));
|
||||
result = prime * result + (int) (length ^ (length >>> 32));
|
||||
result = prime * result + (int) (start ^ (start >>> 32));
|
||||
result = prime * result + (int) (total ^ (total >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Generated code
|
||||
@SuppressWarnings("squid:S1126")
|
||||
public boolean equals(final Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final ByteRange other = (ByteRange) obj;
|
||||
if (end != other.end) {
|
||||
return false;
|
||||
}
|
||||
if (length != other.length) {
|
||||
return false;
|
||||
}
|
||||
if (start != other.start) {
|
||||
return false;
|
||||
}
|
||||
if (total != other.total) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Filter is needed to autowire the {@link HttpServletResponse}.
|
||||
*
|
||||
*/
|
||||
public class FilterHttpResponse implements Filter {
|
||||
|
||||
private ThreadLocal<HttpServletResponse> threadLocalResponse = new ThreadLocal<>();
|
||||
|
||||
@Override
|
||||
public void init(final FilterConfig filterConfig) throws ServletException {
|
||||
// not needed
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
try {
|
||||
threadLocalResponse.set((HttpServletResponse) response);
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
threadLocalResponse.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public HttpServletResponse getHttpServletReponse() {
|
||||
return threadLocalResponse.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
threadLocalResponse = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.NamedBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
|
||||
/**
|
||||
*
|
||||
* Factory bean to autowire the {@link HttpServletResponse}.
|
||||
*
|
||||
*/
|
||||
public class HttpResponseFactoryBean implements FactoryBean<HttpServletResponse>, ApplicationContextAware, NamedBean {
|
||||
|
||||
public static final String FACTORY_BEAN_NAME = "httpResponseFactoryBean";
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public HttpServletResponse getObject() throws Exception {
|
||||
return applicationContext.getBean(FilterHttpResponse.class).getHttpServletReponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return HttpServletResponse.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBeanName() {
|
||||
return FACTORY_BEAN_NAME;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Utility class for the Rest Source API.
|
||||
*/
|
||||
public final class HttpUtil {
|
||||
|
||||
private HttpUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks given CSV string for defined match value or wildcard.
|
||||
*
|
||||
* @param matchHeader
|
||||
* to search through
|
||||
* @param toMatch
|
||||
* to search for
|
||||
*
|
||||
* @return <code>true</code> if string matches.
|
||||
*/
|
||||
public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) {
|
||||
final String[] matchValues = matchHeader.split("\\s*,\\s*");
|
||||
Arrays.sort(matchValues);
|
||||
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Store the request and response for the rest resources.
|
||||
*/
|
||||
public class RequestResponseContextHolder {
|
||||
|
||||
private HttpServletRequest httpServletRequest;
|
||||
|
||||
private HttpServletResponse httpServletResponse;
|
||||
|
||||
public HttpServletRequest getHttpServletRequest() {
|
||||
return httpServletRequest;
|
||||
}
|
||||
|
||||
public HttpServletResponse getHttpServletResponse() {
|
||||
return httpServletResponse;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setHttpServletRequest(final HttpServletRequest httpServletRequest) {
|
||||
this.httpServletRequest = httpServletRequest;
|
||||
}
|
||||
|
||||
@Resource(name = HttpResponseFactoryBean.FACTORY_BEAN_NAME)
|
||||
public void setHttpServletResponse(final HttpServletResponse httpServletResponse) {
|
||||
this.httpServletResponse = httpServletResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.eclipse.hawkbit.repository.FieldNameProvider;
|
||||
import org.eclipse.hawkbit.rest.exception.SortParameterSyntaxErrorException;
|
||||
import org.eclipse.hawkbit.rest.exception.SortParameterUnsupportedDirectionException;
|
||||
import org.eclipse.hawkbit.rest.exception.SortParameterUnsupportedFieldException;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
|
||||
/**
|
||||
* A utility class for parsing query parameters which define the sorting of
|
||||
* elements.
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public final class SortUtility {
|
||||
|
||||
/**
|
||||
* the delimiter between the field and direction in the sort request.
|
||||
*/
|
||||
public static final String DELIMITER_FIELD_DIRECTION = ":";
|
||||
|
||||
private static final String DELIMITER_SORT_TUPLE = ",";
|
||||
|
||||
/*
|
||||
* utility constructor private.
|
||||
*/
|
||||
private SortUtility() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the sort string e.g. given in a REST call based on the definition
|
||||
* of sorting: http://localhost/entity?s=field1:ASC, field2:DESC The fields
|
||||
* will be split into the keys of the returned map. The direction of the
|
||||
* sorting will be mapped into the {@link Direction} enum.
|
||||
*
|
||||
* @param enumType
|
||||
* the class of the enum which the fields in the sort string
|
||||
* should be related to.
|
||||
* @param <T>
|
||||
* the type of the enumeration which must be derived from
|
||||
* {@link FieldNameProvider}
|
||||
* @param sortString
|
||||
* the string representation of the query parameters. Might be
|
||||
* {@code null} or an empty string.
|
||||
* @return a list which holds the {@link FieldNameProvider} and the specific
|
||||
* {@link Direction} for them as a tuple. Never {@code null}. In
|
||||
* case of no sorting parameters an empty map will be returned.
|
||||
* @throws SortParameterSyntaxErrorException
|
||||
* if the sorting query parameter is not well-formed
|
||||
* @throws SortParameterUnsupportedFieldException
|
||||
* if a field name cannot be mapped to the enum type
|
||||
* @throws SortParameterUnsupportedDirectionException
|
||||
* if the given direction is not "ASC" or "DESC"
|
||||
*/
|
||||
public static <T extends Enum<T> & FieldNameProvider> List<Order> parse(final Class<T> enumType,
|
||||
final String sortString) throws SortParameterSyntaxErrorException {
|
||||
final List<Order> parsedSortings = new ArrayList<>();
|
||||
// scan the sort tuples e.g. field:direction
|
||||
if (sortString != null) {
|
||||
final StringTokenizer tupleTokenizer = new StringTokenizer(sortString, DELIMITER_SORT_TUPLE);
|
||||
while (tupleTokenizer.hasMoreTokens()) {
|
||||
final String sortTuple = tupleTokenizer.nextToken().trim();
|
||||
final StringTokenizer fieldDirectionTokenizer = new StringTokenizer(sortTuple,
|
||||
DELIMITER_FIELD_DIRECTION);
|
||||
if (fieldDirectionTokenizer.countTokens() == 2) {
|
||||
final String fieldName = fieldDirectionTokenizer.nextToken().trim().toUpperCase();
|
||||
final String sortDirectionStr = fieldDirectionTokenizer.nextToken().trim();
|
||||
|
||||
final T identifier = getAttributeIdentifierByName(enumType, fieldName);
|
||||
|
||||
final Direction sortDirection = getDirection(sortDirectionStr);
|
||||
parsedSortings.add(new Order(sortDirection, identifier.getFieldName()));
|
||||
} else {
|
||||
throw new SortParameterSyntaxErrorException();
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedSortings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attribute identifier for the given name.
|
||||
*
|
||||
* @param enumType
|
||||
* the class of the enum which the fields in the sort string
|
||||
* should be related to.
|
||||
* @param name
|
||||
* the name of the enum
|
||||
* @param <T>
|
||||
* the type of the enumeration which must be derived from
|
||||
* {@link FieldNameProvider}
|
||||
* @return the corresponding enum
|
||||
* @throws SortParameterUnsupportedFieldException
|
||||
* if there is no matching enum for the specified name
|
||||
*/
|
||||
private static <T extends Enum<T> & FieldNameProvider> T getAttributeIdentifierByName(final Class<T> enumType,
|
||||
final String name) {
|
||||
try {
|
||||
return Enum.valueOf(enumType, name.toUpperCase());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new SortParameterUnsupportedFieldException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Direction getDirection(final String sortDirectionStr) {
|
||||
try {
|
||||
return Direction.fromString(sortDirectionStr);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new SortParameterUnsupportedDirectionException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user