Move Mgmt artifacts into hawkbit-mgmt (#2003)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
95
hawkbit-rest-core/pom.xml
Normal file
95
hawkbit-rest-core/pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<!--
|
||||
|
||||
Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
|
||||
This program and the accompanying materials are made
|
||||
available under the terms of the Eclipse Public License 2.0
|
||||
which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-rest-core</artifactId>
|
||||
<name>hawkBit :: REST :: Core</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.hateoas</groupId>
|
||||
<artifactId>spring-hateoas</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(
|
||||
value = OpenApiConfiguration.HAWKBIT_SERVER_SWAGGER_ENABLED,
|
||||
havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public class OpenApiConfiguration {
|
||||
|
||||
public static final String HAWKBIT_SERVER_SWAGGER_ENABLED = "hawkbit.server.swagger.enabled";
|
||||
|
||||
private static final String API_TITLE = "hawkBit REST APIs";
|
||||
private static final String API_VERSION = "v1";
|
||||
private static final String DESCRIPTION = """
|
||||
Eclipse hawkBit™ is a domain-independent back-end framework for rolling out software updates to constrained edge devices as well as more powerful controllers and gateways connected to IP based networking infrastructure.
|
||||
""";
|
||||
|
||||
@Bean
|
||||
public OpenAPI openApi() {
|
||||
return new OpenAPI().info(new Info().title(API_TITLE).version(API_VERSION).description(DESCRIPTION));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.rest.exception.MessageNotReadableException;
|
||||
import org.eclipse.hawkbit.rest.exception.MultiPartFileUploadException;
|
||||
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
|
||||
import org.eclipse.hawkbit.rest.util.FileStreamingFailedException;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.multipart.MultipartException;
|
||||
|
||||
/**
|
||||
* Configuration for Rest api.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableHypermediaSupport(type = { HypermediaType.HAL })
|
||||
public class RestConfiguration {
|
||||
|
||||
/**
|
||||
* {@link ControllerAdvice} for mapping {@link RuntimeException}s from the repository to {@link HttpStatus} codes.
|
||||
*
|
||||
* @return a controller advice for handling exceptions
|
||||
*/
|
||||
@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<ExcludePathAwareShallowETagFilter> eTagFilter() {
|
||||
final FilterRegistrationBean<ExcludePathAwareShallowETagFilter> filterRegBean = new FilterRegistrationBean<>();
|
||||
|
||||
// Exclude the URLs for downloading artifacts, so no eTag is generated
|
||||
// in the ShallowEtagHeaderFilter, just using the SHA1 hash of the
|
||||
// artifact itself as 'ETag', because otherwise the file will be copied in memory!
|
||||
filterRegBean.setFilter(new ExcludePathAwareShallowETagFilter(
|
||||
"/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download",
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**",
|
||||
"/api/v1/downloadserver/**"));
|
||||
|
||||
return filterRegBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* General controller advice for exception handling.
|
||||
*/
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public static class ResponseExceptionHandler {
|
||||
|
||||
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_ALREADY_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_ENCRYPTION_NOT_SUPPORTED, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_ENCRYPTION_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_SHA256_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_BINARY_DELETED, HttpStatus.GONE);
|
||||
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_FILE_SIZE_QUOTA_EXCEEDED, HttpStatus.FORBIDDEN);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_STORAGE_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);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_ATTRIBUTES_INVALID, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_AUTO_CONFIRMATION_ALREADY_ACTIVE, HttpStatus.CONFLICT);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_AUTO_ASSIGN_ACTION_TYPE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_CONFIGURATION_VALUE_CHANGE_NOT_ALLOWED, HttpStatus.FORBIDDEN);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_MULTIASSIGNMENT_NOT_ENABLED, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_NO_WEIGHT_PROVIDED_IN_MULTIASSIGNMENT_MODE, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_TYPE_IN_USE, HttpStatus.CONFLICT);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_TYPE_INCOMPATIBLE, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_TARGET_TYPE_KEY_OR_NAME_REQUIRED, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_INVALID, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_INCOMPLETE, HttpStatus.BAD_REQUEST);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_LOCKED, HttpStatus.LOCKED);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DELETED, HttpStatus.NOT_FOUND);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_STOP_ROLLOUT_FAILED, HttpStatus.LOCKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link FileStreamingFailedException} which is thrown in case the streaming of a file failed
|
||||
* due to an internal server error. As the streaming of the file has already begun, no JSON response but only the ResponseStatus 500
|
||||
* is returned.
|
||||
* 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 response status 500
|
||||
*/
|
||||
@ExceptionHandler(FileStreamingFailedException.class)
|
||||
public ResponseEntity<Object> handleFileStreamingFailedException(final HttpServletRequest request, final Exception ex) {
|
||||
logRequest(request, ex);
|
||||
log.warn("File streaming failed: {}", ex.getMessage());
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling exception of type HttpMessageNotReadableException and MethodArgumentNotValidException which are thrown in case
|
||||
* the request body is not well-formed (e.g. syntax failures, missing/invalid parameters) 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,
|
||||
MethodArgumentNotValidException.class, HandlerMethodValidationException.class,
|
||||
IllegalArgumentException.class })
|
||||
public ResponseEntity<ExceptionInfo> handleExceptionCausedByIncorrectRequestBody(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 = throwables.get(throwables.size() - 1);
|
||||
|
||||
if (ObjectUtils.isEmpty(responseCause.getMessage())) {
|
||||
log.warn("Request {} lead to MultipartException without root cause message:\n{}", request.getRequestURL(),
|
||||
ex.getStackTrace());
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(createExceptionInfo(new MultiPartFileUploadException(responseCause)), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
private static HttpStatus getStatusOrDefault(final SpServerError error) {
|
||||
return ERROR_TO_HTTP_STATUS.getOrDefault(error, DEFAULT_RESPONSE_STATUS);
|
||||
}
|
||||
|
||||
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());
|
||||
response.setInfo(((AbstractServerRtException) ex).getInfo());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
static 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,33 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Exception which is thrown in case an request body is not well formatted and
|
||||
* cannot be parsed.
|
||||
*/
|
||||
public class MessageNotReadableException extends AbstractServerRtException {
|
||||
|
||||
@Serial
|
||||
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,28 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* Thrown if a multipart exception occurred.
|
||||
*/
|
||||
public final class MultiPartFileUploadException extends AbstractServerRtException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MultiPartFileUploadException(final Throwable cause) {
|
||||
super(cause.getMessage(), SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.json.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* A exception model rest representation with JSON annotations for response
|
||||
* bodies in case of RESTful exception occurrence.
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
public class ExceptionInfo {
|
||||
|
||||
private String exceptionClass;
|
||||
private String errorCode;
|
||||
private String message;
|
||||
private transient Map<String, Object> info;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.json.model;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
/**
|
||||
* List that extends RepresentationModel to ensure that links in content are in HAL format.
|
||||
*
|
||||
* @param <T> of the response content
|
||||
*/
|
||||
public class ResponseList<T> extends RepresentationModel<ResponseList<T>> 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 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResponseList [content=" + content + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
return content.equals(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return content.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
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 {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 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,461 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* Utility class for artifact file streaming.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public final class FileStreamingUtil {
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws FileStreamingFailedException if streaming fails
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
|
||||
* /html/rfc7233</a>
|
||||
*/
|
||||
public static ResponseEntity<InputStream> writeFileResponse(final DbArtifact 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();
|
||||
|
||||
resetResponseExceptHeaders(response);
|
||||
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
|
||||
response.setHeader(HttpHeaders.ETAG, etag);
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
// set the x-content-type options header to prevent browsers from doing
|
||||
// MIME-sniffing when downloading an artifact, as this could cause a
|
||||
// security vulnerability
|
||||
response.setHeader("X-Content-Type-Options", "nosniff");
|
||||
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 void resetResponseExceptHeaders(final HttpServletResponse response) {
|
||||
// do backup the current headers (like CORS related)
|
||||
final Map<String, String> storedHeaders = new HashMap<>();
|
||||
for (final String header : response.getHeaderNames()) {
|
||||
storedHeaders.put(header, response.getHeader(header));
|
||||
}
|
||||
// resetting the response is needed only partially. Headers set before e.b. by
|
||||
// the CORS security config needs to be persisted.
|
||||
response.reset();
|
||||
// restore headers again
|
||||
storedHeaders.forEach(response::addHeader);
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleFullFileRequest(final DbArtifact 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 (final 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 DbArtifact 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 (final 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 DbArtifact 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 (final 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);
|
||||
|
||||
Objects.requireNonNull(from);
|
||||
Objects.requireNonNull(to);
|
||||
final byte[] buf = new byte[BUFFER_SIZE];
|
||||
long total = 0;
|
||||
int progressPercent = 1;
|
||||
|
||||
IOUtils.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 = (int) Math.floor(total * 100.0 / length);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
private long getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
private long getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
private long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
private long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Utility class for the Rest Source API.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class 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,42 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* Gives access to the request and response for the rest resources.
|
||||
*/
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
public class RequestResponseContextHolder {
|
||||
|
||||
public static HttpServletRequest getHttpServletRequest() {
|
||||
return Objects
|
||||
.requireNonNull(
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes(),
|
||||
"Request attribute is unavailable")
|
||||
.getRequest();
|
||||
}
|
||||
|
||||
public static HttpServletResponse getHttpServletResponse() {
|
||||
return Objects
|
||||
.requireNonNull(
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes(),
|
||||
"Request attribute is unavailable")
|
||||
.getResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
|
||||
import org.eclipse.hawkbit.repository.test.TestConfiguration;
|
||||
import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.filter.CharacterEncodingFilter;
|
||||
|
||||
/**
|
||||
* Abstract Test for Rest tests.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
|
||||
@ContextConfiguration(classes = {
|
||||
RestConfiguration.class, RepositoryApplicationConfiguration.class, TestConfiguration.class })
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@WebAppConfiguration
|
||||
@AutoConfigureMockMvc
|
||||
public abstract class AbstractRestIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
protected MockMvc mvc;
|
||||
@Autowired
|
||||
protected WebApplicationContext webApplicationContext;
|
||||
@Autowired
|
||||
private CharacterEncodingFilter characterEncodingFilter;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
mvc = createMvcWebAppContext(webApplicationContext).build();
|
||||
}
|
||||
|
||||
protected DefaultMockMvcBuilder createMvcWebAppContext(final WebApplicationContext context) {
|
||||
final DefaultMockMvcBuilder createMvcWebAppContext = MockMvcBuilders.webAppContextSetup(context);
|
||||
|
||||
// CharacterEncodingFilter is needed for the encoding properties to be imported properly
|
||||
createMvcWebAppContext.addFilter(characterEncodingFilter);
|
||||
createMvcWebAppContext.addFilter(
|
||||
new RestConfiguration.ExcludePathAwareShallowETagFilter("/rest/v1/softwaremodules/{smId}/artifacts/{artId}/download",
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/**",
|
||||
"/api/v1/downloadserver/**"));
|
||||
|
||||
return createMvcWebAppContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mockingDetails;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@Feature("Unit Tests - Security")
|
||||
@Story("Exclude path aware shallow ETag filter")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ExcludePathAwareShallowETagFilterTest {
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest servletRequestMock;
|
||||
|
||||
@Mock
|
||||
private HttpServletResponse servletResponseMock;
|
||||
|
||||
@Mock
|
||||
private FilterChain filterChainMock;
|
||||
|
||||
@Test
|
||||
public void excludePathDoesNotCalculateETag() throws ServletException, IOException {
|
||||
final String knownContextPath = "/bumlux/test";
|
||||
final String knownUri = knownContextPath + "/exclude/download";
|
||||
final String antPathExclusion = "/exclude/**";
|
||||
|
||||
// mock
|
||||
when(servletRequestMock.getContextPath()).thenReturn(knownContextPath);
|
||||
when(servletRequestMock.getRequestURI()).thenReturn(knownUri);
|
||||
|
||||
final RestConfiguration.ExcludePathAwareShallowETagFilter filterUnderTest = new RestConfiguration.ExcludePathAwareShallowETagFilter(
|
||||
antPathExclusion);
|
||||
|
||||
filterUnderTest.doFilterInternal(servletRequestMock, servletResponseMock, filterChainMock);
|
||||
|
||||
// verify no eTag header is set and response has not been changed
|
||||
assertThat(servletResponseMock.getHeader("ETag"))
|
||||
.as("ETag header should not be set during downloading, too expensive").isNull();
|
||||
// the servlet response must be the same mock!
|
||||
verify(filterChainMock, times(1)).doFilter(servletRequestMock, servletResponseMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathNotExcludedETagIsCalculated() throws ServletException, IOException {
|
||||
final String knownContextPath = "/bumlux/test";
|
||||
final String knownUri = knownContextPath + "/include/download";
|
||||
final String antPathExclusion = "/exclude/**";
|
||||
|
||||
// mock
|
||||
when(servletRequestMock.getContextPath()).thenReturn(knownContextPath);
|
||||
when(servletRequestMock.getRequestURI()).thenReturn(knownUri);
|
||||
|
||||
final RestConfiguration.ExcludePathAwareShallowETagFilter filterUnderTest = new RestConfiguration.ExcludePathAwareShallowETagFilter(
|
||||
antPathExclusion);
|
||||
|
||||
final ArgumentCaptor<HttpServletResponse> responseArgumentCaptor = ArgumentCaptor
|
||||
.forClass(HttpServletResponse.class);
|
||||
|
||||
filterUnderTest.doFilterInternal(servletRequestMock, servletResponseMock, filterChainMock);
|
||||
|
||||
// the servlet response must be the same mock!
|
||||
verify(filterChainMock, times(1)).doFilter(Mockito.eq(servletRequestMock), responseArgumentCaptor.capture());
|
||||
assertThat(mockingDetails(responseArgumentCaptor.getValue()).isMock()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.json.model;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Feature("Unit Tests - Management API")
|
||||
@Story("Error Handling")
|
||||
public class ExceptionInfoTest {
|
||||
|
||||
@Test
|
||||
@Description("Ensures that setters and getters match on teh payload.")
|
||||
public void setterAndGetterOnExceptionInfo() {
|
||||
final String knownExceptionClass = "hawkbit.test.exception.Class";
|
||||
final String knownErrorCode = "hawkbit.error.code.Known";
|
||||
final String knownMessage = "a known message";
|
||||
final Map<String, Object> knownInfo = new HashMap<>();
|
||||
knownInfo.put("param1", "1");
|
||||
knownInfo.put("param2", 2);
|
||||
|
||||
final ExceptionInfo underTest = new ExceptionInfo();
|
||||
underTest.setErrorCode(knownErrorCode);
|
||||
underTest.setExceptionClass(knownExceptionClass);
|
||||
underTest.setMessage(knownMessage);
|
||||
underTest.setInfo(knownInfo);
|
||||
|
||||
assertThat(underTest.getErrorCode()).as("The error code should match with the known error code in the test")
|
||||
.isEqualTo(knownErrorCode);
|
||||
assertThat(underTest.getExceptionClass())
|
||||
.as("The exception class should match with the known error code in the test")
|
||||
.isEqualTo(knownExceptionClass);
|
||||
assertThat(underTest.getMessage()).as("The message should match with the known error code in the test")
|
||||
.isEqualTo(knownMessage);
|
||||
assertThat(underTest.getInfo()).as("The parameters should match with the known error code in the test")
|
||||
.isEqualTo(knownInfo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
@Feature("Component Tests - Management API")
|
||||
@Story("File streaming")
|
||||
class FileStreamingUtilTest {
|
||||
|
||||
private final static String CONTENT = "This is some very long string which is intended to test";
|
||||
private final static byte[] CONTENT_BYTES = CONTENT.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final DbArtifact TEST_ARTIFACT = new DbArtifact() {
|
||||
|
||||
@Override
|
||||
public String getArtifactId() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbArtifactHash getHashes() {
|
||||
return new DbArtifactHash("sha1-111", "md5-123", "sha256-123");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return CONTENT_BYTES.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getFileInputStream() {
|
||||
return new ByteArrayInputStream(CONTENT_BYTES);
|
||||
}
|
||||
};
|
||||
|
||||
@Test
|
||||
void shouldProcessRangeHeaderForMultipartRequests() throws IOException {
|
||||
final HttpServletResponse servletResponse = Mockito.mock(HttpServletResponse.class);
|
||||
final ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
|
||||
Mockito.when(servletResponse.getOutputStream()).thenReturn(outputStream);
|
||||
final HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
|
||||
Mockito.when(servletRequest.getHeader("Range")).thenReturn("bytes=0-10,9-15,16-");
|
||||
long lastModified = System.currentTimeMillis();
|
||||
|
||||
final ResponseEntity<InputStream> responseEntity = FileStreamingUtil.writeFileResponse(TEST_ARTIFACT,
|
||||
"test.file", lastModified, servletResponse, servletRequest, null);
|
||||
|
||||
assertThat(responseEntity).isNotNull();
|
||||
verify(servletResponse).setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
|
||||
final ArgumentCaptor<String> stringCaptor = ArgumentCaptor.forClass(String.class);
|
||||
final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
|
||||
|
||||
verify(outputStream).print(stringCaptor.capture());
|
||||
assertThat(stringCaptor.getValue()).contains("--THIS_STRING_SEPARATES_MULTIPART--");
|
||||
verify(outputStream, times(3)).write(any(), anyInt(), lenCaptor.capture());
|
||||
assertThat(lenCaptor.getAllValues()).containsExactly(11, 7, 39); // Range lengths
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateRangeHeaderForMultipartRequests() throws IOException {
|
||||
long lastModified = System.currentTimeMillis();
|
||||
final HttpServletResponse servletResponse = Mockito.mock(HttpServletResponse.class);
|
||||
final ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(servletResponse.getOutputStream()).thenReturn(outputStream);
|
||||
|
||||
final HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
|
||||
Mockito.when(servletRequest.getHeader("Range")).thenReturn("bytes=0-10***,9-15,16-");
|
||||
|
||||
final ResponseEntity<InputStream> responseEntity = FileStreamingUtil.writeFileResponse(TEST_ARTIFACT,
|
||||
"test.file", lastModified, servletResponse, servletRequest, null);
|
||||
|
||||
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
verify(outputStream, times(0)).print(anyString());
|
||||
verify(outputStream, times(0)).write(any(), anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||
import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder;
|
||||
import org.eclipse.hawkbit.repository.model.RolloutGroupConditions;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
import org.eclipse.hawkbit.repository.model.Tag;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetType;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Builder class for building certain json strings.
|
||||
*/
|
||||
public abstract class JsonBuilder {
|
||||
|
||||
public static String ids(final Collection<Long> ids) throws JSONException {
|
||||
final JSONArray list = new JSONArray();
|
||||
for (final Long smID : ids) {
|
||||
list.put(new JSONObject().put("id", smID));
|
||||
}
|
||||
|
||||
return list.toString();
|
||||
}
|
||||
|
||||
public static <T> String toArray(final Collection<T> ids) throws JSONException {
|
||||
final JSONArray list = new JSONArray();
|
||||
for (final T smID : ids) {
|
||||
list.put(smID);
|
||||
}
|
||||
return list.toString();
|
||||
}
|
||||
|
||||
public static String tags(final List<Tag> tags) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final Tag tag : tags) {
|
||||
createTagLine(builder, tag);
|
||||
|
||||
if (++i < tags.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String tag(final Tag tag) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
createTagLine(builder, tag);
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String softwareModules(final List<SoftwareModule> modules) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final SoftwareModule module : modules) {
|
||||
builder.append(new JSONObject().put("name", module.getName()).put("description", module.getDescription())
|
||||
.put("type", module.getType().getKey()).put("id", Long.MAX_VALUE).put("vendor", module.getVendor())
|
||||
.put("version", module.getVersion()).put("createdAt", "0").put("updatedAt", "0")
|
||||
.put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh")
|
||||
.put("encrypted", module.isEncrypted()).toString());
|
||||
|
||||
if (++i < modules.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String softwareModulesCreatableFieldsOnly(final List<SoftwareModule> modules) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final SoftwareModule module : modules) {
|
||||
builder.append(new JSONObject().put("name", module.getName()).put("description", module.getDescription())
|
||||
.put("type", module.getType().getKey()).put("vendor", module.getVendor())
|
||||
.put("version", module.getVersion()).toString());
|
||||
|
||||
if (++i < modules.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String softwareModuleUpdatableFieldsOnly(final SoftwareModule module) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append(new JSONObject().put("description", module.getDescription()).put("vendor", module.getVendor())
|
||||
.toString());
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String softwareModuleTypes(final List<SoftwareModuleType> types) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final SoftwareModuleType module : types) {
|
||||
builder.append(new JSONObject().put("name", module.getName()).put("description", module.getDescription())
|
||||
.put("colour", module.getColour()).put("id", Long.MAX_VALUE).put("key", module.getKey())
|
||||
.put("maxAssignments", module.getMaxAssignments()).put("createdAt", "0").put("updatedAt", "0")
|
||||
.put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh").toString());
|
||||
|
||||
if (++i < types.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String softwareModuleTypesCreatableFieldsOnly(final List<SoftwareModuleType> types)
|
||||
throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final SoftwareModuleType module : types) {
|
||||
builder.append(new JSONObject().put("name", module.getName()).put("description", module.getDescription())
|
||||
.put("colour", module.getColour()).put("key", module.getKey())
|
||||
.put("maxAssignments", module.getMaxAssignments()).toString());
|
||||
|
||||
if (++i < types.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an invalid request body with missing result for feedback message.
|
||||
*
|
||||
* @param id id of the action
|
||||
* @param execution the execution
|
||||
* @param message the message
|
||||
* @return a invalid request body
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static String missingResultInFeedback(final String id, final String execution, final String message)
|
||||
throws JSONException {
|
||||
return new JSONObject().put("id", id).put("time", "20140511T121314")
|
||||
.put("status",
|
||||
new JSONObject().put("execution", execution).put("details", new JSONArray().put(message)))
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an invalid request body with missing finished result for feedback
|
||||
* message.
|
||||
*
|
||||
* @param id id of the action
|
||||
* @param execution the execution
|
||||
* @param message the message
|
||||
* @return a invalid request body
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static String missingFinishedResultInFeedback(final String id, final String execution, final String message)
|
||||
throws JSONException {
|
||||
return new JSONObject().put("id", id).put("time", "20140511T121314")
|
||||
.put("status", new JSONObject().put("execution", execution).put("result", new JSONObject())
|
||||
.put("details", new JSONArray().put(message)))
|
||||
.toString();
|
||||
}
|
||||
|
||||
public static String distributionSetTypes(final List<DistributionSetType> types) throws JSONException {
|
||||
final JSONArray result = new JSONArray();
|
||||
|
||||
for (final DistributionSetType type : types) {
|
||||
|
||||
final JSONArray osmTypes = new JSONArray();
|
||||
type.getOptionalModuleTypes().forEach(smt -> {
|
||||
try {
|
||||
osmTypes.put(new JSONObject().put("id", smt.getId()));
|
||||
} catch (final JSONException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
final JSONArray msmTypes = new JSONArray();
|
||||
type.getMandatoryModuleTypes().forEach(smt -> {
|
||||
try {
|
||||
msmTypes.put(new JSONObject().put("id", smt.getId()));
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
result.put(new JSONObject().put("name", type.getName()).put("description", type.getDescription())
|
||||
.put("colour", type.getColour()).put("id", Long.MAX_VALUE).put("key", type.getKey())
|
||||
.put("createdAt", "0").put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh")
|
||||
.put("optionalmodules", osmTypes).put("mandatorymodules", msmTypes)
|
||||
.put("updatedBy", "fghdfkjghdfkjh"));
|
||||
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String distributionSetTypesCreateValidFieldsOnly(final List<DistributionSetType> types) {
|
||||
final JSONArray result = new JSONArray();
|
||||
for (final DistributionSetType module : types) {
|
||||
try {
|
||||
final JSONArray osmTypes = new JSONArray();
|
||||
module.getOptionalModuleTypes().forEach(smt -> {
|
||||
try {
|
||||
osmTypes.put(new JSONObject().put("id", smt.getId()));
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
final JSONArray msmTypes = new JSONArray();
|
||||
module.getMandatoryModuleTypes().forEach(smt -> {
|
||||
try {
|
||||
msmTypes.put(new JSONObject().put("id", smt.getId()));
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
result.put(new JSONObject().put("name", module.getName()).put("description", module.getDescription())
|
||||
.put("colour", module.getColour()).put("key", module.getKey()).put("optionalmodules", osmTypes)
|
||||
.put("mandatorymodules", msmTypes));
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String distributionSets(final List<DistributionSet> sets) throws JSONException {
|
||||
final JSONArray setsJson = new JSONArray();
|
||||
|
||||
sets.forEach(set -> {
|
||||
try {
|
||||
setsJson.put(distributionSet(set));
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
return setsJson.toString();
|
||||
|
||||
}
|
||||
|
||||
public static String distributionSetsCreateValidFieldsOnly(final List<DistributionSet> sets) throws JSONException {
|
||||
final JSONArray result = new JSONArray();
|
||||
|
||||
for (final DistributionSet set : sets) {
|
||||
result.put(distributionSetCreateValidFieldsOnly(set));
|
||||
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
|
||||
}
|
||||
|
||||
public static JSONObject distributionSetCreateValidFieldsOnly(final DistributionSet set) throws JSONException {
|
||||
|
||||
final List<JSONObject> modules = set.getModules().stream().map(module -> {
|
||||
try {
|
||||
return new JSONObject().put("id", module.getId());
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return new JSONObject().put("name", set.getName()).put("description", set.getDescription())
|
||||
.put("type", set.getType() == null ? null : set.getType().getKey()).put("version", set.getVersion())
|
||||
.put("requiredMigrationStep", set.isRequiredMigrationStep()).put("modules", new JSONArray(modules));
|
||||
|
||||
}
|
||||
|
||||
public static String distributionSetUpdateValidFieldsOnly(final DistributionSet set) throws JSONException {
|
||||
|
||||
set.getModules().stream().map(module -> {
|
||||
try {
|
||||
return new JSONObject().put("id", module.getId());
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return new JSONObject().put("name", set.getName()).put("description", set.getDescription())
|
||||
.put("version", set.getVersion()).put("requiredMigrationStep", set.isRequiredMigrationStep())
|
||||
.toString();
|
||||
|
||||
}
|
||||
|
||||
public static JSONObject distributionSet(final DistributionSet set) throws JSONException {
|
||||
|
||||
final List<JSONObject> modules = set.getModules().stream().map(module -> {
|
||||
try {
|
||||
return new JSONObject().put("id", module.getId());
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return new JSONObject().put("name", set.getName()).put("description", set.getDescription())
|
||||
.put("type", set.getType() == null ? null : set.getType().getKey()).put("id", Long.MAX_VALUE)
|
||||
.put("version", set.getVersion()).put("createdAt", "0").put("updatedAt", "0")
|
||||
.put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh")
|
||||
.put("requiredMigrationStep", set.isRequiredMigrationStep()).put("modules", new JSONArray(modules));
|
||||
|
||||
}
|
||||
|
||||
public static String targets(final List<Target> targets, final boolean withToken) throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final Target target : targets) {
|
||||
final String address = target.getAddress() != null ? target.getAddress().toString() : null;
|
||||
final String targetType = target.getTargetType() != null ? target.getTargetType().getId().toString() : null;
|
||||
final String token = withToken ? target.getSecurityToken() : null;
|
||||
|
||||
builder.append(new JSONObject().put("controllerId", target.getControllerId())
|
||||
.put("description", target.getDescription()).put("name", target.getName()).put("createdAt", "0")
|
||||
.put("updatedAt", "0").put("createdBy", "systemtest").put("updatedBy", "systemtest")
|
||||
.put("address", address).put("securityToken", token).put("targetType", targetType).toString());
|
||||
|
||||
if (++i < targets.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String targets(final List<Target> targets, final boolean withToken, final long targetTypeId)
|
||||
throws JSONException {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("[");
|
||||
int i = 0;
|
||||
for (final Target target : targets) {
|
||||
final String address = target.getAddress() != null ? target.getAddress().toString() : null;
|
||||
final String type = target.getTargetType() != null ? target.getTargetType().getId().toString() : null;
|
||||
final String token = withToken ? target.getSecurityToken() : null;
|
||||
|
||||
builder.append(new JSONObject().put("controllerId", target.getControllerId())
|
||||
.put("description", target.getDescription()).put("name", target.getName()).put("createdAt", "0")
|
||||
.put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh")
|
||||
.put("address", address).put("securityToken", token).put("targetType", targetTypeId).toString());
|
||||
|
||||
if (++i < targets.size()) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("]");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String targetTypes(final List<TargetType> types) throws JSONException {
|
||||
final JSONArray result = new JSONArray();
|
||||
|
||||
for (final TargetType type : types) {
|
||||
|
||||
final JSONArray dsTypes = new JSONArray();
|
||||
type.getCompatibleDistributionSetTypes().forEach(dsType -> {
|
||||
try {
|
||||
dsTypes.put(new JSONObject().put("id", dsType.getId()));
|
||||
} catch (final JSONException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
result.put(new JSONObject().put("name", type.getName()).put("description", type.getDescription())
|
||||
.put("id", Long.MAX_VALUE).put("colour", type.getColour()).put("createdAt", "0")
|
||||
.put("updatedAt", "0").put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh")
|
||||
.put("distributionsets", dsTypes));
|
||||
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String targetTypesCreatableFieldsOnly(final List<TargetType> types) throws JSONException {
|
||||
final JSONArray result = new JSONArray();
|
||||
|
||||
for (final TargetType type : types) {
|
||||
|
||||
final JSONArray dsTypes = new JSONArray();
|
||||
type.getCompatibleDistributionSetTypes().forEach(dsType -> {
|
||||
try {
|
||||
dsTypes.put(new JSONObject().put("id", dsType.getId()));
|
||||
} catch (final JSONException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
final JSONObject json = new JSONObject().put("name", type.getName())
|
||||
.put("description", type.getDescription()).put("colour", type.getColour());
|
||||
|
||||
if (dsTypes.length() != 0) {
|
||||
json.put("compatibledistributionsettypes", dsTypes);
|
||||
}
|
||||
|
||||
result.put(json);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String rollout(final String name, final String description, final int groupSize,
|
||||
final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions) {
|
||||
return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, null, null,
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
public static String rollout(final String name, final String description, final Integer groupSize,
|
||||
final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions,
|
||||
final String type) {
|
||||
return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, null, type,
|
||||
null, null, null, null);
|
||||
}
|
||||
|
||||
public static String rolloutWithGroups(final String name, final String description, final Integer groupSize,
|
||||
final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions,
|
||||
final List<RolloutGroup> groups) {
|
||||
return rolloutWithGroups(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, groups,
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
public static String rolloutWithGroups(final String name, final String description, final Integer groupSize,
|
||||
final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions,
|
||||
final List<RolloutGroup> groups, final String type, final Integer weight,
|
||||
final Boolean confirmationRequired) {
|
||||
final List<String> rolloutGroupsJson = groups.stream().map(JsonBuilder::rolloutGroup)
|
||||
.collect(Collectors.toList());
|
||||
return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions,
|
||||
rolloutGroupsJson, type, weight, System.currentTimeMillis(), null, confirmationRequired);
|
||||
}
|
||||
|
||||
public static String rollout(final String name, final String description, final Integer groupSize,
|
||||
final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions,
|
||||
final List<String> groupJsonList, final String type, final Integer weight, final Long startAt, final Long forceTime,
|
||||
final Boolean confirmationRequired) {
|
||||
return rollout(name, description, groupSize, distributionSetId, targetFilterQuery, conditions, groupJsonList, type,
|
||||
weight, startAt, forceTime, confirmationRequired, false, null, 0);
|
||||
}
|
||||
|
||||
public static String rollout(final String name, final String description, final Integer groupSize,
|
||||
final long distributionSetId, final String targetFilterQuery, final RolloutGroupConditions conditions,
|
||||
final List<String> groupJsonList, final String type, final Integer weight, final Long startAt, final Long forceTime,
|
||||
final Boolean confirmationRequired, final boolean isDynamic, final String dynamicGroupSuffix, final int dynamicGroupTargetsCount) {
|
||||
final JSONObject json = new JSONObject();
|
||||
|
||||
try {
|
||||
json.put("name", name);
|
||||
json.put("description", description);
|
||||
json.put("amountGroups", groupSize);
|
||||
json.put("distributionSetId", distributionSetId);
|
||||
json.put("targetFilterQuery", targetFilterQuery);
|
||||
|
||||
if (type != null) {
|
||||
json.put("type", type);
|
||||
}
|
||||
|
||||
if (weight != null) {
|
||||
json.put("weight", weight);
|
||||
}
|
||||
|
||||
if (startAt != null) {
|
||||
json.put("startAt", startAt);
|
||||
}
|
||||
|
||||
if (forceTime != null) {
|
||||
json.put("forcetime", forceTime);
|
||||
}
|
||||
|
||||
if (confirmationRequired != null) {
|
||||
json.put("confirmationRequired", confirmationRequired);
|
||||
}
|
||||
|
||||
if (conditions != null) {
|
||||
final JSONObject successCondition = new JSONObject();
|
||||
|
||||
json.put("successCondition", successCondition);
|
||||
|
||||
successCondition.put("condition", conditions.getSuccessCondition().toString());
|
||||
successCondition.put("expression", conditions.getSuccessConditionExp());
|
||||
|
||||
final JSONObject successAction = new JSONObject();
|
||||
json.put("successAction", successAction);
|
||||
successAction.put("action", conditions.getSuccessAction().toString());
|
||||
successAction.put("expression", conditions.getSuccessActionExp());
|
||||
|
||||
final JSONObject errorCondition = new JSONObject();
|
||||
json.put("errorCondition", errorCondition);
|
||||
errorCondition.put("condition", conditions.getErrorCondition().toString());
|
||||
errorCondition.put("expression", conditions.getErrorConditionExp());
|
||||
|
||||
final JSONObject errorAction = new JSONObject();
|
||||
json.put("errorAction", errorAction);
|
||||
errorAction.put("action", conditions.getErrorAction().toString());
|
||||
errorAction.put("expression", conditions.getErrorActionExp());
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
json.put("dynamic", isDynamic);
|
||||
|
||||
final JSONObject dynamicGroupTemplate = new JSONObject();
|
||||
json.put("dynamicGroupTemplate", dynamicGroupTemplate);
|
||||
dynamicGroupTemplate.put("nameSuffix",
|
||||
(dynamicGroupSuffix == null || dynamicGroupSuffix.isEmpty()) ? "-dynamic" : dynamicGroupSuffix);
|
||||
dynamicGroupTemplate.put("targetCount", dynamicGroupTargetsCount < 0 ? 1 : dynamicGroupTargetsCount);
|
||||
}
|
||||
|
||||
if (!CollectionUtils.isEmpty(groupJsonList)) {
|
||||
final JSONArray jsonGroups = new JSONArray();
|
||||
for (final String groupJson : groupJsonList) {
|
||||
jsonGroups.put(new JSONObject(groupJson));
|
||||
}
|
||||
json.put("groups", jsonGroups);
|
||||
}
|
||||
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
public static String rolloutGroup(final RolloutGroup rolloutGroup) {
|
||||
final RolloutGroupConditions conditions = getConditions(rolloutGroup);
|
||||
return rolloutGroup(rolloutGroup.getName(), rolloutGroup.getDescription(), rolloutGroup.getTargetFilterQuery(),
|
||||
rolloutGroup.getTargetPercentage(), rolloutGroup.isConfirmationRequired(), conditions);
|
||||
|
||||
}
|
||||
|
||||
public static String rolloutGroup(final String name, final String description, final String targetFilterQuery,
|
||||
final Float targetPercentage, final Boolean confirmationRequired,
|
||||
final RolloutGroupConditions rolloutGroupConditions) {
|
||||
final JSONObject jsonGroup = new JSONObject();
|
||||
try {
|
||||
jsonGroup.put("name", name);
|
||||
jsonGroup.put("description", description);
|
||||
jsonGroup.put("targetFilterQuery", targetFilterQuery);
|
||||
if (targetPercentage == null) {
|
||||
jsonGroup.put("targetPercentage", 100F);
|
||||
} else {
|
||||
jsonGroup.put("targetPercentage", targetPercentage);
|
||||
}
|
||||
|
||||
if (confirmationRequired != null) {
|
||||
jsonGroup.put("confirmationRequired", confirmationRequired);
|
||||
}
|
||||
|
||||
if (rolloutGroupConditions.getSuccessCondition() != null) {
|
||||
final JSONObject successCondition = new JSONObject();
|
||||
jsonGroup.put("successCondition", successCondition);
|
||||
successCondition.put("condition", rolloutGroupConditions.getSuccessCondition().toString());
|
||||
successCondition.put("expression", rolloutGroupConditions.getSuccessConditionExp());
|
||||
}
|
||||
if (rolloutGroupConditions.getSuccessAction() != null) {
|
||||
final JSONObject successAction = new JSONObject();
|
||||
jsonGroup.put("successAction", successAction);
|
||||
successAction.put("action", rolloutGroupConditions.getSuccessAction().toString());
|
||||
successAction.put("expression", rolloutGroupConditions.getSuccessActionExp());
|
||||
}
|
||||
if (rolloutGroupConditions.getErrorCondition() != null) {
|
||||
final JSONObject errorCondition = new JSONObject();
|
||||
jsonGroup.put("errorCondition", errorCondition);
|
||||
errorCondition.put("condition", rolloutGroupConditions.getErrorCondition().toString());
|
||||
errorCondition.put("expression", rolloutGroupConditions.getErrorConditionExp());
|
||||
}
|
||||
if (rolloutGroupConditions.getErrorAction() != null) {
|
||||
final JSONObject errorAction = new JSONObject();
|
||||
jsonGroup.put("errorAction", errorAction);
|
||||
errorAction.put("action", rolloutGroupConditions.getErrorAction().toString());
|
||||
errorAction.put("expression", rolloutGroupConditions.getErrorActionExp());
|
||||
}
|
||||
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
fail("Cannot parse JSON for rollout group.");
|
||||
}
|
||||
|
||||
return jsonGroup.toString();
|
||||
}
|
||||
|
||||
public static String cancelActionFeedback(final String id, final String execution) throws JSONException {
|
||||
return cancelActionFeedback(id, execution, null, RandomStringUtils.randomAlphanumeric(1000));
|
||||
|
||||
}
|
||||
|
||||
public static String cancelActionFeedback(final String id, final String execution, final String message)
|
||||
throws JSONException {
|
||||
return cancelActionFeedback(id, execution, null, message);
|
||||
}
|
||||
|
||||
public static String cancelActionFeedback(final String id, final String execution, final Integer code,
|
||||
final String message) throws JSONException {
|
||||
final JSONObject statusJson = new JSONObject().put("execution", execution)
|
||||
.put("result", new JSONObject().put("finished", "success"))
|
||||
.put("details", new JSONArray().put(message));
|
||||
|
||||
if (code != null) {
|
||||
statusJson.put("code", code);
|
||||
}
|
||||
|
||||
return new JSONObject().put("id", id).put("status", statusJson).toString();
|
||||
}
|
||||
|
||||
public static JSONObject configData(final Map<String, String> attributes) throws JSONException {
|
||||
return configData(attributes, null);
|
||||
}
|
||||
|
||||
public static JSONObject configData(final Map<String, String> attributes, final String mode) throws JSONException {
|
||||
|
||||
final JSONObject data = new JSONObject();
|
||||
attributes.entrySet().forEach(entry -> {
|
||||
try {
|
||||
data.put(entry.getKey(), entry.getValue());
|
||||
} catch (final JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
final JSONObject json = new JSONObject().put("data", data);
|
||||
if (mode != null) {
|
||||
json.put("mode", mode);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private static void createTagLine(final StringBuilder builder, final Tag tag) throws JSONException {
|
||||
builder.append(new JSONObject().put("name", tag.getName()).put("description", tag.getDescription())
|
||||
.put("colour", tag.getColour()).put("createdAt", "0").put("updatedAt", "0")
|
||||
.put("createdBy", "fghdfkjghdfkjh").put("updatedBy", "fghdfkjghdfkjh").toString());
|
||||
}
|
||||
|
||||
private static RolloutGroupConditions getConditions(final RolloutGroup rolloutGroup) {
|
||||
return new RolloutGroupConditionBuilder()
|
||||
.errorCondition(rolloutGroup.getErrorCondition(), rolloutGroup.getErrorConditionExp())
|
||||
.errorAction(rolloutGroup.getErrorAction(), rolloutGroup.getErrorActionExp())
|
||||
.successAction(rolloutGroup.getSuccessAction(), rolloutGroup.getSuccessActionExp())
|
||||
.successCondition(rolloutGroup.getSuccessCondition(), rolloutGroup.getSuccessConditionExp()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultHandler;
|
||||
import org.springframework.test.web.servlet.result.PrintingResultHandler;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public abstract class MockMvcResultPrinter {
|
||||
|
||||
/**
|
||||
* Print {@link MvcResult} details to logger.
|
||||
*/
|
||||
public static ResultHandler print() {
|
||||
return new ConsolePrintingResultHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link PrintingResultHandler} that writes to logger
|
||||
*/
|
||||
private static class ConsolePrintingResultHandler extends PrintingResultHandler {
|
||||
|
||||
public ConsolePrintingResultHandler() {
|
||||
super(new ResultValuePrinter() {
|
||||
|
||||
@Override
|
||||
public void printHeading(final String heading) {
|
||||
log.debug(String.format("%20s:", heading));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printValue(final String label, final Object v) {
|
||||
Object value = v;
|
||||
|
||||
if (value != null && value.getClass().isArray()) {
|
||||
value = CollectionUtils.arrayToList(value);
|
||||
}
|
||||
log.debug(String.format("%20s = %s", label, value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
/**
|
||||
* @param <T>
|
||||
*/
|
||||
public interface SuccessCondition<T> {
|
||||
|
||||
/**
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
boolean success(final T result);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user