Move Mgmt artifacts into hawkbit-mgmt (#2003)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-11 15:57:56 +02:00
committed by GitHub
parent 05d8d6cc7e
commit baab2fcf95
200 changed files with 167 additions and 114 deletions

95
hawkbit-rest-core/pom.xml Normal file
View 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>

View File

@@ -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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
});
}
}
}

View File

@@ -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);
}